@intentius/chant-lexicon-sql 0.124.30 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/change-set.js +250 -0
- package/dist/clickhouse/access.js +722 -0
- package/dist/clickhouse/apply/apply.js +243 -0
- package/dist/clickhouse/apply/mutations.js +66 -0
- package/dist/clickhouse/apply/statements.js +545 -0
- package/dist/clickhouse/catalog-types.js +7 -0
- package/dist/clickhouse/container.js +152 -0
- package/dist/clickhouse/engine-syntax.js +159 -0
- package/dist/clickhouse/entities.js +593 -0
- package/dist/clickhouse/expressions.js +107 -0
- package/dist/clickhouse/http.js +55 -0
- package/dist/clickhouse/import/generator.js +192 -0
- package/dist/clickhouse/import/inlined.js +207 -0
- package/dist/clickhouse/import/ir.js +160 -0
- package/dist/clickhouse/import/live-export.js +131 -0
- package/dist/clickhouse/import/parser.js +17 -0
- package/dist/clickhouse/live/bind.js +116 -0
- package/dist/clickhouse/live/catalog.js +223 -0
- package/dist/clickhouse/live/describe-resources.js +91 -0
- package/dist/clickhouse/live/undeclarable.js +59 -0
- package/dist/clickhouse/overlays/codecs.js +57 -0
- package/dist/clickhouse/overlays/engines.js +101 -0
- package/dist/clickhouse/overlays/kinds.js +1 -0
- package/dist/clickhouse/overlays/projections.js +26 -0
- package/dist/clickhouse/overlays/skip-indexes.js +40 -0
- package/dist/clickhouse/overlays/ttl.js +34 -0
- package/dist/clickhouse/overlays/types.js +59 -0
- package/dist/clickhouse/ownership.js +83 -0
- package/dist/clickhouse/parser.js +674 -0
- package/dist/clickhouse/plan/commands.js +159 -0
- package/dist/clickhouse/plan/deep.js +345 -0
- package/dist/clickhouse/plan/diff.js +312 -0
- package/dist/clickhouse/plan/disruption.js +73 -0
- package/dist/clickhouse/plan/normalize.js +545 -0
- package/dist/clickhouse/plan/rebuild-handoff.js +57 -0
- package/dist/clickhouse/plan/report.js +26 -0
- package/dist/clickhouse/plan/rules.js +72 -0
- package/dist/clickhouse/plan/schema.js +67 -0
- package/dist/clickhouse/plan/server-format.js +35 -0
- package/dist/clickhouse/rebuild/backfill.js +265 -0
- package/dist/clickhouse/rebuild/engines.js +146 -0
- package/dist/clickhouse/rebuild/observe.js +260 -0
- package/dist/clickhouse/rebuild/op.js +152 -0
- package/dist/clickhouse/rebuild/partitions.js +65 -0
- package/dist/clickhouse/rebuild/receipts.js +120 -0
- package/dist/clickhouse/rebuild/replicas.js +121 -0
- package/dist/clickhouse/rebuild/shards.js +76 -0
- package/dist/clickhouse/rebuild/steps.js +383 -0
- package/dist/clickhouse/rebuild/verify.js +279 -0
- package/dist/clickhouse/row-types.js +151 -0
- package/dist/clickhouse/testing/fake-server.js +47 -0
- package/dist/clickhouse/testing/roundtrip-schema.js +25 -0
- package/dist/clickhouse/testing/writable-server.js +123 -0
- package/dist/clickhouse/tokens.js +23 -0
- package/dist/clickhouse/topology.js +463 -0
- package/dist/clickhouse/type-syntax.js +73 -0
- package/dist/clickhouse.js +25 -0
- package/dist/codegen/classifier-page.js +93 -0
- package/dist/codegen/clickhouse-module.js +249 -0
- package/dist/codegen/coverage-page.js +70 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +222 -0
- package/dist/codegen/generate-cli.js +13 -0
- package/dist/codegen/generate.js +97 -0
- package/dist/codegen/package.js +28 -0
- package/dist/codegen/postgres-classifier-page.js +95 -0
- package/dist/codegen/postgres-module.js +418 -0
- package/dist/codegen/registry.js +52 -0
- package/dist/composites/audit-log-table.js +35 -0
- package/dist/composites/catalog.js +524 -0
- package/dist/composites/cdc-mirror.js +32 -0
- package/dist/composites/clickhouse.js +6 -0
- package/dist/composites/events-table.js +26 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/join-table.js +25 -0
- package/dist/composites/postgres.js +6 -0
- package/dist/composites/refreshed-view.js +24 -0
- package/dist/composites/replacing-table.js +28 -0
- package/dist/composites/rollup-view.js +29 -0
- package/dist/composites/sharded-table.js +27 -0
- package/dist/composites/soft-delete-table.js +34 -0
- package/dist/composites/tenant-table.js +26 -0
- package/dist/composites/typed.js +8 -0
- package/dist/config.js +206 -0
- package/dist/core/apply.js +93 -0
- package/dist/core/classifier.js +115 -0
- package/dist/core/container.js +69 -0
- package/dist/core/cursor.js +261 -0
- package/dist/core/diff.js +48 -0
- package/dist/core/entity.js +58 -0
- package/dist/core/find-templates.js +118 -0
- package/dist/core/foreign-tables.js +27 -0
- package/dist/core/handoff.js +32 -0
- package/dist/core/index.js +64 -0
- package/dist/core/interpolation.js +191 -0
- package/dist/core/normalize.js +43 -0
- package/dist/core/ownership.js +119 -0
- package/dist/core/receipts.js +22 -0
- package/dist/core/references.js +61 -0
- package/dist/core/statements.js +42 -0
- package/dist/core/template.js +79 -0
- package/dist/core/testing/http-stub.js +21 -0
- package/dist/core/tokens.js +202 -0
- package/dist/core/typed.js +40 -0
- package/dist/core/undeclarable.js +46 -0
- package/dist/coverage.js +161 -0
- package/dist/detect.js +32 -0
- package/dist/dialects.js +18 -0
- package/dist/files/build-root.js +72 -0
- package/dist/files/clickhouse.js +138 -0
- package/dist/files/common.js +58 -0
- package/dist/files/postgres.js +389 -0
- package/dist/generated/clickhouse-types.js +5 -0
- package/dist/generated/clickhouse.js +4361 -0
- package/dist/generated/postgres-types.js +227 -0
- package/dist/generated/postgres.js +3667 -0
- package/dist/import-generator.js +14 -0
- package/dist/import-parser.js +71 -0
- package/dist/index.js +24 -0
- package/dist/init-templates.js +290 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +31 -0
- package/dist/lint/audit-lineage.js +22 -0
- package/dist/lint/post-synth/audit-entries.js +34 -0
- package/dist/lint/post-synth/clickhouse-helpers.js +79 -0
- package/dist/lint/post-synth/clickhouse-type-check.js +162 -0
- package/dist/lint/post-synth/index.js +112 -0
- package/dist/lint/post-synth/postgres-audit-entries.js +35 -0
- package/dist/lint/post-synth/postgres-helpers.js +65 -0
- package/dist/lint/post-synth/postgres-names.js +507 -0
- package/dist/lint/post-synth/sql-helpers.js +23 -0
- package/dist/lint/post-synth/sql101.js +59 -0
- package/dist/lint/post-synth/sql102.js +34 -0
- package/dist/lint/post-synth/sqlch101.js +34 -0
- package/dist/lint/post-synth/sqlch102.js +23 -0
- package/dist/lint/post-synth/sqlch103.js +45 -0
- package/dist/lint/post-synth/sqlch104.js +41 -0
- package/dist/lint/post-synth/sqlch105.js +36 -0
- package/dist/lint/post-synth/sqlch106.js +27 -0
- package/dist/lint/post-synth/sqlch107.js +39 -0
- package/dist/lint/post-synth/sqlch108.js +28 -0
- package/dist/lint/post-synth/sqlch109.js +21 -0
- package/dist/lint/post-synth/sqlch110.js +31 -0
- package/dist/lint/post-synth/sqlch111.js +41 -0
- package/dist/lint/post-synth/sqlch112.js +28 -0
- package/dist/lint/post-synth/sqlch113.js +19 -0
- package/dist/lint/post-synth/sqlch114.js +26 -0
- package/dist/lint/post-synth/sqlch115.js +30 -0
- package/dist/lint/post-synth/sqlch116.js +22 -0
- package/dist/lint/post-synth/sqlch117.js +26 -0
- package/dist/lint/post-synth/sqlch118.js +27 -0
- package/dist/lint/post-synth/sqlch119.js +32 -0
- package/dist/lint/post-synth/sqlch120.js +20 -0
- package/dist/lint/post-synth/sqlch121.js +11 -0
- package/dist/lint/post-synth/sqlch122.js +11 -0
- package/dist/lint/post-synth/sqlch123.js +10 -0
- package/dist/lint/post-synth/sqlch124.js +21 -0
- package/dist/lint/post-synth/sqlch125.js +55 -0
- package/dist/lint/post-synth/sqlch126.js +62 -0
- package/dist/lint/post-synth/sqlch127.js +132 -0
- package/dist/lint/post-synth/sqlpg101.js +25 -0
- package/dist/lint/post-synth/sqlpg102.js +35 -0
- package/dist/lint/post-synth/sqlpg103.js +23 -0
- package/dist/lint/post-synth/sqlpg104.js +23 -0
- package/dist/lint/post-synth/sqlpg105.js +22 -0
- package/dist/lint/post-synth/sqlpg106.js +22 -0
- package/dist/lint/post-synth/sqlpg107.js +22 -0
- package/dist/lint/post-synth/sqlpg108.js +28 -0
- package/dist/lint/post-synth/sqlpg109.js +39 -0
- package/dist/lint/post-synth/sqlpg110.js +38 -0
- package/dist/lint/post-synth/sqlpg111.js +23 -0
- package/dist/lint/post-synth/sqlpg112.js +25 -0
- package/dist/lint/post-synth/sqlpg113.js +29 -0
- package/dist/lint/post-synth/sqlpg114.js +39 -0
- package/dist/lint/post-synth/sqlpg115.js +48 -0
- package/dist/lint/post-synth/sqlpg116.js +26 -0
- package/dist/lint/post-synth/sqlpg117.js +24 -0
- package/dist/lint/post-synth/sqlpg118.js +22 -0
- package/dist/lint/post-synth/sqlpg119.js +41 -0
- package/dist/lint/post-synth/sqlpg120.js +83 -0
- package/dist/lint/post-synth/sqlpg121.js +33 -0
- package/dist/lint/post-synth/sqlpg122.js +27 -0
- package/dist/lint/post-synth/sqlpg123.js +69 -0
- package/dist/lint/post-synth/sqlpg124.js +50 -0
- package/dist/lint/post-synth/sqlpg125.js +41 -0
- package/dist/lint/post-synth/sqlpg126.js +38 -0
- package/dist/lint/rules/index.js +12 -0
- package/dist/lint/rules/postgres-templates.js +27 -0
- package/dist/lint/rules/sqlch001.js +79 -0
- package/dist/lint/rules/sqlch002.js +124 -0
- package/dist/lint/rules/sqlch003.js +69 -0
- package/dist/lint/rules/sqlch004.js +83 -0
- package/dist/lint/rules/sqlpg001.js +68 -0
- package/dist/lint/rules/sqlpg002.js +69 -0
- package/dist/lint/rules/sqlpg003.js +58 -0
- package/dist/lint/rules/sqlpg004.js +77 -0
- package/dist/lint/rules/sqlpg005.js +86 -0
- package/dist/lint/rules/templates.js +25 -0
- package/dist/lint/rules/typed-names.js +132 -0
- package/dist/live-dialect.js +32 -0
- package/dist/lsp/catalog.js +44 -0
- package/dist/lsp/completions.js +136 -0
- package/dist/lsp/hover.js +115 -0
- package/dist/lsp/postgres-catalog.js +176 -0
- package/dist/lsp/postgres-template.js +171 -0
- package/dist/lsp/postgres.js +266 -0
- package/dist/lsp/refs.js +60 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +19 -0
- package/dist/lsp/template.js +143 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +293 -0
- package/dist/mcp/postgres.js +117 -0
- package/dist/migration-statements.js +243 -0
- package/dist/op/activities/clickhouse-apply.js +83 -0
- package/dist/op/activities/clickhouse-emulator.js +53 -0
- package/dist/op/activities/clickhouse-rebuild.js +119 -0
- package/dist/op/activities/index.js +26 -0
- package/dist/op/activities/postgres-apply.js +126 -0
- package/dist/op/activities/postgres-emulator.js +70 -0
- package/dist/op/activities/postgres-migration.js +156 -0
- package/dist/op/activities/sql-exec.js +57 -0
- package/dist/op/activity-contracts.js +103 -0
- package/dist/package-cli.js +14 -0
- package/dist/plugin.js +259 -0
- package/dist/postgres/access/acl.js +344 -0
- package/dist/postgres/access/changes.js +30 -0
- package/dist/postgres/access/import.js +61 -0
- package/dist/postgres/access/live.js +96 -0
- package/dist/postgres/apply/apply.js +557 -0
- package/dist/postgres/apply/statements.js +882 -0
- package/dist/postgres/catalog-types.js +18 -0
- package/dist/postgres/container.js +58 -0
- package/dist/postgres/entities.js +1390 -0
- package/dist/postgres/entity-types.js +28 -0
- package/dist/postgres/import/generator.js +283 -0
- package/dist/postgres/import/ir.js +53 -0
- package/dist/postgres/import/live-export.js +83 -0
- package/dist/postgres/keywords.js +32 -0
- package/dist/postgres/live/bind.js +130 -0
- package/dist/postgres/live/catalog-kinds.js +301 -0
- package/dist/postgres/live/catalog.js +644 -0
- package/dist/postgres/live/client.js +151 -0
- package/dist/postgres/live/describe-resources.js +135 -0
- package/dist/postgres/live/undeclarable.js +111 -0
- package/dist/postgres/migrate/backfill.js +170 -0
- package/dist/postgres/migrate/batches.js +122 -0
- package/dist/postgres/migrate/carry.js +518 -0
- package/dist/postgres/migrate/handoff.js +59 -0
- package/dist/postgres/migrate/names.js +61 -0
- package/dist/postgres/migrate/observe.js +360 -0
- package/dist/postgres/migrate/op.js +178 -0
- package/dist/postgres/migrate/receipts.js +117 -0
- package/dist/postgres/migrate/replication.js +106 -0
- package/dist/postgres/migrate/steps.js +514 -0
- package/dist/postgres/overlays/columns.js +31 -0
- package/dist/postgres/overlays/constraints.js +52 -0
- package/dist/postgres/overlays/extension-objects.js +88 -0
- package/dist/postgres/overlays/indexes.js +25 -0
- package/dist/postgres/overlays/kinds.js +2 -0
- package/dist/postgres/overlays/partitioning.js +23 -0
- package/dist/postgres/overlays/relations.js +25 -0
- package/dist/postgres/overlays/storage.js +65 -0
- package/dist/postgres/overlays/type-ddl.js +36 -0
- package/dist/postgres/overlays/types.js +74 -0
- package/dist/postgres/parser.js +1824 -0
- package/dist/postgres/plan/commands.js +137 -0
- package/dist/postgres/plan/deep.js +324 -0
- package/dist/postgres/plan/diff.js +499 -0
- package/dist/postgres/plan/disruption.js +64 -0
- package/dist/postgres/plan/normalize.js +889 -0
- package/dist/postgres/plan/report.js +21 -0
- package/dist/postgres/plan/rules.js +115 -0
- package/dist/postgres/plan/schema.js +105 -0
- package/dist/postgres/plan/server-normalize.js +166 -0
- package/dist/postgres/providers/aurora.js +44 -0
- package/dist/postgres/providers/azure.js +53 -0
- package/dist/postgres/providers/cloud-sql.js +41 -0
- package/dist/postgres/providers/index.js +73 -0
- package/dist/postgres/providers/neon.js +44 -0
- package/dist/postgres/providers/rds.js +47 -0
- package/dist/postgres/providers/supabase.js +45 -0
- package/dist/postgres/providers/types.js +21 -0
- package/dist/postgres/row-types.js +107 -0
- package/dist/postgres/testing/fake-client.js +34 -0
- package/dist/postgres/testing/migration.js +48 -0
- package/dist/postgres/testing/roundtrip-schema.js +64 -0
- package/dist/postgres/testing/server.js +47 -0
- package/dist/postgres/testing/writable-server.js +160 -0
- package/dist/postgres/tokens.js +41 -0
- package/dist/postgres.js +15 -0
- package/dist/read/clickhouse.js +82 -0
- package/dist/read/objects.js +44 -0
- package/dist/read/postgres.js +104 -0
- package/dist/read.js +52 -0
- package/dist/receipts.js +207 -0
- package/dist/serializer.js +172 -0
- package/dist/skill-defs.js +93 -0
- package/dist/spec/catalog.js +191 -0
- package/dist/spec/fetch.d.ts.map +1 -1
- package/dist/spec/fetch.js +90 -0
- package/dist/spec/pin.js +42 -0
- package/dist/spec/postgres-catalog.js +171 -0
- package/dist/spec/postgres-fetch.js +91 -0
- package/dist/spec/postgres-pin.js +82 -0
- package/dist/sql-files.js +206 -0
- package/dist/sql-types-command.js +97 -0
- package/dist/sql-types.js +323 -0
- package/dist/token-source.js +156 -0
- package/dist/validate-cli.js +5 -0
- package/dist/validate.js +151 -0
- package/dist/workspace-outputs.js +191 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/lint/rules/typescript-7.test.ts +2 -1
- package/src/lsp/catalog.ts +2 -2
- package/src/lsp/postgres-catalog.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- package/src/mcp/index.ts +4 -2
- package/src/spec/fetch.ts +2 -2
- package/src/spec/postgres-fetch.ts +2 -2
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The statements that make one classified Postgres change (#3280): the
|
|
3
|
+
* declared `CREATE` with chant's marker set by `COMMENT ON`, the `ALTER` for
|
|
4
|
+
* each change the classifier (`../plan/rules.ts`) does not refuse, and the
|
|
5
|
+
* `DROP` a prune issues.
|
|
6
|
+
*
|
|
7
|
+
* Each function takes a declared object, what the server holds for it, and
|
|
8
|
+
* the changes the diff reported, and returns statements; nothing is sent. The
|
|
9
|
+
* applier (`./apply.ts`) groups them into transactions and sends them, and
|
|
10
|
+
* the expand-and-contract migration Op (#3281) builds its expand and contract
|
|
11
|
+
* steps from the same pieces.
|
|
12
|
+
*
|
|
13
|
+
* The SQL is the declaration's own text wherever there is some: a column's
|
|
14
|
+
* type and default, a constraint's expression, an index's `CREATE`, a view's
|
|
15
|
+
* `CREATE`. The canonical forms the diff compared are for comparing; what the
|
|
16
|
+
* server is sent is what the author wrote.
|
|
17
|
+
*
|
|
18
|
+
* Every step carries the class of the change it makes, which picks its
|
|
19
|
+
* transaction and its statement timeout, and whether it may run inside a
|
|
20
|
+
* transaction block at all (`CREATE INDEX CONCURRENTLY` may not).
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { stampedComment } from "../../core/ownership.js";
|
|
24
|
+
import { isTrivia, tokenizeText } from "../tokens.js";
|
|
25
|
+
import { parseStatements } from "../parser.js";
|
|
26
|
+
import { quoteIdent } from "../keywords.js";
|
|
27
|
+
import { POSTGRES_ENTITY_TYPES } from "../entity-types.js";
|
|
28
|
+
import { addConstraintRule, matchPgObjects } from "../plan/diff.js";
|
|
29
|
+
import { PG_CLASSIFIER_RULES } from "../plan/rules.js";
|
|
30
|
+
import { ACCESS_KINDS, sameConstraint } from "../plan/normalize.js";
|
|
31
|
+
import { migrationTarget } from "../migrate/handoff.js";
|
|
32
|
+
import { pgName } from "../migrate/names.js";
|
|
33
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
34
|
+
// ── Names and literals ─────────────────────────────────────────────────
|
|
35
|
+
/** A string literal: `'` doubled (standard_conforming_strings is on). */
|
|
36
|
+
export const pgString = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
37
|
+
/** `schema.name`, each part quoted where Postgres needs it. */
|
|
38
|
+
export const pgQualified = (schema, name) => (schema ? `${quoteIdent(schema)}.${quoteIdent(name)}` : quoteIdent(name));
|
|
39
|
+
/**
|
|
40
|
+
* An object's name in a statement. A schema, an extension and an event
|
|
41
|
+
* trigger have no schema; a routine or an aggregate is named with its
|
|
42
|
+
* parameter types (`app.f(integer,text)`), a trigger with its table
|
|
43
|
+
* (`touch ON app.users`), a cast by its types (`(app.money AS numeric)`).
|
|
44
|
+
*/
|
|
45
|
+
export const objectIdent = (o) => o.kind === "cast"
|
|
46
|
+
? o.name
|
|
47
|
+
: o.kind === "schema" || o.kind === "extension" || o.kind === "role" || o.kind === "eventTrigger"
|
|
48
|
+
? quoteIdent(o.name)
|
|
49
|
+
: o.kind === "trigger" || o.kind === "policy"
|
|
50
|
+
? `${quoteIdent(o.name)}${o.signature ?? ""}`
|
|
51
|
+
: `${pgQualified(o.schema, o.name)}${o.signature ?? ""}`;
|
|
52
|
+
/** The words a statement names the object's kind with. */
|
|
53
|
+
export const KIND_WORDS = {
|
|
54
|
+
schema: "SCHEMA",
|
|
55
|
+
table: "TABLE",
|
|
56
|
+
index: "INDEX",
|
|
57
|
+
view: "VIEW",
|
|
58
|
+
materializedView: "MATERIALIZED VIEW",
|
|
59
|
+
sequence: "SEQUENCE",
|
|
60
|
+
enum: "TYPE",
|
|
61
|
+
composite: "TYPE",
|
|
62
|
+
range: "TYPE",
|
|
63
|
+
domain: "DOMAIN",
|
|
64
|
+
extension: "EXTENSION",
|
|
65
|
+
function: "FUNCTION",
|
|
66
|
+
procedure: "PROCEDURE",
|
|
67
|
+
aggregate: "AGGREGATE",
|
|
68
|
+
collation: "COLLATION",
|
|
69
|
+
cast: "CAST",
|
|
70
|
+
eventTrigger: "EVENT TRIGGER",
|
|
71
|
+
trigger: "TRIGGER",
|
|
72
|
+
policy: "POLICY",
|
|
73
|
+
role: "ROLE",
|
|
74
|
+
grant: "GRANT",
|
|
75
|
+
defaultPrivileges: "DEFAULT PRIVILEGES",
|
|
76
|
+
};
|
|
77
|
+
/** The canonical kind of an entity type. */
|
|
78
|
+
export const kindOfType = (type) => (Object.entries(POSTGRES_ENTITY_TYPES).find(([, t]) => t === type)?.[0] ?? "table");
|
|
79
|
+
const step = (sql, cls, rule, transactional = true) => ({ sql, class: cls, transactional, ...(rule ? { rule } : {}) });
|
|
80
|
+
const classOf = (rule) => PG_CLASSIFIER_RULES[rule].class;
|
|
81
|
+
// ── Pre-checks (#3686) ─────────────────────────────────────────────────
|
|
82
|
+
/** The rows of `table` with `column` NULL: what SET NOT NULL fails on. */
|
|
83
|
+
export function nullPrecheck(table, column) {
|
|
84
|
+
return { sql: `SELECT count(*) AS n FROM ${table} WHERE ${quoteIdent(column)} IS NULL`, detail: `rows where ${column} is NULL, which SET NOT NULL fails on` };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The groups of rows of `table` with the same `columns`: what a unique index
|
|
88
|
+
* or a unique or primary key constraint fails on. NULLs are distinct unless
|
|
89
|
+
* `nullsNotDistinct`; a primary key also counts the rows with a NULL key
|
|
90
|
+
* column, which its implied NOT NULL fails on. `where` is a partial index's
|
|
91
|
+
* predicate.
|
|
92
|
+
*/
|
|
93
|
+
export function duplicatePrecheck(table, columns, opts = {}) {
|
|
94
|
+
const cols = columns.map(quoteIdent);
|
|
95
|
+
const filter = [...(opts.primaryKey || opts.nullsNotDistinct ? [] : cols.map((c) => `${c} IS NOT NULL`)), ...(opts.where ? [`(${opts.where})`] : [])];
|
|
96
|
+
const groups = `SELECT count(*) FROM (SELECT 1 FROM ${table}${filter.length ? ` WHERE ${filter.join(" AND ")}` : ""} GROUP BY ${cols.join(", ")} HAVING count(*) > 1) AS duplicates`;
|
|
97
|
+
const what = columns.join(", ");
|
|
98
|
+
if (!opts.primaryKey)
|
|
99
|
+
return { sql: `SELECT (${groups}) AS n`, detail: `values of (${what}) held by more than one row, which a unique index fails on` };
|
|
100
|
+
return {
|
|
101
|
+
sql: `SELECT (${groups}) + (SELECT count(*) FROM ${table} WHERE ${cols.map((c) => `${c} IS NULL`).join(" OR ")}) AS n`,
|
|
102
|
+
detail: `values of (${what}) held by more than one row, and rows with one of them NULL, which a primary key fails on`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** The rows of `table` that fail a CHECK: its expression false (a NULL result passes). */
|
|
106
|
+
export function checkPrecheck(table, expr, name) {
|
|
107
|
+
return { sql: `SELECT count(*) AS n FROM ${table} WHERE NOT (${expr})`, detail: `rows that fail the check${name ? ` ${name}` : ""} (${expr})` };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The rows of `table` whose foreign key references no row: every key column
|
|
111
|
+
* set and no referenced row with those values (MATCH SIMPLE, the default).
|
|
112
|
+
* Undefined for a foreign key that names no referenced columns or is MATCH
|
|
113
|
+
* FULL, which the query does not cover.
|
|
114
|
+
*/
|
|
115
|
+
export function foreignKeyPrecheck(table, fk) {
|
|
116
|
+
if (fk.refColumns.length !== fk.columns.length || (fk.match !== undefined && fk.match.toUpperCase() !== "SIMPLE"))
|
|
117
|
+
return undefined;
|
|
118
|
+
const child = fk.columns.map((c) => `c.${quoteIdent(c)}`);
|
|
119
|
+
const join = fk.refColumns.map((r, i) => `p.${quoteIdent(r)} = ${child[i]}`).join(" AND ");
|
|
120
|
+
return {
|
|
121
|
+
sql: `SELECT count(*) AS n FROM ${table} AS c WHERE ${child.map((c) => `${c} IS NOT NULL`).join(" AND ")} AND NOT EXISTS (SELECT 1 FROM ${fk.refTable} AS p WHERE ${join})`,
|
|
122
|
+
detail: `rows whose (${fk.columns.join(", ")}) references no row of ${fk.refTable}, which the foreign key${fk.name ? ` ${fk.name}` : ""} fails on`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** The check that proves a column NOT NULL while SET NOT NULL runs (SQLPG210), named as the migration Op names its own. */
|
|
126
|
+
export const notNullCheckName = (column) => pgName(`${column}__chant_nn`);
|
|
127
|
+
/** The statements of a template's DDL, split at top-level semicolons, comments kept with the statement they precede. */
|
|
128
|
+
export function splitStatements(ddl) {
|
|
129
|
+
const tokens = tokenizeText(ddl, 0);
|
|
130
|
+
const out = [];
|
|
131
|
+
let current = [];
|
|
132
|
+
const flush = () => {
|
|
133
|
+
let end = current.length;
|
|
134
|
+
while (end > 0 && isTrivia(current[end - 1]))
|
|
135
|
+
end--;
|
|
136
|
+
const body = current.slice(0, end);
|
|
137
|
+
if (body.some((t) => !isTrivia(t)))
|
|
138
|
+
out.push({ text: body.map((t) => t.text).join("").trim(), tokens: body });
|
|
139
|
+
current = [];
|
|
140
|
+
};
|
|
141
|
+
for (const t of tokens) {
|
|
142
|
+
if (t.kind === "punct" && t.text === ";")
|
|
143
|
+
flush();
|
|
144
|
+
else
|
|
145
|
+
current.push(t);
|
|
146
|
+
}
|
|
147
|
+
flush();
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
const firstNode = (s) => {
|
|
151
|
+
try {
|
|
152
|
+
return parseStatements(s.tokens)[0];
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
/** Whether a `COMMENT ON` statement sets the object's own comment, not a column's or a constraint's. */
|
|
159
|
+
function isOwnComment(s, kind) {
|
|
160
|
+
const node = firstNode(s);
|
|
161
|
+
return node?.statement === "comment" && node.objectType.toUpperCase() === KIND_WORDS[kind];
|
|
162
|
+
}
|
|
163
|
+
/** The declared comment with chant's trailer: what the object's own `COMMENT ON` sets. */
|
|
164
|
+
export function commentStatement(obj, marker, base) {
|
|
165
|
+
const declared = obj.canonical.fields.comment;
|
|
166
|
+
return `COMMENT ON ${KIND_WORDS[obj.canonical.kind]} ${objectIdent(obj.canonical)} IS ${pgString(stampedComment(declared ?? base, marker))}`;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The declared statements that create the object, its own `COMMENT ON`
|
|
170
|
+
* replaced by one carrying chant's marker, so no object is left unmarked.
|
|
171
|
+
* `base` is the comment kept under the trailer when nothing is declared (an
|
|
172
|
+
* extension's own comment, from its control file). `orReplace` turns
|
|
173
|
+
* `CREATE VIEW` into `CREATE OR REPLACE VIEW`.
|
|
174
|
+
*/
|
|
175
|
+
export function createSteps(obj, marker, opts = {}) {
|
|
176
|
+
const cls = opts.cls ?? "create";
|
|
177
|
+
const out = [];
|
|
178
|
+
splitStatements(obj.ddl).forEach((s, i) => {
|
|
179
|
+
if (i > 0 && isOwnComment(s, obj.canonical.kind))
|
|
180
|
+
return;
|
|
181
|
+
// A table's row-level security is access, which only a profile that manages access applies.
|
|
182
|
+
if (i > 0 && opts.access === false && firstNode(s)?.statement === "rowSecurity")
|
|
183
|
+
return;
|
|
184
|
+
let sql = s.text;
|
|
185
|
+
const replaceable = obj.canonical.kind === "view" ||
|
|
186
|
+
obj.canonical.kind === "function" ||
|
|
187
|
+
obj.canonical.kind === "procedure" ||
|
|
188
|
+
obj.canonical.kind === "aggregate" ||
|
|
189
|
+
(obj.canonical.kind === "trigger" && obj.canonical.fields.constraint !== true);
|
|
190
|
+
if (i === 0 && opts.orReplace && replaceable && !/^\s*CREATE\s+OR\s+REPLACE\b/i.test(sql))
|
|
191
|
+
sql = sql.replace(/\bCREATE\s+/i, "CREATE OR REPLACE ");
|
|
192
|
+
const concurrently = i === 0 && obj.canonical.kind === "index" && obj.props.concurrently === true;
|
|
193
|
+
out.push({ ...step(sql, cls, opts.rule, !concurrently), ...(concurrently ? { buildsIndex: objectIdent(obj.canonical) } : {}) });
|
|
194
|
+
});
|
|
195
|
+
out.push(step(commentStatement(obj, marker, opts.base), cls, opts.rule));
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
/** The `DROP` for an object a prune removes. Never `CASCADE`: what something else still uses is kept. */
|
|
199
|
+
export function dropStatement(kind, schema, name, signature) {
|
|
200
|
+
const what = objectIdent({ kind, ...(schema !== undefined ? { schema } : {}), name, ...(signature !== undefined ? { signature } : {}) });
|
|
201
|
+
if (kind === "index")
|
|
202
|
+
return step(`DROP INDEX CONCURRENTLY ${what}`, "concurrently", "SQLPG242", false);
|
|
203
|
+
return step(`DROP ${KIND_WORDS[kind]} ${what}`, "drop", kind === "trigger" ? "SQLPG285" : kind === "policy" ? "SQLPG292" : "SQLPG270");
|
|
204
|
+
}
|
|
205
|
+
// ── Columns ────────────────────────────────────────────────────────────
|
|
206
|
+
/** A column's definition for `ADD COLUMN`: its type, storage, collation, generation, default and NOT NULL; its constraints are added on their own. */
|
|
207
|
+
export function columnDefinition(c) {
|
|
208
|
+
const parts = [quoteIdent(c.name)];
|
|
209
|
+
if (c.type)
|
|
210
|
+
parts.push(c.type);
|
|
211
|
+
if (c.storage)
|
|
212
|
+
parts.push(`STORAGE ${c.storage}`);
|
|
213
|
+
if (c.compression)
|
|
214
|
+
parts.push(`COMPRESSION ${c.compression}`);
|
|
215
|
+
if (c.collate)
|
|
216
|
+
parts.push(`COLLATE ${c.collate}`);
|
|
217
|
+
if (c.generated?.kind === "identity")
|
|
218
|
+
parts.push(`GENERATED ${c.generated.always ? "ALWAYS" : "BY DEFAULT"} AS IDENTITY${c.generated.options ? ` (${c.generated.options})` : ""}`);
|
|
219
|
+
else if (c.generated)
|
|
220
|
+
parts.push(`GENERATED ALWAYS AS (${c.generated.expr ?? ""}) ${c.generated.kind.toUpperCase()}`);
|
|
221
|
+
if (c.default !== undefined)
|
|
222
|
+
parts.push(`DEFAULT ${c.default}`);
|
|
223
|
+
if (c.notNull)
|
|
224
|
+
parts.push(c.notNullName ? `CONSTRAINT ${quoteIdent(c.notNullName)} NOT NULL` : "NOT NULL");
|
|
225
|
+
return parts.join(" ");
|
|
226
|
+
}
|
|
227
|
+
const SEQUENCE_OPTION = /\b(START\s+(?:WITH\s+)?-?\s*\d+|INCREMENT\s+(?:BY\s+)?-?\s*\d+|NO\s+MINVALUE|NO\s+MAXVALUE|MINVALUE\s+-?\s*\d+|MAXVALUE\s+-?\s*\d+|CACHE\s+\d+|NO\s+CYCLE|CYCLE)\b/gi;
|
|
228
|
+
/** An identity column's options as `SET` clauses, the ones not written set back to their defaults. */
|
|
229
|
+
function identitySetClauses(options) {
|
|
230
|
+
const written = [...(options ?? "").matchAll(SEQUENCE_OPTION)].map((m) => m[1].replace(/\s+/g, " ").toUpperCase());
|
|
231
|
+
const has = (re) => written.some((w) => re.test(w));
|
|
232
|
+
const out = written.map((w) => `SET ${w}`);
|
|
233
|
+
if (!has(/^INCREMENT/))
|
|
234
|
+
out.push("SET INCREMENT BY 1");
|
|
235
|
+
if (!has(/MINVALUE/))
|
|
236
|
+
out.push("SET NO MINVALUE");
|
|
237
|
+
if (!has(/MAXVALUE/))
|
|
238
|
+
out.push("SET NO MAXVALUE");
|
|
239
|
+
if (!has(/^CACHE/))
|
|
240
|
+
out.push("SET CACHE 1");
|
|
241
|
+
if (!has(/CYCLE/))
|
|
242
|
+
out.push("SET NO CYCLE");
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
const COLUMN_FIELD = /^columns\.(.+?)(?:\.(type|default|notNull|generated|identity|collate|storage|comment))?$/;
|
|
246
|
+
// ── Constraints ────────────────────────────────────────────────────────
|
|
247
|
+
/** A table's declared constraints in the canonical order (primary key, unique, check, foreign key, exclusion), each with its props. */
|
|
248
|
+
function declaredConstraintDefs(p) {
|
|
249
|
+
return [
|
|
250
|
+
...(p.primaryKey ? [{ kind: "PRIMARY KEY", def: p.primaryKey }] : []),
|
|
251
|
+
...p.uniques.map((def) => ({ kind: "UNIQUE", def })),
|
|
252
|
+
...p.checks.map((def) => ({ kind: "CHECK", def })),
|
|
253
|
+
...p.foreignKeys.map((def) => ({ kind: "FOREIGN KEY", def })),
|
|
254
|
+
...p.exclusions.map((def) => ({ kind: "EXCLUDE", def })),
|
|
255
|
+
];
|
|
256
|
+
}
|
|
257
|
+
const deferralSql = (d) => [d.deferrable ? "DEFERRABLE" : "", d.initiallyDeferred ? "INITIALLY DEFERRED" : ""].filter(Boolean).join(" ");
|
|
258
|
+
const parens = (s) => (s.trim().startsWith("(") ? s.trim() : `(${s.trim()})`);
|
|
259
|
+
const named = (name) => (name ? `CONSTRAINT ${quoteIdent(name)} ` : "");
|
|
260
|
+
/** A constraint as a table constraint (`ADD CONSTRAINT ...`), whether it was declared on a column or the table. */
|
|
261
|
+
export function constraintSql(kind, def) {
|
|
262
|
+
const cols = (c) => c.map(quoteIdent).join(", ");
|
|
263
|
+
switch (kind) {
|
|
264
|
+
case "CHECK": {
|
|
265
|
+
const c = def;
|
|
266
|
+
return [`${named(c.name)}CHECK (${c.expr})`, c.noInherit ? "NO INHERIT" : "", c.notEnforced ? "NOT ENFORCED" : "", c.notValid ? "NOT VALID" : ""].filter(Boolean).join(" ");
|
|
267
|
+
}
|
|
268
|
+
case "FOREIGN KEY": {
|
|
269
|
+
const f = def;
|
|
270
|
+
return [
|
|
271
|
+
`${named(f.name)}FOREIGN KEY (${cols(f.columns)}) REFERENCES ${f.refTable}${f.refColumns.length ? ` (${cols(f.refColumns)})` : ""}`,
|
|
272
|
+
f.match ? `MATCH ${f.match.toUpperCase()}` : "",
|
|
273
|
+
f.onUpdate ? `ON UPDATE ${f.onUpdate}` : "",
|
|
274
|
+
f.onDelete ? `ON DELETE ${f.onDelete}` : "",
|
|
275
|
+
deferralSql(f),
|
|
276
|
+
f.notEnforced ? "NOT ENFORCED" : "",
|
|
277
|
+
f.notValid ? "NOT VALID" : "",
|
|
278
|
+
]
|
|
279
|
+
.filter(Boolean)
|
|
280
|
+
.join(" ");
|
|
281
|
+
}
|
|
282
|
+
case "EXCLUDE": {
|
|
283
|
+
const x = def;
|
|
284
|
+
return [
|
|
285
|
+
`${named(x.name)}EXCLUDE${x.using ? ` USING ${x.using}` : ""} (${x.elements})`,
|
|
286
|
+
x.include ? `INCLUDE ${parens(x.include)}` : "",
|
|
287
|
+
x.with ? `WITH ${parens(x.with)}` : "",
|
|
288
|
+
x.where ? `WHERE (${x.where})` : "",
|
|
289
|
+
deferralSql(x),
|
|
290
|
+
]
|
|
291
|
+
.filter(Boolean)
|
|
292
|
+
.join(" ");
|
|
293
|
+
}
|
|
294
|
+
default: {
|
|
295
|
+
const k = def;
|
|
296
|
+
return [
|
|
297
|
+
`${named(k.name)}${kind}${k.nullsNotDistinct ? " NULLS NOT DISTINCT" : ""} (${cols(k.columns)})`,
|
|
298
|
+
k.include ? `INCLUDE ${parens(k.include)}` : "",
|
|
299
|
+
k.with ? `WITH ${parens(k.with)}` : "",
|
|
300
|
+
deferralSql(k),
|
|
301
|
+
]
|
|
302
|
+
.filter(Boolean)
|
|
303
|
+
.join(" ");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** The name Postgres would give a key constraint's index: `<table>_pkey`, `<table>_<columns>_key`. */
|
|
308
|
+
function keyIndexName(table, kind, columns) {
|
|
309
|
+
return (kind === "PRIMARY KEY" ? `${table}_pkey` : `${table}_${columns.join("_")}_key`).slice(0, 63);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* A primary key or unique constraint added to a table that exists: its index
|
|
313
|
+
* built CONCURRENTLY outside a transaction, then the constraint made from it
|
|
314
|
+
* (`ADD CONSTRAINT ... USING INDEX`), which changes only the catalog
|
|
315
|
+
* (SQLPG221).
|
|
316
|
+
*/
|
|
317
|
+
function keyConstraintSteps(table, kind, k) {
|
|
318
|
+
const name = k.name ?? keyIndexName(table.name, kind, k.columns);
|
|
319
|
+
const index = pgQualified(table.schema, name);
|
|
320
|
+
const build = [
|
|
321
|
+
`CREATE UNIQUE INDEX CONCURRENTLY ${quoteIdent(name)} ON ${pgQualified(table.schema, table.name)} (${k.columns.map(quoteIdent).join(", ")})`,
|
|
322
|
+
k.include ? `INCLUDE ${parens(k.include)}` : "",
|
|
323
|
+
k.nullsNotDistinct ? "NULLS NOT DISTINCT" : "",
|
|
324
|
+
k.with ? `WITH ${parens(k.with)}` : "",
|
|
325
|
+
]
|
|
326
|
+
.filter(Boolean)
|
|
327
|
+
.join(" ");
|
|
328
|
+
return [
|
|
329
|
+
{
|
|
330
|
+
...step(build, "concurrently", "SQLPG221", false),
|
|
331
|
+
buildsIndex: index,
|
|
332
|
+
precheck: duplicatePrecheck(pgQualified(table.schema, table.name), k.columns, { primaryKey: kind === "PRIMARY KEY", ...(k.nullsNotDistinct ? { nullsNotDistinct: true } : {}) }),
|
|
333
|
+
},
|
|
334
|
+
step([`ALTER TABLE ${pgQualified(table.schema, table.name)} ADD CONSTRAINT ${quoteIdent(name)} ${kind} USING INDEX ${quoteIdent(name)}`, deferralSql(k)].filter(Boolean).join(" "), "metadata", "SQLPG221"),
|
|
335
|
+
];
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* A CHECK (SQLPG218) or a foreign key (SQLPG219) added to a table that
|
|
339
|
+
* exists, without reading its rows under a lock that blocks writes: the
|
|
340
|
+
* constraint added NOT VALID, which checks new rows only (SQLPG217), then
|
|
341
|
+
* validated (SQLPG220), which reads the rows under SHARE UPDATE EXCLUSIVE
|
|
342
|
+
* while writes go on. The applier commits between the two, since their
|
|
343
|
+
* classes differ, so the validation runs in a transaction of its own. A run
|
|
344
|
+
* interrupted after the first statement leaves the constraint NOT VALID on
|
|
345
|
+
* the server, and the next plan validates it as found. The first statement
|
|
346
|
+
* carries the pre-check (the rows the check or the foreign key refuses).
|
|
347
|
+
* VALIDATE needs a name, so a constraint declared without one is given
|
|
348
|
+
* {@link unnamedConstraintName}'s; the diff matches an unnamed declaration
|
|
349
|
+
* to the constraint by its body, whatever it is called.
|
|
350
|
+
*/
|
|
351
|
+
/** The name given to a CHECK or foreign key declared without one: `<table>_<hash of its body>_check` or `_fkey`. */
|
|
352
|
+
export function unnamedConstraintName(table, c) {
|
|
353
|
+
const hash = createHash("sha256").update(c.body).digest("hex").slice(0, 8);
|
|
354
|
+
return pgName(`${table}_${hash}_${c.kind === "CHECK" ? "check" : "fkey"}`);
|
|
355
|
+
}
|
|
356
|
+
function notValidThenValidateSteps(alter, kind, def, precheck) {
|
|
357
|
+
const add = step(`${alter} ADD ${constraintSql(kind, { ...def, notValid: true })}`, "metadata", "SQLPG217");
|
|
358
|
+
return [precheck ? { ...add, precheck } : add, step(`${alter} VALIDATE CONSTRAINT ${quoteIdent(def.name)}`, "validate", "SQLPG220")];
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The constraint changes of a table or a domain, matched the way the diff
|
|
362
|
+
* matches them (`../plan/diff.ts`): a declared constraint the server lacks is
|
|
363
|
+
* added, one the server holds NOT VALID and the declaration does not is
|
|
364
|
+
* validated, one the declaration lost is dropped, and a changed comment is
|
|
365
|
+
* set.
|
|
366
|
+
*/
|
|
367
|
+
function constraintSteps(obj, live, keep = new Set()) {
|
|
368
|
+
const domain = obj.canonical.kind === "domain";
|
|
369
|
+
const target = objectIdent(obj.canonical);
|
|
370
|
+
const alter = domain ? `ALTER DOMAIN ${target}` : `ALTER TABLE ${target}`;
|
|
371
|
+
const defs = domain
|
|
372
|
+
? obj.props.checks.map((def) => ({ kind: "CHECK", def: def }))
|
|
373
|
+
: declaredConstraintDefs(obj.props);
|
|
374
|
+
// A NOT NULL proof left by an interrupted SET NOT NULL (SQLPG210) is that sequence's to reuse and drop.
|
|
375
|
+
const remaining = live.constraints.filter((b) => !(b.name && keep.has(b.name)));
|
|
376
|
+
const out = [];
|
|
377
|
+
/** What the rows must pass for a CHECK or a foreign key to be valid. */
|
|
378
|
+
const precheckOf = (kind, def) => domain || def === undefined ? undefined : kind === "CHECK" ? checkPrecheck(target, def.expr, def.name) : kind === "FOREIGN KEY" ? foreignKeyPrecheck(target, def) : undefined;
|
|
379
|
+
const withPrecheck = (s, p) => (p ? { ...s, precheck: p } : s);
|
|
380
|
+
obj.canonical.constraints.forEach((a, i) => {
|
|
381
|
+
const def = defs[i]?.def;
|
|
382
|
+
const at = remaining.findIndex((b) => sameConstraint(a, b));
|
|
383
|
+
if (at < 0) {
|
|
384
|
+
if (!def)
|
|
385
|
+
return;
|
|
386
|
+
if (domain) {
|
|
387
|
+
const c = def;
|
|
388
|
+
out.push(step(`${alter} ADD ${named(c.name)}CHECK (${c.expr})${c.notValid ? " NOT VALID" : ""}`, c.notValid ? "metadata" : "validate", c.notValid ? "SQLPG217" : "SQLPG262"));
|
|
389
|
+
}
|
|
390
|
+
else if ((a.kind === "PRIMARY KEY" || a.kind === "UNIQUE") && !a.notValid) {
|
|
391
|
+
out.push(...keyConstraintSteps(obj.canonical, a.kind, def));
|
|
392
|
+
}
|
|
393
|
+
else if ((a.kind === "CHECK" || a.kind === "FOREIGN KEY") && !a.notValid && !def.notEnforced) {
|
|
394
|
+
const d = def;
|
|
395
|
+
const withName = d.name ? d : { ...d, name: unnamedConstraintName(obj.canonical.name, a) };
|
|
396
|
+
out.push(...notValidThenValidateSteps(alter, a.kind, withName, precheckOf(a.kind, withName)));
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const rule = addConstraintRule(a);
|
|
400
|
+
out.push(withPrecheck(step(`${alter} ADD ${constraintSql(a.kind, def)}`, classOf(rule), rule), rule === "SQLPG217" || def.notEnforced ? undefined : precheckOf(a.kind, def)));
|
|
401
|
+
}
|
|
402
|
+
if (a.comment !== undefined) {
|
|
403
|
+
const name = a.name ?? def.name;
|
|
404
|
+
if (name)
|
|
405
|
+
out.push(step(`COMMENT ON CONSTRAINT ${quoteIdent(name)} ON ${domain ? "DOMAIN " : ""}${target} IS ${pgString(a.comment)}`, "metadata", "SQLPG216"));
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const b = remaining.splice(at, 1)[0];
|
|
410
|
+
const name = b.name ?? a.name;
|
|
411
|
+
if (!name)
|
|
412
|
+
return;
|
|
413
|
+
if (b.notValid && !a.notValid)
|
|
414
|
+
out.push(withPrecheck(step(`${alter} VALIDATE CONSTRAINT ${quoteIdent(name)}`, "validate", "SQLPG220"), precheckOf(a.kind, def)));
|
|
415
|
+
if (b.comment !== a.comment) {
|
|
416
|
+
out.push(step(`COMMENT ON CONSTRAINT ${quoteIdent(name)} ON ${domain ? "DOMAIN " : ""}${target} IS ${a.comment === undefined ? "NULL" : pgString(a.comment)}`, "metadata", "SQLPG216"));
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
for (const b of remaining)
|
|
420
|
+
if (b.name)
|
|
421
|
+
out.push(step(`${alter} DROP CONSTRAINT ${quoteIdent(b.name)}`, "metadata", domain ? "SQLPG263" : "SQLPG223"));
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
// ── Options ────────────────────────────────────────────────────────────
|
|
425
|
+
/** `SET (...)` for options the declaration sets differently and `RESET (...)` for those it drops. */
|
|
426
|
+
function optionSteps(alter, declared, live, rule) {
|
|
427
|
+
const d = (declared ?? {});
|
|
428
|
+
const l = (live ?? {});
|
|
429
|
+
const set = Object.entries(d).filter(([k, v]) => l[k] !== v);
|
|
430
|
+
const reset = Object.keys(l).filter((k) => !(k in d));
|
|
431
|
+
return [
|
|
432
|
+
...(set.length ? [step(`${alter} SET (${set.map(([k, v]) => `${k} = ${/^[A-Za-z0-9_.-]+$/.test(v) ? v : pgString(v)}`).join(", ")})`, classOf(rule), rule)] : []),
|
|
433
|
+
...(reset.length ? [step(`${alter} RESET (${reset.join(", ")})`, classOf(rule), rule)] : []),
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
/** `ALTER SEQUENCE`'s options for what is declared, every option left out set back to its default. */
|
|
437
|
+
export function sequenceOptionsSql(p) {
|
|
438
|
+
const inc = (p.increment ?? "1").replace(/\s+/g, "");
|
|
439
|
+
const ascending = !inc.startsWith("-");
|
|
440
|
+
const start = p.start ?? (ascending ? (p.minValue ?? "1") : (p.maxValue ?? "-1"));
|
|
441
|
+
return [
|
|
442
|
+
`AS ${p.dataType ?? "bigint"}`,
|
|
443
|
+
`INCREMENT BY ${inc}`,
|
|
444
|
+
p.minValue === undefined || p.minValue === null ? "NO MINVALUE" : `MINVALUE ${p.minValue}`,
|
|
445
|
+
p.maxValue === undefined || p.maxValue === null ? "NO MAXVALUE" : `MAXVALUE ${p.maxValue}`,
|
|
446
|
+
`START WITH ${String(start).replace(/\s+/g, "")}`,
|
|
447
|
+
`CACHE ${p.cache ?? "1"}`,
|
|
448
|
+
p.cycle ? "CYCLE" : "NO CYCLE",
|
|
449
|
+
`OWNED BY ${p.ownedBy ?? "NONE"}`,
|
|
450
|
+
].join(" ");
|
|
451
|
+
}
|
|
452
|
+
// ── Changes ────────────────────────────────────────────────────────────
|
|
453
|
+
/** Changes a plan refuses to make in place: expand and contract, by the migration Op (#3281). */
|
|
454
|
+
export const isRefused = (c) => c.class === "expand";
|
|
455
|
+
/** Changes that destroy data, which only an apply allowed to delete makes. */
|
|
456
|
+
export const isDestructive = (c) => c.rule === "SQLPG204";
|
|
457
|
+
/** Changes that replace the object: drop it and create it again from its declaration. */
|
|
458
|
+
export const isRecreate = (c) => c.rule === "SQLPG252" || c.rule === "SQLPG243" || c.rule === "SQLPG281" || c.rule === "SQLPG300";
|
|
459
|
+
/**
|
|
460
|
+
* SET NOT NULL on a table that exists, without reading the table under
|
|
461
|
+
* ACCESS EXCLUSIVE (SQLPG210): a CHECK (column IS NOT NULL) added NOT VALID,
|
|
462
|
+
* which reads no rows; validated, which reads them under SHARE UPDATE
|
|
463
|
+
* EXCLUSIVE while writes go on; then SET NOT NULL, which the valid check
|
|
464
|
+
* proves without a scan (12 and later), and the check dropped. The applier
|
|
465
|
+
* commits between the catalog changes and the validation, so the brief
|
|
466
|
+
* ACCESS EXCLUSIVE locks are never held across the scan. A check left by an
|
|
467
|
+
* interrupted run is validated as found. The first statement carries the
|
|
468
|
+
* NULL count as its pre-check. 18's `NOT NULL ... NOT VALID` would save the
|
|
469
|
+
* last statement; the check is used on every major for one sequence.
|
|
470
|
+
*/
|
|
471
|
+
export function setNotNullSteps(table, column, live) {
|
|
472
|
+
const check = quoteIdent(notNullCheckName(column));
|
|
473
|
+
const left = live?.constraints.some((k) => k.name === notNullCheckName(column)) ?? false;
|
|
474
|
+
const steps = [
|
|
475
|
+
...(left ? [] : [step(`ALTER TABLE ${table} ADD CONSTRAINT ${check} CHECK (${quoteIdent(column)} IS NOT NULL) NOT VALID`, "metadata", "SQLPG217")]),
|
|
476
|
+
step(`ALTER TABLE ${table} VALIDATE CONSTRAINT ${check}`, "validate", "SQLPG220"),
|
|
477
|
+
step(`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent(column)} SET NOT NULL`, "metadata", "SQLPG210"),
|
|
478
|
+
step(`ALTER TABLE ${table} DROP CONSTRAINT ${check}`, "metadata", "SQLPG223"),
|
|
479
|
+
];
|
|
480
|
+
steps[0] = { ...steps[0], precheck: nullPrecheck(table, column) };
|
|
481
|
+
return steps;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* The statements for an object that exists on the server, change by change.
|
|
485
|
+
* `live` is the server's definition, matched to the declaration (a renamed
|
|
486
|
+
* index by its `-- previously:` name). What has no statement in place is
|
|
487
|
+
* returned in `unsupported`, so the applier can refuse the object rather
|
|
488
|
+
* than make half of it.
|
|
489
|
+
*/
|
|
490
|
+
export function alterSteps(obj, live, changes, opts) {
|
|
491
|
+
const steps = [];
|
|
492
|
+
const unsupported = [];
|
|
493
|
+
const d = obj.canonical;
|
|
494
|
+
const target = objectIdent(d);
|
|
495
|
+
const alterKind = `ALTER ${KIND_WORDS[d.kind]} ${target}`;
|
|
496
|
+
const columns = new Map((obj.props.columns ?? []).map((c) => [c.name, c]));
|
|
497
|
+
const liveColumns = new Map(live.columns.map((c) => [c.name, c]));
|
|
498
|
+
const declaredColumns = new Map(d.columns.map((c) => [c.name, c]));
|
|
499
|
+
let constraintsDone = false;
|
|
500
|
+
let recreated = false;
|
|
501
|
+
const notNullChecks = new Set(changes.filter((c) => c.rule === "SQLPG210" && c.class !== "metadata").map((c) => notNullCheckName(COLUMN_FIELD.exec(c.field)?.[1] ?? "")));
|
|
502
|
+
for (const c of changes) {
|
|
503
|
+
const no = (why) => unsupported.push({ change: c, why });
|
|
504
|
+
if (isRecreate(c)) {
|
|
505
|
+
if (recreated)
|
|
506
|
+
continue;
|
|
507
|
+
recreated = true;
|
|
508
|
+
const concurrently = d.kind === "index" && obj.props.concurrently === true;
|
|
509
|
+
steps.push(step(d.kind === "index" ? `DROP INDEX${concurrently ? " CONCURRENTLY" : ""} ${objectIdent(live)}` : `DROP ${KIND_WORDS[d.kind]} ${objectIdent(live)}`, c.class, c.rule, !concurrently));
|
|
510
|
+
steps.push(...createSteps(obj, opts.marker, { cls: c.class, rule: c.rule }));
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (c.field.startsWith("constraints.")) {
|
|
514
|
+
if (!constraintsDone)
|
|
515
|
+
steps.push(...constraintSteps(obj, live, notNullChecks));
|
|
516
|
+
constraintsDone = true;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const col = COLUMN_FIELD.exec(c.field);
|
|
520
|
+
if (col && (d.kind === "table")) {
|
|
521
|
+
const name = col[1];
|
|
522
|
+
const prop = col[2];
|
|
523
|
+
const def = columns.get(name);
|
|
524
|
+
const dc = declaredColumns.get(name);
|
|
525
|
+
const lc = liveColumns.get(name);
|
|
526
|
+
const column = `${alterKind} ALTER COLUMN ${quoteIdent(name)}`;
|
|
527
|
+
switch (c.rule) {
|
|
528
|
+
case "SQLPG201":
|
|
529
|
+
case "SQLPG202":
|
|
530
|
+
if (!def)
|
|
531
|
+
no(`column ${name} is not in the declaration`);
|
|
532
|
+
else
|
|
533
|
+
steps.push(step(`${alterKind} ADD COLUMN ${columnDefinition(def)}`, c.class, c.rule));
|
|
534
|
+
break;
|
|
535
|
+
case "SQLPG204":
|
|
536
|
+
steps.push(step(`${alterKind} DROP COLUMN ${quoteIdent(name)}`, c.class, c.rule));
|
|
537
|
+
break;
|
|
538
|
+
case "SQLPG206":
|
|
539
|
+
case "SQLPG207":
|
|
540
|
+
case "SQLPG214":
|
|
541
|
+
if (!def?.type)
|
|
542
|
+
no(`column ${name} declares no type`);
|
|
543
|
+
else
|
|
544
|
+
steps.push(step(`${column} TYPE ${def.type}${def.collate ? ` COLLATE ${def.collate}` : prop === "collate" ? ' COLLATE "default"' : ""}`, c.class, c.rule));
|
|
545
|
+
break;
|
|
546
|
+
case "SQLPG209":
|
|
547
|
+
steps.push(step(def?.default !== undefined ? `${column} SET DEFAULT ${def.default}` : `${column} DROP DEFAULT`, c.class, c.rule));
|
|
548
|
+
break;
|
|
549
|
+
case "SQLPG210":
|
|
550
|
+
// A valid check the server already holds proves it (the diff classes that metadata): SET NOT NULL alone.
|
|
551
|
+
if (c.class === "metadata")
|
|
552
|
+
steps.push(step(`${column} SET NOT NULL`, c.class, c.rule));
|
|
553
|
+
else
|
|
554
|
+
steps.push(...setNotNullSteps(target, name, live));
|
|
555
|
+
break;
|
|
556
|
+
case "SQLPG211":
|
|
557
|
+
steps.push(step(`${column} DROP NOT NULL`, c.class, c.rule));
|
|
558
|
+
break;
|
|
559
|
+
case "SQLPG212": {
|
|
560
|
+
const before = lc?.generated?.split(" ")[0];
|
|
561
|
+
const after = def?.generated?.kind;
|
|
562
|
+
if (after && after !== "identity" && before === after)
|
|
563
|
+
steps.push(step(`${column} SET EXPRESSION AS (${def.generated.expr ?? ""})`, c.class, c.rule));
|
|
564
|
+
else if (!after && before === "stored")
|
|
565
|
+
steps.push(step(`${column} DROP EXPRESSION`, c.class, c.rule));
|
|
566
|
+
else
|
|
567
|
+
no(`a generated column's kind cannot change in place (${before ?? "not generated"} to ${after ?? "not generated"}); the column is dropped and added`);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case "SQLPG213": {
|
|
571
|
+
const g = def?.generated?.kind === "identity" ? def.generated : undefined;
|
|
572
|
+
if (!g)
|
|
573
|
+
steps.push(step(`${column} DROP IDENTITY`, c.class, c.rule));
|
|
574
|
+
else if (!lc?.identity)
|
|
575
|
+
steps.push(step(`${column} ADD GENERATED ${g.always ? "ALWAYS" : "BY DEFAULT"} AS IDENTITY${g.options ? ` (${g.options})` : ""}`, c.class, c.rule));
|
|
576
|
+
else
|
|
577
|
+
steps.push(step(`${column} SET GENERATED ${g.always ? "ALWAYS" : "BY DEFAULT"} ${identitySetClauses(g.options).join(" ")}`, c.class, c.rule));
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
case "SQLPG215": {
|
|
581
|
+
if (dc?.storage !== lc?.storage) {
|
|
582
|
+
if (def?.storage)
|
|
583
|
+
steps.push(step(`${column} SET STORAGE ${def.storage}`, c.class, c.rule));
|
|
584
|
+
else if (opts.major >= 16)
|
|
585
|
+
steps.push(step(`${column} SET STORAGE DEFAULT`, c.class, c.rule));
|
|
586
|
+
else
|
|
587
|
+
no(`Postgres ${opts.major} has no SET STORAGE DEFAULT; write the type's storage to set it back`);
|
|
588
|
+
}
|
|
589
|
+
if (dc?.compression !== lc?.compression)
|
|
590
|
+
steps.push(step(`${column} SET COMPRESSION ${def?.compression ?? "DEFAULT"}`, c.class, c.rule));
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
case "SQLPG216":
|
|
594
|
+
steps.push(step(`COMMENT ON COLUMN ${target}.${quoteIdent(name)} IS ${dc?.comment === undefined ? "NULL" : pgString(dc.comment)}`, c.class, c.rule));
|
|
595
|
+
break;
|
|
596
|
+
default:
|
|
597
|
+
no("no in-place statement");
|
|
598
|
+
}
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
switch (c.rule) {
|
|
602
|
+
case "SQLPG216":
|
|
603
|
+
if (c.field === "comment")
|
|
604
|
+
steps.push(step(commentStatement(obj, opts.marker), c.class, c.rule));
|
|
605
|
+
else if (c.field === "columnComments") {
|
|
606
|
+
const dcc = (d.fields.columnComments ?? {});
|
|
607
|
+
const lcc = (live.fields.columnComments ?? {});
|
|
608
|
+
for (const k of [...new Set([...Object.keys(lcc), ...Object.keys(dcc)])]) {
|
|
609
|
+
if (dcc[k] !== lcc[k])
|
|
610
|
+
steps.push(step(`COMMENT ON COLUMN ${target}.${quoteIdent(k)} IS ${dcc[k] === undefined ? "NULL" : pgString(dcc[k])}`, c.class, c.rule));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// VALID to NOT VALID on a constraint is a note, not a change: nothing to send.
|
|
614
|
+
break;
|
|
615
|
+
case "SQLPG224":
|
|
616
|
+
case "SQLPG253":
|
|
617
|
+
if (c.field === "checkOption") {
|
|
618
|
+
const opt = d.fields.checkOption;
|
|
619
|
+
steps.push(step(opt ? `${alterKind} SET (check_option = ${opt})` : `${alterKind} RESET (check_option)`, c.class, c.rule));
|
|
620
|
+
}
|
|
621
|
+
else
|
|
622
|
+
steps.push(...optionSteps(alterKind, d.fields.with, live.fields.with, c.rule));
|
|
623
|
+
break;
|
|
624
|
+
case "SQLPG225":
|
|
625
|
+
steps.push(step(`${alterKind} SET ${d.fields.unlogged ? "UNLOGGED" : "LOGGED"}`, c.class, c.rule));
|
|
626
|
+
break;
|
|
627
|
+
case "SQLPG226":
|
|
628
|
+
if (c.field === "using")
|
|
629
|
+
steps.push(step(`${alterKind} SET ACCESS METHOD ${quoteIdent(String(d.fields.using ?? "heap"))}`, c.class, c.rule));
|
|
630
|
+
else
|
|
631
|
+
steps.push(step(`${alterKind} SET TABLESPACE ${quoteIdent(String(d.fields.tablespace ?? "pg_default"))}`, c.class, c.rule));
|
|
632
|
+
break;
|
|
633
|
+
case "SQLPG229":
|
|
634
|
+
steps.push(step(`ALTER INDEX ${objectIdent(live)} RENAME TO ${quoteIdent(d.name)}`, c.class, c.rule));
|
|
635
|
+
break;
|
|
636
|
+
case "SQLPG250":
|
|
637
|
+
case "SQLPG280":
|
|
638
|
+
case "SQLPG302":
|
|
639
|
+
steps.push(...createSteps(obj, opts.marker, { orReplace: true, cls: c.class, rule: c.rule }).slice(0, 1));
|
|
640
|
+
break;
|
|
641
|
+
case "SQLPG291": {
|
|
642
|
+
const p = obj.props;
|
|
643
|
+
const recreate = ["table", "restrictive", "command"].some((k) => !same(d.fields[k], live.fields[k])) ||
|
|
644
|
+
(live.fields.using !== undefined && d.fields.using === undefined) ||
|
|
645
|
+
(live.fields.check !== undefined && d.fields.check === undefined);
|
|
646
|
+
if (recreate) {
|
|
647
|
+
steps.push(step(`DROP POLICY ${objectIdent(live)}`, c.class, c.rule));
|
|
648
|
+
steps.push(...createSteps(obj, opts.marker, { cls: c.class, rule: c.rule }));
|
|
649
|
+
}
|
|
650
|
+
else {
|
|
651
|
+
const roles = p.roles.map((r) => (r === "public" ? "PUBLIC" : quoteIdent(r))).join(", ");
|
|
652
|
+
steps.push(step([`ALTER POLICY ${objectIdent(d)} TO ${roles}`, p.using ? `USING (${p.using})` : "", p.check ? `WITH CHECK (${p.check})` : ""].filter(Boolean).join(" "), c.class, c.rule));
|
|
653
|
+
}
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case "SQLPG293":
|
|
657
|
+
if (!same(d.fields.rowSecurity, live.fields.rowSecurity))
|
|
658
|
+
steps.push(step(`${alterKind} ${d.fields.rowSecurity ? "ENABLE" : "DISABLE"} ROW LEVEL SECURITY`, c.class, c.rule));
|
|
659
|
+
if (!same(d.fields.forceRowSecurity, live.fields.forceRowSecurity))
|
|
660
|
+
steps.push(step(`${alterKind} ${d.fields.forceRowSecurity ? "FORCE" : "NO FORCE"} ROW LEVEL SECURITY`, c.class, c.rule));
|
|
661
|
+
break;
|
|
662
|
+
case "SQLPG294": {
|
|
663
|
+
if (steps.some((x) => x.rule === "SQLPG294"))
|
|
664
|
+
break;
|
|
665
|
+
const want = new Set(d.fields.attributes ?? []);
|
|
666
|
+
const have = new Set(live.fields.attributes ?? []);
|
|
667
|
+
const flip = (a) => (a.startsWith("no") ? a.slice(2) : `no${a}`);
|
|
668
|
+
// An attribute at its default is in neither set: write the side that changes.
|
|
669
|
+
const words = [...[...want].filter((a) => !have.has(a)), ...[...have].filter((a) => !want.has(a)).map(flip)];
|
|
670
|
+
const limit = d.fields.connectionLimit !== live.fields.connectionLimit ? [`CONNECTION LIMIT ${String(d.fields.connectionLimit ?? -1)}`] : [];
|
|
671
|
+
steps.push(step(`ALTER ROLE ${objectIdent(d)} WITH ${[...words.map((w) => w.toUpperCase()), ...limit].join(" ")}`, c.class, c.rule));
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
case "SQLPG284":
|
|
675
|
+
if (d.fields.constraint === true || live.fields.constraint === true || d.signature !== live.signature) {
|
|
676
|
+
steps.push(step(`DROP TRIGGER ${objectIdent(live)}`, c.class, c.rule));
|
|
677
|
+
steps.push(...createSteps(obj, opts.marker, { cls: c.class, rule: c.rule }));
|
|
678
|
+
}
|
|
679
|
+
else
|
|
680
|
+
steps.push(...createSteps(obj, opts.marker, { orReplace: true, cls: c.class, rule: c.rule }).slice(0, 1));
|
|
681
|
+
break;
|
|
682
|
+
case "SQLPG260": {
|
|
683
|
+
const before = live.fields.labels ?? [];
|
|
684
|
+
const after = d.fields.labels ?? [];
|
|
685
|
+
const present = new Set(before);
|
|
686
|
+
after.forEach((label, i) => {
|
|
687
|
+
if (present.has(label))
|
|
688
|
+
return;
|
|
689
|
+
const prev = after.slice(0, i).reverse().find((l) => present.has(l));
|
|
690
|
+
const next = after.slice(i + 1).find((l) => present.has(l));
|
|
691
|
+
const where = prev !== undefined ? ` AFTER ${pgString(prev)}` : next !== undefined ? ` BEFORE ${pgString(next)}` : "";
|
|
692
|
+
// Kept out of a transaction: a label added in one cannot be used in it.
|
|
693
|
+
steps.push(step(`ALTER TYPE ${target} ADD VALUE ${pgString(label)}${where}`, c.class, c.rule, false));
|
|
694
|
+
present.add(label);
|
|
695
|
+
});
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
case "SQLPG262":
|
|
699
|
+
case "SQLPG263":
|
|
700
|
+
if (c.field === "notNull")
|
|
701
|
+
steps.push(step(`${alterKind} ${d.fields.notNull ? "SET" : "DROP"} NOT NULL`, c.class, c.rule));
|
|
702
|
+
else if (c.field === "default")
|
|
703
|
+
steps.push(step(d.fields.default !== undefined ? `${alterKind} SET DEFAULT ${obj.props.default}` : `${alterKind} DROP DEFAULT`, c.class, c.rule));
|
|
704
|
+
else
|
|
705
|
+
no("no in-place statement");
|
|
706
|
+
break;
|
|
707
|
+
case "SQLPG265":
|
|
708
|
+
if (!steps.some((s) => s.rule === "SQLPG265"))
|
|
709
|
+
steps.push(step(`${alterKind} ${sequenceOptionsSql(obj.props)}`, c.class, c.rule));
|
|
710
|
+
break;
|
|
711
|
+
case "SQLPG266":
|
|
712
|
+
if (c.field === "version")
|
|
713
|
+
steps.push(step(`${alterKind} UPDATE${d.fields.version ? ` TO ${pgString(String(d.fields.version))}` : ""}`, c.class, c.rule));
|
|
714
|
+
else if (d.fields.schema !== undefined)
|
|
715
|
+
steps.push(step(`${alterKind} SET SCHEMA ${quoteIdent(String(d.fields.schema))}`, c.class, c.rule));
|
|
716
|
+
break;
|
|
717
|
+
case "SQLPG267":
|
|
718
|
+
if (obj.props.authorization !== undefined)
|
|
719
|
+
steps.push(step(`${alterKind} OWNER TO ${String(obj.props.authorization)}`, c.class, c.rule));
|
|
720
|
+
break;
|
|
721
|
+
default:
|
|
722
|
+
no("no in-place statement");
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return { steps, unsupported };
|
|
726
|
+
}
|
|
727
|
+
/** Whether a change class reads or rewrites rows, so its statement runs in a transaction of its own with the scan timeout. */
|
|
728
|
+
export const scansRows = (cls) => cls === "rewrite" || cls === "validate" || cls === "concurrently";
|
|
729
|
+
/**
|
|
730
|
+
* The detail of a refused object: each change, the rule and restriction
|
|
731
|
+
* behind it, and the Op that makes it. `declared` is the object as declared,
|
|
732
|
+
* which names the table a column rename or type change is handed to
|
|
733
|
+
* `PostgresMigrationOp` for (`../migrate/handoff.ts`).
|
|
734
|
+
*/
|
|
735
|
+
export function refusalDetail(changes, name, declared) {
|
|
736
|
+
const lines = changes.map((c) => {
|
|
737
|
+
const r = PG_CLASSIFIER_RULES[c.rule];
|
|
738
|
+
const what = [c.field, c.before !== undefined || c.after !== undefined ? `${c.before ?? "-"} -> ${c.after ?? "-"}` : ""].filter(Boolean).join(" ");
|
|
739
|
+
return `${r.id} ${r.title} (${what}): ${c.note ? `${c.note}. ` : ""}${r.restriction} ${r.cite}`;
|
|
740
|
+
});
|
|
741
|
+
const ops = [...new Set(changes.map((c) => migrationTarget(c, declared)).filter((t) => t !== undefined).map((t) => `PostgresMigrationOp({ table: ${JSON.stringify(t.table)}, column: ${JSON.stringify(t.column)}, ... })`))];
|
|
742
|
+
const instead = ops.length > 0
|
|
743
|
+
? `Run it as the expand-and-contract migration Op: ${ops.join(", ")} from @intentius/chant-lexicon-sql/postgres (add the new column, write both, backfill, switch readers, then drop the old).` +
|
|
744
|
+
(ops.length < changes.length ? " The other changes have no migration Op yet: make them by hand as expand and contract." : "")
|
|
745
|
+
: "No migration Op makes this change yet: make it by hand as expand and contract (add the new, write both, backfill, move readers, then drop the old).";
|
|
746
|
+
// A clause chant does not read (#3872) is refused for review: what the change takes is not known.
|
|
747
|
+
if (changes.every((c) => c.rule === "SQLPG299"))
|
|
748
|
+
return `${name} needs review, since chant does not read the clause, so nothing was sent for it. ${lines.join(" ")}`;
|
|
749
|
+
return `${name} needs expand and contract, which no in-place statement makes, so nothing was sent for it. ${lines.join(" ")} ${instead}`;
|
|
750
|
+
}
|
|
751
|
+
/** The rules that create an object, which label its create statements. */
|
|
752
|
+
const CREATE_RULES = new Set(["SQLPG200", "SQLPG240", "SQLPG241", "SQLPG283", "SQLPG290"]);
|
|
753
|
+
/** Drop order: what reads from a table before the table, types after the tables using them, a schema last. */
|
|
754
|
+
const DROP_ORDER = {
|
|
755
|
+
grant: 0,
|
|
756
|
+
defaultPrivileges: 0,
|
|
757
|
+
policy: 0,
|
|
758
|
+
trigger: 0,
|
|
759
|
+
materializedView: 1,
|
|
760
|
+
view: 2,
|
|
761
|
+
index: 3,
|
|
762
|
+
table: 4,
|
|
763
|
+
sequence: 5,
|
|
764
|
+
// An event trigger and a cast before the function they call; an aggregate before its functions.
|
|
765
|
+
eventTrigger: 5.5,
|
|
766
|
+
cast: 5.5,
|
|
767
|
+
aggregate: 5.5,
|
|
768
|
+
// A routine after what calls it (a view, a trigger, a default), before the types its parameters use.
|
|
769
|
+
function: 6,
|
|
770
|
+
procedure: 6,
|
|
771
|
+
domain: 7,
|
|
772
|
+
composite: 7.5,
|
|
773
|
+
range: 7.5,
|
|
774
|
+
enum: 8,
|
|
775
|
+
// A collation after the types and columns that use it.
|
|
776
|
+
collation: 8.5,
|
|
777
|
+
extension: 9,
|
|
778
|
+
schema: 10,
|
|
779
|
+
// A role is the cluster's: never dropped by a prune (`../live/catalog.ts` reads only the declared ones).
|
|
780
|
+
role: 11,
|
|
781
|
+
};
|
|
782
|
+
/** A unique index's duplicate count, when every element is a plain column; undefined otherwise. */
|
|
783
|
+
function uniqueIndexPrecheck(p) {
|
|
784
|
+
if (!p.unique || p.elements.length === 0 || p.elements.some((e) => e.column === undefined))
|
|
785
|
+
return undefined;
|
|
786
|
+
return duplicatePrecheck(p.tableName, p.elements.map((e) => e.column), { ...(p.nullsNotDistinct ? { nullsNotDistinct: true } : {}), ...(p.where ? { where: p.where } : {}) });
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* The statements that take the current schema to the declared one, from the
|
|
790
|
+
* classified changes between them. Pure: nothing is read or sent. The applier
|
|
791
|
+
* (`./apply.ts`) groups the steps into transactions and runs them;
|
|
792
|
+
* `diffStatements` (`../../migration-statements.ts`) renders the same steps
|
|
793
|
+
* for a migration file.
|
|
794
|
+
*/
|
|
795
|
+
export function planPgStatements(input) {
|
|
796
|
+
const keyOf = input.keyOf ?? ((o) => o.key);
|
|
797
|
+
const byObject = new Map();
|
|
798
|
+
for (const c of input.changes)
|
|
799
|
+
byObject.set(c.object, [...(byObject.get(c.object) ?? []), c]);
|
|
800
|
+
const currentByKey = new Map(input.current.map((o) => [o.key, o.canonical]));
|
|
801
|
+
const matched = new Map();
|
|
802
|
+
for (const m of matchPgObjects(input.current, input.declared.map((o) => ({ key: keyOf(o), canonical: o.canonical })))) {
|
|
803
|
+
if (m.kind === "matched")
|
|
804
|
+
matched.set(m.after.key, m.before.key);
|
|
805
|
+
}
|
|
806
|
+
const recreatedRelations = new Set();
|
|
807
|
+
// Grants and default privileges are access, made by their own statements (`../access/acl.ts`).
|
|
808
|
+
const objects = input.declared.filter((obj) => !ACCESS_KINDS.has(obj.canonical.kind)).map((obj) => {
|
|
809
|
+
const key = keyOf(obj);
|
|
810
|
+
const currentKey = matched.get(key);
|
|
811
|
+
const live = currentKey !== undefined ? currentByKey.get(currentKey) : undefined;
|
|
812
|
+
const mine = byObject.get(key) ?? [];
|
|
813
|
+
const withLive = live ? { live } : {};
|
|
814
|
+
if (live?.foreign)
|
|
815
|
+
return { verdict: "foreign", obj, changes: mine, live, detail: `${live.foreign} keeps ${obj.name}; it is not chant's to change` };
|
|
816
|
+
const refused = mine.filter(isRefused);
|
|
817
|
+
if (refused.length > 0)
|
|
818
|
+
return { verdict: "refused", obj, changes: mine, refused, ...withLive, detail: refusalDetail(refused, obj.name, obj.canonical) };
|
|
819
|
+
const destructive = mine.filter(isDestructive);
|
|
820
|
+
if (destructive.length > 0 && !input.allowDestructive) {
|
|
821
|
+
return {
|
|
822
|
+
verdict: "withheld",
|
|
823
|
+
obj,
|
|
824
|
+
changes: mine,
|
|
825
|
+
withheld: destructive,
|
|
826
|
+
...withLive,
|
|
827
|
+
detail: `drops ${destructive.map((c) => c.field).join(", ")}, which destroys the data in it; an apply that may delete (prune, ApplyOp delete "owned-only" or "gated") makes it`,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
// An index on a materialized view this plan recreates went with the view, so it is created again.
|
|
831
|
+
const onRecreated = obj.canonical.kind === "index" && recreatedRelations.has(String(obj.canonical.fields.table));
|
|
832
|
+
if (!live || onRecreated) {
|
|
833
|
+
const base = obj.canonical.kind === "extension" && !live ? input.extensionComment?.(obj.canonical.name) : undefined;
|
|
834
|
+
// An index on a table that exists is built under the scan timeout (SQLPG240, SQLPG241), and a trigger or policy
|
|
835
|
+
// on one locks it (SQLPG283, SQLPG290); on a new table each is part of the create (SQLPG200).
|
|
836
|
+
const how = mine.find((c) => CREATE_RULES.has(c.rule));
|
|
837
|
+
const steps = createSteps(obj, input.marker, { ...(base !== undefined ? { base } : {}), ...(how ? { cls: how.class, rule: how.rule } : {}), ...(input.access === false ? { access: false } : {}) });
|
|
838
|
+
// A unique index on a table that has rows fails on duplicates: its build carries their count.
|
|
839
|
+
const unique = how && how.rule !== "SQLPG200" && obj.canonical.kind === "index" ? uniqueIndexPrecheck(obj.props) : undefined;
|
|
840
|
+
if (unique && steps[0])
|
|
841
|
+
steps[0] = { ...steps[0], precheck: unique };
|
|
842
|
+
return { verdict: "create", obj, changes: mine, steps, ...withLive };
|
|
843
|
+
}
|
|
844
|
+
const altered = alterSteps(obj, live, mine, { ...(input.marker ? { marker: input.marker } : {}), major: input.major });
|
|
845
|
+
if (altered.unsupported.length > 0) {
|
|
846
|
+
return {
|
|
847
|
+
verdict: "unsupported",
|
|
848
|
+
obj,
|
|
849
|
+
changes: mine,
|
|
850
|
+
unsupported: altered.unsupported,
|
|
851
|
+
live,
|
|
852
|
+
detail: `${obj.name}: ${altered.unsupported.map((u) => `${u.change.rule} ${u.change.field}: ${u.why}`).join("; ")}. Nothing was sent for it.`,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
const steps = altered.steps;
|
|
856
|
+
if (mine.some((c) => c.rule === "SQLPG252"))
|
|
857
|
+
recreatedRelations.add(`${obj.canonical.schema}.${obj.canonical.name}`);
|
|
858
|
+
const stamp = commentStatement(obj, input.marker);
|
|
859
|
+
if (!steps.some((s) => s.sql === stamp) && !(input.carriesMarker?.(live, currentKey) ?? true))
|
|
860
|
+
steps.push(step(stamp, "metadata", "SQLPG216"));
|
|
861
|
+
return { verdict: "alter", obj, changes: mine, steps, live };
|
|
862
|
+
});
|
|
863
|
+
const drops = [];
|
|
864
|
+
for (const c of input.changes) {
|
|
865
|
+
if (!((c.rule === "SQLPG270" || c.rule === "SQLPG242" || c.rule === "SQLPG285" || c.rule === "SQLPG292") && c.after === undefined))
|
|
866
|
+
continue;
|
|
867
|
+
const o = currentByKey.get(c.object);
|
|
868
|
+
if (!o || o.foreign)
|
|
869
|
+
continue;
|
|
870
|
+
const schema = o.kind === "schema" || o.kind === "extension" ? undefined : o.schema;
|
|
871
|
+
drops.push({
|
|
872
|
+
key: c.object,
|
|
873
|
+
kind: o.kind,
|
|
874
|
+
...(schema !== undefined ? { schema } : {}),
|
|
875
|
+
name: o.name,
|
|
876
|
+
...(o.signature !== undefined ? { signature: o.signature } : {}),
|
|
877
|
+
step: dropStatement(o.kind, schema, o.name, o.signature),
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
drops.sort((a, b) => DROP_ORDER[a.kind] - DROP_ORDER[b.kind]);
|
|
881
|
+
return { objects, drops };
|
|
882
|
+
}
|