sqlcj is configured by a single YAML file named sqlcj.yaml.
For a project that uses this file end to end, see the Quickstart. For the query annotations, supported SQL, and generated API, see Queries.
The generate command reads sqlcj.yaml from the directory it is run in:
sqlcj generate--config <path> selects another file, so generation can run from any working
directory:
sqlcj generate --config ../project/sqlcj.yamlThe option defaults to sqlcj.yaml, and a relative value is resolved against
the directory the command is run in. Relative paths inside the file are still
resolved against the directory that contains the file, not against the working
directory. A missing or invalid explicit file reports the usual configuration
diagnostic and exits with status 1; there is no fallback to sqlcj.yaml.
There is no short alias, no environment variable, and no configuration discovery in parent directories.
Only configuration version "1" is supported. Every field below is required;
none of them has a default value.
Version "1" commits sqlcj to PostgreSQL schema and query input and to
blocking JDBC execution; there is no engine or dialect option. See
PostgreSQL Support for the accepted column types, the accepted
CREATE TABLE constructs, and the null contract.
version: "1"
sql:
- name: Author
schema: schema.sql
queries: queries.sql
java:
package: dev.example.generated
out: generated| Field | Type | Description |
|---|---|---|
version |
string | Configuration contract version. Must be "1". |
sql |
list | Non-empty, ordered list of source entries. |
sql[].name |
string | Identity of the query group. Names the generated repository. |
sql[].schema |
string or list | One path, or a non-empty ordered list of paths, to a file containing CREATE TABLE statements or to a directory of .sql migration files. |
sql[].queries |
string | Path to a file containing named queries. |
java |
mapping | Java generation settings. |
java.package |
string | Package of the generated Java classes. |
java.out |
string | Directory that receives the generated package directories. |
Each entry pairs one group name with one schema source and one named-query source. Entries are loaded in declared order, each entry's queries are analyzed against that entry's own schema, and query order inside a query file is preserved.
Schemas are not shared or merged between entries. A query can only use tables declared in the schema sources of its own entry.
One entry generates exactly one repository containing every query of its query source, in declared query order. Query names are therefore scoped to their entry: two entries may use the same query name, while two queries of one entry that generate the same method name are rejected. See Generated Java Names for the naming rules and the collisions that end a run.
sql[].name is required and names the generated repository type: the entry is
converted to upper camel case and followed by Repository, so name: Author
generates AuthorRepository and name: author_admin generates
AuthorAdminRepository.
It must be a single valid, non-blank Java identifier. A Java keyword, the
literals true, false, and null, the identifier _, and a restricted
identifier such as var or record are rejected:
sqlcj: Invalid configuration in /home/dev/project/sqlcj.yaml: 'sql[0].name' value 'record' is not a valid Java identifier for a generated repository name
sqlcj does not derive the name from a table or a file name. A query file may join or write several tables, so the group boundary is declared, not guessed.
sql[].schema is either one path or a non-empty ordered list of paths:
sql:
- name: Author
schema: sql/authors/schema.sql
queries: sql/authors/queries.sql
- name: Order
schema:
- sql/orders/baseline.sql
- sql/orders/migrations
queries: sql/orders/queries.sqlA listed path is either a file, which contributes itself, or a directory, which
contributes the regular files in it whose name ends in .sql. A directory is
read non-recursively, and the name match is case-sensitive: a subdirectory and a
file named anything else, including schema.SQL, are ignored. The listed paths
keep their declared order and each directory is expanded in place.
A directory is ordered like a Flyway migration directory:
- A versioned migration file, named
V<version>__<description>.sqlwith version parts separated by.or_, comes first. Two versions are compared part by part as numbers, counting a missing trailing part as zero, soV1__init.sql,V1_1__add_index.sql,V2__add_orders.sql, andV10__add_totals.sqlare ordered exactly that way. - Every other
.sqlfile follows, ordered by file name. - An undo file, named
U<version>__<description>.sql, is ignored.
The order never depends on how the filesystem lists the directory. sqlcj only reads the files, so a Flyway placeholder, a configured file-name prefix, and a repeatable migration have no meaning of their own.
The statements of the files are applied in that order to one schema model, so a later migration alters the tables an earlier file created and a schema failure names the file that contains it. A statement that refers to a table or a column no earlier statement created is such a failure; see Ordered Table DDL:
sqlcj: Invalid schema source /home/dev/project/sql/orders/migrations/V2__add_orders.sql: Table not found in schema: payments
A directory that contributes no file, and two versioned files of one directory that declare the same version, are invalid schema input:
sqlcj: Invalid schema source /home/dev/project/sql/orders/migrations: directory contains no .sql files
sqlcj: Invalid schema source /home/dev/project/sql/orders/migrations: duplicate migration version in V1_0__add_index.sql and V1__init.sql
A configured path that cannot be read, whether it is a file or a directory, ends the run the same way:
sqlcj: Cannot read schema source: /home/dev/project/sql/orders/migrations: Permission denied
java.package must be a dot-separated sequence of valid, non-keyword Java
identifiers, for example dev.example.generated. It is used both for the
generated package declaration and for the generated file layout.
An entry named Author with java.package: dev.example.generated and
java.out: generated is written to:
generated/dev/example/generated/AuthorRepository.java
- String values must not be blank.
- Unknown fields are rejected.
- Wrong-typed fields are rejected, including an unquoted numeric
version. - A missing or unsupported
version, a missing section, an emptysqllist, an emptysql[].schemalist, a nullsqlentry, a blank value, an invalidsql[].name, and an invalidjava.packageare all invalid configuration.
A relative queries, java.out, or listed schema path is resolved against
the directory that contains the configuration file, whether that file is the
default sqlcj.yaml or one named by --config, and not against the process
working directory at a later point in time. An absolute path is used as-is.
Resolved paths are normalized lexically, so ../sql/schema.sql is supported.
Resolution does not require java.out to exist, does not expand ~,
environment variables, or globs, and does not follow symbolic links to a
canonical location.
version: "1"
sql:
- name: User
schema: sql/users/schema.sql
queries: sql/users/queries.sql
- name: Order
schema: sql/orders/schema.sql
queries: sql/orders/queries.sql
java:
package: dev.example.generated
out: target/generated-sources/sqlcjWith the configuration above, sqlcj generates one repository per entry:
target/generated-sources/sqlcj/dev/example/generated/UserRepository.java
target/generated-sources/sqlcj/dev/example/generated/OrderRepository.java
Both repositories may contain a query named GetById, because each name is
resolved inside its own repository.
The entries share one generated package, so they also share its row records and
its enums: a table whose complete row both entries return generates one
<TableName>Row file that both repositories return, and an enum type both
entries use generates one Java enum file that both repositories use.
The configured sql[].name and the SQL names inside the entry — query names,
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.
Every name is derived from its SQL spelling by one deterministic rule set that uses no locale-dependent case mapping and no configuration:
-
The SQL name is split into words at every character that is not a letter or a digit, so
get_author,Get-User, anduser idhave two words each. There is no split inside a word, soGetAuthorandHTTPStatusare one word. -
A word written without a lower-case letter is lower-cased, so the acronyms
IDandURLbecome the wordsidandurl. Every other word keeps its spelling. -
A type name is upper camel case: the first character of each word is upper-cased and the words are joined.
authorsbecomesAuthors,get_authorandGetAuthorboth becomeGetAuthor, anduser_IDbecomesUserId. -
A method, record component, or parameter name is lower camel case: the upper camel form's leading run of upper-case letters is lower-cased. A run of two or more letters that is followed by a lower-case letter keeps its last letter upper-case, because that letter starts the next word. So
created_atbecomescreatedAt,GetAuthorbecomesgetAuthor,HTTPStatusbecomeshttpStatus,HTTP2Statusbecomeshttp2Status, andGetHTTPStatusbecomesgetHTTPStatus. -
A name that would start with a digit is prefixed with
_, so the query1st_querygenerates the result record_1stQueryResult. -
A SQL name with no letter and no digit at all, such as
_,***, or$, has no Java name and ends the run:sqlcj: Invalid query group 'User' in /home/dev/project/sql/queries.sql: SQL name '***' of query 'ListUsers' has no letter or digit to generate a Java name fromAn enum label is rejected the same way, and so are two labels of one enum type that generate one constant:
sqlcj: Invalid query group 'Stage' in /home/dev/project/sql/queries.sql: Label '***' of enum type 'stage_setting' has no letter or digit to generate a Java constant from sqlcj: Invalid query group 'Stage' in /home/dev/project/sql/queries.sql: Labels 'in progress' and 'in-progress' of enum type 'stage_setting' generate the same constant IN_PROGRESS
The rules are applied as follows:
- the repository type is the upper camel form of
sql[].namefollowed byRepository, soauthor_admingeneratesAuthorAdminRepository, - a result record is the upper camel form of the query name followed by
Result, soget_authorgeneratesGetAuthorResult, - a row record is the upper camel form of the table name followed by
Row, so the tableauthorsgeneratesAuthorsRow. Row names are normalized, never singularized. A row record is a top-level type ofjava.package, generated once per table into its own file and shared by every repository of the package that returns that row, - an enum type is the upper camel form of its PostgreSQL name, so
stage_settinggeneratesStageSetting. It is a top-level type ofjava.package, generated once per enum type into its own file, and only for an enum type a query reads or binds, - an enum constant is the label's words, upper-cased and joined with
_, so the labelsin progressandin-progressboth generateIN_PROGRESS, the labelInProgress, which is one word, generatesINPROGRESS, and the label2fastgenerates_2FAST, - a method is the lower camel form of the query name, so
get_authorgeneratesgetAuthor, - 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 byMapper, soAuthorsRowgeneratesauthorsRowMapper, - record components and method parameters are the lower camel form of the column
name or projection alias, so
created_atgeneratescreatedAt.
Generated names also avoid names that Java or the generated source already uses:
- a method, record component, or parameter that would be a Java keyword or one
of the literals
true,false, andnullis suffixed with_, so a query namedClassgenerates the recordClassResultand the methodclass_, and a column namedclassgenerates the componentclass_, - a method name also avoids the inherited
Objectmethod names, so a query namedToStringgenerates the methodtoString_, - record components avoid inherited
Objectmethod names, - method parameters avoid the generator-owned name
executorand 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
AuthorskeepsauthorsRowMapperwhile the row record of the tableauthorsusesauthorsRowMapper1.
Method parameters and record components are disambiguated inside their own
generated method or record, in logical parameter order and selected-column
order, using the suffixes 1, 2, and so on. Two parameters resolved from the
column id become id1 and id2; the columns user id and user-id both
convert to userId and become the components userId1 and userId2; and a
component that would be an inherited Object method name, such as hashCode,
becomes hashCode1.
A projection alias is the name of its result component, so
SELECT u.id AS user_id, p.id AS profile_id generates the components userId
and profileId instead of two disambiguated id components.
The generated row mapper reads each result column by its one-based position in the selected-column list, so renaming a component never changes which column it reads, and identically named columns selected from different query sources stay distinct.
Two queries of one entry that generate the same method name are rejected instead of being renamed, because a repository method is a name the application calls. The diagnostic names both queries:
sqlcj: Invalid query group 'User' in /home/dev/project/sql/queries.sql: Queries 'get_author' and 'GetAuthor' generate the same repository method 'getAuthor'
Two queries of one entry whose nested result types differ only by case are rejected for the same reason, because those class files are one path on a 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 whose row records are equal ignoring case are rejected on the same grounds, naming both tables and both row types. Because a row record belongs to the package, the two tables may come from one entry or from two:
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
Two enum types whose Java enums are equal ignoring case, and an enum type whose Java enum is equal ignoring case to a repository, row record, or nested result record of the package, are rejected on the same grounds, in either generation order:
sqlcj: Invalid query group 'Stage' in /home/dev/project/sql/queries.sql: Enum types 'stage_setting' and 'stagesetting' generate enum types that are equal ignoring case: StageSetting and Stagesetting
sqlcj: Invalid query group 'Stage' in /home/dev/project/sql/queries.sql: Enum type 'users_row' generates UsersRow, which is equal ignoring case to the generated type UsersRow
Two entries of one package that return the complete row of one table generate one row record, so they must define that table alike: the analyzed row columns must have the same names, types, and nullability, in the same order. Otherwise the run ends, naming the entry that defined the table first:
sqlcj: Invalid query group 'Library' in /home/dev/project/sql/library.sql: Table 'authors' differs from its definition in query group 'Author', which generates the same row type AuthorsRow
Two entries that use one enum type must define its labels alike for the same reason, because the package generates one Java enum for it:
sqlcj: Invalid query group 'Library' in /home/dev/project/sql/library.sql: Enum type 'stage_setting' differs from its definition in query group 'Author', which generates the same enum type StageSetting
A cross-entry failure is reported against the later entry in configuration order and, like every generation failure, ends the run before any file is written.
Two entries whose repository files resolve to the same path, or to paths that
differ only by case and are therefore not portable, are rejected before any file
of the run is written, so existing output is not overwritten. Two entry names
that differ only by the case of their first character, such as User and
user, generate one repository name and collide as the same path:
sqlcj: Duplicate generated file for repositories 'User' and 'user': generated/UserRepository.java
sqlcj: Generated file paths for repositories 'UserData' and 'Userdata' differ only by case: generated/UserDataRepository.java and generated/UserdataRepository.java
Invalid configuration, unreadable SQL sources, and output that cannot be written
or cleaned up make sqlcj generate print a single message on standard error and
exit with status 1. The message names the configuration file and, when
available, the offending field, value, source path, or output path, and ends
with the fact that explains the failure, such as the reason the filesystem
reports or the problem the YAML parser reports with its line and column:
sqlcj: Cannot read configuration file: /home/dev/project/sqlcj.yaml: No such file or directory
sqlcj: Malformed configuration file: /home/dev/project/sqlcj.yaml: expected <block end>, but found '<block mapping start>' at line 2, column 3
sqlcj: Invalid configuration in /home/dev/project/sqlcj.yaml: 'java.package' value 'dev.class.generated' is not a valid Java package name
sqlcj: Cannot read queries source: /home/dev/project/sql/missing.sql: No such file or directory
sqlcj: Cannot create output directory: /home/dev/project/target/generated-sources/sqlcj/dev/example/generated: Not a directory
sqlcj: Cannot write generated file: /home/dev/project/target/generated-sources/sqlcj/dev/example/generated/UsersRepository.java: Permission denied
sqlcj: Cannot read output manifest: /home/dev/project/target/generated-sources/sqlcj/sqlcj-manifest.txt: Permission denied
sqlcj: Cannot delete stale generated file: /home/dev/project/target/generated-sources/sqlcj/dev/example/generated/OrdersRepository.java: Permission denied
sqlcj: Cannot write output manifest: /home/dev/project/target/generated-sources/sqlcj/sqlcj-manifest.txt: Permission denied
Every configured entry is loaded, analyzed, and generated before the run writes its first file. A configuration, source, schema, query, or generated-path failure therefore ends the run without writing any file, and output from an earlier successful run is left unchanged.
Writing itself is not transactional. The files of a successful compilation are written one after another, each created or truncated in place, so a filesystem failure part-way through can leave a mixture of newly written files and files from a previous run. sqlcj does not remove or roll back files it has already written.
A filesystem failure while writing ends the run with one diagnostic naming the
output directory or generated file it could not write and exit status 1. The
files the run had already written stay in place.
A successful run records what it generated in sqlcj-manifest.txt inside
java.out. The manifest is a UTF-8 text file listing every generated file,
repositories, row records, and enums alike, as a path relative to java.out,
with /
between its name elements, sorted, one per line. The next successful run deletes
the files the previous manifest listed that it did not generate itself, so a
renamed or removed configuration entry leaves no stale repository behind, and a
row record no entry returns any more, and an enum no entry uses any more, are
deleted too.
Cleanup is deliberately narrow:
- A file no previous manifest listed is never deleted, so a hand-written file in the generated package survives, and so does output written before the first manifest existed.
- No directory is deleted, so a package directory stays in place after the repository inside it is removed.
- Changing
java.outleaves the previous directory and its files untouched, because cleanup only reads the manifest of the directory it generates into. - A run that fails while writing a generated file, deleting a stale one, or writing the manifest leaves the previous manifest in place, so the next successful run cleans up the files it still lists.