@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,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant sql diff` and `chant sql plan`: the classified change between two
|
|
3
|
+
* builds, or between a build and a live server. A Postgres build (its output
|
|
4
|
+
* says `"dialect": "postgres"`) or a Postgres binding goes to
|
|
5
|
+
* `../../postgres/plan/commands.ts`.
|
|
6
|
+
*
|
|
7
|
+
* chant sql diff <before.json> <after.json> [--json] [--statements] [--topology <topology>]
|
|
8
|
+
* chant sql plan <env> <build.json> [--json]
|
|
9
|
+
*
|
|
10
|
+
* `diff` is offline: it compares two `chant build` outputs, say a pull
|
|
11
|
+
* request's base and head, and needs no server. `plan` compares a build with
|
|
12
|
+
* the server `sql.profiles.<env>` binds and asks that server's formatter about
|
|
13
|
+
* expressions the rules leave different. Both exit 2 when a change needs a
|
|
14
|
+
* rebuild, which a plan refuses to make in place, and `plan` also when the
|
|
15
|
+
* declared databases hold an object whose definition it cannot read (#3653),
|
|
16
|
+
* which it names; 0 otherwise. For each
|
|
17
|
+
* refused table they name the `ClickHouseRebuildOp` to run instead
|
|
18
|
+
* (`rebuildOps` in `--json`, `./rebuild-handoff.ts`).
|
|
19
|
+
*
|
|
20
|
+
* `diff --statements` prints the statements instead, for a migration file:
|
|
21
|
+
* each one the applier would send, with its change's rule and class, and a
|
|
22
|
+
* refused change as the Op that makes it (`../../migration-statements.ts`).
|
|
23
|
+
* The marker in each `CREATE` is the project's `ownership` config's.
|
|
24
|
+
*
|
|
25
|
+
* `--topology` (`single`, `cluster:<name>`, `replicated`, `replicated:<cluster>`,
|
|
26
|
+
* `cloud`; `../topology.ts`) renders a ClickHouse diff's declarations and
|
|
27
|
+
* statements for that topology: `ON CLUSTER` and the engine. `--statements`
|
|
28
|
+
* renders for `single` without it; the plain diff compares the declarations as
|
|
29
|
+
* written. `plan` takes the topology from `sql.profiles.<env>.topology`.
|
|
30
|
+
*/
|
|
31
|
+
import { diffSchemas } from "./diff.js";
|
|
32
|
+
import { renderDiff } from "./report.js";
|
|
33
|
+
import { keyedByQualifiedName, schemaFromBuildFile, schemaFromServer } from "./schema.js";
|
|
34
|
+
import { dropFormattingOnly } from "./server-format.js";
|
|
35
|
+
import { scopeOf } from "./normalize.js";
|
|
36
|
+
import { ACCESS_KINDS, accessUnmanagedHint, declaredAccess } from "../live/catalog.js";
|
|
37
|
+
import { bindClickHouse } from "../live/bind.js";
|
|
38
|
+
import { rebuildOpSuggestions } from "./rebuild-handoff.js";
|
|
39
|
+
import { readFileSync } from "node:fs";
|
|
40
|
+
import { parseTopology } from "../topology.js";
|
|
41
|
+
/** The dialect a build output names. */
|
|
42
|
+
function outputDialect(path) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readFileSync(path, "utf-8")).dialect;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function split(args) {
|
|
51
|
+
const positional = [];
|
|
52
|
+
let topology;
|
|
53
|
+
for (let i = 0; i < args.length; i++) {
|
|
54
|
+
const a = args[i];
|
|
55
|
+
if (a === "--topology")
|
|
56
|
+
topology = args[++i] ?? "";
|
|
57
|
+
else if (a.startsWith("--topology="))
|
|
58
|
+
topology = a.slice("--topology=".length);
|
|
59
|
+
else if (!a.startsWith("--"))
|
|
60
|
+
positional.push(a);
|
|
61
|
+
}
|
|
62
|
+
return { positional, json: args.includes("--json"), statements: args.includes("--statements"), ...(topology !== undefined ? { topology } : {}) };
|
|
63
|
+
}
|
|
64
|
+
/** `chant sql diff --statements`: the statements between two builds, as SQL with comments or as JSON. Exits 2 when a step is an Op or manual. */
|
|
65
|
+
async function runDiffStatements(before, after, json, topology) {
|
|
66
|
+
const { diffStatements, renderStatements } = await import("../../migration-statements.js");
|
|
67
|
+
const { resolveOwnershipMarker } = await import("../../core/apply.js");
|
|
68
|
+
let marker;
|
|
69
|
+
try {
|
|
70
|
+
const { loadChantConfig } = await import("@intentius/chant/config");
|
|
71
|
+
const config = await loadChantConfig(process.cwd()).then((r) => r.config).catch(() => undefined);
|
|
72
|
+
marker = resolveOwnershipMarker({}, config, "chant sql diff --statements");
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
const doc = diffStatements(readFileSync(before, "utf-8"), readFileSync(after, "utf-8"), { ...(marker ? { marker } : {}), ...(topology ? { topology } : {}) });
|
|
79
|
+
console.log(json ? JSON.stringify(doc, null, 2) : renderStatements(doc));
|
|
80
|
+
return doc.refused ? 2 : 0;
|
|
81
|
+
}
|
|
82
|
+
function emit(diff, json, title) {
|
|
83
|
+
console.log(json ? JSON.stringify(diff, null, 2) : renderDiff(diff, { title }));
|
|
84
|
+
return diff.rebuilds.length > 0 || (diff.unreadable?.length ?? 0) > 0 ? 2 : 0;
|
|
85
|
+
}
|
|
86
|
+
export async function runDiff(ctx) {
|
|
87
|
+
const { positional, json, statements, topology: topologyArg } = split(ctx.rawArgs);
|
|
88
|
+
if (positional.length !== 2) {
|
|
89
|
+
console.error("usage: chant sql diff <before.json> <after.json> [--json] [--statements] [--topology <topology>]");
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
let topology;
|
|
93
|
+
try {
|
|
94
|
+
topology = topologyArg !== undefined ? parseTopology(topologyArg) : undefined;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.error(`--topology: ${err.message}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
if (statements)
|
|
101
|
+
return runDiffStatements(positional[0], positional[1], json, topology);
|
|
102
|
+
if (outputDialect(positional[1]) === "postgres") {
|
|
103
|
+
const { diffPgBuildFiles, emitPg, projectMajor } = await import("../../postgres/plan/commands.js");
|
|
104
|
+
return emitPg(diffPgBuildFiles(positional[0], positional[1], await projectMajor()), json, `${positional[0]} -> ${positional[1]}`);
|
|
105
|
+
}
|
|
106
|
+
const after = schemaFromBuildFile(positional[1], "default", topology);
|
|
107
|
+
const diff = diffSchemas(schemaFromBuildFile(positional[0], "default", topology), after);
|
|
108
|
+
const rebuildOps = rebuildOpSuggestions(diff, new Map(after.map((o) => [o.key, o.canonical])), "<env>");
|
|
109
|
+
return emit(rebuildOps.length > 0 ? { ...diff, rebuildOps } : diff, json, `${positional[0]} -> ${positional[1]}`);
|
|
110
|
+
}
|
|
111
|
+
/** The declared objects against the server, keys and labels by `database.name`. */
|
|
112
|
+
export async function planAgainstServer(environment, buildFile, options = {}) {
|
|
113
|
+
const target = await bindClickHouse({ ...options, environment });
|
|
114
|
+
const all = keyedByQualifiedName(schemaFromBuildFile(buildFile, target.defaultDatabase, target.topology));
|
|
115
|
+
// Access declarations a profile does not manage are not planned, and the server's access is not read (#3716).
|
|
116
|
+
const declared = target.access === true ? all : all.filter((o) => !ACCESS_KINDS.has(o.canonical.kind));
|
|
117
|
+
const unmanaged = all.length - declared.length;
|
|
118
|
+
const databases = new Set(declared.map((o) => scopeOf(o.canonical)));
|
|
119
|
+
const unreadable = [];
|
|
120
|
+
const live = await schemaFromServer(target, databases, unreadable, declaredAccess(declared.map((o) => o.canonical)));
|
|
121
|
+
const diff = diffSchemas(live, declared);
|
|
122
|
+
const changes = await dropFormattingOnly(target.endpoint, diff.changes);
|
|
123
|
+
const rebuildOps = rebuildOpSuggestions({ ...diff, changes, rebuilds: changes.filter((c) => c.class === "rebuild") }, new Map(declared.map((o) => [o.key, o.canonical])), environment);
|
|
124
|
+
const label = new Map(declared.map((o) => [o.key, o.exportName === o.key ? o.key : `${o.exportName} (${o.key})`]));
|
|
125
|
+
const relabel = changes.map((c) => ({ ...c, object: label.get(c.object) ?? c.object }));
|
|
126
|
+
return {
|
|
127
|
+
changes: relabel,
|
|
128
|
+
rebuilds: relabel.filter((c) => c.class === "rebuild"),
|
|
129
|
+
hints: unmanaged > 0 ? [...diff.hints, accessUnmanagedHint(unmanaged, environment)] : diff.hints,
|
|
130
|
+
...(rebuildOps.length > 0 ? { rebuildOps } : {}),
|
|
131
|
+
...(unreadable.length > 0 ? { unreadable } : {}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export async function runPlan(ctx) {
|
|
135
|
+
const { positional, json } = split(ctx.rawArgs);
|
|
136
|
+
if (positional.length !== 2) {
|
|
137
|
+
console.error("usage: chant sql plan <env> <build.json> [--json]");
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
if (outputDialect(positional[1]) === "postgres") {
|
|
141
|
+
const { planPgAgainstServer, emitPg } = await import("../../postgres/plan/commands.js");
|
|
142
|
+
return emitPg(await planPgAgainstServer(positional[0], positional[1]), json, `${positional[1]} against ${positional[0]}`);
|
|
143
|
+
}
|
|
144
|
+
const diff = await planAgainstServer(positional[0], positional[1]);
|
|
145
|
+
return emit(diff, json, `${positional[1]} against ${positional[0]}`);
|
|
146
|
+
}
|
|
147
|
+
export const sqlCommands = {
|
|
148
|
+
name: "sql",
|
|
149
|
+
description: "Schema changes, classified: between two builds, or a build and a live server",
|
|
150
|
+
commands: [
|
|
151
|
+
{ name: "diff", description: "Classify the change between two chant build outputs (offline); --statements prints the statements it takes, --topology renders them for single, cluster:<name>, replicated or cloud", handler: runDiff },
|
|
152
|
+
{ name: "plan", description: "Classify the change from an environment's server to a chant build output", handler: runPlan },
|
|
153
|
+
{
|
|
154
|
+
name: "types",
|
|
155
|
+
description: "Write sql-types.d.ts: typed columns for each table, view and dictionary the build declares, for tags given their name (table<\"app.users\">) and RowOf; --output <path> writes it elsewhere; --check compares instead of writing and exits 1 when the file differs",
|
|
156
|
+
handler: async (ctx) => (await import("../../sql-types-command.js")).runTypes(ctx),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `observeResourcesDeep()` for the ClickHouse dialect: each declared object's
|
|
3
|
+
* live definition as a property tree in the declaration's own shape, so
|
|
4
|
+
* `chant lifecycle diff --live` and `plan` report property drift with no false
|
|
5
|
+
* drift.
|
|
6
|
+
*
|
|
7
|
+
* The reader parses the server's `SHOW CREATE` with the same tag the
|
|
8
|
+
* declaration used, which gives the same props shape. A field whose canonical
|
|
9
|
+
* form (`./normalize.ts`) equals the declaration's is written with the
|
|
10
|
+
* declaration's own value, so `INTERVAL 180 DAY` and `toIntervalDay(180)` do
|
|
11
|
+
* not read as drift; a field that differs keeps the server's value.
|
|
12
|
+
* `deepNormalizationHooks` prunes what is not a property of the object (the
|
|
13
|
+
* DDL text, the template source, lineage, a view's reads and `TO` target).
|
|
14
|
+
* `chant sql plan` covers the target, and classifies every change.
|
|
15
|
+
*
|
|
16
|
+
* On a `cluster:<name>` topology every replica of every shard is read
|
|
17
|
+
* (#3664). When a server's copy of a table differs from the profile
|
|
18
|
+
* server's, that copy is what is reported, with the server in `observedOn`,
|
|
19
|
+
* which the drift report prints as `seen on`.
|
|
20
|
+
*
|
|
21
|
+
* Where the profile manages access, the grants an apply would give or take
|
|
22
|
+
* from each declared grantee are reported as `pending` (#3733), the same
|
|
23
|
+
* statements `chant sql plan` plans (SQLCH273, SQLCH274), so a privilege
|
|
24
|
+
* granted or revoked by hand after an approval moves the plan digest.
|
|
25
|
+
*/
|
|
26
|
+
import { deepObservation, } from "@intentius/chant/deep-observation";
|
|
27
|
+
import { bindClickHouse, classifyClickHouseFailure } from "../live/bind.js";
|
|
28
|
+
import { ACCESS_ENTITY_TYPES, ACCESS_UNMANAGED_DETAIL, accessOf, readLiveAccess, readLiveSchema, SYSTEM_DATABASES } from "../live/catalog.js";
|
|
29
|
+
import { clickhouseQuery } from "../http.js";
|
|
30
|
+
import { sqlString } from "../apply/statements.js";
|
|
31
|
+
import { stripMarkerFromStatement } from "../ownership.js";
|
|
32
|
+
import { database, dictionary, func, policy, quota, role, settingsProfile, table, user, view, CLICKHOUSE_ENTITY_TYPES } from "../entities.js";
|
|
33
|
+
import { canonicalObject, grantsObject, objectKey } from "./normalize.js";
|
|
34
|
+
import { grantStatement, grantsByGrantee, revokeStatement } from "../access.js";
|
|
35
|
+
import { renderFor } from "../topology.js";
|
|
36
|
+
// `concurrently` is how a Postgres index is created, not a property of it.
|
|
37
|
+
const PRUNED = new Set(["ddl", "source", "lineage", "reads", "to", "orReplace", "ifNotExists", "concurrently"]);
|
|
38
|
+
export const sqlDeepNormalizationHooks = {
|
|
39
|
+
prune: (node) => PRUNED.has(node.pattern.split(/[.[]/)[0]),
|
|
40
|
+
};
|
|
41
|
+
function liveProps(o) {
|
|
42
|
+
const tag = o.type === CLICKHOUSE_ENTITY_TYPES.database
|
|
43
|
+
? database
|
|
44
|
+
: o.type === CLICKHOUSE_ENTITY_TYPES.table
|
|
45
|
+
? table
|
|
46
|
+
: o.type === CLICKHOUSE_ENTITY_TYPES.dictionary
|
|
47
|
+
? dictionary
|
|
48
|
+
: o.type === CLICKHOUSE_ENTITY_TYPES.function
|
|
49
|
+
? func
|
|
50
|
+
: o.type === CLICKHOUSE_ENTITY_TYPES.user
|
|
51
|
+
? user
|
|
52
|
+
: o.type === CLICKHOUSE_ENTITY_TYPES.role
|
|
53
|
+
? role
|
|
54
|
+
: o.type === CLICKHOUSE_ENTITY_TYPES.rowPolicy
|
|
55
|
+
? policy
|
|
56
|
+
: o.type === CLICKHOUSE_ENTITY_TYPES.quota
|
|
57
|
+
? quota
|
|
58
|
+
: o.type === CLICKHOUSE_ENTITY_TYPES.settingsProfile
|
|
59
|
+
? settingsProfile
|
|
60
|
+
: view;
|
|
61
|
+
const strings = Object.assign([o.statement], { raw: [o.statement] });
|
|
62
|
+
return { ...tag(strings).props };
|
|
63
|
+
}
|
|
64
|
+
const sameColumn = (a, b) => {
|
|
65
|
+
const strip = ({ text: _t, position: _p, previously: _v, ...rest }) => rest;
|
|
66
|
+
return JSON.stringify(strip(a)) === JSON.stringify(strip(b));
|
|
67
|
+
};
|
|
68
|
+
/** The live props, with each field the declaration means the same as written the declaration's way. */
|
|
69
|
+
export function inDeclaredVocabulary(declared, live, d, l) {
|
|
70
|
+
const out = { ...live };
|
|
71
|
+
const same = (k) => JSON.stringify(d[k]) === JSON.stringify(l[k]);
|
|
72
|
+
const adopt = (key, equal) => {
|
|
73
|
+
if (!equal)
|
|
74
|
+
return;
|
|
75
|
+
if (declared[key] === undefined)
|
|
76
|
+
delete out[key];
|
|
77
|
+
else
|
|
78
|
+
out[key] = declared[key];
|
|
79
|
+
};
|
|
80
|
+
adopt("name", d.name === l.name);
|
|
81
|
+
adopt("database", d.database === l.database);
|
|
82
|
+
adopt("engine", same("engine"));
|
|
83
|
+
for (const k of ["orderBy", "primaryKey", "partitionBy", "sampleBy", "ttl", "comment", "select", "refresh", "dataSource", "layout", "lifetime", "range", "asFunction"])
|
|
84
|
+
adopt(k, same(k));
|
|
85
|
+
adopt("settings", same("settings"));
|
|
86
|
+
// A view's DEFINER and SQL SECURITY are not compared by a plan; the server adds its own to a refreshable materialized view (#3911).
|
|
87
|
+
adopt("security", d.kind === "view" || d.kind === "materializedView");
|
|
88
|
+
// A user's, role's or row policy's fields, compared as `../access.ts` reads them.
|
|
89
|
+
if (d.kind === "user" || d.kind === "role" || d.kind === "rowPolicy") {
|
|
90
|
+
for (const k of ["identified", "host", "validUntil", "defaultRole", "defaultDatabase", "grantees", "settings", "as", "for", "using", "to"]) {
|
|
91
|
+
adopt(k, d.access?.[k] === l.access?.[k]);
|
|
92
|
+
}
|
|
93
|
+
adopt("table", d.table === l.table);
|
|
94
|
+
// A user's authentication is the environment's unless the declaration says it.
|
|
95
|
+
if (d.kind === "user" && declared.identified === undefined)
|
|
96
|
+
delete out.identified;
|
|
97
|
+
}
|
|
98
|
+
// A quota's or settings profile's fields; a quota's intervals are one prop, the same when every interval is.
|
|
99
|
+
if (d.kind === "quota" || d.kind === "settingsProfile") {
|
|
100
|
+
const fields = (o, pick) => JSON.stringify(Object.entries(o.access ?? {}).filter(([k]) => pick(k)));
|
|
101
|
+
for (const k of ["keyed", "settings", "to"])
|
|
102
|
+
adopt(k, d.access?.[k] === l.access?.[k]);
|
|
103
|
+
adopt("intervals", fields(d, (k) => k.startsWith("interval.")) === fields(l, (k) => k.startsWith("interval.")));
|
|
104
|
+
}
|
|
105
|
+
// A function's parameters and expression are one lambda: the same lambda is the declaration's words.
|
|
106
|
+
adopt("params", d.kind === "function" && same("lambda"));
|
|
107
|
+
adopt("body", d.kind === "function" && same("lambda"));
|
|
108
|
+
adopt("indexes", same("indexes"));
|
|
109
|
+
adopt("projections", same("projections"));
|
|
110
|
+
adopt("constraints", same("constraints"));
|
|
111
|
+
const declaredColumns = declared.columns ?? [];
|
|
112
|
+
if (d.kind === "view" || d.kind === "materializedView" || (d.kind === "table" && d.asFunction !== undefined && declaredColumns.length === 0)) {
|
|
113
|
+
// The server infers a view's columns, and a table function's (#3911); a declaration that lists none is not drift.
|
|
114
|
+
if (declaredColumns.length === 0)
|
|
115
|
+
out.columns = [];
|
|
116
|
+
}
|
|
117
|
+
else if (d.kind === "table" || d.kind === "dictionary") {
|
|
118
|
+
const liveColumns = live.columns ?? [];
|
|
119
|
+
out.columns = liveColumns.map((c, i) => {
|
|
120
|
+
const lc = l.columns[i];
|
|
121
|
+
const dc = d.columns.find((x) => x.name === c.name);
|
|
122
|
+
const di = dc ? declaredColumns[dc.position] : undefined;
|
|
123
|
+
return lc && dc && di && sameColumn(lc, dc) ? di : c;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
/** The entity types that belong to no database. */
|
|
129
|
+
const GLOBAL_TYPES = new Set([
|
|
130
|
+
CLICKHOUSE_ENTITY_TYPES.database,
|
|
131
|
+
CLICKHOUSE_ENTITY_TYPES.function,
|
|
132
|
+
CLICKHOUSE_ENTITY_TYPES.user,
|
|
133
|
+
CLICKHOUSE_ENTITY_TYPES.role,
|
|
134
|
+
CLICKHOUSE_ENTITY_TYPES.quota,
|
|
135
|
+
CLICKHOUSE_ENTITY_TYPES.settingsProfile,
|
|
136
|
+
]);
|
|
137
|
+
/** Whether the server's formatter prints two expressions the same. A formatter that cannot parse them says no. */
|
|
138
|
+
async function sameExpression(target, a, b) {
|
|
139
|
+
try {
|
|
140
|
+
const [row] = await clickhouseQuery(target.endpoint, `SELECT formatQuerySingleLine(${sqlString(`SELECT ${a}`)}) AS a, formatQuerySingleLine(${sqlString(`SELECT ${b}`)}) AS b`);
|
|
141
|
+
return row !== undefined && row.a === row.b;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* On a `cluster:<name>` topology, read `system.tables` on every replica of
|
|
149
|
+
* every shard through the profile's server (`clusterAllReplicas`). A change
|
|
150
|
+
* made on one server without `ON CLUSTER` (a TTL altered on shard 2 alone)
|
|
151
|
+
* is then seen, which the profile's server alone would not show. Undefined
|
|
152
|
+
* for any other topology: there is one copy.
|
|
153
|
+
*/
|
|
154
|
+
async function clusterCopies(target) {
|
|
155
|
+
if (target.topology?.kind !== "cluster")
|
|
156
|
+
return undefined;
|
|
157
|
+
const scope = target.databases
|
|
158
|
+
? `IN (${target.databases.map(sqlString).join(", ")})`
|
|
159
|
+
: `NOT IN (${[...SYSTEM_DATABASES].map(sqlString).join(", ")})`;
|
|
160
|
+
const [self] = await clickhouseQuery(target.endpoint, "SELECT hostName() AS host");
|
|
161
|
+
const rows = await clickhouseQuery(target.endpoint, `SELECT hostName() AS host, database, name, create_table_query AS statement ` +
|
|
162
|
+
`FROM clusterAllReplicas(${sqlString(target.topology.cluster)}, system.tables) ` +
|
|
163
|
+
`WHERE database ${scope} AND NOT is_temporary AND name NOT LIKE '.inner%' AND engine != 'Dictionary' ORDER BY host, database, name`);
|
|
164
|
+
const byKey = new Map();
|
|
165
|
+
for (const r of rows) {
|
|
166
|
+
const key = `${r.database}.${r.name}`;
|
|
167
|
+
if (!byKey.has(key))
|
|
168
|
+
byKey.set(key, new Map());
|
|
169
|
+
byKey.get(key).set(r.host, stripMarkerFromStatement(r.statement));
|
|
170
|
+
}
|
|
171
|
+
return { self: self?.host ?? "", byKey };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* When the servers' copies of `key` do not all read the same, the copy to
|
|
175
|
+
* report and its server: whichever departs furthest from the declaration
|
|
176
|
+
* `d`, the profile server's own winning a tie. A copy that differs from the
|
|
177
|
+
* declaration on any server is then the one the diff sees. Undefined when
|
|
178
|
+
* every copy reads the same.
|
|
179
|
+
*/
|
|
180
|
+
function copyToReport(copies, key, d, defaultDatabase) {
|
|
181
|
+
const byHost = copies.byKey.get(key);
|
|
182
|
+
const mine = byHost?.get(copies.self);
|
|
183
|
+
if (!byHost || mine === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
const shape = (c) => {
|
|
186
|
+
const { columns, ...rest } = c;
|
|
187
|
+
return JSON.stringify({ ...rest, columns: columns.map(({ text: _t, position: _p, ...col }) => col) });
|
|
188
|
+
};
|
|
189
|
+
const reference = shape(canonicalObject(mine, defaultDatabase));
|
|
190
|
+
const declared = JSON.parse(shape(d));
|
|
191
|
+
/** How many of the declaration's fields this copy holds differently. */
|
|
192
|
+
const departures = (statement) => {
|
|
193
|
+
const live = JSON.parse(shape(canonicalObject(statement, defaultDatabase)));
|
|
194
|
+
return Object.keys({ ...declared, ...live }).filter((k) => k !== "name" && k !== "database" && JSON.stringify(declared[k]) !== JSON.stringify(live[k])).length;
|
|
195
|
+
};
|
|
196
|
+
let pick;
|
|
197
|
+
for (const [host, statement] of byHost) {
|
|
198
|
+
if (host === copies.self || shape(canonicalObject(statement, defaultDatabase)) === reference)
|
|
199
|
+
continue;
|
|
200
|
+
const score = departures(statement);
|
|
201
|
+
if (!pick || score > pick.score)
|
|
202
|
+
pick = { host, statement, score };
|
|
203
|
+
}
|
|
204
|
+
if (!pick)
|
|
205
|
+
return undefined;
|
|
206
|
+
return pick.score > departures(mine) ? { host: pick.host, statement: pick.statement } : { host: copies.self, statement: mine };
|
|
207
|
+
}
|
|
208
|
+
export async function observeResourcesDeep(options) {
|
|
209
|
+
const resources = {};
|
|
210
|
+
const unobserved = {};
|
|
211
|
+
let target;
|
|
212
|
+
let live;
|
|
213
|
+
try {
|
|
214
|
+
target = await bindClickHouse(options);
|
|
215
|
+
live = [...(await readLiveSchema(target)), ...(target.access === true ? await readLiveAccess(target, accessOf(options.entities, target.defaultDatabase)) : [])];
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
const why = classifyClickHouseFailure(err);
|
|
219
|
+
for (const name of options.entityNames) {
|
|
220
|
+
unobserved[name] = { type: options.entities.get(name)?.entityType ?? "", reason: why.reason, detail: why.detail };
|
|
221
|
+
}
|
|
222
|
+
return deepObservation({}, unobserved);
|
|
223
|
+
}
|
|
224
|
+
const byKey = new Map(live.map((o) => [objectKey(o), o]));
|
|
225
|
+
let copies;
|
|
226
|
+
try {
|
|
227
|
+
copies = await clusterCopies(target);
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
// A cluster whose servers cannot all be read is not reported as matching.
|
|
231
|
+
const why = classifyClickHouseFailure(err);
|
|
232
|
+
for (const name of options.entityNames) {
|
|
233
|
+
unobserved[name] = { type: options.entities.get(name)?.entityType ?? "", reason: why.reason, detail: `reading every server of the cluster: ${why.detail}` };
|
|
234
|
+
}
|
|
235
|
+
return deepObservation({}, unobserved);
|
|
236
|
+
}
|
|
237
|
+
const pending = [];
|
|
238
|
+
if (target.access === true) {
|
|
239
|
+
try {
|
|
240
|
+
pending.push(...(await pendingGrants(target, options.entities)));
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
const why = classifyClickHouseFailure(err);
|
|
244
|
+
for (const name of options.entityNames) {
|
|
245
|
+
if (options.entities.get(name)?.entityType !== CLICKHOUSE_ENTITY_TYPES.grant)
|
|
246
|
+
continue;
|
|
247
|
+
unobserved[name] = { type: CLICKHOUSE_ENTITY_TYPES.grant, reason: "read-failed", detail: `the server's grants could not be compared: ${why.detail}` };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const name of options.entityNames) {
|
|
252
|
+
const entity = options.entities.get(name);
|
|
253
|
+
if (!entity || !entity.entityType.startsWith("ClickHouse::")) {
|
|
254
|
+
unobserved[name] = { type: entity?.entityType ?? "", reason: "unsupported-kind" };
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (target.access !== true && ACCESS_ENTITY_TYPES.has(entity.entityType)) {
|
|
258
|
+
unobserved[name] = { type: entity.entityType, reason: "filtered", detail: ACCESS_UNMANAGED_DETAIL };
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (entity.entityType === CLICKHOUSE_ENTITY_TYPES.grant) {
|
|
262
|
+
if (unobserved[name])
|
|
263
|
+
continue;
|
|
264
|
+
unobserved[name] = {
|
|
265
|
+
type: entity.entityType,
|
|
266
|
+
reason: "unsupported-kind",
|
|
267
|
+
detail: "a grant is compared as part of its grantees' grants: what an apply would grant or revoke is listed as pending (SQLCH273, SQLCH274)",
|
|
268
|
+
};
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const props = entity.props;
|
|
272
|
+
const isDb = GLOBAL_TYPES.has(entity.entityType);
|
|
273
|
+
const db = isDb ? undefined : typeof props.database === "string" ? props.database : target.defaultDatabase;
|
|
274
|
+
const o = byKey.get(objectKey({ type: entity.entityType, ...(db !== undefined ? { database: db } : {}), name: String(props.name), ...(typeof props.table === "string" ? { table: props.table } : {}) }));
|
|
275
|
+
if (!o)
|
|
276
|
+
continue;
|
|
277
|
+
try {
|
|
278
|
+
const d = canonicalObject(renderFor(String(props.ddl), target.topology), target.defaultDatabase);
|
|
279
|
+
const physicalId = o.uuid ?? `${db ? `${db}.` : ""}${o.name}`;
|
|
280
|
+
const other = copies && !isDb ? copyToReport(copies, `${db ?? ""}.${o.name}`, d, target.defaultDatabase) : undefined;
|
|
281
|
+
if (other) {
|
|
282
|
+
// The servers disagree: report the copy that departs from the declaration, and where it was seen.
|
|
283
|
+
const copy = { ...o, statement: other.host === copies.self ? o.statement : other.statement };
|
|
284
|
+
resources[name] = {
|
|
285
|
+
type: o.type,
|
|
286
|
+
physicalId,
|
|
287
|
+
properties: inDeclaredVocabulary(props, liveProps(copy), d, canonicalObject(copy.statement, target.defaultDatabase)),
|
|
288
|
+
observedOn: other.host,
|
|
289
|
+
};
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
let l = canonicalObject(o.statement, target.defaultDatabase);
|
|
293
|
+
// The server prints a function's expression its own way (`k*x + b` as `((k * x) + b)`): its formatter says whether the two are one.
|
|
294
|
+
if (d.kind === "function" && d.lambda !== l.lambda && (await sameExpression(target, d.lambda ?? "", l.lambda ?? "")))
|
|
295
|
+
l = { ...l, lambda: d.lambda };
|
|
296
|
+
// A row policy's condition, the same way.
|
|
297
|
+
const using = d.access?.using;
|
|
298
|
+
if (d.kind === "rowPolicy" && using !== undefined && using !== l.access?.using && (await sameExpression(target, using, l.access?.using ?? ""))) {
|
|
299
|
+
l = { ...l, access: { ...l.access, using } };
|
|
300
|
+
}
|
|
301
|
+
resources[name] = {
|
|
302
|
+
type: o.type,
|
|
303
|
+
physicalId,
|
|
304
|
+
properties: inDeclaredVocabulary(props, liveProps(o), d, l),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
unobserved[name] = { type: entity.entityType, reason: "read-failed", detail: `the server's definition does not parse: ${err.message}` };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return deepObservation(resources, unobserved, pending);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The grants an apply would give or take from each declared grantee (#3733),
|
|
315
|
+
* as `chant sql plan` compares them: the declared grants against what
|
|
316
|
+
* `SHOW GRANTS FOR` prints, atom by atom. One change per grantee, its
|
|
317
|
+
* REVOKEs before its GRANTs as the applier sends them, with the grant
|
|
318
|
+
* declarations behind it. A grantee the server does not hold yet is given
|
|
319
|
+
* every declared grant.
|
|
320
|
+
*/
|
|
321
|
+
export async function pendingGrants(target, entities) {
|
|
322
|
+
const declared = grantsByGrantee([...entities].map(([name, e]) => ({ export: name, type: e.entityType, ddl: String(e.props.ddl ?? "") })));
|
|
323
|
+
if (declared.length === 0)
|
|
324
|
+
return [];
|
|
325
|
+
const live = await readLiveAccess(target, declared.map((g) => ({ kind: "grants", name: g.grantee })));
|
|
326
|
+
const held = new Map(live.filter((o) => o.type === CLICKHOUSE_ENTITY_TYPES.grant).map((o) => [o.name, grantsObject(o.statement, o.name, target.defaultDatabase).access ?? {}]));
|
|
327
|
+
const atom = (json) => JSON.parse(json);
|
|
328
|
+
const out = [];
|
|
329
|
+
for (const g of declared) {
|
|
330
|
+
const want = grantsObject(g.ddl, g.grantee, target.defaultDatabase).access ?? {};
|
|
331
|
+
const have = held.get(g.grantee) ?? {};
|
|
332
|
+
const statements = [
|
|
333
|
+
...Object.keys(have)
|
|
334
|
+
.filter((k) => !(k in want))
|
|
335
|
+
.map((k) => revokeStatement(atom(have[k]), g.grantee)),
|
|
336
|
+
...Object.keys(want)
|
|
337
|
+
.filter((k) => !(k in have))
|
|
338
|
+
.map((k) => grantStatement(atom(want[k]), g.grantee)),
|
|
339
|
+
];
|
|
340
|
+
if (statements.length === 0)
|
|
341
|
+
continue;
|
|
342
|
+
out.push({ subject: `grants ${g.grantee}`, change: statements.join("; "), entities: [...g.exports].sort() });
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|