Skip to content

Commit 5add590

Browse files
committed
feat(schema): split schema sources so unreadable statements fail alone
Split each schema source at the statement separators JSqlParser's own token manager reports and parse every statement alone. A tagged dollar-quoted body ends at the next occurrence of its delimiter, and psql meta-command lines such as \restrict are blanked before lexing. A statement the parser cannot read is ignored unless it opens as table or type DDL. An unreadable ALTER TABLE whose every top-level action adds a constraint or an identity is ignored without resolving its table, which lets pg_dump identity columns load. Any other unreadable table or type statement is rejected with the parser's reason at its file line and column. Functions and procedures with bodies that are not dollar-quoted are now ignored.
1 parent aa3475e commit 5add590

4 files changed

Lines changed: 921 additions & 218 deletions

File tree

‎docs/postgresql.md‎

Lines changed: 112 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ left:
249249
| `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. |
250250
| Any other `ALTER TYPE` action, such as `RENAME TO`, `RENAME VALUE`, `OWNER TO`, `SET SCHEMA`, or an attribute change | Rejected as `Unsupported ALTER TYPE action at line <n>`. |
251251
| `CREATE TYPE` of a composite, range, or shell type | Ignored. A column of such a type is recorded with its declared type, as [Unsupported Types and DDL](#unsupported-types-and-ddl) describes. |
252-
| `DROP TYPE` | Rejected. sqlcj's parser does not read the statement, so it is reported as a syntax error; see [Ignored Statements](#ignored-statements). |
252+
| `DROP TYPE` | Ignored. sqlcj's parser cannot read the statement, and an unreadable statement that does not open as table or type DDL is ignored, so an enum the schema models stays modeled with its labels and the columns of that type stay modeled; see [Ignored Statements](#ignored-statements). |
253253

254254
Type names are matched case-insensitively, after their SQL identifier
255255
delimiters are removed, and labels are matched exactly. A statement that repeats
@@ -428,24 +428,30 @@ forms of [Ordered Table DDL](#ordered-table-ddl) and the
428428
including
429429

430430
- `CREATE INDEX`, `CREATE UNIQUE INDEX`, `ALTER INDEX`, and `DROP INDEX`,
431-
- `COMMENT ON TABLE`, `COMMENT ON COLUMN`, and `COMMENT ON VIEW`, the only
432-
`COMMENT ON` targets sqlcj's parser reads; see below,
431+
- `COMMENT ON` of any target, including `TABLE`, `COLUMN`, `VIEW`, `TYPE`,
432+
`INDEX`, `FUNCTION`, `SCHEMA`, `EXTENSION`, and `CONSTRAINT`,
433433
- `CREATE EXTENSION`, `CREATE SEQUENCE`, `ALTER SEQUENCE`, and `DROP SEQUENCE`,
434434
- `GRANT`, `REVOKE`, and `ALTER DEFAULT PRIVILEGES`,
435435
- `CREATE FUNCTION`, `CREATE PROCEDURE`, `DROP FUNCTION`, `CREATE TRIGGER`, and
436-
`DROP TRIGGER`; a function or procedure body must be dollar-quoted, as below,
436+
`DROP TRIGGER`, with a body written any way: the untagged `$$ ... $$`, a
437+
tagged `$body$ ... $body$`, a single-quoted `AS 'SELECT 1'`, or `RETURN 1`,
438+
- `DO` blocks, with an untagged or a tagged body and with or without a stated
439+
`LANGUAGE`,
437440
- `INSERT`, `UPDATE`, `DELETE`, and `TRUNCATE`, a migration's data statements,
438441
- `CREATE VIEW`, `DROP VIEW`, and the `CREATE`, `REFRESH`, and `DROP` forms of
439442
`MATERIALIZED VIEW`,
440443
- `CREATE SCHEMA` and `DROP SCHEMA`,
441-
- `SET`, `RESET`, `ANALYZE`, `COMMIT`, and a plain `SELECT`, such as the
444+
- `SET`, including `SET search_path TO a, b`, `RESET`, `ANALYZE`, `VACUUM`,
445+
`LOCK TABLE`, `BEGIN`, `COMMIT`, `COPY`, and a plain `SELECT`, such as the
442446
`SELECT pg_catalog.set_config(...)` a `pg_dump` snapshot opens with,
443-
- `CREATE POLICY`, `CREATE ROLE`, `CREATE USER`, and `ALTER ROLE`,
444-
- `CREATE DOMAIN` and `ALTER DOMAIN`,
445-
- a `CREATE TYPE` of a composite, range, or shell type, and
446-
- a statement sqlcj's parser reports only as opaque text and that does not open
447-
as table or type DDL, such as `ALTER FUNCTION`, `ALTER SCHEMA`,
448-
`CREATE AGGREGATE`, or `CREATE CAST`.
447+
- `CREATE POLICY`, `DROP POLICY`, `CREATE ROLE`, `CREATE USER`, and
448+
`ALTER ROLE`,
449+
- `CREATE DOMAIN`, `ALTER DOMAIN`, and `DROP DOMAIN`,
450+
- `CREATE RULE` and `CREATE EVENT TRIGGER`,
451+
- a `CREATE TYPE` of a composite, range, or shell type, and `DROP TYPE`, and
452+
- a statement sqlcj's parser cannot read, or reports only as opaque text, and
453+
that does not open as table or type DDL, such as `ALTER FUNCTION`,
454+
`ALTER SCHEMA`, `CREATE AGGREGATE`, or `CREATE CAST`.
449455

450456
An ignored statement is not resolved against the schema at all, so it may name a
451457
table or a column the snapshot does not model: a view over an unmodeled table
@@ -458,14 +464,14 @@ what they would do to the schema model:
458464
| --- | --- |
459465
| An `ALTER TABLE` action outside [Ordered Table DDL](#ordered-table-ddl) and the constraint actions below | `Unsupported ALTER TABLE action at line <n>` |
460466
| An `ALTER TYPE` action other than `ADD VALUE` | `Unsupported ALTER TYPE action at line <n>` |
461-
| A `CREATE FUNCTION` or `CREATE PROCEDURE` whose body is not delimited by the untagged `$$ ... $$` | `Unsupported schema statement: CREATE FUNCTION without a dollar-quoted body at line <n>`, with `CREATE PROCEDURE` for a procedure |
462-
| A statement sqlcj's parser reports only as opaque text and that opens as table or type DDL | `Unsupported schema statement: ALTER FOREIGN TABLE at line <n>`, quoting the opening words |
463-
464-
An opaque statement opens as table DDL when its first words are `CREATE`,
465-
`ALTER`, or `DROP`, then any of `GLOBAL`, `LOCAL`, `TEMP`, `TEMPORARY`,
466-
`UNLOGGED`, or `FOREIGN`, then `TABLE`, and as type DDL when they are `CREATE`
467-
or `ALTER`, then `TYPE`. The words are matched case-insensitively and the
468-
diagnostic quotes them in upper case, single-spaced, so both
467+
| 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` |
468+
| 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 |
469+
470+
A statement opens as table DDL when its first words are `CREATE`, `ALTER`, or
471+
`DROP`, then any of `GLOBAL`, `LOCAL`, `TEMP`, `TEMPORARY`, `UNLOGGED`, or
472+
`FOREIGN`, then `TABLE`, and as type DDL when they are `CREATE` or `ALTER`, then
473+
`TYPE`. The words are matched case-insensitively and the diagnostic quotes them
474+
in upper case, single-spaced, so both
469475
`ALTER FOREIGN TABLE ft ADD COLUMN x integer` and its lower-case spelling are
470476
rejected as `Unsupported schema statement: ALTER FOREIGN TABLE at line <n>`.
471477

