11# Quickstart
22
3- This walkthrough builds a small Maven application that reads and writes a
4- PostgreSQL table through sqlcj-generated Java. Every file is listed in full, so
3+ This walkthrough builds a small Maven application that reads and writes two
4+ PostgreSQL tables through sqlcj-generated Java. Every file is listed in full, so
55the steps can be followed from an empty directory.
66
77The finished project uses only the packaged sqlcj artifacts: the executable CLI
@@ -169,6 +169,13 @@ CREATE TABLE authors
169169 bio TEXT,
170170 created_at TIMESTAMP
171171);
172+
173+ CREATE TABLE books
174+ (
175+ id BIGSERIAL PRIMARY KEY,
176+ author_id BIGINT NOT NULL REFERENCES authors (id),
177+ title VARCHAR(255) NOT NULL
178+ );
172179` ` `
173180
174181The accepted column types and `CREATE TABLE` constructs are listed in
@@ -206,16 +213,47 @@ WHERE id = $1;
206213DELETE
207214FROM authors
208215WHERE id = $1;
216+
217+ -- name: SearchAuthors :many
218+ SELECT *
219+ FROM authors
220+ WHERE name ILIKE $1
221+ ORDER BY id;
222+
223+ -- name: CountAuthors :one
224+ SELECT COUNT(*) AS total
225+ FROM authors;
226+
227+ -- name: ListAuthorPage :many
228+ SELECT *
229+ FROM authors
230+ ORDER BY id
231+ LIMIT $1 OFFSET $2;
232+
233+ -- name: CreateBook :exec
234+ INSERT INTO books (author_id, title)
235+ VALUES ($1, $2);
236+
237+ -- name: ListAuthorBooks :many
238+ SELECT a.name, b.title
239+ FROM authors a
240+ LEFT JOIN books b ON b.author_id = a.id
241+ ORDER BY a.id, b.id;
209242` ` `
210243
211- All six queries become methods of the one generated `AuthorRepository` :
212- ` createAuthor` , `getAuthor`, `findAuthor`, `listAuthors`, `updateAuthorBio`, and
213- ` deleteAuthor` . `CreateAuthor`, `GetAuthor`, `FindAuthor`, and `ListAuthors`
214- each return one complete `authors` row, so all four share the nested record
215- ` AuthorRepository.AuthorsRow` , generated once from the schema's column order. A
216- query with its own result shape, such as a partial projection or a `RETURNING`
217- column list, generates a nested `AuthorRepository.<QueryName>Result` record
218- instead.
244+ All eleven queries become methods of the one generated `AuthorRepository` :
245+ ` createAuthor` , `getAuthor`, `findAuthor`, `listAuthors`, `updateAuthorBio`,
246+ ` deleteAuthor` , `searchAuthors`, `countAuthors`, `listAuthorPage`, `createBook`,
247+ and `listAuthorBooks`. `CreateAuthor`, `GetAuthor`, `FindAuthor`, `ListAuthors`,
248+ ` SearchAuthors` , and `ListAuthorPage` each return one complete `authors` row, so
249+ all six share the nested record `AuthorRepository.AuthorsRow`, generated once
250+ from the schema's column order. A query with its own result shape, such as a
251+ partial projection or a `RETURNING` column list, generates a nested
252+ `AuthorRepository.<QueryName>Result` record instead : ` CountAuthors` generates
253+ ` CountAuthorsResult` with the single non-null `Long` component `total`, and
254+ ` ListAuthorBooks` generates `ListAuthorBooksResult` with the components `name`
255+ and `title`, where `title` is `null` for an author that the left-joined `books`
256+ table does not match.
219257
220258` GetAuthor` and `FindAuthor` read the same row by the same key and differ only
221259in cardinality : ` getAuthor` returns `AuthorsRow` and requires exactly one row,
@@ -273,6 +311,8 @@ public final class App {
273311 System.out.println("missing row rejected: " + e.getMessage());
274312 }
275313
314+ Long committedId;
315+
276316 try (Connection connection = dataSource.getConnection()) {
277317 connection.setAutoCommit(false);
278318
@@ -284,7 +324,9 @@ public final class App {
284324
285325 connection.commit();
286326
287- System.out.println("committed: " + authors.getAuthor(committed.id()).bio());
327+ committedId = committed.id();
328+
329+ System.out.println("committed: " + authors.getAuthor(committedId).bio());
288330 }
289331
290332 try (Connection connection = dataSource.getConnection()) {
@@ -301,6 +343,24 @@ public final class App {
301343 System.out.println("rolled back: " + authors.findAuthor(discarded.id()).isPresent());
302344 }
303345
346+ for (AuthorRepository.AuthorsRow author : authors.searchAuthors("%lovelace%")) {
347+ System.out.println("searched: " + author.id() + " " + author.name());
348+ }
349+
350+ System.out.println("count: " + authors.countAuthors().total());
351+
352+ for (AuthorRepository.AuthorsRow author : authors.listAuthorPage(1, 1)) {
353+ System.out.println("page: " + author.id() + " " + author.name());
354+ }
355+
356+ int bookRows = authors.createBook(committedId, "The Education of a Computer");
357+
358+ System.out.println("created book rows: " + bookRows);
359+
360+ for (AuthorRepository.ListAuthorBooksResult book : authors.listAuthorBooks()) {
361+ System.out.println("book: " + book.name() + " / " + book.title());
362+ }
363+
304364 System.out.println("deleted rows: " + authors.deleteAuthor(created.id()));
305365 }
306366
@@ -376,6 +436,12 @@ missing row: false
376436missing row rejected: Query 'GetAuthor' in AuthorRepository returned no row; expected exactly one
377437committed: Compiler pioneer
378438rolled back: false
439+ searched: 1 Ada Lovelace
440+ count: 2
441+ page: 2 Grace Hopper
442+ created book rows: 1
443+ book: Ada Lovelace / null
444+ book: Grace Hopper / The Education of a Computer
379445deleted rows: 1
380446` ` `
381447
@@ -393,10 +459,24 @@ That output is the whole MVP contract in one run:
393459 missing, rolled back, or deleted author.
394460- ` ListAuthors` is a `:many` read, and returns an empty list when no row
395461 matches.
396- - ` UpdateAuthorBio` and `DeleteAuthor` are `:exec` writes and return their
397- affected-row counts. `UpdateAuthorBio` also shows that parameter order and
398- binding order are different things : ` $1` is the first method parameter even
399- though `$2` occurs first in the SQL text.
462+ - ` SearchAuthors` filters with `name ILIKE $1`, so the pattern is a `String`
463+ parameter carrying its own `%` wildcards, and PostgreSQL matches it without
464+ regard to case.
465+ - ` CountAuthors` projects an aliased `COUNT(*)` as its only result item, so
466+ ` countAuthors()` returns a `CountAuthorsResult` whose `total` is a non-null
467+ ` Long` .
468+ - ` ListAuthorPage` pages with `LIMIT $1 OFFSET $2`, which generates
469+ ` listAuthorPage(Integer limit, Integer offset)` , so asking for one row after
470+ the first returns the second author alone.
471+ - ` ListAuthorBooks` selects one column from each side of a `LEFT JOIN`, so it
472+ gets its own `ListAuthorBooksResult` record and reads `title` as `null` for
473+ the author with no book, even though `books.title` is declared `NOT NULL`.
474+ - ` UpdateAuthorBio` , `DeleteAuthor`, and `CreateBook` are `:exec` writes and
475+ return their affected-row counts. `UpdateAuthorBio` also shows that parameter
476+ order and binding order are different things : ` $1` is the first method
477+ parameter even though `$2` occurs first in the SQL text. `CreateBook` writes
478+ the schema's other table, so the methods of one query group may span every
479+ table of its schema.
400480- The two `try` blocks run generated operations on a caller-owned `Connection`
401481 with auto-commit disabled. The runtime never closes, commits, rolls back, or
402482 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,
416496the query, and the line, and exits with status `1` :
417497
418498` ` ` text
419- sqlcj: Invalid query 'CountAuthorBios' in /home/dev/my-app/sql/queries.sql at line 31 : Unsupported SELECT expression: Function
499+ sqlcj: Invalid query 'CountAuthorBios' in /home/dev/my-app/sql/queries.sql at line 57 : Unsupported SELECT expression: Function
420500` ` `
421501
422502Every configured source is compiled before any file is written, so a diagnostic
0 commit comments