Skip to content

Commit aa3475e

Browse files
authored
Merge pull request #63 from sqlcj-dev/feature/one-rule-for-ignored-schema-statements
feat(schema): ignore unmodeled statements and report errors in SQL terms
2 parents bfd499f + d007daa commit aa3475e

4 files changed

Lines changed: 346 additions & 170 deletions

File tree

‎docs/postgresql.md‎

Lines changed: 121 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,13 @@ file, a list of files, or a directory of `.sql` migration files; see
9292
[`sql[].schema`](configuration.md#sqlschema) for the accepted forms and the
9393
order the files are read in.
9494

95-
- Only `CREATE TABLE`, `DROP TABLE`, and the `ALTER TABLE` forms listed in
96-
[Ordered Table DDL](#ordered-table-ddl) update the schema model. The
97-
statements listed in [Ignored Statements](#ignored-statements) are accepted
98-
and leave it unchanged. Any other statement in a schema file is rejected.
95+
- One rule decides every statement: only the `CREATE TABLE`, `DROP TABLE`, and
96+
`ALTER TABLE` forms listed in [Ordered Table DDL](#ordered-table-ddl) and the
97+
`CREATE TYPE ... AS ENUM` and `ALTER TYPE ... ADD VALUE` forms listed in
98+
[Enum Types](#enum-types) update the schema model, and every other statement
99+
a schema file records is accepted and leaves it unchanged. Only the few
100+
statements listed in [Ignored Statements](#ignored-statements) are rejected
101+
instead, because sqlcj cannot tell what they would do to the model.
99102
- The statements of the schema files of one entry are applied in order to one
100103
schema model, so each of them sees the tables and columns the statements and
101104
files before it left.
@@ -244,9 +247,9 @@ left:
244247
| `ALTER TYPE ... ADD VALUE '<label>' BEFORE '<neighbour>'` | Inserts the label directly before the neighbour. |
245248
| `ALTER TYPE ... ADD VALUE '<label>' AFTER '<neighbour>'` | Inserts the label directly after the neighbour. |
246249
| `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. |
247-
| Any other `ALTER TYPE` action, such as `RENAME TO`, `RENAME VALUE`, `OWNER TO`, `SET SCHEMA`, or an attribute change | Rejected. |
248-
| `CREATE TYPE` of a composite, range, or shell type | Rejected. |
249-
| `DROP TYPE` | Rejected. sqlcj's parser does not read the statement, so it is reported as a syntax error. |
250+
| 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>`. |
251+
| `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). |
250253

251254
Type names are matched case-insensitively, after their SQL identifier
252255
delimiters are removed, and labels are matched exactly. A statement that repeats
@@ -359,7 +362,7 @@ are recorded with their declared type; see
359362
| Column-level `CHECK (...)` | Accepted and ignored. |
360363
| Table-level `FOREIGN KEY (...) REFERENCES ...`, named or unnamed | Accepted and ignored. |
361364
| Table-level `CHECK (...)`, named or unnamed | Accepted and ignored. |
362-
| Any statement outside [Ordered Table DDL](#ordered-table-ddl) and [Ignored Statements](#ignored-statements), such as `CREATE VIEW` | Rejected. |
365+
| A table statement sqlcj's parser cannot read, such as `ALTER FOREIGN TABLE` | Rejected; see [Ignored Statements](#ignored-statements). |
363366
| Any other table-constraint kind | Rejected. |
364367
| Unparsable SQL | Rejected. |
365368

@@ -392,8 +395,8 @@ statements and files before them left; the enum-type statements are listed in
392395
| `ALTER TABLE ... ALTER COLUMN ... SET NOT NULL` | Models the column non-null. |
393396
| `ALTER TABLE ... ALTER COLUMN ... DROP NOT NULL` | Models the column nullable. |
394397
| `ALTER TABLE IF EXISTS ...` | Does nothing when the table is not modeled. It covers only the table, so a missing column of a modeled table still fails. |
395-
| Any other `ALTER TABLE` action outside [Ignored Statements](#ignored-statements), such as `SET DEFAULT` | Rejected. |
396-
| `DROP` of an object that is neither a table nor listed in [Ignored Statements](#ignored-statements), such as `DROP VIEW` | Rejected. |
398+
| Any other `ALTER TABLE` action outside the constraint actions listed in [Ignored Statements](#ignored-statements), such as `SET DEFAULT` | Rejected as `Unsupported ALTER TABLE action at line <n>`. |
399+
| `DROP` of any object other than a table, such as `DROP VIEW` or `DROP INDEX` | Ignored; see [Ignored Statements](#ignored-statements). |
397400

398401
One `ALTER TABLE` may state several actions; they are applied in the written
399402
order. Table and column names are matched case-insensitively, as query analysis
@@ -413,22 +416,64 @@ sqlcj: Invalid schema source /home/dev/project/sql/migrations/V2__orders.sql: Co
413416
## Ignored Statements
414417

415418
A schema file records the whole history of a database, not only its tables, so
416-
these statements are accepted and leave the schema model exactly as the
417-
statements and files before them left it:
418-
419-
| Statement | Notes |
419+
one rule decides every statement it states: a statement sqlcj models is applied,
420+
and every other statement is accepted and leaves the schema model exactly as the
421+
statements and files before it left it. There is no list of accepted kinds to
422+
keep up to date.
423+
424+
The modeled statements are the `CREATE TABLE`, `DROP TABLE`, and `ALTER TABLE`
425+
forms of [Ordered Table DDL](#ordered-table-ddl) and the
426+
`CREATE TYPE ... AS ENUM` and `ALTER TYPE ... ADD VALUE` forms of
427+
[Enum Types](#enum-types). Everything else a migration history holds is ignored,
428+
including
429+
430+
- `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,
433+
- `CREATE EXTENSION`, `CREATE SEQUENCE`, `ALTER SEQUENCE`, and `DROP SEQUENCE`,
434+
- `GRANT`, `REVOKE`, and `ALTER DEFAULT PRIVILEGES`,
435+
- `CREATE FUNCTION`, `CREATE PROCEDURE`, `DROP FUNCTION`, `CREATE TRIGGER`, and
436+
`DROP TRIGGER`; a function or procedure body must be dollar-quoted, as below,
437+
- `INSERT`, `UPDATE`, `DELETE`, and `TRUNCATE`, a migration's data statements,
438+
- `CREATE VIEW`, `DROP VIEW`, and the `CREATE`, `REFRESH`, and `DROP` forms of
439+
`MATERIALIZED VIEW`,
440+
- `CREATE SCHEMA` and `DROP SCHEMA`,
441+
- `SET`, `RESET`, `ANALYZE`, `COMMIT`, and a plain `SELECT`, such as the
442+
`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`.
449+
450+
An ignored statement is not resolved against the schema at all, so it may name a
451+
table or a column the snapshot does not model: a view over an unmodeled table
452+
and a `TRUNCATE` of one are both accepted.
453+
454+
These statements are rejected rather than ignored, because sqlcj cannot tell
455+
what they would do to the schema model:
456+
457+
| Statement | Diagnostic |
458+
| --- | --- |
459+
| An `ALTER TABLE` action outside [Ordered Table DDL](#ordered-table-ddl) and the constraint actions below | `Unsupported ALTER TABLE action at line <n>` |
460+
| 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
469+
`ALTER FOREIGN TABLE ft ADD COLUMN x integer` and its lower-case spelling are
470+
rejected as `Unsupported schema statement: ALTER FOREIGN TABLE at line <n>`.
471+
472+
These `ALTER TABLE` actions are ignored like any other unmodeled statement,
473+
except that the statement still resolves its table:
474+
475+
| Action | Notes |
420476
| --- | --- |
421-
| `CREATE INDEX`, `CREATE UNIQUE INDEX` | |
422-
| `ALTER INDEX` | |
423-
| `DROP INDEX`, `DROP INDEX IF EXISTS` | |
424-
| `COMMENT ON TABLE`, `COMMENT ON COLUMN`, `COMMENT ON VIEW` | These are the only `COMMENT ON` targets sqlcj's parser reads; see below. |
425-
| `CREATE EXTENSION`, `CREATE EXTENSION IF NOT EXISTS` | |
426-
| `CREATE SEQUENCE`, `ALTER SEQUENCE`, `DROP SEQUENCE` | |
427-
| `GRANT`, `REVOKE` | |
428-
| `CREATE FUNCTION`, `CREATE OR REPLACE FUNCTION` | Only with a body delimited by the untagged `$$ ... $$`, such as `AS $$ ... $$`; see below. |
429-
| `DROP FUNCTION`, `DROP FUNCTION IF EXISTS` | |
430-
| `CREATE TRIGGER`, `DROP TRIGGER` | |
431-
| `INSERT`, `UPDATE`, `DELETE` | A migration's data statements do not change the modeled tables. |
432477
| `ALTER TABLE ... ADD [CONSTRAINT name] PRIMARY KEY (...)` | Named or unnamed. |
433478
| `ALTER TABLE ... ADD [CONSTRAINT name] UNIQUE (...)` | Named or unnamed. |
434479
| `ALTER TABLE ... ADD [CONSTRAINT name] FOREIGN KEY (...) REFERENCES ...` | Named or unnamed. |
@@ -437,36 +482,46 @@ statements and files before them left it:
437482
| `ALTER TABLE ... RENAME CONSTRAINT` | |
438483

439484
A constraint an `ALTER TABLE` adds is not recorded, so it is not carried into
440-
the schema model the way a `CREATE TABLE` constraint is.
441-
442-
An ignored statement is not resolved against the schema at all, so it may name
443-
a table or a column the snapshot does not model. An ignored `ALTER TABLE`
444-
action is the one exception: the statement still resolves its table, so
485+
the schema model the way a `CREATE TABLE` constraint is. Because the table is
486+
still resolved,
445487
`ALTER TABLE payments ADD CONSTRAINT payments_pkey PRIMARY KEY (id)` fails when
446488
`payments` is not modeled, exactly as any other `ALTER TABLE` of a missing
447-
table does. An ignored action may stand alone or beside modeled actions of one
489+
table does. Such an action may stand alone or beside modeled actions of one
448490
`ALTER TABLE`, so
449491
`ALTER TABLE users ADD COLUMN age INTEGER, ADD CONSTRAINT users_age_check CHECK (age > 0)`
450492
appends `age` and records nothing for the constraint.
451493

452494
Three limitations follow from what sqlcj's SQL parser reads, and sqlcj does not
453495
split or pre-process the SQL to work around them:
454496

455-
- A `CREATE FUNCTION` is ignored only when its body is delimited by the untagged
456-
`$$ ... $$`, which is the only dollar-quote delimiter sqlcj's parser reads. A
457-
tagged delimiter such as `$body$ ... $body$` is not read at all: it is a
458-
syntax error, reported for the closing delimiter, as in
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
459502
`Encountered unexpected token: "$body$" at line 1, column 74`. A body written
460-
any other way, such as `AS 'SELECT 1' LANGUAGE sql`, is captured through the
461-
end of the file, so such a function is rejected at the line it begins on and
462-
nothing after it is applied.
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.
463506
- `COMMENT ON` is read only for the `TABLE`, `COLUMN`, and `VIEW` targets. Any
464507
other target, such as `COMMENT ON TYPE`, is a syntax error rather than an
465508
ignored statement.
466509
- `ALTER TABLE ... ADD CHECK (...)` without a constraint name is a syntax
467510
error. The named `ADD CONSTRAINT name CHECK (...)` spelling is ignored as
468511
listed above.
469512

513+
sqlcj reads only the statements a schema file states, so a schema change a
514+
statement makes indirectly is never seen:
515+
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.
520+
- `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).
524+
470525
## Nulls
471526

472527
Every generated method parameter and every generated result component uses a
@@ -567,13 +622,15 @@ written by an earlier successful run is left unchanged. The writing step itself
567622
is sequential rather than atomic; see
568623
[Generated Output and Failures](configuration.md#generated-output-and-failures).
569624

570-
A schema message names the schema source and the offending statement with the
571-
line it begins on, the table or column the statement refers to, or the syntax
572-
error with the line and column it was found at. A query message names the
573-
query, its source, its header line, and the offending column and recorded type:
625+
A schema message names the schema source and the offending statement or action
626+
in SQL terms with the line it begins on, the table or column the statement
627+
refers to, or the syntax error with the line and column it was found at. A query
628+
message names the query, its source, its header line, and the offending column
629+
and recorded type:
574630

575631
```text
576-
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported schema statement: CreateView at line 12
632+
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported schema statement: ALTER FOREIGN TABLE at line 12
633+
sqlcj: Invalid schema source /home/dev/project/schema.sql: Unsupported ALTER TABLE action at line 18
577634
sqlcj: Invalid schema source /home/dev/project/schema.sql: Encountered unexpected token: ";" at line 4, column 1
578635
sqlcj: Invalid query 'ListTags' in /home/dev/project/queries.sql at line 5: Column 'tags' has unsupported type JSONB[]
579636
```
@@ -686,8 +743,12 @@ Enum types:
686743
`DefaultSchemaParserTest.shouldModelEnumArraysAndRecordUndeclaredTypesAsUnsupported`
687744
covers the modeled enum array beside the multidimensional enum array and the
688745
undeclared type that stay recorded.
689-
- `DefaultSchemaParserTest.shouldRejectStatementOutsideTheIgnoredList` covers
690-
the rejected `ALTER TYPE` actions and the composite `CREATE TYPE`, and
746+
- `DefaultSchemaParserTest.shouldRejectUnsupportedStatementsInSqlTerms` covers
747+
the rejected `ALTER TYPE` actions,
748+
`DefaultSchemaParserTest.shouldIgnoreEveryStatementItDoesNotModel` and
749+
`DefaultSchemaParserTest.shouldRecordAColumnOfAnIgnoredCompositeType` cover
750+
the ignored composite, range, and shell `CREATE TYPE` and the column a
751+
composite type leaves recorded, and
691752
`DefaultSchemaParserTest.shouldRejectDropType` covers `DROP TYPE`.
692753
- `QueryAnalyzerTest.shouldCarryTheEnumTypeOfASelectedColumnAndItsParameter`,
693754
`QueryAnalyzerTest.shouldCarryTheEnumTypeOfAReturningColumnAndItsParameter`,
@@ -766,20 +827,25 @@ Ordered table DDL:
766827

767828
Ignored statements:
768829

769-
- `DefaultSchemaParserTest.shouldIgnoreDocumentedStatements` covers one spelling
770-
per listed statement, including the lower-case `ALTER INDEX` form and
771-
`CREATE OR REPLACE FUNCTION ... $$ ... $$ LANGUAGE plpgsql`, and
830+
- `DefaultSchemaParserTest.shouldIgnoreEveryStatementItDoesNotModel` covers one
831+
spelling per statement kind JSqlParser parses, including the lower-case
832+
`ALTER INDEX` form, a dollar-quoted `CREATE OR REPLACE FUNCTION` and a
833+
dollar-quoted `CREATE PROCEDURE`, the views, schemas, session, privilege,
834+
role, policy, domain, and non-enum `CREATE TYPE` statements, and the opaque
835+
`ALTER FUNCTION`, `ALTER SCHEMA`, `CREATE AGGREGATE`, and `CREATE CAST`; and
772836
`DefaultSchemaParserTest.shouldIgnoreConstraintAlterTableActions` covers the
773837
named and unnamed constraint actions.
774838
- `DefaultSchemaParserTest.shouldNotResolveAnIgnoredStatementAgainstTheSchema`
775-
covers that an ignored statement may name an unmodeled table or column, and
839+
covers that an ignored statement, including a view over an unmodeled table and
840+
a `TRUNCATE` of one, may name an unmodeled table or column, and
776841
`DefaultSchemaParserTest.shouldReportTheMissingTableOfAnIgnoredConstraintAction`
777842
covers that an ignored `ALTER TABLE` action still resolves its table.
778843
- `DefaultSchemaParserTest.shouldApplyAModeledActionBesideAnIgnoredConstraintAction`
779844
covers an ignored action beside a modeled one.
780-
- `DefaultSchemaParserTest.shouldRejectStatementOutsideTheIgnoredList` covers the
781-
kind and the line of each rejected statement, including the single-quoted
782-
function body, and
845+
- `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
783849
`DefaultSchemaParserTest.shouldRejectUnsupportedSchemaStatement` and
784850
`DefaultSchemaParserTest.shouldReportTheLineOfARejectedStatementAfterCommentsAndAFunctionBody`
785851
cover the reported line after comments that contain a statement separator and
@@ -788,8 +854,9 @@ Ignored statements:
788854
covers that a single-quoted body is rejected at its own line even when it
789855
captures a later dollar-quoted body.
790856
- `SqlcjCompilerIntegrationTest.shouldReportTheMigrationFileAndLineOfAnUnsupportedStatement`
791-
covers the migration file and line of a rejected statement that follows an
792-
ignored one, and that no file is written.
857+
covers the migration file, the SQL-term message, and the line of a rejected
858+
statement that follows an ignored index and an ignored view, and that no file
859+
is written.
793860

794861
Nulls:
795862

0 commit comments

Comments
 (0)