Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 96 additions & 16 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Quickstart

This walkthrough builds a small Maven application that reads and writes a
PostgreSQL table through sqlcj-generated Java. Every file is listed in full, so
This walkthrough builds a small Maven application that reads and writes two
PostgreSQL tables through sqlcj-generated Java. Every file is listed in full, so
the steps can be followed from an empty directory.

The finished project uses only the packaged sqlcj artifacts: the executable CLI
Expand Down Expand Up @@ -169,6 +169,13 @@ CREATE TABLE authors
bio TEXT,
created_at TIMESTAMP
);

CREATE TABLE books
(
id BIGSERIAL PRIMARY KEY,
author_id BIGINT NOT NULL REFERENCES authors (id),
title VARCHAR(255) NOT NULL
);
```

The accepted column types and `CREATE TABLE` constructs are listed in
Expand Down Expand Up @@ -206,16 +213,47 @@ WHERE id = $1;
DELETE
FROM authors
WHERE id = $1;

-- name: SearchAuthors :many
SELECT *
FROM authors
WHERE name ILIKE $1
ORDER BY id;

-- name: CountAuthors :one
SELECT COUNT(*) AS total
FROM authors;

-- name: ListAuthorPage :many
SELECT *
FROM authors
ORDER BY id
LIMIT $1 OFFSET $2;

-- name: CreateBook :exec
INSERT INTO books (author_id, title)
VALUES ($1, $2);

-- name: ListAuthorBooks :many
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON b.author_id = a.id
ORDER BY a.id, b.id;
```

All six queries become methods of the one generated `AuthorRepository`:
`createAuthor`, `getAuthor`, `findAuthor`, `listAuthors`, `updateAuthorBio`, and
`deleteAuthor`. `CreateAuthor`, `GetAuthor`, `FindAuthor`, and `ListAuthors`
each return one complete `authors` row, so all four share the nested record
`AuthorRepository.AuthorsRow`, generated once from the schema's column order. A
query with its own result shape, such as a partial projection or a `RETURNING`
column list, generates a nested `AuthorRepository.<QueryName>Result` record
instead.
All eleven queries become methods of the one generated `AuthorRepository`:
`createAuthor`, `getAuthor`, `findAuthor`, `listAuthors`, `updateAuthorBio`,
`deleteAuthor`, `searchAuthors`, `countAuthors`, `listAuthorPage`, `createBook`,
and `listAuthorBooks`. `CreateAuthor`, `GetAuthor`, `FindAuthor`, `ListAuthors`,
`SearchAuthors`, and `ListAuthorPage` each return one complete `authors` row, so
all six share the nested record `AuthorRepository.AuthorsRow`, generated once
from the schema's column order. A query with its own result shape, such as a
partial projection or a `RETURNING` column list, generates a nested
`AuthorRepository.<QueryName>Result` record instead: `CountAuthors` generates
`CountAuthorsResult` with the single non-null `Long` component `total`, and
`ListAuthorBooks` generates `ListAuthorBooksResult` with the components `name`
and `title`, where `title` is `null` for an author that the left-joined `books`
table does not match.

`GetAuthor` and `FindAuthor` read the same row by the same key and differ only
in cardinality: `getAuthor` returns `AuthorsRow` and requires exactly one row,
Expand Down Expand Up @@ -273,6 +311,8 @@ public final class App {
System.out.println("missing row rejected: " + e.getMessage());
}

Long committedId;

try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);

Expand All @@ -284,7 +324,9 @@ public final class App {

connection.commit();

System.out.println("committed: " + authors.getAuthor(committed.id()).bio());
committedId = committed.id();

System.out.println("committed: " + authors.getAuthor(committedId).bio());
}

try (Connection connection = dataSource.getConnection()) {
Expand All @@ -301,6 +343,24 @@ public final class App {
System.out.println("rolled back: " + authors.findAuthor(discarded.id()).isPresent());
}

for (AuthorRepository.AuthorsRow author : authors.searchAuthors("%lovelace%")) {
System.out.println("searched: " + author.id() + " " + author.name());
}

System.out.println("count: " + authors.countAuthors().total());

for (AuthorRepository.AuthorsRow author : authors.listAuthorPage(1, 1)) {
System.out.println("page: " + author.id() + " " + author.name());
}

int bookRows = authors.createBook(committedId, "The Education of a Computer");

System.out.println("created book rows: " + bookRows);

for (AuthorRepository.ListAuthorBooksResult book : authors.listAuthorBooks()) {
System.out.println("book: " + book.name() + " / " + book.title());
}

System.out.println("deleted rows: " + authors.deleteAuthor(created.id()));
}

