@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,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a Postgres plan hands an expand-and-contract change to the migration
|
|
3
|
+
* Op (#3281), as the ClickHouse plan hands a rebuild to its Op (#3250).
|
|
4
|
+
*
|
|
5
|
+
* `chant sql plan`, `chant sql diff`, the `classify-change` MCP tool and the
|
|
6
|
+
* applier refuse an expand-and-contract change in place. For a column
|
|
7
|
+
* rename (SQLPG205) and a type change across kinds (SQLPG208) what they hand
|
|
8
|
+
* on is a `PostgresMigrationOp` declaration per column, ready to put in an
|
|
9
|
+
* `*.op.ts` file, reviewed in the pull request like the schema change it
|
|
10
|
+
* carries out. The Op's own Plan phase classifies the change again against
|
|
11
|
+
* the server. The other expand-and-contract rules (a NOT NULL column with no
|
|
12
|
+
* default, partitioning, an object rename, a view's columns, enum labels, a
|
|
13
|
+
* domain's type, an object's kind) have no Op yet, and get none.
|
|
14
|
+
*
|
|
15
|
+
* Nothing is written for the user. The hand-off's shape is the shared
|
|
16
|
+
* core's (`../../core/handoff.ts`).
|
|
17
|
+
*/
|
|
18
|
+
import { migrationOpName, renderMigrationOps } from "../../core/handoff.js";
|
|
19
|
+
/** The rules a refused change is handed to the Op for. */
|
|
20
|
+
export const HANDED_OFF = new Set(["SQLPG205", "SQLPG208"]);
|
|
21
|
+
const COLUMN = /^columns\.([^.]+)(?:\.type)?$/;
|
|
22
|
+
/** The table and column a refused change is about, when the Op makes it. */
|
|
23
|
+
export function migrationTarget(c, declared, defaultSchema = "public") {
|
|
24
|
+
if (!HANDED_OFF.has(c.rule) || !declared || declared.kind !== "table")
|
|
25
|
+
return undefined;
|
|
26
|
+
const m = COLUMN.exec(c.field);
|
|
27
|
+
if (!m || (c.rule === "SQLPG208" && !c.field.endsWith(".type")) || (c.rule === "SQLPG205" && c.field.endsWith(".type")))
|
|
28
|
+
return undefined;
|
|
29
|
+
return { table: `${declared.schema ?? defaultSchema}.${declared.name}`, column: m[1] };
|
|
30
|
+
}
|
|
31
|
+
/** The Op's declaration for one column. */
|
|
32
|
+
export function migrationDeclaration(name, env, table, column) {
|
|
33
|
+
return `export const { op } = PostgresMigrationOp({ name: ${JSON.stringify(name)}, env: ${JSON.stringify(env)}, table: ${JSON.stringify(table)}, column: ${JSON.stringify(column)} });`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* One suggestion per column the diff refuses for a rename or a type change
|
|
37
|
+
* across kinds. `declared` maps the diff's object keys to the declared
|
|
38
|
+
* definitions.
|
|
39
|
+
*/
|
|
40
|
+
export function migrationOpSuggestions(refused, declared, env, defaultSchema = "public") {
|
|
41
|
+
const out = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const c of refused) {
|
|
44
|
+
const t = migrationTarget(c, declared.get(c.object), defaultSchema);
|
|
45
|
+
if (!t)
|
|
46
|
+
continue;
|
|
47
|
+
const id = `${t.table}.${t.column}`;
|
|
48
|
+
if (seen.has(id))
|
|
49
|
+
continue;
|
|
50
|
+
seen.add(id);
|
|
51
|
+
const name = migrationOpName("migrate", id);
|
|
52
|
+
out.push({ table: t.table, column: t.column, rule: c.rule, name, env, declaration: migrationDeclaration(name, env, t.table, t.column) });
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** The suggestions as text, for the report. */
|
|
57
|
+
export function renderMigrationOpSuggestions(ops) {
|
|
58
|
+
return renderMigrationOps(ops, { what: "the expand-and-contract migration Op", exportName: "PostgresMigrationOp", importPath: "@intentius/chant-lexicon-sql/postgres" });
|
|
59
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names an expand-and-contract migration of one column uses (#3281), and
|
|
3
|
+
* the trailer that marks its working objects.
|
|
4
|
+
*
|
|
5
|
+
* A migration is keyed by `<schema>.<table>.<column>`, the column as the
|
|
6
|
+
* build declares it. Its working objects are:
|
|
7
|
+
*
|
|
8
|
+
* - the new column: the declared name for a rename (`login`), or
|
|
9
|
+
* `<column>__chant_new` for a type change, until the switch;
|
|
10
|
+
* - the old column after the switch: the old name for a rename (`email`,
|
|
11
|
+
* still written by the dual-write trigger), or `<column>__chant_old` for a
|
|
12
|
+
* type change (no longer written);
|
|
13
|
+
* - the dual-write trigger `<column>__chant_sync` on the table and its
|
|
14
|
+
* function `<table>__<column>__chant_sync` in the table's schema;
|
|
15
|
+
* - the check `<new>__chant_nn` that proves NOT NULL before it is set.
|
|
16
|
+
*
|
|
17
|
+
* Each carries chant's ownership trailer (`../../core/ownership.ts`) with
|
|
18
|
+
* `migration=<key>` and its role in its comment. The catalog read
|
|
19
|
+
* (`../live/catalog.ts`) leaves out a column or a constraint whose comment
|
|
20
|
+
* carries the `migration` key, so a plan, an import and a prune see the table
|
|
21
|
+
* as it was before the migration until the switch, and as declared after it.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { quoteIdent } from "../keywords.js";
|
|
25
|
+
/** The trailer key on a migration's working objects. */
|
|
26
|
+
export const MIGRATION_TRAILER_KEY = "migration";
|
|
27
|
+
/** The longest identifier Postgres keeps (NAMEDATALEN - 1 bytes). */
|
|
28
|
+
const MAX_IDENT = 63;
|
|
29
|
+
/** A name Postgres keeps whole: `base`, or its first bytes and a short hash of all of it when it is longer than 63 bytes. */
|
|
30
|
+
export function pgName(base) {
|
|
31
|
+
if (Buffer.byteLength(base, "utf8") <= MAX_IDENT)
|
|
32
|
+
return base;
|
|
33
|
+
const hash = createHash("sha256").update(base).digest("hex").slice(0, 8);
|
|
34
|
+
let head = base;
|
|
35
|
+
while (Buffer.byteLength(`${head}_${hash}`, "utf8") > MAX_IDENT)
|
|
36
|
+
head = head.slice(0, -1);
|
|
37
|
+
return `${head}_${hash}`;
|
|
38
|
+
}
|
|
39
|
+
export function migrationNames(schema, table, column, change, source) {
|
|
40
|
+
const newColumn = change === "rename" ? column : pgName(`${column}__chant_new`);
|
|
41
|
+
const fn = pgName(`${table}__${column}__chant_sync`);
|
|
42
|
+
return {
|
|
43
|
+
key: `${schema}.${table}.${column}`,
|
|
44
|
+
schema,
|
|
45
|
+
table,
|
|
46
|
+
column,
|
|
47
|
+
change,
|
|
48
|
+
source,
|
|
49
|
+
newColumn,
|
|
50
|
+
oldColumn: change === "rename" ? source : pgName(`${column}__chant_old`),
|
|
51
|
+
trigger: pgName(`${column}__chant_sync`),
|
|
52
|
+
fn,
|
|
53
|
+
check: pgName(`${newColumn}__chant_nn`),
|
|
54
|
+
qualifiedTable: pgQualified(schema, table),
|
|
55
|
+
qualifiedFn: pgQualified(schema, fn),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** `schema.name`, each part quoted where Postgres needs it. */
|
|
59
|
+
const pgQualified = (schema, name) => `${quoteIdent(schema)}.${quoteIdent(name)}`;
|
|
60
|
+
/** A quoted column name. */
|
|
61
|
+
export const col = (name) => quoteIdent(name);
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The expand-and-contract migration's view of the server (#3281): which
|
|
3
|
+
* column of which table is migrated, how far the migration has got, and
|
|
4
|
+
* what it will do.
|
|
5
|
+
*
|
|
6
|
+
* Every step of the Op re-observes from here rather than trusting what an
|
|
7
|
+
* earlier run or step said, so the Op converges: run it again after a crash,
|
|
8
|
+
* a gate or an approval, and each step finds its work done or still to do.
|
|
9
|
+
* The progress is read off the table itself: its columns, the dual-write
|
|
10
|
+
* trigger and its function, and the NOT NULL check, each carrying chant's
|
|
11
|
+
* trailer with `migration=<key>` and a role (`./names.ts`).
|
|
12
|
+
*
|
|
13
|
+
* The change is found the way `chant sql plan` finds it: the declared table
|
|
14
|
+
* against what the catalog read prints (`../plan/commands.ts`), which leaves
|
|
15
|
+
* the working columns out. So the plan shows the change refused until the
|
|
16
|
+
* switch, and the declaration held after it.
|
|
17
|
+
*
|
|
18
|
+
* States:
|
|
19
|
+
*
|
|
20
|
+
* - `migrate`: the declared column differs from the server's by a rename
|
|
21
|
+
* (SQLPG205) or a type change (SQLPG207, SQLPG208), and the switch has not
|
|
22
|
+
* run.
|
|
23
|
+
* - `switched`: the switch ran; the old column is kept until its date.
|
|
24
|
+
* - `done`: the server holds the declared column and nothing of the
|
|
25
|
+
* migration is left.
|
|
26
|
+
*
|
|
27
|
+
* Anything else is refused with a {@link MigrationRefusal}, naming what to
|
|
28
|
+
* do: a table that is not there, a partition (run it on the partitioned
|
|
29
|
+
* table) or an inheritance tree; no primary key to batch by; a column in the
|
|
30
|
+
* partition key; a generated column; a column change the Op does not make
|
|
31
|
+
* (a rename and a type change at once, a rename of a column with a default
|
|
32
|
+
* or a sequence); something that uses the column and is
|
|
33
|
+
* not carried over to the new one (`./carry.ts`: indexes, key, check and
|
|
34
|
+
* foreign key constraints and declared views are); a publication that sends
|
|
35
|
+
* it; a working object chant did not make.
|
|
36
|
+
*/
|
|
37
|
+
import { computePlanDigest } from "@intentius/chant/op";
|
|
38
|
+
import { carriesMarker, readTrailerPairs } from "../../core/ownership.js";
|
|
39
|
+
import { planAgainstClient } from "../plan/commands.js";
|
|
40
|
+
import { PG_CLASSIFIER_RULES } from "../plan/rules.js";
|
|
41
|
+
import { qualifiedKey } from "../plan/schema.js";
|
|
42
|
+
import { POSTGRES_ENTITY_TYPES } from "../entity-types.js";
|
|
43
|
+
import { col, migrationNames, MIGRATION_TRAILER_KEY, pgName } from "./names.js";
|
|
44
|
+
import { publicationsOf } from "./replication.js";
|
|
45
|
+
import { keyText } from "./batches.js";
|
|
46
|
+
import { carriedStates, carriedSubject, discoverDependents, replaceColumn, storedType } from "./carry.js";
|
|
47
|
+
/** A refusal: the migration cannot start, or cannot go on, for a reason a person has to act on. */
|
|
48
|
+
export class MigrationRefusal extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "MigrationRefusal";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The rules the Op makes: a rename, and a type change with or without a rewrite in place. */
|
|
55
|
+
export const MIGRATION_RULES = new Set(["SQLPG205", "SQLPG207", "SQLPG208"]);
|
|
56
|
+
/** The declared table with the given key (`schema.table`) or export name, or a refusal naming what is declared. */
|
|
57
|
+
export function declaredTable(declared, key) {
|
|
58
|
+
const found = declared.find((o) => o.name === key || o.exportName === key);
|
|
59
|
+
if (!found) {
|
|
60
|
+
throw new MigrationRefusal(`${key} is not declared in the build. Declared tables: ${declared.filter((o) => o.type === POSTGRES_ENTITY_TYPES.table).map((o) => o.name).join(", ") || "none"}`);
|
|
61
|
+
}
|
|
62
|
+
if (found.type !== POSTGRES_ENTITY_TYPES.table)
|
|
63
|
+
throw new MigrationRefusal(`${key} is a ${found.type}, and the migration Op migrates a table's column`);
|
|
64
|
+
return found;
|
|
65
|
+
}
|
|
66
|
+
const pairsOf = (comment) => {
|
|
67
|
+
const p = readTrailerPairs(comment);
|
|
68
|
+
return p?.has(MIGRATION_TRAILER_KEY) ? p : undefined;
|
|
69
|
+
};
|
|
70
|
+
async function serverColumns(client, oid) {
|
|
71
|
+
const rows = await client.query(`SELECT a.attnum, a.attname AS name, pg_catalog.format_type(a.atttypid, a.atttypmod) AS type, a.attnotnull AS notnull,
|
|
72
|
+
pg_catalog.pg_get_expr(d.adbin, d.adrelid, true) AS dflt, pg_catalog.col_description(a.attrelid, a.attnum) AS comment,
|
|
73
|
+
a.attgenerated::text AS generated, a.attidentity::text AS identity
|
|
74
|
+
FROM pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
|
|
75
|
+
WHERE a.attrelid = $1::oid AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum`, [oid]);
|
|
76
|
+
return new Map(rows.map((r) => {
|
|
77
|
+
const comment = r.comment ?? undefined;
|
|
78
|
+
const pairs = pairsOf(comment);
|
|
79
|
+
return [
|
|
80
|
+
r.name,
|
|
81
|
+
{
|
|
82
|
+
attnum: Number(r.attnum),
|
|
83
|
+
name: r.name,
|
|
84
|
+
type: r.type,
|
|
85
|
+
notNull: r.notnull,
|
|
86
|
+
...(r.dflt !== null ? { default: r.dflt } : {}),
|
|
87
|
+
...(comment !== undefined ? { comment } : {}),
|
|
88
|
+
generated: r.generated !== "",
|
|
89
|
+
identity: r.identity !== "",
|
|
90
|
+
...(pairs ? { pairs } : {}),
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
/** The server's major, from `server_version_num`. */
|
|
96
|
+
export async function serverMajor(client) {
|
|
97
|
+
const [row] = await client.query("SELECT pg_catalog.current_setting('server_version_num') AS v");
|
|
98
|
+
return Math.floor(Number(row?.v ?? "0") / 10000);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A working object is this migration's only when it carries this project's
|
|
102
|
+
* marker, names this migration and has the role its name says. One that has
|
|
103
|
+
* the name and not the marker is somebody else's, and the migration stops
|
|
104
|
+
* rather than drop or fill it.
|
|
105
|
+
*/
|
|
106
|
+
function own(o, names, role, what, marker) {
|
|
107
|
+
if (!o)
|
|
108
|
+
return undefined;
|
|
109
|
+
if (!carriesMarker(o.comment, marker) || o.pairs?.get(MIGRATION_TRAILER_KEY) !== names.key || o.pairs.get("role") !== role) {
|
|
110
|
+
throw new MigrationRefusal(`${names.schema}.${names.table}: ${what} ${o.name} exists and is not this migration's (its comment carries no migration=${names.key} role=${role} marker for this project). ` +
|
|
111
|
+
`The migration uses that name, so it stops rather than touch it; rename or drop it by hand.`);
|
|
112
|
+
}
|
|
113
|
+
return o;
|
|
114
|
+
}
|
|
115
|
+
/** The change the declaration makes to the column, as a plan against the server classifies it. */
|
|
116
|
+
async function columnChanges(input) {
|
|
117
|
+
const { declared } = input;
|
|
118
|
+
const plan = await planAgainstClient(input.client, input.target, [{ key: declared.exportName, canonical: declared.canonical }], input.major !== undefined ? { major: input.major } : {});
|
|
119
|
+
const key = qualifiedKey(declared.canonical);
|
|
120
|
+
const changes = plan.diff.changes.filter((c) => c.object === key && (c.field === `columns.${input.column}` || c.field.startsWith(`columns.${input.column}.`)));
|
|
121
|
+
return { changes, ...(plan.major !== undefined ? { major: plan.major } : {}) };
|
|
122
|
+
}
|
|
123
|
+
const ruleText = (c) => `${c.rule} ${PG_CLASSIFIER_RULES[c.rule].title} (${c.field}${c.before !== undefined || c.after !== undefined ? `: ${c.before ?? "-"} -> ${c.after ?? "-"}` : ""})`;
|
|
124
|
+
/**
|
|
125
|
+
* The tables the Op migrates: an ordinary table, and a partitioned one, whose
|
|
126
|
+
* partitions take every column change, trigger, check and `NOT NULL` from it.
|
|
127
|
+
* A partition is migrated through its partitioned table. Inheritance (not
|
|
128
|
+
* partitioning) is refused: a write to a child table does not fire its
|
|
129
|
+
* parent's trigger, so the dual write would miss it, and a child's indexes
|
|
130
|
+
* and constraints are its own, which the carry-over does not read.
|
|
131
|
+
*/
|
|
132
|
+
function refuseRelation(where, rel) {
|
|
133
|
+
if (rel.partition) {
|
|
134
|
+
throw new MigrationRefusal(`${where} is a partition of ${rel.parents}. A partition's columns are its partitioned table's: run the Op on ${rel.parents}, which migrates every partition.`);
|
|
135
|
+
}
|
|
136
|
+
if (rel.parents) {
|
|
137
|
+
throw new MigrationRefusal(`${where} inherits from ${rel.parents}. An inherited column is changed through its parent, and the Op does not migrate inheritance trees: ` +
|
|
138
|
+
`a write to a child table does not fire its parent's dual-write trigger, and each child's indexes and constraints are its own. Migrate by hand, or move to declarative partitioning (chant #3333).`);
|
|
139
|
+
}
|
|
140
|
+
if (rel.kind === "r" && rel.children) {
|
|
141
|
+
throw new MigrationRefusal(`${where} has inheritance children (${rel.children}). The Op does not migrate inheritance trees: a write to a child table does not fire the parent's dual-write trigger, ` +
|
|
142
|
+
`so the new column would miss it, and each child's indexes and constraints are its own. Migrate by hand, or move to declarative partitioning (chant #3333).`);
|
|
143
|
+
}
|
|
144
|
+
if (rel.kind !== "r" && rel.kind !== "p")
|
|
145
|
+
throw new MigrationRefusal(`${where} is not a table (relkind ${rel.kind}); the migration Op migrates a table's column`);
|
|
146
|
+
}
|
|
147
|
+
/** Observe the migration of the declared table's column on the server. */
|
|
148
|
+
export async function observeMigration(input) {
|
|
149
|
+
const { client, declared, marker } = input;
|
|
150
|
+
const schema = declared.canonical.schema ?? input.target.defaultSchema;
|
|
151
|
+
const table = declared.canonical.name;
|
|
152
|
+
const where = `${schema}.${table}`;
|
|
153
|
+
const [rel] = await client.query(`SELECT c.oid::text AS oid, c.relkind::text AS kind, c.relispartition AS partition,
|
|
154
|
+
(SELECT pg_catalog.string_agg(i.inhparent::pg_catalog.regclass::text, ', ') FROM pg_catalog.pg_inherits i WHERE i.inhrelid = c.oid) AS parents,
|
|
155
|
+
(SELECT pg_catalog.string_agg(i.inhrelid::pg_catalog.regclass::text, ', ' ORDER BY i.inhrelid::pg_catalog.regclass::text) FROM pg_catalog.pg_inherits i WHERE i.inhparent = c.oid) AS children,
|
|
156
|
+
CASE WHEN c.relkind = 'p' THEN pg_catalog.pg_get_partkeydef(c.oid) END AS partkey,
|
|
157
|
+
(SELECT p.partattrs::int2[] FROM pg_catalog.pg_partitioned_table p WHERE p.partrelid = c.oid) AS partattrs
|
|
158
|
+
FROM pg_catalog.pg_class c WHERE c.oid = pg_catalog.to_regclass($1)`, [`${col(schema)}.${col(table)}`]);
|
|
159
|
+
if (!rel)
|
|
160
|
+
throw new MigrationRefusal(`${where} is not on the server, so there is no column to migrate; the applier creates it`);
|
|
161
|
+
refuseRelation(where, rel);
|
|
162
|
+
const declaredColumns = declared.props.columns ?? [];
|
|
163
|
+
const column = declaredColumns.find((c) => c.name === input.column);
|
|
164
|
+
if (!column)
|
|
165
|
+
throw new MigrationRefusal(`${where} declares no column ${input.column}. Declared columns: ${declaredColumns.map((c) => c.name).join(", ") || "none"}`);
|
|
166
|
+
const columns = await serverColumns(client, rel.oid);
|
|
167
|
+
const { changes, major: planned } = await columnChanges(input);
|
|
168
|
+
const major = planned ?? (await serverMajor(client));
|
|
169
|
+
// The change, and the column it comes from: from the plan while it is
|
|
170
|
+
// pending, else from the old column this migration kept after its switch.
|
|
171
|
+
const rename = changes.find((c) => c.rule === "SQLPG205");
|
|
172
|
+
const typeChange = changes.find((c) => c.rule === "SQLPG206" || c.rule === "SQLPG207" || c.rule === "SQLPG208");
|
|
173
|
+
const key = `${schema}.${table}.${input.column}`;
|
|
174
|
+
const mine = [...columns.values()].filter((c) => c.pairs?.get(MIGRATION_TRAILER_KEY) === key);
|
|
175
|
+
const keptOld = mine.find((c) => c.pairs.get("role") === "old");
|
|
176
|
+
let change = "type";
|
|
177
|
+
let sourceName = input.column;
|
|
178
|
+
if (rename) {
|
|
179
|
+
change = "rename";
|
|
180
|
+
sourceName = String(rename.before);
|
|
181
|
+
}
|
|
182
|
+
else if (keptOld && keptOld.name !== pgName(`${input.column}__chant_old`)) {
|
|
183
|
+
change = "rename";
|
|
184
|
+
sourceName = keptOld.name;
|
|
185
|
+
}
|
|
186
|
+
const names = migrationNames(schema, table, input.column, change, sourceName);
|
|
187
|
+
// Whatever the state, a working object under a migration name, or with this
|
|
188
|
+
// migration's key, is this migration's or it is refused.
|
|
189
|
+
for (const c of mine)
|
|
190
|
+
own(c, names, String(c.pairs.get("role")), "column", marker);
|
|
191
|
+
const working = (name) => (change === "type" || columns.get(name)?.pairs ? columns.get(name) : undefined);
|
|
192
|
+
const newColumn = own(working(names.newColumn), names, "new", "column", marker);
|
|
193
|
+
const old = own(working(names.oldColumn), names, "old", "column", marker);
|
|
194
|
+
const [trigRow] = await client.query("SELECT t.tgname AS name, pg_catalog.obj_description(t.oid, 'pg_trigger') AS comment FROM pg_catalog.pg_trigger t WHERE t.tgrelid = $1::oid AND t.tgname = $2", [rel.oid, names.trigger]);
|
|
195
|
+
const [fnRow] = await client.query("SELECT p.proname AS name, pg_catalog.obj_description(p.oid, 'pg_proc') AS comment FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = $1 AND p.proname = $2 AND p.pronargs = 0", [schema, names.fn]);
|
|
196
|
+
const [checkRow] = await client.query("SELECT c.conname AS name, pg_catalog.obj_description(c.oid, 'pg_constraint') AS comment, c.convalidated AS validated FROM pg_catalog.pg_constraint c WHERE c.conrelid = $1::oid AND c.conname = $2", [rel.oid, names.check]);
|
|
197
|
+
const asObject = (r) => r ? { name: r.name, ...(r.comment !== null ? { comment: r.comment } : {}), ...(pairsOf(r.comment ?? undefined) ? { pairs: pairsOf(r.comment ?? undefined) } : {}) } : undefined;
|
|
198
|
+
const trigger = own(asObject(trigRow), names, "dual", "trigger", marker);
|
|
199
|
+
const fn = own(asObject(fnRow), names, "dual", "function", marker);
|
|
200
|
+
const checkObj = own(asObject(checkRow), names, "nn", "check constraint", marker);
|
|
201
|
+
const check = checkObj ? { ...checkObj, validated: checkRow.validated } : undefined;
|
|
202
|
+
const migrating = rename !== undefined || (typeChange !== undefined && typeChange.rule !== "SQLPG206");
|
|
203
|
+
let state;
|
|
204
|
+
if (old) {
|
|
205
|
+
if (migrating) {
|
|
206
|
+
throw new MigrationRefusal(`${where}.${input.column} differs from its declaration again, and the old column of the last migration is still kept as ${names.oldColumn}. ` +
|
|
207
|
+
`Run the contract (its gate, then the Contract phase) before migrating the column again.`);
|
|
208
|
+
}
|
|
209
|
+
state = "switched";
|
|
210
|
+
}
|
|
211
|
+
else if (migrating) {
|
|
212
|
+
state = "migrate";
|
|
213
|
+
}
|
|
214
|
+
else if (changes.length === 0) {
|
|
215
|
+
if (newColumn || trigger || fn || check) {
|
|
216
|
+
throw new MigrationRefusal(`${where}.${input.column} already holds its declaration, and ${[newColumn && `column ${newColumn.name}`, trigger && `trigger ${trigger.name}`, fn && `function ${fn.name}`, check && `check ${check.name}`].filter(Boolean).join(", ")} ` +
|
|
217
|
+
`from an unfinished migration is still there. The run fails so onFailure drops it.`);
|
|
218
|
+
}
|
|
219
|
+
state = "done";
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
throw new MigrationRefusal(`${where}.${input.column} has no change the migration Op makes: ${changes.map(ruleText).join("; ")}. ` +
|
|
223
|
+
`The applier makes these in place (ApplyOp with target "postgres").`);
|
|
224
|
+
}
|
|
225
|
+
const source = state === "migrate" ? columns.get(sourceName) : undefined;
|
|
226
|
+
let batchKey = "";
|
|
227
|
+
let keyColumns = [];
|
|
228
|
+
let dependents = { carried: [], views: [] };
|
|
229
|
+
let states = new Map();
|
|
230
|
+
let expression = col(sourceName);
|
|
231
|
+
let publications = [];
|
|
232
|
+
if (state === "migrate") {
|
|
233
|
+
if (!source)
|
|
234
|
+
throw new MigrationRefusal(`${where}: the column ${sourceName} the values come from is not on the server`);
|
|
235
|
+
({ batchKey, keyColumns, dependents } = await refuseUnsupported(input, names, rel, source, column, changes, rename !== undefined, typeChange));
|
|
236
|
+
states = await carriedStates(client, dependents.carried, (comment) => {
|
|
237
|
+
const pairs = pairsOf(comment);
|
|
238
|
+
if (!pairs)
|
|
239
|
+
return comment === undefined ? "none" : "theirs";
|
|
240
|
+
return pairs.get(MIGRATION_TRAILER_KEY) === names.key && pairs.get("role") === "carry" && carriesMarker(comment, marker) ? "ours" : "theirs";
|
|
241
|
+
}, (what) => {
|
|
242
|
+
throw new MigrationRefusal(`${where}: ${what} exists and is not this migration's (its comment carries no migration=${names.key} role=carry marker for this project). The migration uses that name, so it stops rather than touch it; rename or drop it by hand.`);
|
|
243
|
+
});
|
|
244
|
+
if (change === "type") {
|
|
245
|
+
expression = input.using ?? `CAST(${col(sourceName)} AS ${storedType(column.type)})`;
|
|
246
|
+
if (expression.includes("$chant$"))
|
|
247
|
+
throw new MigrationRefusal(`${where}: the using expression may not contain $chant$, which quotes the dual-write function's body`);
|
|
248
|
+
}
|
|
249
|
+
else if (input.using !== undefined) {
|
|
250
|
+
throw new MigrationRefusal(`${where}.${input.column} is a rename of ${sourceName}, which copies the value as it is; using is for a type change`);
|
|
251
|
+
}
|
|
252
|
+
// The declared type as the classifier compared it: the server's spelling (character varying(10), not varchar(10)).
|
|
253
|
+
const serial = storedType(column.type) !== column.type;
|
|
254
|
+
const expectedType = change === "type" ? (serial ? storedType(column.type) : String(typeChange?.after ?? column.type)) : source.type;
|
|
255
|
+
if (newColumn && newColumn.type !== expectedType) {
|
|
256
|
+
throw new MigrationRefusal(`${where}: ${newColumn.name} was made as ${newColumn.type}, from another declaration of ${input.column} (now ${column.type}). The run fails so onFailure drops it, and the next run starts again from the current declaration.`);
|
|
257
|
+
}
|
|
258
|
+
publications = await publicationsOf(client, schema, table, sourceName, major);
|
|
259
|
+
if (publications.length > 0) {
|
|
260
|
+
throw new MigrationRefusal(`${where}.${sourceName} is sent by logical replication publication(s) ${publications.map((p) => `${p.name}${p.allColumns ? " (every column)" : ""}`).join(", ")}. ` +
|
|
261
|
+
`A subscriber applies changes by column name and must have every column the publisher sends, so the column the expand adds would stop the subscriber's apply at the first backfilled row, ` +
|
|
262
|
+
`and the switch would need the same change on the subscriber at the same moment (https://www.postgresql.org/docs/18/logical-replication-col-lists.html). ` +
|
|
263
|
+
`Leave the column out of the publication with a column list (15 and later), or migrate the subscriber's table as well and run this Op on a table no publication sends. ` +
|
|
264
|
+
`A migration of the publisher and the subscriber together is chant #3332.`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
state,
|
|
269
|
+
names,
|
|
270
|
+
declared,
|
|
271
|
+
column,
|
|
272
|
+
oid: rel.oid,
|
|
273
|
+
batchKey,
|
|
274
|
+
keyColumns,
|
|
275
|
+
carried: dependents.carried,
|
|
276
|
+
carriedStates: states,
|
|
277
|
+
views: dependents.views,
|
|
278
|
+
...(dependents.sequence ? { sequence: dependents.sequence } : {}),
|
|
279
|
+
partitioned: rel.kind === "p",
|
|
280
|
+
columns,
|
|
281
|
+
...(source ? { source } : {}),
|
|
282
|
+
...(newColumn ? { newColumn } : {}),
|
|
283
|
+
...(old ? { old } : {}),
|
|
284
|
+
...(trigger ? { trigger } : {}),
|
|
285
|
+
...(fn ? { fn } : {}),
|
|
286
|
+
...(check ? { check } : {}),
|
|
287
|
+
changes,
|
|
288
|
+
expression,
|
|
289
|
+
publications,
|
|
290
|
+
major,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/** The checks that hold only while there is something to migrate. Returns the batch key and what the Op carries over. */
|
|
294
|
+
async function refuseUnsupported(input, names, rel, source, column, changes, rename, typeChange) {
|
|
295
|
+
const where = `${names.schema}.${names.table}`;
|
|
296
|
+
const oid = rel.oid;
|
|
297
|
+
if (rename && typeChange) {
|
|
298
|
+
throw new MigrationRefusal(`${where}: ${source.name} is renamed to ${names.column} and its type changes (${typeChange.before} -> ${typeChange.after}) in one declaration. ` +
|
|
299
|
+
`Migrate one at a time: declare the rename with the old type, run the Op to its end, then change the type. ` +
|
|
300
|
+
`(In one run both names would stay written until the contract, which needs a conversion back to the old type for writers of the new name; chant #3331.)`);
|
|
301
|
+
}
|
|
302
|
+
if (rename && (source.default !== undefined || column.default !== undefined)) {
|
|
303
|
+
throw new MigrationRefusal(`${where}: ${source.name} has a default (${source.default ?? column.default}). During a rename both columns are written, each from the other, ` +
|
|
304
|
+
`and a row inserted by a writer that names one column would get the other's default, which the trigger cannot tell from a value it was given. ` +
|
|
305
|
+
`Drop the default first (the applier does), rename, then declare it again. A rename that keeps its default needs versioned views (chant #3331).`);
|
|
306
|
+
}
|
|
307
|
+
if (source.generated || (column.generated && column.generated.kind !== "identity")) {
|
|
308
|
+
throw new MigrationRefusal(`${where}.${source.name} is a generated column. There is no expand and contract for one: Postgres cannot make an existing column generated, so the new column could only be added generated, ` +
|
|
309
|
+
`which computes every row under ACCESS EXCLUSIVE (a stored one) or needs no migration (a virtual one stores nothing). The applier makes the change in place (SQLPG207), in a maintenance window for a stored column.`);
|
|
310
|
+
}
|
|
311
|
+
const unsupported = changes.filter((c) => /\.(generated|identity|collate|storage)$/.test(c.field));
|
|
312
|
+
if (unsupported.length > 0) {
|
|
313
|
+
throw new MigrationRefusal(`${where}.${names.column}: a change to its collation, storage, generation or identity (${unsupported.map(ruleText).join("; ")}) is not one the Op makes; the applier makes it in place.`);
|
|
314
|
+
}
|
|
315
|
+
if (rel.kind === "p" && ((rel.partattrs ?? []).map(Number).includes(source.attnum) || (rel.partkey !== null && replaceColumn(rel.partkey, source.name, `${source.name}_`) !== rel.partkey))) {
|
|
316
|
+
throw new MigrationRefusal(`${where}.${source.name} is in the partition key (${rel.partkey}). A changed partition key would move rows between partitions, which no trigger can do; the Op migrates a partitioned table's other columns.`);
|
|
317
|
+
}
|
|
318
|
+
const dependents = await discoverDependents({
|
|
319
|
+
client: input.client,
|
|
320
|
+
names,
|
|
321
|
+
oid,
|
|
322
|
+
attnum: source.attnum,
|
|
323
|
+
objects: input.objects ?? [input.declared],
|
|
324
|
+
declared: input.declared,
|
|
325
|
+
defaultSchema: input.target.defaultSchema,
|
|
326
|
+
partitioned: rel.kind === "p",
|
|
327
|
+
});
|
|
328
|
+
if (dependents.refused.length > 0) {
|
|
329
|
+
throw new MigrationRefusal(`${where}.${source.name} is used by what the migration Op does not carry over to the new column: ${dependents.refused.join("; ")}. ` +
|
|
330
|
+
`The old column could not be dropped while they use it.`);
|
|
331
|
+
}
|
|
332
|
+
const keys = await input.client.query(`SELECT a.attname AS name, pg_catalog.format_type(a.atttypid, a.atttypmod) AS type
|
|
333
|
+
FROM pg_catalog.pg_constraint c, pg_catalog.unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
|
|
334
|
+
JOIN pg_catalog.pg_attribute a ON a.attnum = k.attnum
|
|
335
|
+
WHERE c.conrelid = $1::oid AND c.contype = 'p' AND a.attrelid = c.conrelid ORDER BY k.ord`, [oid]);
|
|
336
|
+
if (keys.length === 0) {
|
|
337
|
+
throw new MigrationRefusal(`${where} has no primary key. The backfill fills the new column in batches of the primary key's ranges, the same rows in every run, which needs one; declare a primary key first.`);
|
|
338
|
+
}
|
|
339
|
+
return { batchKey: keyText(keys), keyColumns: keys, dependents: { carried: dependents.carried, views: dependents.views, ...(dependents.sequence ? { sequence: dependents.sequence } : {}) } };
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* What the migration will do, independent of how far it has got: the
|
|
343
|
+
* change, the column on both sides, the expression, the batch key. The
|
|
344
|
+
* Plan phase publishes its digest; the gate binds a digest of this and the
|
|
345
|
+
* verification.
|
|
346
|
+
*/
|
|
347
|
+
export function migrationPlanSubject(o) {
|
|
348
|
+
return {
|
|
349
|
+
migration: o.names.key,
|
|
350
|
+
change: o.names.change,
|
|
351
|
+
from: o.source ? { name: o.source.name, type: o.source.type, notNull: o.source.notNull, default: o.source.default ?? null } : null,
|
|
352
|
+
to: { name: o.column.name, type: o.column.type ?? null, notNull: o.column.notNull === true, default: o.column.default ?? null, comment: o.column.comment ?? null },
|
|
353
|
+
expression: o.expression,
|
|
354
|
+
batchKey: o.batchKey,
|
|
355
|
+
rules: o.changes.map((c) => c.rule),
|
|
356
|
+
...(o.carried.length > 0 || o.views.length > 0 || o.sequence ? { dependents: carriedSubject(o) } : {}),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
export const migrationPlanDigest = (o) => computePlanDigest("postgres-migration", migrationPlanSubject(o));
|
|
360
|
+
export { MIGRATION_TRAILER_KEY };
|