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