Skip to content
Merged
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
203 changes: 203 additions & 0 deletions docs/postgresql.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# PostgreSQL Support

sqlcj compiles PostgreSQL schema snapshots and queries into Java that runs on
JDBC. This document states the database contract of configuration version `"1"`
and lists the SQL types and `CREATE TABLE` constructs that a schema source may
use.

## Engine Contract

Configuration version `"1"` means PostgreSQL schema and query input and
blocking JDBC execution. There is no engine option and no dialect option in
`sqlcj.yaml`; the full file format is documented separately.

The application owns the database connection: sqlcj's runtime
`dev.sqlcj.runtime.JdbcQueryExecutor` is constructed with a
`javax.sql.DataSource` and executes each generated query as a JDBC
`PreparedStatement`. sqlcj does not ship a JDBC driver, so the application also
supplies the PostgreSQL driver.

Behavior is verified against PostgreSQL 16. The pipeline is executed end to end
against a `postgres:16-alpine` container: the schema snapshot is run as
PostgreSQL DDL, the generated Java is compiled, and the generated classes are
executed through the JDBC runtime. Those tests are skipped when Docker is
unavailable.

H2 is used only inside sqlcj's own tests as a convenience database and is not a
supported target. The PostgreSQL driver, the container library, and H2 are all
test-scoped dependencies of the build.

## Schema Snapshot Input

A `sql[].schema` file is an explicit snapshot of the tables a query may use.

- Only `CREATE TABLE` statements are accepted. Any other statement in the file
is rejected.
- A file may contain several `CREATE TABLE` statements, and table order is
preserved.
- SQL identifier delimiters are removed for the parsed model, so the table
`"user data"` is modeled as `user data` and the column `"user id"` is modeled
as `user id`.

## Supported Column Types

Type spellings are matched case-insensitively, after parenthesized type
arguments are removed and remaining whitespace is collapsed to single spaces.
A declared length, precision, or scale is therefore accepted and ignored:
`VARCHAR(255)`, `DECIMAL(10, 2)`, and `timestamp(3) with time zone` are all
accepted and map exactly like their unparameterized spellings.

| Accepted SQL spellings | Java type | Notes |
| --- | --- | --- |
| `INTEGER`, `INT`, `SERIAL` | `Integer` | A `SERIAL` column is always modeled non-null. |
| `BIGINT`, `BIGSERIAL` | `Long` | A `BIGSERIAL` column is always modeled non-null. |
| `SMALLINT` | `Short` | |
| `BOOLEAN`, `BOOL` | `Boolean` | |
| `VARCHAR` | `String` | |
| `TEXT` | `String` | |
| `DATE` | `java.time.LocalDate` | |
| `TIMESTAMP` | `java.time.LocalDateTime` | |
| `TIMESTAMP WITH TIME ZONE` | `java.time.OffsetDateTime` | PostgreSQL normalizes the stored value to the session time zone, so a value read back equals the written value by instant rather than by offset. |
| `DECIMAL`, `NUMERIC` | `java.math.BigDecimal` | |
| `UUID` | `java.util.UUID` | |

Any spelling that is not listed above is rejected.

