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
46 changes: 41 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,10 @@ 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: a
table whose complete row both entries return generates one `<TableName>Row`
file that both repositories return.
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

Expand Down Expand Up @@ -261,6 +262,14 @@ uses no locale-dependent case mapping and no configuration:
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:

```text
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
Expand All @@ -272,6 +281,14 @@ The rules are applied as follows:
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
Expand Down Expand Up @@ -340,6 +357,16 @@ 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:

```text
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
Expand All @@ -351,6 +378,13 @@ 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:

```text
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.
Expand Down Expand Up @@ -408,11 +442,13 @@ 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 and row records alike, as a path relative to `java.out`, with `/`
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 is deleted too.
row record no entry returns any more, and an enum no entry uses any more, are
deleted too.

Cleanup is deliberately narrow:

Expand Down
189 changes: 173 additions & 16 deletions docs/postgresql.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,26 +133,139 @@ accepted and map exactly like their unparameterized spellings.
| `BYTEA` | `byte[]` | A record compares an array component by reference, so two row records holding equal bytes are not `equals`. |
| `JSON` | `String` | The JSON text itself. PostgreSQL stores it as written, so it reads back exactly as written. sqlcj never parses, validates, or normalizes it. |
| `JSONB` | `String` | The JSON text itself. PostgreSQL stores a decomposed value, so the text reads back as PostgreSQL renders it rather than as written, and `=` compares by value. sqlcj never parses, validates, or normalizes it. |
| The name of an enum type the schema declares | The generated Java enum of that type | Matched without SQL identifier delimiters and case-insensitively, as PostgreSQL resolves an unquoted type name. See [Enum Types](#enum-types). |

Any spelling that is not listed above, and any array of any element type, has no
Java mapping. Such a column is recorded with its declared type instead of
Any spelling that is not listed above, and any array of any element type,
including an array of an enum type, has no Java mapping. Such a column is recorded with its declared type instead of
failing the schema, and fails only a query that uses it; see
[Unsupported Types and DDL](#unsupported-types-and-ddl).

The generated repository imports `java.time.LocalDate`, `java.time.LocalTime`,
`java.time.LocalDateTime`, `java.time.OffsetDateTime`, `java.math.BigDecimal`,
and `java.util.UUID` as needed; the remaining types need no import. Each result
column is read at its one-based position in the selected-column list, with
`resultSet.getObject(position, JavaType.class)`, or with
`resultSet.getString(position)` for a `JSON` or `JSONB` column, which the driver
reports as a type of its own rather than as a character type.
and `java.util.UUID` as needed; the remaining types need no import. A generated
enum belongs to the generated package, so it is used by its simple name and
needs no import either. Each result column is read at its one-based position in
the selected-column list, with `resultSet.getObject(position, JavaType.class)`,
or with `resultSet.getString(position)` for a `JSON` or `JSONB` column, which
the driver reports as a type of its own rather than as a character type. An
enum column reads its label the same way and resolves it with
`<EnumType>.fromLabel(...)`.

A `JSON` or `JSONB` argument is passed to the executor as
`new dev.sqlcj.runtime.UntypedText(value)`, written out in full so that the
generated imports are unchanged, and `JdbcQueryExecutor` binds that text with
`java.sql.Types.OTHER`. PostgreSQL then types the text from the context of its
placeholder, which is what a `json` or `jsonb` column or comparison needs: text
bound as `varchar` is rejected there.
bound as `varchar` is rejected there. An enum argument is passed the same way,
around the label of its constant, because a label bound as `varchar` is
rejected where an enum is expected.

## Enum Types

`CREATE TYPE <name> AS ENUM (...)` adds an enum type to the schema, and a
non-array column whose declared type names it is modeled as a column of that
type. Every query that reads or binds such a column uses the one Java enum the
package generates for the type:

```sql
CREATE TYPE stage_setting AS ENUM ('indoor', 'outdoor');

ALTER TYPE stage_setting ADD VALUE 'covered' BEFORE 'outdoor';
```

```java
// Code generated by sqlcj. DO NOT EDIT.

package com.example.app.db;

/**
* Generated by sqlcj.
*
* Enum: stage_setting
*/

