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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ sql:

```sql
-- name: GetAuthor :one
SELECT id, name, bio
SELECT *
FROM authors
WHERE id = $1;
```

```java
AuthorRepository authors = new AuthorRepository(executor);

AuthorRepository.GetAuthorResult author = authors.getAuthor(1L);
AuthorRepository.AuthorsRow author = authors.getAuthor(1L);
```

Every query of `sql/queries.sql` becomes a method of that one
Expand Down
26 changes: 22 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ resolved inside its own repository.
## Generated Java Names

The configured `sql[].name` and the SQL names inside the entry — query names,
column names, and projection aliases — become conventional Java identifiers.
table names, column names, and projection aliases — become conventional Java
identifiers.
SQL identifier delimiters are removed before a name is converted, so the quoted
column `"user id"` has the JDBC label `user id` and generates the component
`userId`, while executable SQL keeps the query exactly as written.
Expand Down Expand Up @@ -187,9 +188,14 @@ The rules are applied as follows:
`Repository`, so `author_admin` generates `AuthorAdminRepository`,
- a result record is the upper camel form of the query name followed by
`Result`, so `get_author` generates `GetAuthorResult`,
- a row record is the upper camel form of the table name followed by `Row`, so
the table `authors` generates `AuthorsRow`. Row names are normalized, never
singularized,
- a method is the lower camel form of the query name, so `get_author` generates
`getAuthor`,
- a row-mapper field is the method name followed by `RowMapper`,
- a row-mapper field is the method name followed by `RowMapper`, and the mapper
of a row record is the lower camel form of the row type followed by `Mapper`,
so `AuthorsRow` generates `authorsRowMapper`,
- record components and method parameters are the lower camel form of the column
name or projection alias, so `created_at` generates `createdAt`.

Expand All @@ -202,8 +208,13 @@ Generated names also avoid names that Java or the generated source already uses:
- a method name also avoids the inherited `Object` method names, so a query
named `ToString` generates the method `toString_`,
- record components avoid inherited `Object` method names,
- method parameters avoid the generator-owned name `executor` and the row-mapper
field names of the repository.
- method parameters avoid the generator-owned name `executor` and every
row-mapper field name of the repository, including the mappers of its row
records,
- a row record's mapper field yields to the mapper of a query that generates its
own, using the same numeric suffixes, so a query named `Authors` keeps
`authorsRowMapper` while the row record of the table `authors` uses
`authorsRowMapper1`.

