From c2f1dd11ec5f57f09852b1d290ae214a24e04772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20=C3=87engel?= Date: Fri, 25 Sep 2026 21:21:25 +0300 Subject: [PATCH 1/2] docs: demonstrate search, count, paging, and a left join in the sample The external sample gains a books table and five queries, and its application checks a case-insensitive search, a scalar count, a page, a book insert, and a left join that reads a null title for an unmatched author. Release verification now resets both tables and checks the five new generated repository methods. --- examples/maven-postgresql/sql/queries.sql | 26 ++++++++ examples/maven-postgresql/sql/schema.sql | 7 ++ .../src/main/java/com/example/app/App.java | 64 +++++++++++++++++-- scripts/verify-release.sh | 5 +- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/examples/maven-postgresql/sql/queries.sql b/examples/maven-postgresql/sql/queries.sql index 3f1ee3e..b6a0bc0 100644 --- a/examples/maven-postgresql/sql/queries.sql +++ b/examples/maven-postgresql/sql/queries.sql @@ -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; diff --git a/examples/maven-postgresql/sql/schema.sql b/examples/maven-postgresql/sql/schema.sql index e3d3e96..3ba0835 100644 --- a/examples/maven-postgresql/sql/schema.sql +++ b/examples/maven-postgresql/sql/schema.sql @@ -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 +); diff --git a/examples/maven-postgresql/src/main/java/com/example/app/App.java b/examples/maven-postgresql/src/main/java/com/example/app/App.java index 4dd4048..c12b4b1 100644 --- a/examples/maven-postgresql/src/main/java/com/example/app/App.java +++ b/examples/maven-postgresql/src/main/java/com/example/app/App.java @@ -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. * - *

All six named queries are methods of one generated + *

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. * - *

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. + *

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. * *

Row absence is expressed by the {@code :optional} {@code FindAuthor} * query, which returns an empty {@link Optional}, while the {@code :one} @@ -35,8 +41,8 @@ *

Every step is checked, so the process exits non-zero as soon as one * generated operation returns an unexpected result. * - *

The verification harness applies {@code sql/schema.sql} to an empty - * {@code authors} table before this application runs. + *

The verification harness applies {@code sql/schema.sql} to empty + * {@code authors} and {@code books} tables before this application runs. */ public final class App { @@ -124,6 +130,50 @@ public static void main(String[] args) throws SQLException { System.out.println("rolled back: empty"); + List 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 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 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"); diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index 97828f7..edf6bc0 100755 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -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 @@ -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:}" From 1313a79acf0a557986021b56b57daa62b1fa7bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20=C3=87engel?= Date: Fri, 25 Sep 2026 21:21:31 +0300 Subject: [PATCH 2/2] docs: walk through search, count, paging, and a left join in quickstart --- docs/quickstart.md | 112 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 16 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index f492082..a534f73 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -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 @@ -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 @@ -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.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.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, @@ -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); @@ -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()) { @@ -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())); } @@ -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 ``` @@ -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 @@ -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