public enum StageSetting {

INDOOR("indoor"),
COVERED("covered"),
OUTDOOR("outdoor");

private final String label;

StageSetting(String label) {
this.label = label;
}

public String label() {
return label;
}

public static StageSetting fromLabel(String label) {
if (label == null) {
return null;
}

for (StageSetting value : values()) {
if (value.label.equals(label)) {
return value;
}
}

throw new IllegalArgumentException("Unknown label for enum type stage_setting: " + label);
}
}
```

- The constants keep their exact PostgreSQL labels and follow them in
PostgreSQL's own sort order, which is the declared order with each added
label in the position its `ALTER TYPE ... ADD VALUE` places it.
- A constant is named by the naming rules in
[Generated Java Names](configuration.md#generated-java-names).
- `fromLabel` reads back a label: `null` for a SQL `NULL`, and a failure for a
label the generated enum does not hold, which means the database declares one
the schema source does not.
- Only an enum type a query actually uses generates a file.
- An array of an enum type, such as `stage_setting[]`, has no Java mapping and
is recorded with its declared type like any other array.

These statements update the enum types the statements and files before them
left:

| Statement | Handling |
| --- | --- |
| `CREATE TYPE ... AS ENUM (...)` | Adds the type with its declared labels. |
| `ALTER TYPE ... ADD VALUE '<label>'` | Appends the label after the last one. |
| `ALTER TYPE ... ADD VALUE '<label>' BEFORE '<neighbour>'` | Inserts the label directly before the neighbour. |
| `ALTER TYPE ... ADD VALUE '<label>' AFTER '<neighbour>'` | Inserts the label directly after the neighbour. |
| `ALTER TYPE ... ADD VALUE IF NOT EXISTS '<label>'` | Does nothing when the type already has the label. As PostgreSQL does, the existing label decides before the neighbour, so a neighbour the type does not have is not resolved at all. |
| Any other `ALTER TYPE` action, such as `RENAME TO`, `RENAME VALUE`, `OWNER TO`, `SET SCHEMA`, or an attribute change | Rejected. |
| `CREATE TYPE` of a composite, range, or shell type | Rejected. |
| `DROP TYPE` | Rejected. sqlcj's parser does not read the statement, so it is reported as a syntax error. |

Type names are matched case-insensitively, after their SQL identifier
delimiters are removed, and labels are matched exactly. A statement that repeats
a type or a label, or that refers to a type or a label that is not modeled, is
rejected as PostgreSQL rejects it:

```text
sqlcj: Invalid schema source /home/dev/project/sql/migrations/V2__stages.sql: Type already exists in schema: stage_setting
sqlcj: Invalid schema source /home/dev/project/sql/migrations/V2__stages.sql: Type not found in schema: stage_setting
sqlcj: Invalid schema source /home/dev/project/sql/migrations/V2__stages.sql: Label already exists in type stage_setting: indoor
sqlcj: Invalid schema source /home/dev/project/sql/migrations/V2__stages.sql: Label not found in type stage_setting: covered
```

A placeholder index repeated across two enum types, or across an enum and any
other type, has no single Java type and is rejected by query analysis, which
names each enum by its PostgreSQL type:

```text
sqlcj: Invalid query 'UpdateStage' in /home/dev/project/sql/queries.sql at line 5: Placeholder $1 has conflicting types: stage_setting from 'setting' and String from 'handle'
```

Two entries of one package that use one enum type must define its labels alike,
and a generated enum must not collide with another generated type of the
package; both are documented in
[Generated Java Names](configuration.md#generated-java-names).

## Supported `CREATE TABLE` Constructs

Expand Down Expand Up @@ -182,7 +295,8 @@ input and is not carried into the model at all.

A schema of `CREATE TABLE` statements alone is modeled exactly as it was before
ordered DDL existed. Beyond it, these statements update the schema the
statements and files before them left:
statements and files before them left; the enum-type statements are listed in
[Enum Types](#enum-types):

| Statement | Handling |
| --- | --- |
Expand Down Expand Up @@ -237,7 +351,6 @@ statements and files before them left it:
| `DROP FUNCTION`, `DROP FUNCTION IF EXISTS` | |
| `CREATE TRIGGER`, `DROP TRIGGER` | |
| `INSERT`, `UPDATE`, `DELETE` | A migration's data statements do not change the modeled tables. |
| `CREATE TYPE ... AS ENUM` | The type is not modeled, so a column of it is recorded as an unsupported type. |
| `ALTER TABLE ... ADD [CONSTRAINT name] PRIMARY KEY (...)` | Named or unnamed. |
| `ALTER TABLE ... ADD [CONSTRAINT name] UNIQUE (...)` | Named or unnamed. |
| `ALTER TABLE ... ADD [CONSTRAINT name] FOREIGN KEY (...) REFERENCES ...` | Named or unnamed. |
Expand Down Expand Up @@ -302,10 +415,12 @@ Null handling does not depend on the column type:
`PreparedStatement.setObject`, so a null argument is bound as SQL `NULL`. A
`JSON` or `JSONB` argument is bound the same way through its
`dev.sqlcj.runtime.UntypedText` wrapper, so a null value becomes a SQL `NULL`
without a declared type,
without a declared type. A null enum argument is wrapped the same way, so it
is bound as SQL `NULL` instead of as the label of a constant,
- each result column is read with `ResultSet.getObject(position, Class)`, or
with `ResultSet.getString(position)` for `JSON` and `JSONB`, so a SQL `NULL` is
read back as `null`.
with `ResultSet.getString(position)` for `JSON`, `JSONB`, and an enum, so a
SQL `NULL` is read back as `null`, which `fromLabel` keeps `null` for an
enum.

Nullability itself is parsed from `NOT NULL` only:

Expand All @@ -322,15 +437,14 @@ Null binding and null reading are executed against PostgreSQL for the nullable
columns of the integration schema snapshots, which cover `SMALLINT`, `VARCHAR`,
`TEXT`, `BOOLEAN`, `DATE`, `TIMESTAMP`, `DECIMAL`, `UUID`,
`TIMESTAMP WITH TIME ZONE`, `REAL`, `DOUBLE PRECISION`, `BYTEA`, `TIME`, `JSON`,
and `JSONB`.
`JSONB`, and an enum type.

## Unsupported Types and DDL

The following type families have no Java mapping, because only the spellings
listed in [Supported Column Types](#supported-column-types) are mapped:

- arrays, including `JSON[]` and `JSONB[]`,
- enum types,
- arrays, including `JSON[]`, `JSONB[]`, and an array of an enum type,
- domain types,
- range types,
- composite types,
Expand Down Expand Up @@ -407,6 +521,11 @@ Type table:
`JdbcQueryExecutorTest.shouldBindNullUntypedTextWithoutADeclaredSqlType`
cover the runtime binding of a null and a non-null `UntypedText` beside an
ordinary argument.
- `DefaultSchemaParserTest.shouldModelColumnsOfADeclaredEnumType` covers the
enum column of a `CREATE TABLE`, an added column, and a changed column type,
and `JavaCodeGeneratorTest.shouldBindEnumLabelsAndReadEnumColumnsByLabel`
covers the generated enum parameter, its `UntypedText` argument at every
binding position, and the `fromLabel` read.
- `DefaultSchemaParserTest.shouldRecordUnsupportedColumnType` and
`DefaultSchemaParserTest.shouldParseNullabilityOfUnsupportedColumnTypes` cover
the recorded type of an unmapped spelling and of an array, beside the mapped
Expand All @@ -421,6 +540,44 @@ Type table:
`SqlcjCompilerIntegrationTest.shouldGenerateCompilableRepositoryBesideUnsupportedTypeColumns`
compiles a repository generated beside an array and an `XML` column.

Enum types:

- `DefaultSchemaParserTest.shouldAddEnumTypeWithItsDeclaredLabels`,
`DefaultSchemaParserTest.shouldAddEnumLabelInItsStatedPosition`,
`DefaultSchemaParserTest.shouldIgnoreAddValueIfNotExistsForAnExistingLabel`,
and `DefaultSchemaParserTest.shouldApplyAnAddedLabelToAnEarlierEnumColumn`
cover the applied forms and the label order they leave, and
`DefaultSchemaParserTest.shouldReportTheEnumStatementItCannotApply` covers
each diagnostic.
- `DefaultSchemaParserTest.shouldKeepTheEnumTypeOfARenamedAndRetypedColumn`
covers the enum type a rename and a nullability change carry along, and
`DefaultSchemaParserTest.shouldRecordEnumArraysAndUndeclaredTypesAsUnsupported`
covers the enum array and the undeclared type that stay recorded.
- `DefaultSchemaParserTest.shouldRejectStatementOutsideTheIgnoredList` covers
the rejected `ALTER TYPE` actions and the composite `CREATE TYPE`, and
`DefaultSchemaParserTest.shouldRejectDropType` covers `DROP TYPE`.
- `QueryAnalyzerTest.shouldCarryTheEnumTypeOfASelectedColumnAndItsParameter`,
`QueryAnalyzerTest.shouldCarryTheEnumTypeOfAReturningColumnAndItsParameter`,
`QueryAnalyzerTest.shouldAcceptARepeatedIndexOfOneEnumType`, and
`QueryAnalyzerTest.shouldRejectARepeatedIndexAcrossConflictingEnumTypes`
cover the analyzed enum type and the repeated-index rejection.
- `JavaCodeGeneratorTest.shouldGenerateCompilableEnumWithLabelLookups` compiles
the generated enum and executes `label`, `fromLabel`, its null, and its
unknown label;
`JavaCodeGeneratorTest.shouldGenerateOnlyTheEnumTypesTheQueriesUse`,
`JavaCodeGeneratorTest.shouldNameTheGeneratedEnumAndItsConstants`, and the
four generator rejection tests cover the naming rules and the collisions.
- `SqlcjCompilerIntegrationTest.shouldGenerateOneSharedEnumForTwoEntries`,
`SqlcjCompilerIntegrationTest.shouldDeleteTheStaleEnumOfARemovedEnumQuery`,
and
`SqlcjCompilerIntegrationTest.shouldReportTwoEntriesThatDefineOneEnumTypeDifferently`
cover the shared file, the manifest, the stale deletion, and the cross-entry
diagnostic that writes nothing.
- `PostgresIntegrationTest.shouldRoundTripEnumValues` runs the ordered DDL
against PostgreSQL 16, compares the generated constants with
`pg_enum` in `enumsortorder`, and round trips a null and a non-null value as
an `INSERT` value, an equality predicate, and a result component.

`CREATE TABLE` table:

- `DefaultSchemaParserTest.shouldParseTableConstraints`,
Expand Down
Loading
Loading