Skip to content

Commit da50266

Browse files
authored
Merge pull request #34 from sqlcj-dev/chore/sample-search-count-page-and-left-join
docs: demonstrate everyday reads in the sample and quickstart
2 parents 274d721 + 1313a79 commit da50266

5 files changed

Lines changed: 189 additions & 25 deletions

File tree

‎docs/quickstart.md‎

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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
55
the steps can be followed from an empty directory.
66

77
The 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

174181
The accepted column types and `CREATE TABLE` constructs are listed in
@@ -206,16 +213,47 @@ WHERE id = $1;
206213
DELETE
207214
FROM authors
208215
WHERE 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
221259
in 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
376436
missing row rejected: Query 'GetAuthor' in AuthorRepository returned no row; expected exactly one
377437
committed: Compiler pioneer
378438
rolled 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
379445
deleted 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,
416496
the 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

422502
Every configured source is compiled before any file is written, so a diagnostic

‎examples/maven-postgresql/sql/queries.sql‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,29 @@ WHERE id = $1;
2727
DELETE
2828
FROM authors
2929
WHERE id = $1;
30+
31+
-- name: SearchAuthors :many
32+
SELECT *
33+
FROM authors
34+
WHERE name ILIKE $1
35+
ORDER BY id;
36+
37+
-- name: CountAuthors :one
38+
SELECT COUNT(*) AS total
39+
FROM authors;
40+
41+
-- name: ListAuthorPage :many
42+
SELECT *
43+
FROM authors
44+
ORDER BY id
45+
LIMIT $1 OFFSET $2;
46+
47+
-- name: CreateBook :exec
48+
INSERT INTO books (author_id, title)
49+
VALUES ($1, $2);
50+
51+
-- name: ListAuthorBooks :many
52+
SELECT a.name, b.title
53+
FROM authors a
54+
LEFT JOIN books b ON b.author_id = a.id
55+
ORDER BY a.id, b.id;

‎examples/maven-postgresql/sql/schema.sql‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@ CREATE TABLE authors
55
bio TEXT,
66
created_at TIMESTAMP
77
);
8+
9+
CREATE TABLE books
10+
(
11+
id BIGSERIAL PRIMARY KEY,
12+
author_id BIGINT NOT NULL REFERENCES authors (id),
13+
title VARCHAR(255) NOT NULL
14+
);

‎examples/maven-postgresql/src/main/java/com/example/app/App.java‎

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@
1616
/**
1717
* Runs the documented sqlcj workflow against PostgreSQL through generated
1818
* code: create, read, optional read, list, update, a missing-row read, a
19-
* committed transaction, a rolled back transaction, and delete.
19+
* committed transaction, a rolled back transaction, a case-insensitive search,
20+
* a count, a page, a book insert, a left-joined projection, and delete.
2021
*
21-
* <p>All six named queries are methods of one generated
22+
* <p>All eleven named queries are methods of one generated
2223
* {@link AuthorRepository}. The repository is constructed once per execution
2324
* context: once from the {@code DataSource}-backed executor, and once more per
2425
* transaction from a caller-owned connection.
2526
*
26-
* <p>Create, read, optional read, and list each return one complete
27-
* {@code authors} row, so all four share the repository's single
28-
* {@link AuthorRepository.AuthorsRow} record.
27+
* <p>Create, read, optional read, list, search, and page each return one
28+
* complete {@code authors} row, so all six share the repository's single
29+
* {@link AuthorRepository.AuthorsRow} record. A query with its own result shape
30+
* generates its own record: {@link AuthorRepository.CountAuthorsResult} carries
31+
* the non-null {@code Long} count, and
32+
* {@link AuthorRepository.ListAuthorBooksResult} carries an author name beside
33+
* the title of the left-joined {@code books} row, which is {@code null} for an
34+
* author without a book.
2935
*
3036
* <p>Row absence is expressed by the {@code :optional} {@code FindAuthor}
3137
* query, which returns an empty {@link Optional}, while the {@code :one}
@@ -35,8 +41,8 @@
3541
* <p>Every step is checked, so the process exits non-zero as soon as one
3642
* generated operation returns an unexpected result.
3743
*
38-
* <p>The verification harness applies {@code sql/schema.sql} to an empty
39-
* {@code authors} table before this application runs.
44+
* <p>The verification harness applies {@code sql/schema.sql} to empty
45+
* {@code authors} and {@code books} tables before this application runs.
4046
*/
4147
public final class App {
4248

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

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

133+
List<AuthorRepository.AuthorsRow> searched = authors.searchAuthors("%lovelace%");
134+
135+
checkEquals(1, searched.size(), "SearchAuthors row count");
136+
checkEquals(created.id(), searched.get(0).id(), "SearchAuthors id");
137+
checkEquals("Ada Lovelace", searched.get(0).name(), "SearchAuthors name");
138+
139+
System.out.println("searched: " + searched.get(0).id() + " " + searched.get(0).name());
140+
141+
AuthorRepository.CountAuthorsResult count = authors.countAuthors();
142+
143+
check(count != null, "CountAuthors returned no row");
144+
checkEquals(2L, count.total(), "CountAuthors total");
145+
146+
System.out.println("count: " + count.total());
147+
148+
List<AuthorRepository.AuthorsRow> page = authors.listAuthorPage(1, 1);
149+
150+
checkEquals(1, page.size(), "ListAuthorPage row count");
151+
checkEquals(committedId, page.get(0).id(), "ListAuthorPage id");
152+
checkEquals("Grace Hopper", page.get(0).name(), "ListAuthorPage name");
153+
154+
System.out.println("page: " + page.get(0).id() + " " + page.get(0).name());
155+
156+
String bookTitle = "The Education of a Computer";
157+
158+
int bookRows = authors.createBook(committedId, bookTitle);
159+
160+
checkEquals(1, bookRows, "CreateBook affected rows");
161+
162+
System.out.println("created book rows: " + bookRows);
163+
164+
List<AuthorRepository.ListAuthorBooksResult> books = authors.listAuthorBooks();
165+
166+
checkEquals(2, books.size(), "ListAuthorBooks row count");
167+
checkEquals("Ada Lovelace", books.get(0).name(), "ListAuthorBooks unmatched author name");
168+
checkEquals(null, books.get(0).title(), "ListAuthorBooks unmatched title");
169+
checkEquals("Grace Hopper", books.get(1).name(), "ListAuthorBooks matched author name");
170+
checkEquals(bookTitle, books.get(1).title(), "ListAuthorBooks matched title");
171+
172+
System.out.println(
173+
"author books: " + books.get(0).name() + " / " + books.get(0).title()
174+
+ ", " + books.get(1).name() + " / " + books.get(1).title()
175+
);
176+
127177
int deletedRows = authors.deleteAuthor(created.id());
128178

129179
checkEquals(1, deletedRows, "DeleteAuthor affected rows");

‎scripts/verify-release.sh‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ generated_count=$(find "${generated_root}" -type f -name '*.java' | wc -l)
123123
[ "${generated_count}" -eq 1 ] \
124124
|| fail "expected 1 generated source but found ${generated_count}"
125125

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

0 commit comments

Comments
 (0)