@intentius/chant-lexicon-sql 0.124.31 → 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,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the backfill cuts a table into batches (#3281, #3322). A batch must be
|
|
3
|
+
* the same rows on every run, or a receipt written for it by one run would
|
|
4
|
+
* vouch for different rows in the next.
|
|
5
|
+
*
|
|
6
|
+
* - A primary key of one smallint, integer or bigint column: ranges of its
|
|
7
|
+
* value, `[b * size, (b + 1) * size)`. Arithmetic, so nothing needs to be
|
|
8
|
+
* remembered, and a batch is the same range however many rows were
|
|
9
|
+
* written since.
|
|
10
|
+
* - Any other primary key (uuid, text, a timestamp, several columns):
|
|
11
|
+
* ranges between recorded boundaries in the key's order. The first run
|
|
12
|
+
* walks the key's index once, taking every `size`-th key as a boundary
|
|
13
|
+
* (`WHERE key >= boundary ORDER BY key OFFSET size LIMIT 1`, so the walk
|
|
14
|
+
* reads each key once and sorts nothing), and records the boundaries in
|
|
15
|
+
* the receipt store, beside the batches' receipts, before any batch runs.
|
|
16
|
+
* Every later run reads them back instead of walking again, so batch `i`
|
|
17
|
+
* is `boundary[i] <= key < boundary[i + 1]` in every run. The first batch
|
|
18
|
+
* has no lower bound and the last no upper one, so a row written since,
|
|
19
|
+
* anywhere in the key's order, falls in exactly one batch (and the dual
|
|
20
|
+
* write has already filled it). Each batch is a range scan of the primary
|
|
21
|
+
* key's index, with a row comparison (`(a, b) >= ($1, $2)`) in the index's
|
|
22
|
+
* own column order.
|
|
23
|
+
*
|
|
24
|
+
* Hash buckets (`hash(key) % n`) were the other option: they need nothing
|
|
25
|
+
* recorded but the bucket count, but no index can serve them, so every batch
|
|
26
|
+
* would read the whole table. Ranges recorded once read each row once.
|
|
27
|
+
*
|
|
28
|
+
* The record is bound to the table's oid and the new column's attribute
|
|
29
|
+
* number, like the batches' receipts: a migration started again after
|
|
30
|
+
* onFailure (which drops the new column and the receipts) walks the key
|
|
31
|
+
* again. A record left from another batch size is kept, since its batches'
|
|
32
|
+
* receipts are what the run resumes from.
|
|
33
|
+
*/
|
|
34
|
+
import { col } from "./names.js";
|
|
35
|
+
export const INTEGER_KEY_TYPES = new Set(["smallint", "integer", "bigint"]);
|
|
36
|
+
/** Whether the key is batched by arithmetic ranges of its one integer column. */
|
|
37
|
+
export const isIntegerKey = (keys) => keys.length === 1 && INTEGER_KEY_TYPES.has(keys[0].type);
|
|
38
|
+
/** `id`, or `(tenant_id, id)`. */
|
|
39
|
+
export const keyText = (keys) => (keys.length === 1 ? keys[0].name : `(${keys.map((k) => k.name).join(", ")})`);
|
|
40
|
+
const tuple = (keys) => (keys.length === 1 ? col(keys[0].name) : `(${keys.map((k) => col(k.name)).join(", ")})`);
|
|
41
|
+
const placeholders = (keys, from) => {
|
|
42
|
+
const p = keys.map((k, i) => `$${from + i}::${k.type}`);
|
|
43
|
+
return keys.length === 1 ? p[0] : `(${p.join(", ")})`;
|
|
44
|
+
};
|
|
45
|
+
/** The batches of an integer key: one per range that holds rows. */
|
|
46
|
+
export async function integerBatches(client, table, key, size) {
|
|
47
|
+
const k = col(key.name);
|
|
48
|
+
const rows = await client.query(`SELECT pg_catalog.floor(${k}::numeric / ${size})::bigint::text AS b FROM ${table} GROUP BY 1 ORDER BY 1`);
|
|
49
|
+
return rows.map(({ b }) => {
|
|
50
|
+
const lo = (BigInt(b) * BigInt(size)).toString();
|
|
51
|
+
const hi = ((BigInt(b) + 1n) * BigInt(size)).toString();
|
|
52
|
+
return { id: b, where: `${k} >= $1 AND ${k} < $2`, params: [lo, hi], inputs: { batch: b, size, key: key.name } };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/** The batches between recorded boundaries: `[undefined, b1)`, `[b1, b2)`, ..., `[bn, undefined)`, where `b0` is only where the walk started. */
|
|
56
|
+
export function boundaryBatches(keys, bounds) {
|
|
57
|
+
const k = tuple(keys);
|
|
58
|
+
return bounds.map((lo, i) => {
|
|
59
|
+
const hi = bounds[i + 1];
|
|
60
|
+
const where = [];
|
|
61
|
+
const params = [];
|
|
62
|
+
if (i > 0) {
|
|
63
|
+
where.push(`${k} >= ${placeholders(keys, 1)}`);
|
|
64
|
+
params.push(...lo);
|
|
65
|
+
}
|
|
66
|
+
if (hi) {
|
|
67
|
+
where.push(`${k} < ${placeholders(keys, params.length + 1)}`);
|
|
68
|
+
params.push(...hi);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
id: `k${i}`,
|
|
72
|
+
where: where.join(" AND ") || "true",
|
|
73
|
+
params,
|
|
74
|
+
inputs: { batch: `k${i}`, key: keys.map((c) => c.name), lo: i > 0 ? lo : null, hi: hi ?? null },
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** Walk the key's order and take every `size`-th key: the boundaries the batches lie between. */
|
|
79
|
+
export async function walkBoundaries(client, table, keys, size, signal) {
|
|
80
|
+
const select = keys.map((k, i) => `${col(k.name)}::text AS k${i}`).join(", ");
|
|
81
|
+
const order = keys.map((k) => col(k.name)).join(", ");
|
|
82
|
+
const values = (row) => (row ? keys.map((_, i) => String(row[`k${i}`])) : undefined);
|
|
83
|
+
const bounds = [];
|
|
84
|
+
let at = values((await client.query(`SELECT ${select} FROM ${table} ORDER BY ${order} LIMIT 1`))[0]);
|
|
85
|
+
while (at) {
|
|
86
|
+
signal?.throwIfAborted();
|
|
87
|
+
bounds.push(at);
|
|
88
|
+
at = values((await client.query(`SELECT ${select} FROM ${table} WHERE ${tuple(keys)} >= ${placeholders(keys, 1)} ORDER BY ${order} OFFSET ${size} LIMIT 1`, at))[0]);
|
|
89
|
+
}
|
|
90
|
+
return bounds;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The boundary batches of a key that is not one integer column: read from
|
|
94
|
+
* the record in the receipt store, or walked and recorded when there is
|
|
95
|
+
* none for this table and new column.
|
|
96
|
+
*/
|
|
97
|
+
export async function recordedBatches(client, input) {
|
|
98
|
+
const ref = { name: input.effect, effect: input.effect, flavor: "hash", inputs: {} };
|
|
99
|
+
const keys = input.keys.map((k) => k.name);
|
|
100
|
+
const raw = await input.receipts.read(ref);
|
|
101
|
+
let record;
|
|
102
|
+
try {
|
|
103
|
+
const parsed = raw === undefined ? undefined : JSON.parse(raw);
|
|
104
|
+
if (parsed?.v === 1 && parsed.table === input.oid && parsed.column === input.attnum && JSON.stringify(parsed.keys) === JSON.stringify(keys))
|
|
105
|
+
record = parsed;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
record = undefined;
|
|
109
|
+
}
|
|
110
|
+
let walked = false;
|
|
111
|
+
if (!record) {
|
|
112
|
+
const bounds = await walkBoundaries(client, input.table, input.keys, input.size, input.signal);
|
|
113
|
+
record = { v: 1, table: input.oid, column: input.attnum, keys, size: input.size, bounds };
|
|
114
|
+
await input.receipts.write(ref, JSON.stringify(record));
|
|
115
|
+
walked = true;
|
|
116
|
+
input.log(`-- batches of ${keyText(input.keys)}: ${bounds.length} boundary(ies) every ${input.size} key(s), recorded`);
|
|
117
|
+
}
|
|
118
|
+
else if (record.size !== input.size) {
|
|
119
|
+
input.log(`-- batches of ${keyText(input.keys)}: kept as recorded every ${record.size} key(s); batchSize ${input.size} applies to a migration started again`);
|
|
120
|
+
}
|
|
121
|
+
return { batches: boundaryBatches(input.keys, record.bounds), record, walked };
|
|
122
|
+
}
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What uses the migrated column, carried over to the new one (#3322).
|
|
3
|
+
*
|
|
4
|
+
* An index, a constraint or a view on the column binds the column by its
|
|
5
|
+
* attribute number, not its name, so a swap of names at the switch would
|
|
6
|
+
* leave it on the old column, and the contract could not drop that column
|
|
7
|
+
* while something uses it. Each is made again on the new column before the
|
|
8
|
+
* switch, under a working name, and the switch swaps the names:
|
|
9
|
+
*
|
|
10
|
+
* | On the old column | Before the switch | In the switch's transaction |
|
|
11
|
+
* |---|---|---|
|
|
12
|
+
* | an index | `CREATE INDEX CONCURRENTLY` on the new column | the old one dropped (a type change) or kept as `<name>__chant_old` (a rename), the new one renamed to the name |
|
|
13
|
+
* | a primary key or unique constraint | its unique index built `CONCURRENTLY` | the old constraint dropped (or a rename's unique one kept as `__chant_old`), `ADD CONSTRAINT ... USING INDEX` |
|
|
14
|
+
* | a check or foreign key, this table's or another's that references the column | added `NOT VALID`, then `VALIDATE` | the old one dropped, the new one renamed to the name |
|
|
15
|
+
* | a view (and the views that read it) | nothing | dropped, then created again from its declaration once the columns are swapped, with its grants and owner |
|
|
16
|
+
*
|
|
17
|
+
* `CREATE INDEX CONCURRENTLY` reads the table without blocking writes, and
|
|
18
|
+
* `VALIDATE CONSTRAINT` holds SHARE UPDATE EXCLUSIVE, so both run while the
|
|
19
|
+
* application keeps writing; the dual-write trigger keeps the new column
|
|
20
|
+
* equal to the old one, so the index and the constraints hold the same rows
|
|
21
|
+
* as the old ones. A foreign key from another table is added against the
|
|
22
|
+
* new column's working unique index, which the switch then attaches to the
|
|
23
|
+
* key constraint. Everything in the switch changes only the catalog.
|
|
24
|
+
*
|
|
25
|
+
* Working objects carry chant's trailer with `migration=<key>` and
|
|
26
|
+
* `role=carry`, so the catalog read leaves them out of plans until the
|
|
27
|
+
* switch, and onFailure drops them (`./steps.ts`). The switch gives each its
|
|
28
|
+
* old comment back.
|
|
29
|
+
*
|
|
30
|
+
* Names: a type change keeps every name. A rename keeps a name the
|
|
31
|
+
* declaration does not give otherwise, except one Postgres made from the
|
|
32
|
+
* old column (`users_email_key` becomes `users_login_key`); a key, foreign
|
|
33
|
+
* key or index the build declares on the new column takes the declared name.
|
|
34
|
+
*
|
|
35
|
+
* Refused, each named with what to do: exclusion constraints and constraint
|
|
36
|
+
* triggers (no `USING INDEX` form), materialized views (made again only by
|
|
37
|
+
* a full scan in the switch), rules, triggers, policies and statistics on the
|
|
38
|
+
* column, a generated column computed from it, an INVALID index on it, a view
|
|
39
|
+
* the build does not declare, a view something other than a view depends on.
|
|
40
|
+
*/
|
|
41
|
+
import { quoteIdent } from "../keywords.js";
|
|
42
|
+
import { tokenizeText } from "../tokens.js";
|
|
43
|
+
import { identValue } from "../parser.js";
|
|
44
|
+
import { createSteps, pgString } from "../apply/statements.js";
|
|
45
|
+
import { POSTGRES_ENTITY_TYPES } from "../entity-types.js";
|
|
46
|
+
import { canonicalName } from "../plan/normalize.js";
|
|
47
|
+
import { pgName } from "./names.js";
|
|
48
|
+
/** The working name of a carried object, and the name a rename keeps the old one under. */
|
|
49
|
+
export const carriedWorkingName = (name) => pgName(`${name}__chant_new`);
|
|
50
|
+
export const carriedOldName = (name) => pgName(`${name}__chant_old`);
|
|
51
|
+
/**
|
|
52
|
+
* `sql` with every reference to the column `from` replaced by `to`: an
|
|
53
|
+
* identifier token naming it, not a qualifier (`x.`), a qualified name's
|
|
54
|
+
* last part, a function's name, a cast's type or an access method. Within
|
|
55
|
+
* `[start, end)` token indexes when given.
|
|
56
|
+
*/
|
|
57
|
+
export function replaceColumn(sql, from, to, range) {
|
|
58
|
+
const tokens = tokenizeText(sql, 0);
|
|
59
|
+
const ranges = range ? range(tokens) : [[0, tokens.length]];
|
|
60
|
+
const significant = (i, step) => {
|
|
61
|
+
for (let j = i + step; j >= 0 && j < tokens.length; j += step)
|
|
62
|
+
if (tokens[j].kind !== "ws" && tokens[j].kind !== "comment")
|
|
63
|
+
return tokens[j];
|
|
64
|
+
return undefined;
|
|
65
|
+
};
|
|
66
|
+
const inRange = (i) => ranges.some(([a, b]) => i >= a && i < b);
|
|
67
|
+
return tokens
|
|
68
|
+
.map((t, i) => {
|
|
69
|
+
if ((t.kind !== "ident" && t.kind !== "qident") || !inRange(i) || identValue(t) !== from)
|
|
70
|
+
return t.text;
|
|
71
|
+
const prev = significant(i, -1);
|
|
72
|
+
const next = significant(i, 1);
|
|
73
|
+
if (prev?.text === "." || next?.text === "." || next?.text === "(")
|
|
74
|
+
return t.text;
|
|
75
|
+
if (prev?.kind === "op" && prev.text === "::")
|
|
76
|
+
return t.text;
|
|
77
|
+
if (prev?.kind === "ident" && /^using$/i.test(prev.text))
|
|
78
|
+
return t.text;
|
|
79
|
+
return quoteIdent(to);
|
|
80
|
+
})
|
|
81
|
+
.join("");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A foreign key's token ranges: the referencing side (before `REFERENCES`,
|
|
85
|
+
* and an `ON DELETE SET NULL (...)` list after it) and the referenced
|
|
86
|
+
* column list.
|
|
87
|
+
*/
|
|
88
|
+
function foreignKeyRanges(tokens) {
|
|
89
|
+
const at = tokens.findIndex((t) => t.kind === "ident" && /^references$/i.test(t.text));
|
|
90
|
+
if (at < 0)
|
|
91
|
+
return { local: [[0, tokens.length]], referenced: [] };
|
|
92
|
+
let open = at + 1;
|
|
93
|
+
while (open < tokens.length && !(tokens[open].kind === "punct" && tokens[open].text === "("))
|
|
94
|
+
open++;
|
|
95
|
+
let close = open;
|
|
96
|
+
for (let depth = 0; close < tokens.length; close++) {
|
|
97
|
+
if (tokens[close].text === "(")
|
|
98
|
+
depth++;
|
|
99
|
+
else if (tokens[close].text === ")" && --depth === 0)
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
return { local: [[0, at], [close + 1, tokens.length]], referenced: open < tokens.length ? [[open, close + 1]] : [] };
|
|
103
|
+
}
|
|
104
|
+
/** A foreign key's body with the column replaced on the side(s) that are the migrated table. */
|
|
105
|
+
export function replaceInForeignKey(def, from, to, sides) {
|
|
106
|
+
return replaceColumn(def, from, to, (tokens) => {
|
|
107
|
+
const r = foreignKeyRanges(tokens);
|
|
108
|
+
return [...(sides.local ? r.local : []), ...(sides.referenced ? r.referenced : [])];
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** A constraint's body without a trailing `NOT VALID`, which the carry adds itself. */
|
|
112
|
+
const withoutNotValid = (def) => def.replace(/\s+NOT VALID\s*$/i, "");
|
|
113
|
+
/**
|
|
114
|
+
* `CREATE [UNIQUE] INDEX <name> ON ...` as `pg_get_indexdef` prints it, as
|
|
115
|
+
* the `CONCURRENTLY` build of `working` on the new column.
|
|
116
|
+
*/
|
|
117
|
+
export function carriedIndexStatement(def, working, from, to) {
|
|
118
|
+
const m = /^CREATE (UNIQUE )?INDEX (?:"(?:[^"]|"")+"|\S+) ON (.*)$/s.exec(def);
|
|
119
|
+
if (!m)
|
|
120
|
+
throw new Error(`unexpected index definition: ${def}`);
|
|
121
|
+
return `CREATE ${m[1] ?? ""}INDEX CONCURRENTLY ${quoteIdent(working)} ON ${replaceColumn(m[2], from, to)}`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The name Postgres gives an object it names after its columns
|
|
125
|
+
* (`users_email_key`, `orders_user_id_fkey`, `users_email_idx`), when `name`
|
|
126
|
+
* is that name for the old columns: the same name for the new ones. Only a
|
|
127
|
+
* name that fits untruncated; any other name is kept.
|
|
128
|
+
*/
|
|
129
|
+
export function renamedDefault(name, table, oldCols, newCols, suffix) {
|
|
130
|
+
const made = (cols) => `${table}_${cols.join("_")}_${suffix}`;
|
|
131
|
+
if (name !== made(oldCols))
|
|
132
|
+
return undefined;
|
|
133
|
+
const next = made(newCols);
|
|
134
|
+
return Buffer.byteLength(next, "utf8") <= 63 && next !== name ? next : undefined;
|
|
135
|
+
}
|
|
136
|
+
const sameColumns = (a, b) => a.length === b.length && a.every((c, i) => c === b[i]);
|
|
137
|
+
/** What uses the column the values come from, sorted into what the Op carries over and what it refuses. */
|
|
138
|
+
export async function discoverDependents(input) {
|
|
139
|
+
const { client, names: n, oid, attnum } = input;
|
|
140
|
+
const from = n.source;
|
|
141
|
+
const to = n.newColumn;
|
|
142
|
+
const deps = await client.query(`SELECT d.deptype::text AS deptype, CASE d.classid WHEN 'pg_catalog.pg_constraint'::pg_catalog.regclass THEN 'constraint' WHEN 'pg_catalog.pg_class'::pg_catalog.regclass THEN 'class'
|
|
143
|
+
WHEN 'pg_catalog.pg_rewrite'::pg_catalog.regclass THEN 'rewrite' WHEN 'pg_catalog.pg_attrdef'::pg_catalog.regclass THEN 'attrdef' ELSE 'other' END AS kind,
|
|
144
|
+
d.objid::text AS objid, d.objsubid, pg_catalog.pg_describe_object(d.classid, d.objid, d.objsubid) AS what
|
|
145
|
+
FROM pg_catalog.pg_depend d
|
|
146
|
+
WHERE d.refclassid = 'pg_catalog.pg_class'::pg_catalog.regclass AND d.refobjid = $1::oid AND d.refobjsubid = $2 AND d.deptype IN ('n', 'a', 'i')
|
|
147
|
+
UNION
|
|
148
|
+
SELECT 'n', 'constraint', c.oid::text, 0, pg_catalog.pg_describe_object('pg_catalog.pg_constraint'::pg_catalog.regclass, c.oid, 0)
|
|
149
|
+
FROM pg_catalog.pg_constraint c WHERE c.confrelid = $1::oid AND $2 = ANY(c.confkey)`, [oid, attnum]);
|
|
150
|
+
const refused = [];
|
|
151
|
+
const carried = [];
|
|
152
|
+
let sequence;
|
|
153
|
+
const byKind = (k) => [...new Set(deps.filter((d) => d.kind === k).map((d) => d.objid))];
|
|
154
|
+
const table = n.table;
|
|
155
|
+
const rename = n.change === "rename";
|
|
156
|
+
const tprops = input.declared.props;
|
|
157
|
+
// ── constraints ─────────────────────────────────────────────────────
|
|
158
|
+
const constraintIds = byKind("constraint");
|
|
159
|
+
const keyIndexes = new Set();
|
|
160
|
+
if (constraintIds.length > 0) {
|
|
161
|
+
const rows = await client.query(`SELECT c.oid::text AS oid, c.conname AS name, c.contype::text AS type, c.conrelid::text AS rel, c.confrelid::text AS frel, c.conindid::text AS idx,
|
|
162
|
+
pg_catalog.pg_get_constraintdef(c.oid) AS def, c.convalidated AS validated, c.condeferrable AS deferrable, c.condeferred AS deferred,
|
|
163
|
+
pg_catalog.obj_description(c.oid, 'pg_constraint') AS comment, n.nspname AS schema, t.relname AS table_name,
|
|
164
|
+
CASE WHEN c.conindid <> 0 THEN pg_catalog.obj_description(c.conindid, 'pg_class') END AS index_comment,
|
|
165
|
+
CASE WHEN c.conindid <> 0 AND c.contype IN ('p', 'u') THEN pg_catalog.pg_get_indexdef(c.conindid) END AS indexdef,
|
|
166
|
+
COALESCE(i.indisreplident, false) AS replident, COALESCE(i.indisclustered, false) AS clustered,
|
|
167
|
+
(SELECT a.attname FROM pg_catalog.pg_attribute a WHERE a.attrelid = c.conrelid AND a.attnum = c.conkey[1]) AS first_col,
|
|
168
|
+
ARRAY(SELECT a.attname FROM pg_catalog.unnest(c.conkey) WITH ORDINALITY k(n, o) JOIN pg_catalog.pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.n ORDER BY k.o)::text[] AS cols,
|
|
169
|
+
t.relkind = 'p' OR EXISTS (SELECT 1 FROM pg_catalog.pg_class f WHERE f.oid = c.confrelid AND f.relkind = 'p') AS frel_partitioned
|
|
170
|
+
FROM pg_catalog.pg_constraint c JOIN pg_catalog.pg_class t ON t.oid = c.conrelid JOIN pg_catalog.pg_namespace n ON n.oid = t.relnamespace
|
|
171
|
+
LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.conindid
|
|
172
|
+
WHERE c.oid = ANY($1::oid[]) ORDER BY c.conname`, [constraintIds]);
|
|
173
|
+
for (const r of rows) {
|
|
174
|
+
const name = String(r.name);
|
|
175
|
+
const own = String(r.rel) === oid;
|
|
176
|
+
const where = `${String(r.schema)}.${String(r.table_name)}`;
|
|
177
|
+
const qtable = `${quoteIdent(String(r.schema))}.${quoteIdent(String(r.table_name))}`;
|
|
178
|
+
const type = String(r.type);
|
|
179
|
+
if (type === "n" && own)
|
|
180
|
+
continue; // the column's own NOT NULL (18)
|
|
181
|
+
const comment = r.comment === null ? undefined : String(r.comment);
|
|
182
|
+
const base = { name, table: qtable, tableName: where, tableOid: String(r.rel), schema: String(r.schema), working: carriedWorkingName(name), validated: r.validated === true, ...(comment !== undefined ? { comment } : {}) };
|
|
183
|
+
const cols = r.cols ?? [];
|
|
184
|
+
const newCols = cols.map((c) => (own && c === from ? n.column : c));
|
|
185
|
+
if (input.partitioned && (type === "p" || type === "u" || type === "f")) {
|
|
186
|
+
keyIndexes.add(String(r.idx));
|
|
187
|
+
refused.push(`${type === "f" ? "foreign key" : type === "p" ? "primary key" : "unique constraint"} ${name} on ${where}: on a partitioned table ${type === "f" ? "a foreign key cannot be added NOT VALID and validated while writes go on" : "a key cannot be attached from an index built CONCURRENTLY (ADD CONSTRAINT ... USING INDEX is not supported on partitioned tables)"}, so carrying it over would hold ACCESS EXCLUSIVE for a full build; drop it, migrate, then declare it again (chant #3333)`);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (type === "f" && !own && r.frel_partitioned === true) {
|
|
191
|
+
refused.push(`foreign key ${name} on ${where} references a partitioned table, where it cannot be added NOT VALID and validated while writes go on; drop it, migrate, then declare it again`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (type === "p" || type === "u") {
|
|
195
|
+
keyIndexes.add(String(r.idx));
|
|
196
|
+
const declaredKey = type === "p" ? (tprops.primaryKey?.name ? tprops.primaryKey : undefined) : tprops.uniques?.find((u) => u.name && sameColumns(u.columns, newCols));
|
|
197
|
+
const target = !rename ? name : (declaredKey?.name ?? (type === "u" ? renamedDefault(name, table, cols, newCols, "key") : undefined) ?? name);
|
|
198
|
+
const deferral = r.deferrable === true ? ` DEFERRABLE${r.deferred === true ? " INITIALLY DEFERRED" : ""}` : "";
|
|
199
|
+
carried.push({
|
|
200
|
+
...base,
|
|
201
|
+
kind: "key",
|
|
202
|
+
target,
|
|
203
|
+
primary: type === "p",
|
|
204
|
+
...(deferral ? { deferral } : {}),
|
|
205
|
+
definition: carriedIndexStatement(String(r.indexdef), base.working, from, to),
|
|
206
|
+
...(r.index_comment !== null && r.index_comment !== undefined ? { indexComment: String(r.index_comment) } : {}),
|
|
207
|
+
...(r.replident === true ? { replicaIdentity: true } : {}),
|
|
208
|
+
...(r.clustered === true ? { clustered: true } : {}),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else if (type === "c") {
|
|
212
|
+
const first = String(r.first_col ?? "");
|
|
213
|
+
const target = !rename ? name : (renamedDefault(name, table, [first], [first === from ? n.column : first], "check") ?? name);
|
|
214
|
+
carried.push({ ...base, kind: "check", target, definition: replaceColumn(withoutNotValid(String(r.def)), from, to) });
|
|
215
|
+
}
|
|
216
|
+
else if (type === "f") {
|
|
217
|
+
const referenced = String(r.frel) === oid;
|
|
218
|
+
const def = replaceInForeignKey(withoutNotValid(String(r.def)), from, to, { local: own, referenced });
|
|
219
|
+
let target = name;
|
|
220
|
+
if (rename && own) {
|
|
221
|
+
const declaredFk = tprops.foreignKeys?.find((f) => f.name && sameColumns(f.columns, newCols));
|
|
222
|
+
target = declaredFk?.name ?? renamedDefault(name, table, cols, newCols, "fkey") ?? name;
|
|
223
|
+
}
|
|
224
|
+
carried.push({ ...base, kind: "foreign-key", target, definition: def });
|
|
225
|
+
}
|
|
226
|
+
else if (type === "x") {
|
|
227
|
+
refused.push(`exclusion constraint ${name} on ${where}: an exclusion constraint cannot be attached from an index built CONCURRENTLY, so adding it again would hold ACCESS EXCLUSIVE for a full build; drop it, migrate, then declare it again`);
|
|
228
|
+
}
|
|
229
|
+
else if (type === "t") {
|
|
230
|
+
refused.push(`constraint trigger ${name} on ${where}: its function reads the column by name, which the Op cannot rewrite; drop it, migrate, then create it again`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
refused.push(`constraint ${name} on ${where} (type ${type})`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ── indexes, sequences, columns ─────────────────────────────────────
|
|
238
|
+
const classIds = byKind("class");
|
|
239
|
+
if (classIds.length > 0) {
|
|
240
|
+
const rows = await client.query(`SELECT c.oid::text AS oid, c.relname AS name, c.relkind::text AS kind, n.nspname AS schema, pg_catalog.obj_description(c.oid, 'pg_class') AS comment,
|
|
241
|
+
CASE WHEN c.relkind IN ('i', 'I') THEN pg_catalog.pg_get_indexdef(c.oid) END AS def,
|
|
242
|
+
i.indisvalid AS valid, i.indisunique AS is_unique, COALESCE(i.indisreplident, false) AS replident, COALESCE(i.indisclustered, false) AS clustered,
|
|
243
|
+
ARRAY(SELECT a.attname FROM pg_catalog.unnest(i.indkey::int2[]) WITH ORDINALITY k(n, o) JOIN pg_catalog.pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = k.n ORDER BY k.o)::text[] AS cols,
|
|
244
|
+
i.indexprs IS NOT NULL OR i.indpred IS NOT NULL AS has_exprs
|
|
245
|
+
FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid
|
|
246
|
+
WHERE c.oid = ANY($1::oid[]) ORDER BY c.relname`, [classIds]);
|
|
247
|
+
const declaredIndexes = input.objects.filter((o) => o.type === POSTGRES_ENTITY_TYPES.index);
|
|
248
|
+
for (const r of rows) {
|
|
249
|
+
const name = String(r.name);
|
|
250
|
+
const kind = String(r.kind);
|
|
251
|
+
if (kind === "i" || kind === "I") {
|
|
252
|
+
if (keyIndexes.has(String(r.oid)))
|
|
253
|
+
continue;
|
|
254
|
+
if (kind === "I") {
|
|
255
|
+
refused.push(`index ${r.schema}.${name} is a partitioned index, which cannot be built CONCURRENTLY; drop it, migrate, then declare it again (chant #3333)`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (r.valid !== true) {
|
|
259
|
+
refused.push(`index ${r.schema}.${name} is INVALID (a failed CONCURRENTLY build); drop it, or REINDEX it, first`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const working = carriedWorkingName(name);
|
|
263
|
+
let target = name;
|
|
264
|
+
if (rename) {
|
|
265
|
+
const oldCols = r.cols ?? [];
|
|
266
|
+
const cols = oldCols.map((c) => (c === from ? n.column : c));
|
|
267
|
+
const declaredIndex = r.has_exprs === true
|
|
268
|
+
? undefined
|
|
269
|
+
: declaredIndexes.find((d) => {
|
|
270
|
+
const p = d.props;
|
|
271
|
+
const els = p.elements ?? [];
|
|
272
|
+
return (canonicalName(String(p.tableName ?? ""), input.defaultSchema) === `${n.schema}.${n.table}` &&
|
|
273
|
+
(p.unique === true) === (r.is_unique === true) &&
|
|
274
|
+
!p.where &&
|
|
275
|
+
els.every((e) => e.column !== undefined) &&
|
|
276
|
+
sameColumns(els.map((e) => e.column), cols));
|
|
277
|
+
});
|
|
278
|
+
target = declaredIndex?.canonical.name ?? (r.has_exprs === true ? undefined : renamedDefault(name, table, oldCols, cols, "idx")) ?? name;
|
|
279
|
+
}
|
|
280
|
+
carried.push({
|
|
281
|
+
kind: "index",
|
|
282
|
+
name,
|
|
283
|
+
table: n.qualifiedTable,
|
|
284
|
+
tableName: `${n.schema}.${n.table}`,
|
|
285
|
+
tableOid: oid,
|
|
286
|
+
schema: String(r.schema),
|
|
287
|
+
working,
|
|
288
|
+
target,
|
|
289
|
+
definition: carriedIndexStatement(String(r.def), working, from, to),
|
|
290
|
+
validated: true,
|
|
291
|
+
...(r.comment !== null ? { comment: String(r.comment) } : {}),
|
|
292
|
+
...(r.replident === true ? { replicaIdentity: true } : {}),
|
|
293
|
+
...(r.clustered === true ? { clustered: true } : {}),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
else if (kind === "S") {
|
|
297
|
+
const dep = deps.find((d) => d.kind === "class" && d.objid === String(r.oid));
|
|
298
|
+
const seq = { kind: dep?.deptype === "i" ? "identity" : "serial", schema: String(r.schema), name, ident: `${quoteIdent(String(r.schema))}.${quoteIdent(name)}` };
|
|
299
|
+
if (rename) {
|
|
300
|
+
refused.push(`sequence ${r.schema}.${name} gives the column its values (${seq.kind === "identity" ? "an identity column" : "a serial column's default"}); during a rename a writer that names the new column would be given the old one's next value by the trigger, which it cannot tell from a value the writer chose; a rename that keeps its sequence needs versioned views (chant #3331)`);
|
|
301
|
+
}
|
|
302
|
+
else
|
|
303
|
+
sequence = seq;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
const what = deps.find((d) => d.kind === "class" && d.objid === String(r.oid))?.what ?? `${r.schema}.${name}`;
|
|
307
|
+
refused.push(what);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// ── generated columns computed from it ──────────────────────────────
|
|
312
|
+
const attrdefIds = byKind("attrdef");
|
|
313
|
+
if (attrdefIds.length > 0) {
|
|
314
|
+
const rows = await client.query("SELECT a.attname AS name, d.adnum FROM pg_catalog.pg_attrdef d JOIN pg_catalog.pg_attribute a ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE d.oid = ANY($1::oid[])", [attrdefIds]);
|
|
315
|
+
for (const r of rows) {
|
|
316
|
+
if (Number(r.adnum) === attnum)
|
|
317
|
+
continue; // its own default
|
|
318
|
+
refused.push(`generated column ${n.schema}.${n.table}.${r.name} is computed from it; computing it from the new column would rewrite the table, so drop the generated column, migrate, then declare it again`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// ── views, rules ────────────────────────────────────────────────────
|
|
322
|
+
const views = [];
|
|
323
|
+
const rewriteIds = byKind("rewrite");
|
|
324
|
+
if (rewriteIds.length > 0) {
|
|
325
|
+
const rows = await client.query(`WITH RECURSIVE v(oid, depth) AS (
|
|
326
|
+
SELECT r.ev_class, 0 FROM pg_catalog.pg_rewrite r WHERE r.oid = ANY($1::oid[]) AND r.ev_class <> $2::oid
|
|
327
|
+
UNION
|
|
328
|
+
SELECT r.ev_class, v.depth + 1 FROM v
|
|
329
|
+
JOIN pg_catalog.pg_depend d ON d.refclassid = 'pg_catalog.pg_class'::pg_catalog.regclass AND d.refobjid = v.oid AND d.classid = 'pg_catalog.pg_rewrite'::pg_catalog.regclass
|
|
330
|
+
JOIN pg_catalog.pg_rewrite r ON r.oid = d.objid AND r.ev_class <> v.oid
|
|
331
|
+
)
|
|
332
|
+
SELECT c.oid::text AS oid, n.nspname AS schema, c.relname AS name, c.relkind::text AS kind, max(v.depth) AS depth,
|
|
333
|
+
pg_catalog.pg_get_userbyid(c.relowner) AS owner,
|
|
334
|
+
ARRAY(SELECT CASE WHEN x.grantee = 0 THEN 'PUBLIC' ELSE pg_catalog.quote_ident(pg_catalog.pg_get_userbyid(x.grantee)) END || E'\\t' || x.privilege_type || E'\\t' || x.is_grantable::text
|
|
335
|
+
FROM pg_catalog.aclexplode(c.relacl) x WHERE x.grantee <> c.relowner)::text[] AS grants,
|
|
336
|
+
ARRAY(SELECT pg_catalog.pg_describe_object(d.classid, d.objid, d.objsubid) FROM pg_catalog.pg_depend d
|
|
337
|
+
WHERE d.refclassid = 'pg_catalog.pg_class'::pg_catalog.regclass AND d.refobjid = c.oid AND d.deptype = 'n'
|
|
338
|
+
AND d.classid NOT IN ('pg_catalog.pg_rewrite'::pg_catalog.regclass, 'pg_catalog.pg_type'::pg_catalog.regclass))::text[] AS others
|
|
339
|
+
FROM v JOIN pg_catalog.pg_class c ON c.oid = v.oid JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
340
|
+
GROUP BY c.oid, n.nspname, c.relname, c.relkind, c.relowner, c.relacl ORDER BY 5, 2, 3`, [rewriteIds, oid]);
|
|
341
|
+
const ownRules = await client.query("SELECT r.rulename AS name FROM pg_catalog.pg_rewrite r WHERE r.oid = ANY($1::oid[]) AND r.ev_class = $2::oid", [rewriteIds, oid]);
|
|
342
|
+
for (const r of ownRules)
|
|
343
|
+
refused.push(`rule ${r.name} on ${n.schema}.${n.table} reads the column; drop it, migrate, then create it again`);
|
|
344
|
+
for (const r of rows) {
|
|
345
|
+
const name = `${String(r.schema)}.${String(r.name)}`;
|
|
346
|
+
if (r.kind === "m") {
|
|
347
|
+
refused.push(`materialized view ${name} reads the column; making it again would scan in the switch's transaction, so drop it, migrate, then declare it again`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (r.kind !== "v") {
|
|
351
|
+
refused.push(`${name} (relkind ${String(r.kind)}) reads the column`);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const others = r.others ?? [];
|
|
355
|
+
if (others.length > 0)
|
|
356
|
+
refused.push(`view ${name} is used by ${others.join(", ")}, which dropping and creating the view again would break`);
|
|
357
|
+
const declared = input.objects.find((o) => (o.type === POSTGRES_ENTITY_TYPES.view || o.type === POSTGRES_ENTITY_TYPES.materializedView) && `${o.canonical.schema ?? input.defaultSchema}.${o.canonical.name}` === name);
|
|
358
|
+
if (!declared) {
|
|
359
|
+
refused.push(`view ${name} reads ${rename ? `${from}, which the rename takes away` : "the column"}, and the build does not declare it; the switch makes each such view again from its declaration, so declare it (reading ${n.column}) in the build`);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (declared.type !== POSTGRES_ENTITY_TYPES.view) {
|
|
363
|
+
refused.push(`view ${name} is declared as a ${declared.type}`);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
views.push({
|
|
367
|
+
name,
|
|
368
|
+
ident: `${quoteIdent(String(r.schema))}.${quoteIdent(String(r.name))}`,
|
|
369
|
+
materialized: false,
|
|
370
|
+
depth: Number(r.depth),
|
|
371
|
+
declared,
|
|
372
|
+
owner: String(r.owner),
|
|
373
|
+
grants: (r.grants ?? []).map((g) => {
|
|
374
|
+
const [grantee, privilege, grantable] = g.split("\t");
|
|
375
|
+
return { grantee, privilege, grantable: grantable === "true" };
|
|
376
|
+
}),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (const d of deps.filter((x) => x.kind === "other"))
|
|
381
|
+
refused.push(`${d.what} uses the column; drop it, migrate, then create it again`);
|
|
382
|
+
// A new name already taken by something else stops the switch; say so now.
|
|
383
|
+
const renamed = carried.filter((c) => c.target !== c.name);
|
|
384
|
+
if (renamed.length > 0) {
|
|
385
|
+
const taken = await client.query(`SELECT x.name FROM ROWS FROM (pg_catalog.unnest($1::text[]), pg_catalog.unnest($2::text[]), pg_catalog.unnest($3::oid[])) AS x(name, schema, rel)
|
|
386
|
+
WHERE EXISTS (SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace s ON s.oid = c.relnamespace WHERE s.nspname = x.schema AND c.relname = x.name)
|
|
387
|
+
OR EXISTS (SELECT 1 FROM pg_catalog.pg_constraint k WHERE k.conrelid = x.rel AND k.conname = x.name)`, [renamed.map((c) => c.target), renamed.map((c) => c.schema), renamed.map((c) => c.tableOid)]);
|
|
388
|
+
for (const t of taken)
|
|
389
|
+
refused.push(`${t.name}, the name ${renamed.find((c) => c.target === t.name).name} takes after the rename, is already used; rename that object first`);
|
|
390
|
+
}
|
|
391
|
+
return { carried, views, refused, ...(sequence ? { sequence } : {}) };
|
|
392
|
+
}
|
|
393
|
+
// ── statements ──────────────────────────────────────────────────────
|
|
394
|
+
/** `ALTER TABLE ... ADD CONSTRAINT <working> <body> NOT VALID`: a check or foreign key on the new column. */
|
|
395
|
+
export const addCarriedConstraint = (c) => `ALTER TABLE ${c.table} ADD CONSTRAINT ${quoteIdent(c.working)} ${c.definition} NOT VALID`;
|
|
396
|
+
const commentOn = (what, text) => `COMMENT ON ${what} IS ${text === undefined ? "NULL" : pgString(text)}`;
|
|
397
|
+
const indexIdent = (c, name) => `${quoteIdent(c.schema)}.${quoteIdent(name)}`;
|
|
398
|
+
/**
|
|
399
|
+
* The switch's statements for the carried objects, around the swap of the
|
|
400
|
+
* columns: `before` drops the views and swaps every index and constraint;
|
|
401
|
+
* `after` creates the views again from their declarations once the column
|
|
402
|
+
* they read has its name.
|
|
403
|
+
*
|
|
404
|
+
* Foreign keys that reference the column go first: the old key constraint
|
|
405
|
+
* cannot be dropped while one depends on its index.
|
|
406
|
+
*/
|
|
407
|
+
export function carriedSwitchStatements(carried, views, change, oldComment, marker) {
|
|
408
|
+
const before = [];
|
|
409
|
+
const after = [];
|
|
410
|
+
for (const v of [...views].sort((a, b) => b.depth - a.depth))
|
|
411
|
+
before.push(`DROP VIEW ${v.ident}`);
|
|
412
|
+
const order = ["foreign-key", "check", "key", "index"];
|
|
413
|
+
const sorted = [...carried].sort((a, b) => order.indexOf(a.kind) - order.indexOf(b.kind));
|
|
414
|
+
for (const c of sorted) {
|
|
415
|
+
const keepOld = change === "rename" && (c.kind === "index" || (c.kind === "key" && !c.primary));
|
|
416
|
+
if (c.kind === "check" || c.kind === "foreign-key") {
|
|
417
|
+
before.push(`ALTER TABLE ${c.table} DROP CONSTRAINT ${quoteIdent(c.name)}`);
|
|
418
|
+
before.push(`ALTER TABLE ${c.table} RENAME CONSTRAINT ${quoteIdent(c.working)} TO ${quoteIdent(c.target)}`);
|
|
419
|
+
before.push(commentOn(`CONSTRAINT ${quoteIdent(c.target)} ON ${c.table}`, c.comment));
|
|
420
|
+
}
|
|
421
|
+
else if (c.kind === "key") {
|
|
422
|
+
if (keepOld) {
|
|
423
|
+
before.push(`ALTER TABLE ${c.table} RENAME CONSTRAINT ${quoteIdent(c.name)} TO ${quoteIdent(carriedOldName(c.name))}`);
|
|
424
|
+
before.push(commentOn(`CONSTRAINT ${quoteIdent(carriedOldName(c.name))} ON ${c.table}`, oldComment(c)));
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
before.push(`ALTER TABLE ${c.table} DROP CONSTRAINT ${quoteIdent(c.name)}`);
|
|
428
|
+
}
|
|
429
|
+
before.push(`ALTER TABLE ${c.table} ADD CONSTRAINT ${quoteIdent(c.target)} ${c.primary ? "PRIMARY KEY" : "UNIQUE"} USING INDEX ${quoteIdent(c.working)}${c.deferral ?? ""}`);
|
|
430
|
+
before.push(commentOn(`CONSTRAINT ${quoteIdent(c.target)} ON ${c.table}`, c.comment));
|
|
431
|
+
before.push(commentOn(`INDEX ${indexIdent(c, c.target)}`, c.indexComment));
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
if (keepOld) {
|
|
435
|
+
before.push(`ALTER INDEX ${indexIdent(c, c.name)} RENAME TO ${quoteIdent(carriedOldName(c.name))}`);
|
|
436
|
+
before.push(commentOn(`INDEX ${indexIdent(c, carriedOldName(c.name))}`, oldComment(c)));
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
before.push(`DROP INDEX ${indexIdent(c, c.name)}`);
|
|
440
|
+
}
|
|
441
|
+
before.push(`ALTER INDEX ${indexIdent(c, c.working)} RENAME TO ${quoteIdent(c.target)}`);
|
|
442
|
+
before.push(commentOn(`INDEX ${indexIdent(c, c.target)}`, c.comment));
|
|
443
|
+
}
|
|
444
|
+
if (c.replicaIdentity)
|
|
445
|
+
before.push(`ALTER TABLE ${c.table} REPLICA IDENTITY USING INDEX ${quoteIdent(c.target)}`);
|
|
446
|
+
if (c.clustered)
|
|
447
|
+
before.push(`ALTER TABLE ${c.table} CLUSTER ON ${quoteIdent(c.target)}`);
|
|
448
|
+
}
|
|
449
|
+
for (const v of [...views].sort((a, b) => a.depth - b.depth)) {
|
|
450
|
+
for (const s of createSteps(v.declared, marker))
|
|
451
|
+
after.push(s.sql);
|
|
452
|
+
after.push(`ALTER VIEW ${v.ident} OWNER TO ${quoteIdent(v.owner)}`);
|
|
453
|
+
for (const g of v.grants)
|
|
454
|
+
after.push(`GRANT ${g.privilege} ON TABLE ${v.ident} TO ${g.grantee}${g.grantable ? " WITH GRANT OPTION" : ""}`);
|
|
455
|
+
}
|
|
456
|
+
return { before, after };
|
|
457
|
+
}
|
|
458
|
+
/** What the gate binds about the carried objects: each one's kind, names and definition, and each view. */
|
|
459
|
+
export function carriedSubject(d) {
|
|
460
|
+
return {
|
|
461
|
+
...(d.sequence ? { sequence: { kind: d.sequence.kind, name: `${d.sequence.schema}.${d.sequence.name}` } } : {}),
|
|
462
|
+
carried: d.carried.map((c) => ({ kind: c.kind, table: c.tableName, name: c.name, target: c.target, definition: c.definition, ...(c.primary !== undefined ? { primary: c.primary } : {}) })),
|
|
463
|
+
views: d.views.map((v) => ({ name: v.name, ddl: v.declared.ddl })),
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/** Whether a declared column is NOT NULL once it is switched: declared so, an identity column, or part of a carried primary key. */
|
|
467
|
+
export const requiresNotNull = (column, carried) => column.notNull === true || column.generated?.kind === "identity" || SERIAL_TYPES[column.type ?? ""] !== undefined || carried.some((c) => c.kind === "key" && c.primary === true);
|
|
468
|
+
/** The integer type of each `serial` spelling: what the column holds, and the sequence's type. */
|
|
469
|
+
export const SERIAL_TYPES = {
|
|
470
|
+
smallserial: "smallint",
|
|
471
|
+
serial2: "smallint",
|
|
472
|
+
serial: "integer",
|
|
473
|
+
serial4: "integer",
|
|
474
|
+
bigserial: "bigint",
|
|
475
|
+
serial8: "bigint",
|
|
476
|
+
};
|
|
477
|
+
/** A declared column's type as the server holds it: a `serial` spelling is its integer type. */
|
|
478
|
+
export const storedType = (type) => (type === undefined ? undefined : (SERIAL_TYPES[type.replace(/^(public|pg_catalog)\./, "")] ?? type));
|
|
479
|
+
/**
|
|
480
|
+
* The working copies of the carried objects on the server, by working name.
|
|
481
|
+
* One under a working name that is somebody else's is refused, as for the
|
|
482
|
+
* other working objects (`./observe.ts`).
|
|
483
|
+
*/
|
|
484
|
+
export async function carriedStates(client, carried, ours, refuse) {
|
|
485
|
+
const out = new Map();
|
|
486
|
+
const indexes = carried.filter((c) => c.kind === "index" || c.kind === "key");
|
|
487
|
+
const constraints = carried.filter((c) => c.kind === "check" || c.kind === "foreign-key");
|
|
488
|
+
if (indexes.length > 0) {
|
|
489
|
+
const rows = await client.query(`SELECT c.relname AS name, i.indisvalid AS valid, pg_catalog.obj_description(c.oid, 'pg_class') AS comment, pg_catalog.pg_get_indexdef(c.oid) AS def
|
|
490
|
+
FROM ROWS FROM (pg_catalog.unnest($1::text[]), pg_catalog.unnest($2::text[])) AS x(name, schema)
|
|
491
|
+
JOIN pg_catalog.pg_namespace s ON s.nspname = x.schema JOIN pg_catalog.pg_class c ON c.relnamespace = s.oid AND c.relname = x.name
|
|
492
|
+
LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid`, [indexes.map((c) => c.working), indexes.map((c) => c.schema)]);
|
|
493
|
+
for (const r of rows) {
|
|
494
|
+
const c = indexes.find((x) => x.working === r.name);
|
|
495
|
+
const owner = ours(r.comment ?? undefined);
|
|
496
|
+
const expected = c.definition.replace(/ INDEX CONCURRENTLY /, " INDEX ");
|
|
497
|
+
if (owner === "theirs" || (owner === "none" && r.def !== expected))
|
|
498
|
+
refuse(`index ${c.schema}.${c.working}`);
|
|
499
|
+
out.set(c.working, { present: true, ready: r.valid === true, marked: owner === "ours" });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (constraints.length > 0) {
|
|
503
|
+
const rows = await client.query(`SELECT k.conname AS name, k.conrelid::text AS rel, k.convalidated AS validated, pg_catalog.obj_description(k.oid, 'pg_constraint') AS comment
|
|
504
|
+
FROM ROWS FROM (pg_catalog.unnest($1::text[]), pg_catalog.unnest($2::oid[])) AS x(name, rel) JOIN pg_catalog.pg_constraint k ON k.conrelid = x.rel AND k.conname = x.name`, [constraints.map((c) => c.working), constraints.map((c) => c.tableOid)]);
|
|
505
|
+
for (const r of rows) {
|
|
506
|
+
const c = constraints.find((x) => x.working === r.name && x.tableOid === r.rel);
|
|
507
|
+
if (ours(r.comment ?? undefined) !== "ours")
|
|
508
|
+
refuse(`constraint ${c.working} on ${c.tableName}`);
|
|
509
|
+
out.set(c.working, { present: true, ready: r.validated === true || !c.validated, marked: true });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
/** The carried objects whose working copy is not there, not marked, or not yet valid. */
|
|
515
|
+
export const carriedNotReady = (carried, states) => carried.filter((c) => {
|
|
516
|
+
const s = states.get(c.working);
|
|
517
|
+
return !s || !s.ready || !s.marked;
|
|
518
|
+
});
|