Expand Down Expand Up @@ -376,6 +436,12 @@ missing row: false
missing row rejected: Query 'GetAuthor' in AuthorRepository returned no row; expected exactly one
committed: Compiler pioneer
rolled back: false
searched: 1 Ada Lovelace
count: 2
page: 2 Grace Hopper
created book rows: 1
book: Ada Lovelace / null
book: Grace Hopper / The Education of a Computer
deleted rows: 1
```

Expand All @@ -393,10 +459,24 @@ That output is the whole MVP contract in one run:
missing, rolled back, or deleted author.
- `ListAuthors` is a `:many` read, and returns an empty list when no row
matches.
- `UpdateAuthorBio` and `DeleteAuthor` are `:exec` writes and return their
affected-row counts. `UpdateAuthorBio` also shows that parameter order and
binding order are different things: `$1` is the first method parameter even
though `$2` occurs first in the SQL text.
- `SearchAuthors` filters with `name ILIKE $1`, so the pattern is a `String`
parameter carrying its own `%` wildcards, and PostgreSQL matches it without
regard to case.
- `CountAuthors` projects an aliased `COUNT(*)` as its only result item, so
`countAuthors()` returns a `CountAuthorsResult` whose `total` is a non-null
`Long`.
- `ListAuthorPage` pages with `LIMIT $1 OFFSET $2`, which generates
`listAuthorPage(Integer limit, Integer offset)`, so asking for one row after
the first returns the second author alone.
- `ListAuthorBooks` selects one column from each side of a `LEFT JOIN`, so it
gets its own `ListAuthorBooksResult` record and reads `title` as `null` for
the author with no book, even though `books.title` is declared `NOT NULL`.
- `UpdateAuthorBio`, `DeleteAuthor`, and `CreateBook` are `:exec` writes and
return their affected-row counts. `UpdateAuthorBio` also shows that parameter
order and binding order are different things: `$1` is the first method
parameter even though `$2` occurs first in the SQL text. `CreateBook` writes
the schema's other table, so the methods of one query group may span every
table of its schema.
- The two `try` blocks run generated operations on a caller-owned `Connection`
with auto-commit disabled. The runtime never closes, commits, rolls back, or
reconfigures that connection, so the application's own `commit` makes both
Expand All @@ -416,7 +496,7 @@ If a query is invalid, `sqlcj generate` prints one diagnostic naming the source,
the query, and the line, and exits with status `1`:

```text
sqlcj: Invalid query 'CountAuthorBios' in /home/dev/my-app/sql/queries.sql at line 31: Unsupported SELECT expression: Function
sqlcj: Invalid query 'CountAuthorBios' in /home/dev/my-app/sql/queries.sql at line 57: Unsupported SELECT expression: Function
```

Every configured source is compiled before any file is written, so a diagnostic
Expand Down
26 changes: 26 additions & 0 deletions examples/maven-postgresql/sql/queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,29 @@ WHERE id = $1;
DELETE
FROM authors
WHERE id = $1;

-- name: SearchAuthors :many
SELECT *
FROM authors
WHERE name ILIKE $1
ORDER BY id;

-- name: CountAuthors :one
SELECT COUNT(*) AS total
FROM authors;

-- name: ListAuthorPage :many
SELECT *
FROM authors
ORDER BY id
LIMIT $1 OFFSET $2;

-- name: CreateBook :exec
INSERT INTO books (author_id, title)
VALUES ($1, $2);

-- name: ListAuthorBooks :many
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON b.author_id = a.id
ORDER BY a.id, b.id;
7 changes: 7 additions & 0 deletions examples/maven-postgresql/sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ CREATE TABLE authors
bio TEXT,
created_at TIMESTAMP
);

CREATE TABLE books
(
id BIGSERIAL PRIMARY KEY,
author_id BIGINT NOT NULL REFERENCES authors (id),
title VARCHAR(255) NOT NULL
);
64 changes: 57 additions & 7 deletions examples/maven-postgresql/src/main/java/com/example/app/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@
/**
* Runs the documented sqlcj workflow against PostgreSQL through generated
* code: create, read, optional read, list, update, a missing-row read, a
* committed transaction, a rolled back transaction, and delete.
* committed transaction, a rolled back transaction, a case-insensitive search,
* a count, a page, a book insert, a left-joined projection, and delete.
*
* <p>All six named queries are methods of one generated
* <p>All eleven named queries are methods of one generated
* {@link AuthorRepository}. The repository is constructed once per execution
* context: once from the {@code DataSource}-backed executor, and once more per
* transaction from a caller-owned connection.
*
* <p>Create, read, optional read, and list each return one complete
* {@code authors} row, so all four share the repository's single
* {@link AuthorRepository.AuthorsRow} record.
* <p>Create, read, optional read, list, search, and page each return one
* complete {@code authors} row, so all six share the repository's single
* {@link AuthorRepository.AuthorsRow} record. A query with its own result shape
* generates its own record: {@link AuthorRepository.CountAuthorsResult} carries
* the non-null {@code Long} count, and
* {@link AuthorRepository.ListAuthorBooksResult} carries an author name beside
* the title of the left-joined {@code books} row, which is {@code null} for an
* author without a book.
*
* <p>Row absence is expressed by the {@code :optional} {@code FindAuthor}
* query, which returns an empty {@link Optional}, while the {@code :one}
Expand All @@ -35,8 +41,8 @@
* <p>Every step is checked, so the process exits non-zero as soon as one
* generated operation returns an unexpected result.
*
* <p>The verification harness applies {@code sql/schema.sql} to an empty
* {@code authors} table before this application runs.
* <p>The verification harness applies {@code sql/schema.sql} to empty
* {@code authors} and {@code books} tables before this application runs.
*/
public final class App {

Expand Down Expand Up @@ -124,6 +130,50 @@ public static void main(String[] args) throws SQLException {

System.out.println("rolled back: empty");

List<AuthorRepository.AuthorsRow> searched = authors.searchAuthors("%lovelace%");

checkEquals(1, searched.size(), "SearchAuthors row count");
checkEquals(created.id(), searched.get(0).id(), "SearchAuthors id");
checkEquals("Ada Lovelace", searched.get(0).name(), "SearchAuthors name");

System.out.println("searched: " + searched.get(0).id() + " " + searched.get(0).name());

AuthorRepository.CountAuthorsResult count = authors.countAuthors();

check(count != null, "CountAuthors returned no row");
checkEquals(2L, count.total(), "CountAuthors total");

System.out.println("count: " + count.total());

List<AuthorRepository.AuthorsRow> page = authors.listAuthorPage(1, 1);

checkEquals(1, page.size(), "ListAuthorPage row count");
checkEquals(committedId, page.get(0).id(), "ListAuthorPage id");
checkEquals("Grace Hopper", page.get(0).name(), "ListAuthorPage name");

System.out.println("page: " + page.get(0).id() + " " + page.get(0).name());

String bookTitle = "The Education of a Computer";

int bookRows = authors.createBook(committedId, bookTitle);

checkEquals(1, bookRows, "CreateBook affected rows");

System.out.println("created book rows: " + bookRows);

List<AuthorRepository.ListAuthorBooksResult> books = authors.listAuthorBooks();

checkEquals(2, books.size(), "ListAuthorBooks row count");
checkEquals("Ada Lovelace", books.get(0).name(), "ListAuthorBooks unmatched author name");
checkEquals(null, books.get(0).title(), "ListAuthorBooks unmatched title");
checkEquals("Grace Hopper", books.get(1).name(), "ListAuthorBooks matched author name");
checkEquals(bookTitle, books.get(1).title(), "ListAuthorBooks matched title");

System.out.println(
"author books: " + books.get(0).name() + " / " + books.get(0).title()
+ ", " + books.get(1).name() + " / " + books.get(1).title()
);

int deletedRows = authors.deleteAuthor(created.id());

checkEquals(1, deletedRows, "DeleteAuthor affected rows");
Expand Down
5 changes: 3 additions & 2 deletions scripts/verify-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ generated_count=$(find "${generated_root}" -type f -name '*.java' | wc -l)
[ "${generated_count}" -eq 1 ] \
|| fail "expected 1 generated source but found ${generated_count}"

for method in createAuthor getAuthor findAuthor listAuthors updateAuthorBio deleteAuthor; do
for method in createAuthor getAuthor findAuthor listAuthors updateAuthorBio deleteAuthor \
searchAuthors countAuthors listAuthorPage createBook listAuthorBooks; do
grep -q " ${method}(" "${generated_package}/AuthorRepository.java" \
|| fail "the generated repository is missing the ${method} method"
done
Expand Down Expand Up @@ -154,7 +155,7 @@ PGPASSWORD="${db_password}" psql \
--set=ON_ERROR_STOP=1 \
--quiet \
--username "${db_user}" \
--command 'DROP TABLE IF EXISTS authors;' \
--command 'DROP TABLE IF EXISTS books, authors;' \
--file "${sample_dir}/sql/schema.sql" \
"${jdbc_url#jdbc:}"

Expand Down
Loading