@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,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The change classifier's rules (#3046 question 3): every way a declared
|
|
3
|
+
* ClickHouse object can change, the class of change it is on the server, and
|
|
4
|
+
* the `ALTER` restriction behind the class, cited.
|
|
5
|
+
*
|
|
6
|
+
* Three classes:
|
|
7
|
+
*
|
|
8
|
+
* - `metadata`: the server records the change and touches no existing data
|
|
9
|
+
* (a new column reads its default from old parts; a new index covers new
|
|
10
|
+
* parts until `MATERIALIZE INDEX`).
|
|
11
|
+
* - `rewrite`: the change is a mutation that rewrites existing parts in the
|
|
12
|
+
* background (`system.mutations`), with no rollback.
|
|
13
|
+
* - `rebuild`: ClickHouse cannot make the change to the table it has; the data
|
|
14
|
+
* has to be copied into a new table. A plan refuses to make one in place.
|
|
15
|
+
*
|
|
16
|
+
* Creating and dropping whole objects are classed `create` and `drop`.
|
|
17
|
+
*/
|
|
18
|
+
import { classifierRule } from "../../core/classifier.js";
|
|
19
|
+
/** The classes as data: report order and labels, and what each costs for `classifyDisruption()`. */
|
|
20
|
+
export const CHANGE_CLASSES = {
|
|
21
|
+
order: ["create", "metadata", "rewrite", "rebuild", "drop"],
|
|
22
|
+
label: { create: "create", drop: "drop", metadata: "metadata only", rewrite: "background rewrite", rebuild: "REBUILD" },
|
|
23
|
+
disruption: { metadata: "in-place", rewrite: "rolling", rebuild: "replace", create: "in-place", drop: "destroy" },
|
|
24
|
+
};
|
|
25
|
+
const DOCS = "https://clickhouse.com/docs/sql-reference/statements";
|
|
26
|
+
const rule = (id, cls, title, restriction, cite) => classifierRule(id, cls, title, restriction, cite);
|
|
27
|
+
export const CLASSIFIER_RULES = {
|
|
28
|
+
SQLCH200: rule("SQLCH200", "create", "Create an object", "A new database, table, view, dictionary, function, user, role, row policy, quota or settings profile is created, or a grantee gets its first grants; nothing existing changes.", `${DOCS}/create`),
|
|
29
|
+
SQLCH201: rule("SQLCH201", "metadata", "Add a column", "ADD COLUMN only changes metadata: parts written before it read the column's default until they are merged or the column is materialized.", `${DOCS}/alter/column#add-column`),
|
|
30
|
+
SQLCH202: rule("SQLCH202", "metadata", "Drop a column", "DROP COLUMN removes the column's data from every part; it is not undone. A column in a key cannot be dropped (SQLCH213 covers that case).", `${DOCS}/alter/column#drop-column`),
|
|
31
|
+
SQLCH203: rule("SQLCH203", "metadata", "Change a comment", "COMMENT COLUMN and MODIFY COMMENT change metadata only.", `${DOCS}/alter/comment`),
|
|
32
|
+
SQLCH204: rule("SQLCH204", "metadata", "Add, drop or change a skip index", "ADD INDEX and DROP INDEX change metadata; a new index covers parts written after it until MATERIALIZE INDEX rebuilds it for older parts.", `${DOCS}/alter/skipping-index`),
|
|
33
|
+
SQLCH205: rule("SQLCH205", "rewrite", "Change a TTL", "MODIFY TTL is recorded in metadata, and with materialize_ttl_after_modify (on by default) the server recalculates the TTL over existing data as a mutation.", `${DOCS}/alter/ttl`),
|
|
34
|
+
SQLCH206: rule("SQLCH206", "metadata", "Change a table setting", "MODIFY SETTING and RESET SETTING change metadata, for settings the server lets change after creation.", `${DOCS}/alter/setting`),
|
|
35
|
+
SQLCH207: rule("SQLCH207", "metadata", "Change a column's default", "MODIFY COLUMN with a new DEFAULT, MATERIALIZED or ALIAS expression changes metadata; values already written keep the old default until the column is materialized.", `${DOCS}/alter/column#modify-column`),
|
|
36
|
+
SQLCH208: rule("SQLCH208", "metadata", "Change a column's codec", "MODIFY COLUMN ... CODEC applies to parts written afterwards; existing parts keep their codec until they are merged or rewritten.", `${DOCS}/alter/column#modify-column`),
|
|
37
|
+
SQLCH209: rule("SQLCH209", "metadata", "Move a column", "MODIFY COLUMN ... FIRST | AFTER changes the column order in metadata only.", `${DOCS}/alter/column#modify-column`),
|
|
38
|
+
SQLCH210: rule("SQLCH210", "rewrite", "Change a column's type", "Changing the type of a column outside the primary key is a mutation that rewrites the column in every part in the background, with no rollback.", `${DOCS}/alter/column#modify-column`),
|
|
39
|
+
SQLCH211: rule("SQLCH211", "rebuild", "Change the type of a key column", "The type of a primary key column can change only when the change does not modify the data (for example adding values to an Enum); anything else needs a new table.", `${DOCS}/alter/column#modify-column`),
|
|
40
|
+
SQLCH212: rule("SQLCH212", "metadata", "Rename a column", "RENAME COLUMN changes metadata, for a column that is not in a key.", `${DOCS}/alter/column#rename-column`),
|
|
41
|
+
SQLCH213: rule("SQLCH213", "rebuild", "Rename or drop a key column", "A column in the sorting key, the primary key or the partition key cannot be renamed or dropped.", `${DOCS}/alter/column#rename-column`),
|
|
42
|
+
SQLCH214: rule("SQLCH214", "metadata", "Add, drop or change a projection", "ADD PROJECTION and DROP PROJECTION change metadata; a new projection covers new parts until MATERIALIZE PROJECTION builds it for older ones.", `${DOCS}/alter/projection`),
|
|
43
|
+
SQLCH215: rule("SQLCH215", "metadata", "Add or drop a constraint", "ADD CONSTRAINT and DROP CONSTRAINT change metadata; existing rows are not checked.", `${DOCS}/alter/constraint`),
|
|
44
|
+
SQLCH216: rule("SQLCH216", "metadata", "Append new columns to the sorting key", "MODIFY ORDER BY can only append columns added by ADD COLUMN in the same ALTER, and leaves the primary key as it was; the declaration must keep the old key as PRIMARY KEY.", `${DOCS}/alter/order-by`),
|
|
45
|
+
SQLCH217: rule("SQLCH217", "metadata", "Change the sampling key", "MODIFY SAMPLE BY changes metadata; the new sampling expression must be part of the primary key, and the server refuses it otherwise.", `${DOCS}/alter/sample-by`),
|
|
46
|
+
SQLCH218: rule("SQLCH218", "rebuild", "Change a setting fixed at creation", "A table setting the server marks read-only (such as index_granularity) is fixed when the table is created and needs a new table to change.", `${DOCS}/alter/setting`),
|
|
47
|
+
SQLCH219: rule("SQLCH219", "metadata", "Replace a table over a table function", "A table declared AS a table function (numbers, remote, s3, url) stores no data of its own and has no ALTER for its function; CREATE OR REPLACE TABLE replaces its call and columns, and queries read through the new one.", `${DOCS}/create/table#from-a-table-function`),
|
|
48
|
+
SQLCH220: rule("SQLCH220", "rebuild", "Change the sorting key", "The sorting key is the on-disk order of every part (and in ReplacingMergeTree the deduplication identity); beyond appending new columns (SQLCH216) it cannot change in place.", `${DOCS}/alter/order-by`),
|
|
49
|
+
SQLCH221: rule("SQLCH221", "rebuild", "Change the primary key", "The primary key cannot be changed by ALTER; MODIFY ORDER BY leaves it as it was.", `${DOCS}/alter/order-by`),
|
|
50
|
+
SQLCH222: rule("SQLCH222", "rebuild", "Change the partition key", "There is no ALTER for the partition key; parts are laid out by it when they are written.", `${DOCS}/alter`),
|
|
51
|
+
SQLCH223: rule("SQLCH223", "rebuild", "Change the table engine or its arguments", "There is no ALTER for a table's engine or its arguments (a ReplacingMergeTree version column, a Distributed sharding key); a different engine is a different table.", `${DOCS}/alter`),
|
|
52
|
+
SQLCH224: rule("SQLCH224", "rebuild", "Change what kind of object it is", "A table cannot become a view or a view a table; the old object is dropped and the new one created.", `${DOCS}/create`),
|
|
53
|
+
SQLCH230: rule("SQLCH230", "metadata", "Rename or move a table or view", "RENAME TABLE changes the name, or moves the object to another database, in metadata on an Atomic database.", `${DOCS}/rename`),
|
|
54
|
+
SQLCH231: rule("SQLCH231", "metadata", "Rename a database", "RENAME DATABASE is metadata only, for an Atomic database.", `${DOCS}/rename`),
|
|
55
|
+
SQLCH232: rule("SQLCH232", "rebuild", "Change a database's engine", "A database's engine is fixed at creation; there is no ALTER for it.", `${DOCS}/create/database`),
|
|
56
|
+
SQLCH240: rule("SQLCH240", "metadata", "Change a view's query", "A plain view stores no data; CREATE OR REPLACE VIEW replaces its query.", `${DOCS}/create/view`),
|
|
57
|
+
SQLCH241: rule("SQLCH241", "metadata", "Change a materialized view's query", "ALTER TABLE ... MODIFY QUERY replaces the query without stopping inserts; rows already written are not recomputed.", `${DOCS}/alter/view`),
|
|
58
|
+
SQLCH242: rule("SQLCH242", "rebuild", "Change a materialized view's target", "A materialized view's TO target is fixed at creation; the view is dropped and created again, and inserts into its source in between are not seen.", `${DOCS}/create/view`),
|
|
59
|
+
SQLCH243: rule("SQLCH243", "rebuild", "Change a materialized view's own storage", "A materialized view without TO keeps its rows in an inner table whose engine and keys are fixed like any table's.", `${DOCS}/create/view`),
|
|
60
|
+
SQLCH244: rule("SQLCH244", "metadata", "Change a refreshable view's schedule", "ALTER TABLE ... MODIFY REFRESH changes the schedule of a refreshable materialized view.", `${DOCS}/alter/view`),
|
|
61
|
+
SQLCH245: rule("SQLCH245", "metadata", "Change a dictionary", "A dictionary stores no data of its own: CREATE OR REPLACE DICTIONARY replaces its attributes, key, source, layout, lifetime or range, and it loads again from its source.", `${DOCS}/create/dictionary`),
|
|
62
|
+
SQLCH260: rule("SQLCH260", "metadata", "Change a function", "A SQL user-defined function stores no data: CREATE OR REPLACE FUNCTION replaces its parameters and expression, and queries that call it use the new one.", `${DOCS}/create/function`),
|
|
63
|
+
SQLCH270: rule("SQLCH270", "metadata", "Change a role", "ALTER ROLE ... SETTINGS replaces a role's settings; its grants are untouched.", `${DOCS}/alter/role`),
|
|
64
|
+
SQLCH271: rule("SQLCH271", "metadata", "Change a user", "ALTER USER sets one clause (HOST, DEFAULT ROLE, DEFAULT DATABASE, GRANTEES, SETTINGS, VALID UNTIL, IDENTIFIED) and leaves the rest, the password and the grants untouched. Sessions already open keep the old settings.", `${DOCS}/alter/user`),
|
|
65
|
+
SQLCH272: rule("SQLCH272", "metadata", "Change a row policy", "CREATE ROW POLICY OR REPLACE replaces the policy's condition, kind and roles; queries started after it see the new one.", `${DOCS}/create/row-policy`),
|
|
66
|
+
SQLCH273: rule("SQLCH273", "metadata", "Grant a privilege or a role", "GRANT adds a privilege or a role to a user or role; it takes effect for new queries.", `${DOCS}/grant`),
|
|
67
|
+
SQLCH274: rule("SQLCH274", "metadata", "Revoke a privilege or a role", "REVOKE takes a privilege or a role away, including one granted by hand to a grantee the build declares grants for; queries that relied on it fail from then on.", `${DOCS}/revoke`),
|
|
68
|
+
SQLCH275: rule("SQLCH275", "metadata", "Change a quota", "ALTER QUOTA sets the quota's key, one interval's limits (NO LIMITS takes the interval away) or whom it applies to, on the quota that is there, so what names it still does.", `${DOCS}/alter/quota`),
|
|
69
|
+
SQLCH276: rule("SQLCH276", "metadata", "Change a settings profile", "ALTER SETTINGS PROFILE replaces the profile's settings, constraints and inherited profiles, or whom it applies to; the users, roles and profiles that name it keep it. Sessions already open keep the old settings.", `${DOCS}/alter/settings-profile`),
|
|
70
|
+
SQLCH290: rule("SQLCH290", "rebuild", "Change a clause chant does not read", "chant's parser does not know this clause, so it cannot tell whether ALTER can make the change in place; the plan refuses it rather than guess. Review it: make it by hand with the ALTER the server documents for the clause, or drop and create the object.", `${DOCS}/create/table`),
|
|
71
|
+
SQLCH250: rule("SQLCH250", "drop", "Drop an object", "DROP removes the object and, for a table, its data; it is not undone.", `${DOCS}/drop`),
|
|
72
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two sides a diff compares: a build's output (the declarations), or what
|
|
3
|
+
* a live server reports.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { canonicalObject, grantsObject, liveCanonical, objectKey, scopeOf } from "./normalize.js";
|
|
7
|
+
import { grantsByGrantee } from "../access.js";
|
|
8
|
+
import { readLiveAccess, readLiveSchema } from "../live/catalog.js";
|
|
9
|
+
import { renderFor } from "../topology.js";
|
|
10
|
+
/** The objects a `chant build` output holds, keyed by export name; with a `topology`, each declaration as rendered for it (`../topology.ts`). */
|
|
11
|
+
export function schemaFromBuildOutput(json, defaultDatabase = "default", topology) {
|
|
12
|
+
const doc = JSON.parse(json);
|
|
13
|
+
if (doc.dialect !== "clickhouse" || !Array.isArray(doc.objects)) {
|
|
14
|
+
throw new Error("not a sql lexicon build output: expected { dialect: \"clickhouse\", objects: [...] }");
|
|
15
|
+
}
|
|
16
|
+
const out = doc.objects
|
|
17
|
+
.filter((o) => o.type !== "ClickHouse::Grant")
|
|
18
|
+
.map((o) => ({ key: o.export, canonical: canonicalObject(renderFor(o.ddl, topology), defaultDatabase) }));
|
|
19
|
+
// Grants are compared per grantee (`../access.ts`).
|
|
20
|
+
for (const g of grantsByGrantee(doc.objects.map((o) => ({ ...o, type: o.type ?? "" })))) {
|
|
21
|
+
const canonical = grantsObject(g.ddl, g.grantee, defaultDatabase);
|
|
22
|
+
out.push({ key: objectKey(canonical), canonical });
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
export function schemaFromBuildFile(path, defaultDatabase = "default", topology) {
|
|
27
|
+
return schemaFromBuildOutput(readFileSync(path, "utf-8"), defaultDatabase, topology);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The declared objects re-keyed by `database.name`, the only identity a live
|
|
31
|
+
* server has. The export name is kept for the report.
|
|
32
|
+
*/
|
|
33
|
+
export function keyedByQualifiedName(objects) {
|
|
34
|
+
return objects.map((o) => ({
|
|
35
|
+
key: objectKey(o.canonical),
|
|
36
|
+
canonical: o.canonical,
|
|
37
|
+
exportName: o.key,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What the server holds, keyed by `database.name`, limited to the databases
|
|
42
|
+
* the declarations use (and the target's scope) and the functions they
|
|
43
|
+
* declare (`scopeOf`). The `default` database is
|
|
44
|
+
* left out, as import leaves it out. An object whose definition chant cannot
|
|
45
|
+
* read is not left out silently (#3653): it goes in `unreadable`, when
|
|
46
|
+
* given, and otherwise fails the read.
|
|
47
|
+
*/
|
|
48
|
+
export async function schemaFromServer(target, databases, unreadable, access = []) {
|
|
49
|
+
const live = [...(await readLiveSchema(target)), ...(await readLiveAccess(target, access))];
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const o of live) {
|
|
52
|
+
if (o.type === "ClickHouse::Database" && o.name === "default")
|
|
53
|
+
continue;
|
|
54
|
+
if (databases && !databases.has(scopeOf(o)))
|
|
55
|
+
continue;
|
|
56
|
+
const key = objectKey(o);
|
|
57
|
+
try {
|
|
58
|
+
out.push({ key, canonical: liveCanonical(o, target.defaultDatabase) });
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (!unreadable)
|
|
62
|
+
throw err;
|
|
63
|
+
unreadable.push({ object: key, type: o.type, reason: `its definition does not parse: ${err.message}` });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backstop for normalization against a live server: for each changed
|
|
3
|
+
* expression the rules leave different, ask the server to format both sides
|
|
4
|
+
* (`formatQuerySingleLine`, which parses and prints and touches nothing) and
|
|
5
|
+
* drop the change when they print the same. `a+b*2` and `a + (b * 2)` are one
|
|
6
|
+
* expression to the server.
|
|
7
|
+
*/
|
|
8
|
+
import { clickhouseQuery } from "../http.js";
|
|
9
|
+
const EXPRESSION_FIELDS = /^(orderBy|primaryKey|partitionBy|sampleBy|select|lambda|using|columns\.[^.]+\.(default|ttl)|indexes\..+|projections\..+)$/;
|
|
10
|
+
const quote = (s) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
11
|
+
function asQuery(field, text) {
|
|
12
|
+
if (field === "select")
|
|
13
|
+
return text;
|
|
14
|
+
if (field.endsWith(".default"))
|
|
15
|
+
return `SELECT ${text.replace(/^(DEFAULT|MATERIALIZED|ALIAS|EPHEMERAL) /, "")}`;
|
|
16
|
+
return `SELECT ${text}`;
|
|
17
|
+
}
|
|
18
|
+
/** The changes that remain once the server's formatter has had its say. */
|
|
19
|
+
export async function dropFormattingOnly(endpoint, changes) {
|
|
20
|
+
const candidates = changes.filter((c) => EXPRESSION_FIELDS.test(c.field) && c.before !== undefined && c.after !== undefined);
|
|
21
|
+
if (candidates.length === 0)
|
|
22
|
+
return changes;
|
|
23
|
+
const same = new Set();
|
|
24
|
+
for (const c of candidates) {
|
|
25
|
+
try {
|
|
26
|
+
const [row] = await clickhouseQuery(endpoint, `SELECT formatQuerySingleLine(${quote(asQuery(c.field, c.before))}) AS a, formatQuerySingleLine(${quote(asQuery(c.field, c.after))}) AS b`);
|
|
27
|
+
if (row && row.a === row.b)
|
|
28
|
+
same.add(c);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// The server could not parse one side as a query: the rules' answer stands.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return changes.filter((c) => !same.has(c));
|
|
35
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backfill (#3198): copy the old table into the new one with
|
|
3
|
+
* `INSERT ... SELECT`, one partition at a time, each partition an effect
|
|
4
|
+
* with its own receipt.
|
|
5
|
+
*
|
|
6
|
+
* Per partition, the cycle core's `effect()` step runs
|
|
7
|
+
* (concepts/effect-receipts.mdx): read the receipt, skip the copy when it
|
|
8
|
+
* matches, otherwise copy and write the receipt last, on success only.
|
|
9
|
+
* `effect()` itself wraps steps declared at build time around one receipt
|
|
10
|
+
* declared at build time; the partitions are only known when the run reads
|
|
11
|
+
* the old table, so the cycle runs here, once per partition, over the same
|
|
12
|
+
* `ReceiptStore` seam (`./receipts.ts`).
|
|
13
|
+
*
|
|
14
|
+
* A receipt's expectation is a hash of what the copy is: the table, the new
|
|
15
|
+
* table's UUID, the partition, the columns copied and the cut-over. A new
|
|
16
|
+
* table made again, or a new cut-over, makes every receipt stale and the
|
|
17
|
+
* partitions are copied again into the new table.
|
|
18
|
+
*
|
|
19
|
+
* At-least-once is the receipt model's guarantee, and a partition copied
|
|
20
|
+
* twice would be in the new table twice, so a copy that may have run before
|
|
21
|
+
* is undone first. A partition with no matching receipt is a partition whose
|
|
22
|
+
* copy never finished as far as anything recorded; if the new table holds
|
|
23
|
+
* any of its rows (a run killed between the INSERT and the receipt), they are
|
|
24
|
+
* deleted, the delete is waited on in `system.mutations`, and the partition
|
|
25
|
+
* is copied once more. Each copy runs under a query id derived from the new
|
|
26
|
+
* table and the partition, and any query still running under it (the server
|
|
27
|
+
* goes on with an INSERT whose client went away) is killed before the
|
|
28
|
+
* partition is cleared. A backfill interrupted halfway therefore resumes at
|
|
29
|
+
* the first partition without a receipt, and no row is copied twice.
|
|
30
|
+
*
|
|
31
|
+
* In a Replicated database (#3249) a resumed backfill may run on another
|
|
32
|
+
* replica than the one the copy ran on. The kill is `KILL QUERY ON CLUSTER`
|
|
33
|
+
* the database's own cluster, so it reaches the replica still running the
|
|
34
|
+
* copy; before the old table's partitions are listed, and before the new
|
|
35
|
+
* table's rows are counted, this replica fetches what the others wrote
|
|
36
|
+
* (`SYSTEM SYNC REPLICA`); and the receipts replicate with the tables
|
|
37
|
+
* (`./receipts.ts`). Each copy runs with `insert_deduplicate = 0`: a
|
|
38
|
+
* replicated table remembers the blocks it was given, and a partition copied
|
|
39
|
+
* again after its rows were cleared would otherwise be dropped as a repeat.
|
|
40
|
+
*
|
|
41
|
+
* With a replica down (#3270) the kill goes on without it, and each sync
|
|
42
|
+
* either finds nothing that replica alone has and returns, or stops the step
|
|
43
|
+
* naming it (`./replicas.ts`).
|
|
44
|
+
*
|
|
45
|
+
* On a cluster of more than one shard (#3663) the unit is a partition on one
|
|
46
|
+
* shard: read from that shard, written back to it, cleared on it alone, with
|
|
47
|
+
* its own receipt (`./shards.ts`), so a rerun after a failure on one shard
|
|
48
|
+
* skips what every shard already copied.
|
|
49
|
+
*
|
|
50
|
+
* In materialized-view mode a partition is copied in two parts
|
|
51
|
+
* (INTENTIUS/sql-yodeler#45). The rows before the cut-over are copied as
|
|
52
|
+
* above. The rows at or after it are the view's only when they were written
|
|
53
|
+
* after the view was made; the ones the old table already held (a booking
|
|
54
|
+
* next month, an expiry date) reach the new table only through the backfill.
|
|
55
|
+
* The two kinds cannot be told apart by their values, so the second part
|
|
56
|
+
* copies the difference: for each distinct row at or after the cut-over, as
|
|
57
|
+
* many copies as the old table has more of it than the new one. That copy
|
|
58
|
+
* converges on its own, so a run killed during it, or a partition copied
|
|
59
|
+
* again, copies only what is still missing. A row the view is a moment from
|
|
60
|
+
* committing can be copied by both: the copy is followed by a check for rows
|
|
61
|
+
* the new table holds more often than the old one, which deletes and copies
|
|
62
|
+
* those again. Whatever is left, the verification finds, and the swap does
|
|
63
|
+
* not go ahead.
|
|
64
|
+
*/
|
|
65
|
+
import { EffectReceipt, receiptExpectation } from "@intentius/chant/effect-receipt";
|
|
66
|
+
import { clickhouseQuery } from "../http.js";
|
|
67
|
+
import { ident, sqlString } from "../apply/statements.js";
|
|
68
|
+
import { clickhouseReceiptStore, receiptAddress } from "./receipts.js";
|
|
69
|
+
import { RebuildRefusal } from "./observe.js";
|
|
70
|
+
import { DDL_SETTINGS } from "./replicas.js";
|
|
71
|
+
import { shardPartitions, sourcePartitionExpression, sourcePartitions } from "./partitions.js";
|
|
72
|
+
import { rowHash } from "./verify.js";
|
|
73
|
+
import { cutoverOf, observe, serverCutoverProbe, syncReplica, utcLiteral, waitForCutover, waitOn } from "./steps.js";
|
|
74
|
+
import { intoShard, onShard, onShardServers, shardTable, SHARD_DELETE_SETTINGS, SHARD_INSERT_SETTINGS } from "./shards.js";
|
|
75
|
+
import { renderFor } from "../topology.js";
|
|
76
|
+
/** The effect a partition's copy is, and its receipt's address suffix. */
|
|
77
|
+
export const partitionEffect = (key, partition, shard) => (shard ? `rebuild/${key}/shard${shard.num}/${partition}` : `rebuild/${key}/${partition}`);
|
|
78
|
+
/** The receipt's expected value for one partition's copy, on one shard of a cluster when `shard` is given. */
|
|
79
|
+
export function partitionExpectation(o, partition, cutover, shard) {
|
|
80
|
+
const effect = partitionEffect(o.names.key, partition, shard);
|
|
81
|
+
return receiptExpectation(EffectReceipt(effect, {
|
|
82
|
+
effect,
|
|
83
|
+
flavor: "hash",
|
|
84
|
+
inputs: {
|
|
85
|
+
table: o.names.key,
|
|
86
|
+
newTable: o.newTable.uuid,
|
|
87
|
+
partition,
|
|
88
|
+
...(shard ? { shard: shard.num } : {}),
|
|
89
|
+
copied: o.copied,
|
|
90
|
+
cutover: cutover === undefined ? null : new Date(cutover).toISOString(),
|
|
91
|
+
// The rows at or after the cut-over the old table held are copied too (sql-yodeler#45); a receipt from before that copied fewer.
|
|
92
|
+
...(cutover === undefined ? {} : { after: "difference" }),
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
export async function backfill(run, deps = {}) {
|
|
97
|
+
const o = await observe(run);
|
|
98
|
+
const result = { state: o.state, partitions: 0, copied: 0, skipped: 0, cleared: 0, repaired: 0 };
|
|
99
|
+
if (o.state !== "rebuild")
|
|
100
|
+
return result;
|
|
101
|
+
if (!o.newTable)
|
|
102
|
+
throw new RebuildRefusal(`${o.names.key}: the new table is not there yet; the Create phase makes it`);
|
|
103
|
+
const n = o.names;
|
|
104
|
+
const cutover = cutoverOf(run, o);
|
|
105
|
+
// Rows before the cut-over are the backfill's; wait until none of them is still to arrive.
|
|
106
|
+
if (cutover !== undefined)
|
|
107
|
+
await waitForCutover(run, cutover, deps.cutoverProbe ?? serverCutoverProbe(run, o), run.cutoverTimeoutMs !== undefined ? { timeoutMs: run.cutoverTimeoutMs } : {});
|
|
108
|
+
await waitOn(run, n.database, n.name);
|
|
109
|
+
await syncReplica(run, o, n.database, n.name);
|
|
110
|
+
const cutColumn = run.dualWrite.mode === "materialized-view" ? o.copied.find((c) => c.name === run.dualWrite.cutoverColumn) : undefined;
|
|
111
|
+
const sourceRange = cutColumn && cutover !== undefined ? ` AND ${ident(cutColumn.source)} < ${utcLiteral(cutover)}` : "";
|
|
112
|
+
const targetRange = cutColumn && cutover !== undefined ? ` AND ${ident(cutColumn.name)} < ${utcLiteral(cutover)}` : "";
|
|
113
|
+
const fromOld = sourcePartitionExpression(o.live.partitionKey, o.newTable.partitionKey, o.copied);
|
|
114
|
+
const columns = o.copied.map((c) => ident(c.name)).join(", ");
|
|
115
|
+
const select = o.copied.map((c) => ident(c.source)).join(", ");
|
|
116
|
+
const later = cutColumn && cutover !== undefined ? await laterRowsCopy(run, o, cutColumn, cutover, fromOld) : undefined;
|
|
117
|
+
const sharding = run.sharding;
|
|
118
|
+
// The old and new tables as one shard's rows (#3663, `./shards.ts`), or as this server's.
|
|
119
|
+
const oldFrom = (shard) => (shard && sharding ? `${shardTable(sharding, n.database, n.name)} WHERE ${onShard(shard)} AND` : `${n.table} WHERE`);
|
|
120
|
+
const newFrom = (shard) => (shard && sharding ? `${shardTable(sharding, n.database, n.newName)} WHERE ${onShard(shard)} AND` : `${n.newTable} WHERE`);
|
|
121
|
+
const into = (shard) => (shard && sharding ? intoShard(sharding, shard, n.database, n.newName) : n.newTable);
|
|
122
|
+
const insertSettings = sharding ? SHARD_INSERT_SETTINGS : {};
|
|
123
|
+
const identity = { ...(run.marker?.stack ? { stack: run.marker.stack } : {}), ...(run.marker?.env ? { env: run.marker.env } : {}) };
|
|
124
|
+
const receipts = deps.receipts ??
|
|
125
|
+
clickhouseReceiptStore(run.target.endpoint, identity, {
|
|
126
|
+
...(run.runId ? { runId: run.runId } : {}),
|
|
127
|
+
...(o.replicated ? { replicatedIn: n.database } : {}),
|
|
128
|
+
...(run.target.topology ? { topology: run.target.topology } : {}),
|
|
129
|
+
...(run.replicaTimeoutMs !== undefined ? { replicaTimeoutMs: run.replicaTimeoutMs } : {}),
|
|
130
|
+
});
|
|
131
|
+
const recorded = await receipts.readAll(receiptAddress(identity, `rebuild/${n.key}/`));
|
|
132
|
+
// The unit of work: a partition of the old table, on each shard of a cluster that has more than one.
|
|
133
|
+
const units = sharding
|
|
134
|
+
? (await shardPartitions(run, sharding, n.database, n.name)).map((u) => ({ shard: sharding.shards.find((x) => x.num === u.shard), partition: u.partition }))
|
|
135
|
+
: (await sourcePartitions(run.target, n)).map((partition) => ({ partition }));
|
|
136
|
+
result.partitions = units.length;
|
|
137
|
+
for (const { shard, partition: p } of units) {
|
|
138
|
+
run.signal?.throwIfAborted();
|
|
139
|
+
const effect = partitionEffect(n.key, p, shard);
|
|
140
|
+
const expectation = partitionExpectation(o, p, cutover, shard);
|
|
141
|
+
if (recorded.get(receiptAddress(identity, effect)) === expectation) {
|
|
142
|
+
result.skipped++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const queryId = `chant-rebuild-${o.newTable.uuid}-${shard ? `shard${shard.num}-` : ""}${p}`;
|
|
146
|
+
const laterQueryId = `${queryId}-after`;
|
|
147
|
+
const killWhere = `query_id IN (${sqlString(queryId)}, ${sqlString(laterQueryId)})`;
|
|
148
|
+
if (sharding) {
|
|
149
|
+
// The copy reads and writes every shard from this server; what it started on the others goes with it.
|
|
150
|
+
await clickhouseQuery(run.target.endpoint, `KILL QUERY ON CLUSTER ${sqlString(sharding.cluster)} WHERE ${killWhere} OR initial_query_id IN (${sqlString(queryId)}, ${sqlString(laterQueryId)}) SYNC`, { settings: DDL_SETTINGS });
|
|
151
|
+
}
|
|
152
|
+
else if (o.replicated) {
|
|
153
|
+
// On every replica that answers: the copy may be running on another one
|
|
154
|
+
// than this run's. A replica that is down is skipped once Keeper sees it
|
|
155
|
+
// inactive; nothing of its can be running a copy that still writes,
|
|
156
|
+
// and a part it wrote before going down is waited for by the sync below.
|
|
157
|
+
await clickhouseQuery(run.target.endpoint, `KILL QUERY ON CLUSTER ${sqlString(n.database)} WHERE ${killWhere} SYNC`, {
|
|
158
|
+
settings: DDL_SETTINGS,
|
|
159
|
+
});
|
|
160
|
+
await syncReplica(run, o, n.database, n.newName);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await clickhouseQuery(run.target.endpoint, `KILL QUERY WHERE ${killWhere} SYNC`);
|
|
164
|
+
}
|
|
165
|
+
const mine = `${fromOld} = ${sqlString(p)}${targetRange}`;
|
|
166
|
+
const where = shard ? `partition ${p} on shard ${shard.num}` : `partition ${p}`;
|
|
167
|
+
const [left] = await clickhouseQuery(run.target.endpoint, `SELECT count() AS n FROM ${newFrom(shard)} ${mine}`);
|
|
168
|
+
if (Number(left?.n ?? 0) > 0) {
|
|
169
|
+
run.log(`-- ${where}: ${left.n} row(s) from a copy that did not finish; deleting them before copying again`);
|
|
170
|
+
const sql = shard && sharding
|
|
171
|
+
? renderFor(`ALTER TABLE ${n.newTable} DELETE WHERE ${onShardServers(shard)} AND ${mine}`, run.target.topology)
|
|
172
|
+
: `ALTER TABLE ${n.newTable} DELETE WHERE ${mine}`;
|
|
173
|
+
run.log(sql);
|
|
174
|
+
await clickhouseQuery(run.target.endpoint, sql, shard && sharding ? { settings: { ...DDL_SETTINGS, ...SHARD_DELETE_SETTINGS } } : {});
|
|
175
|
+
await waitOn(run, n.database, n.newName);
|
|
176
|
+
result.cleared++;
|
|
177
|
+
}
|
|
178
|
+
const sql = `INSERT INTO ${into(shard)} (${columns}) SELECT ${select} FROM ${oldFrom(shard)} _partition_id = ${sqlString(p)}${sourceRange}`;
|
|
179
|
+
run.log(sql);
|
|
180
|
+
await clickhouseQuery(run.target.endpoint, sql, { queryId, settings: { async_insert: "0", insert_deduplicate: "0", ...insertSettings }, ...(run.signal ? { signal: run.signal } : {}) });
|
|
181
|
+
if (later)
|
|
182
|
+
result.repaired += await copyAfterCutover(run, o, later, p, laterQueryId, shard);
|
|
183
|
+
// The receipt, last, on success only.
|
|
184
|
+
await receipts.write({ name: effect, effect, flavor: "hash", inputs: {} }, expectation);
|
|
185
|
+
result.copied++;
|
|
186
|
+
await deps.afterPartition?.(p, shard?.num);
|
|
187
|
+
}
|
|
188
|
+
run.log(`-- backfill of ${n.key}: ${result.partitions} partition(s), ${result.copied} copied, ${result.skipped} already copied, ${result.cleared} cleared first${result.repaired > 0 ? `, ${result.repaired} row(s) after the cut-over copied twice and repaired` : ""}`);
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
/** The most extra rows one partition's repair deletes and copies again; past that the verification decides. */
|
|
192
|
+
const REPAIR_LIMIT = 1000;
|
|
193
|
+
const REPAIR_ROUNDS = 3;
|
|
194
|
+
/**
|
|
195
|
+
* The statements for the rows at or after the cut-over. The old side casts
|
|
196
|
+
* each column to its type in the new table before hashing, as the
|
|
197
|
+
* verification does, so a row and its copy hash the same.
|
|
198
|
+
*/
|
|
199
|
+
async function laterRowsCopy(run, o, cut, cutover, fromOld) {
|
|
200
|
+
const n = o.names;
|
|
201
|
+
const types = new Map((await clickhouseQuery(run.target.endpoint, `SELECT name, type FROM system.columns WHERE database = ${sqlString(n.database)} AND table = ${sqlString(n.newName)}`)).map((c) => [c.name, c.type]));
|
|
202
|
+
const oldHash = rowHash(o.copied.map((c) => `CAST(${ident(c.source)}, ${sqlString(types.get(c.name) ?? "String")})`));
|
|
203
|
+
const newHash = rowHash(o.copied.map((c) => ident(c.name)));
|
|
204
|
+
const columns = o.copied.map((c) => ident(c.name)).join(", ");
|
|
205
|
+
const aliases = o.copied.map((_, i) => `__chant_c${i}`);
|
|
206
|
+
const fromOldSide = o.copied.map((c, i) => `${ident(c.source)} AS ${aliases[i]}`).join(", ");
|
|
207
|
+
const sharding = run.sharding;
|
|
208
|
+
const on = (shard) => (shard && sharding ? ` AND ${onShard(shard)}` : "");
|
|
209
|
+
const oldTable = (shard) => (shard && sharding ? shardTable(sharding, n.database, n.name) : n.table);
|
|
210
|
+
const newTable = (shard) => (shard && sharding ? shardTable(sharding, n.database, n.newName) : n.newTable);
|
|
211
|
+
const oldRows = (p, shard) => `FROM ${oldTable(shard)} WHERE _partition_id = ${sqlString(p)} AND ${ident(cut.source)} >= ${utcLiteral(cutover)}${on(shard)}`;
|
|
212
|
+
const newRows = (p, shard) => `FROM ${newTable(shard)} WHERE ${fromOld} = ${sqlString(p)} AND ${ident(cut.name)} >= ${utcLiteral(cutover)}${on(shard)}`;
|
|
213
|
+
return {
|
|
214
|
+
copy: (p, shard) => `INSERT INTO ${shard && sharding ? intoShard(sharding, shard, n.database, n.newName) : n.newTable} (${columns}) SELECT ${aliases.join(", ")} FROM (` +
|
|
215
|
+
`SELECT ${fromOldSide}, ${oldHash} AS __chant_h, row_number() OVER (PARTITION BY __chant_h) AS __chant_k ${oldRows(p, shard)}` +
|
|
216
|
+
`) AS old_rows LEFT JOIN (SELECT ${newHash} AS __chant_h, count() AS __chant_n ${newRows(p, shard)} GROUP BY __chant_h) AS new_rows ` +
|
|
217
|
+
`USING (__chant_h) WHERE __chant_k > ifNull(__chant_n, 0)`,
|
|
218
|
+
extras: (p, shard) => `SELECT toString(__chant_h) AS h FROM (SELECT ${newHash} AS __chant_h, count() AS __chant_n ${newRows(p, shard)} GROUP BY __chant_h) AS new_rows ` +
|
|
219
|
+
`LEFT JOIN (SELECT ${oldHash} AS __chant_h, count() AS __chant_o ${oldRows(p, shard)} GROUP BY __chant_h) AS old_rows ` +
|
|
220
|
+
`USING (__chant_h) WHERE __chant_n > ifNull(__chant_o, 0) LIMIT ${REPAIR_LIMIT + 1}`,
|
|
221
|
+
drop: (p, hashes, shard) => {
|
|
222
|
+
const where = `${fromOld} = ${sqlString(p)} AND ${ident(cut.name)} >= ${utcLiteral(cutover)} AND ${newHash} IN (${hashes.join(", ")})`;
|
|
223
|
+
return shard && sharding
|
|
224
|
+
? renderFor(`ALTER TABLE ${n.newTable} DELETE WHERE ${onShardServers(shard)} AND ${where}`, run.target.topology)
|
|
225
|
+
: `ALTER TABLE ${n.newTable} DELETE WHERE ${where}`;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Copy one old partition's rows at or after the cut-over that the new table
|
|
231
|
+
* lacks. A row the view had written to the old table but not yet to the new
|
|
232
|
+
* one when the copy read them is copied by both, and is then in the new
|
|
233
|
+
* table once too often: every copy of such a row is deleted from the new
|
|
234
|
+
* table and the difference copied again, a few rounds at most. What is left
|
|
235
|
+
* after that, the verification finds. Returns the rows repaired.
|
|
236
|
+
*/
|
|
237
|
+
async function copyAfterCutover(run, o, later, p, queryId, shard) {
|
|
238
|
+
const n = o.names;
|
|
239
|
+
const sharded = shard !== undefined && run.sharding !== undefined;
|
|
240
|
+
const insert = async () => {
|
|
241
|
+
const sql = later.copy(p, shard);
|
|
242
|
+
run.log(sql);
|
|
243
|
+
await clickhouseQuery(run.target.endpoint, sql, {
|
|
244
|
+
queryId,
|
|
245
|
+
settings: { async_insert: "0", insert_deduplicate: "0", ...(sharded ? SHARD_INSERT_SETTINGS : {}) },
|
|
246
|
+
...(run.signal ? { signal: run.signal } : {}),
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
await insert();
|
|
250
|
+
let repaired = 0;
|
|
251
|
+
for (let round = 0; round < REPAIR_ROUNDS; round++) {
|
|
252
|
+
await syncReplica(run, o, n.database, n.newName);
|
|
253
|
+
const extras = (await clickhouseQuery(run.target.endpoint, later.extras(p, shard))).map((r) => r.h);
|
|
254
|
+
if (extras.length === 0 || extras.length > REPAIR_LIMIT)
|
|
255
|
+
break;
|
|
256
|
+
run.log(`-- partition ${p}: ${extras.length} row(s) after the cut-over in the new table more often than in the old one; deleting them and copying again`);
|
|
257
|
+
const sql = later.drop(p, extras, shard);
|
|
258
|
+
run.log(sql);
|
|
259
|
+
await clickhouseQuery(run.target.endpoint, sql, sharded ? { settings: { ...DDL_SETTINGS, ...SHARD_DELETE_SETTINGS } } : {});
|
|
260
|
+
await waitOn(run, n.database, n.newName);
|
|
261
|
+
repaired += extras.length;
|
|
262
|
+
await insert();
|
|
263
|
+
}
|
|
264
|
+
return repaired;
|
|
265
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which table engines the rebuild reads as merged (#3674): the verification
|
|
3
|
+
* (`./verify.ts`) and the plan's hand-off note (`../plan/rebuild-handoff.ts`).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The MergeTree engines that collapse rows sharing a sorting key when parts
|
|
7
|
+
* merge, as `system.tables.engine` names them, replicated or shared too. A
|
|
8
|
+
* count of such a table depends on which parts have merged; under `FINAL` it
|
|
9
|
+
* does not.
|
|
10
|
+
*/
|
|
11
|
+
const COLLAPSING_ENGINE = /^(?:Replicated|Shared)?(?:Summing|Replacing|Aggregating|Collapsing|VersionedCollapsing|Coalescing|Graphite)MergeTree$/;
|
|
12
|
+
/** Whether an engine collapses rows that share a sorting key (#3674). */
|
|
13
|
+
export const collapsesRows = (engine) => COLLAPSING_ENGINE.test(engine ?? "");
|
|
14
|
+
/** Split on the commas outside parentheses and quotes. */
|
|
15
|
+
export function splitTopLevel(text) {
|
|
16
|
+
const out = [];
|
|
17
|
+
let depth = 0;
|
|
18
|
+
let quote;
|
|
19
|
+
let start = 0;
|
|
20
|
+
for (let i = 0; i < text.length; i++) {
|
|
21
|
+
const c = text[i];
|
|
22
|
+
if (quote) {
|
|
23
|
+
if (c === "\\")
|
|
24
|
+
i++;
|
|
25
|
+
else if (c === quote)
|
|
26
|
+
quote = undefined;
|
|
27
|
+
}
|
|
28
|
+
else if (c === "'" || c === '"' || c === "`")
|
|
29
|
+
quote = c;
|
|
30
|
+
else if (c === "(" || c === "[")
|
|
31
|
+
depth++;
|
|
32
|
+
else if (c === ")" || c === "]")
|
|
33
|
+
depth--;
|
|
34
|
+
else if (c === "," && depth === 0) {
|
|
35
|
+
out.push(text.slice(start, i).trim());
|
|
36
|
+
start = i + 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const last = text.slice(start).trim();
|
|
40
|
+
if (last || out.length > 0)
|
|
41
|
+
out.push(last);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** The engine's own arguments in `system.tables.engine_full`, without a `Replicated` engine's Keeper path and replica name. */
|
|
45
|
+
export function engineArguments(engineFull) {
|
|
46
|
+
const m = /^\s*(\w+)\s*\(/.exec(engineFull);
|
|
47
|
+
if (!m)
|
|
48
|
+
return [];
|
|
49
|
+
let depth = 0;
|
|
50
|
+
let quote;
|
|
51
|
+
const from = m[0].length;
|
|
52
|
+
let end = -1;
|
|
53
|
+
for (let i = from - 1; i < engineFull.length && end < 0; i++) {
|
|
54
|
+
const c = engineFull[i];
|
|
55
|
+
if (quote) {
|
|
56
|
+
if (c === "\\")
|
|
57
|
+
i++;
|
|
58
|
+
else if (c === quote)
|
|
59
|
+
quote = undefined;
|
|
60
|
+
}
|
|
61
|
+
else if (c === "'")
|
|
62
|
+
quote = c;
|
|
63
|
+
else if (c === "(")
|
|
64
|
+
depth++;
|
|
65
|
+
else if (c === ")" && --depth === 0)
|
|
66
|
+
end = i;
|
|
67
|
+
}
|
|
68
|
+
if (end < 0)
|
|
69
|
+
return [];
|
|
70
|
+
const args = splitTopLevel(engineFull.slice(from, end)).filter((a) => a !== "");
|
|
71
|
+
if (/^(?:Replicated|Shared)/.test(m[1]))
|
|
72
|
+
while (args.length > 0 && args[0].startsWith("'"))
|
|
73
|
+
args.shift();
|
|
74
|
+
return args;
|
|
75
|
+
}
|
|
76
|
+
const NUMERIC = /^(?:U?Int(?:8|16|32|64|128|256)|Float(?:32|64)|BFloat16|Decimal(?:32|64|128|256)?\s*\(.*\))$/;
|
|
77
|
+
const FLOAT = /^(?:Float(?:32|64)|BFloat16)$/;
|
|
78
|
+
/** The SimpleAggregateFunction functions whose result a merge keeps exactly, whatever the order of the rows. */
|
|
79
|
+
const SIMPLE_ORDER_FREE = new Set(["sum", "sumWithOverflow", "min", "max", "groupBitAnd", "groupBitOr", "groupBitXor"]);
|
|
80
|
+
const quoted = (name) => `\`${name.replace(/\\/g, "\\\\").replace(/`/g, "\\`")}\``;
|
|
81
|
+
const typeLiteral = (type) => `'${type.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
82
|
+
/** A sum in the column's own type, as the engine stores it; a float to 32 bits, since the order rows are added in moves its last bits. */
|
|
83
|
+
const summed = (c) => (FLOAT.test(c.type) ? `toFloat32(sum(${quoted(c.name)}))` : `CAST(sum(${quoted(c.name)}), ${typeLiteral(c.type)})`);
|
|
84
|
+
/**
|
|
85
|
+
* What a merge of `engine` keeps per sorting key, or undefined for an engine
|
|
86
|
+
* that does not collapse rows, or whose result depends on the order rows
|
|
87
|
+
* merge in (`GraphiteMergeTree` thins its rows by age).
|
|
88
|
+
*/
|
|
89
|
+
export function collapseSemantics(engine, engineFull, columns) {
|
|
90
|
+
if (!collapsesRows(engine))
|
|
91
|
+
return undefined;
|
|
92
|
+
const base = engine.replace(/^(?:Replicated|Shared)/, "").replace(/MergeTree$/, "");
|
|
93
|
+
const args = engineArguments(engineFull);
|
|
94
|
+
const values = columns.filter((c) => !c.inSortingKey);
|
|
95
|
+
const named = (n) => (n ? columns.find((c) => c.name === n.replace(/^`|`$/g, "")) : undefined);
|
|
96
|
+
switch (base) {
|
|
97
|
+
case "Summing": {
|
|
98
|
+
// The columns listed, else every numeric column outside the sorting and partition keys; a key whose sums are all zero is removed.
|
|
99
|
+
const listed = args[0] ? splitTopLevel(args[0].replace(/^\((.*)\)$/s, "$1")).map(named) : undefined;
|
|
100
|
+
const sums = (listed ? listed.filter((c) => c !== undefined) : values).filter((c) => !c.inSortingKey && !c.inPartitionKey && NUMERIC.test(c.type));
|
|
101
|
+
const aggregates = sums.map(summed);
|
|
102
|
+
return {
|
|
103
|
+
engine: `${base}MergeTree`,
|
|
104
|
+
aggregates,
|
|
105
|
+
...(aggregates.length > 0 ? { having: `NOT (${aggregates.map((a) => `${a} = 0`).join(" AND ")})` } : {}),
|
|
106
|
+
describe: sums.length > 0 ? `the sums of ${sums.map((c) => c.name).join(", ")}` : "the keys",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
case "Replacing": {
|
|
110
|
+
// The row with the highest version is kept; without a version, the last one written, so only the key is compared.
|
|
111
|
+
const ver = named(args[0]);
|
|
112
|
+
return { engine: `${base}MergeTree`, aggregates: ver ? [`max(${quoted(ver.name)})`] : [], describe: ver ? `the highest ${ver.name}` : "the keys" };
|
|
113
|
+
}
|
|
114
|
+
case "Collapsing":
|
|
115
|
+
case "VersionedCollapsing": {
|
|
116
|
+
// A pair of rows with opposite signs cancels: the sum of the signs is what a merge keeps, and a key whose signs cancel out is removed.
|
|
117
|
+
const sign = named(args[0]);
|
|
118
|
+
if (!sign)
|
|
119
|
+
return undefined;
|
|
120
|
+
const s = `sum(${quoted(sign.name)})`;
|
|
121
|
+
return { engine: `${base}MergeTree`, aggregates: [s], having: `${s} != 0`, describe: `the sum of ${sign.name}` };
|
|
122
|
+
}
|
|
123
|
+
case "Aggregating": {
|
|
124
|
+
const simple = values.flatMap((c) => {
|
|
125
|
+
const m = /^SimpleAggregateFunction\(\s*(\w+)\s*,/.exec(c.type);
|
|
126
|
+
return m && SIMPLE_ORDER_FREE.has(m[1]) ? [{ c, fn: m[1] }] : [];
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
engine: `${base}MergeTree`,
|
|
130
|
+
aggregates: simple.map(({ c, fn }) => `${fn}(${quoted(c.name)})`),
|
|
131
|
+
describe: simple.length > 0 ? `${simple.map(({ c, fn }) => `${fn}(${c.name})`).join(", ")}` : "the keys",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
case "Coalescing": {
|
|
135
|
+
// The last value that is not NULL is kept: whether a column has one is what a merge keeps.
|
|
136
|
+
const nullable = values.filter((c) => /^Nullable\(/.test(c.type));
|
|
137
|
+
return {
|
|
138
|
+
engine: `${base}MergeTree`,
|
|
139
|
+
aggregates: nullable.map((c) => `count(${quoted(c.name)}) > 0`),
|
|
140
|
+
describe: nullable.length > 0 ? `which of ${nullable.map((c) => c.name).join(", ")} hold a value` : "the keys",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|