@intentius/chant-lexicon-sql 0.114.1 → 0.116.0
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/clickhouse/apply/mutations.d.ts +7 -1
- package/dist/clickhouse/apply/mutations.d.ts.map +1 -1
- package/dist/clickhouse/container.d.ts +22 -7
- package/dist/clickhouse/container.d.ts.map +1 -1
- package/dist/clickhouse/plan/deep.d.ts +5 -0
- package/dist/clickhouse/plan/deep.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/backfill.d.ts +11 -5
- package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/op.d.ts +29 -0
- package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/partitions.d.ts +8 -0
- package/dist/clickhouse/rebuild/partitions.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/receipts.d.ts +4 -6
- package/dist/clickhouse/rebuild/receipts.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/shards.d.ts +59 -0
- package/dist/clickhouse/rebuild/shards.d.ts.map +1 -0
- package/dist/clickhouse/rebuild/steps.d.ts +3 -0
- package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
- package/dist/clickhouse/rebuild/verify.d.ts +4 -0
- package/dist/clickhouse/rebuild/verify.d.ts.map +1 -1
- package/dist/codegen/registry.d.ts.map +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/core/receipts.d.ts +5 -3
- package/dist/core/receipts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -5
- package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
- package/dist/lsp/postgres-template.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/meta.json +15 -0
- package/dist/okf/index.md +3 -0
- package/dist/okf/types/PostgresFunction.md +9 -0
- package/dist/okf/types/PostgresProcedure.md +9 -0
- package/dist/okf/types/PostgresTrigger.md +9 -0
- package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +13 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/sql-exec.d.ts +41 -0
- package/dist/op/activities/sql-exec.d.ts.map +1 -0
- package/dist/op/activity-contracts.d.ts +20 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/postgres/apply/apply.d.ts +6 -1
- package/dist/postgres/apply/apply.d.ts.map +1 -1
- package/dist/postgres/apply/statements.d.ts +9 -2
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +110 -3
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/entity-types.d.ts +3 -0
- package/dist/postgres/entity-types.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/import/live-export.d.ts.map +1 -1
- package/dist/postgres/live/catalog.d.ts +17 -5
- package/dist/postgres/live/catalog.d.ts.map +1 -1
- package/dist/postgres/live/describe-resources.d.ts +6 -1
- package/dist/postgres/live/describe-resources.d.ts.map +1 -1
- package/dist/postgres/migrate/receipts.d.ts +8 -5
- package/dist/postgres/migrate/receipts.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +82 -5
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/deep.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts +21 -2
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +6 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres/plan/schema.d.ts +11 -1
- package/dist/postgres/plan/schema.d.ts.map +1 -1
- package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/receipts.d.ts +102 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/rules/postgres-templates.ts +2 -2
- package/dist/rules/sqlpg001.ts +3 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/apply/mutations.ts +9 -3
- package/src/clickhouse/container.ts +39 -11
- package/src/clickhouse/plan/cluster-drift.e2e.test.ts +117 -0
- package/src/clickhouse/plan/deep.ts +102 -3
- package/src/clickhouse/plan/normalize.ts +10 -0
- package/src/clickhouse/plan/plan.test.ts +10 -0
- package/src/clickhouse/rebuild/backfill.ts +85 -42
- package/src/clickhouse/rebuild/op.ts +80 -35
- package/src/clickhouse/rebuild/partitions.ts +11 -0
- package/src/clickhouse/rebuild/rebuild-sharded.e2e.test.ts +161 -0
- package/src/clickhouse/rebuild/rebuild.e2e.test.ts +110 -4
- package/src/clickhouse/rebuild/rebuild.test.ts +56 -4
- package/src/clickhouse/rebuild/receipts.ts +4 -6
- package/src/clickhouse/rebuild/shards.ts +104 -0
- package/src/clickhouse/rebuild/steps.ts +4 -0
- package/src/clickhouse/rebuild/verify.ts +14 -5
- package/src/codegen/postgres-classifier-page.ts +1 -1
- package/src/codegen/registry.ts +3 -0
- package/src/config.ts +7 -0
- package/src/core/receipts.ts +5 -3
- package/src/generated/lexicon-sql.json +15 -0
- package/src/index.ts +1 -1
- package/src/lint/rules/postgres-templates.ts +2 -2
- package/src/lint/rules/sqlpg001.ts +3 -0
- package/src/lsp/postgres-template.ts +6 -0
- package/src/mcp/postgres.ts +1 -1
- package/src/op/activities/clickhouse-rebuild.ts +32 -7
- package/src/op/activities/index.ts +21 -4
- package/src/op/activities/sql-exec.ts +84 -0
- package/src/op/activity-contracts.ts +15 -0
- package/src/op/effect-receipts.e2e.test.ts +277 -0
- package/src/plugin.ts +3 -0
- package/src/postgres/apply/apply.ts +21 -5
- package/src/postgres/apply/statements.ts +53 -11
- package/src/postgres/entities.ts +332 -9
- package/src/postgres/entity-types.ts +3 -0
- package/src/postgres/import/generator.ts +5 -2
- package/src/postgres/import/live-export.ts +4 -0
- package/src/postgres/live/catalog.ts +81 -5
- package/src/postgres/live/describe-resources.ts +17 -7
- package/src/postgres/live/routines.e2e.test.ts +173 -0
- package/src/postgres/migrate/batches.e2e.test.ts +2 -1
- package/src/postgres/migrate/migrate.e2e.test.ts +2 -1
- package/src/postgres/migrate/migrate.test.ts +3 -2
- package/src/postgres/migrate/receipts.ts +18 -10
- package/src/postgres/parser.ts +314 -8
- package/src/postgres/plan/commands.ts +2 -2
- package/src/postgres/plan/deep.ts +57 -17
- package/src/postgres/plan/diff.ts +46 -6
- package/src/postgres/plan/normalize.ts +158 -4
- package/src/postgres/plan/rules.ts +36 -0
- package/src/postgres/plan/schema.ts +25 -6
- package/src/postgres/plan/server-normalize.ts +24 -0
- package/src/postgres/routines.test.ts +150 -0
- package/src/postgres.ts +11 -0
- package/src/receipts.test.ts +91 -0
- package/src/receipts.ts +259 -0
- package/src/skill-defs.ts +1 -1
- package/src/skills/chant-sql-postgres.md +18 -2
- package/src/skills/chant-sql-rebuild.md +5 -1
- package/src/validate.ts +3 -0
|
@@ -51,8 +51,12 @@ export async function startScratchServer(
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export interface ScratchCluster {
|
|
54
|
-
/** One endpoint per
|
|
54
|
+
/** One endpoint per server, in order: shard 1's replicas first (`r1`, `r2`, ...), then shard 2's. */
|
|
55
55
|
replicas: ClickHouseEndpoint[];
|
|
56
|
+
/** Each server's shard and replica macros, in the order of `replicas`. */
|
|
57
|
+
layout: Array<{ shard: string; replica: string; host: string }>;
|
|
58
|
+
/** The cluster `remote_servers` defines over every server: `ON CLUSTER` and `cluster:<name>` name it. */
|
|
59
|
+
cluster: string;
|
|
56
60
|
/** The containers' common name prefix, for a log line. */
|
|
57
61
|
name: string;
|
|
58
62
|
/** Remove the containers and their network. Safe to call twice. */
|
|
@@ -72,23 +76,37 @@ export interface ScratchCluster {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
/**
|
|
75
|
-
* Start
|
|
76
|
-
*
|
|
79
|
+
* Start servers of `image` on their own Docker network, and wait until each
|
|
80
|
+
* answers and reaches Keeper: `replicas` servers as one shard of replicas,
|
|
81
|
+
* or, with `shards`, that many replicas in each shard (`[2, 1]` is a shard of
|
|
82
|
+
* two replicas and a shard of one server).
|
|
77
83
|
*
|
|
78
84
|
* Keeper is the server's embedded one (`keeper_server`), run by the first
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* a `Replicated` database's
|
|
82
|
-
*
|
|
85
|
+
* server, so the pinned server image and its digest pin Keeper too. Each
|
|
86
|
+
* server gets the macros `{shard}` = `s<n>` and `{replica}` = `r<n>` (the
|
|
87
|
+
* replica numbered within its shard), which a `Replicated` database's and a
|
|
88
|
+
* `Replicated*MergeTree`'s arguments name, and `remote_servers` defines the
|
|
89
|
+
* cluster `cluster` (default `chant`) over all of them, with
|
|
90
|
+
* `internal_replication`, so `ON CLUSTER` reaches every server. Configuration
|
|
91
|
+
* is copied into each container before it starts, with no bind mount.
|
|
83
92
|
*/
|
|
84
93
|
export async function startScratchCluster(
|
|
85
94
|
image: string,
|
|
86
|
-
options: { replicas?: number; namePrefix?: string; readyTimeoutMs?: number } = {},
|
|
95
|
+
options: { replicas?: number; shards?: number[]; cluster?: string; namePrefix?: string; readyTimeoutMs?: number } = {},
|
|
87
96
|
): Promise<ScratchCluster> {
|
|
88
|
-
const
|
|
97
|
+
const shards = options.shards ?? [options.replicas ?? 2];
|
|
98
|
+
const clusterName = options.cluster ?? "chant";
|
|
89
99
|
const name = uniqueContainerName(options.namePrefix ?? "chant-sql-cluster");
|
|
90
100
|
const network = `${name}-net`;
|
|
91
|
-
|
|
101
|
+
// One shard keeps the names the single-shard cluster always had (`<name>-r<n>`).
|
|
102
|
+
const layout = shards.flatMap((replicas, s) =>
|
|
103
|
+
Array.from({ length: replicas }, (_, r) => ({
|
|
104
|
+
shard: `s${s + 1}`,
|
|
105
|
+
replica: `r${r + 1}`,
|
|
106
|
+
host: shards.length === 1 ? `${name}-r${r + 1}` : `${name}-s${s + 1}r${r + 1}`,
|
|
107
|
+
})),
|
|
108
|
+
);
|
|
109
|
+
const nodes = layout.map((l) => l.host);
|
|
92
110
|
const dir = mkdtempSync(join(tmpdir(), "chant-sql-cluster-"));
|
|
93
111
|
let removed = false;
|
|
94
112
|
const stop = async (): Promise<void> => {
|
|
@@ -100,6 +118,12 @@ export async function startScratchCluster(
|
|
|
100
118
|
};
|
|
101
119
|
|
|
102
120
|
const keeper = nodes[0]!;
|
|
121
|
+
const remoteServers = shards
|
|
122
|
+
.map((_, s) => {
|
|
123
|
+
const hosts = layout.filter((l) => l.shard === `s${s + 1}`).map((l) => `<replica><host>${l.host}</host><port>9000</port></replica>`);
|
|
124
|
+
return `<shard><internal_replication>true</internal_replication>${hosts.join("")}</shard>`;
|
|
125
|
+
})
|
|
126
|
+
.join("");
|
|
103
127
|
const config = (node: string, i: number) => `<clickhouse>
|
|
104
128
|
${
|
|
105
129
|
i === 0
|
|
@@ -113,7 +137,9 @@ ${
|
|
|
113
137
|
`
|
|
114
138
|
: ""
|
|
115
139
|
} <zookeeper><node><host>${keeper}</host><port>9181</port></node></zookeeper>
|
|
116
|
-
<
|
|
140
|
+
<distributed_ddl><path>/clickhouse/task_queue/ddl</path></distributed_ddl>
|
|
141
|
+
<remote_servers><${clusterName}>${remoteServers}</${clusterName}></remote_servers>
|
|
142
|
+
<macros><shard>${layout[i]!.shard}</shard><replica>${layout[i]!.replica}</replica></macros>
|
|
117
143
|
<interserver_http_host>${node}</interserver_http_host>
|
|
118
144
|
</clickhouse>
|
|
119
145
|
`;
|
|
@@ -156,6 +182,8 @@ ${
|
|
|
156
182
|
};
|
|
157
183
|
return {
|
|
158
184
|
replicas: endpoints,
|
|
185
|
+
layout,
|
|
186
|
+
cluster: clusterName,
|
|
159
187
|
name,
|
|
160
188
|
stop,
|
|
161
189
|
async stopReplica(index) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift on a multi-shard cluster (#3664), against a scratch cluster of the
|
|
3
|
+
* pinned image: two shards (two replicas and one server) with Keeper and a
|
|
4
|
+
* cluster `chant` over all three, the profile naming the first server with
|
|
5
|
+
* topology `cluster:chant`.
|
|
6
|
+
*
|
|
7
|
+
* - The schema applied `ON CLUSTER`, with a `Distributed` table declared with
|
|
8
|
+
* unquoted identifiers, reads as no drift: the server's quoted arguments
|
|
9
|
+
* are the same arguments.
|
|
10
|
+
* - A TTL changed on shard 2's server alone, without `ON CLUSTER`, is drift,
|
|
11
|
+
* seen on that server, though the profile's server still holds the
|
|
12
|
+
* declaration.
|
|
13
|
+
*
|
|
14
|
+
* Runs the sql lexicon's deep reader and core's deep diff, as
|
|
15
|
+
* `chant lifecycle diff --live` does. Needs Docker; skips cleanly without it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
22
|
+
import { diffDeepObservation } from "@intentius/chant/lifecycle/deep-observe";
|
|
23
|
+
import { normalizeDeepObservation } from "@intentius/chant/deep-observation";
|
|
24
|
+
import { dockerAvailable, startScratchCluster, type ScratchCluster } from "../container";
|
|
25
|
+
import { clickhouseQuery } from "../http";
|
|
26
|
+
import { clickhouseImage } from "../../spec/pin";
|
|
27
|
+
import { clickhouseApply, toApplyResult } from "../../op/activities/clickhouse-apply";
|
|
28
|
+
import { normalizeApply } from "@intentius/chant/apply";
|
|
29
|
+
import { database, table } from "../entities";
|
|
30
|
+
import { observeResourcesDeep, sqlDeepNormalizationHooks } from "./deep";
|
|
31
|
+
|
|
32
|
+
const enabled = await dockerAvailable();
|
|
33
|
+
const DB = "chant_e2e_3664";
|
|
34
|
+
let cluster: ScratchCluster | undefined;
|
|
35
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-sql-cluster-drift-"));
|
|
36
|
+
|
|
37
|
+
const DB_DDL = `CREATE DATABASE ${DB}`;
|
|
38
|
+
const EVENTS_DDL = `CREATE TABLE ${DB}.events
|
|
39
|
+
(
|
|
40
|
+
id UInt64,
|
|
41
|
+
ts DateTime
|
|
42
|
+
)
|
|
43
|
+
ENGINE = MergeTree
|
|
44
|
+
ORDER BY id
|
|
45
|
+
TTL ts + INTERVAL 30 DAY`;
|
|
46
|
+
const EVENTS_ALL_DDL = `CREATE TABLE ${DB}.events_all
|
|
47
|
+
(
|
|
48
|
+
id UInt64,
|
|
49
|
+
ts DateTime
|
|
50
|
+
)
|
|
51
|
+
ENGINE = Distributed(chant, ${DB}, events, id)`;
|
|
52
|
+
|
|
53
|
+
const config = () => ({ ownership: { stack: "e2e", env: "test" }, sql: { profiles: { e2e: { url: cluster!.replicas[0]!.url, topology: "cluster:chant" } } } });
|
|
54
|
+
const tag = (fn: (strings: TemplateStringsArray) => unknown, ddl: string) => fn(Object.assign([ddl], { raw: [ddl] }) as unknown as TemplateStringsArray) as unknown as { entityType: string; props: Record<string, unknown> };
|
|
55
|
+
|
|
56
|
+
const ENTITIES = new Map([
|
|
57
|
+
["db", tag(database, DB_DDL)],
|
|
58
|
+
["events", tag(table, EVENTS_DDL)],
|
|
59
|
+
["eventsAll", tag(table, EVENTS_ALL_DDL)],
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
async function drift() {
|
|
63
|
+
const entities = new Map([...ENTITIES].map(([k, v]) => [k, { entityType: v.entityType, props: v.props }]));
|
|
64
|
+
const live = await observeResourcesDeep({ environment: "e2e", entityNames: [...entities.keys()], entities, config: config(), env: {} });
|
|
65
|
+
return diffDeepObservation(entities, normalizeDeepObservation(live), sqlDeepNormalizationHooks).drifted;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
if (!enabled) return;
|
|
70
|
+
cluster = await startScratchCluster(clickhouseImage(), { shards: [2, 1], cluster: "chant", namePrefix: "chant-sql-cluster-drift" });
|
|
71
|
+
}, 600_000);
|
|
72
|
+
|
|
73
|
+
afterAll(async () => {
|
|
74
|
+
await cluster?.stop();
|
|
75
|
+
rmSync(dir, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe.skipIf(!enabled)("drift on a two-shard cluster (#3664)", () => {
|
|
79
|
+
test("applied ON CLUSTER, with unquoted Distributed arguments, it reads as no drift", async () => {
|
|
80
|
+
const build = join(dir, "schema.json");
|
|
81
|
+
writeFileSync(
|
|
82
|
+
build,
|
|
83
|
+
JSON.stringify({
|
|
84
|
+
dialect: "clickhouse",
|
|
85
|
+
applyOrder: ["db", "events", "eventsAll"],
|
|
86
|
+
objects: [
|
|
87
|
+
{ export: "db", type: "ClickHouse::Database", ddl: DB_DDL, dependsOn: [] },
|
|
88
|
+
{ export: "events", type: "ClickHouse::Table", ddl: EVENTS_DDL, dependsOn: ["db"] },
|
|
89
|
+
{ export: "eventsAll", type: "ClickHouse::Table", ddl: EVENTS_ALL_DDL, dependsOn: ["events"] },
|
|
90
|
+
],
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
const applied = normalizeApply(toApplyResult(await clickhouseApply({ buildPath: build, environment: "e2e" }, undefined, { config: config(), env: {}, log: () => undefined })));
|
|
94
|
+
expect(applied.notAttempted).toEqual([]);
|
|
95
|
+
// Every server has both tables.
|
|
96
|
+
for (const endpoint of cluster!.replicas) {
|
|
97
|
+
const rows = await clickhouseQuery<{ name: string }>(endpoint, `SELECT name FROM system.tables WHERE database = '${DB}' ORDER BY name`);
|
|
98
|
+
expect(rows.map((r) => r.name)).toEqual(["events", "events_all"]);
|
|
99
|
+
}
|
|
100
|
+
expect(await drift()).toEqual([]);
|
|
101
|
+
}, 300_000);
|
|
102
|
+
|
|
103
|
+
test("a TTL changed on shard 2 alone is drift, seen on that server", async () => {
|
|
104
|
+
const shard2 = cluster!.layout.findIndex((l) => l.shard === "s2");
|
|
105
|
+
await clickhouseQuery(cluster!.replicas[shard2]!, `ALTER TABLE ${DB}.events MODIFY TTL ts + INTERVAL 7 DAY`);
|
|
106
|
+
// The profile's server still holds the declaration.
|
|
107
|
+
const [mine] = await clickhouseQuery<{ q: string }>(cluster!.replicas[0]!, `SELECT create_table_query AS q FROM system.tables WHERE database = '${DB}' AND name = 'events'`);
|
|
108
|
+
expect(mine!.q).toContain("toIntervalDay(30)");
|
|
109
|
+
|
|
110
|
+
const drifted = await drift();
|
|
111
|
+
expect(drifted.map((d) => d.name)).toEqual(["events"]);
|
|
112
|
+
const ttl = drifted[0]!.changes.find((c) => c.path.startsWith("ttl"));
|
|
113
|
+
expect(ttl, JSON.stringify(drifted[0]!.changes)).toBeDefined();
|
|
114
|
+
expect(String(ttl!.live)).toMatch(/7/);
|
|
115
|
+
expect(ttl!.seenOn).toBe(cluster!.layout[shard2]!.host);
|
|
116
|
+
}, 300_000);
|
|
117
|
+
});
|
|
@@ -12,12 +12,20 @@
|
|
|
12
12
|
* `deepNormalizationHooks` prunes what is not a property of the object (the
|
|
13
13
|
* DDL text, the template source, lineage, a view's reads and `TO` target).
|
|
14
14
|
* `chant sql plan` covers the target, and classifies every change.
|
|
15
|
+
*
|
|
16
|
+
* On a `cluster:<name>` topology every replica of every shard is read
|
|
17
|
+
* (#3664). When a server's copy of a table differs from the profile
|
|
18
|
+
* server's, that copy is what is reported, with the server in `observedOn`,
|
|
19
|
+
* which the drift report prints as `seen on`.
|
|
15
20
|
*/
|
|
16
21
|
|
|
17
22
|
import { deepObservation, type DeepObservationResult, type DeepResourceObservation, type DeepNormalizationHooks } from "@intentius/chant/deep-observation";
|
|
18
23
|
import type { UnobservedEntity } from "@intentius/chant/lexicon";
|
|
19
|
-
import { bindClickHouse, classifyClickHouseFailure, type BindOptions } from "../live/bind";
|
|
20
|
-
import { readLiveSchema, type LiveObject } from "../live/catalog";
|
|
24
|
+
import { bindClickHouse, classifyClickHouseFailure, type BindOptions, type ClickHouseTarget } from "../live/bind";
|
|
25
|
+
import { readLiveSchema, SYSTEM_DATABASES, type LiveObject } from "../live/catalog";
|
|
26
|
+
import { clickhouseQuery } from "../http";
|
|
27
|
+
import { sqlString } from "../apply/statements";
|
|
28
|
+
import { stripMarkerFromStatement } from "../ownership";
|
|
21
29
|
import { database, table, view, CLICKHOUSE_ENTITY_TYPES, type ColumnDef } from "../entities";
|
|
22
30
|
import { canonicalObject, type CanonicalColumn, type CanonicalObject } from "./normalize";
|
|
23
31
|
import { renderFor } from "../topology";
|
|
@@ -76,6 +84,73 @@ export function inDeclaredVocabulary(declared: Props, live: Props, d: CanonicalO
|
|
|
76
84
|
return out;
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
/** Every server's copy of each table and view on a cluster (#3664), as `<database>.<name>` → host → statement. */
|
|
88
|
+
interface ClusterCopies {
|
|
89
|
+
/** The host the profile names, whose copy the rest are compared with. */
|
|
90
|
+
self: string;
|
|
91
|
+
byKey: Map<string, Map<string, string>>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* On a `cluster:<name>` topology, read `system.tables` on every replica of
|
|
96
|
+
* every shard through the profile's server (`clusterAllReplicas`). A change
|
|
97
|
+
* made on one server without `ON CLUSTER` (a TTL altered on shard 2 alone)
|
|
98
|
+
* is then seen, which the profile's server alone would not show. Undefined
|
|
99
|
+
* for any other topology: there is one copy.
|
|
100
|
+
*/
|
|
101
|
+
async function clusterCopies(target: ClickHouseTarget): Promise<ClusterCopies | undefined> {
|
|
102
|
+
if (target.topology?.kind !== "cluster") return undefined;
|
|
103
|
+
const scope = target.databases
|
|
104
|
+
? `IN (${target.databases.map(sqlString).join(", ")})`
|
|
105
|
+
: `NOT IN (${[...SYSTEM_DATABASES].map(sqlString).join(", ")})`;
|
|
106
|
+
const [self] = await clickhouseQuery<{ host: string }>(target.endpoint, "SELECT hostName() AS host");
|
|
107
|
+
const rows = await clickhouseQuery<{ host: string; database: string; name: string; statement: string }>(
|
|
108
|
+
target.endpoint,
|
|
109
|
+
`SELECT hostName() AS host, database, name, create_table_query AS statement ` +
|
|
110
|
+
`FROM clusterAllReplicas(${sqlString(target.topology.cluster)}, system.tables) ` +
|
|
111
|
+
`WHERE database ${scope} AND NOT is_temporary AND name NOT LIKE '.inner%' AND engine != 'Dictionary' ORDER BY host, database, name`,
|
|
112
|
+
);
|
|
113
|
+
const byKey = new Map<string, Map<string, string>>();
|
|
114
|
+
for (const r of rows) {
|
|
115
|
+
const key = `${r.database}.${r.name}`;
|
|
116
|
+
if (!byKey.has(key)) byKey.set(key, new Map());
|
|
117
|
+
byKey.get(key)!.set(r.host, stripMarkerFromStatement(r.statement));
|
|
118
|
+
}
|
|
119
|
+
return { self: self?.host ?? "", byKey };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* When the servers' copies of `key` do not all read the same, the copy to
|
|
124
|
+
* report and its server: whichever departs furthest from the declaration
|
|
125
|
+
* `d`, the profile server's own winning a tie. A copy that differs from the
|
|
126
|
+
* declaration on any server is then the one the diff sees. Undefined when
|
|
127
|
+
* every copy reads the same.
|
|
128
|
+
*/
|
|
129
|
+
function copyToReport(copies: ClusterCopies, key: string, d: CanonicalObject, defaultDatabase: string): { host: string; statement: string } | undefined {
|
|
130
|
+
const byHost = copies.byKey.get(key);
|
|
131
|
+
const mine = byHost?.get(copies.self);
|
|
132
|
+
if (!byHost || mine === undefined) return undefined;
|
|
133
|
+
const shape = (c: CanonicalObject) => {
|
|
134
|
+
const { columns, ...rest } = c;
|
|
135
|
+
return JSON.stringify({ ...rest, columns: columns.map(({ text: _t, position: _p, ...col }) => col) });
|
|
136
|
+
};
|
|
137
|
+
const reference = shape(canonicalObject(mine, defaultDatabase));
|
|
138
|
+
const declared = JSON.parse(shape(d)) as Record<string, unknown>;
|
|
139
|
+
/** How many of the declaration's fields this copy holds differently. */
|
|
140
|
+
const departures = (statement: string) => {
|
|
141
|
+
const live = JSON.parse(shape(canonicalObject(statement, defaultDatabase))) as Record<string, unknown>;
|
|
142
|
+
return Object.keys({ ...declared, ...live }).filter((k) => k !== "name" && k !== "database" && JSON.stringify(declared[k]) !== JSON.stringify(live[k])).length;
|
|
143
|
+
};
|
|
144
|
+
let pick: { host: string; statement: string; score: number } | undefined;
|
|
145
|
+
for (const [host, statement] of byHost) {
|
|
146
|
+
if (host === copies.self || shape(canonicalObject(statement, defaultDatabase)) === reference) continue;
|
|
147
|
+
const score = departures(statement);
|
|
148
|
+
if (!pick || score > pick.score) pick = { host, statement, score };
|
|
149
|
+
}
|
|
150
|
+
if (!pick) return undefined;
|
|
151
|
+
return pick.score > departures(mine) ? { host: pick.host, statement: pick.statement } : { host: copies.self, statement: mine };
|
|
152
|
+
}
|
|
153
|
+
|
|
79
154
|
export async function observeResourcesDeep(
|
|
80
155
|
options: {
|
|
81
156
|
environment: string;
|
|
@@ -98,6 +173,17 @@ export async function observeResourcesDeep(
|
|
|
98
173
|
return deepObservation({}, unobserved);
|
|
99
174
|
}
|
|
100
175
|
const byKey = new Map(live.map((o) => [`${o.database ?? ""}.${o.name}`, o]));
|
|
176
|
+
let copies: ClusterCopies | undefined;
|
|
177
|
+
try {
|
|
178
|
+
copies = await clusterCopies(target);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// A cluster whose servers cannot all be read is not reported as matching.
|
|
181
|
+
const why = classifyClickHouseFailure(err);
|
|
182
|
+
for (const name of options.entityNames) {
|
|
183
|
+
unobserved[name] = { type: options.entities.get(name)?.entityType ?? "", reason: why.reason, detail: `reading every server of the cluster: ${why.detail}` };
|
|
184
|
+
}
|
|
185
|
+
return deepObservation({}, unobserved);
|
|
186
|
+
}
|
|
101
187
|
for (const name of options.entityNames) {
|
|
102
188
|
const entity = options.entities.get(name);
|
|
103
189
|
if (!entity || !entity.entityType.startsWith("ClickHouse::")) {
|
|
@@ -111,10 +197,23 @@ export async function observeResourcesDeep(
|
|
|
111
197
|
if (!o) continue;
|
|
112
198
|
try {
|
|
113
199
|
const d = canonicalObject(renderFor(String(props.ddl), target.topology), target.defaultDatabase);
|
|
200
|
+
const physicalId = o.uuid ?? `${db ? `${db}.` : ""}${o.name}`;
|
|
201
|
+
const other = copies && !isDb ? copyToReport(copies, `${db ?? ""}.${o.name}`, d, target.defaultDatabase) : undefined;
|
|
202
|
+
if (other) {
|
|
203
|
+
// The servers disagree: report the copy that departs from the declaration, and where it was seen.
|
|
204
|
+
const copy: LiveObject = { ...o, statement: other.host === copies!.self ? o.statement : other.statement };
|
|
205
|
+
resources[name] = {
|
|
206
|
+
type: o.type,
|
|
207
|
+
physicalId,
|
|
208
|
+
properties: inDeclaredVocabulary(props, liveProps(copy), d, canonicalObject(copy.statement, target.defaultDatabase)),
|
|
209
|
+
observedOn: other.host,
|
|
210
|
+
};
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
114
213
|
const l = canonicalObject(o.statement, target.defaultDatabase);
|
|
115
214
|
resources[name] = {
|
|
116
215
|
type: o.type,
|
|
117
|
-
physicalId
|
|
216
|
+
physicalId,
|
|
118
217
|
properties: inDeclaredVocabulary(props, liveProps(o), d, l),
|
|
119
218
|
};
|
|
120
219
|
} catch (err) {
|
|
@@ -284,6 +284,10 @@ export function canonicalObject(ddl: string, defaultDatabase = "default"): Canon
|
|
|
284
284
|
name = shared[1]!;
|
|
285
285
|
if (/^'.*'$/s.test(args[0] ?? "") && /^'.*'$/s.test(args[1] ?? "")) args = args.slice(2);
|
|
286
286
|
}
|
|
287
|
+
// `Distributed(cluster, db, table, ...)` takes its first three arguments
|
|
288
|
+
// as identifiers or as strings, and the server prints them back quoted
|
|
289
|
+
// (#3664): an unquoted declaration says the same.
|
|
290
|
+
if (name === "Distributed") args = args.map((a, i) => (i < 3 ? quotedIdentifier(a) : a));
|
|
287
291
|
obj.engineName = name;
|
|
288
292
|
obj.engine = args.length ? `${name}(${args.join(", ")})` : name;
|
|
289
293
|
}
|
|
@@ -359,3 +363,9 @@ function column(tokens: Token[], c: ColumnNode, position: number, database?: str
|
|
|
359
363
|
if (prev) out.previously = prev;
|
|
360
364
|
return out;
|
|
361
365
|
}
|
|
366
|
+
|
|
367
|
+
/** A bare or backquoted identifier as the string literal the server prints it as; anything else as it is. */
|
|
368
|
+
function quotedIdentifier(arg: string): string {
|
|
369
|
+
const bare = /^[A-Za-z_][A-Za-z0-9_]*$/.test(arg) ? arg : /^`((?:[^`]|``)+)`$/.exec(arg)?.[1]?.replace(/``/g, "`");
|
|
370
|
+
return bare === undefined ? arg : `'${bare.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
371
|
+
}
|
|
@@ -87,6 +87,16 @@ describe("normalization", () => {
|
|
|
87
87
|
expect(shown("ReplicatedMergeTree('/clickhouse/tables/{shard}/t', '{replica}')")).not.toBe(shown("ReplicatedMergeTree"));
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test("a Distributed engine's cluster, database and table read the same quoted or not (#3664)", () => {
|
|
91
|
+
const shown = (engine: string) => canonicalObject(`CREATE TABLE d.events_all (id UInt64) ENGINE = ${engine}`).engine;
|
|
92
|
+
expect(shown("Distributed(my_cluster, d, events, id)")).toBe(shown("Distributed('my_cluster', 'd', 'events', id)"));
|
|
93
|
+
expect(shown("Distributed(`my_cluster`, `d`, `events`)")).toBe(shown("Distributed('my_cluster', 'd', 'events')"));
|
|
94
|
+
expect(shown("Distributed('{cluster}', currentDatabase(), events, rand())")).toBe(shown("Distributed('{cluster}', currentDatabase(), 'events', rand())"));
|
|
95
|
+
// The sharding key is an expression, not a name: it stays as written.
|
|
96
|
+
expect(shown("Distributed(c, d, events, id)")).not.toBe(shown("Distributed(c, d, events, 'id')"));
|
|
97
|
+
expect(shown("Distributed(c, d, events)")).not.toBe(shown("Distributed(c, d, other)"));
|
|
98
|
+
});
|
|
99
|
+
|
|
90
100
|
test("a setting at its default is not part of the definition", () => {
|
|
91
101
|
expect(canonicalObject("CREATE TABLE t (a UInt8) ENGINE = MergeTree ORDER BY a SETTINGS index_granularity = 8192").settings).toEqual({});
|
|
92
102
|
expect(canonicalObject("CREATE TABLE t (a UInt8) ENGINE = MergeTree ORDER BY a SETTINGS min_bytes_for_wide_part = '0'").settings).toEqual({ min_bytes_for_wide_part: "0" });
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
* either finds nothing that replica alone has and returns, or stops the step
|
|
43
43
|
* naming it (`./replicas.ts`).
|
|
44
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
|
+
*
|
|
45
50
|
* In materialized-view mode a partition is copied in two parts
|
|
46
51
|
* (INTENTIUS/sql-yodeler#45). The rows before the cut-over are copied as
|
|
47
52
|
* above. The rows at or after it are the view's only when they were written
|
|
@@ -64,9 +69,11 @@ import { ident, sqlString } from "../apply/statements";
|
|
|
64
69
|
import { clickhouseReceiptStore, receiptAddress, type ClickHouseReceiptStore } from "./receipts";
|
|
65
70
|
import { RebuildRefusal, type RebuildObservation } from "./observe";
|
|
66
71
|
import { DDL_SETTINGS } from "./replicas";
|
|
67
|
-
import { sourcePartitionExpression, sourcePartitions } from "./partitions";
|
|
72
|
+
import { shardPartitions, sourcePartitionExpression, sourcePartitions } from "./partitions";
|
|
68
73
|
import { rowHash } from "./verify";
|
|
69
74
|
import { cutoverOf, observe, serverNow, syncReplica, utcLiteral, waitOn, type RebuildRun } from "./steps";
|
|
75
|
+
import { intoShard, onShard, onShardServers, shardTable, SHARD_DELETE_SETTINGS, SHARD_INSERT_SETTINGS, type Shard } from "./shards";
|
|
76
|
+
import { renderFor } from "../topology";
|
|
70
77
|
|
|
71
78
|
export interface BackfillResult {
|
|
72
79
|
state: RebuildObservation["state"];
|
|
@@ -83,11 +90,11 @@ export interface BackfillResult {
|
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/** The effect a partition's copy is, and its receipt's address suffix. */
|
|
86
|
-
export const partitionEffect = (key: string, partition: string): string => `rebuild/${key}/${partition}
|
|
93
|
+
export const partitionEffect = (key: string, partition: string, shard?: Shard): string => (shard ? `rebuild/${key}/shard${shard.num}/${partition}` : `rebuild/${key}/${partition}`);
|
|
87
94
|
|
|
88
|
-
/** The receipt's expected value for one partition's copy. */
|
|
89
|
-
export function partitionExpectation(o: RebuildObservation, partition: string, cutover: number | undefined): string {
|
|
90
|
-
const effect = partitionEffect(o.names.key, partition);
|
|
95
|
+
/** The receipt's expected value for one partition's copy, on one shard of a cluster when `shard` is given. */
|
|
96
|
+
export function partitionExpectation(o: RebuildObservation, partition: string, cutover: number | undefined, shard?: Shard): string {
|
|
97
|
+
const effect = partitionEffect(o.names.key, partition, shard);
|
|
91
98
|
return receiptExpectation(
|
|
92
99
|
EffectReceipt(effect, {
|
|
93
100
|
effect,
|
|
@@ -96,6 +103,7 @@ export function partitionExpectation(o: RebuildObservation, partition: string, c
|
|
|
96
103
|
table: o.names.key,
|
|
97
104
|
newTable: o.newTable!.uuid,
|
|
98
105
|
partition,
|
|
106
|
+
...(shard ? { shard: shard.num } : {}),
|
|
99
107
|
copied: o.copied,
|
|
100
108
|
cutover: cutover === undefined ? null : new Date(cutover).toISOString(),
|
|
101
109
|
// 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.
|
|
@@ -108,8 +116,8 @@ export function partitionExpectation(o: RebuildObservation, partition: string, c
|
|
|
108
116
|
export interface BackfillDeps {
|
|
109
117
|
/** The receipt store. Default: the receipts table on the target server. */
|
|
110
118
|
receipts?: ClickHouseReceiptStore;
|
|
111
|
-
/** Called after each partition is copied and its receipt written; a test interrupts the backfill here. */
|
|
112
|
-
afterPartition?: (partition: string) => void | Promise<void>;
|
|
119
|
+
/** Called after each partition is copied and its receipt written, with its shard on a cluster of shards; a test interrupts the backfill here. */
|
|
120
|
+
afterPartition?: (partition: string, shard?: number) => void | Promise<void>;
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
export async function backfill(run: RebuildRun, deps: BackfillDeps = {}): Promise<BackfillResult> {
|
|
@@ -140,6 +148,12 @@ export async function backfill(run: RebuildRun, deps: BackfillDeps = {}): Promis
|
|
|
140
148
|
const columns = o.copied.map((c) => ident(c.name)).join(", ");
|
|
141
149
|
const select = o.copied.map((c) => ident(c.source)).join(", ");
|
|
142
150
|
const later = cutColumn && cutover !== undefined ? await laterRowsCopy(run, o, cutColumn, cutover, fromOld) : undefined;
|
|
151
|
+
const sharding = run.sharding;
|
|
152
|
+
// The old and new tables as one shard's rows (#3663, `./shards.ts`), or as this server's.
|
|
153
|
+
const oldFrom = (shard?: Shard) => (shard && sharding ? `${shardTable(sharding, n.database, n.name)} WHERE ${onShard(shard)} AND` : `${n.table} WHERE`);
|
|
154
|
+
const newFrom = (shard?: Shard) => (shard && sharding ? `${shardTable(sharding, n.database, n.newName)} WHERE ${onShard(shard)} AND` : `${n.newTable} WHERE`);
|
|
155
|
+
const into = (shard?: Shard) => (shard && sharding ? intoShard(sharding, shard, n.database, n.newName) : n.newTable);
|
|
156
|
+
const insertSettings = sharding ? SHARD_INSERT_SETTINGS : {};
|
|
143
157
|
|
|
144
158
|
const identity = { ...(run.marker?.stack ? { stack: run.marker.stack } : {}), ...(run.marker?.env ? { env: run.marker.env } : {}) };
|
|
145
159
|
const receipts =
|
|
@@ -152,21 +166,31 @@ export async function backfill(run: RebuildRun, deps: BackfillDeps = {}): Promis
|
|
|
152
166
|
});
|
|
153
167
|
const recorded = await receipts.readAll(receiptAddress(identity, `rebuild/${n.key}/`));
|
|
154
168
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
// The unit of work: a partition of the old table, on each shard of a cluster that has more than one.
|
|
170
|
+
const units: Array<{ shard?: Shard; partition: string }> = sharding
|
|
171
|
+
? (await shardPartitions(run, sharding, n.database, n.name)).map((u) => ({ shard: sharding.shards.find((x) => x.num === u.shard)!, partition: u.partition }))
|
|
172
|
+
: (await sourcePartitions(run.target, n)).map((partition) => ({ partition }));
|
|
173
|
+
result.partitions = units.length;
|
|
174
|
+
for (const { shard, partition: p } of units) {
|
|
158
175
|
run.signal?.throwIfAborted();
|
|
159
|
-
const effect = partitionEffect(n.key, p);
|
|
160
|
-
const expectation = partitionExpectation(o, p, cutover);
|
|
176
|
+
const effect = partitionEffect(n.key, p, shard);
|
|
177
|
+
const expectation = partitionExpectation(o, p, cutover, shard);
|
|
161
178
|
if (recorded.get(receiptAddress(identity, effect)) === expectation) {
|
|
162
179
|
result.skipped++;
|
|
163
180
|
continue;
|
|
164
181
|
}
|
|
165
182
|
|
|
166
|
-
const queryId = `chant-rebuild-${o.newTable.uuid}-${p}`;
|
|
183
|
+
const queryId = `chant-rebuild-${o.newTable.uuid}-${shard ? `shard${shard.num}-` : ""}${p}`;
|
|
167
184
|
const laterQueryId = `${queryId}-after`;
|
|
168
185
|
const killWhere = `query_id IN (${sqlString(queryId)}, ${sqlString(laterQueryId)})`;
|
|
169
|
-
if (
|
|
186
|
+
if (sharding) {
|
|
187
|
+
// The copy reads and writes every shard from this server; what it started on the others goes with it.
|
|
188
|
+
await clickhouseQuery(
|
|
189
|
+
run.target.endpoint,
|
|
190
|
+
`KILL QUERY ON CLUSTER ${sqlString(sharding.cluster)} WHERE ${killWhere} OR initial_query_id IN (${sqlString(queryId)}, ${sqlString(laterQueryId)}) SYNC`,
|
|
191
|
+
{ settings: DDL_SETTINGS },
|
|
192
|
+
);
|
|
193
|
+
} else if (o.replicated) {
|
|
170
194
|
// On every replica that answers: the copy may be running on another one
|
|
171
195
|
// than this run's. A replica that is down is skipped once Keeper sees it
|
|
172
196
|
// inactive; nothing of its can be running a copy that still writes,
|
|
@@ -179,24 +203,28 @@ export async function backfill(run: RebuildRun, deps: BackfillDeps = {}): Promis
|
|
|
179
203
|
await clickhouseQuery(run.target.endpoint, `KILL QUERY WHERE ${killWhere} SYNC`);
|
|
180
204
|
}
|
|
181
205
|
const mine = `${fromOld} = ${sqlString(p)}${targetRange}`;
|
|
182
|
-
const
|
|
206
|
+
const where = shard ? `partition ${p} on shard ${shard.num}` : `partition ${p}`;
|
|
207
|
+
const [left] = await clickhouseQuery<{ n: string | number }>(run.target.endpoint, `SELECT count() AS n FROM ${newFrom(shard)} ${mine}`);
|
|
183
208
|
if (Number(left?.n ?? 0) > 0) {
|
|
184
|
-
run.log(`--
|
|
185
|
-
const sql =
|
|
209
|
+
run.log(`-- ${where}: ${left!.n} row(s) from a copy that did not finish; deleting them before copying again`);
|
|
210
|
+
const sql =
|
|
211
|
+
shard && sharding
|
|
212
|
+
? renderFor(`ALTER TABLE ${n.newTable} DELETE WHERE ${onShardServers(shard)} AND ${mine}`, run.target.topology)
|
|
213
|
+
: `ALTER TABLE ${n.newTable} DELETE WHERE ${mine}`;
|
|
186
214
|
run.log(sql);
|
|
187
|
-
await clickhouseQuery(run.target.endpoint, sql);
|
|
215
|
+
await clickhouseQuery(run.target.endpoint, sql, shard && sharding ? { settings: { ...DDL_SETTINGS, ...SHARD_DELETE_SETTINGS } } : {});
|
|
188
216
|
await waitOn(run, n.database, n.newName);
|
|
189
217
|
result.cleared++;
|
|
190
218
|
}
|
|
191
219
|
|
|
192
|
-
const sql = `INSERT INTO ${
|
|
220
|
+
const sql = `INSERT INTO ${into(shard)} (${columns}) SELECT ${select} FROM ${oldFrom(shard)} _partition_id = ${sqlString(p)}${sourceRange}`;
|
|
193
221
|
run.log(sql);
|
|
194
|
-
await clickhouseQuery(run.target.endpoint, sql, { queryId, settings: { async_insert: "0", insert_deduplicate: "0" }, ...(run.signal ? { signal: run.signal } : {}) });
|
|
195
|
-
if (later) result.repaired += await copyAfterCutover(run, o, later, p, laterQueryId);
|
|
222
|
+
await clickhouseQuery(run.target.endpoint, sql, { queryId, settings: { async_insert: "0", insert_deduplicate: "0", ...insertSettings }, ...(run.signal ? { signal: run.signal } : {}) });
|
|
223
|
+
if (later) result.repaired += await copyAfterCutover(run, o, later, p, laterQueryId, shard);
|
|
196
224
|
// The receipt, last, on success only.
|
|
197
225
|
await receipts.write({ name: effect, effect, flavor: "hash", inputs: {} }, expectation);
|
|
198
226
|
result.copied++;
|
|
199
|
-
await deps.afterPartition?.(p);
|
|
227
|
+
await deps.afterPartition?.(p, shard?.num);
|
|
200
228
|
}
|
|
201
229
|
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` : ""}`);
|
|
202
230
|
return result;
|
|
@@ -206,13 +234,14 @@ export async function backfill(run: RebuildRun, deps: BackfillDeps = {}): Promis
|
|
|
206
234
|
interface LaterRows {
|
|
207
235
|
/**
|
|
208
236
|
* Per distinct row (by its hash), the old table's copies past the number
|
|
209
|
-
* the new table holds, into the new table.
|
|
237
|
+
* the new table holds, into the new table. On a cluster of shards, one
|
|
238
|
+
* shard's rows into the same shard.
|
|
210
239
|
*/
|
|
211
|
-
copy: (partition: string) => string;
|
|
240
|
+
copy: (partition: string, shard?: Shard) => string;
|
|
212
241
|
/** The hashes of the rows the new table holds more copies of than the old one. */
|
|
213
|
-
extras: (partition: string) => string;
|
|
242
|
+
extras: (partition: string, shard?: Shard) => string;
|
|
214
243
|
/** Delete every copy of those rows from the new table. */
|
|
215
|
-
drop: (partition: string, hashes: readonly string[]) => string;
|
|
244
|
+
drop: (partition: string, hashes: readonly string[], shard?: Shard) => string;
|
|
216
245
|
}
|
|
217
246
|
|
|
218
247
|
/** The most extra rows one partition's repair deletes and copies again; past that the verification decides. */
|
|
@@ -245,19 +274,28 @@ async function laterRowsCopy(
|
|
|
245
274
|
const columns = o.copied.map((c) => ident(c.name)).join(", ");
|
|
246
275
|
const aliases = o.copied.map((_, i) => `__chant_c${i}`);
|
|
247
276
|
const fromOldSide = o.copied.map((c, i) => `${ident(c.source)} AS ${aliases[i]}`).join(", ");
|
|
248
|
-
const
|
|
249
|
-
const
|
|
277
|
+
const sharding = run.sharding;
|
|
278
|
+
const on = (shard?: Shard) => (shard && sharding ? ` AND ${onShard(shard)}` : "");
|
|
279
|
+
const oldTable = (shard?: Shard) => (shard && sharding ? shardTable(sharding, n.database, n.name) : n.table);
|
|
280
|
+
const newTable = (shard?: Shard) => (shard && sharding ? shardTable(sharding, n.database, n.newName) : n.newTable);
|
|
281
|
+
const oldRows = (p: string, shard?: Shard) => `FROM ${oldTable(shard)} WHERE _partition_id = ${sqlString(p)} AND ${ident(cut.source)} >= ${utcLiteral(cutover)}${on(shard)}`;
|
|
282
|
+
const newRows = (p: string, shard?: Shard) => `FROM ${newTable(shard)} WHERE ${fromOld} = ${sqlString(p)} AND ${ident(cut.name)} >= ${utcLiteral(cutover)}${on(shard)}`;
|
|
250
283
|
return {
|
|
251
|
-
copy: (p) =>
|
|
252
|
-
`INSERT INTO ${n.newTable} (${columns}) SELECT ${aliases.join(", ")} FROM (` +
|
|
253
|
-
`SELECT ${fromOldSide}, ${oldHash} AS __chant_h, row_number() OVER (PARTITION BY __chant_h) AS __chant_k ${oldRows(p)}` +
|
|
254
|
-
`) AS old_rows LEFT JOIN (SELECT ${newHash} AS __chant_h, count() AS __chant_n ${newRows(p)} GROUP BY __chant_h) AS new_rows ` +
|
|
284
|
+
copy: (p, shard) =>
|
|
285
|
+
`INSERT INTO ${shard && sharding ? intoShard(sharding, shard, n.database, n.newName) : n.newTable} (${columns}) SELECT ${aliases.join(", ")} FROM (` +
|
|
286
|
+
`SELECT ${fromOldSide}, ${oldHash} AS __chant_h, row_number() OVER (PARTITION BY __chant_h) AS __chant_k ${oldRows(p, shard)}` +
|
|
287
|
+
`) AS old_rows LEFT JOIN (SELECT ${newHash} AS __chant_h, count() AS __chant_n ${newRows(p, shard)} GROUP BY __chant_h) AS new_rows ` +
|
|
255
288
|
`USING (__chant_h) WHERE __chant_k > ifNull(__chant_n, 0)`,
|
|
256
|
-
extras: (p) =>
|
|
257
|
-
`SELECT toString(__chant_h) AS h FROM (SELECT ${newHash} AS __chant_h, count() AS __chant_n ${newRows(p)} GROUP BY __chant_h) AS new_rows ` +
|
|
258
|
-
`LEFT JOIN (SELECT ${oldHash} AS __chant_h, count() AS __chant_o ${oldRows(p)} GROUP BY __chant_h) AS old_rows ` +
|
|
289
|
+
extras: (p, shard) =>
|
|
290
|
+
`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 ` +
|
|
291
|
+
`LEFT JOIN (SELECT ${oldHash} AS __chant_h, count() AS __chant_o ${oldRows(p, shard)} GROUP BY __chant_h) AS old_rows ` +
|
|
259
292
|
`USING (__chant_h) WHERE __chant_n > ifNull(__chant_o, 0) LIMIT ${REPAIR_LIMIT + 1}`,
|
|
260
|
-
drop: (p, hashes) =>
|
|
293
|
+
drop: (p, hashes, shard) => {
|
|
294
|
+
const where = `${fromOld} = ${sqlString(p)} AND ${ident(cut.name)} >= ${utcLiteral(cutover)} AND ${newHash} IN (${hashes.join(", ")})`;
|
|
295
|
+
return shard && sharding
|
|
296
|
+
? renderFor(`ALTER TABLE ${n.newTable} DELETE WHERE ${onShardServers(shard)} AND ${where}`, run.target.topology)
|
|
297
|
+
: `ALTER TABLE ${n.newTable} DELETE WHERE ${where}`;
|
|
298
|
+
},
|
|
261
299
|
};
|
|
262
300
|
}
|
|
263
301
|
|
|
@@ -269,23 +307,28 @@ async function laterRowsCopy(
|
|
|
269
307
|
* table and the difference copied again, a few rounds at most. What is left
|
|
270
308
|
* after that, the verification finds. Returns the rows repaired.
|
|
271
309
|
*/
|
|
272
|
-
async function copyAfterCutover(run: RebuildRun, o: RebuildObservation, later: LaterRows, p: string, queryId: string): Promise<number> {
|
|
310
|
+
async function copyAfterCutover(run: RebuildRun, o: RebuildObservation, later: LaterRows, p: string, queryId: string, shard?: Shard): Promise<number> {
|
|
273
311
|
const n = o.names;
|
|
312
|
+
const sharded = shard !== undefined && run.sharding !== undefined;
|
|
274
313
|
const insert = async () => {
|
|
275
|
-
const sql = later.copy(p);
|
|
314
|
+
const sql = later.copy(p, shard);
|
|
276
315
|
run.log(sql);
|
|
277
|
-
await clickhouseQuery(run.target.endpoint, sql, {
|
|
316
|
+
await clickhouseQuery(run.target.endpoint, sql, {
|
|
317
|
+
queryId,
|
|
318
|
+
settings: { async_insert: "0", insert_deduplicate: "0", ...(sharded ? SHARD_INSERT_SETTINGS : {}) },
|
|
319
|
+
...(run.signal ? { signal: run.signal } : {}),
|
|
320
|
+
});
|
|
278
321
|
};
|
|
279
322
|
await insert();
|
|
280
323
|
let repaired = 0;
|
|
281
324
|
for (let round = 0; round < REPAIR_ROUNDS; round++) {
|
|
282
325
|
await syncReplica(run, o, n.database, n.newName);
|
|
283
|
-
const extras = (await clickhouseQuery<{ h: string }>(run.target.endpoint, later.extras(p))).map((r) => r.h);
|
|
326
|
+
const extras = (await clickhouseQuery<{ h: string }>(run.target.endpoint, later.extras(p, shard))).map((r) => r.h);
|
|
284
327
|
if (extras.length === 0 || extras.length > REPAIR_LIMIT) break;
|
|
285
328
|
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`);
|
|
286
|
-
const sql = later.drop(p, extras);
|
|
329
|
+
const sql = later.drop(p, extras, shard);
|
|
287
330
|
run.log(sql);
|
|
288
|
-
await clickhouseQuery(run.target.endpoint, sql);
|
|
331
|
+
await clickhouseQuery(run.target.endpoint, sql, sharded ? { settings: { ...DDL_SETTINGS, ...SHARD_DELETE_SETTINGS } } : {});
|
|
289
332
|
await waitOn(run, n.database, n.newName);
|
|
290
333
|
repaired += extras.length;
|
|
291
334
|
await insert();
|