Skip to content

Commit 95c0715

Browse files
committed
docs: describe the PostgreSQL contract and supported types
1 parent 104edf2 commit 95c0715

1 file changed

Lines changed: 203 additions & 0 deletions

File tree

‎docs/postgresql.md‎

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# PostgreSQL Support
2+
3+
sqlcj compiles PostgreSQL schema snapshots and queries into Java that runs on
4+
JDBC. This document states the database contract of configuration version `"1"`
5+
and lists the SQL types and `CREATE TABLE` constructs that a schema source may
6+
use.
7+
8+
## Engine Contract
9+
10+
Configuration version `"1"` means PostgreSQL schema and query input and
11+
blocking JDBC execution. There is no engine option and no dialect option in
12+
`sqlcj.yaml`; the full file format is documented separately.
13+
14+
The application owns the database connection: sqlcj's runtime
15+
`dev.sqlcj.runtime.JdbcQueryExecutor` is constructed with a
16+
`javax.sql.DataSource` and executes each generated query as a JDBC
17+
`PreparedStatement`. sqlcj does not ship a JDBC driver, so the application also
18+
supplies the PostgreSQL driver.
19+
20+
Behavior is verified against PostgreSQL 16. The pipeline is executed end to end
21+
against a `postgres:16-alpine` container: the schema snapshot is run as
22+
PostgreSQL DDL, the generated Java is compiled, and the generated classes are
23+
executed through the JDBC runtime. Those tests are skipped when Docker is
24+
unavailable.
25+
26+
H2 is used only inside sqlcj's own tests as a convenience database and is not a
27+
supported target. The PostgreSQL driver, the container library, and H2 are all
28+
test-scoped dependencies of the build.
29+
30+
## Schema Snapshot Input
31+
32+
A `sql[].schema` file is an explicit snapshot of the tables a query may use.
33+
34+
- Only `CREATE TABLE` statements are accepted. Any other statement in the file
35+
is rejected.
36+
- A file may contain several `CREATE TABLE` statements, and table order is
37+
preserved.
38+
- SQL identifier delimiters are removed for the parsed model, so the table
39+
`"user data"` is modeled as `user data` and the column `"user id"` is modeled
40+
as `user id`.
41+
42+
## Supported Column Types
43+
44+
Type spellings are matched case-insensitively, after parenthesized type
45+
arguments are removed and remaining whitespace is collapsed to single spaces.
46+
A declared length, precision, or scale is therefore accepted and ignored:
47+
`VARCHAR(255)`, `DECIMAL(10, 2)`, and `timestamp(3) with time zone` are all
48+
accepted and map exactly like their unparameterized spellings.
49+
50+
| Accepted SQL spellings | Java type | Notes |
51+
| --- | --- | --- |
52+
| `INTEGER`, `INT`, `SERIAL` | `Integer` | A `SERIAL` column is always modeled non-null. |
53+
| `BIGINT`, `BIGSERIAL` | `Long` | A `BIGSERIAL` column is always modeled non-null. |
54+
| `SMALLINT` | `Short` | |
55+
| `BOOLEAN`, `BOOL` | `Boolean` | |
56+
| `VARCHAR` | `String` | |
57+
| `TEXT` | `String` | |
58+
| `DATE` | `java.time.LocalDate` | |
59+
| `TIMESTAMP` | `java.time.LocalDateTime` | |
60+
| `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. |
61+
| `DECIMAL`, `NUMERIC` | `java.math.BigDecimal` | |
62+
| `UUID` | `java.util.UUID` | |
63+
64+
Any spelling that is not listed above is rejected.
65+
66+
The generated class imports `java.time.LocalDate`, `java.time.LocalDateTime`,
67+
`java.time.OffsetDateTime`, `java.math.BigDecimal`, and `java.util.UUID` as
68+
needed; the remaining types need no import. Each result column is read with
69+
`resultSet.getObject(position, JavaType.class)` at its one-based position in the
70+
selected-column list.
71+
72+
## Supported `CREATE TABLE` Constructs
73+
74+
| Construct | Handling |
75+
| --- | --- |
76+
| `NOT NULL` | Accepted and modeled. It is the only source of column nullability. |
77+
| Column-level `PRIMARY KEY` | Accepted and recorded as a primary-key constraint on that column. |
78+
| Table-level `PRIMARY KEY (...)`, named or unnamed | Accepted and recorded as a primary-key constraint. |
79+
| Column-level `UNIQUE` | Accepted and recorded as a unique constraint on that column. |
80+
| Table-level `UNIQUE (...)`, named or unnamed | Accepted and recorded as a unique constraint. |
81+
| `DEFAULT` with a literal or a function, such as `DEFAULT 1`, `DEFAULT 'new'`, `DEFAULT now()` | Accepted and ignored. |
82+
| Column-level `REFERENCES t (c)`, including referential actions such as `ON DELETE CASCADE` | Accepted and ignored. |
83+
| Column-level `CHECK (...)` | Accepted and ignored. |
84+
| Table-level `FOREIGN KEY (...) REFERENCES ...`, named or unnamed | Accepted and ignored. |
85+
| Table-level `CHECK (...)`, named or unnamed | Accepted and ignored. |
86+
| Any statement other than `CREATE TABLE`, such as `ALTER TABLE` | Rejected. |
87+
| Any other table-constraint kind | Rejected. |
88+
| Unparsable SQL | Rejected. |
89+
90+
Constraints never affect generated Java. A recorded primary-key or unique
91+
constraint is kept in the parsed schema model, but no code in the compilation
92+
pipeline reads it, so it changes no generated type, method, parameter, or
93+
result component. "Ignored" means the construct is accepted as valid schema
94+
input and is not carried into the model at all.
95+
96+
## Nulls
97+
98+
Every generated method parameter and every generated result component uses a
99+
reference type, so each of them can represent SQL `NULL`. `Optional` and custom
100+
nullable wrappers are not used.
101+
102+
Null handling is uniform and independent of the column type:
103+
104+
- the generated argument list is built with `java.util.Arrays.asList`, which
105+
accepts null elements,
106+
- `JdbcQueryExecutor` binds each argument positionally with
107+
`PreparedStatement.setObject`, so a null argument is bound as SQL `NULL`,
108+
- each result column is read with `ResultSet.getObject(position, Class)`, so a
109+
SQL `NULL` is read back as `null`.
110+
111+
Nullability itself is parsed from `NOT NULL` only:
112+
113+
- a column without `NOT NULL` is modeled nullable, so a column declared bare
114+
`PRIMARY KEY` is modeled nullable,
115+
- a `SERIAL` or `BIGSERIAL` column is always modeled non-null, because
116+
PostgreSQL defines those spellings as an integer type with a sequence default
117+
and `NOT NULL`,
118+
- nullability never changes a generated Java type. It is carried into the
119+
analyzed model, but the generated type is resolved from the column type alone.
120+
121+
Null binding and null reading are executed against PostgreSQL for the nullable
122+
columns of the integration schema snapshot, which cover `SMALLINT`, `VARCHAR`,
123+
`TEXT`, `BOOLEAN`, `DATE`, `TIMESTAMP`, `DECIMAL`, `UUID`, and
124+
`TIMESTAMP WITH TIME ZONE`.
125+
126+
## Unsupported Types and DDL
127+
128+
These spellings are rejected even though PostgreSQL accepts them:
129+
130+
- `TIMESTAMPTZ`
131+
- `SERIAL4`
132+
- `SERIAL8`
133+
- `SMALLSERIAL`
134+
- `TIMESTAMP WITHOUT TIME ZONE`
135+
136+
The following type families are not supported at all, because only the
137+
spellings listed in [Supported Column Types](#supported-column-types) are
138+
accepted:
139+
140+
- floating point, such as `REAL` and `DOUBLE PRECISION`,
141+
- binary, such as `BYTEA`,
142+
- `JSON` and `JSONB`,
143+
- arrays,
144+
- enum types,
145+
- domain types,
146+
- range types,
147+
- composite types,
148+
- spatial types.
149+
150+
### Failure Behavior
151+
152+
An unsupported type or an unsupported statement stops compilation. `sqlcj
153+
generate` prints a single message on standard error, exits with a non-zero
154+
status, and writes no generated file for that run, because every configured
155+
source is compiled before any file is written. Output written by an earlier
156+
successful run is left unchanged.
157+
158+
The message names the schema source and the offending type or statement:
159+
160+
```text
161+
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported SQL column type: TIMESTAMPTZ
162+
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported schema statement: Alter
163+
sqlcj: Invalid schema source /home/dev/project/schema.sql: Failed to parse schema
164+
```
165+
166+
## Verified by
167+
168+
Type table:
169+
170+
- `DefaultSchemaParserTest.shouldKeepParsingDeliveredColumnTypes` and
171+
`DefaultSchemaParserTest.shouldParseAddedColumnTypes` cover the accepted
172+
spellings, their case-insensitive forms, and the ignored type arguments.
173+
- `PostgresIntegrationTest.shouldExecuteGeneratedOneQueryAgainstPostgres`,
174+
`PostgresIntegrationTest.shouldExecuteGeneratedWriteAgainstPostgres`, and
175+
`PostgresIntegrationTest.shouldRoundTripSerialUuidAndTimestampWithTimeZoneValues`
176+
execute the Java mappings against PostgreSQL 16.
177+
- `DefaultSchemaParserTest.shouldRejectUnsupportedColumnType` covers the
178+
rejected spellings.
179+
180+
`CREATE TABLE` table:
181+
182+
- `DefaultSchemaParserTest.shouldParseTableConstraints`,
183+
`DefaultSchemaParserTest.shouldParseTableWithoutConstraints`,
184+
`DefaultSchemaParserTest.shouldCanonicalizeQuotedTableAndColumnNames`,
185+
`DefaultSchemaParserTest.shouldParseColumnSpecificationsThatDoNotAffectTypes`,
186+
`DefaultSchemaParserTest.shouldIgnoreForeignKeyAndCheckTableConstraints`, and
187+
`DefaultSchemaParserTest.shouldParseNamedTableConstraintsAlongsideIgnoredOnes`
188+
cover the modeled and ignored constructs.
189+
- `DefaultSchemaParserTest.shouldRejectUnsupportedSchemaStatement` and
190+
`DefaultSchemaParserTest.shouldThrowSchemaParseExceptionForInvalidSql` cover
191+
the rejected input.
192+
- `PostgresIntegrationTest.shouldExecuteGeneratedQueryForSnapshotWithIgnoredTableConstraints`
193+
proves that such a snapshot is valid PostgreSQL DDL and compiles and executes.
194+
195+
Nulls:
196+
197+
- `PostgresIntegrationTest.shouldBindAndReadNullValuesThroughGeneratedCode`
198+
binds null arguments and reads null results through generated code against
199+
PostgreSQL.
200+
- `DefaultSchemaParserTest.shouldParseColumns`,
201+
`DefaultSchemaParserTest.shouldParseNullabilityOfAddedColumnTypes`, and
202+
`DefaultSchemaParserTest.shouldParseSerialColumnAsNotNullable` cover parsed
203+
nullability.

0 commit comments

Comments
 (0)