@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,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalization: a ClickHouse object's definition in a form two definitions
|
|
3
|
+
* can be compared in, so a declaration and what the server reports for it are
|
|
4
|
+
* equal when they mean the same thing (#3047 question 4).
|
|
5
|
+
*
|
|
6
|
+
* The server rewrites what it is given. `SHOW CREATE` quotes every column,
|
|
7
|
+
* qualifies names with their database, writes `INTERVAL 180 DAY` as
|
|
8
|
+
* `toIntervalDay(180)`, fills in codec levels (`ZSTD(1)`, `Delta(4)`), drops a
|
|
9
|
+
* TTL's trailing `DELETE`, prints `index_granularity = 8192`, writes `int` as
|
|
10
|
+
* `Int32` and infers a view's column list. Each of those is undone here, on
|
|
11
|
+
* both sides, by rule:
|
|
12
|
+
*
|
|
13
|
+
* - expressions compare as their token sequences, whitespace, comments and
|
|
14
|
+
* identifier quoting left out, keywords in upper case, `INTERVAL n UNIT`
|
|
15
|
+
* rewritten to `toIntervalUnit(n)`, the object's own database dropped from a
|
|
16
|
+
* qualified name;
|
|
17
|
+
* - a type's family is its canonical name (`int` is `Int32`), from the pinned
|
|
18
|
+
* catalog's alias table;
|
|
19
|
+
* - a codec's default level goes (`ZSTD(1)` is `ZSTD`, `Delta(4)` is `Delta`);
|
|
20
|
+
* - a TTL rule's trailing `DELETE` goes;
|
|
21
|
+
* - a setting at the pinned server's default goes;
|
|
22
|
+
* - a view's column list is not compared: the server infers it;
|
|
23
|
+
* - a clause the parser does not know (#3872) is kept as an expression, by
|
|
24
|
+
* its name, and compared as one.
|
|
25
|
+
*
|
|
26
|
+
* What the rules cannot see is the server's own expression formatting
|
|
27
|
+
* (`a+b*2` prints as `a + (b * 2)`). When a plan reads a live server it asks
|
|
28
|
+
* that server's formatter (`formatQuerySingleLine`, a read with no side
|
|
29
|
+
* effects) about any expression the rules leave different
|
|
30
|
+
* (`./server-format.ts`). Nothing is created anywhere to normalize: a scratch
|
|
31
|
+
* database on the target would start Kafka consumers and register replicas in
|
|
32
|
+
* Keeper, and a scratch container would need Docker at plan time and could
|
|
33
|
+
* differ from the server in version.
|
|
34
|
+
*/
|
|
35
|
+
import { isTrivia, tokenizeText } from "../tokens.js";
|
|
36
|
+
import { parseCreate, unquote } from "../parser.js";
|
|
37
|
+
import { MERGE_TREE_SETTINGS, TYPE_FAMILIES } from "../../generated/clickhouse.js";
|
|
38
|
+
import { previouslyIn, splitTopLevel as splitTop } from "../../core/normalize.js";
|
|
39
|
+
import { accessCanonical, grantsCanonical, isAccessStatement } from "../access.js";
|
|
40
|
+
export { previouslyIn } from "../../core/normalize.js";
|
|
41
|
+
const KEYWORDS = new Set(("AS AND OR NOT IN IS NULL LIKE ILIKE BETWEEN CASE WHEN THEN ELSE END INTERVAL ASC DESC NULLS FIRST LAST " +
|
|
42
|
+
"SELECT FROM WHERE GROUP BY ORDER HAVING LIMIT OFFSET JOIN LEFT RIGHT INNER OUTER FULL CROSS ON USING ARRAY " +
|
|
43
|
+
"DISTINCT ALL ANY UNION WITH TOTALS ROLLUP CUBE PREWHERE SETTINGS FORMAT GLOBAL SEMI ANTI ASOF FINAL SAMPLE " +
|
|
44
|
+
"DELETE TO DISK VOLUME RECOMPRESS SET TRUE FALSE EVERY AFTER RANDOMIZE FOR DEPENDS APPEND").split(" "));
|
|
45
|
+
const INTERVAL_UNITS = {
|
|
46
|
+
NANOSECOND: "Nanosecond",
|
|
47
|
+
MICROSECOND: "Microsecond",
|
|
48
|
+
MILLISECOND: "Millisecond",
|
|
49
|
+
SECOND: "Second",
|
|
50
|
+
MINUTE: "Minute",
|
|
51
|
+
HOUR: "Hour",
|
|
52
|
+
DAY: "Day",
|
|
53
|
+
WEEK: "Week",
|
|
54
|
+
MONTH: "Month",
|
|
55
|
+
QUARTER: "Quarter",
|
|
56
|
+
YEAR: "Year",
|
|
57
|
+
};
|
|
58
|
+
const isName = (t) => t !== undefined && (t.kind === "ident" || t.kind === "qident");
|
|
59
|
+
/**
|
|
60
|
+
* The Keeper path and replica name a `Replicated*MergeTree` gets when its
|
|
61
|
+
* declaration leaves them out: the stock `default_replica_path` and
|
|
62
|
+
* `default_replica_name`, which `SHOW CREATE` then prints.
|
|
63
|
+
*/
|
|
64
|
+
export const DEFAULT_REPLICA_PATH = "'/clickhouse/tables/{uuid}/{shard}'";
|
|
65
|
+
export const DEFAULT_REPLICA_NAME = "'{replica}'";
|
|
66
|
+
/** One token's canonical text. */
|
|
67
|
+
function tokenText(t) {
|
|
68
|
+
if (t.kind === "qident")
|
|
69
|
+
return unquote(t.text);
|
|
70
|
+
if (t.kind === "ident" && KEYWORDS.has(t.text.toUpperCase()))
|
|
71
|
+
return t.text.toUpperCase();
|
|
72
|
+
return t.text;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* An expression's canonical text: its significant tokens joined by single
|
|
76
|
+
* spaces, with the rewrites the server makes undone.
|
|
77
|
+
*/
|
|
78
|
+
export function canonicalExpression(text, ownDatabase) {
|
|
79
|
+
let sig;
|
|
80
|
+
try {
|
|
81
|
+
sig = tokenizeText(text, 0).filter((t) => !isTrivia(t));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return text.trim();
|
|
85
|
+
}
|
|
86
|
+
const out = [];
|
|
87
|
+
for (let i = 0; i < sig.length; i++) {
|
|
88
|
+
const t = sig[i];
|
|
89
|
+
// INTERVAL 180 DAY -> toIntervalDay(180)
|
|
90
|
+
if (t.kind === "ident" && t.text.toUpperCase() === "INTERVAL" && sig[i + 1] && sig[i + 2]) {
|
|
91
|
+
const unit = INTERVAL_UNITS[sig[i + 2].text.toUpperCase().replace(/S$/, "")];
|
|
92
|
+
if (unit && (sig[i + 1].kind === "number" || sig[i + 1].kind === "ref" || isName(sig[i + 1]))) {
|
|
93
|
+
out.push(`toInterval${unit}`, "(", tokenText(sig[i + 1]), ")");
|
|
94
|
+
i += 2;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// own_db.name -> name
|
|
99
|
+
if (ownDatabase !== undefined && isName(t) && unquote(t.text) === ownDatabase && sig[i + 1]?.text === "." && isName(sig[i + 2])) {
|
|
100
|
+
const prev = sig[i - 1];
|
|
101
|
+
if (!(prev?.kind === "punct" && prev.text === ".")) {
|
|
102
|
+
i += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
out.push(tokenText(t));
|
|
107
|
+
}
|
|
108
|
+
return unwrapSingle(out).join(" ");
|
|
109
|
+
}
|
|
110
|
+
/** `( a )` with no top-level comma is `a`; `tuple ( )` is empty. */
|
|
111
|
+
function unwrapSingle(tokens) {
|
|
112
|
+
if (tokens.length === 3 && tokens[0].toLowerCase() === "tuple" && tokens[1] === "(" && tokens[2] === ")")
|
|
113
|
+
return [];
|
|
114
|
+
if (tokens[0] !== "(" || tokens[tokens.length - 1] !== ")")
|
|
115
|
+
return tokens;
|
|
116
|
+
let depth = 0;
|
|
117
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
118
|
+
if (tokens[i] === "(")
|
|
119
|
+
depth++;
|
|
120
|
+
else if (tokens[i] === ")")
|
|
121
|
+
depth--;
|
|
122
|
+
if (depth === 0 && i < tokens.length - 1)
|
|
123
|
+
return tokens;
|
|
124
|
+
if (depth === 1 && tokens[i] === ",")
|
|
125
|
+
return tokens;
|
|
126
|
+
}
|
|
127
|
+
return unwrapSingle(tokens.slice(1, -1));
|
|
128
|
+
}
|
|
129
|
+
/** A type with its family names canonical: `int` is `Int32`, `BIGINT` is `Int64`. */
|
|
130
|
+
export function canonicalType(text) {
|
|
131
|
+
const families = TYPE_FAMILIES;
|
|
132
|
+
const lower = new Map();
|
|
133
|
+
for (const [name, spec] of Object.entries(families))
|
|
134
|
+
if (spec?.caseInsensitive)
|
|
135
|
+
lower.set(name.toLowerCase(), spec.canonical);
|
|
136
|
+
return canonicalExpression(text)
|
|
137
|
+
.split(" ")
|
|
138
|
+
.map((w) => families[w]?.canonical ?? lower.get(w.toLowerCase()) ?? w)
|
|
139
|
+
.join(" ");
|
|
140
|
+
}
|
|
141
|
+
const CODEC_DEFAULT_ARGS = {
|
|
142
|
+
ZSTD: (a) => a === "1",
|
|
143
|
+
LZ4HC: (a) => a === "0" || a === "9",
|
|
144
|
+
Delta: () => true,
|
|
145
|
+
DoubleDelta: () => true,
|
|
146
|
+
Gorilla: () => true,
|
|
147
|
+
T64: (a) => a === "'byte'",
|
|
148
|
+
};
|
|
149
|
+
/** A codec chain with each codec's default level left out. */
|
|
150
|
+
export function canonicalCodec(text) {
|
|
151
|
+
return splitTop(canonicalExpression(text))
|
|
152
|
+
.map((c) => {
|
|
153
|
+
const m = /^(\w+) \( (.*) \)$/.exec(c);
|
|
154
|
+
if (!m)
|
|
155
|
+
return c;
|
|
156
|
+
const [, name, args] = m;
|
|
157
|
+
return CODEC_DEFAULT_ARGS[name]?.(args) ? name : c;
|
|
158
|
+
})
|
|
159
|
+
.join(" , ");
|
|
160
|
+
}
|
|
161
|
+
/** A TTL with each rule's trailing `DELETE` (the default action) left out. */
|
|
162
|
+
export function canonicalTtl(text, ownDatabase) {
|
|
163
|
+
return splitTop(canonicalExpression(text, ownDatabase))
|
|
164
|
+
.map((rule) => rule.replace(/ DELETE$/, ""))
|
|
165
|
+
.join(" , ");
|
|
166
|
+
}
|
|
167
|
+
function spanText(tokens, span) {
|
|
168
|
+
if (!span || span.to <= span.from)
|
|
169
|
+
return undefined;
|
|
170
|
+
return tokens
|
|
171
|
+
.slice(span.from, span.to)
|
|
172
|
+
.map((t) => t.text)
|
|
173
|
+
.join("")
|
|
174
|
+
.trim();
|
|
175
|
+
}
|
|
176
|
+
/** The comments on the line a column starts on, after its name. */
|
|
177
|
+
function lineComments(tokens, from) {
|
|
178
|
+
const out = [];
|
|
179
|
+
for (let i = from + 1; i < tokens.length; i++) {
|
|
180
|
+
const t = tokens[i];
|
|
181
|
+
if (t.kind === "ws" && t.text.includes("\n"))
|
|
182
|
+
break;
|
|
183
|
+
if (t.kind === "comment")
|
|
184
|
+
out.push(t.text);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
const kindOf = (node) => node.statement === "view" ? (node.materialized ? "materializedView" : "view") : node.statement;
|
|
189
|
+
const stringValue = (s) => s === undefined ? undefined : /^'.*'$/s.test(s) ? s.slice(1, -1).replace(/''/g, "'").replace(/\\(.)/g, "$1") : s;
|
|
190
|
+
/**
|
|
191
|
+
* Parse a CREATE statement into its canonical form. `defaultDatabase` is the
|
|
192
|
+
* database an unqualified name is created in.
|
|
193
|
+
*/
|
|
194
|
+
export function canonicalObject(ddl, defaultDatabase = "default") {
|
|
195
|
+
if (isAccessStatement(ddl)) {
|
|
196
|
+
if (/^\s*(?:--[^\n]*\n\s*)*(GRANT|REVOKE)\b/i.test(ddl))
|
|
197
|
+
throw new Error("a grant is compared as its grantee's grants: grantsObject(ddl, grantee)");
|
|
198
|
+
const a = accessCanonical(ddl, defaultDatabase);
|
|
199
|
+
return {
|
|
200
|
+
kind: a.kind,
|
|
201
|
+
...(a.database !== undefined ? { database: a.database } : {}),
|
|
202
|
+
name: a.name,
|
|
203
|
+
...(a.table !== undefined ? { table: a.table } : {}),
|
|
204
|
+
access: a.fields,
|
|
205
|
+
columns: [],
|
|
206
|
+
settings: {},
|
|
207
|
+
indexes: {},
|
|
208
|
+
projections: {},
|
|
209
|
+
constraints: {},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const tokens = tokenizeText(ddl, 0);
|
|
213
|
+
const node = parseCreate(tokens);
|
|
214
|
+
const nameParts = tokens
|
|
215
|
+
.slice(node.name.from, node.name.to)
|
|
216
|
+
.filter((t) => !isTrivia(t) && !(t.kind === "punct" && t.text === "."))
|
|
217
|
+
.map((t) => unquote(t.text));
|
|
218
|
+
const kind = kindOf(node);
|
|
219
|
+
const name = nameParts[nameParts.length - 1];
|
|
220
|
+
const database = kind === "database" || kind === "function" ? undefined : nameParts.length >= 2 ? nameParts[nameParts.length - 2] : defaultDatabase;
|
|
221
|
+
const expr = (span) => {
|
|
222
|
+
const t = spanText(tokens, span);
|
|
223
|
+
return t === undefined ? undefined : canonicalExpression(t, database);
|
|
224
|
+
};
|
|
225
|
+
const leading = tokens.slice(0, tokens.findIndex((t) => !isTrivia(t))).filter((t) => t.kind === "comment").map((t) => t.text);
|
|
226
|
+
const obj = {
|
|
227
|
+
kind,
|
|
228
|
+
...(database !== undefined ? { database } : {}),
|
|
229
|
+
name,
|
|
230
|
+
columns: [],
|
|
231
|
+
settings: {},
|
|
232
|
+
indexes: {},
|
|
233
|
+
projections: {},
|
|
234
|
+
constraints: {},
|
|
235
|
+
};
|
|
236
|
+
const previously = previouslyIn(leading);
|
|
237
|
+
if (previously)
|
|
238
|
+
obj.previously = previously;
|
|
239
|
+
const unparsed = unparsedOf(tokens, node.unknown, database);
|
|
240
|
+
if (unparsed)
|
|
241
|
+
obj.unparsed = unparsed;
|
|
242
|
+
const comment0 = stringValue(spanText(tokens, node.comment));
|
|
243
|
+
if (node.statement === "dictionary") {
|
|
244
|
+
if (comment0 !== undefined && comment0 !== "")
|
|
245
|
+
obj.comment = comment0;
|
|
246
|
+
return dictionaryObject(obj, tokens, node, database);
|
|
247
|
+
}
|
|
248
|
+
if (node.statement === "function") {
|
|
249
|
+
obj.lambda = canonicalExpression(spanText(tokens, node.lambda) ?? "");
|
|
250
|
+
return obj;
|
|
251
|
+
}
|
|
252
|
+
const engine = node.engine;
|
|
253
|
+
if (engine) {
|
|
254
|
+
let args = engine.args?.map((a) => canonicalExpression(spanText(tokens, a) ?? "", database)) ?? [];
|
|
255
|
+
// `ENGINE = ReplicatedMergeTree` with no Keeper path and replica name is
|
|
256
|
+
// printed back with the server's defaults filled in; the two say the same.
|
|
257
|
+
if (/^Replicated.*MergeTree$/.test(engine.name) && args[0] === DEFAULT_REPLICA_PATH && args[1] === DEFAULT_REPLICA_NAME)
|
|
258
|
+
args = args.slice(2);
|
|
259
|
+
// ClickHouse Cloud turns a declared MergeTree-family engine into its
|
|
260
|
+
// `Shared*` one and prints it back with the path and replica it chose
|
|
261
|
+
// (#3645, `../topology.ts`): the declaration said the plain family.
|
|
262
|
+
let name = engine.name;
|
|
263
|
+
const shared = /^Shared(\w*MergeTree)$/.exec(name);
|
|
264
|
+
if (shared) {
|
|
265
|
+
name = shared[1];
|
|
266
|
+
if (/^'.*'$/s.test(args[0] ?? "") && /^'.*'$/s.test(args[1] ?? ""))
|
|
267
|
+
args = args.slice(2);
|
|
268
|
+
}
|
|
269
|
+
// `Distributed(cluster, db, table, ...)` takes its first three arguments
|
|
270
|
+
// as identifiers or as strings, and the server prints them back quoted
|
|
271
|
+
// (#3664): an unquoted declaration says the same.
|
|
272
|
+
if (name === "Distributed")
|
|
273
|
+
args = args.map((a, i) => (i < 3 ? quotedIdentifier(a) : a));
|
|
274
|
+
obj.engineName = name;
|
|
275
|
+
obj.engine = args.length ? `${name}(${args.join(", ")})` : name;
|
|
276
|
+
}
|
|
277
|
+
if (comment0 !== undefined && comment0 !== "")
|
|
278
|
+
obj.comment = comment0;
|
|
279
|
+
if (node.statement === "database") {
|
|
280
|
+
for (const s of node.settings ?? [])
|
|
281
|
+
obj.settings[s.key] = canonicalExpression(spanText(tokens, s.value) ?? "").replace(/^'(.*)'$/, "$1");
|
|
282
|
+
return obj;
|
|
283
|
+
}
|
|
284
|
+
const set = (k, span) => {
|
|
285
|
+
const v = expr(span);
|
|
286
|
+
if (v !== undefined && v !== "")
|
|
287
|
+
obj[k] = v;
|
|
288
|
+
};
|
|
289
|
+
set("orderBy", node.orderBy);
|
|
290
|
+
set("primaryKey", node.primaryKey);
|
|
291
|
+
set("partitionBy", node.partitionBy);
|
|
292
|
+
set("sampleBy", node.sampleBy);
|
|
293
|
+
if (node.ttl)
|
|
294
|
+
obj.ttl = canonicalTtl(spanText(tokens, node.ttl), database);
|
|
295
|
+
for (const s of node.settings ?? []) {
|
|
296
|
+
const value = canonicalExpression(spanText(tokens, s.value) ?? "").replace(/^'(.*)'$/, "$1");
|
|
297
|
+
const def = MERGE_TREE_SETTINGS[s.key];
|
|
298
|
+
if (def && def.default === value)
|
|
299
|
+
continue;
|
|
300
|
+
obj.settings[s.key] = value;
|
|
301
|
+
}
|
|
302
|
+
if (node.statement === "table") {
|
|
303
|
+
if (node.asFunction)
|
|
304
|
+
obj.asFunction = canonicalExpression(spanText(tokens, node.asFunction) ?? "", database);
|
|
305
|
+
node.columns.forEach((c, i) => obj.columns.push(column(tokens, c, i, database)));
|
|
306
|
+
for (const ix of node.indexes) {
|
|
307
|
+
obj.indexes[ix.name] = [expr(ix.expr), "TYPE", expr(ix.type), ix.granularity ? `GRANULARITY ${expr(ix.granularity)}` : ""]
|
|
308
|
+
.filter(Boolean)
|
|
309
|
+
.join(" ");
|
|
310
|
+
}
|
|
311
|
+
for (const p of node.projections)
|
|
312
|
+
obj.projections[p.name] = expr(p.body) ?? "";
|
|
313
|
+
for (const c of node.constraints)
|
|
314
|
+
obj.constraints[c.name] = `${c.kind} ${expr(c.expr)}`;
|
|
315
|
+
return obj;
|
|
316
|
+
}
|
|
317
|
+
obj.select = expr(node.select);
|
|
318
|
+
if (node.to)
|
|
319
|
+
obj.to = qualified(tokens, node.to, defaultDatabase);
|
|
320
|
+
if (node.refresh)
|
|
321
|
+
obj.refresh = expr(node.refresh);
|
|
322
|
+
return obj;
|
|
323
|
+
}
|
|
324
|
+
/** A statement's unknown clauses (#3872), by clause, each canonical as an expression; a second of the same name is `CLAUSE#2`. */
|
|
325
|
+
function unparsedOf(tokens, unknown, database) {
|
|
326
|
+
if (!unknown || unknown.length === 0)
|
|
327
|
+
return undefined;
|
|
328
|
+
const out = {};
|
|
329
|
+
for (const u of unknown) {
|
|
330
|
+
const base = u.column !== undefined ? `columns.${u.column}.${u.clause}` : u.clause;
|
|
331
|
+
let key = base;
|
|
332
|
+
for (let n = 2; key in out; n++)
|
|
333
|
+
key = `${base}#${n}`;
|
|
334
|
+
out[key] = canonicalExpression(spanText(tokens, u.span) ?? "", database);
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
// ── dictionaries ──────────────────────────────────────────────────────
|
|
339
|
+
/**
|
|
340
|
+
* A dictionary's clause (`SOURCE`, `LAYOUT`) as the server prints it: each
|
|
341
|
+
* function name and each key upper case (`clickhouse(table 'r')` is
|
|
342
|
+
* `CLICKHOUSE(TABLE 'r')`), each value as written, and a `PASSWORD` value
|
|
343
|
+
* as `'[HIDDEN]'`, since `SHOW CREATE` never prints it.
|
|
344
|
+
*/
|
|
345
|
+
export function canonicalDictionaryClause(text) {
|
|
346
|
+
let sig;
|
|
347
|
+
try {
|
|
348
|
+
sig = tokenizeText(text, 0).filter((t) => !isTrivia(t));
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return text.trim();
|
|
352
|
+
}
|
|
353
|
+
const out = [];
|
|
354
|
+
let key = true;
|
|
355
|
+
let hide = false;
|
|
356
|
+
for (let i = 0; i < sig.length; i++) {
|
|
357
|
+
const t = sig[i];
|
|
358
|
+
const next = sig[i + 1];
|
|
359
|
+
if (t.kind === "punct" && (t.text === "(" || t.text === "," || t.text === ")")) {
|
|
360
|
+
out.push(t.text);
|
|
361
|
+
key = true;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (key && t.kind === "ident") {
|
|
365
|
+
const word = t.text.toUpperCase();
|
|
366
|
+
out.push(word);
|
|
367
|
+
// A function name opens its own list of keys; a key is followed by its value.
|
|
368
|
+
key = next?.kind === "punct" && next.text === "(";
|
|
369
|
+
hide = word === "PASSWORD";
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
out.push(hide ? "'[HIDDEN]'" : t.kind === "qident" ? unquote(t.text) : t.text);
|
|
373
|
+
hide = false;
|
|
374
|
+
key = !(next?.kind === "punct" && next.text === "(");
|
|
375
|
+
}
|
|
376
|
+
return out.join(" ");
|
|
377
|
+
}
|
|
378
|
+
/** `300` is `MIN 0 MAX 300`; `MAX b MIN a` is `MIN a MAX b`. */
|
|
379
|
+
export function canonicalLifetime(text) {
|
|
380
|
+
const words = canonicalExpression(text).split(" ");
|
|
381
|
+
if (words.length === 1)
|
|
382
|
+
return `MIN 0 MAX ${words[0]}`;
|
|
383
|
+
const at = (w) => words.findIndex((x) => x.toUpperCase() === w);
|
|
384
|
+
const min = at("MIN");
|
|
385
|
+
const max = at("MAX");
|
|
386
|
+
if (min < 0 || max < 0)
|
|
387
|
+
return words.join(" ");
|
|
388
|
+
return `MIN ${words[min + 1] ?? ""} MAX ${words[max + 1] ?? ""}`;
|
|
389
|
+
}
|
|
390
|
+
/** A key list with any parentheses around the whole of it left out: `(a, b)` is `a , b`. */
|
|
391
|
+
function keyList(text, database) {
|
|
392
|
+
const e = canonicalExpression(text, database);
|
|
393
|
+
if (!e.startsWith("( ") || !e.endsWith(" )"))
|
|
394
|
+
return e;
|
|
395
|
+
let depth = 0;
|
|
396
|
+
const words = e.split(" ");
|
|
397
|
+
for (let i = 0; i < words.length; i++) {
|
|
398
|
+
if (words[i] === "(")
|
|
399
|
+
depth++;
|
|
400
|
+
else if (words[i] === ")")
|
|
401
|
+
depth--;
|
|
402
|
+
if (depth === 0 && i < words.length - 1)
|
|
403
|
+
return e;
|
|
404
|
+
}
|
|
405
|
+
return words.slice(1, -1).join(" ");
|
|
406
|
+
}
|
|
407
|
+
function dictionaryAttribute(tokens, a, position, database) {
|
|
408
|
+
const text = (span) => spanText(tokens, span);
|
|
409
|
+
const out = { name: a.name, position, type: canonicalType(text(a.type)), text: [a.name, text(a.type)].join(" ") };
|
|
410
|
+
const d = text(a.default);
|
|
411
|
+
if (d !== undefined) {
|
|
412
|
+
out.defaultKind = "DEFAULT";
|
|
413
|
+
out.defaultExpr = canonicalExpression(d, database);
|
|
414
|
+
}
|
|
415
|
+
const e = text(a.expression);
|
|
416
|
+
if (e !== undefined)
|
|
417
|
+
out.expression = canonicalExpression(e, database);
|
|
418
|
+
if (a.flags.length > 0)
|
|
419
|
+
out.flags = [...a.flags].sort().join(" ");
|
|
420
|
+
const prev = previouslyIn(lineComments(tokens, a.nameSpan.from));
|
|
421
|
+
if (prev)
|
|
422
|
+
out.previously = prev;
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
function dictionaryObject(obj, tokens, node, database) {
|
|
426
|
+
const text = (span) => spanText(tokens, span);
|
|
427
|
+
node.attributes.forEach((a, i) => obj.columns.push(dictionaryAttribute(tokens, a, i, database)));
|
|
428
|
+
const pk = text(node.primaryKey);
|
|
429
|
+
if (pk !== undefined)
|
|
430
|
+
obj.primaryKey = keyList(pk, database);
|
|
431
|
+
const source = text(node.source);
|
|
432
|
+
if (source !== undefined)
|
|
433
|
+
obj.dataSource = canonicalDictionaryClause(source);
|
|
434
|
+
const layout = text(node.layout);
|
|
435
|
+
if (layout !== undefined)
|
|
436
|
+
obj.layout = canonicalDictionaryClause(layout);
|
|
437
|
+
const lifetime = text(node.lifetime);
|
|
438
|
+
if (lifetime !== undefined)
|
|
439
|
+
obj.lifetime = canonicalLifetime(lifetime);
|
|
440
|
+
const range = text(node.range);
|
|
441
|
+
if (range !== undefined)
|
|
442
|
+
obj.range = canonicalExpression(range, database).replace(/\b(min|max)\b/gi, (w) => w.toUpperCase());
|
|
443
|
+
const settings = text(node.settings);
|
|
444
|
+
if (settings !== undefined) {
|
|
445
|
+
for (const item of splitTop(canonicalExpression(settings))) {
|
|
446
|
+
const m = /^(\S+) = (.*)$/.exec(item);
|
|
447
|
+
if (m)
|
|
448
|
+
obj.settings[m[1]] = m[2].replace(/^'(.*)'$/, "$1");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return obj;
|
|
452
|
+
}
|
|
453
|
+
function qualified(tokens, span, defaultDatabase) {
|
|
454
|
+
const parts = tokens
|
|
455
|
+
.slice(span.from, span.to)
|
|
456
|
+
.filter((t) => !isTrivia(t) && !(t.kind === "punct" && t.text === "."))
|
|
457
|
+
.map((t) => unquote(t.text));
|
|
458
|
+
return parts.length >= 2 ? `${parts[parts.length - 2]}.${parts[parts.length - 1]}` : `${defaultDatabase}.${parts[0]}`;
|
|
459
|
+
}
|
|
460
|
+
function column(tokens, c, position, database) {
|
|
461
|
+
const text = (span) => spanText(tokens, span);
|
|
462
|
+
const out = {
|
|
463
|
+
name: c.name,
|
|
464
|
+
position,
|
|
465
|
+
type: c.type ? canonicalType(text(c.type)) : "",
|
|
466
|
+
text: [c.name, text(c.type)].filter(Boolean).join(" "),
|
|
467
|
+
};
|
|
468
|
+
if (c.nullable !== undefined)
|
|
469
|
+
out.nullable = c.nullable;
|
|
470
|
+
if (c.default) {
|
|
471
|
+
out.defaultKind = c.default.kind;
|
|
472
|
+
const e = text(c.default.expr);
|
|
473
|
+
if (e !== undefined)
|
|
474
|
+
out.defaultExpr = canonicalExpression(e, database);
|
|
475
|
+
}
|
|
476
|
+
if (c.codec)
|
|
477
|
+
out.codec = canonicalCodec(text(c.codec));
|
|
478
|
+
if (c.ttl)
|
|
479
|
+
out.ttl = canonicalExpression(text(c.ttl), database);
|
|
480
|
+
const comment = stringValue(text(c.comment));
|
|
481
|
+
if (comment !== undefined && comment !== "")
|
|
482
|
+
out.comment = comment;
|
|
483
|
+
const prev = previouslyIn(lineComments(tokens, c.nameSpan.from));
|
|
484
|
+
if (prev)
|
|
485
|
+
out.previously = prev;
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
/** A bare or backquoted identifier as the string literal the server prints it as; anything else as it is. */
|
|
489
|
+
function quotedIdentifier(arg) {
|
|
490
|
+
const bare = /^[A-Za-z_][A-Za-z0-9_]*$/.test(arg) ? arg : /^`((?:[^`]|``)+)`$/.exec(arg)?.[1]?.replace(/``/g, "`");
|
|
491
|
+
return bare === undefined ? arg : `'${bare.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* The key an object is compared under against a server: `database.name`,
|
|
495
|
+
* a database's name, and `function <name>` for a function, which belongs to
|
|
496
|
+
* no database and must not meet a database of the same name.
|
|
497
|
+
*/
|
|
498
|
+
export function objectKey(o) {
|
|
499
|
+
const kind = o.kind ?? KIND_OF_TYPE[o.type ?? ""];
|
|
500
|
+
if (kind === "function")
|
|
501
|
+
return `function ${o.name}`;
|
|
502
|
+
if (kind === "user")
|
|
503
|
+
return `user ${o.name}`;
|
|
504
|
+
if (kind === "role")
|
|
505
|
+
return `role ${o.name}`;
|
|
506
|
+
if (kind === "quota")
|
|
507
|
+
return `quota ${o.name}`;
|
|
508
|
+
if (kind === "settingsProfile")
|
|
509
|
+
return `settings profile ${o.name}`;
|
|
510
|
+
if (kind === "grants")
|
|
511
|
+
return `grants ${o.name}`;
|
|
512
|
+
if (kind === "rowPolicy")
|
|
513
|
+
return `row policy ${o.name} ON ${o.database}.${o.table}`;
|
|
514
|
+
return o.database !== undefined ? `${o.database}.${o.name}` : o.name;
|
|
515
|
+
}
|
|
516
|
+
const KIND_OF_TYPE = {
|
|
517
|
+
"ClickHouse::Function": "function",
|
|
518
|
+
"ClickHouse::User": "user",
|
|
519
|
+
"ClickHouse::Role": "role",
|
|
520
|
+
"ClickHouse::RowPolicy": "rowPolicy",
|
|
521
|
+
"ClickHouse::Quota": "quota",
|
|
522
|
+
"ClickHouse::SettingsProfile": "settingsProfile",
|
|
523
|
+
"ClickHouse::Grant": "grants",
|
|
524
|
+
};
|
|
525
|
+
/** A grantee's grants, from grant statements (one or more, on their own lines), as a plan compares them. */
|
|
526
|
+
export function grantsObject(ddl, grantee, defaultDatabase = "default") {
|
|
527
|
+
return { kind: "grants", name: grantee, access: grantsCanonical(ddl, grantee, defaultDatabase), columns: [], settings: {}, indexes: {}, projections: {}, constraints: {} };
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* What an object is scoped by when a plan reads a server: its database, a
|
|
531
|
+
* database's own name, and for a function its key. A plan reads the
|
|
532
|
+
* databases the declarations use and the functions they declare, nothing
|
|
533
|
+
* else.
|
|
534
|
+
*/
|
|
535
|
+
export function scopeOf(o) {
|
|
536
|
+
const kind = o.kind ?? KIND_OF_TYPE[o.type ?? ""];
|
|
537
|
+
if (kind === "function" || kind === "user" || kind === "role" || kind === "quota" || kind === "settingsProfile" || kind === "grants")
|
|
538
|
+
return objectKey(o);
|
|
539
|
+
return o.database ?? o.name;
|
|
540
|
+
}
|
|
541
|
+
/** A live object's canonical form: a grantee's grants from its `SHOW GRANTS` lines, anything else from its `SHOW CREATE`. */
|
|
542
|
+
export function liveCanonical(o, defaultDatabase = "default") {
|
|
543
|
+
const canonical = o.type === "ClickHouse::Grant" ? grantsObject(o.statement, o.name, defaultDatabase) : canonicalObject(o.statement, defaultDatabase);
|
|
544
|
+
return o.foreign ? { ...canonical, foreign: o.foreign } : canonical;
|
|
545
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a plan hands a rebuild to the rebuild migration Op (#3198).
|
|
3
|
+
*
|
|
4
|
+
* `chant sql plan`, `chant sql diff` and the applier refuse a rebuild in
|
|
5
|
+
* place. What they hand on is the Op to run instead, one per table: a
|
|
6
|
+
* `ClickHouseRebuildOp` declaration ready to put in an `*.op.ts` file, with
|
|
7
|
+
* the table's `database.name` filled in and a dual-write mode suggested from
|
|
8
|
+
* the declared columns (materialized-view mode on the first time column,
|
|
9
|
+
* otherwise app mode). The Op's own Plan phase then classifies the same
|
|
10
|
+
* change again against the server, so what it runs is what the plan refused,
|
|
11
|
+
* read fresh.
|
|
12
|
+
*
|
|
13
|
+
* Nothing is written for the user: the Op is a declaration, reviewed in the
|
|
14
|
+
* pull request like the schema change it carries out. The hand-off's shape is
|
|
15
|
+
* the shared core's (`../../core/handoff.ts`); the Op and the dual-write
|
|
16
|
+
* suggestion are ClickHouse's.
|
|
17
|
+
*/
|
|
18
|
+
import { migrationOpName, renderMigrationOps } from "../../core/handoff.js";
|
|
19
|
+
import { collapsesRows } from "../rebuild/engines.js";
|
|
20
|
+
const TIME_TYPE = /^(?:Nullable\s*\(\s*)?(?:Date|Date32|DateTime|DateTime64)\b/;
|
|
21
|
+
/**
|
|
22
|
+
* One suggestion per object the diff refuses as a rebuild. `declared` maps
|
|
23
|
+
* the diff's object keys to the declared definitions.
|
|
24
|
+
*/
|
|
25
|
+
export function rebuildOpSuggestions(diff, declared, env) {
|
|
26
|
+
const out = [];
|
|
27
|
+
// A clause chant does not read (SQLCH290) is refused for review, not because a rebuild is known to be what it takes.
|
|
28
|
+
for (const key of [...new Set(diff.rebuilds.filter((c) => c.rule !== "SQLCH290").map((c) => c.object))]) {
|
|
29
|
+
const c = declared.get(key);
|
|
30
|
+
if (!c || c.kind !== "table")
|
|
31
|
+
continue;
|
|
32
|
+
const table = `${c.database ?? "default"}.${c.name}`;
|
|
33
|
+
const time = c.columns.find((col) => !col.nullable && TIME_TYPE.test(col.type) && (!col.defaultKind || col.defaultKind === "DEFAULT"));
|
|
34
|
+
const dualWrite = time ? { mode: "materialized-view", cutoverColumn: time.name } : { mode: "app" };
|
|
35
|
+
const name = migrationOpName("rebuild", table);
|
|
36
|
+
const dw = dualWrite.mode === "app" ? `{ mode: "app" }` : `{ mode: "materialized-view", cutoverColumn: ${JSON.stringify(dualWrite.cutoverColumn)} }`;
|
|
37
|
+
const note = collapsesRows(c.engineName)
|
|
38
|
+
? `${table} is a ${c.engineName}: when the old table's engine collapses rows too, the Verify phase reads both tables under FINAL, so rows the old table has not merged yet compare as merged. ` +
|
|
39
|
+
`A new sorting key that collapses rows differently from the old one still fails it; OPTIMIZE TABLE ${table} FINAL before the run avoids that. ` +
|
|
40
|
+
`From an engine that keeps every row (a plain MergeTree), it groups both tables by the new sorting key instead and compares what a ${c.engineName} keeps per key (its sums, its highest version, the sum of its signs).`
|
|
41
|
+
: undefined;
|
|
42
|
+
out.push({
|
|
43
|
+
table,
|
|
44
|
+
name,
|
|
45
|
+
env,
|
|
46
|
+
dualWrite,
|
|
47
|
+
...(note ? { note } : {}),
|
|
48
|
+
declaration: `export const { op } = ClickHouseRebuildOp({ name: ${JSON.stringify(name)}, env: ${JSON.stringify(env)}, table: ${JSON.stringify(table)}, dualWrite: ${dw} });`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** The suggestions as text, for the report. */
|
|
54
|
+
export function renderRebuildOps(ops) {
|
|
55
|
+
const notes = ops.flatMap((o) => (o.note ? [` Note: ${o.note}`] : []));
|
|
56
|
+
return [...renderMigrationOps(ops, { what: "the rebuild migration Op", exportName: "ClickHouseRebuildOp", importPath: "@intentius/chant-lexicon-sql/clickhouse" }), ...notes];
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** A schema diff as text, for a terminal or a pull request (the shared core's report, with ClickHouse's refusal). */
|
|
2
|
+
import { renderChangeSet } from "../../core/classifier.js";
|
|
3
|
+
import { CHANGE_CLASSES, CLASSIFIER_RULES } from "./rules.js";
|
|
4
|
+
import { renderRebuildOps } from "./rebuild-handoff.js";
|
|
5
|
+
export function renderDiff(diff, opts = {}) {
|
|
6
|
+
const unreadable = diff.unreadable ?? [];
|
|
7
|
+
const refusedRead = unreadable.length > 0
|
|
8
|
+
? [
|
|
9
|
+
"",
|
|
10
|
+
`Refused: ${unreadable.length} object(s) in the declared databases could not be read, so this plan cannot say what changes for them:`,
|
|
11
|
+
...unreadable.map((u) => ` ${u.object} (${u.type}): ${u.reason}`),
|
|
12
|
+
]
|
|
13
|
+
: [];
|
|
14
|
+
const trailer = [
|
|
15
|
+
...refusedRead,
|
|
16
|
+
...(diff.rebuilds.length > 0
|
|
17
|
+
? [
|
|
18
|
+
"",
|
|
19
|
+
`Refused: ${diff.rebuilds.length} change(s) need a rebuild, which ClickHouse cannot make to the existing table. ` +
|
|
20
|
+
"A rebuild runs as its own migration (create the new table, backfill, verify, swap), not in place.",
|
|
21
|
+
...renderRebuildOps(diff.rebuildOps ?? []),
|
|
22
|
+
]
|
|
23
|
+
: []),
|
|
24
|
+
];
|
|
25
|
+
return renderChangeSet(diff, { ...(opts.title ? { title: opts.title } : {}), rules: CLASSIFIER_RULES, classes: CHANGE_CLASSES, trailer });
|
|
26
|
+
}
|