@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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A replica of a Replicated database that is down (#3270): what each step of
|
|
3
|
+
* the rebuild does about it.
|
|
4
|
+
*
|
|
5
|
+
* DDL (create, dual-write view, EXCHANGE, the views' DETACH and ATTACH, the
|
|
6
|
+
* drops, the receipts table) goes through the database's log in Keeper. The
|
|
7
|
+
* replica the run talks to executes it, and the others take it from the log;
|
|
8
|
+
* one that is down runs it in order when it comes back. So DDL proceeds on
|
|
9
|
+
* the live replicas: every statement carries
|
|
10
|
+
* `distributed_ddl_output_mode = throw_only_active`, which "doesn't wait for
|
|
11
|
+
* inactive replicas of the Replicated database" and still throws when a live
|
|
12
|
+
* one fails the statement
|
|
13
|
+
* (https://clickhouse.com/docs/reference/settings/session-settings/distributed-ddl).
|
|
14
|
+
* With the default `throw` the statement would wait the whole
|
|
15
|
+
* `distributed_ddl_task_timeout` (180s) for the down replica and then fail,
|
|
16
|
+
* after the live replicas had already run it. A replica counts as inactive
|
|
17
|
+
* once its Keeper session has expired (the client's `session_timeout_ms`,
|
|
18
|
+
* 30s by default), so a statement right after a replica goes down waits that
|
|
19
|
+
* long and then goes on. `KILL QUERY ON CLUSTER` takes the same setting and
|
|
20
|
+
* behaves the same way.
|
|
21
|
+
*
|
|
22
|
+
* Reading rows is different. A part written on a replica that went down
|
|
23
|
+
* before the others fetched it is on that replica alone, and the rows in it
|
|
24
|
+
* cannot be read anywhere else. A backfill that went on without them would
|
|
25
|
+
* copy the table less those rows, the verification would compare two tables
|
|
26
|
+
* that both lack them and pass, and the swap would lose them. So before a
|
|
27
|
+
* step reads rows it waits, for a bounded time, until this replica has
|
|
28
|
+
* fetched everything the others wrote (`SYSTEM SYNC REPLICA ... LIGHTWEIGHT`,
|
|
29
|
+
* https://clickhouse.com/docs/sql-reference/statements/system#sync-replica).
|
|
30
|
+
* When the down replica holds nothing this one lacks, which is the usual case,
|
|
31
|
+
* the wait returns at once and the step goes on. Otherwise the step stops
|
|
32
|
+
* with a {@link ReplicaFetchError} naming the replica and its parts.
|
|
33
|
+
*/
|
|
34
|
+
import { clickhouseQuery, ClickHouseQueryError } from "../http.js";
|
|
35
|
+
import { qualifiedIdent, sqlString } from "../apply/statements.js";
|
|
36
|
+
/** Settings every DDL statement of the rebuild carries: go on without the replicas that are down. Ignored in an Atomic database. */
|
|
37
|
+
export const DDL_SETTINGS = { distributed_ddl_output_mode: "throw_only_active" };
|
|
38
|
+
/** How long a step waits for this replica to fetch what the others wrote, by default. */
|
|
39
|
+
export const DEFAULT_REPLICA_TIMEOUT_MS = 120000;
|
|
40
|
+
/**
|
|
41
|
+
* One `SYSTEM SYNC REPLICA` waits at most this long (its `receive_timeout`),
|
|
42
|
+
* and the wait is repeated until the deadline. It keeps each HTTP request
|
|
43
|
+
* well inside the client's own five-minute response timeout, and puts a log
|
|
44
|
+
* line between them.
|
|
45
|
+
*/
|
|
46
|
+
const SYNC_SLICE_S = 60;
|
|
47
|
+
/** A step stopped because this replica could not fetch parts that only another replica has. */
|
|
48
|
+
export class ReplicaFetchError extends Error {
|
|
49
|
+
constructor(
|
|
50
|
+
/** `db.t`. */
|
|
51
|
+
table,
|
|
52
|
+
/** The replica the run talks to. */
|
|
53
|
+
replica, missing, waitedMs) {
|
|
54
|
+
const sources = [...new Set(missing.map((m) => m.source))];
|
|
55
|
+
const shown = missing
|
|
56
|
+
.slice(0, 10)
|
|
57
|
+
.map((m) => m.part + " from " + m.source + (m.sourceActive ? "" : " (inactive)") + (m.lastError ? " (last fetch error: " + m.lastError + ")" : ""))
|
|
58
|
+
.join(", ");
|
|
59
|
+
super(`${table}: waited ${Math.round(waitedMs / 1000)}s for replica ${replica} to fetch what the other replicas wrote, and ${missing.length} part(s) are still to fetch: ${shown}${missing.length > 10 ? ", ..." : ""}. ` +
|
|
60
|
+
`Those rows are on ${sources.join(", ")} alone, so the step stops rather than read the table without them; nothing has been swapped. ` +
|
|
61
|
+
`Bring ${sources.join(", ")} back (it rejoins through Keeper and this replica fetches the parts), then run again. ` +
|
|
62
|
+
`replicaTimeout sets how long a step waits.`);
|
|
63
|
+
this.table = table;
|
|
64
|
+
this.replica = replica;
|
|
65
|
+
this.missing = missing;
|
|
66
|
+
this.waitedMs = waitedMs;
|
|
67
|
+
this.name = "ReplicaFetchError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const isTimeout = (err) => err instanceof ClickHouseQueryError && /TIMEOUT_EXCEEDED|command timed out/.test(err.serverMessage);
|
|
71
|
+
/**
|
|
72
|
+
* Wait until this replica has fetched every part the others have written to
|
|
73
|
+
* `database.table`, for at most `timeoutMs`; then throw a
|
|
74
|
+
* {@link ReplicaFetchError} naming what it still lacks and where that is.
|
|
75
|
+
*/
|
|
76
|
+
export async function syncReplicaWithin(endpoint, database, table, opts = {}) {
|
|
77
|
+
const started = Date.now();
|
|
78
|
+
const deadline = started + (opts.timeoutMs ?? DEFAULT_REPLICA_TIMEOUT_MS);
|
|
79
|
+
const sql = `SYSTEM SYNC REPLICA ${qualifiedIdent(database, table)} LIGHTWEIGHT`;
|
|
80
|
+
opts.log?.(sql);
|
|
81
|
+
for (;;) {
|
|
82
|
+
opts.signal?.throwIfAborted();
|
|
83
|
+
const slice = Math.max(1, Math.min(SYNC_SLICE_S, Math.ceil((deadline - Date.now()) / 1000)));
|
|
84
|
+
try {
|
|
85
|
+
await clickhouseQuery(endpoint, sql, { settings: { receive_timeout: String(slice) }, ...(opts.signal ? { signal: opts.signal } : {}) });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (!isTimeout(err))
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
const { replica, missing } = await missingParts(endpoint, database, table);
|
|
93
|
+
// Fetched between the timeout and the look: nothing is left to wait for.
|
|
94
|
+
if (missing.length === 0)
|
|
95
|
+
return;
|
|
96
|
+
if (Date.now() >= deadline)
|
|
97
|
+
throw new ReplicaFetchError(`${database}.${table}`, replica, missing, Date.now() - started);
|
|
98
|
+
opts.log?.(`-- ${database}.${table}: replica ${replica} is still to fetch ${missing.length} part(s) from ${[...new Set(missing.map((m) => m.source))].join(", ")}; waiting`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** The parts this replica's queue is still to fetch for `database.table`, with where from. */
|
|
102
|
+
export async function missingParts(endpoint, database, table) {
|
|
103
|
+
const where = `database = ${sqlString(database)} AND table = ${sqlString(table)}`;
|
|
104
|
+
const [replicas] = await clickhouseQuery(endpoint, `SELECT replica_name, replica_is_active FROM system.replicas WHERE ${where}`);
|
|
105
|
+
// The entries LIGHTWEIGHT waits for.
|
|
106
|
+
const queue = await clickhouseQuery(endpoint, `SELECT new_part_name, source_replica, last_exception FROM system.replication_queue WHERE ${where} ` +
|
|
107
|
+
`AND type IN ('GET_PART', 'ATTACH_PART', 'DROP_RANGE', 'REPLACE_RANGE', 'DROP_PART') ORDER BY new_part_name`);
|
|
108
|
+
const active = replicas?.replica_is_active ?? {};
|
|
109
|
+
return {
|
|
110
|
+
replica: replicas?.replica_name ?? "this replica",
|
|
111
|
+
missing: queue.map((e) => {
|
|
112
|
+
const lastError = e.last_exception.trim().split("\n")[0];
|
|
113
|
+
return {
|
|
114
|
+
part: e.new_part_name,
|
|
115
|
+
source: e.source_replica || "another replica",
|
|
116
|
+
sourceActive: Number(active[e.source_replica] ?? 0) === 1,
|
|
117
|
+
...(lastError ? { lastError } : {}),
|
|
118
|
+
};
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rebuild across the shards of a cluster (#3663).
|
|
3
|
+
*
|
|
4
|
+
* On a `cluster:<name>` topology the rebuild's DDL runs `ON CLUSTER`, so
|
|
5
|
+
* every server gets the new table, the dual-write view and the swap. The
|
|
6
|
+
* rows do not move that way: each shard holds its own, and a copy run on
|
|
7
|
+
* the profile's server alone copies that server's shard and no other. So
|
|
8
|
+
* with more than one shard, the backfill and the verification work shard by
|
|
9
|
+
* shard, all through the profile's server:
|
|
10
|
+
*
|
|
11
|
+
* - a shard's rows are read with `cluster('<name>', db, t)` and
|
|
12
|
+
* `_shard_num = <n>`, one replica per shard, as the cluster's own
|
|
13
|
+
* configuration reaches it;
|
|
14
|
+
* - they are written back to the same shard with
|
|
15
|
+
* `INSERT INTO FUNCTION cluster('<name>', db, t__chant_new, <slot>)`, a
|
|
16
|
+
* constant sharding key that lands on that shard, inserted in the
|
|
17
|
+
* foreground so the statement returns once the shard has the rows;
|
|
18
|
+
* - a shard's half-copied rows are deleted with an `ALTER ... ON CLUSTER`
|
|
19
|
+
* whose condition holds on that shard's servers only
|
|
20
|
+
* (`getMacro('shard')`, the macro the cluster's Keeper paths already use).
|
|
21
|
+
*
|
|
22
|
+
* Each (shard, partition) is one copy with its own receipt, so a rerun after
|
|
23
|
+
* a failure on one shard skips what every shard already copied.
|
|
24
|
+
*
|
|
25
|
+
* With one shard nothing changes: the replicas of a shard replicate what is
|
|
26
|
+
* written on any of them, which is what the rebuild has always relied on.
|
|
27
|
+
*/
|
|
28
|
+
import { clickhouseQuery } from "../http.js";
|
|
29
|
+
import { ident, sqlString } from "../apply/statements.js";
|
|
30
|
+
import { RebuildRefusal } from "./observe.js";
|
|
31
|
+
/** Settings for a copy into one shard: in the foreground, and read from the cluster rather than pushed down to each shard. */
|
|
32
|
+
export const SHARD_INSERT_SETTINGS = { distributed_foreground_insert: "1", parallel_distributed_insert_select: "0" };
|
|
33
|
+
/** Settings for a delete on one shard: its condition is the server's own macro, the same on every replica of the shard. */
|
|
34
|
+
export const SHARD_DELETE_SETTINGS = { allow_nondeterministic_mutations: "1", mutations_sync: "2" };
|
|
35
|
+
/**
|
|
36
|
+
* The shards of the target's cluster, when its topology is `cluster:<name>`
|
|
37
|
+
* and the cluster has more than one. Undefined otherwise: the rebuild works
|
|
38
|
+
* on the profile's server as it always has.
|
|
39
|
+
*/
|
|
40
|
+
export async function clusterSharding(target) {
|
|
41
|
+
if (target.topology?.kind !== "cluster")
|
|
42
|
+
return undefined;
|
|
43
|
+
let cluster = target.topology.cluster;
|
|
44
|
+
const macro = /^\{(\w+)\}$/.exec(cluster);
|
|
45
|
+
if (macro) {
|
|
46
|
+
const [row] = await clickhouseQuery(target.endpoint, `SELECT getMacro(${sqlString(macro[1])}) AS c`);
|
|
47
|
+
cluster = row?.c ?? cluster;
|
|
48
|
+
}
|
|
49
|
+
const weights = await clickhouseQuery(target.endpoint, `SELECT shard_num AS shard, any(shard_weight) AS weight FROM system.clusters WHERE cluster = ${sqlString(cluster)} GROUP BY shard_num ORDER BY shard_num`);
|
|
50
|
+
if (weights.length <= 1)
|
|
51
|
+
return undefined;
|
|
52
|
+
const macros = new Map((await clickhouseQuery(target.endpoint, `SELECT _shard_num AS shard, getMacro('shard') AS macro FROM cluster(${sqlString(cluster)}, system.one)`)).map((r) => [Number(r.shard), r.macro]));
|
|
53
|
+
let slot = 0;
|
|
54
|
+
const shards = [];
|
|
55
|
+
for (const w of weights) {
|
|
56
|
+
const num = Number(w.shard);
|
|
57
|
+
const m = macros.get(num);
|
|
58
|
+
if (m === undefined)
|
|
59
|
+
throw new RebuildRefusal(`cluster ${cluster}: shard ${num} has no {shard} macro, which a rebuild across shards needs to delete a half-copied partition on that shard alone`);
|
|
60
|
+
shards.push({ num, macro: m, slot });
|
|
61
|
+
slot += Number(w.weight) || 1;
|
|
62
|
+
}
|
|
63
|
+
const distinct = new Set(shards.map((s) => s.macro));
|
|
64
|
+
if (distinct.size !== shards.length) {
|
|
65
|
+
throw new RebuildRefusal(`cluster ${cluster}: two shards share a {shard} macro (${shards.map((s) => `${s.num}=${s.macro}`).join(", ")}); each shard needs its own`);
|
|
66
|
+
}
|
|
67
|
+
return { cluster, shards };
|
|
68
|
+
}
|
|
69
|
+
/** One shard's rows of `database.table`, as a table expression; filter with {@link onShard}. */
|
|
70
|
+
export const shardTable = (sharding, database, table) => `cluster(${sqlString(sharding.cluster)}, ${ident(database)}, ${ident(table)})`;
|
|
71
|
+
/** The condition that keeps one shard's rows of a {@link shardTable}. */
|
|
72
|
+
export const onShard = (shard) => `_shard_num = ${shard.num}`;
|
|
73
|
+
/** Where a copy into one shard of `database.table` writes. */
|
|
74
|
+
export const intoShard = (sharding, shard, database, table) => `FUNCTION cluster(${sqlString(sharding.cluster)}, ${ident(database)}, ${ident(table)}, ${shard.slot})`;
|
|
75
|
+
/** The condition, in an `ALTER ... ON CLUSTER` mutation, that holds on that shard's servers only. */
|
|
76
|
+
export const onShardServers = (shard) => `getMacro('shard') = ${sqlString(shard.macro)}`;
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rebuild migration's steps (#3198), each one convergent: it observes
|
|
3
|
+
* the server (`./observe.ts`), does what is left of its own part, and
|
|
4
|
+
* reports. Running a step twice does what running it once did.
|
|
5
|
+
*
|
|
6
|
+
* The backfill and the verification are in `./backfill.ts` and
|
|
7
|
+
* `./verify.ts`; the Op that orders them is `./op.ts`.
|
|
8
|
+
*/
|
|
9
|
+
import { computePlanDigest, parseDuration } from "@intentius/chant/op";
|
|
10
|
+
import { clickhouseQuery } from "../http.js";
|
|
11
|
+
import { carriesMarker, readTrailerPairs, REBUILD_TRAILER_KEY, stampedComment } from "../ownership.js";
|
|
12
|
+
import { createStatement, ident, qualifiedIdent, renamedDeclaration, sqlString } from "../apply/statements.js";
|
|
13
|
+
import { waitForMutations } from "../apply/mutations.js";
|
|
14
|
+
import { renderFor } from "../topology.js";
|
|
15
|
+
import { DDL_SETTINGS, syncReplicaWithin } from "./replicas.js";
|
|
16
|
+
import { assertSameRows } from "./verify.js";
|
|
17
|
+
import { changesBetween, observeRebuild, rebuildNames, RebuildRefusal, showCreate, } from "./observe.js";
|
|
18
|
+
/** The server's clock, in milliseconds: the cut-over and the retention are the server's times, not this machine's. */
|
|
19
|
+
export async function serverNow(target) {
|
|
20
|
+
const [row] = await clickhouseQuery(target.endpoint, "SELECT toUnixTimestamp64Milli(now64(3)) AS t");
|
|
21
|
+
return Number(row?.t ?? Date.now());
|
|
22
|
+
}
|
|
23
|
+
/** `2026-10-02 12:00:00.000`, UTC: how a cut-over is written into SQL. */
|
|
24
|
+
export const sqlUtc = (ms) => new Date(ms).toISOString().replace("T", " ").replace("Z", "");
|
|
25
|
+
/** A `DateTime64` literal in UTC. */
|
|
26
|
+
export const utcLiteral = (ms) => `toDateTime64(${sqlString(sqlUtc(ms))}, 3, 'UTC')`;
|
|
27
|
+
export const observe = (run) => observeRebuild(run.target, run.declared, run.marker);
|
|
28
|
+
/**
|
|
29
|
+
* One DDL statement, rendered for the environment's topology when it names
|
|
30
|
+
* one (`../topology.ts`: `ON CLUSTER` and the engine). In a Replicated
|
|
31
|
+
* database it goes on without the replicas that are down, which run it from
|
|
32
|
+
* the database's log when they come back (`./replicas.ts`).
|
|
33
|
+
*/
|
|
34
|
+
const q = (run, statement) => {
|
|
35
|
+
run.signal?.throwIfAborted();
|
|
36
|
+
const sql = renderFor(statement, run.target.topology);
|
|
37
|
+
run.log(sql);
|
|
38
|
+
return clickhouseQuery(run.target.endpoint, sql, { settings: DDL_SETTINGS, ...(run.signal ? { signal: run.signal } : {}) });
|
|
39
|
+
};
|
|
40
|
+
export async function waitOn(run, database, table) {
|
|
41
|
+
const ids = await waitForMutations(run.target.endpoint, database, table, {
|
|
42
|
+
...(run.sharding ? { source: `clusterAllReplicas(${sqlString(run.sharding.cluster)}, system.mutations)` } : {}),
|
|
43
|
+
...(run.mutationTimeoutMs !== undefined ? { timeoutMs: run.mutationTimeoutMs } : {}),
|
|
44
|
+
...(run.signal ? { signal: run.signal } : {}),
|
|
45
|
+
});
|
|
46
|
+
if (ids.length > 0)
|
|
47
|
+
run.log(`-- waited for ${database}.${table} mutation(s) ${ids.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* In a Replicated database, wait until this replica has every part the
|
|
51
|
+
* others have written to `database.table` (`SYSTEM SYNC REPLICA ...
|
|
52
|
+
* LIGHTWEIGHT`, which waits for fetches and not for merges). A step that
|
|
53
|
+
* reads rows on one replica after a copy ran on another reads them all.
|
|
54
|
+
* A replica that is down and holds parts this one lacks stops the step once
|
|
55
|
+
* `replicaTimeoutMs` has passed, naming it (`./replicas.ts`). Nothing to
|
|
56
|
+
* wait for in an Atomic database.
|
|
57
|
+
*/
|
|
58
|
+
export async function syncReplica(run, o, database, table) {
|
|
59
|
+
if (!o.replicated)
|
|
60
|
+
return;
|
|
61
|
+
await syncReplicaWithin(run.target.endpoint, database, table, {
|
|
62
|
+
...(run.replicaTimeoutMs !== undefined ? { timeoutMs: run.replicaTimeoutMs } : {}),
|
|
63
|
+
log: run.log,
|
|
64
|
+
...(run.signal ? { signal: run.signal } : {}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** The trailer pairs a working object carries besides the marker. */
|
|
68
|
+
const working = (key, role, extra = {}) => ({ [REBUILD_TRAILER_KEY]: key, role, ...extra });
|
|
69
|
+
/**
|
|
70
|
+
* Create the new table from the declaration, under `t__chant_new`, marked as
|
|
71
|
+
* this project's and this rebuild's. The old table's unfinished mutations are
|
|
72
|
+
* waited on first, so what is copied is what they produce. A new table that
|
|
73
|
+
* is already there is kept when it matches the declaration; one made from an
|
|
74
|
+
* earlier declaration is refused, and the run's onFailure drops it.
|
|
75
|
+
*/
|
|
76
|
+
export async function createNewTable(run) {
|
|
77
|
+
const o = await observe(run);
|
|
78
|
+
const table = `${o.names.database}.${o.names.newName}`;
|
|
79
|
+
if (o.state !== "rebuild")
|
|
80
|
+
return { state: o.state, table, created: false };
|
|
81
|
+
await waitOn(run, o.names.database, o.names.name);
|
|
82
|
+
if (o.newTable) {
|
|
83
|
+
const live = await showCreate(run.target, o.names.newTable);
|
|
84
|
+
const stale = (await changesBetween(run.target, o.names.key, { ...live, name: run.declared.canonical.name }, run.declared.canonical)).filter((c) => c.field !== "comment");
|
|
85
|
+
if (stale.length > 0) {
|
|
86
|
+
throw new RebuildRefusal(`${table} was made from another declaration of ${o.names.key} (${stale.map((c) => `${c.rule} on ${c.field}`).join(", ")}). ` +
|
|
87
|
+
`The run fails so onFailure drops it, and the next run starts again from the current declaration.`);
|
|
88
|
+
}
|
|
89
|
+
run.log(`-- ${table} is there and matches the declaration`);
|
|
90
|
+
return { state: o.state, table, created: false };
|
|
91
|
+
}
|
|
92
|
+
const renamed = renamedDeclaration(run.declared, o.names.newName, run.target.defaultDatabase);
|
|
93
|
+
await q(run, createStatement(renamed, run.marker, { trailer: working(o.names.key, "new") }));
|
|
94
|
+
return { state: o.state, table, created: true };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Materialized-view mode: create `t__chant_dual`, a materialized view on the
|
|
98
|
+
* old table that writes every row at or after the cut-over into the new
|
|
99
|
+
* table. The cut-over is the server's time now plus the delay, kept in the
|
|
100
|
+
* view's own comment (`cutover=`), so a later run and the backfill read it
|
|
101
|
+
* back from the server. The rows at or after the cut-over that the old table
|
|
102
|
+
* already held are the backfill's (`./backfill.ts`). App mode creates nothing; its gate is the Op's.
|
|
103
|
+
*/
|
|
104
|
+
export async function startDualWrite(run) {
|
|
105
|
+
const o = await observe(run);
|
|
106
|
+
const mode = run.dualWrite.mode;
|
|
107
|
+
if (o.state !== "rebuild" || run.dualWrite.mode === "app")
|
|
108
|
+
return { state: o.state, mode, created: false };
|
|
109
|
+
if (o.dual)
|
|
110
|
+
return { state: o.state, mode, cutover: o.dual.pairs.get("cutover"), created: false };
|
|
111
|
+
if (!o.newTable)
|
|
112
|
+
throw new RebuildRefusal(`${o.names.key}: the new table is not there yet; the Create phase makes it`);
|
|
113
|
+
const column = run.dualWrite.cutoverColumn;
|
|
114
|
+
const cut = o.copied.find((c) => c.name === column);
|
|
115
|
+
if (!cut) {
|
|
116
|
+
throw new RebuildRefusal(`${o.names.key}: the cut-over column ${column} is not a column the new table copies from the old one ` +
|
|
117
|
+
`(copied: ${o.copied.map((c) => c.name).join(", ")})`);
|
|
118
|
+
}
|
|
119
|
+
// On a whole second: a DateTime column holds whole seconds, and a row written
|
|
120
|
+
// during the cut-over's second would otherwise carry a time before it and
|
|
121
|
+
// reach the old table after the backfill has started.
|
|
122
|
+
const cutover = Math.ceil(((await serverNow(run.target)) + parseDuration(run.dualWrite.cutoverDelay ?? CUTOVER_DELAY)) / 1000) * 1000;
|
|
123
|
+
const iso = new Date(cutover).toISOString();
|
|
124
|
+
const select = o.copied.map((c) => (c.source === c.name ? ident(c.name) : `${ident(c.source)} AS ${ident(c.name)}`)).join(", ");
|
|
125
|
+
const comment = stampedComment(`chant rebuild of ${o.names.key}: rows at or after the cut-over`, run.marker, working(o.names.key, "dual", { cutover: iso }));
|
|
126
|
+
await q(run, `CREATE MATERIALIZED VIEW ${o.names.dualView} TO ${o.names.newTable} AS SELECT ${select} FROM ${o.names.table} ` +
|
|
127
|
+
`WHERE ${ident(cut.source)} >= ${utcLiteral(cutover)} COMMENT ${sqlString(comment)}`);
|
|
128
|
+
return { state: o.state, mode, cutover: iso, created: true };
|
|
129
|
+
}
|
|
130
|
+
/** How far after the dual-write view is made the cut-over falls, unless `cutoverDelay` says otherwise. */
|
|
131
|
+
export const CUTOVER_DELAY = "5s";
|
|
132
|
+
/** The cut-over in milliseconds, from the dual-write view; undefined in app mode. */
|
|
133
|
+
export function cutoverOf(run, o) {
|
|
134
|
+
if (run.dualWrite.mode === "app")
|
|
135
|
+
return undefined;
|
|
136
|
+
const iso = o.dual?.pairs?.get("cutover");
|
|
137
|
+
if (!iso)
|
|
138
|
+
throw new RebuildRefusal(`${o.names.key}: the dual-write view ${o.names.database}.${o.names.dualName} is not there; the Dual write phase makes it`);
|
|
139
|
+
return Date.parse(iso);
|
|
140
|
+
}
|
|
141
|
+
/** How long the backfill waits, once the cut-over has passed, for the writes begun before it. Default: ten minutes. */
|
|
142
|
+
export const CUTOVER_TIMEOUT_MS = 10 * 60000;
|
|
143
|
+
/**
|
|
144
|
+
* Wait until no row before the cut-over is still to arrive in the old table,
|
|
145
|
+
* then return. Two conditions, both polled: the server's clock has passed
|
|
146
|
+
* the cut-over (until then a row can still be written with a time before
|
|
147
|
+
* it), and every write into the old table that began before the cut-over has
|
|
148
|
+
* finished (a long INSERT, or an asynchronous insert still in its buffer,
|
|
149
|
+
* can carry such rows after the clock has passed). The second wait is
|
|
150
|
+
* bounded by `timeoutMs`; past it the step stops naming the writes, rather
|
|
151
|
+
* than copy without their rows. A row whose time lags further behind its
|
|
152
|
+
* write than `cutoverDelay` is neither: the verification finds it.
|
|
153
|
+
*/
|
|
154
|
+
export async function waitForCutover(run, cutover, probe, opts = {}) {
|
|
155
|
+
const at = new Date(cutover).toISOString();
|
|
156
|
+
for (;;) {
|
|
157
|
+
const now = await probe.now();
|
|
158
|
+
if (now > cutover)
|
|
159
|
+
break;
|
|
160
|
+
run.log(`-- waiting ${Math.ceil((cutover - now) / 1000)}s for the cut-over at ${at}`);
|
|
161
|
+
await sleep(Math.min(cutover - now + 50, 10000), run.signal);
|
|
162
|
+
}
|
|
163
|
+
const timeoutMs = opts.timeoutMs ?? CUTOVER_TIMEOUT_MS;
|
|
164
|
+
const deadline = Date.now() + timeoutMs;
|
|
165
|
+
let pollMs = opts.pollMs ?? 100;
|
|
166
|
+
let logged = "";
|
|
167
|
+
for (;;) {
|
|
168
|
+
const pending = await probe.pendingWrites(cutover);
|
|
169
|
+
if (pending.length === 0)
|
|
170
|
+
return;
|
|
171
|
+
if (Date.now() >= deadline) {
|
|
172
|
+
throw new RebuildRefusal(`writes into the old table begun before the cut-over at ${at} are still running after ${Math.round(timeoutMs / 1000)}s: ${pending.join(", ")}. ` +
|
|
173
|
+
`Let them finish, or stop them, and run again; cutoverTimeout sets how long the backfill waits.`);
|
|
174
|
+
}
|
|
175
|
+
const line = pending.join(", ");
|
|
176
|
+
if (line !== logged)
|
|
177
|
+
run.log(`-- waiting for writes begun before the cut-over at ${at}: ${line}`);
|
|
178
|
+
logged = line;
|
|
179
|
+
await sleep(pollMs, run.signal);
|
|
180
|
+
pollMs = Math.min(pollMs * 2, 2000);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** The table an `INSERT INTO` statement writes to, or undefined for anything else (`INSERT INTO FUNCTION` included). */
|
|
184
|
+
export function insertTarget(query, currentDatabase) {
|
|
185
|
+
const text = query.replace(/^(?:\s+|--[^\n]*\n|\/\*[\s\S]*?\*\/)*/, "");
|
|
186
|
+
const part = "(`(?:[^`\\\\]|\\\\.)+`|\"(?:[^\"\\\\]|\\\\.)+\"|[A-Za-z_][A-Za-z0-9_]*)";
|
|
187
|
+
const m = new RegExp(`^INSERT\\s+INTO\\s+(?:TABLE\\s+)?${part}(?:\\s*\\.\\s*${part})?`, "i").exec(text);
|
|
188
|
+
if (!m || /^FUNCTION$/i.test(m[1]))
|
|
189
|
+
return undefined;
|
|
190
|
+
const unquote = (s) => (/^[`"]/.test(s) ? s.slice(1, -1).replace(/\\(.)/g, "$1") : s);
|
|
191
|
+
return m[2] !== undefined ? { database: unquote(m[1]), name: unquote(m[2]) } : { database: currentDatabase, name: unquote(m[1]) };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The probe against the server. On a cluster of shards it reads every
|
|
195
|
+
* server's queries and buffers, and in a Replicated database every
|
|
196
|
+
* replica's, since a write can be running on any of them. A server that
|
|
197
|
+
* cannot be reached is left out rather than failing the wait.
|
|
198
|
+
*/
|
|
199
|
+
export function serverCutoverProbe(run, o) {
|
|
200
|
+
const { database, name } = o.names;
|
|
201
|
+
const cluster = run.sharding?.cluster ?? (o.replicated ? database : undefined);
|
|
202
|
+
const from = (table) => (cluster ? `clusterAllReplicas(${sqlString(cluster)}, system.${table})` : `system.${table}`);
|
|
203
|
+
// A server that cannot be reached runs no write the backfill could miss
|
|
204
|
+
// from here: its parts reach this one by replication, which the backfill
|
|
205
|
+
// waits for (`syncReplica`), or the verification finds them missing. So an
|
|
206
|
+
// unreachable replica or shard is skipped, as every other step goes on
|
|
207
|
+
// with the live ones; `clusterAllReplicas` counts each replica as a shard.
|
|
208
|
+
const opts = { ...(cluster ? { settings: { skip_unavailable_shards: "1" } } : {}), ...(run.signal ? { signal: run.signal } : {}) };
|
|
209
|
+
return {
|
|
210
|
+
now: () => serverNow(run.target),
|
|
211
|
+
async pendingWrites(cutover) {
|
|
212
|
+
const running = await clickhouseQuery(run.target.endpoint, `SELECT query_id, query, current_database, elapsed, toUnixTimestamp64Milli(now64(3)) AS now FROM ${from("processes")} WHERE query_kind = 'Insert'`, opts);
|
|
213
|
+
const queued = await clickhouseQuery(run.target.endpoint, `SELECT entries.query_id AS ids FROM ${from("asynchronous_inserts")} ` +
|
|
214
|
+
`WHERE database = ${sqlString(database)} AND table = ${sqlString(name)} AND toUnixTimestamp64Milli(first_update) < ${cutover}`, opts);
|
|
215
|
+
const ids = running
|
|
216
|
+
.filter((r) => {
|
|
217
|
+
const t = insertTarget(r.query, r.current_database);
|
|
218
|
+
return t?.database === database && t.name === name && Number(r.now) - Number(r.elapsed) * 1000 < cutover;
|
|
219
|
+
})
|
|
220
|
+
.map((r) => r.query_id);
|
|
221
|
+
for (const r of queued)
|
|
222
|
+
ids.push(...r.ids);
|
|
223
|
+
return [...new Set(ids)];
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export function sleep(ms, signal) {
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
signal?.throwIfAborted();
|
|
230
|
+
const t = setTimeout(resolve, ms);
|
|
231
|
+
signal?.addEventListener("abort", () => {
|
|
232
|
+
clearTimeout(t);
|
|
233
|
+
reject(signal.reason);
|
|
234
|
+
}, { once: true });
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Swap the new table in: compare every row of the two tables once more, as
|
|
239
|
+
* the verification does, and stop on any difference; then `EXCHANGE TABLES`,
|
|
240
|
+
* drop the dual-write view, rename the old table (now under the new table's name) to `t__chant_old`
|
|
241
|
+
* with its retention date, recreate the materialized views that read the
|
|
242
|
+
* table, and stamp the table's own comment as declared.
|
|
243
|
+
*
|
|
244
|
+
* Each part checks for itself whether it is done, so a swap interrupted
|
|
245
|
+
* anywhere finishes on the next run. The table's comment is stamped last:
|
|
246
|
+
* until it is, the table still carries `role=new` and the next run knows the
|
|
247
|
+
* swap has not finished.
|
|
248
|
+
*
|
|
249
|
+
* Recreating a dependent view: in this server line a materialized view
|
|
250
|
+
* follows its source by name, so after the EXCHANGE it already reads the new
|
|
251
|
+
* table. It is detached and attached again so the server analyzes its query
|
|
252
|
+
* against the new table's columns and types, and a view the new definition
|
|
253
|
+
* breaks fails here, with the old table still kept, rather than on the next
|
|
254
|
+
* insert. A view with an inner table keeps its data, which dropping and
|
|
255
|
+
* creating it would not. Inserts that land while a view is detached are not
|
|
256
|
+
* passed to it; in materialized-view mode writes go on through the swap, so
|
|
257
|
+
* that window is the length of one DETACH and one ATTACH. In a Replicated
|
|
258
|
+
* database the DETACH is `PERMANENTLY`, the form that database takes, and
|
|
259
|
+
* the EXCHANGE, the DETACH and the ATTACH each run on every replica: now on
|
|
260
|
+
* the live ones, and from the database's log on one that is down, when it
|
|
261
|
+
* comes back.
|
|
262
|
+
*/
|
|
263
|
+
export async function swapTables(run) {
|
|
264
|
+
const o = await observe(run);
|
|
265
|
+
const n = o.names;
|
|
266
|
+
if (o.state === "done")
|
|
267
|
+
return { state: o.state, swapped: false, dependents: [] };
|
|
268
|
+
if (o.state === "swapped" && !o.exchanged) {
|
|
269
|
+
return { state: o.state, swapped: false, dependents: [], oldTable: `${n.database}.${n.oldName}`, ...retention(o) };
|
|
270
|
+
}
|
|
271
|
+
if (!o.exchanged) {
|
|
272
|
+
if (!o.newTable)
|
|
273
|
+
throw new RebuildRefusal(`${n.key}: there is no new table to swap in`);
|
|
274
|
+
// The new table holds every row the old one does, or nothing is swapped
|
|
275
|
+
// (sql-yodeler#45): rows that reached the old table alone since the
|
|
276
|
+
// verification would stay behind in it.
|
|
277
|
+
await assertSameRows(run, o);
|
|
278
|
+
await q(run, `EXCHANGE TABLES ${n.table} AND ${n.newTable}`);
|
|
279
|
+
}
|
|
280
|
+
// The dual-write view now reads the new table by name; drop it before
|
|
281
|
+
// anything else so it writes nothing more into the old one.
|
|
282
|
+
const dual = await objectComment(run, n.database, n.dualName);
|
|
283
|
+
if (dual !== undefined && readTrailerPairs(dual)?.get(REBUILD_TRAILER_KEY) === n.key && carriesMarker(dual, run.marker)) {
|
|
284
|
+
await q(run, `DROP VIEW ${n.dualView} SYNC`);
|
|
285
|
+
}
|
|
286
|
+
if ((await objectComment(run, n.database, n.oldName)) === undefined) {
|
|
287
|
+
await q(run, `RENAME TABLE ${n.newTable} TO ${n.oldTable}`);
|
|
288
|
+
}
|
|
289
|
+
const oldComment = await objectComment(run, n.database, n.oldName);
|
|
290
|
+
let retainUntil = readTrailerPairs(oldComment)?.get("retain-until");
|
|
291
|
+
if (readTrailerPairs(oldComment)?.get("role") !== "old") {
|
|
292
|
+
retainUntil = new Date((await serverNow(run.target)) + run.retainMs).toISOString();
|
|
293
|
+
const comment = stampedComment(`chant rebuild of ${n.key}: the old table, kept until ${retainUntil}`, run.marker, working(n.key, "old", { "retain-until": retainUntil }));
|
|
294
|
+
await q(run, `ALTER TABLE ${n.oldTable} MODIFY COMMENT ${sqlString(comment)}`);
|
|
295
|
+
}
|
|
296
|
+
const dependents = await dependentViews(run, n.database, n.name);
|
|
297
|
+
for (const d of dependents) {
|
|
298
|
+
const [database, name] = d;
|
|
299
|
+
// A Replicated database takes DETACH only as PERMANENTLY, run on every replica.
|
|
300
|
+
const permanently = database === n.database ? o.replicated : await isReplicatedDatabase(run, database);
|
|
301
|
+
await q(run, `DETACH TABLE ${qualifiedIdent(database, name)}${permanently ? " PERMANENTLY" : ""}`);
|
|
302
|
+
await q(run, `ATTACH TABLE ${qualifiedIdent(database, name)}`);
|
|
303
|
+
}
|
|
304
|
+
await q(run, `ALTER TABLE ${n.table} MODIFY COMMENT ${sqlString(stampedComment(run.declared.canonical.comment, run.marker))}`);
|
|
305
|
+
return { state: "swapped", swapped: true, dependents: dependents.map(([d, v]) => `${d}.${v}`), oldTable: `${n.database}.${n.oldName}`, ...(retainUntil ? { retainUntil } : {}) };
|
|
306
|
+
}
|
|
307
|
+
async function isReplicatedDatabase(run, database) {
|
|
308
|
+
const [row] = await clickhouseQuery(run.target.endpoint, `SELECT engine FROM system.databases WHERE name = ${sqlString(database)}`);
|
|
309
|
+
return row?.engine === "Replicated";
|
|
310
|
+
}
|
|
311
|
+
async function objectComment(run, database, name) {
|
|
312
|
+
const [row] = await clickhouseQuery(run.target.endpoint, `SELECT comment FROM system.tables WHERE database = ${sqlString(database)} AND name = ${sqlString(name)}`);
|
|
313
|
+
return row?.comment;
|
|
314
|
+
}
|
|
315
|
+
/** The materialized views that read `database.name`, as `system.tables` lists them. */
|
|
316
|
+
export async function dependentViews(run, database, name) {
|
|
317
|
+
const [row] = await clickhouseQuery(run.target.endpoint, `SELECT dependencies_database, dependencies_table FROM system.tables WHERE database = ${sqlString(database)} AND name = ${sqlString(name)}`);
|
|
318
|
+
if (!row)
|
|
319
|
+
return [];
|
|
320
|
+
return row.dependencies_table.map((t, i) => [row.dependencies_database[i], t]);
|
|
321
|
+
}
|
|
322
|
+
// ── retain and drop ────────────────────────────────────────────────────
|
|
323
|
+
function retention(o) {
|
|
324
|
+
const until = o.old?.pairs?.get("retain-until");
|
|
325
|
+
return until ? { retainUntil: until } : {};
|
|
326
|
+
}
|
|
327
|
+
/** Where the old table is kept and until when, and the digest the drop gate binds. */
|
|
328
|
+
export async function retainPlan(run) {
|
|
329
|
+
const o = await observe(run);
|
|
330
|
+
if (!o.old)
|
|
331
|
+
return { state: o.state, due: false };
|
|
332
|
+
const retainUntil = o.old.pairs?.get("retain-until");
|
|
333
|
+
const due = retainUntil !== undefined && (await serverNow(run.target)) >= Date.parse(retainUntil);
|
|
334
|
+
const oldTable = `${o.names.database}.${o.names.oldName}`;
|
|
335
|
+
return {
|
|
336
|
+
state: o.state,
|
|
337
|
+
oldTable,
|
|
338
|
+
...(retainUntil ? { retainUntil } : {}),
|
|
339
|
+
due,
|
|
340
|
+
dropDigest: computePlanDigest("clickhouse-rebuild-drop", { table: o.names.key, old: oldTable, uuid: o.old.uuid, retainUntil: retainUntil ?? null }),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/** Drop the old table once its retention date has passed. Before then, say until when and drop nothing. */
|
|
344
|
+
export async function dropOldTable(run) {
|
|
345
|
+
const plan = await retainPlan(run);
|
|
346
|
+
if (!plan.oldTable)
|
|
347
|
+
return { state: plan.state, dropped: false };
|
|
348
|
+
if (!plan.due) {
|
|
349
|
+
run.log(`-- ${plan.oldTable} is kept until ${plan.retainUntil}; a run after that drops it`);
|
|
350
|
+
return { state: plan.state, dropped: false, oldTable: plan.oldTable, ...(plan.retainUntil ? { retainUntil: plan.retainUntil } : {}) };
|
|
351
|
+
}
|
|
352
|
+
const n = rebuildNames(run.declared.canonical.database ?? run.target.defaultDatabase, run.declared.canonical.name);
|
|
353
|
+
await q(run, `DROP TABLE ${n.oldTable} SYNC`);
|
|
354
|
+
return { state: "done", dropped: true, oldTable: plan.oldTable };
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* onFailure: drop the dual-write view and the new table, and nothing else.
|
|
358
|
+
* Each is dropped only when its comment names this rebuild and its role and
|
|
359
|
+
* carries this project's marker. After the EXCHANGE the new table is under
|
|
360
|
+
* the table's own name and the old one under the new table's name without
|
|
361
|
+
* `role=new`, so neither is touched: the swap is finished by the next run,
|
|
362
|
+
* not undone. The old table kept after a swap (`role=old`) is never dropped
|
|
363
|
+
* here; the drop gate decides that.
|
|
364
|
+
*/
|
|
365
|
+
export async function compensate(run) {
|
|
366
|
+
const n = rebuildNames(run.declared.canonical.database ?? run.target.defaultDatabase, run.declared.canonical.name);
|
|
367
|
+
const rows = await clickhouseQuery(run.target.endpoint, `SELECT name, comment FROM system.tables WHERE database = ${sqlString(n.database)} AND name IN (${sqlString(n.dualName)}, ${sqlString(n.newName)})`);
|
|
368
|
+
const dropped = [];
|
|
369
|
+
for (const [name, role, what] of [
|
|
370
|
+
[n.dualName, "dual", "VIEW"],
|
|
371
|
+
[n.newName, "new", "TABLE"],
|
|
372
|
+
]) {
|
|
373
|
+
const row = rows.find((r) => r.name === name);
|
|
374
|
+
const pairs = readTrailerPairs(row?.comment);
|
|
375
|
+
if (!row || pairs?.get(REBUILD_TRAILER_KEY) !== n.key || pairs.get("role") !== role || !carriesMarker(row.comment, run.marker))
|
|
376
|
+
continue;
|
|
377
|
+
const sql = renderFor(`DROP ${what} ${qualifiedIdent(n.database, name)} SYNC`, run.target.topology);
|
|
378
|
+
run.log(sql);
|
|
379
|
+
await clickhouseQuery(run.target.endpoint, sql, { settings: DDL_SETTINGS });
|
|
380
|
+
dropped.push(`${n.database}.${name}`);
|
|
381
|
+
}
|
|
382
|
+
return { dropped };
|
|
383
|
+
}
|