@@ -477,7 +483,7 @@ except that the statement still resolves its table:
477483
| `ALTER TABLE ... ADD [CONSTRAINT name] PRIMARY KEY (...)` | Named or unnamed. |
478484
| `ALTER TABLE ... ADD [CONSTRAINT name] UNIQUE (...)` | Named or unnamed. |
479485
| `ALTER TABLE ... ADD [CONSTRAINT name] FOREIGN KEY (...) REFERENCES ...` | Named or unnamed. |
480-
| `ALTER TABLE ... ADD CONSTRAINT name CHECK (...)` | The unnamed `ADD CHECK (...)` spelling is a syntax error for sqlcj's parser; see below. |
486+
| `ALTER TABLE ... ADD CONSTRAINT name CHECK (...)` | |
481487
| `ALTER TABLE ... DROP CONSTRAINT [IF EXISTS] name` | |
482488
| `ALTER TABLE ... RENAME CONSTRAINT` | |
483489

@@ -491,36 +497,54 @@ table does. Such an action may stand alone or beside modeled actions of one
491497
`ALTER TABLE users ADD COLUMN age INTEGER, ADD CONSTRAINT users_age_check CHECK (age > 0)`
492498
appends `age` and records nothing for the constraint.
493499

494-
Three limitations follow from what sqlcj's SQL parser reads, and sqlcj does not
495-
split or pre-process the SQL to work around them:
496-
497-
- A `CREATE FUNCTION` or `CREATE PROCEDURE` is ignored only when its body is
498-
delimited by the untagged `$$ ... $$`, which is the only dollar-quote
499-
delimiter sqlcj's parser reads. A tagged delimiter such as
500-
`$body$ ... $body$` is not read at all: it is a syntax error, reported for the
501-
closing delimiter, as in
502-
`Encountered unexpected token: "$body$" at line 1, column 74`. A body written
503-
any other way, such as `AS 'SELECT 1' LANGUAGE sql` or `RETURN 1`, is captured
504-
through the end of the file, so such a function or procedure is rejected at
505-
the line it begins on and nothing after it is applied.
506-
- `COMMENT ON` is read only for the `TABLE`, `COLUMN`, and `VIEW` targets. Any
507-
other target, such as `COMMENT ON TYPE`, is a syntax error rather than an
508-
ignored statement.
509-
- `ALTER TABLE ... ADD CHECK (...)` without a constraint name is a syntax
510-
error. The named `ADD CONSTRAINT name CHECK (...)` spelling is ignored as
511-
listed above.
500+
An `ALTER TABLE` sqlcj's parser cannot read is classified by its words instead,
501+
and ignored when every one of its top-level actions — the parts separated by
502+
the commas that stand outside parentheses — begins with one of
503+
504+
- `ADD CONSTRAINT <name>`,
505+
- `ADD CHECK`, `ADD UNIQUE`, or `ADD EXCLUDE`,
506+
- `ADD PRIMARY KEY` or `ADD FOREIGN KEY`, or
507+
- `ALTER [COLUMN] <name> ADD GENERATED`.
508+
509+
PostgreSQL 16 writes `ALTER TABLE ONLY public.t ALTER COLUMN id ADD GENERATED
510+
... AS IDENTITY (SEQUENCE NAME ...)` for every identity column of a
511+
`pg_dump --schema-only` snapshot, and sqlcj's parser reads neither that form nor
512+
an `EXCLUDE`, `DEFERRABLE INITIALLY DEFERRED`, `UNIQUE USING INDEX`,
513+
`PRIMARY KEY USING INDEX`, or `NOT VALID` constraint, nor an unnamed
514+
`ADD CHECK`. None of them can change a modeled column's existence, name, type,
515+
or nullability, which comes from `NOT NULL` alone. Such a statement is ignored
516+
without its table being resolved, like every other ignored statement, so it also
517+
loads for a table the snapshot does not model. Every other unreadable
518+
`ALTER TABLE`, including `ALTER COLUMN ... SET DATA TYPE` and an ignored action
519+
written beside it, stays rejected with the parser's reason.
520+
521+
sqlcj splits a schema file at the statement separators its own SQL lexer
522+
reports and parses each statement alone, so a statement the parser cannot read
523+
fails only itself:
524+
525+
- A separator inside a quoted string, a quoted identifier, a comment, or the
526+
untagged `$$ ... $$` literal is not a boundary. A tagged dollar-quoted body
527+
ends at the next occurrence of its own opening delimiter, as PostgreSQL ends
528+
it, whether that delimiter stands alone or is glued to the body as in
529+
`$fn$BEGIN ... END$fn$`. A body that is never closed fails the source as
530+
`Unterminated dollar-quoted string at line <n>, column <c>`.
531+
- A psql meta-command line, a line whose first non-blank character is a
532+
backslash, such as the `\restrict` and `\unrestrict` lines recent `pg_dump`
533+
releases write, is skipped. Every line and column a diagnostic reports is a
534+
position in the file as it was written.
535+
- An escape string that quotes a single quote with a backslash, such as
536+
`E'it\'s'`, is mis-lexed by sqlcj's SQL lexer and fails the whole file.
537+
Writing the doubled `E'it''s'`, or the ordinary `'it''s'`, avoids it.
512538

