@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,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verification (#3198, INTENTIUS/sql-yodeler#45): row counts and
|
|
3
|
+
* checksums per partition, the old table against the new one, before anyone
|
|
4
|
+
* is asked to approve the swap.
|
|
5
|
+
*
|
|
6
|
+
* Per partition of the old table: the number of rows and the sum of a
|
|
7
|
+
* 64-bit hash of each row's copied columns, read in one grouped query on
|
|
8
|
+
* each side. The old side casts each column to its type in the new table
|
|
9
|
+
* first, so a column whose type the rebuild changes hashes the same when
|
|
10
|
+
* its values survived the change.
|
|
11
|
+
*
|
|
12
|
+
* Every row is compared, in both modes. In materialized-view mode the rows
|
|
13
|
+
* at or after the cut-over are in the new table twice over: those the old
|
|
14
|
+
* table held before the view existed, which the backfill copies, and those
|
|
15
|
+
* written since, which the view writes. Comparing only the rows before the
|
|
16
|
+
* cut-over let a swap go ahead without the first kind (sql-yodeler#45).
|
|
17
|
+
* Writes go on while the tables are read, and a row the old table has
|
|
18
|
+
* committed may be a moment from committing in the new one through the view,
|
|
19
|
+
* so the old table is read first and a difference is read again a few times
|
|
20
|
+
* before it counts.
|
|
21
|
+
*
|
|
22
|
+
* The same comparison runs again in the Swap step just before the
|
|
23
|
+
* `EXCHANGE`, so rows that reach the old table alone after this step (a row
|
|
24
|
+
* later than the cut-over delay, with a time before the cut-over) stop the
|
|
25
|
+
* swap too, rather than stay behind in the old table.
|
|
26
|
+
*
|
|
27
|
+
* A table whose engine collapses rows that share a sorting key when parts
|
|
28
|
+
* merge (Summing, Replacing, Aggregating, Collapsing, VersionedCollapsing,
|
|
29
|
+
* Coalescing and Graphite, replicated or not), rebuilt into such an engine,
|
|
30
|
+
* is read under `FINAL` on both sides (#3674). The old table can hold unmerged parts with several rows for
|
|
31
|
+
* one key, which the copy inserts in one block and the new table collapses
|
|
32
|
+
* as it writes them, so the raw counts differ although the data is the same.
|
|
33
|
+
* Under `FINAL` both sides are compared as merged.
|
|
34
|
+
*
|
|
35
|
+
* On a cluster of more than one shard (#3663) every shard is read, through
|
|
36
|
+
* the profile's server, and each partition is compared per shard
|
|
37
|
+
* (`shard<n>/<partition>`), so a shard the copy missed is a difference.
|
|
38
|
+
*
|
|
39
|
+
* Any difference fails the run, naming the partitions; the run's onFailure
|
|
40
|
+
* then drops the new table. On a match the result carries the digest the
|
|
41
|
+
* swap gate binds: the rebuild's plan and the counts and checksums of the
|
|
42
|
+
* rows before the cut-over, which no later write changes, so an approval
|
|
43
|
+
* is for this plan with these counts, and a changed count needs a new
|
|
44
|
+
* approval. The rows at or after the cut-over keep arriving through the view
|
|
45
|
+
* and are compared, not bound.
|
|
46
|
+
*/
|
|
47
|
+
import { computePlanDigest } from "@intentius/chant/op";
|
|
48
|
+
import { clickhouseQuery } from "../http.js";
|
|
49
|
+
import { ident, sqlString } from "../apply/statements.js";
|
|
50
|
+
import { rebuildPlanSubject, RebuildRefusal } from "./observe.js";
|
|
51
|
+
import { sourcePartitionExpression } from "./partitions.js";
|
|
52
|
+
import { cutoverOf, observe, syncReplica, utcLiteral, waitOn } from "./steps.js";
|
|
53
|
+
import { shardTable } from "./shards.js";
|
|
54
|
+
import { collapseSemantics, collapsesRows, splitTopLevel } from "./engines.js";
|
|
55
|
+
/** A verification that found the tables different. */
|
|
56
|
+
export class RebuildVerificationError extends Error {
|
|
57
|
+
constructor(table, mismatches,
|
|
58
|
+
/** How the tables were compared when an engine collapses rows; an engine's name alone means under FINAL. */
|
|
59
|
+
collapsed) {
|
|
60
|
+
const how = typeof collapsed === "string" ? { engine: collapsed, how: "final" } : collapsed;
|
|
61
|
+
const shown = mismatches
|
|
62
|
+
.slice(0, 10)
|
|
63
|
+
.map((m) => `${m.partition}: old ${m.old ? `${m.old.rows} ${unit(how)}, checksum ${m.old.checksum}` : "none"}, new ${m.new ? `${m.new.rows} ${unit(how)}, checksum ${m.new.checksum}` : "none"}`)
|
|
64
|
+
.join("; ");
|
|
65
|
+
const lateWrite = `in materialized-view mode a row written to the old table later than the cut-over delay, with a time before the cut-over, ` +
|
|
66
|
+
`and in app mode a write the application did not stop`;
|
|
67
|
+
super(`${table}: the new table does not match the old one in ${mismatches.length} partition(s): ${shown}${mismatches.length > 10 ? "; ..." : ""}. Nothing was swapped. ` +
|
|
68
|
+
(how?.how === "final"
|
|
69
|
+
? `${table} is a ${how.engine}, which collapses rows that share a sorting key when parts merge, and both tables were compared under FINAL. ` +
|
|
70
|
+
`They still differ when the new table collapses the copied rows by its sorting key differently from how the old one merges them: ` +
|
|
71
|
+
`run OPTIMIZE TABLE ${table} FINAL, drop the new table if the Op kept it, and run the rebuild again, so the copy reads merged rows. ` +
|
|
72
|
+
`The other causes are ${lateWrite}.`
|
|
73
|
+
: how?.how === "keys"
|
|
74
|
+
? `The new table is a ${how.engine}, which collapses rows that share a sorting key, so both tables were grouped by its sorting key and partition ` +
|
|
75
|
+
`and compared per key on ${how.describe ?? "the keys"}, which its merges keep. A key missing from the new table, or one whose values differ, is rows the copy lost or changed. ` +
|
|
76
|
+
`The usual cause is ${lateWrite}.`
|
|
77
|
+
: `The usual cause is ${lateWrite}.`));
|
|
78
|
+
this.table = table;
|
|
79
|
+
this.mismatches = mismatches;
|
|
80
|
+
this.name = "RebuildVerificationError";
|
|
81
|
+
if (how)
|
|
82
|
+
this.collapsingEngine = how.engine;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const unit = (how) => (how?.how === "keys" ? "keys" : "rows");
|
|
86
|
+
/** How often a difference is read again before it counts, and how long apart. */
|
|
87
|
+
const COMPARE_ATTEMPTS = 5;
|
|
88
|
+
const COMPARE_INTERVAL_MS = 1000;
|
|
89
|
+
/**
|
|
90
|
+
* Both tables grouped by the new table's sorting key and its partition, each
|
|
91
|
+
* key compared on what the new engine's merges keep (#3727): its sums, its
|
|
92
|
+
* highest version, the sum of its signs. Each side is read raw, so the
|
|
93
|
+
* comparison is the same whichever parts have merged. The old side's columns
|
|
94
|
+
* are cast to the new table's types and named as there, so the sorting key,
|
|
95
|
+
* the partition key and the aggregates read the same on both sides. A
|
|
96
|
+
* partition is the new table's (`shard<n>/` before it on a cluster), and its
|
|
97
|
+
* rows are its keys.
|
|
98
|
+
*/
|
|
99
|
+
async function keyComparison(run, o, columns) {
|
|
100
|
+
const n = o.names;
|
|
101
|
+
const [t] = await clickhouseQuery(run.target.endpoint, `SELECT engine_full, sorting_key, partition_key FROM system.tables WHERE database = ${sqlString(n.database)} AND name = ${sqlString(n.newName)}`);
|
|
102
|
+
if (!t || !o.newTable)
|
|
103
|
+
return undefined;
|
|
104
|
+
const semantics = collapseSemantics(o.newTable.engine, t.engine_full, columns);
|
|
105
|
+
if (!semantics)
|
|
106
|
+
return undefined;
|
|
107
|
+
// Every column of the sorting key must come from the old table, or the old rows have no key to group by.
|
|
108
|
+
const copied = new Set(o.copied.map((c) => c.name));
|
|
109
|
+
if (columns.some((c) => c.inSortingKey && !copied.has(c.name)))
|
|
110
|
+
return undefined;
|
|
111
|
+
const types = new Map(columns.map((c) => [c.name, c.type]));
|
|
112
|
+
const sharding = run.sharding;
|
|
113
|
+
const cutover = cutoverOf(run, o);
|
|
114
|
+
const cut = run.dualWrite.mode === "materialized-view" ? run.dualWrite.cutoverColumn : undefined;
|
|
115
|
+
const before = cut && copied.has(cut) && cutover !== undefined ? `${ident(cut)} < ${utcLiteral(cutover)}` : "1";
|
|
116
|
+
const key = splitTopLevel(t.sorting_key).filter(Boolean);
|
|
117
|
+
const parts = splitTopLevel(t.partition_key).filter(Boolean);
|
|
118
|
+
const partition = parts.length === 0 ? "'all'" : `toString(${parts.length === 1 ? parts[0] : `tuple(${parts.join(", ")})`})`;
|
|
119
|
+
const label = sharding ? `concat('shard', toString(__shard), '/', ${partition})` : partition;
|
|
120
|
+
const hash = `cityHash64(toString(tuple(${[...key, ...semantics.aggregates].join(", ")})))`;
|
|
121
|
+
const read = async (side) => {
|
|
122
|
+
const name = side === "old" ? n.name : n.newName;
|
|
123
|
+
const from = sharding ? shardTable(sharding, n.database, name) : side === "old" ? n.table : n.newTable;
|
|
124
|
+
const projected = o.copied.map((c) => (side === "old" ? `CAST(${ident(c.source)}, ${sqlString(types.get(c.name) ?? "String")}) AS ${ident(c.name)}` : ident(c.name)));
|
|
125
|
+
const rowsOf = `SELECT ${sharding ? "_shard_num AS __shard, " : ""}${projected.join(", ")} FROM ${from}`;
|
|
126
|
+
const perKey = `SELECT ${label} AS __p, ${hash} AS __h, max(${before}) AS __b, count() AS __n, ${semantics.having ?? "1"} AS __k FROM (${rowsOf}) ` +
|
|
127
|
+
`GROUP BY __p${sharding ? ", __shard" : ""}${key.length > 0 ? `, ${key.join(", ")}` : ""}`;
|
|
128
|
+
// A key the engine removes (its sums all zero, its signs cancelled) is not compared, but its rows are counted as read.
|
|
129
|
+
const rows = await clickhouseQuery(run.target.endpoint, `SELECT __p AS p, countIf(__k) AS rows, toString(sumIf(__h, __k)) AS checksum, countIf(__b AND __k) AS brows, toString(sumIf(__h, __b AND __k)) AS bchecksum, sum(__n) AS raw ` +
|
|
130
|
+
`FROM (${perKey}) GROUP BY p`);
|
|
131
|
+
const out = { all: new Map(), before: new Map(), rows: 0 };
|
|
132
|
+
for (const r of rows) {
|
|
133
|
+
out.rows += Number(r.raw);
|
|
134
|
+
// A partition whose keys the engine all removes holds nothing in the new table, as if it had no rows.
|
|
135
|
+
if (Number(r.rows) === 0)
|
|
136
|
+
continue;
|
|
137
|
+
out.all.set(r.p, { partition: r.p, rows: Number(r.rows), checksum: r.checksum });
|
|
138
|
+
if (Number(r.brows) > 0)
|
|
139
|
+
out.before.set(r.p, { partition: r.p, rows: Number(r.brows), checksum: r.bchecksum });
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
};
|
|
143
|
+
return { semantics, read };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Read both tables' counts and checksums per old partition, the old table
|
|
147
|
+
* first, until they agree or `attempts` reads have disagreed. Returns the
|
|
148
|
+
* last read and the partitions that differ in it.
|
|
149
|
+
*/
|
|
150
|
+
async function compareTables(run, o, opts = {}) {
|
|
151
|
+
if (!o.newTable)
|
|
152
|
+
throw new RebuildRefusal(`${o.names.key}: the new table is not there yet; the Create phase makes it`);
|
|
153
|
+
const n = o.names;
|
|
154
|
+
const cutover = cutoverOf(run, o);
|
|
155
|
+
await waitOn(run, n.database, n.name);
|
|
156
|
+
await waitOn(run, n.database, n.newName);
|
|
157
|
+
const newColumns = (await clickhouseQuery(run.target.endpoint, `SELECT name, type, is_in_sorting_key AS key, is_in_partition_key AS partition FROM system.columns ` +
|
|
158
|
+
`WHERE database = ${sqlString(n.database)} AND table = ${sqlString(n.newName)} ORDER BY position`)).map((c) => ({ name: c.name, type: c.type, inSortingKey: Number(c.key) === 1, inPartitionKey: Number(c.partition) === 1 }));
|
|
159
|
+
const types = new Map(newColumns.map((c) => [c.name, c.type]));
|
|
160
|
+
const oldHash = rowHash(o.copied.map((c) => `CAST(${ident(c.source)}, ${sqlString(types.get(c.name) ?? "String")})`));
|
|
161
|
+
const newHash = rowHash(o.copied.map((c) => ident(c.name)));
|
|
162
|
+
const cut = run.dualWrite.mode === "materialized-view" ? o.copied.find((c) => c.name === run.dualWrite.cutoverColumn) : undefined;
|
|
163
|
+
const oldBefore = cut && cutover !== undefined ? `${ident(cut.source)} < ${utcLiteral(cutover)}` : "1";
|
|
164
|
+
const newBefore = cut && cutover !== undefined ? `${ident(cut.name)} < ${utcLiteral(cutover)}` : "1";
|
|
165
|
+
const fromOld = sourcePartitionExpression(o.live.partitionKey, o.newTable.partitionKey, o.copied);
|
|
166
|
+
// Across the shards of a cluster (#3663), every shard's rows, each partition counted per shard as `shard<n>/<partition>`.
|
|
167
|
+
const sharding = run.sharding;
|
|
168
|
+
// Tables whose engines collapse rows by sorting key are read as merged (#3674). Only when both do: a collapsing
|
|
169
|
+
// table rebuilt into a plain MergeTree keeps every row it copies, and is compared row for row.
|
|
170
|
+
const final = collapsesRows(o.live.engine) && collapsesRows(o.newTable.engine);
|
|
171
|
+
// A table that does not collapse rows rebuilt into one that does (#3727): the new table keeps one row per sorting
|
|
172
|
+
// key where the old one keeps them all, by design. Both are grouped by the new sorting key and compared on what
|
|
173
|
+
// the new engine's merges keep per key.
|
|
174
|
+
const keys = !final && collapsesRows(o.newTable.engine) ? await keyComparison(run, o, newColumns) : undefined;
|
|
175
|
+
const read = async (table, name, partition, hash, before, final) => {
|
|
176
|
+
const from = `${sharding ? shardTable(sharding, n.database, name) : table}${final ? " FINAL" : ""}`;
|
|
177
|
+
const key = sharding ? `concat('shard', toString(_shard_num), '/', ${partition})` : partition;
|
|
178
|
+
const rows = await clickhouseQuery(run.target.endpoint, `SELECT ${key} AS p, count() AS rows, toString(sum(${hash})) AS checksum, countIf(${before}) AS brows, toString(sumIf(${hash}, ${before})) AS bchecksum ` +
|
|
179
|
+
`FROM ${from} GROUP BY p`);
|
|
180
|
+
const side = { all: new Map(), before: new Map() };
|
|
181
|
+
for (const r of rows) {
|
|
182
|
+
side.all.set(r.p, { partition: r.p, rows: Number(r.rows), checksum: r.checksum });
|
|
183
|
+
// A partition with no row before the cut-over is left out, as a WHERE would leave it out.
|
|
184
|
+
if (Number(r.brows) > 0)
|
|
185
|
+
side.before.set(r.p, { partition: r.p, rows: Number(r.brows), checksum: r.bchecksum });
|
|
186
|
+
}
|
|
187
|
+
return side;
|
|
188
|
+
};
|
|
189
|
+
const attempts = Math.max(1, opts.attempts ?? COMPARE_ATTEMPTS);
|
|
190
|
+
for (let attempt = 1;; attempt++) {
|
|
191
|
+
// In a Replicated database: count what every replica wrote, not what this one has fetched so far.
|
|
192
|
+
await syncReplica(run, o, n.database, n.name);
|
|
193
|
+
await syncReplica(run, o, n.database, n.newName);
|
|
194
|
+
// The old table first: a row the view has yet to commit in the new one is read as missing there, never the other way round.
|
|
195
|
+
const before = keys ? await keys.read("old") : await read(n.table, n.name, "_partition_id", oldHash, oldBefore, final);
|
|
196
|
+
const after = keys ? await keys.read("new") : await read(n.newTable, n.newName, fromOld, newHash, newBefore, final);
|
|
197
|
+
const mismatches = differences(before.all, after.all);
|
|
198
|
+
if (mismatches.length === 0 || attempt >= attempts) {
|
|
199
|
+
return {
|
|
200
|
+
old: before,
|
|
201
|
+
new: after,
|
|
202
|
+
mismatches,
|
|
203
|
+
...(final ? { collapsed: { engine: o.live.engine, how: "final" } } : {}),
|
|
204
|
+
...(keys ? { collapsed: { engine: o.newTable.engine, how: "keys", describe: keys.semantics.describe } } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
run.log(`-- ${n.key}: ${mismatches.length} partition(s) differ (${mismatches.map((m) => m.partition).slice(0, 5).join(", ")}); reading again (${attempt}/${attempts})`);
|
|
208
|
+
await sleep(opts.intervalMs ?? COMPARE_INTERVAL_MS, run.signal);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function differences(before, after) {
|
|
212
|
+
const out = [];
|
|
213
|
+
for (const p of [...new Set([...before.keys(), ...after.keys()])].sort()) {
|
|
214
|
+
const a = before.get(p);
|
|
215
|
+
const b = after.get(p);
|
|
216
|
+
if (!a || !b || a.rows !== b.rows || a.checksum !== b.checksum)
|
|
217
|
+
out.push({ partition: p, ...(a ? { old: a } : {}), ...(b ? { new: b } : {}) });
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
/** The 64-bit hash of a row's copied columns, as the backfill and the verification compute it. */
|
|
222
|
+
export const rowHash = (columns) => `cityHash64(toString(tuple(${columns.join(", ")})))`;
|
|
223
|
+
/**
|
|
224
|
+
* Before the swap: the new table holds every row the old one does. Throws a
|
|
225
|
+
* {@link RebuildVerificationError} when it does not, so nothing is swapped.
|
|
226
|
+
*/
|
|
227
|
+
export async function assertSameRows(run, o) {
|
|
228
|
+
const { mismatches, collapsed } = await compareTables(run, o);
|
|
229
|
+
if (mismatches.length > 0)
|
|
230
|
+
throw new RebuildVerificationError(o.names.key, mismatches, collapsed);
|
|
231
|
+
}
|
|
232
|
+
export async function verifyRebuild(run) {
|
|
233
|
+
const o = await observe(run);
|
|
234
|
+
if (o.state !== "rebuild")
|
|
235
|
+
return { state: o.state, partitions: 0, rows: 0, verification: [], summary: `nothing to verify: ${o.state}` };
|
|
236
|
+
const cutover = cutoverOf(run, o);
|
|
237
|
+
const compared = await compareTables(run, o);
|
|
238
|
+
if (compared.mismatches.length > 0)
|
|
239
|
+
throw new RebuildVerificationError(o.names.key, compared.mismatches, compared.collapsed);
|
|
240
|
+
const all = [...compared.old.all.values()];
|
|
241
|
+
const counted = all.reduce((s, v) => s + v.rows, 0);
|
|
242
|
+
const c = compared.collapsed;
|
|
243
|
+
// Grouped by sorting key (#3727), each partition counts keys; the old table's rows are still every row read.
|
|
244
|
+
const rows = c?.how === "keys" ? (compared.old.rows ?? counted) : counted;
|
|
245
|
+
// What the approval binds: the rows before the cut-over (every row in app mode), which later writes do not change.
|
|
246
|
+
const verification = [...compared.old.before.values()].sort((x, y) => x.partition.localeCompare(y.partition));
|
|
247
|
+
const bound = verification.reduce((s, v) => s + v.rows, 0);
|
|
248
|
+
const range = cutover !== undefined ? ` (${bound} ${c?.how === "keys" ? "key(s)" : "of them"} before the cut-over at ${new Date(cutover).toISOString()})` : "";
|
|
249
|
+
const counts = c?.how === "keys" ? `${rows} row(s) of the old table as ${counted} key(s)` : `${rows} row(s)`;
|
|
250
|
+
const merged = c?.how === "final"
|
|
251
|
+
? `, read under FINAL (${c.engine})`
|
|
252
|
+
: c?.how === "keys"
|
|
253
|
+
? `, grouped by the sorting key of the new ${c.engine} and compared on ${c.describe}`
|
|
254
|
+
: "";
|
|
255
|
+
const summary = `${o.names.key}: ${all.length} partition(s), ${counts}${range}, counts and checksums equal in the old and new tables${merged}`;
|
|
256
|
+
run.log(`-- ${summary}`);
|
|
257
|
+
return {
|
|
258
|
+
state: o.state,
|
|
259
|
+
planDigest: computePlanDigest("clickhouse-rebuild", {
|
|
260
|
+
plan: rebuildPlanSubject(o, run.dualWrite),
|
|
261
|
+
cutover: cutover === undefined ? null : new Date(cutover).toISOString(),
|
|
262
|
+
verification,
|
|
263
|
+
}),
|
|
264
|
+
partitions: all.length,
|
|
265
|
+
rows,
|
|
266
|
+
summary,
|
|
267
|
+
verification,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function sleep(ms, signal) {
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
signal?.throwIfAborted();
|
|
273
|
+
const t = setTimeout(resolve, ms);
|
|
274
|
+
signal?.addEventListener("abort", () => {
|
|
275
|
+
clearTimeout(t);
|
|
276
|
+
reject(signal.reason);
|
|
277
|
+
}, { once: true });
|
|
278
|
+
});
|
|
279
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a ClickHouse column's value is in TypeScript, for `RowOf` (#3751): the
|
|
3
|
+
* value a row has in ClickHouse's JSON formats (`JSONEachRow`, what the HTTP
|
|
4
|
+
* clients read) with the server's default settings. 64-bit and wider
|
|
5
|
+
* integers are quoted (`output_format_json_quote_64bit_integers`), so they
|
|
6
|
+
* are strings; narrower integers, floats and decimals are numbers; dates,
|
|
7
|
+
* times, UUIDs and addresses are their text; a named tuple is an object
|
|
8
|
+
* (`output_format_json_named_tuples_as_objects`) and an unnamed one an array.
|
|
9
|
+
* `Nullable(T)` adds `| null` and `LowCardinality(T)` is `T`.
|
|
10
|
+
*
|
|
11
|
+
* The table names every type family of the pinned server's catalog
|
|
12
|
+
* (`TYPE_FAMILIES`, aliases resolved), and a test fails on one it does not,
|
|
13
|
+
* so a family a new release adds is a decision rather than a silent
|
|
14
|
+
* `unknown`.
|
|
15
|
+
*/
|
|
16
|
+
import { familyOf } from "../lint/post-synth/clickhouse-type-check.js";
|
|
17
|
+
import { readType, typeTokens } from "./type-syntax.js";
|
|
18
|
+
import { isTrivia } from "./tokens.js";
|
|
19
|
+
const NUMBER = "number";
|
|
20
|
+
const STRING = "string";
|
|
21
|
+
const UNKNOWN = "unknown";
|
|
22
|
+
const POINT = "[number, number]";
|
|
23
|
+
/** A union needs parentheses before `[]`. */
|
|
24
|
+
const element = (ts) => (/[|&]/.test(ts) && !/^\{.*\}$/s.test(ts) ? `(${ts})` : ts);
|
|
25
|
+
/** An enum's labels as a union of string literals; its text when none read. */
|
|
26
|
+
const enumLabels = (params) => {
|
|
27
|
+
const labels = params.flatMap((p) => {
|
|
28
|
+
const first = p.tokens.find((t) => !isTrivia(t));
|
|
29
|
+
return first?.kind === "string" ? [first.text.slice(1, -1).replace(/\\(.)/g, "$1").replace(/''/g, "'")] : [];
|
|
30
|
+
});
|
|
31
|
+
return labels.length > 0 ? labels.map((l) => JSON.stringify(l)).join(" | ") : STRING;
|
|
32
|
+
};
|
|
33
|
+
/** The TypeScript type of each type family's value, or how its parameters make it. */
|
|
34
|
+
export const CLICKHOUSE_ROW_TYPES = {
|
|
35
|
+
UInt8: NUMBER,
|
|
36
|
+
UInt16: NUMBER,
|
|
37
|
+
UInt32: NUMBER,
|
|
38
|
+
Int8: NUMBER,
|
|
39
|
+
Int16: NUMBER,
|
|
40
|
+
Int32: NUMBER,
|
|
41
|
+
Float32: NUMBER,
|
|
42
|
+
Float64: NUMBER,
|
|
43
|
+
BFloat16: NUMBER,
|
|
44
|
+
Decimal: NUMBER,
|
|
45
|
+
Decimal32: NUMBER,
|
|
46
|
+
Decimal64: NUMBER,
|
|
47
|
+
Decimal128: NUMBER,
|
|
48
|
+
Decimal256: NUMBER,
|
|
49
|
+
// Quoted in JSON, so text.
|
|
50
|
+
UInt64: STRING,
|
|
51
|
+
UInt128: STRING,
|
|
52
|
+
UInt256: STRING,
|
|
53
|
+
Int64: STRING,
|
|
54
|
+
Int128: STRING,
|
|
55
|
+
Int256: STRING,
|
|
56
|
+
IntervalNanosecond: STRING,
|
|
57
|
+
IntervalMicrosecond: STRING,
|
|
58
|
+
IntervalMillisecond: STRING,
|
|
59
|
+
IntervalSecond: STRING,
|
|
60
|
+
IntervalMinute: STRING,
|
|
61
|
+
IntervalHour: STRING,
|
|
62
|
+
IntervalDay: STRING,
|
|
63
|
+
IntervalWeek: STRING,
|
|
64
|
+
IntervalMonth: STRING,
|
|
65
|
+
IntervalQuarter: STRING,
|
|
66
|
+
IntervalYear: STRING,
|
|
67
|
+
Bool: "boolean",
|
|
68
|
+
String: STRING,
|
|
69
|
+
FixedString: STRING,
|
|
70
|
+
UUID: STRING,
|
|
71
|
+
IPv4: STRING,
|
|
72
|
+
IPv6: STRING,
|
|
73
|
+
Date: STRING,
|
|
74
|
+
Date32: STRING,
|
|
75
|
+
DateTime: STRING,
|
|
76
|
+
DateTime32: STRING,
|
|
77
|
+
DateTime64: STRING,
|
|
78
|
+
Time: STRING,
|
|
79
|
+
Time64: STRING,
|
|
80
|
+
Enum: enumLabels,
|
|
81
|
+
Enum8: enumLabels,
|
|
82
|
+
Enum16: enumLabels,
|
|
83
|
+
// Wrappers and containers.
|
|
84
|
+
Nullable: (p, inner) => `${inner(p[0]?.text ?? "")} | null`,
|
|
85
|
+
LowCardinality: (p, inner) => inner(p[0]?.text ?? ""),
|
|
86
|
+
Array: (p, inner) => `${element(inner(p[0]?.text ?? ""))}[]`,
|
|
87
|
+
Map: (p, inner) => `Record<string, ${inner(p[1]?.text ?? "")}>`,
|
|
88
|
+
Tuple: (p, inner) => {
|
|
89
|
+
const elements = p.map((x) => named(x));
|
|
90
|
+
if (elements.length > 0 && elements.every((e) => e.name !== undefined)) {
|
|
91
|
+
return `{ ${elements.map((e) => `${propertyName(e.name)}: ${inner(e.type)}`).join("; ")} }`;
|
|
92
|
+
}
|
|
93
|
+
return `[${elements.map((e) => inner(e.type)).join(", ")}]`;
|
|
94
|
+
},
|
|
95
|
+
SimpleAggregateFunction: (p, inner) => inner(p[1]?.text ?? ""),
|
|
96
|
+
AggregateFunction: UNKNOWN,
|
|
97
|
+
Nested: UNKNOWN,
|
|
98
|
+
// Self-describing: the application's own shape.
|
|
99
|
+
JSON: UNKNOWN,
|
|
100
|
+
Dynamic: UNKNOWN,
|
|
101
|
+
Variant: UNKNOWN,
|
|
102
|
+
QBit: UNKNOWN,
|
|
103
|
+
Nothing: "null",
|
|
104
|
+
// Geometry, as the JSON formats write it: points as pairs, nested in arrays.
|
|
105
|
+
Point: POINT,
|
|
106
|
+
Ring: `${POINT}[]`,
|
|
107
|
+
LineString: `${POINT}[]`,
|
|
108
|
+
Polygon: `${POINT}[][]`,
|
|
109
|
+
MultiLineString: `${POINT}[][]`,
|
|
110
|
+
MultiPoint: `${POINT}[]`,
|
|
111
|
+
MultiPolygon: `${POINT}[][][]`,
|
|
112
|
+
Geometry: UNKNOWN,
|
|
113
|
+
};
|
|
114
|
+
/** A tuple element: `a String` is named `a`, `String` is not. */
|
|
115
|
+
function named(param) {
|
|
116
|
+
const syn = readType(param.tokens);
|
|
117
|
+
if (!syn || syn.words.length < 2)
|
|
118
|
+
return { type: param.text };
|
|
119
|
+
if (familyOf(syn.words.map((w) => w.text).join(" ")))
|
|
120
|
+
return { type: param.text };
|
|
121
|
+
const first = syn.words[0];
|
|
122
|
+
const rest = param.text.slice(param.text.indexOf(first.text) + first.text.length).trim();
|
|
123
|
+
return { name: first.kind === "qident" ? first.text.slice(1, -1) : first.text, type: rest };
|
|
124
|
+
}
|
|
125
|
+
const propertyName = (name) => (/^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name));
|
|
126
|
+
/** A type's row type, wrappers and all; `unknown` for a type this cannot read. */
|
|
127
|
+
export function clickhouseTsType(type) {
|
|
128
|
+
const tokens = typeTokens(type);
|
|
129
|
+
const syn = tokens && readType(tokens);
|
|
130
|
+
if (!syn)
|
|
131
|
+
return UNKNOWN;
|
|
132
|
+
const spec = familyOf(syn.words.map((w) => w.text).join(" "));
|
|
133
|
+
const entry = spec ? CLICKHOUSE_ROW_TYPES[spec.canonical] : undefined;
|
|
134
|
+
if (entry === undefined)
|
|
135
|
+
return UNKNOWN;
|
|
136
|
+
return typeof entry === "string" ? entry : entry(syn.params ?? [], clickhouseTsType);
|
|
137
|
+
}
|
|
138
|
+
/** A column's row type and whether it can be NULL: `Nullable(...)` at the top, under any `LowCardinality(...)`. */
|
|
139
|
+
export function clickhouseRowType(type) {
|
|
140
|
+
let t = type.trim();
|
|
141
|
+
let nullable = false;
|
|
142
|
+
for (;;) {
|
|
143
|
+
const m = /^(Nullable|LowCardinality)\s*\(([\s\S]*)\)$/i.exec(t);
|
|
144
|
+
if (!m)
|
|
145
|
+
break;
|
|
146
|
+
if (m[1].toLowerCase() === "nullable")
|
|
147
|
+
nullable = true;
|
|
148
|
+
t = m[2].trim();
|
|
149
|
+
}
|
|
150
|
+
return { ts: clickhouseTsType(t), nullable };
|
|
151
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stand-in for a ClickHouse server's HTTP interface, for tests: answers the
|
|
3
|
+
* catalog queries chant's live reads make from a fixed set of objects.
|
|
4
|
+
* Not a SQL engine.
|
|
5
|
+
*/
|
|
6
|
+
import { startHttpStub } from "../../core/testing/http-stub.js";
|
|
7
|
+
const row = (o) => `${JSON.stringify(o)}\n`;
|
|
8
|
+
const unquote = (s) => s.replace(/^`|`$/g, "").replace(/``/g, "`");
|
|
9
|
+
export async function fakeClickHouse(objects, options = {}) {
|
|
10
|
+
const queries = [];
|
|
11
|
+
const ok = (text) => ({ status: 200, text });
|
|
12
|
+
const stub = await startHttpStub((body) => {
|
|
13
|
+
queries.push(body);
|
|
14
|
+
if (options.fail)
|
|
15
|
+
return { status: options.fail.status, text: options.fail.body };
|
|
16
|
+
if (/version\(\)/.test(body))
|
|
17
|
+
return ok(row({ v: options.version ?? "26.8.15.10" }));
|
|
18
|
+
// A probe for one undeclarable kind (`../live/undeclarable.ts`), which its comment names.
|
|
19
|
+
const probe = /^\/\* chant undeclarable: (.+?) \*\//.exec(body);
|
|
20
|
+
if (probe)
|
|
21
|
+
return ok((options.undeclarable ?? []).filter((u) => u.kind === probe[1]).map((u) => row({ database: u.database ?? "", name: u.name })).join(""));
|
|
22
|
+
if (/FROM system\.databases/.test(body)) {
|
|
23
|
+
return ok(objects
|
|
24
|
+
.filter((o) => o.database === undefined)
|
|
25
|
+
.map((o) => row({ name: o.name, engine: o.engine, uuid: "00000000-0000-0000-0000-000000000000", comment: o.comment ?? "" }))
|
|
26
|
+
.join(""));
|
|
27
|
+
}
|
|
28
|
+
if (/FROM system\.tables/.test(body)) {
|
|
29
|
+
return ok(objects
|
|
30
|
+
.filter((o) => o.database !== undefined)
|
|
31
|
+
.map((o) => row({ database: o.database, name: o.name, engine: o.engine, uuid: "00000000-0000-0000-0000-000000000000", comment: o.comment ?? "" }))
|
|
32
|
+
.join(""));
|
|
33
|
+
}
|
|
34
|
+
if (/FROM system\.functions/.test(body))
|
|
35
|
+
return ok((options.functions ?? []).map((f) => row(f)).join(""));
|
|
36
|
+
const show = /^SHOW CREATE (DATABASE|TABLE) (`(?:[^`]|``)*`)(?:\.(`(?:[^`]|``)*`))?$/.exec(body.trim());
|
|
37
|
+
if (show) {
|
|
38
|
+
const [db, name] = show[3] ? [unquote(show[2]), unquote(show[3])] : [undefined, unquote(show[2])];
|
|
39
|
+
const o = objects.find((x) => x.database === db && x.name === name);
|
|
40
|
+
if (o)
|
|
41
|
+
return ok(row({ statement: o.statement }));
|
|
42
|
+
return { status: 404, text: `Code: 60. DB::Exception: Table ${name} does not exist. (UNKNOWN_TABLE)` };
|
|
43
|
+
}
|
|
44
|
+
return { status: 400, text: `fake ClickHouse cannot answer: ${body.slice(0, 80)}` };
|
|
45
|
+
});
|
|
46
|
+
return { url: stub.url, queries, close: stub.close };
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The schema the round trip tests import, build, and compare: roundtrip.e2e.test.ts against live servers, import/roundtrip.test.ts offline. */
|
|
2
|
+
export const SCHEMA = [
|
|
3
|
+
"CREATE DATABASE analytics ENGINE = Atomic COMMENT 'product analytics'",
|
|
4
|
+
`CREATE TABLE analytics.events (
|
|
5
|
+
user_id UUID,
|
|
6
|
+
kind LowCardinality(String) DEFAULT 'click' COMMENT 'what happened',
|
|
7
|
+
ts DateTime CODEC(Delta, ZSTD(3)),
|
|
8
|
+
props Map(String, String),
|
|
9
|
+
n Nullable(UInt32),
|
|
10
|
+
total UInt64 MATERIALIZED ifNull(n, 0) * 2,
|
|
11
|
+
INDEX by_kind kind TYPE bloom_filter(0.01) GRANULARITY 4,
|
|
12
|
+
PROJECTION per_kind (SELECT kind, count() GROUP BY kind)
|
|
13
|
+
)
|
|
14
|
+
ENGINE = MergeTree
|
|
15
|
+
PARTITION BY toYYYYMM(ts)
|
|
16
|
+
ORDER BY (user_id, kind, ts)
|
|
17
|
+
TTL ts + INTERVAL 180 DAY
|
|
18
|
+
SETTINGS min_bytes_for_wide_part = 0
|
|
19
|
+
COMMENT 'raw events'`,
|
|
20
|
+
"CREATE TABLE analytics.users (id UUID, email String, updated_at DateTime) ENGINE = ReplacingMergeTree(updated_at) ORDER BY id",
|
|
21
|
+
"CREATE TABLE analytics.daily (day Date, kind LowCardinality(String), users AggregateFunction(uniq, UUID)) ENGINE = AggregatingMergeTree ORDER BY (day, kind)",
|
|
22
|
+
"CREATE MATERIALIZED VIEW analytics.daily_mv TO analytics.daily AS SELECT toDate(ts) AS day, kind, uniqState(user_id) AS users FROM analytics.events GROUP BY day, kind",
|
|
23
|
+
"CREATE VIEW analytics.by_kind AS SELECT kind, count() AS n FROM analytics.events GROUP BY kind",
|
|
24
|
+
"CREATE TABLE default.scratch (a UInt8) ENGINE = Log",
|
|
25
|
+
];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stand-in for a ClickHouse server that an apply can write to, for the
|
|
3
|
+
* applier's tests (#3208). It keeps a set of objects, answers the catalog
|
|
4
|
+
* reads observation makes, and takes the writes an apply sends: a `CREATE`
|
|
5
|
+
* adds an object with the statement as its definition, a `DROP` removes one,
|
|
6
|
+
* `MODIFY COMMENT` sets a comment. Every other `ALTER` is recorded and
|
|
7
|
+
* answered, and does not change the definition. Not a SQL engine.
|
|
8
|
+
*/
|
|
9
|
+
import { startHttpStub } from "../../core/testing/http-stub.js";
|
|
10
|
+
import { canonicalObject } from "../plan/normalize.js";
|
|
11
|
+
const row = (o) => `${JSON.stringify(o)}\n`;
|
|
12
|
+
const unquote = (s) => s.replace(/^`|`$/g, "").replace(/``/g, "`");
|
|
13
|
+
const literal = (s) => s.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
14
|
+
function engineOf(statement) {
|
|
15
|
+
const c = canonicalObject(statement);
|
|
16
|
+
if (c.kind === "view")
|
|
17
|
+
return "View";
|
|
18
|
+
if (c.kind === "materializedView")
|
|
19
|
+
return "MaterializedView";
|
|
20
|
+
return c.engineName ?? (c.kind === "database" ? "Atomic" : "MergeTree");
|
|
21
|
+
}
|
|
22
|
+
export async function writableClickHouse(initial = []) {
|
|
23
|
+
const state = {
|
|
24
|
+
url: "",
|
|
25
|
+
objects: initial.map((o) => ({ ...o })),
|
|
26
|
+
writes: [],
|
|
27
|
+
deletes: [],
|
|
28
|
+
mutations: [],
|
|
29
|
+
close: async () => undefined,
|
|
30
|
+
};
|
|
31
|
+
const find = (db, name) => state.objects.find((o) => o.database === db && o.name === name);
|
|
32
|
+
const answer = (body) => {
|
|
33
|
+
const sql = body.trim();
|
|
34
|
+
if (/^SELECT version\(\)/.test(sql))
|
|
35
|
+
return { status: 200, text: row({ v: "26.8.15.10" }) };
|
|
36
|
+
if (/FROM system\.databases/.test(sql)) {
|
|
37
|
+
return {
|
|
38
|
+
status: 200,
|
|
39
|
+
text: state.objects
|
|
40
|
+
.filter((o) => o.database === undefined)
|
|
41
|
+
.map((o) => row({ name: o.name, engine: o.engine, uuid: "00000000-0000-0000-0000-000000000000", comment: o.comment ?? "" }))
|
|
42
|
+
.join(""),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (/^SELECT count\(\) AS n FROM system\.tables WHERE database = '(.*)'$/.test(sql)) {
|
|
46
|
+
const db = literal(/'(.*)'$/.exec(sql)[0]);
|
|
47
|
+
return { status: 200, text: row({ n: state.objects.filter((o) => o.database === db).length }) };
|
|
48
|
+
}
|
|
49
|
+
if (/FROM system\.tables/.test(sql)) {
|
|
50
|
+
return {
|
|
51
|
+
status: 200,
|
|
52
|
+
text: state.objects
|
|
53
|
+
.filter((o) => o.database !== undefined)
|
|
54
|
+
.map((o) => row({ database: o.database, name: o.name, engine: o.engine, uuid: "00000000-0000-0000-0000-000000000000", comment: o.comment ?? "" }))
|
|
55
|
+
.join(""),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// No SQL user-defined functions.
|
|
59
|
+
if (/FROM system\.functions/.test(sql))
|
|
60
|
+
return { status: 200, text: "" };
|
|
61
|
+
if (/FROM system\.mutations/.test(sql)) {
|
|
62
|
+
const m = /database = '([^']*)' AND table = '([^']*)'/.exec(sql);
|
|
63
|
+
return {
|
|
64
|
+
status: 200,
|
|
65
|
+
text: state.mutations
|
|
66
|
+
.filter((x) => m && x.database === m[1] && x.table === m[2])
|
|
67
|
+
.map((x) => row({ mutation_id: x.mutation_id, command: "(MODIFY COLUMN)", latest_fail_reason: x.latest_fail_reason ?? "" }))
|
|
68
|
+
.join(""),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const show = /^SHOW CREATE (DATABASE|TABLE) (`(?:[^`]|``)*`)(?:\.(`(?:[^`]|``)*`))?$/.exec(sql);
|
|
72
|
+
if (show) {
|
|
73
|
+
const [db, name] = show[3] ? [unquote(show[2]), unquote(show[3])] : [undefined, unquote(show[2])];
|
|
74
|
+
const o = find(db, name);
|
|
75
|
+
return o ? { status: 200, text: row({ statement: o.statement }) } : { status: 404, text: `Code: 60. DB::Exception: Table ${name} does not exist. (UNKNOWN_TABLE)` };
|
|
76
|
+
}
|
|
77
|
+
if (/^SELECT formatQuerySingleLine/.test(sql))
|
|
78
|
+
return { status: 400, text: "fake ClickHouse does not format" };
|
|
79
|
+
state.writes.push(sql);
|
|
80
|
+
if (state.refuse?.test(sql))
|
|
81
|
+
return { status: 500, text: `Code: 36. DB::Exception: refused by the test. (BAD_ARGUMENTS)` };
|
|
82
|
+
if (/^(?:--[^\n]*\n\s*)*CREATE\b/i.test(sql)) {
|
|
83
|
+
const c = canonicalObject(sql);
|
|
84
|
+
const existing = find(c.kind === "database" ? undefined : c.database, c.name);
|
|
85
|
+
if (existing && !/^(?:--[^\n]*\n\s*)*CREATE OR REPLACE/i.test(sql))
|
|
86
|
+
return { status: 500, text: `Code: 57. DB::Exception: Table ${c.name} already exists. (TABLE_ALREADY_EXISTS)` };
|
|
87
|
+
if (existing)
|
|
88
|
+
state.objects.splice(state.objects.indexOf(existing), 1);
|
|
89
|
+
state.objects.push({
|
|
90
|
+
...(c.kind === "database" ? {} : { database: c.database }),
|
|
91
|
+
name: c.name,
|
|
92
|
+
engine: engineOf(sql),
|
|
93
|
+
statement: sql.replace(/^CREATE OR REPLACE /i, "CREATE "),
|
|
94
|
+
...(c.comment !== undefined ? { comment: c.comment } : {}),
|
|
95
|
+
});
|
|
96
|
+
return { status: 200, text: "" };
|
|
97
|
+
}
|
|
98
|
+
const drop = /^DROP (DATABASE|TABLE|VIEW) (`(?:[^`]|``)*`)(?:\.(`(?:[^`]|``)*`))?/.exec(sql);
|
|
99
|
+
if (drop) {
|
|
100
|
+
const [db, name] = drop[3] ? [unquote(drop[2]), unquote(drop[3])] : [undefined, unquote(drop[2])];
|
|
101
|
+
state.deletes.push(db ? `${db}.${name}` : name);
|
|
102
|
+
const o = find(db, name);
|
|
103
|
+
if (o)
|
|
104
|
+
state.objects.splice(state.objects.indexOf(o), 1);
|
|
105
|
+
return { status: 200, text: "" };
|
|
106
|
+
}
|
|
107
|
+
const comment = /^ALTER (DATABASE|TABLE) (`(?:[^`]|``)*`)(?:\.(`(?:[^`]|``)*`))? MODIFY COMMENT ('(?:[^'\\]|\\.)*')$/.exec(sql);
|
|
108
|
+
if (comment) {
|
|
109
|
+
const [db, name] = comment[3] ? [unquote(comment[2]), unquote(comment[3])] : [undefined, unquote(comment[2])];
|
|
110
|
+
const o = find(db, name);
|
|
111
|
+
if (o) {
|
|
112
|
+
o.comment = literal(comment[4]);
|
|
113
|
+
o.statement = `${o.statement.replace(/\s+COMMENT '(?:[^'\\]|\\.)*'\s*$/, "")}\nCOMMENT ${comment[4]}`;
|
|
114
|
+
}
|
|
115
|
+
return { status: 200, text: "" };
|
|
116
|
+
}
|
|
117
|
+
return { status: 200, text: "" };
|
|
118
|
+
};
|
|
119
|
+
const stub = await startHttpStub((body) => answer(body));
|
|
120
|
+
state.url = stub.url;
|
|
121
|
+
state.close = stub.close;
|
|
122
|
+
return state;
|
|
123
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickHouse's lexical rules over the shared lossless tokenizer
|
|
3
|
+
* (`../core/tokens.ts`, chant #3196): `` ` `` and `"` quote an identifier, a
|
|
4
|
+
* backslash escapes inside any quote, `( ) , ; . =` are punctuation, and a
|
|
5
|
+
* number may run into letters (`00662_events`, which the parser accepts as a
|
|
6
|
+
* name).
|
|
7
|
+
*/
|
|
8
|
+
import { tokenize as tokenizeWith, tokenizeText as tokenizeTextWith } from "../core/tokens.js";
|
|
9
|
+
export { isTrivia, SqlSyntaxError, untokenize } from "../core/tokens.js";
|
|
10
|
+
export const CLICKHOUSE_LEXICAL = {
|
|
11
|
+
identQuotes: '`"',
|
|
12
|
+
backslashEscapes: true,
|
|
13
|
+
punct: "(),;.=",
|
|
14
|
+
opChars: /[+\-*/%<>!|&^~?:[\]{}@]/,
|
|
15
|
+
};
|
|
16
|
+
/** Tokenize one string with ClickHouse's rules. `part` is the part index recorded on each token and on an error. */
|
|
17
|
+
export function tokenizeText(src, part) {
|
|
18
|
+
return tokenizeTextWith(src, part, CLICKHOUSE_LEXICAL);
|
|
19
|
+
}
|
|
20
|
+
/** Tokenize a template's parts with ClickHouse's rules, with a `ref` token for each interpolation between them. */
|
|
21
|
+
export function tokenize(parts) {
|
|
22
|
+
return tokenizeWith(parts, CLICKHOUSE_LEXICAL);
|
|
23
|
+
}
|