The generated class imports `java.time.LocalDate`, `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 with
`resultSet.getObject(position, JavaType.class)` at its one-based position in the
selected-column list.

## Supported `CREATE TABLE` Constructs

| Construct | Handling |
| --- | --- |
| `NOT NULL` | Accepted and modeled. It is the only source of column nullability. |
| Column-level `PRIMARY KEY` | Accepted and recorded as a primary-key constraint on that column. |
| Table-level `PRIMARY KEY (...)`, named or unnamed | Accepted and recorded as a primary-key constraint. |
| Column-level `UNIQUE` | Accepted and recorded as a unique constraint on that column. |
| Table-level `UNIQUE (...)`, named or unnamed | Accepted and recorded as a unique constraint. |
| `DEFAULT` with a literal or a function, such as `DEFAULT 1`, `DEFAULT 'new'`, `DEFAULT now()` | Accepted and ignored. |
| Column-level `REFERENCES t (c)`, including referential actions such as `ON DELETE CASCADE` | Accepted and ignored. |
| Column-level `CHECK (...)` | Accepted and ignored. |
| Table-level `FOREIGN KEY (...) REFERENCES ...`, named or unnamed | Accepted and ignored. |
| Table-level `CHECK (...)`, named or unnamed | Accepted and ignored. |
| Any statement other than `CREATE TABLE`, such as `ALTER TABLE` | Rejected. |
| Any other table-constraint kind | Rejected. |
| Unparsable SQL | Rejected. |

Constraints never affect generated Java. A recorded primary-key or unique
constraint is kept in the parsed schema model, but no code in the compilation
pipeline reads it, so it changes no generated type, method, parameter, or
result component. "Ignored" means the construct is accepted as valid schema
input and is not carried into the model at all.

## Nulls

Every generated method parameter and every generated result component uses a
reference type, so each of them can represent SQL `NULL`. `Optional` and custom
nullable wrappers are not used.

Null handling is uniform and independent of the column type:

- the generated argument list is built with `java.util.Arrays.asList`, which
accepts null elements,
- `JdbcQueryExecutor` binds each argument positionally with
`PreparedStatement.setObject`, so a null argument is bound as SQL `NULL`,
- each result column is read with `ResultSet.getObject(position, Class)`, so a
SQL `NULL` is read back as `null`.

Nullability itself is parsed from `NOT NULL` only:

- a column without `NOT NULL` is modeled nullable, so a column declared bare
`PRIMARY KEY` is modeled nullable,
- a `SERIAL` or `BIGSERIAL` column is always modeled non-null, because
PostgreSQL defines those spellings as an integer type with a sequence default
and `NOT NULL`,
- nullability never changes a generated Java type. It is carried into the
analyzed model, but the generated type is resolved from the column type alone.

Null binding and null reading are executed against PostgreSQL for the nullable
columns of the integration schema snapshot, which cover `SMALLINT`, `VARCHAR`,
`TEXT`, `BOOLEAN`, `DATE`, `TIMESTAMP`, `DECIMAL`, `UUID`, and
`TIMESTAMP WITH TIME ZONE`.

## Unsupported Types and DDL

These spellings are rejected even though PostgreSQL accepts them:

- `TIMESTAMPTZ`
- `SERIAL4`
- `SERIAL8`
- `SMALLSERIAL`
- `TIMESTAMP WITHOUT TIME ZONE`

The following type families are not supported at all, because only the
spellings listed in [Supported Column Types](#supported-column-types) are
accepted:

- floating point, such as `REAL` and `DOUBLE PRECISION`,
- binary, such as `BYTEA`,
- `JSON` and `JSONB`,
- arrays,
- enum types,
- domain types,
- range types,
- composite types,
- spatial types.

### Failure Behavior

An unsupported type or an unsupported statement stops compilation. `sqlcj
generate` prints a single message on standard error, exits with a non-zero
status, and writes no generated file for that run, because every configured
source is compiled before any file is written. Output written by an earlier
successful run is left unchanged.

The message names the schema source and the offending type or statement:

```text
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported SQL column type: TIMESTAMPTZ
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported schema statement: Alter
sqlcj: Invalid schema source /home/dev/project/schema.sql: Failed to parse schema
```

## Verified by

Type table:

- `DefaultSchemaParserTest.shouldKeepParsingDeliveredColumnTypes` and
`DefaultSchemaParserTest.shouldParseAddedColumnTypes` cover the accepted
spellings, their case-insensitive forms, and the ignored type arguments.
- `PostgresIntegrationTest.shouldExecuteGeneratedOneQueryAgainstPostgres`,
`PostgresIntegrationTest.shouldExecuteGeneratedWriteAgainstPostgres`, and
`PostgresIntegrationTest.shouldRoundTripSerialUuidAndTimestampWithTimeZoneValues`
execute the Java mappings against PostgreSQL 16.
- `DefaultSchemaParserTest.shouldRejectUnsupportedColumnType` covers the
rejected spellings.

`CREATE TABLE` table:

- `DefaultSchemaParserTest.shouldParseTableConstraints`,
`DefaultSchemaParserTest.shouldParseTableWithoutConstraints`,
`DefaultSchemaParserTest.shouldCanonicalizeQuotedTableAndColumnNames`,
`DefaultSchemaParserTest.shouldParseColumnSpecificationsThatDoNotAffectTypes`,
`DefaultSchemaParserTest.shouldIgnoreForeignKeyAndCheckTableConstraints`, and
`DefaultSchemaParserTest.shouldParseNamedTableConstraintsAlongsideIgnoredOnes`
cover the modeled and ignored constructs.
- `DefaultSchemaParserTest.shouldRejectUnsupportedSchemaStatement` and
`DefaultSchemaParserTest.shouldThrowSchemaParseExceptionForInvalidSql` cover
the rejected input.
- `PostgresIntegrationTest.shouldExecuteGeneratedQueryForSnapshotWithIgnoredTableConstraints`
proves that such a snapshot is valid PostgreSQL DDL and compiles and executes.

Nulls:

- `PostgresIntegrationTest.shouldBindAndReadNullValuesThroughGeneratedCode`
binds null arguments and reads null results through generated code against
PostgreSQL.
- `DefaultSchemaParserTest.shouldParseColumns`,
`DefaultSchemaParserTest.shouldParseNullabilityOfAddedColumnTypes`, and
`DefaultSchemaParserTest.shouldParseSerialColumnAsNotNullable` cover parsed
nullability.
Loading