@intentius/chant-lexicon-sql 0.124.31 → 0.124.33
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,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One ClickHouse statement rendered for the topology it runs on (#3645):
|
|
3
|
+
* a single node, a cluster (`ON CLUSTER`), a `Replicated` database, or
|
|
4
|
+
* ClickHouse Cloud.
|
|
5
|
+
*
|
|
6
|
+
* A migration source is written once. What differs between topologies is
|
|
7
|
+
* two things, and this module rewrites only those:
|
|
8
|
+
*
|
|
9
|
+
* - `ON CLUSTER`. A cluster's DDL carries it so every host runs the
|
|
10
|
+
* statement. A single node has no cluster to name. A `Replicated`
|
|
11
|
+
* database replicates its own DDL and refuses `ON CLUSTER` naming any
|
|
12
|
+
* other cluster ("ON CLUSTER is not allowed for Replicated database",
|
|
13
|
+
* code 80 on 26.8) unless `ignore_on_cluster_for_replicated_database` is
|
|
14
|
+
* set (default 0). Cloud's databases are Replicated, so the same holds
|
|
15
|
+
* there.
|
|
16
|
+
* - The table engine. A MergeTree-family table replicates its rows only as
|
|
17
|
+
* `Replicated*MergeTree`. On a cluster that engine names its Keeper path
|
|
18
|
+
* and replica; inside a `Replicated` database it takes no path arguments,
|
|
19
|
+
* since `database_replicated_allow_replicated_engine_arguments` defaults to
|
|
20
|
+
* 0 (refuse them) and the database supplies its own. A single node has no
|
|
21
|
+
* Keeper to replicate through, so it gets the plain family. Cloud converts
|
|
22
|
+
* every MergeTree and `Replicated*` engine to its `Shared*` equivalent
|
|
23
|
+
* itself ("You don't add a `Shared` or `Replicated` prefix yourself",
|
|
24
|
+
* https://clickhouse.com/docs/whats-new/cloud-compatibility), so it gets
|
|
25
|
+
* the plain family as well, with no path arguments to clash.
|
|
26
|
+
*
|
|
27
|
+
* Every function here is pure: SQL in, SQL out, nothing sent. Statements
|
|
28
|
+
* this module does not recognize (`INSERT`, `SELECT`, `SYSTEM`) come back
|
|
29
|
+
* unchanged, so a caller can pass every statement it sends.
|
|
30
|
+
*/
|
|
31
|
+
import { isTrivia, tokenizeText } from "./tokens.js";
|
|
32
|
+
import { parseCreate, unquote } from "./parser.js";
|
|
33
|
+
/**
|
|
34
|
+
* The Keeper path a cluster's `Replicated*MergeTree` gets: the server's own
|
|
35
|
+
* `default_replica_path`. `{uuid}` is the same on every host because the
|
|
36
|
+
* `CREATE` runs `ON CLUSTER`, and it does not change when the table is
|
|
37
|
+
* renamed or exchanged, so a rebuild's new table never shares the old one's
|
|
38
|
+
* path.
|
|
39
|
+
*/
|
|
40
|
+
export const DEFAULT_REPLICA_PATH = "/clickhouse/tables/{uuid}/{shard}";
|
|
41
|
+
/** The replica name a cluster's `Replicated*MergeTree` gets: the server's `default_replica_name`. */
|
|
42
|
+
export const DEFAULT_REPLICA_NAME = "{replica}";
|
|
43
|
+
/** The Keeper path a `Replicated` database is created with in the `replicated` topology. */
|
|
44
|
+
export const replicatedDatabasePath = (database) => `/clickhouse/databases/${database}`;
|
|
45
|
+
/**
|
|
46
|
+
* Read a topology from its string form, as a command-line flag or an
|
|
47
|
+
* environment variable gives it: `single`, `cluster:<name>`, `replicated`,
|
|
48
|
+
* `replicated:<cluster>`, `cloud`.
|
|
49
|
+
*/
|
|
50
|
+
export function parseTopology(value) {
|
|
51
|
+
const at = value.indexOf(":");
|
|
52
|
+
const kind = (at < 0 ? value : value.slice(0, at)).trim();
|
|
53
|
+
const arg = at < 0 ? undefined : value.slice(at + 1).trim();
|
|
54
|
+
switch (kind) {
|
|
55
|
+
case "single":
|
|
56
|
+
case "cloud":
|
|
57
|
+
if (arg !== undefined)
|
|
58
|
+
throw new Error(`topology ${kind} takes no argument: ${JSON.stringify(value)}`);
|
|
59
|
+
return { kind };
|
|
60
|
+
case "cluster":
|
|
61
|
+
if (!arg)
|
|
62
|
+
throw new Error(`topology cluster needs the cluster's name: cluster:<name>, got ${JSON.stringify(value)}`);
|
|
63
|
+
return { kind, cluster: arg };
|
|
64
|
+
case "replicated":
|
|
65
|
+
return arg ? { kind, cluster: arg } : { kind };
|
|
66
|
+
default:
|
|
67
|
+
throw new Error(`unknown topology ${JSON.stringify(value)}: expected single, cluster:<name>, replicated or cloud`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const FAMILY = /^(Replicated)?(\w*MergeTree)$/;
|
|
71
|
+
const isStringLiteral = (arg) => arg !== undefined && /^'(?:[^'\\]|\\.)*'$/s.test(arg.trim());
|
|
72
|
+
const sqlString = (value) => `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
73
|
+
/**
|
|
74
|
+
* A MergeTree-family engine split into its family and its own arguments,
|
|
75
|
+
* with the Keeper path and replica name a `Replicated*` engine names taken
|
|
76
|
+
* apart. Undefined for any other engine (`Log`, `Distributed`) and for a
|
|
77
|
+
* `Shared*` one, which only Cloud writes.
|
|
78
|
+
*
|
|
79
|
+
* As ClickHouse reads it, a `Replicated*MergeTree` names a path and replica
|
|
80
|
+
* when its first argument is a string literal; otherwise every argument is
|
|
81
|
+
* the family's own (`ReplicatedReplacingMergeTree(ver)`).
|
|
82
|
+
*/
|
|
83
|
+
function mergeTreeFamily(engine) {
|
|
84
|
+
const m = FAMILY.exec(engine.name);
|
|
85
|
+
if (!m || m[2].startsWith("Shared"))
|
|
86
|
+
return undefined;
|
|
87
|
+
const args = (engine.args ?? []).map((a) => a.trim());
|
|
88
|
+
if (m[1] && isStringLiteral(args[0]) && isStringLiteral(args[1])) {
|
|
89
|
+
return { family: m[2], replicated: true, replica: [args[0], args[1]], args: args.slice(2) };
|
|
90
|
+
}
|
|
91
|
+
return { family: m[2], replicated: m[1] !== undefined, args };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The engine a table gets on a topology. A MergeTree-family engine becomes
|
|
95
|
+
* `Replicated*` on a cluster (with the path and replica it declared, or the
|
|
96
|
+
* topology's) and in a Replicated database (with none); the plain family on
|
|
97
|
+
* a single node and on Cloud. Any other engine is returned as declared.
|
|
98
|
+
*/
|
|
99
|
+
export function renderEngine(engine, topology) {
|
|
100
|
+
const f = mergeTreeFamily(engine);
|
|
101
|
+
if (!f)
|
|
102
|
+
return engine;
|
|
103
|
+
const parens = engine.args !== undefined;
|
|
104
|
+
const withArgs = (name, args) => (args.length > 0 || parens ? { name, args } : { name });
|
|
105
|
+
switch (topology.kind) {
|
|
106
|
+
case "single":
|
|
107
|
+
case "cloud":
|
|
108
|
+
return withArgs(f.family, f.args);
|
|
109
|
+
case "replicated":
|
|
110
|
+
return withArgs(`Replicated${f.family}`, f.args);
|
|
111
|
+
case "cluster": {
|
|
112
|
+
const replica = f.replica ?? [sqlString(topology.replicaPath ?? DEFAULT_REPLICA_PATH), sqlString(topology.replicaName ?? DEFAULT_REPLICA_NAME)];
|
|
113
|
+
return { name: `Replicated${f.family}`, args: [...replica, ...f.args] };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const engineText = (e) => (e.args === undefined ? e.name : `${e.name}(${e.args.join(", ")})`);
|
|
118
|
+
// ── statements ────────────────────────────────────────────────────────
|
|
119
|
+
/** `ON CLUSTER` and the name after it: a backquoted identifier, or a string literal for a macro (`'{cluster}'`). */
|
|
120
|
+
export function onClusterClause(cluster) {
|
|
121
|
+
return `ON CLUSTER ${/[{}]/.test(cluster) ? sqlString(cluster) : `\`${cluster.replace(/`/g, "``")}\``}`;
|
|
122
|
+
}
|
|
123
|
+
function applyEdits(tokens, edits) {
|
|
124
|
+
const sorted = [...edits].sort((a, b) => a.from - b.from);
|
|
125
|
+
let out = "";
|
|
126
|
+
let at = 0;
|
|
127
|
+
for (const e of sorted) {
|
|
128
|
+
out += tokens.slice(at, e.from).map((t) => t.text).join("") + e.text;
|
|
129
|
+
at = e.to;
|
|
130
|
+
}
|
|
131
|
+
return out + tokens.slice(at).map((t) => t.text).join("");
|
|
132
|
+
}
|
|
133
|
+
const word = (t, ...words) => t !== undefined && t.kind === "ident" && words.includes(t.text.toUpperCase());
|
|
134
|
+
const isName = (t) => t !== undefined && (t.kind === "ident" || t.kind === "qident" || t.kind === "number" || t.kind === "ref");
|
|
135
|
+
/** A walker over the significant tokens, which knows each one's index in the full list. */
|
|
136
|
+
class Walk {
|
|
137
|
+
constructor(tokens) {
|
|
138
|
+
this.tokens = tokens;
|
|
139
|
+
this.p = 0;
|
|
140
|
+
this.sig = tokens.flatMap((t, i) => (isTrivia(t) ? [] : [i]));
|
|
141
|
+
}
|
|
142
|
+
peek(n = 0) {
|
|
143
|
+
const i = this.sig[this.p + n];
|
|
144
|
+
return i === undefined ? undefined : this.tokens[i];
|
|
145
|
+
}
|
|
146
|
+
/** Index in `tokens` of the significant token `n` ahead, or the end. */
|
|
147
|
+
index(n = 0) {
|
|
148
|
+
return this.sig[this.p + n] ?? this.tokens.length;
|
|
149
|
+
}
|
|
150
|
+
/** Index in `tokens` just past the last significant token consumed. */
|
|
151
|
+
end() {
|
|
152
|
+
return this.p === 0 ? 0 : this.sig[this.p - 1] + 1;
|
|
153
|
+
}
|
|
154
|
+
accept(...words) {
|
|
155
|
+
if (!word(this.peek(), ...words))
|
|
156
|
+
return false;
|
|
157
|
+
this.p++;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
acceptSeq(...words) {
|
|
161
|
+
if (!words.every((w, i) => word(this.peek(i), w)))
|
|
162
|
+
return false;
|
|
163
|
+
this.p += words.length;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
/** `name` or `db.name`. False when no name is here. */
|
|
167
|
+
name() {
|
|
168
|
+
if (!isName(this.peek()))
|
|
169
|
+
return false;
|
|
170
|
+
this.p++;
|
|
171
|
+
while (this.peek()?.text === "." && isName(this.peek(1)))
|
|
172
|
+
this.p += 2;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
/** The significant token index of the last top-level `SETTINGS`, outside any parentheses, or undefined. */
|
|
176
|
+
topLevelSettings() {
|
|
177
|
+
let depth = 0;
|
|
178
|
+
let found;
|
|
179
|
+
for (let k = this.p; k < this.sig.length; k++) {
|
|
180
|
+
const t = this.tokens[this.sig[k]];
|
|
181
|
+
if (t.text === "(")
|
|
182
|
+
depth++;
|
|
183
|
+
else if (t.text === ")")
|
|
184
|
+
depth--;
|
|
185
|
+
else if (depth === 0 && word(t, "SETTINGS"))
|
|
186
|
+
found = k;
|
|
187
|
+
}
|
|
188
|
+
return found;
|
|
189
|
+
}
|
|
190
|
+
/** Index in `tokens` of the significant token `k` (absolute), or the end of the statement. */
|
|
191
|
+
at(k) {
|
|
192
|
+
return this.sig[k] ?? this.tokens.length;
|
|
193
|
+
}
|
|
194
|
+
/** Index in `tokens` past the last significant token, ignoring a trailing `;`. */
|
|
195
|
+
bodyEnd() {
|
|
196
|
+
let k = this.sig.length - 1;
|
|
197
|
+
if (k >= 0 && this.tokens[this.sig[k]].text === ";")
|
|
198
|
+
k--;
|
|
199
|
+
return k < 0 ? 0 : this.sig[k] + 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The edit that makes the `ON CLUSTER` at the walker's position what the
|
|
204
|
+
* topology wants: replaced with the topology's cluster, removed, or (when
|
|
205
|
+
* there is none and `cluster` is given) inserted at `insertAt`.
|
|
206
|
+
*/
|
|
207
|
+
function onClusterEdit(w, cluster, insertAt) {
|
|
208
|
+
if (word(w.peek(), "ON") && word(w.peek(1), "CLUSTER")) {
|
|
209
|
+
// Back to the end of the token before ON, so removing it takes its leading space too.
|
|
210
|
+
const from = w.end();
|
|
211
|
+
w.p += 2;
|
|
212
|
+
if (!w.name() && w.peek()?.kind === "string")
|
|
213
|
+
w.p++;
|
|
214
|
+
const to = w.end();
|
|
215
|
+
return { from, to, text: cluster === undefined ? "" : ` ${onClusterClause(cluster)}` };
|
|
216
|
+
}
|
|
217
|
+
return cluster === undefined ? undefined : { from: insertAt, to: insertAt, text: ` ${onClusterClause(cluster)}` };
|
|
218
|
+
}
|
|
219
|
+
/** The cluster a statement on a table or view carries on this topology, if any. */
|
|
220
|
+
const objectCluster = (topology) => (topology.kind === "cluster" ? topology.cluster : undefined);
|
|
221
|
+
/**
|
|
222
|
+
* The cluster a statement on a database itself carries on this topology, if
|
|
223
|
+
* any. In the replicated topology only creating and dropping the database
|
|
224
|
+
* reach every replica this way; an `ALTER` or `RENAME` of a Replicated
|
|
225
|
+
* database is replicated by the database.
|
|
226
|
+
*/
|
|
227
|
+
const databaseCluster = (topology, createOrDrop = false) => topology.kind === "cluster" ? topology.cluster : topology.kind === "replicated" && createOrDrop ? topology.cluster : undefined;
|
|
228
|
+
const text = (tokens, span) => tokens
|
|
229
|
+
.slice(span.from, span.to)
|
|
230
|
+
.map((t) => t.text)
|
|
231
|
+
.join("")
|
|
232
|
+
.trim();
|
|
233
|
+
/** The `ON CLUSTER` edit for a parsed `CREATE`: after the name and any `UUID '...'`. */
|
|
234
|
+
function createClusterEdit(tokens, node, cluster) {
|
|
235
|
+
if (node.onCluster) {
|
|
236
|
+
let on = node.onCluster.from - 1;
|
|
237
|
+
// Back over the trivia and the CLUSTER and ON keywords, to the end of what came before.
|
|
238
|
+
let seen = 0;
|
|
239
|
+
while (on >= 0 && seen < 2) {
|
|
240
|
+
if (!isTrivia(tokens[on]))
|
|
241
|
+
seen++;
|
|
242
|
+
on--;
|
|
243
|
+
}
|
|
244
|
+
while (on >= 0 && isTrivia(tokens[on]))
|
|
245
|
+
on--;
|
|
246
|
+
return { from: on + 1, to: node.onCluster.to, text: cluster === undefined ? "" : ` ${onClusterClause(cluster)}` };
|
|
247
|
+
}
|
|
248
|
+
if (cluster === undefined)
|
|
249
|
+
return undefined;
|
|
250
|
+
const at = afterName(tokens, node);
|
|
251
|
+
return { from: at, to: at, text: ` ${onClusterClause(cluster)}` };
|
|
252
|
+
}
|
|
253
|
+
/** Index in `tokens` past a `CREATE`'s name and any `UUID '...'` after it: where `ON CLUSTER` goes. */
|
|
254
|
+
function afterName(tokens, node) {
|
|
255
|
+
const w = new Walk(tokens.slice(node.name.to));
|
|
256
|
+
if (w.accept("UUID") && w.peek()?.kind === "string") {
|
|
257
|
+
w.p++;
|
|
258
|
+
return node.name.to + w.end();
|
|
259
|
+
}
|
|
260
|
+
return node.name.to;
|
|
261
|
+
}
|
|
262
|
+
function renderCreate(tokens, node, topology) {
|
|
263
|
+
const edits = [];
|
|
264
|
+
if (node.statement === "database") {
|
|
265
|
+
const cluster = createClusterEdit(tokens, node, databaseCluster(topology, true));
|
|
266
|
+
if (cluster)
|
|
267
|
+
edits.push(cluster);
|
|
268
|
+
const declared = node.engine?.name;
|
|
269
|
+
if (topology.kind === "replicated") {
|
|
270
|
+
if (declared !== "Replicated") {
|
|
271
|
+
const name = unquote(text(tokens, node.name));
|
|
272
|
+
const clause = `ENGINE = Replicated(${sqlString(replicatedDatabasePath(name))}, '{shard}', '{replica}')`;
|
|
273
|
+
if (node.engine)
|
|
274
|
+
edits.push({ from: node.engine.span.from, to: node.engine.span.to, text: clause });
|
|
275
|
+
else {
|
|
276
|
+
// After ON CLUSTER as declared, or where an inserted one goes; edits at one index apply in the order pushed.
|
|
277
|
+
const at = node.onCluster ? node.onCluster.to : afterName(tokens, node);
|
|
278
|
+
edits.push({ from: at, to: at, text: ` ${clause}` });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else if (declared === "Replicated" && node.engine) {
|
|
283
|
+
// Only the replicated topology runs a Replicated database; elsewhere the server's default (Atomic, or Cloud's own).
|
|
284
|
+
let from = node.engine.span.from;
|
|
285
|
+
while (from > 0 && isTrivia(tokens[from - 1]))
|
|
286
|
+
from--;
|
|
287
|
+
edits.push({ from, to: node.engine.span.to, text: "" });
|
|
288
|
+
}
|
|
289
|
+
return applyEdits(tokens, edits);
|
|
290
|
+
}
|
|
291
|
+
// A function belongs to no database, so in the replicated topology it reaches every replica only through the cluster.
|
|
292
|
+
const cluster = createClusterEdit(tokens, node, node.statement === "function" ? databaseCluster(topology, true) : objectCluster(topology));
|
|
293
|
+
if (cluster)
|
|
294
|
+
edits.push(cluster);
|
|
295
|
+
if ((node.statement === "table" || node.statement === "view") && node.engine) {
|
|
296
|
+
const declared = { name: node.engine.name, ...(node.engine.args ? { args: node.engine.args.map((a) => text(tokens, a)) } : {}) };
|
|
297
|
+
const rendered = renderEngine(declared, topology);
|
|
298
|
+
if (engineText(rendered) !== engineText(declared)) {
|
|
299
|
+
// From the engine's name to the end of its argument list; `ENGINE =` stays as written.
|
|
300
|
+
edits.push({ from: node.engine.nameSpan.from, to: node.engine.span.to, text: engineText(rendered) });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return applyEdits(tokens, edits);
|
|
304
|
+
}
|
|
305
|
+
/** A `CREATE` the parser does not take (`CREATE TABLE ... AS`, a dictionary): `ON CLUSTER` after the name, the rest as written. */
|
|
306
|
+
function renderOtherCreate(tokens, topology) {
|
|
307
|
+
const w = new Walk(tokens);
|
|
308
|
+
w.accept("CREATE");
|
|
309
|
+
if (w.accept("OR"))
|
|
310
|
+
w.accept("REPLACE");
|
|
311
|
+
w.accept("TEMPORARY");
|
|
312
|
+
// Access control (`./access.ts`) belongs to no database: like a database's own DDL, it reaches every replica through the cluster.
|
|
313
|
+
if (w.accept("USER", "ROLE", "POLICY", "QUOTA", "PROFILE") || w.acceptSeq("ROW", "POLICY") || w.acceptSeq("SETTINGS", "PROFILE")) {
|
|
314
|
+
w.acceptSeq("IF", "NOT", "EXISTS") || w.acceptSeq("OR", "REPLACE");
|
|
315
|
+
if (!w.name())
|
|
316
|
+
return applyEdits(tokens, []);
|
|
317
|
+
const edit = onClusterEdit(w, databaseCluster(topology, true), w.end());
|
|
318
|
+
return applyEdits(tokens, edit ? [edit] : []);
|
|
319
|
+
}
|
|
320
|
+
let database = false;
|
|
321
|
+
if (w.accept("DATABASE"))
|
|
322
|
+
database = true;
|
|
323
|
+
else if (w.accept("MATERIALIZED", "LIVE", "WINDOW"))
|
|
324
|
+
w.accept("VIEW");
|
|
325
|
+
else if (!w.accept("TABLE", "VIEW", "DICTIONARY"))
|
|
326
|
+
return applyEdits(tokens, []);
|
|
327
|
+
w.acceptSeq("IF", "NOT", "EXISTS");
|
|
328
|
+
if (!w.name())
|
|
329
|
+
return applyEdits(tokens, []);
|
|
330
|
+
let at = w.end();
|
|
331
|
+
if (w.accept("UUID") && w.peek()?.kind === "string") {
|
|
332
|
+
w.p++;
|
|
333
|
+
at = w.end();
|
|
334
|
+
}
|
|
335
|
+
const edit = onClusterEdit(w, database ? databaseCluster(topology) : objectCluster(topology), at);
|
|
336
|
+
return applyEdits(tokens, edit ? [edit] : []);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The `ON CLUSTER` for a statement whose clause follows the object's name:
|
|
340
|
+
* `ALTER`, `DROP`, `TRUNCATE`, `OPTIMIZE`, `DETACH`, `ATTACH`.
|
|
341
|
+
*/
|
|
342
|
+
function renderAfterName(tokens, w, cluster) {
|
|
343
|
+
w.acceptSeq("IF", "NOT", "EXISTS") || w.acceptSeq("IF", "EXISTS");
|
|
344
|
+
if (!w.name())
|
|
345
|
+
return applyEdits(tokens, []);
|
|
346
|
+
const edit = onClusterEdit(w, cluster, w.end());
|
|
347
|
+
return applyEdits(tokens, edit ? [edit] : []);
|
|
348
|
+
}
|
|
349
|
+
/** `RENAME ... TO ...` and `EXCHANGE ... AND ...`: `ON CLUSTER` at the end, before any `SETTINGS`. */
|
|
350
|
+
function renderAtEnd(tokens, w, cluster) {
|
|
351
|
+
// An `ON CLUSTER` already written, after the renames.
|
|
352
|
+
for (let n = 0; w.peek(n) !== undefined; n++) {
|
|
353
|
+
if (word(w.peek(n), "ON") && word(w.peek(n + 1), "CLUSTER")) {
|
|
354
|
+
w.p += n;
|
|
355
|
+
return applyEdits(tokens, [onClusterEdit(w, cluster, 0)]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (cluster === undefined)
|
|
359
|
+
return applyEdits(tokens, []);
|
|
360
|
+
const settings = w.topLevelSettings();
|
|
361
|
+
let at;
|
|
362
|
+
if (settings !== undefined) {
|
|
363
|
+
at = w.at(settings);
|
|
364
|
+
while (at > 0 && isTrivia(tokens[at - 1]))
|
|
365
|
+
at--;
|
|
366
|
+
}
|
|
367
|
+
else
|
|
368
|
+
at = w.bodyEnd();
|
|
369
|
+
return applyEdits(tokens, [{ from: at, to: at, text: ` ${onClusterClause(cluster)}` }]);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* One statement as it runs on `topology`: `ON CLUSTER` added, replaced or
|
|
373
|
+
* removed, and in a `CREATE`, the engine the topology needs. Comments,
|
|
374
|
+
* spacing and everything else in the statement are kept as written.
|
|
375
|
+
* A statement that is not DDL on a database, table or view is returned
|
|
376
|
+
* unchanged, as is `KILL QUERY`/`KILL MUTATION` everywhere but a cluster,
|
|
377
|
+
* where it gains `ON CLUSTER` (inside a Replicated database
|
|
378
|
+
* `KILL QUERY ON CLUSTER '<db>'` names the database's own cluster and is kept).
|
|
379
|
+
*/
|
|
380
|
+
export function renderStatement(sql, topology) {
|
|
381
|
+
const tokens = tokenizeText(sql, 0);
|
|
382
|
+
const w = new Walk(tokens);
|
|
383
|
+
const head = w.peek();
|
|
384
|
+
if (word(head, "CREATE")) {
|
|
385
|
+
let node;
|
|
386
|
+
try {
|
|
387
|
+
node = parseCreate(tokens);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
node = undefined;
|
|
391
|
+
}
|
|
392
|
+
return node ? renderCreate(tokens, node, topology) : renderOtherCreate(tokens, topology);
|
|
393
|
+
}
|
|
394
|
+
if (word(head, "ALTER", "DROP", "DETACH", "ATTACH", "TRUNCATE", "OPTIMIZE", "UNDROP")) {
|
|
395
|
+
w.p++;
|
|
396
|
+
if (word(head, "DROP", "DETACH", "ATTACH"))
|
|
397
|
+
w.accept("TEMPORARY");
|
|
398
|
+
if (w.accept("DATABASE"))
|
|
399
|
+
return renderAfterName(tokens, w, databaseCluster(topology, word(head, "DROP")));
|
|
400
|
+
// A function and access control belong to no database: like the database's own DDL, they reach every replica through the cluster.
|
|
401
|
+
if (w.accept("FUNCTION", "USER", "ROLE", "POLICY", "QUOTA", "PROFILE") || w.acceptSeq("ROW", "POLICY") || w.acceptSeq("SETTINGS", "PROFILE")) {
|
|
402
|
+
return renderAfterName(tokens, w, databaseCluster(topology, true));
|
|
403
|
+
}
|
|
404
|
+
// `TRUNCATE t` may leave out TABLE.
|
|
405
|
+
if (w.accept("TABLE", "VIEW", "DICTIONARY") || word(head, "TRUNCATE"))
|
|
406
|
+
return renderAfterName(tokens, w, objectCluster(topology));
|
|
407
|
+
return sql;
|
|
408
|
+
}
|
|
409
|
+
if (word(head, "GRANT", "REVOKE")) {
|
|
410
|
+
w.p++;
|
|
411
|
+
const edit = onClusterEdit(w, databaseCluster(topology, true), w.end());
|
|
412
|
+
return applyEdits(tokens, edit ? [edit] : []);
|
|
413
|
+
}
|
|
414
|
+
if (word(head, "RENAME")) {
|
|
415
|
+
w.p++;
|
|
416
|
+
if (w.accept("DATABASE"))
|
|
417
|
+
return renderAtEnd(tokens, w, databaseCluster(topology));
|
|
418
|
+
if (w.accept("TABLE", "DICTIONARY"))
|
|
419
|
+
return renderAtEnd(tokens, w, objectCluster(topology));
|
|
420
|
+
return sql;
|
|
421
|
+
}
|
|
422
|
+
if (word(head, "EXCHANGE")) {
|
|
423
|
+
w.p++;
|
|
424
|
+
if (w.accept("TABLES", "DICTIONARIES"))
|
|
425
|
+
return renderAtEnd(tokens, w, objectCluster(topology));
|
|
426
|
+
return sql;
|
|
427
|
+
}
|
|
428
|
+
if (word(head, "KILL") && topology.kind === "cluster") {
|
|
429
|
+
w.p++;
|
|
430
|
+
if (!w.accept("QUERY", "MUTATION"))
|
|
431
|
+
return sql;
|
|
432
|
+
const edit = onClusterEdit(w, topology.cluster, w.end());
|
|
433
|
+
return applyEdits(tokens, edit ? [edit] : []);
|
|
434
|
+
}
|
|
435
|
+
return sql;
|
|
436
|
+
}
|
|
437
|
+
/** Each step's statement rendered for `topology`; every other field of a step is kept. */
|
|
438
|
+
export function renderSteps(steps, topology) {
|
|
439
|
+
return steps.map((s) => ({ ...s, sql: renderStatement(s.sql, topology) }));
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* A statement for a topology that may not be configured: with none, the
|
|
443
|
+
* statement as the source wrote it. The applier, the plan and the rebuild
|
|
444
|
+
* migration render only when an environment names its topology
|
|
445
|
+
* (`sql.profiles.<env>.topology`, `CLICKHOUSE_TOPOLOGY`), so a project that
|
|
446
|
+
* names none sends what it declared, as before.
|
|
447
|
+
*/
|
|
448
|
+
export const renderFor = (sql, topology) => (topology ? renderStatement(sql, topology) : sql);
|
|
449
|
+
/** A topology from its string form or its object form (`sql.profiles.<env>.topology`). */
|
|
450
|
+
export function toTopology(value) {
|
|
451
|
+
return typeof value === "string" ? parseTopology(value) : value;
|
|
452
|
+
}
|
|
453
|
+
/** The topology's string form, as `parseTopology` reads it. Replica path options are not part of it. */
|
|
454
|
+
export function topologyLabel(topology) {
|
|
455
|
+
switch (topology.kind) {
|
|
456
|
+
case "cluster":
|
|
457
|
+
return `cluster:${topology.cluster}`;
|
|
458
|
+
case "replicated":
|
|
459
|
+
return topology.cluster ? `replicated:${topology.cluster}` : "replicated";
|
|
460
|
+
default:
|
|
461
|
+
return topology.kind;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A column type, read into its family and parameters: `Nullable(String)` is
|
|
3
|
+
* the family `Nullable` with the one parameter `String`;
|
|
4
|
+
* `DateTime64(3, 'UTC')` is `DateTime64` with `3` and `'UTC'`.
|
|
5
|
+
*
|
|
6
|
+
* The reading is lexical, over the same tokenizer the parser uses, so a
|
|
7
|
+
* string literal holding a parenthesis (`Enum8('a(' = 1)`) or a comma does not
|
|
8
|
+
* split a parameter. What a parameter means (a type, a number, a name) is the
|
|
9
|
+
* caller's question, answered from `TYPE_PARAMETERS`.
|
|
10
|
+
*/
|
|
11
|
+
import { isTrivia, tokenizeText } from "./tokens.js";
|
|
12
|
+
const textOf = (tokens) => tokens.map((t) => t.text).join("").trim();
|
|
13
|
+
/** The significant tokens of a type, or undefined when it does not tokenize. */
|
|
14
|
+
export function typeTokens(text) {
|
|
15
|
+
try {
|
|
16
|
+
return tokenizeText(text, 0);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read a type into its leading words and its parameters. Undefined when the
|
|
24
|
+
* text is not a type this can read: no leading name, or unbalanced parentheses.
|
|
25
|
+
* Anything after the closing parenthesis (`COLLATE utf8`) is ignored.
|
|
26
|
+
*/
|
|
27
|
+
export function readType(tokens) {
|
|
28
|
+
const sig = tokens.filter((t) => !isTrivia(t));
|
|
29
|
+
const words = [];
|
|
30
|
+
let i = 0;
|
|
31
|
+
// `String COLLATE utf8`: COLLATE ends the type's words.
|
|
32
|
+
while (i < sig.length && (sig[i].kind === "ident" || sig[i].kind === "qident") && sig[i].text.toUpperCase() !== "COLLATE")
|
|
33
|
+
words.push(sig[i++]);
|
|
34
|
+
if (words.length === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (i >= sig.length || !(sig[i].kind === "punct" && sig[i].text === "("))
|
|
37
|
+
return { words };
|
|
38
|
+
// Parameters: the tokens up to the matching parenthesis, split at top-level commas.
|
|
39
|
+
const start = tokens.indexOf(sig[i]);
|
|
40
|
+
const params = [];
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let current = [];
|
|
43
|
+
for (let j = start; j < tokens.length; j++) {
|
|
44
|
+
const t = tokens[j];
|
|
45
|
+
if (t.kind === "punct" && t.text === "(") {
|
|
46
|
+
if (depth++ === 0)
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
else if (t.kind === "punct" && t.text === ")") {
|
|
50
|
+
if (--depth === 0) {
|
|
51
|
+
if (params.length > 0 || current.some((x) => !isTrivia(x)))
|
|
52
|
+
params.push({ text: textOf(current), tokens: current });
|
|
53
|
+
return { words, params };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (depth === 1 && t.kind === "punct" && t.text === ",") {
|
|
57
|
+
params.push({ text: textOf(current), tokens: current });
|
|
58
|
+
current = [];
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
current.push(t);
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/** A parameter that is a numeric literal, possibly negative: its value. */
|
|
66
|
+
export function numericParam(param) {
|
|
67
|
+
const sig = param.tokens.filter((t) => !isTrivia(t));
|
|
68
|
+
const negative = sig.length === 2 && sig[0].kind === "op" && sig[0].text === "-";
|
|
69
|
+
const n = negative ? sig[1] : sig.length === 1 ? sig[0] : undefined;
|
|
70
|
+
if (!n || n.kind !== "number" || !/^\d+(\.\d+)?([eE][+-]?\d+)?$/.test(n.text))
|
|
71
|
+
return undefined;
|
|
72
|
+
return negative ? -Number(n.text) : Number(n.text);
|
|
73
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@intentius/chant-lexicon-sql/clickhouse`: the ClickHouse dialect.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is spec-true to ClickHouse at the pinned server release
|
|
5
|
+
* (`CLICKHOUSE_VERSION`), not a database-agnostic model: the engine names,
|
|
6
|
+
* column type families, codecs, skip index types and settings are the pinned
|
|
7
|
+
* server's own, read from its `system.*` tables, and the argument grammar the
|
|
8
|
+
* catalog does not carry comes from the overlays beside them.
|
|
9
|
+
*/
|
|
10
|
+
export { database, table, view, dictionary, func, user, role, policy, grant, quota, settingsProfile, literal, SqlLiteral, SqlTemplateError, ClickHouseObject, CLICKHOUSE_ENTITY_TYPES, isClickHouseObject, isColumnRef, quoteIdentifier, } from "./clickhouse/entities.js";
|
|
11
|
+
export * from "./generated/clickhouse.js";
|
|
12
|
+
export * from "./composites/clickhouse.js";
|
|
13
|
+
export { ENGINE_ARGUMENTS } from "./clickhouse/overlays/engines.js";
|
|
14
|
+
export { SKIP_INDEX_PARAMETERS } from "./clickhouse/overlays/skip-indexes.js";
|
|
15
|
+
export { CODEC_OVERLAY } from "./clickhouse/overlays/codecs.js";
|
|
16
|
+
export { TYPE_PARAMETERS, WRAPPER_TYPES } from "./clickhouse/overlays/types.js";
|
|
17
|
+
export { TTL_ACTIONS, COLUMN_TTL_ACTIONS, TTL_RESULT_TYPES, TTL_SETTINGS, } from "./clickhouse/overlays/ttl.js";
|
|
18
|
+
export { PROJECTION_FORMS, PROJECTION_SETTINGS, PROJECTION_FORBIDDEN_CLAUSES, } from "./clickhouse/overlays/projections.js";
|
|
19
|
+
export { CLICKHOUSE_IMAGE_DIGEST, CLICKHOUSE_IMAGE_REPOSITORY, clickhouseImage } from "./spec/pin.js";
|
|
20
|
+
export { ClickHouseRebuildOp, } from "./clickhouse/rebuild/op.js";
|
|
21
|
+
export { RECEIPTS_DATABASE, RECEIPTS_TABLE } from "./clickhouse/rebuild/receipts.js";
|
|
22
|
+
export { DEFAULT_REPLICA_NAME, DEFAULT_REPLICA_PATH, onClusterClause, parseTopology, renderEngine, renderFor, renderStatement, renderSteps, replicatedDatabasePath, topologyLabel, toTopology, } from "./clickhouse/topology.js";
|
|
23
|
+
// The build's ClickHouse objects as typed values, for a project's own
|
|
24
|
+
// `lint.policies` (#3870); also at `@intentius/chant-lexicon-sql/read`.
|
|
25
|
+
export { readClickHouse, } from "./read/clickhouse.js";
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `docs/pages/change-classifier.mdx`, written from the classifier's own rule
|
|
3
|
+
* table by `npm run docs`, so the page cannot disagree with the rules.
|
|
4
|
+
*/
|
|
5
|
+
import { CLASSIFIER_RULES } from "../clickhouse/plan/rules.js";
|
|
6
|
+
const LABEL = {
|
|
7
|
+
create: "create",
|
|
8
|
+
drop: "drop",
|
|
9
|
+
metadata: "metadata only",
|
|
10
|
+
rewrite: "background rewrite",
|
|
11
|
+
rebuild: "rebuild",
|
|
12
|
+
};
|
|
13
|
+
const cell = (s) => s.replace(/\|/g, "\\|");
|
|
14
|
+
export function renderClassifierPage() {
|
|
15
|
+
const rows = Object.values(CLASSIFIER_RULES).map((r) => `| ${r.id} | ${LABEL[r.class]} | ${cell(r.title)} | ${cell(r.restriction)} [ClickHouse docs](${r.cite}) |`);
|
|
16
|
+
return `---
|
|
17
|
+
title: "Planning ClickHouse Changes"
|
|
18
|
+
label: "Planning and the Change Classifier"
|
|
19
|
+
description: "How a ClickHouse schema change is classified as metadata only, a background rewrite or a rebuild, each with the ALTER restriction behind it"
|
|
20
|
+
diataxis: reference
|
|
21
|
+
group: "ClickHouse"
|
|
22
|
+
order: 1
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
{/* Written by src/codegen/classifier-page.ts from src/clickhouse/plan/rules.ts. Edit the rules, then run npm run docs. */}
|
|
26
|
+
|
|
27
|
+
Every change between two ClickHouse schemas is classified before anything runs:
|
|
28
|
+
|
|
29
|
+
| Class | Means |
|
|
30
|
+
|---|---|
|
|
31
|
+
| metadata only | the server records the change and touches no existing data |
|
|
32
|
+
| background rewrite | a mutation rewrites existing parts in the background, with no rollback |
|
|
33
|
+
| rebuild | ClickHouse cannot make the change to the existing table, and the data has to be copied into a new one; a plan refuses to make it in place |
|
|
34
|
+
|
|
35
|
+
Two commands report the classification, and \`chant lifecycle plan\` reports it as each update's disruption (metadata only is \`in-place\`, a rewrite \`rolling\`, a rebuild \`replace\`):
|
|
36
|
+
|
|
37
|
+
\`\`\`bash
|
|
38
|
+
chant sql diff base.json head.json # two chant build outputs, offline
|
|
39
|
+
chant sql plan prod schema.json # a build output against the prod server
|
|
40
|
+
\`\`\`
|
|
41
|
+
|
|
42
|
+
Both exit 2 when a change needs a rebuild. \`chant sql diff\` compares a pull request's base and head builds with no server. \`chant sql plan\` reads the server \`sql.profiles.<env>\` binds, and asks that server's formatter about any expression the normalization rules leave different, so the server's own rewriting (\`INTERVAL 1 DAY\` as \`toIntervalDay(1)\`, \`a+b*2\` as \`a + (b * 2)\`) is not reported as a change.
|
|
43
|
+
|
|
44
|
+
\`chant sql plan\` also exits 2 when a declared database holds an object whose definition it cannot read. It names each one under \`Refused\` (\`unreadable\` in \`--json\`) instead of leaving it out. Dictionaries are read and planned like the other objects: any change but a comment replaces one (SQLCH245).
|
|
45
|
+
|
|
46
|
+
A clause chant's parser does not know ([Plain .sql Files](../sql-files/#clauses-chant-does-not-read)) is compared as text, and the rest of its object is planned as usual. A change to one is SQLCH290, refused for review with no rebuild Op suggested, since chant cannot tell what the change takes. A clause only the server prints is a hint, not a change: a newer server printing a clause with its default looks the same as a clause removed from the declaration.
|
|
47
|
+
|
|
48
|
+
## After the plan
|
|
49
|
+
|
|
50
|
+
The plan only reports. [\`clickhouseApply\`](../applying/) makes the metadata-only and background-rewrite changes, and refuses a rebuild the same way the plan does, sending nothing for that object. A rebuild runs as \`ClickHouseRebuildOp\`, a gated migration Op that creates the new table, backfills it, verifies it and swaps it in: see [Rebuilding a Table](../rebuild/). Both commands print the Op declaration to start from for each refused table.
|
|
51
|
+
|
|
52
|
+
\`chant migrate\` plays no part in any of this. It translates a file from one lexicon's format into another's, such as a GitHub Actions workflow into GitLab CI, and does not run schema migrations.
|
|
53
|
+
|
|
54
|
+
## Statements for a migration file
|
|
55
|
+
|
|
56
|
+
\`chant sql diff base.json head.json --statements\` prints the statements that take the base schema to the head one, offline: each one the applier would send for the same change, after a comment naming its object, rule and class. \`--json\` prints them as one document, which \`diffStatements(before, after)\` from \`@intentius/chant-lexicon-sql\` also returns:
|
|
57
|
+
|
|
58
|
+
\`\`\`sql
|
|
59
|
+
-- events (analytics.events): SQLCH201 metadata
|
|
60
|
+
ALTER TABLE \`analytics\`.\`events\` ADD COLUMN region String DEFAULT 'eu' AFTER \`user_id\`;
|
|
61
|
+
|
|
62
|
+
-- events (analytics.events): SQLCH210 rewrite, waits for its mutation
|
|
63
|
+
ALTER TABLE \`analytics\`.\`events\` MODIFY COLUMN \`kind\` LowCardinality(String);
|
|
64
|
+
|
|
65
|
+
-- sessions (analytics.sessions): SQLCH220 made by ClickHouseRebuildOp, not a statement:
|
|
66
|
+
-- export const { op } = ClickHouseRebuildOp({ name: "rebuild-analytics-sessions", env: "<env>", table: "analytics.sessions", dualWrite: { mode: "materialized-view", cutoverColumn: "ts" } });
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
A rebuild is never DDL: it is a step naming \`ClickHouseRebuildOp\`, or for a view or a database, which the Op does not rebuild, a manual step. A column or object drop is a statement marked destructive. Each \`CREATE\` carries the project's ownership marker, as the applier stamps it. The command exits 2 when a step is not a statement. A migration runner's history table (\`schema_migrations\`, \`goose_db_version\`; see [Importing a Live Server](../importing/)) is never dropped: the report names it in a hint. Two things the applier asks the server are left out: whether a difference is formatting only, and whether a database is empty before it is dropped.
|
|
70
|
+
|
|
71
|
+
\`--topology\` renders the statements for where they will run: \`single\` (the default), \`cluster:<name>\`, \`replicated\` or \`cloud\`. On a cluster every statement carries \`ON CLUSTER\` and a MergeTree-family table becomes \`Replicated*MergeTree\` with its Keeper path; in a \`Replicated\` database the tables are \`Replicated*MergeTree\` with no path and no \`ON CLUSTER\`. \`diffStatements(before, after, { topology })\` takes the same choice; see [Applying to a Server](../applying/#topology).
|
|
72
|
+
|
|
73
|
+
## Identity and renames
|
|
74
|
+
|
|
75
|
+
An object is identified by its export name between two builds, so a changed name in the SQL under the same export is a rename. Against a server, which has no export names, an object is identified by \`database.name\`; a declaration renamed since the server last saw it says so with \`-- previously: <old name>\` before its CREATE.
|
|
76
|
+
|
|
77
|
+
A column is identified by its name. A renamed column says so on its own line:
|
|
78
|
+
|
|
79
|
+
\`\`\`sql
|
|
80
|
+
CREATE TABLE events (
|
|
81
|
+
event_kind LowCardinality(String), -- previously: kind
|
|
82
|
+
...
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
Without the hint the change is a drop and an add, and the report points out a drop and an add with the same type in the same place.
|
|
86
|
+
|
|
87
|
+
## The rules
|
|
88
|
+
|
|
89
|
+
| Id | Class | Change | Restriction |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
${rows.join("\n")}
|
|
92
|
+
`;
|
|
93
|
+
}
|