Skip to content

Latest commit

 

History

History
464 lines (371 loc) · 21.6 KB

File metadata and controls

464 lines (371 loc) · 21.6 KB

Configuration

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

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

Supported Version

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

Fields

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.

sql entries

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

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

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

A 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:

  1. A versioned migration file, named V<version>__<description>.sql with version parts separated by . or _, comes first. Two versions are compared part by part as numbers, counting a missing trailing part as zero, so V1__init.sql, V1_1__add_index.sql, V2__add_orders.sql, and V10__add_totals.sql are ordered exactly that way.
  2. Every other .sql file follows, ordered by file name.
  3. 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

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

Value rules

  • 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 empty sql list, an empty sql[].schema list, a null sql entry, a blank value, an invalid sql[].name, and an invalid java.package are all invalid configuration.

Path Resolution

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.

Multiple Entries

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/sqlcj

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

Generated Java Names

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:

  1. The SQL name is split into words at every character that is not a letter or a digit, so get_author, Get-User, and user id have two words each. There is no split inside a word, so GetAuthor and HTTPStatus are one word.

  2. A word written without a lower-case letter is lower-cased, so the acronyms ID and URL become the words id and url. Every other word keeps its spelling.

  3. A type name is upper camel case: the first character of each word is upper-cased and the words are joined. authors becomes Authors, get_author and GetAuthor both become GetAuthor, and user_ID becomes UserId.

  4. 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_at becomes createdAt, GetAuthor becomes getAuthor, HTTPStatus becomes httpStatus, HTTP2Status becomes http2Status, and GetHTTPStatus becomes getHTTPStatus.

  5. A name that would start with a digit is prefixed with _, so the query 1st_query generates the result record _1stQueryResult.

  6. 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 from
    

    An 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[].name followed by 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 row record is a top-level type of java.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_setting generates StageSetting. It is a top-level type of java.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 labels in progress and in-progress both generate IN_PROGRESS, the label InProgress, which is one word, generates INPROGRESS, and the label 2fast generates _2FAST,
  • 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, 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.

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, and null is suffixed with _, so a query named Class generates the record ClassResult and the method class_, and a column named class generates the component class_,
  • 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 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 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.

Repository method collisions

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

Row record collisions

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.

Generated path collisions

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

Diagnostics

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

Generated Output and Failures

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.out leaves 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.