Method parameters and record components are disambiguated inside their own
generated method or record, in logical parameter order and selected-column
Expand Down Expand Up @@ -240,6 +251,13 @@ case-insensitive filesystem:
sqlcj: Invalid query group 'User' in /home/dev/project/sql/queries.sql: Queries 'GetUser' and 'getuser' generate result types that differ only by case: GetUserResult and GetuserResult
```

Two tables of one entry whose row records are equal ignoring case are rejected
on the same grounds, naming both tables and both row types:

```text
sqlcj: Invalid query group 'User' in /home/dev/project/sql/queries.sql: Tables 'user_data' and 'userdata' generate row types that are equal ignoring case: UserDataRow and UserdataRow
```

### Generated path collisions

Two entries whose repository files resolve to the same path, or to paths that
Expand Down
63 changes: 48 additions & 15 deletions docs/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ A write targets exactly one table of its entry's schema.
-- name: CreateAuthor :one
INSERT INTO authors (name, bio)
VALUES ($1, $2)
RETURNING id, name, bio;
RETURNING *;

-- name: UpdateAuthorBio :exec
UPDATE authors
Expand All @@ -148,9 +148,11 @@ with a `RETURNING` clause that lists either:
or
- a bare `*`, which expands in the target table's schema column order.

A returning write produces the same generated result record, positional row
mapper, and cardinality behavior as a read, so a database-generated `SERIAL` or
`BIGSERIAL` value is read back with its declared type.
A returning write produces the same generated record, positional row mapper, and
cardinality behavior as a read, so a database-generated `SERIAL` or `BIGSERIAL`
value is read back with its declared type. A clause that is exactly `*` returns
the target table's shared `<TableName>Row` record, while a `RETURNING` column
list keeps the query's own `<QueryName>Result` record.

An aliased or computed `RETURNING` item, a qualified `table.*`, an unknown
column, and `RETURNING` on `:exec` are rejected. `ON CONFLICT`, multi-row
Expand Down Expand Up @@ -213,11 +215,19 @@ that repository; sqlcj never generates a class per query.
constructor taking that executor.
- Methods appear in query-source order. A method name is the lower camel form
of the query name, as in `get_author` and `GetAuthor` to `getAuthor`.
- A `:one` or `:many` query also generates a nested `public record` named
- A `:one` or `:many` query that returns one complete table row — a
single-source `SELECT *` or `SELECT <source>.*`, or a write whose `RETURNING`
clause is exactly `*` — returns the repository's nested
`<TableName>Row` record. That record and its private `RowMapper` field are
generated once per table, after the constructor, in the order the queries
first use them, and every query returning that row shares them.
- Every other `:one` or `:many` query generates a nested `public record` named
`<QueryName>Result` in upper camel case, whose components follow the
selected-column order and are named after each column's projection alias or
column name, plus a private `RowMapper` field that reads each column by its
one-based position.
one-based position. That includes an explicit column list, even one naming
every column, a wildcard combined with another projection item, a wildcard in
a join, and a `RETURNING` column list.
- A `:exec` query generates no result record and returns `int`.
- Generated source imports only `dev.sqlcj.runtime.QueryExecutor`,
`dev.sqlcj.runtime.RowMapper`, `java.util.List`, and the JDK types of the
Expand All @@ -234,6 +244,7 @@ package com.example.app.db;
import dev.sqlcj.runtime.QueryExecutor;
import dev.sqlcj.runtime.RowMapper;
import java.util.List;
import java.time.LocalDateTime;

/**
* Generated by sqlcj.
Expand All @@ -248,46 +259,53 @@ public final class AuthorRepository {
this.executor = executor;
}

public record CreateAuthorResult(
public record AuthorsRow(
Long id,
String name,
String bio
String bio,
LocalDateTime createdAt
) {
}

private static final RowMapper<CreateAuthorResult> createAuthorRowMapper =
resultSet -> new CreateAuthorResult(
private static final RowMapper<AuthorsRow> authorsRowMapper =
resultSet -> new AuthorsRow(
resultSet.getObject(1, Long.class),
resultSet.getObject(2, String.class),
resultSet.getObject(3, String.class)
resultSet.getObject(3, String.class),
resultSet.getObject(4, LocalDateTime.class)
);

/**
* Query: CreateAuthor
* Table: authors
* Type: ONE
*/
public CreateAuthorResult createAuthor(String name, String bio) {
public AuthorsRow createAuthor(String name, String bio) {
return executor.query(
"""
INSERT INTO authors (name, bio)
VALUES (?, ?)
RETURNING id, name, bio;""",
RETURNING *;""",
java.util.Arrays.asList(name, bio),
createAuthorRowMapper
authorsRowMapper
);
}

// getAuthor, listAuthors, updateAuthorBio, and deleteAuthor follow here.
}
```

`CreateAuthor`, `GetAuthor`, and `ListAuthors` each return one complete
`authors` row, so all three use the one `AuthorsRow` record and the one
`authorsRowMapper` field. `getAuthor` returns `AuthorsRow` and `listAuthors`
returns `List<AuthorsRow>`.

The application constructs the repository once per execution context:

```java
AuthorRepository authors = new AuthorRepository(new JdbcQueryExecutor(dataSource));

AuthorRepository.GetAuthorResult author = authors.getAuthor(1L);
AuthorRepository.AuthorsRow author = authors.getAuthor(1L);
```

A query name, projection alias, column name, and parameter name becomes a
Expand Down Expand Up @@ -403,6 +421,11 @@ Reads:
- `PostgresIntegrationTest.shouldExecuteGeneratedOneQueryAgainstPostgres` and
`PostgresIntegrationTest.shouldExecuteGeneratedManyQueryAgainstPostgres`
execute an ordered list read against PostgreSQL 16.
- `QueryAnalyzerTest.shouldResolveRowTableForFullRowSelect`,
`QueryAnalyzerTest.shouldNotResolveRowTableForQuerySpecificResult`,
`QueryAnalyzerTest.shouldNotResolveRowTableForJoinedWildcard`, and
`QueryAnalyzerTest.shouldNotResolveRowTableForExecWrite` cover which reads
return one complete table row.

Writes and `RETURNING`:

Expand All @@ -415,6 +438,8 @@ Writes and `RETURNING`:
`QueryAnalyzerTest.shouldRejectUnknownReturningColumn`, and
`QueryAnalyzerTest.shouldRejectExcludedReturningWriteForm` cover the write
shapes.
- `QueryAnalyzerTest.shouldResolveRowTableForReturningAllColumns` covers the
returning writes that produce a complete table row.
- `PostgresIntegrationTest.shouldExecuteGeneratedWriteAgainstPostgres`,
`PostgresIntegrationTest.shouldExecuteGeneratedInsertReturningAgainstPostgres`,
`PostgresIntegrationTest.shouldExecuteGeneratedUpdateReturningAgainstPostgres`,
Expand Down Expand Up @@ -452,3 +477,11 @@ Generated Java:
- `SqlcjCompilerIntegrationTest.shouldGenerateCompilableJavaFiles` and
`SqlcjCompilerIntegrationTest.shouldGenerateCompilableJavaForReturningWrites`
compile the generated output of the supported query shapes.
- `JavaCodeGeneratorTest.shouldShareOneRowRecordAcrossFullRowQueriesOfOneTable`,
`JavaCodeGeneratorTest.shouldGenerateOneRowRecordPerRowTable`,
`JavaCodeGeneratorTest.shouldDisambiguateRowMapperFromQueryRowMapper`, and
`JavaCodeGeneratorTest.shouldRejectRowTypesThatAreEqualIgnoringCase` cover the
shared row records, their mapper fields, and their collision.
- `PostgresIntegrationTest.shouldExecuteGeneratedFullRowQueriesIntoOneSharedRowTypeAgainstPostgres`
executes a returning write, a full-row read, and a qualified full-row list of
one table into one row type against PostgreSQL 16.
34 changes: 16 additions & 18 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,15 +180,15 @@ The accepted column types and `CREATE TABLE` constructs are listed in
-- name: CreateAuthor :one
INSERT INTO authors (name, bio)
VALUES ($1, $2)
RETURNING id, name, bio;
RETURNING *;

-- name: GetAuthor :one
SELECT id, name, bio
SELECT *
FROM authors
WHERE id = $1;

-- name: ListAuthors :many
SELECT id, name
SELECT *
FROM authors
ORDER BY id;

Expand All @@ -205,9 +205,12 @@ WHERE id = $1;

All five queries become methods of the one generated `AuthorRepository`:
`createAuthor`, `getAuthor`, `listAuthors`, `updateAuthorBio`, and
`deleteAuthor`. A `:one` or `:many` query also generates a nested result record
such as `AuthorRepository.GetAuthorResult`. The full query contract is
documented in [Queries](queries.md).
`deleteAuthor`. `CreateAuthor`, `GetAuthor`, and `ListAuthors` each return one
complete `authors` row, so all three 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. The full query contract is documented in [Queries](queries.md).

## 8. `src/main/java/com/example/app/App.java`

Expand All @@ -232,20 +235,19 @@ public final class App {

AuthorRepository authors = new AuthorRepository(executor);

AuthorRepository.CreateAuthorResult created =
authors.createAuthor("Ada Lovelace", "First programmer");
AuthorRepository.AuthorsRow created = authors.createAuthor("Ada Lovelace", "First programmer");

System.out.println("created: " + created.id() + " " + created.name());

AuthorRepository.GetAuthorResult read = authors.getAuthor(created.id());
AuthorRepository.AuthorsRow read = authors.getAuthor(created.id());

System.out.println("read: " + read.name() + " / " + read.bio());

int updatedRows = authors.updateAuthorBio(created.id(), "Mathematician");

System.out.println("updated rows: " + updatedRows);

for (AuthorRepository.ListAuthorsResult author : authors.listAuthors()) {
for (AuthorRepository.AuthorsRow author : authors.listAuthors()) {
System.out.println("listed: " + author.id() + " " + author.name());
}

Expand All @@ -254,11 +256,9 @@ public final class App {
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);

AuthorRepository transactionalAuthors =
new AuthorRepository(new JdbcQueryExecutor(connection));
AuthorRepository transactionalAuthors = new AuthorRepository(new JdbcQueryExecutor(connection));

AuthorRepository.CreateAuthorResult committed =
transactionalAuthors.createAuthor("Grace Hopper", null);
AuthorRepository.AuthorsRow committed = transactionalAuthors.createAuthor("Grace Hopper", null);

transactionalAuthors.updateAuthorBio(committed.id(), "Compiler pioneer");

Expand All @@ -270,11 +270,9 @@ public final class App {
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);

AuthorRepository transactionalAuthors =
new AuthorRepository(new JdbcQueryExecutor(connection));
AuthorRepository transactionalAuthors = new AuthorRepository(new JdbcQueryExecutor(connection));

AuthorRepository.CreateAuthorResult discarded =
transactionalAuthors.createAuthor("Temporary Author", null);
AuthorRepository.AuthorsRow discarded = transactionalAuthors.createAuthor("Temporary Author", null);

transactionalAuthors.updateAuthorBio(discarded.id(), "never stored");

Expand Down
6 changes: 3 additions & 3 deletions examples/maven-postgresql/sql/queries.sql
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
-- name: CreateAuthor :one
INSERT INTO authors (name, bio)
VALUES ($1, $2)
RETURNING id, name, bio;
RETURNING *;

-- name: GetAuthor :one
SELECT id, name, bio
SELECT *
FROM authors
WHERE id = $1;

-- name: ListAuthors :many
SELECT id, name
SELECT *
FROM authors
ORDER BY id;

Expand Down
Loading
Loading