513539
sqlcj reads only the statements a schema file states, so a schema change a
514540
statement makes indirectly is never seen:
515541

516-
- DDL inside a function or procedure body belongs to that body, which is one
517-
literal to sqlcj's parser, so a table or a type the body creates or alters is
518-
not modeled. The same holds for DDL inside a `DO` block, which sqlcj's parser
519-
does not read at all: a `DO` statement is reported as a syntax error.
542+
- DDL inside a function or procedure body, or inside a `DO` block, belongs to
543+
that body, so a table or a type the body creates, alters, or drops is not
544+
modeled.
520545
- `DROP TYPE ... CASCADE` drops every column of the dropped type, and sqlcj
521-
never models those columns as dropped. sqlcj's parser does not read
522-
`DROP TYPE` at all either, so such a statement is reported as a syntax error;
523-
see [Enum Types](#enum-types).
546+
never models those columns as dropped; `DROP TYPE` itself is ignored, so the
547+
enum stays modeled with its labels; see [Enum Types](#enum-types).
524548

525549
## Nulls
526550

@@ -749,7 +773,8 @@ Enum types:
749773
`DefaultSchemaParserTest.shouldRecordAColumnOfAnIgnoredCompositeType` cover
750774
the ignored composite, range, and shell `CREATE TYPE` and the column a
751775
composite type leaves recorded, and
752-
`DefaultSchemaParserTest.shouldRejectDropType` covers `DROP TYPE`.
776+
`DefaultSchemaParserTest.shouldIgnoreDropTypeAndKeepTheModeledEnum` covers the
777+
ignored `DROP TYPE` and the enum and enum column that survive it.
753778
- `QueryAnalyzerTest.shouldCarryTheEnumTypeOfASelectedColumnAndItsParameter`,
754779
`QueryAnalyzerTest.shouldCarryTheEnumTypeOfAReturningColumnAndItsParameter`,
755780
`QueryAnalyzerTest.shouldAcceptARepeatedIndexOfOneEnumType`, and
@@ -842,17 +867,48 @@ Ignored statements:
842867
covers that an ignored `ALTER TABLE` action still resolves its table.
843868
- `DefaultSchemaParserTest.shouldApplyAModeledActionBesideAnIgnoredConstraintAction`
844869
covers an ignored action beside a modeled one.
870+
- `DefaultSchemaParserTest.shouldIgnoreEveryUnreadableStatementThatIsNotTableOrTypeDdl`
871+
covers one spelling per statement kind sqlcj's parser cannot read, including
872+
the untagged, tagged, and `LANGUAGE` forms of `DO`, `DROP TYPE`, every
873+
`COMMENT ON` target the parser does not read, `SET search_path TO a, b`,
874+
`DROP POLICY`, `DROP DOMAIN`, `VACUUM`, `LOCK TABLE`, `BEGIN`, `COPY ... TO`,
875+
`CREATE RULE`, `CREATE EVENT TRIGGER`, and the single-quoted, `RETURN`, and
876+
tagged function and procedure bodies.
877+
- `DefaultSchemaParserTest.shouldIgnoreTheUnreadableAlterTableActionsThatCannotChangeAColumn`
878+
and
879+
`DefaultSchemaParserTest.shouldIgnoreThePgDumpIdentityAlterTableAction` cover
880+
every ignored unreadable action, alone and two in one statement, on a table
881+
the schema does not model, including the multi-line `pg_dump` identity form.
845882
- `DefaultSchemaParserTest.shouldRejectUnsupportedStatementsInSqlTerms` covers
846-
the exact message and line of each rejected statement and action, including
847-
the single-quoted function and procedure bodies and the upper- and lower-case
848-
`ALTER FOREIGN TABLE`, and
849-
`DefaultSchemaParserTest.shouldRejectUnsupportedSchemaStatement` and
850-
`DefaultSchemaParserTest.shouldReportTheLineOfARejectedStatementAfterCommentsAndAFunctionBody`
851-
cover the reported line after comments that contain a statement separator and
852-
after a multi-line dollar-quoted body.
853-
- `DefaultSchemaParserTest.shouldRejectASingleQuotedFunctionBodyThatCapturesALaterDollarQuotedBody`
854-
covers that a single-quoted body is rejected at its own line even when it
855-
captures a later dollar-quoted body.
883+
the exact message and line of each statement and action rejected in SQL terms,
884+
including the upper- and lower-case `ALTER FOREIGN TABLE`, and
885+
`DefaultSchemaParserTest.shouldRejectUnsupportedSchemaStatement`,
886+
`DefaultSchemaParserTest.shouldReportTheLineOfARejectedStatementAfterCommentsAndAFunctionBody`,
887+
and
888+
`DefaultSchemaParserTest.shouldReportTheLineOfARejectedStatementAfterUnreadableOnesAndATaggedBody`
889+
cover the reported line after comments that contain a statement separator,
890+
after a dollar-quoted body, and after a blanked meta-command line and
891+
unreadable statements.
892+
- `DefaultSchemaParserTest.shouldRejectAnUnreadableAlterColumnActionAtItsFileLineAndColumn`,
893+
`DefaultSchemaParserTest.shouldRejectAnUnreadableAlterTableActionBesideAnIgnoredOne`,
894+
and
895+
`DefaultSchemaParserTest.shouldRejectAnUnreadableCreateTypeAtItsFileLineAndColumn`
896+
cover the parser's reason at the file line and column of `SET DATA TYPE`, of
897+
`SET DATA TYPE` beside an ignored action in a statement that begins in the
898+
middle of a line, and of an unreadable `CREATE TYPE`.
899+
- `DefaultSchemaParserTest.shouldApplyTheStatementsAroundUnreadableOnesAndNotModelDdlInsideABody`
900+
covers the statements applied before and after unreadable ones and the DDL of
901+
an untagged, a tagged, and a glued `$fn$BEGIN ... END$fn$` body that is not
902+
modeled;
903+
`DefaultSchemaParserTest.shouldApplyTheStatementsAfterAFunctionBodyThatIsNotDollarQuoted`
904+
covers the statements after a single-quoted body.
905+
- `DefaultSchemaParserTest.shouldSkipPsqlMetaCommandLines` covers a schema source
906+
with `\restrict` and `\unrestrict` lines, and
907+
`DefaultSchemaParserTest.shouldRejectAnUnterminatedDollarQuotedBody` covers the
908+
located failure of a tagged body that is never closed.
909+
- `DefaultSchemaParserTest.shouldLoadTheMigrationFilesInOrder` covers a migration
910+
history that states `COMMENT ON TYPE` beside the targets the parser reads,
911+
loaded unchanged.
856912
- `SqlcjCompilerIntegrationTest.shouldReportTheMigrationFileAndLineOfAnUnsupportedStatement`
857913
covers the migration file, the SQL-term message, and the line of a rejected
858914
statement that follows an ignored index and an ignored view, and that no file

0 commit comments

Comments
 (0)