Skip to content

Commit 72fa91a

Browse files
committed
feat(schema): model LIKE and PARTITION OF tables and partition links
CREATE TABLE copies the columns of a modeled LIKE source or partition parent in written order, models () as a table without columns, and rejects AS SELECT, OF type, and INHERITS with their line. A partition follows its parent's column changes, rename, and DROP TABLE until DETACH PARTITION, and ATTACH PARTITION of a modeled table links it.
1 parent 6034302 commit 72fa91a

5 files changed

Lines changed: 1072 additions & 36 deletions

File tree

‎docs/postgresql.md‎

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,12 @@ are recorded with their declared type; see
376376
| Table-level `FOREIGN KEY (...) REFERENCES ...`, named or unnamed | Accepted and ignored. |
377377
| Table-level `CHECK (...)`, named or unnamed | Accepted and ignored. |
378378
| Table-level `EXCLUDE USING ... (...)`, named or unnamed | Accepted and ignored. |
379+
| `LIKE t`, with any `INCLUDING` and `EXCLUDING` options | Accepted. Copies the columns, types, and nullability of the modeled table `t` at the position the clause stands in. |
380+
| `PARTITION OF t`, with `FOR VALUES FROM ... TO ...`, `FOR VALUES IN (...)`, `FOR VALUES WITH (MODULUS ...)`, or `DEFAULT` | Accepted. Copies the columns, types, and nullability of the modeled table `t` and keeps following its column changes; see [Ordered Table DDL](#ordered-table-ddl). |
381+
| An empty element list, `()` | Accepted and modeled as a table without columns. |
382+
| `AS SELECT ...` | Rejected as `Unsupported CREATE TABLE clause: AS SELECT at line <n>`. |
383+
| `OF <type>` | Rejected as `Unsupported CREATE TABLE clause: OF <type> at line <n>`. |
384+
| `INHERITS (p)` | Rejected as `Unsupported CREATE TABLE clause: INHERITS at line <n>`. |
379385
| A table statement sqlcj's parser cannot read, such as `ALTER FOREIGN TABLE` | Rejected; see [Ignored Statements](#ignored-statements). |
380386
| Any other table-constraint kind | Rejected. |
381387
| Unparsable SQL | Rejected. |
@@ -386,6 +392,36 @@ pipeline reads it, so it changes no generated type, method, parameter, or
386392
result component. "Ignored" means the construct is accepted as valid schema
387393
input and is not carried into the model at all.
388394

395+
A `CREATE TABLE` element list may state column definitions, table constraints,
396+
and `LIKE` clauses in any order, and the table's columns are modeled in that
397+
written order, as PostgreSQL creates them, each `LIKE` contributing its source's
398+
columns at its own position. A partition starts with its parent's columns,
399+
before every element of its own list. A column name that is repeated, by two
400+
column definitions, by a `LIKE` and a column definition, or by two `LIKE`
401+
clauses, fails as `Column already exists in table <t>: <c>`.
402+
403+
A `LIKE`'s `INCLUDING` and `EXCLUDING` options cannot change a copied column's
404+
type or nullability and are ignored, and neither `LIKE` nor `PARTITION OF`
405+
copies its source's recorded constraints: a copy records only the constraints
406+
its own statement states. A `LIKE` copy is independent of its source afterwards,
407+
as in PostgreSQL. A `LIKE` or `PARTITION OF` source sqlcj does not model fails
408+
as `Table not found in schema: <t>`.
409+
410+
Because sqlcj models one namespace, a `LIKE` or `PARTITION OF` source and an
411+
`ATTACH` or `DETACH PARTITION` partition resolve by their unqualified name, so
412+
`public.t` is `t`. Until sqlcj models namespaces, such a name qualified with any
413+
other schema names a table sqlcj leaves unmodeled and fails as
414+
`Table not found in schema: <schema>.<t>` rather than resolving to a modeled `t`.
415+
416+
A `CREATE TABLE` with neither an element list, `()`, nor `PARTITION OF`, such as
417+
`CREATE TABLE c LIKE t` without the parentheses PostgreSQL requires, states no
418+
columns at all and is rejected as
419+
`Unsupported schema statement: CREATE TABLE at line <n>`. A form sqlcj's parser
420+
cannot read, such as the column options `PARTITION OF` and `OF <type>` allow
421+
(`id WITH OPTIONS NOT NULL`) or `AS SELECT ... WITH NO DATA`, is rejected as
422+
unreadable table DDL with the parser's reason; see
423+
[Ignored Statements](#ignored-statements).
424+
389425
## Ordered Table DDL
390426

391427
A schema of `CREATE TABLE` statements alone is modeled exactly as it was before
@@ -397,14 +433,16 @@ statements and files before them left; the enum-type statements are listed in
397433
| --- | --- |
398434
| `CREATE TABLE` | Adds the table after the tables already modeled. |
399435
| `CREATE TABLE IF NOT EXISTS` | Does nothing when the table already exists. |
400-
| `DROP TABLE`, with one or several names | Removes each named table. |
436+
| `DROP TABLE`, with one or several names | Removes each named table and, recursively, every partition of it, as PostgreSQL does. |
401437
| `DROP TABLE IF EXISTS` | Does nothing for a name that is not modeled. |
402438
| `ALTER TABLE ... ADD COLUMN` | Appends the column, typed exactly as a `CREATE TABLE` column of the same declaration, and records its column-level `PRIMARY KEY` or `UNIQUE`. |
403439
| `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` | Does nothing when the column already exists. |
404440
| `ALTER TABLE ... DROP COLUMN` | Removes the column and every recorded constraint that lists it. |
405441
| `ALTER TABLE ... DROP COLUMN IF EXISTS` | Does nothing when the column is not modeled. |
406442
| `ALTER TABLE ... RENAME COLUMN` | Renames the column in its position and in the constraints that list it. |
407-
| `ALTER TABLE ... RENAME TO` | Renames the table in its position, keeping its columns and constraints. |
443+
| `ALTER TABLE ... RENAME TO` | Renames the table in its position, keeping its columns and constraints, and keeps every partition of it linked to the new name. |
444+
| `ALTER TABLE ... ATTACH PARTITION` | Links a modeled table to the altered one, so it follows its column changes from then on. A partition sqlcj does not model is ignored. |
445+
| `ALTER TABLE ... DETACH PARTITION` | Ends the link of a modeled partition. A partition sqlcj does not model is ignored. |
408446
| `ALTER TABLE ... ALTER COLUMN ... TYPE` | Maps or records the new type as a `CREATE TABLE` column of that type is mapped or recorded, keeping the column's position and its nullability, which a type change does not state. |
409447
| `ALTER TABLE ... ALTER COLUMN ... SET NOT NULL` | Models the column non-null. |
410448
| `ALTER TABLE ... ALTER COLUMN ... DROP NOT NULL` | Models the column nullable. |
@@ -416,6 +454,16 @@ One `ALTER TABLE` may state several actions; they are applied in the written
416454
order. Table and column names are matched case-insensitively, as query analysis
417455
looks them up, after their SQL identifier delimiters are removed.
418456

457+
PostgreSQL applies every column change of a partitioned table to its partitions,
458+
so a partition created by `PARTITION OF`, or attached by `ATTACH PARTITION` when
459+
sqlcj models its table, stays linked to its parent: `ADD COLUMN`,
460+
`DROP COLUMN`, `RENAME COLUMN`, `ALTER COLUMN ... TYPE`, `SET NOT NULL`, and
461+
`DROP NOT NULL` on the parent are applied to its partitions as well,
462+
recursively, so a nested partition follows too. A partition keeps its own name
463+
when its parent is renamed, `DROP TABLE` of the parent removes it, and
464+
`DETACH PARTITION` ends the link, after which the parent's later column changes
465+
no longer reach it. A `LIKE` copy is never linked to its source.
466+
419467
Outside the `IF [NOT] EXISTS` forms above, a statement that refers to a table or
420468
a column that is not modeled is rejected, and so is a statement that would give
421469
two tables, or two columns of one table, the same name, as PostgreSQL rejects it:
@@ -476,6 +524,7 @@ what they would do to the schema model:
476524
| Statement | Diagnostic |
477525
| --- | --- |
478526
| An `ALTER TABLE` action outside [Ordered Table DDL](#ordered-table-ddl) and the ignored actions below | `Unsupported ALTER TABLE action: <action> at line <n>`, quoting the action's own SQL |
527+
| A `CREATE TABLE ... AS SELECT`, `OF <type>`, or `INHERITS`, whose columns sqlcj cannot determine | `Unsupported CREATE TABLE clause: <clause> at line <n>`, naming `AS SELECT`, `OF <type>`, or `INHERITS` |
479528
| An `ALTER TYPE ... SET SCHEMA` of a modeled enum type | `Unsupported ALTER TYPE action: SET SCHEMA <schema> at line <n>` |
480529
| A statement sqlcj's parser cannot read that opens as table or type DDL, outside the `ALTER TABLE` actions below | The parser's own reason with the line and column of the unexpected token, as in `Encountered unexpected token: "DATA" at line 18, column 41` |
481530
| A statement sqlcj's parser reports only as opaque text, or as several statements, and that opens as table or type DDL | `Unsupported schema statement: ALTER FOREIGN TABLE at line <n>`, quoting the opening words |
@@ -505,7 +554,7 @@ statement still resolves its table:
505554
| `ALTER [COLUMN] c DROP EXPRESSION` | Only the readable spelling; `DROP EXPRESSION IF EXISTS` is unreadable. |
506555
| `ALTER [COLUMN] c ADD GENERATED ... AS IDENTITY`, `SET GENERATED ...`, a sequence option such as `SET INCREMENT BY 2`, `RESTART`, and `DROP IDENTITY [IF EXISTS]` | PostgreSQL requires the column to be `NOT NULL` already, so an identity never changes nullability. |
507556
| `ENABLE`, `DISABLE`, `FORCE`, and `NO FORCE ROW LEVEL SECURITY` | |
508-
| `ATTACH PARTITION` and `DETACH PARTITION` | The partition's own table is modeled by its own statements. |
557+
| `ATTACH PARTITION` and `DETACH PARTITION` of a table sqlcj does not model | A modeled partition is linked or unlinked instead, as [Ordered Table DDL](#ordered-table-ddl) states. |
509558
| `OWNER TO` | |
510559
| `ENABLE TRIGGER`, `DISABLE TRIGGER`, `ENABLE REPLICA TRIGGER`, and `ENABLE ALWAYS TRIGGER`, and the same four for `RULE` | |
511560
| `VALIDATE CONSTRAINT` | |
@@ -701,6 +750,8 @@ and recorded type:
701750
```text
702751
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported schema statement: ALTER FOREIGN TABLE at line 12
703752
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported ALTER TABLE action: SET SCHEMA archive at line 18
753+
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported CREATE TABLE clause: AS SELECT at line 24
754+
sqlcj: Invalid schema source /home/dev/project/schema.sql: Table not found in schema: reporting.events
704755
sqlcj: Invalid schema source /home/dev/project/schema.sql: Encountered unexpected token: ";" at line 4, column 1
705756
sqlcj: Invalid query 'ListTags' in /home/dev/project/queries.sql at line 5: Column 'tags' has unsupported type JSONB[]
706757
```
@@ -857,6 +908,29 @@ Enum types:
857908
- `DefaultSchemaParserTest.shouldRejectUnsupportedSchemaStatement` and
858909
`DefaultSchemaParserTest.shouldThrowSchemaParseExceptionForInvalidSql` cover
859910
the rejected input.
911+
- `DefaultSchemaParserTest.shouldCopyTheColumnsOfALikeSource` covers a `LIKE`
912+
alone with its options and a `public`-qualified source,
913+
`DefaultSchemaParserTest.shouldModelTheColumnsOfAnElementListInWrittenOrder`
914+
covers a `LIKE` written before and after a column definition and two `LIKE`
915+
clauses, and
916+
`DefaultSchemaParserTest.shouldRecordOnlyTheOwnConstraintsOfALikeTable`
917+
covers the source constraints that are not copied beside the copy's own.
918+
- `DefaultSchemaParserTest.shouldCopyTheColumnsOfAPartitionParent` covers the
919+
range, list, hash, and `DEFAULT` bounds and the `public`-qualified parent,
920+
`DefaultSchemaParserTest.shouldRecordTheTableConstraintsOfAPartition` covers
921+
the constraint list a `PARTITION OF` may state, and
922+
`DefaultSchemaParserTest.shouldModelAnEmptyElementListAsATableWithoutColumns`
923+
covers `()`.
924+
- `DefaultSchemaParserTest.shouldReportARepeatedColumnNameOfALikeSource`,
925+
`DefaultSchemaParserTest.shouldReportTheMissingSourceOfACreatedTable`,
926+
`DefaultSchemaParserTest.shouldRejectTheCreateTableClausesWhoseColumnsItCannotDetermine`,
927+
and `DefaultSchemaParserTest.shouldRejectASourceOrPartitionOfAnotherSchema`
928+
cover the repeated column name, the missing source, each rejected clause at
929+
the line of its own statement, and the interim rule for a source and a
930+
partition qualified with another schema.
931+
- `SqlcjCompilerIntegrationTest.shouldGenerateCompilableRepositoriesOverCopiedTableColumns`
932+
generates and compiles `SELECT *` row records and a repository over a `LIKE`
933+
table and a `PARTITION OF` table.
860934
- `PostgresIntegrationTest.shouldExecuteGeneratedQueryForSnapshotWithIgnoredTableConstraints`
861935
proves that such a snapshot is valid PostgreSQL DDL and compiles and executes.
862936

@@ -882,6 +956,19 @@ Ordered table DDL:
882956
`DefaultSchemaParserTest.shouldIgnoreDropColumnIfExistsForAMissingColumn`, and
883957
`DefaultSchemaParserTest.shouldReportTheMissingColumnOfAnAlterTableIfExists`
884958
cover the `IF [NOT] EXISTS` variants.
959+
- `DefaultSchemaParserTest.shouldApplyEveryColumnActionOfAParentToItsPartition`
960+
and `DefaultSchemaParserTest.shouldApplyAColumnActionToANestedPartition`
961+
cover each propagated column action and a nested partition;
962+
`DefaultSchemaParserTest.shouldKeepThePartitionLinkThroughARenameOfTheParent`
963+
and `DefaultSchemaParserTest.shouldDropThePartitionsOfADroppedParent` cover
964+
the parent's rename and `DROP TABLE`, and
965+
`DefaultSchemaParserTest.shouldDropAParentNamedBeforeItsPartition` covers the
966+
one `DROP TABLE` that names a parent before one of its partitions;
967+
`DefaultSchemaParserTest.shouldStopPropagationAfterDetachPartition` and
968+
`DefaultSchemaParserTest.shouldStartPropagationAfterAttachPartition` cover the
969+
ended and the started link; and
970+
`DefaultSchemaParserTest.shouldNotFollowTheSourceOfALikeCopy` covers the
971+
`LIKE` copy that stays independent of its source.
885972
- `DefaultSchemaParserTest.shouldReportTheMissingTableOfAStatement`,
886973
`DefaultSchemaParserTest.shouldReportTheMissingColumnOfAnAlterTableAction`,
887974
`DefaultSchemaParserTest.shouldReportAStatementThatRepeatsATableName`,
@@ -908,8 +995,9 @@ Ignored statements:
908995
named and unnamed constraint actions.
909996
- `DefaultSchemaParserTest.shouldIgnoreTheAlterTableActionsThatCannotChangeAColumn`
910997
covers one spelling per ignored `ALTER TABLE` action, including the lower-case
911-
and `ALTER TABLE ONLY public.users` spellings and two actions classified by
912-
their words in one statement.
998+
and `ALTER TABLE ONLY public.users` spellings, the unqualified and
999+
`public`-qualified `ATTACH` and `DETACH PARTITION` of a partition sqlcj does
1000+
not model, and two actions classified by their words in one statement.
9131001
- `DefaultSchemaParserTest.shouldNotResolveAnIgnoredStatementAgainstTheSchema`
9141002
covers that an ignored statement, including a view over an unmodeled table and
9151003
a `TRUNCATE` of one, may name an unmodeled table or column;

‎sqlcj-cli/src/main/java/dev/sqlcj/schema/Table.java‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,26 @@
22

33
import java.util.List;
44

5+
/**
6+
* One modeled table. {@code partitionOf} names the partitioned table this one
7+
* is a partition of, or is {@code null} when the table is not a partition,
8+
* because PostgreSQL applies every column change of a partitioned table to its
9+
* partitions.
10+
*/
511
public record Table(
612
String name,
713
List<Column> columns,
8-
List<Constraint> constraints
14+
List<Constraint> constraints,
15+
String partitionOf
916
) {
1017

1118
public Table {
1219
columns = List.copyOf(columns);
1320
constraints = List.copyOf(constraints);
1421
}
22+
23+
/** A table that is not a partition of another table. */
24+
public Table(String name, List<Column> columns, List<Constraint> constraints) {
25+
this(name, columns, constraints, null);
26+
}
1527
}

0 commit comments

Comments
 (0)