@intentius/sql-yodeler 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +110 -2
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +14 -0
- package/dist/bin.js.map +1 -0
- package/dist/chant/apply.d.ts +51 -0
- package/dist/chant/apply.js +104 -0
- package/dist/chant/apply.js.map +1 -0
- package/dist/chant/build.d.ts +34 -0
- package/dist/chant/build.js +48 -0
- package/dist/chant/build.js.map +1 -0
- package/dist/chant/drift.d.ts +44 -0
- package/dist/chant/drift.js +91 -0
- package/dist/chant/drift.js.map +1 -0
- package/dist/chant/plan.d.ts +45 -0
- package/dist/chant/plan.js +61 -0
- package/dist/chant/plan.js.map +1 -0
- package/dist/chant/process.d.ts +41 -0
- package/dist/chant/process.js +87 -0
- package/dist/chant/process.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +59 -0
- package/dist/cli.js.map +1 -0
- package/dist/command.d.ts +17 -0
- package/dist/command.js +13 -0
- package/dist/command.js.map +1 -0
- package/dist/commands/apply.d.ts +3 -0
- package/dist/commands/apply.js +306 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/drift.d.ts +3 -0
- package/dist/commands/drift.js +38 -0
- package/dist/commands/drift.js.map +1 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.js +22 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +135 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +3 -0
- package/dist/commands/lint.js +193 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/new.d.ts +3 -0
- package/dist/commands/new.js +168 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +171 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/rebase.d.ts +3 -0
- package/dist/commands/rebase.js +167 -0
- package/dist/commands/rebase.js.map +1 -0
- package/dist/commands/repair.d.ts +3 -0
- package/dist/commands/repair.js +117 -0
- package/dist/commands/repair.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +54 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/config/define.d.ts +50 -0
- package/dist/config/define.js +20 -0
- package/dist/config/define.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +12 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +102 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/settings.d.ts +40 -0
- package/dist/config/settings.js +88 -0
- package/dist/config/settings.js.map +1 -0
- package/dist/env-args.d.ts +18 -0
- package/dist/env-args.js +62 -0
- package/dist/env-args.js.map +1 -0
- package/dist/history/client.d.ts +17 -0
- package/dist/history/client.js +19 -0
- package/dist/history/client.js.map +1 -0
- package/dist/history/ddl.d.ts +58 -0
- package/dist/history/ddl.js +100 -0
- package/dist/history/ddl.js.map +1 -0
- package/dist/history/index.d.ts +25 -0
- package/dist/history/index.js +26 -0
- package/dist/history/index.js.map +1 -0
- package/dist/history/lock.d.ts +100 -0
- package/dist/history/lock.js +224 -0
- package/dist/history/lock.js.map +1 -0
- package/dist/history/postgres/ddl.d.ts +22 -0
- package/dist/history/postgres/ddl.js +47 -0
- package/dist/history/postgres/ddl.js.map +1 -0
- package/dist/history/postgres/index.d.ts +12 -0
- package/dist/history/postgres/index.js +13 -0
- package/dist/history/postgres/index.js.map +1 -0
- package/dist/history/postgres/lock.d.ts +48 -0
- package/dist/history/postgres/lock.js +95 -0
- package/dist/history/postgres/lock.js.map +1 -0
- package/dist/history/postgres/store.d.ts +39 -0
- package/dist/history/postgres/store.js +68 -0
- package/dist/history/postgres/store.js.map +1 -0
- package/dist/history/rows.d.ts +96 -0
- package/dist/history/rows.js +73 -0
- package/dist/history/rows.js.map +1 -0
- package/dist/history/store.d.ts +72 -0
- package/dist/history/store.js +134 -0
- package/dist/history/store.js.map +1 -0
- package/dist/history/topology.d.ts +41 -0
- package/dist/history/topology.js +54 -0
- package/dist/history/topology.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/init/adopt.d.ts +70 -0
- package/dist/init/adopt.js +138 -0
- package/dist/init/adopt.js.map +1 -0
- package/dist/init/baseline.d.ts +61 -0
- package/dist/init/baseline.js +88 -0
- package/dist/init/baseline.js.map +1 -0
- package/dist/init/files.d.ts +23 -0
- package/dist/init/files.js +77 -0
- package/dist/init/files.js.map +1 -0
- package/dist/init/index.d.ts +16 -0
- package/dist/init/index.js +17 -0
- package/dist/init/index.js.map +1 -0
- package/dist/init/live.d.ts +28 -0
- package/dist/init/live.js +45 -0
- package/dist/init/live.js.map +1 -0
- package/dist/lint/check.d.ts +81 -0
- package/dist/lint/check.js +208 -0
- package/dist/lint/check.js.map +1 -0
- package/dist/lint/config.d.ts +21 -0
- package/dist/lint/config.js +43 -0
- package/dist/lint/config.js.map +1 -0
- package/dist/lint/index.d.ts +31 -0
- package/dist/lint/index.js +43 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/project-rules.d.ts +19 -0
- package/dist/lint/project-rules.js +32 -0
- package/dist/lint/project-rules.js.map +1 -0
- package/dist/lint/report.d.ts +7 -0
- package/dist/lint/report.js +48 -0
- package/dist/lint/report.js.map +1 -0
- package/dist/lint/rules.d.ts +53 -0
- package/dist/lint/rules.js +42 -0
- package/dist/lint/rules.js.map +1 -0
- package/dist/lint/silences.d.ts +49 -0
- package/dist/lint/silences.js +91 -0
- package/dist/lint/silences.js.map +1 -0
- package/dist/lint/statements.d.ts +38 -0
- package/dist/lint/statements.js +120 -0
- package/dist/lint/statements.js.map +1 -0
- package/dist/migrations/checksum.d.ts +56 -0
- package/dist/migrations/checksum.js +72 -0
- package/dist/migrations/checksum.js.map +1 -0
- package/dist/migrations/create.d.ts +73 -0
- package/dist/migrations/create.js +119 -0
- package/dist/migrations/create.js.map +1 -0
- package/dist/migrations/directory.d.ts +119 -0
- package/dist/migrations/directory.js +225 -0
- package/dist/migrations/directory.js.map +1 -0
- package/dist/migrations/document.d.ts +83 -0
- package/dist/migrations/document.js +82 -0
- package/dist/migrations/document.js.map +1 -0
- package/dist/migrations/index.d.ts +20 -0
- package/dist/migrations/index.js +21 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/pr/comment.d.ts +32 -0
- package/dist/pr/comment.js +99 -0
- package/dist/pr/comment.js.map +1 -0
- package/dist/pr/forge.d.ts +50 -0
- package/dist/pr/forge.js +105 -0
- package/dist/pr/forge.js.map +1 -0
- package/dist/pr/model.d.ts +91 -0
- package/dist/pr/model.js +113 -0
- package/dist/pr/model.js.map +1 -0
- package/dist/pr/render.d.ts +27 -0
- package/dist/pr/render.js +246 -0
- package/dist/pr/render.js.map +1 -0
- package/dist/project.d.ts +62 -0
- package/dist/project.js +92 -0
- package/dist/project.js.map +1 -0
- package/dist/rebase/index.d.ts +121 -0
- package/dist/rebase/index.js +390 -0
- package/dist/rebase/index.js.map +1 -0
- package/dist/rebase/repair.d.ts +41 -0
- package/dist/rebase/repair.js +68 -0
- package/dist/rebase/repair.js.map +1 -0
- package/dist/replay/clickhouse.d.ts +33 -0
- package/dist/replay/clickhouse.js +106 -0
- package/dist/replay/clickhouse.js.map +1 -0
- package/dist/replay/index.d.ts +36 -0
- package/dist/replay/index.js +54 -0
- package/dist/replay/index.js.map +1 -0
- package/dist/replay/postgres.d.ts +31 -0
- package/dist/replay/postgres.js +140 -0
- package/dist/replay/postgres.js.map +1 -0
- package/dist/replay/replay.d.ts +114 -0
- package/dist/replay/replay.js +98 -0
- package/dist/replay/replay.js.map +1 -0
- package/dist/replay/report.d.ts +18 -0
- package/dist/replay/report.js +72 -0
- package/dist/replay/report.js.map +1 -0
- package/dist/steps/backfill.d.ts +128 -0
- package/dist/steps/backfill.js +307 -0
- package/dist/steps/backfill.js.map +1 -0
- package/dist/steps/index.d.ts +52 -0
- package/dist/steps/index.js +52 -0
- package/dist/steps/index.js.map +1 -0
- package/dist/steps/kinds.d.ts +101 -0
- package/dist/steps/kinds.js +143 -0
- package/dist/steps/kinds.js.map +1 -0
- package/dist/steps/postgres-migration.d.ts +95 -0
- package/dist/steps/postgres-migration.js +149 -0
- package/dist/steps/postgres-migration.js.map +1 -0
- package/dist/steps/rebuild.d.ts +97 -0
- package/dist/steps/rebuild.js +162 -0
- package/dist/steps/rebuild.js.map +1 -0
- package/dist/steps/run.d.ts +45 -0
- package/dist/steps/run.js +72 -0
- package/dist/steps/run.js.map +1 -0
- package/dist/topology/render.d.ts +17 -0
- package/dist/topology/render.js +17 -0
- package/dist/topology/render.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/dist/versioned/approval.d.ts +35 -0
- package/dist/versioned/approval.js +53 -0
- package/dist/versioned/approval.js.map +1 -0
- package/dist/versioned/clickhouse.d.ts +24 -0
- package/dist/versioned/clickhouse.js +34 -0
- package/dist/versioned/clickhouse.js.map +1 -0
- package/dist/versioned/digest.d.ts +77 -0
- package/dist/versioned/digest.js +69 -0
- package/dist/versioned/digest.js.map +1 -0
- package/dist/versioned/driver.d.ts +41 -0
- package/dist/versioned/driver.js +13 -0
- package/dist/versioned/driver.js.map +1 -0
- package/dist/versioned/engine.d.ts +152 -0
- package/dist/versioned/engine.js +277 -0
- package/dist/versioned/engine.js.map +1 -0
- package/dist/versioned/explain.d.ts +123 -0
- package/dist/versioned/explain.js +289 -0
- package/dist/versioned/explain.js.map +1 -0
- package/dist/versioned/index.d.ts +37 -0
- package/dist/versioned/index.js +54 -0
- package/dist/versioned/index.js.map +1 -0
- package/dist/versioned/op.d.ts +62 -0
- package/dist/versioned/op.js +153 -0
- package/dist/versioned/op.js.map +1 -0
- package/dist/versioned/postgres/catalog.d.ts +33 -0
- package/dist/versioned/postgres/catalog.js +34 -0
- package/dist/versioned/postgres/catalog.js.map +1 -0
- package/dist/versioned/postgres/driver.d.ts +69 -0
- package/dist/versioned/postgres/driver.js +113 -0
- package/dist/versioned/postgres/driver.js.map +1 -0
- package/dist/versioned/postgres/executor.d.ts +104 -0
- package/dist/versioned/postgres/executor.js +172 -0
- package/dist/versioned/postgres/executor.js.map +1 -0
- package/dist/versioned/postgres/index.d.ts +11 -0
- package/dist/versioned/postgres/index.js +12 -0
- package/dist/versioned/postgres/index.js.map +1 -0
- package/dist/versioned/report.d.ts +22 -0
- package/dist/versioned/report.js +81 -0
- package/dist/versioned/report.js.map +1 -0
- package/dist/versioned/state.d.ts +108 -0
- package/dist/versioned/state.js +153 -0
- package/dist/versioned/state.js.map +1 -0
- package/package.json +51 -4
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan digest a versioned apply is approved under.
|
|
3
|
+
*
|
|
4
|
+
* It covers what the run would do and the state it would start from:
|
|
5
|
+
*
|
|
6
|
+
* - the migrations it would apply, in order, each with its files' checksum
|
|
7
|
+
* and the statements of it that already succeeded (a part-way one resumes);
|
|
8
|
+
* - the history it starts from: every applied migration and its checksum;
|
|
9
|
+
* - the topology the statements are rendered for (#9): an approval given
|
|
10
|
+
* for a single node never applies on a cluster;
|
|
11
|
+
* - the live schema: a fingerprint of the server's catalog in the
|
|
12
|
+
* environment's databases (every table, view and dictionary there, by its
|
|
13
|
+
* `create_table_query`, and each database's engine). The history database
|
|
14
|
+
* is left out, so recording a run does not move the digest.
|
|
15
|
+
*
|
|
16
|
+
* It is chant's plan digest (`computePlanDigest`, kind `yodel-migrations`),
|
|
17
|
+
* so `chant approve <op> <gate> --plan <digest>` takes it. A change to any of
|
|
18
|
+
* it after approval (a new pending migration, an edited one, a table changed
|
|
19
|
+
* out of band, another run applying first) is a new digest, and the
|
|
20
|
+
* approval does not carry over.
|
|
21
|
+
*/
|
|
22
|
+
import type { ClickHousePort } from "../history/index.js";
|
|
23
|
+
import type { AppliedMigration, PendingMigration } from "./state.js";
|
|
24
|
+
import type { Topology } from "../topology/render.js";
|
|
25
|
+
export declare const DIGEST_KIND = "yodel-migrations";
|
|
26
|
+
export interface LiveScope {
|
|
27
|
+
/** `sql.profiles.<env>.databases`; every database but the server's own when undefined. */
|
|
28
|
+
databases: string[] | undefined;
|
|
29
|
+
/** Left out of the fingerprint. */
|
|
30
|
+
historyDatabase: string;
|
|
31
|
+
}
|
|
32
|
+
export interface LiveCatalog {
|
|
33
|
+
databases: Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
engine: string;
|
|
36
|
+
}>;
|
|
37
|
+
tables: Array<{
|
|
38
|
+
database: string;
|
|
39
|
+
name: string;
|
|
40
|
+
engine: string;
|
|
41
|
+
create: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/** Reads the catalog the fingerprint covers. */
|
|
45
|
+
export declare function readLiveCatalog(port: ClickHousePort, scope: LiveScope): Promise<LiveCatalog>;
|
|
46
|
+
/** `sha256:<hex>` over the catalog's canonical JSON. */
|
|
47
|
+
export declare function catalogFingerprint(catalog: LiveCatalog): string;
|
|
48
|
+
/** What the digest is taken over. */
|
|
49
|
+
export interface DigestSubject {
|
|
50
|
+
environment: string;
|
|
51
|
+
historyDatabase: string;
|
|
52
|
+
/** The environment's topology, with a cluster's Keeper path and replica name when it sets them. Always set by {@link digestSubject}; absent in a subject carried from before #9. */
|
|
53
|
+
topology?: Topology;
|
|
54
|
+
applied: Array<{
|
|
55
|
+
id: string;
|
|
56
|
+
checksum: string;
|
|
57
|
+
}>;
|
|
58
|
+
pending: Array<{
|
|
59
|
+
id: string;
|
|
60
|
+
checksum: string;
|
|
61
|
+
done: number[];
|
|
62
|
+
}>;
|
|
63
|
+
live: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function digestSubject(input: {
|
|
66
|
+
environment: string;
|
|
67
|
+
historyDatabase: string;
|
|
68
|
+
/** Default: a single node. */
|
|
69
|
+
topology?: Topology | undefined;
|
|
70
|
+
applied: readonly AppliedMigration[];
|
|
71
|
+
toApply: readonly PendingMigration[];
|
|
72
|
+
live: string;
|
|
73
|
+
}): DigestSubject;
|
|
74
|
+
/** The plan digest of a subject, as chant computes and compares them. */
|
|
75
|
+
export declare function planDigest(subject: DigestSubject): Promise<string>;
|
|
76
|
+
/** Whether two digests name the same plan, by chant's rule. */
|
|
77
|
+
export declare function sameDigest(a: string | undefined, b: string | undefined): Promise<boolean>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan digest a versioned apply is approved under.
|
|
3
|
+
*
|
|
4
|
+
* It covers what the run would do and the state it would start from:
|
|
5
|
+
*
|
|
6
|
+
* - the migrations it would apply, in order, each with its files' checksum
|
|
7
|
+
* and the statements of it that already succeeded (a part-way one resumes);
|
|
8
|
+
* - the history it starts from: every applied migration and its checksum;
|
|
9
|
+
* - the topology the statements are rendered for (#9): an approval given
|
|
10
|
+
* for a single node never applies on a cluster;
|
|
11
|
+
* - the live schema: a fingerprint of the server's catalog in the
|
|
12
|
+
* environment's databases (every table, view and dictionary there, by its
|
|
13
|
+
* `create_table_query`, and each database's engine). The history database
|
|
14
|
+
* is left out, so recording a run does not move the digest.
|
|
15
|
+
*
|
|
16
|
+
* It is chant's plan digest (`computePlanDigest`, kind `yodel-migrations`),
|
|
17
|
+
* so `chant approve <op> <gate> --plan <digest>` takes it. A change to any of
|
|
18
|
+
* it after approval (a new pending migration, an edited one, a table changed
|
|
19
|
+
* out of band, another run applying first) is a new digest, and the
|
|
20
|
+
* approval does not carry over.
|
|
21
|
+
*/
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { literal } from "../history/index.js";
|
|
24
|
+
export const DIGEST_KIND = "yodel-migrations";
|
|
25
|
+
/** The databases the server keeps for itself, never part of an environment's schema. */
|
|
26
|
+
const SYSTEM_DATABASES = ["system", "information_schema", "INFORMATION_SCHEMA"];
|
|
27
|
+
/** Reads the catalog the fingerprint covers. */
|
|
28
|
+
export async function readLiveCatalog(port, scope) {
|
|
29
|
+
const where = scope.databases
|
|
30
|
+
? `name IN (${scope.databases.filter((d) => d !== scope.historyDatabase).map(literal).join(", ") || "''"})`
|
|
31
|
+
: `name NOT IN (${[...SYSTEM_DATABASES, scope.historyDatabase].map(literal).join(", ")})`;
|
|
32
|
+
const databases = await port.query(`SELECT name, engine FROM system.databases WHERE ${where} ORDER BY name`);
|
|
33
|
+
const names = databases.map((d) => d.name);
|
|
34
|
+
const tables = names.length === 0
|
|
35
|
+
? []
|
|
36
|
+
: await port.query(`SELECT database, name, engine, create_table_query AS create FROM system.tables WHERE database IN (${names.map(literal).join(", ")}) AND NOT is_temporary ORDER BY database, name`);
|
|
37
|
+
return { databases, tables };
|
|
38
|
+
}
|
|
39
|
+
/** `sha256:<hex>` over the catalog's canonical JSON. */
|
|
40
|
+
export function catalogFingerprint(catalog) {
|
|
41
|
+
const canonical = {
|
|
42
|
+
databases: [...catalog.databases].sort((a, b) => a.name.localeCompare(b.name)).map((d) => [d.name, d.engine]),
|
|
43
|
+
tables: [...catalog.tables]
|
|
44
|
+
.sort((a, b) => a.database.localeCompare(b.database) || a.name.localeCompare(b.name))
|
|
45
|
+
.map((t) => [t.database, t.name, t.engine, t.create]),
|
|
46
|
+
};
|
|
47
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(canonical), "utf8").digest("hex")}`;
|
|
48
|
+
}
|
|
49
|
+
export function digestSubject(input) {
|
|
50
|
+
return {
|
|
51
|
+
environment: input.environment,
|
|
52
|
+
historyDatabase: input.historyDatabase,
|
|
53
|
+
topology: input.topology ?? { kind: "single" },
|
|
54
|
+
applied: input.applied.map((a) => ({ id: a.id, checksum: a.checksum })),
|
|
55
|
+
pending: input.toApply.map((p) => ({ id: p.id, checksum: p.checksum, done: p.partWay?.done ?? [] })),
|
|
56
|
+
live: input.live,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** The plan digest of a subject, as chant computes and compares them. */
|
|
60
|
+
export async function planDigest(subject) {
|
|
61
|
+
const { computePlanDigest } = await import("@intentius/chant/lifecycle/plan-digest");
|
|
62
|
+
return computePlanDigest(DIGEST_KIND, subject);
|
|
63
|
+
}
|
|
64
|
+
/** Whether two digests name the same plan, by chant's rule. */
|
|
65
|
+
export async function sameDigest(a, b) {
|
|
66
|
+
const { samePlanDigest } = await import("@intentius/chant/lifecycle/plan-digest");
|
|
67
|
+
return samePlanDigest(a, b);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=digest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.js","sourceRoot":"","sources":["../../src/versioned/digest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the versioned apply needs from a dialect: its lock, the live-schema
|
|
3
|
+
* fingerprint the plan digest covers, and how one statement is sent and
|
|
4
|
+
* recorded. The engine (`./engine.ts`) is the same for both dialects: it
|
|
5
|
+
* reads the history, compares it with the directory, checks the digest and
|
|
6
|
+
* the approval, and walks the statements and Op steps in order, recording
|
|
7
|
+
* each one's start and failure; the driver does the dialect's part.
|
|
8
|
+
*
|
|
9
|
+
* ClickHouse ./clickhouse.ts the Keeper (or local) lock, system.tables, a statement then its row
|
|
10
|
+
* Postgres ./postgres/ the session advisory lock, chant's catalog reader, a statement and its row in one transaction
|
|
11
|
+
*/
|
|
12
|
+
import type { SqlDialect } from "../project.js";
|
|
13
|
+
import type { StatementStep } from "@intentius/chant-lexicon-sql/migration-statements";
|
|
14
|
+
import type { Lock } from "../history/index.js";
|
|
15
|
+
import type { StepContext } from "../steps/index.js";
|
|
16
|
+
export interface StatementRun {
|
|
17
|
+
/**
|
|
18
|
+
* Writes the statement's `succeeded` row, with `note` (the retries it
|
|
19
|
+
* took, an INVALID index it dropped; `""` when there is nothing to say).
|
|
20
|
+
* The driver calls it where the row commits with the statement when the
|
|
21
|
+
* dialect can: inside the statement's transaction on Postgres.
|
|
22
|
+
*/
|
|
23
|
+
succeeded: (note: string) => Promise<unknown>;
|
|
24
|
+
log: (line: string) => void;
|
|
25
|
+
}
|
|
26
|
+
/** One dialect's part of the versioned apply. A failed statement throws with the server's message, which the history's failed row records. */
|
|
27
|
+
export interface VersionedDriver {
|
|
28
|
+
dialect: SqlDialect;
|
|
29
|
+
/** The `note` a statement's rows carry: what ClickHouse rendered it for (#9); none on Postgres. */
|
|
30
|
+
rowNote?: string;
|
|
31
|
+
/** Takes the environment's apply lock, or throws a `LockHeldError` (exit 5). */
|
|
32
|
+
acquireLock(holder: string): Promise<Lock>;
|
|
33
|
+
/** A fingerprint of the live schema in the environment's scope, the history left out. */
|
|
34
|
+
liveFingerprint(): Promise<string>;
|
|
35
|
+
/** Sends one statement and writes its `succeeded` row through `run`; throws with the server's message when it fails. */
|
|
36
|
+
runStatement(step: StatementStep, run: StatementRun): Promise<void>;
|
|
37
|
+
/** What an Op step's runner gets from the dialect: the ClickHouse port and endpoint; the Postgres lock timeout, and the connection, timeouts and receipts schema a backfill uses. */
|
|
38
|
+
stepContext(): Pick<StepContext, "port" | "endpoint" | "lockTimeoutMs" | "postgres">;
|
|
39
|
+
/** Closes what the driver opened (a Postgres connection). Safe to call twice. */
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the versioned apply needs from a dialect: its lock, the live-schema
|
|
3
|
+
* fingerprint the plan digest covers, and how one statement is sent and
|
|
4
|
+
* recorded. The engine (`./engine.ts`) is the same for both dialects: it
|
|
5
|
+
* reads the history, compares it with the directory, checks the digest and
|
|
6
|
+
* the approval, and walks the statements and Op steps in order, recording
|
|
7
|
+
* each one's start and failure; the driver does the dialect's part.
|
|
8
|
+
*
|
|
9
|
+
* ClickHouse ./clickhouse.ts the Keeper (or local) lock, system.tables, a statement then its row
|
|
10
|
+
* Postgres ./postgres/ the session advisory lock, chant's catalog reader, a statement and its row in one transaction
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.js","sourceRoot":"","sources":["../../src/versioned/driver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The versioned apply: pending migrations, statement by statement, each
|
|
3
|
+
* statement's start and outcome appended to the history, under the lock,
|
|
4
|
+
* after the plan digest the run was approved under is checked again.
|
|
5
|
+
*
|
|
6
|
+
* Statements are the `statement` steps of each migration's `migration.json`,
|
|
7
|
+
* written for a single node, each rendered for the environment's topology
|
|
8
|
+
* as it is sent (#9, `../topology/render.ts`). A statement whose latest
|
|
9
|
+
* history row is `succeeded` is never sent again, so a migration that failed
|
|
10
|
+
* part way resumes at the statement that failed. A statement whose latest
|
|
11
|
+
* row is `started` (the runner stopped while it ran) is sent again: the
|
|
12
|
+
* history cannot tell whether the server ran it.
|
|
13
|
+
*
|
|
14
|
+
* An Op step (#12) runs in its place among the statements: the statements
|
|
15
|
+
* before it first, the ones after it only once it succeeded. A
|
|
16
|
+
* `ClickHouseRebuildOp` step runs chant's rebuild Op, a backfill step the
|
|
17
|
+
* author's Op of `effect()` batches (`../steps/`), each in this process
|
|
18
|
+
* under the same lock, and behind the same approval: the step and any file
|
|
19
|
+
* it runs are in the migration's checksum, so in the plan digest. Its rows
|
|
20
|
+
* are `kind = 'step'`, with the step's summary (receipts skipped, work done)
|
|
21
|
+
* in `note`. A step that failed or was stopped runs again on the next run
|
|
22
|
+
* and resumes from its receipts: the partitions or batches that finished
|
|
23
|
+
* are skipped.
|
|
24
|
+
*
|
|
25
|
+
* On Postgres (#13) a `PostgresMigrationOp` step (a column rename or type
|
|
26
|
+
* change) runs the same way, as chant's expand-and-contract Op
|
|
27
|
+
* (`../steps/postgres-migration.ts`).
|
|
28
|
+
*
|
|
29
|
+
* What is the dialect's (the lock, the live-schema fingerprint, how a
|
|
30
|
+
* statement is sent and its `succeeded` row written) is the driver's
|
|
31
|
+
* (`./driver.ts`): on Postgres a transactional statement and its row commit
|
|
32
|
+
* in one transaction, under `lock_timeout`, retried on a lock timeout
|
|
33
|
+
* (`./postgres/executor.ts`).
|
|
34
|
+
*
|
|
35
|
+
* Manual steps, and Op steps apply does not run (a rebuild in app mode, a
|
|
36
|
+
* step of the other dialect), are refused before anything runs (exit 2).
|
|
37
|
+
*/
|
|
38
|
+
import type { ClickHouseEndpoint } from "@intentius/chant-lexicon-sql/clickhouse/http";
|
|
39
|
+
import { YodelError, type Environment, type Project } from "../project.js";
|
|
40
|
+
import type { Migration } from "../migrations/index.js";
|
|
41
|
+
import type { Topology } from "../topology/render.js";
|
|
42
|
+
import { type StepRunner } from "../steps/index.js";
|
|
43
|
+
import type { ReceiptStore } from "@intentius/chant/op/receipt-store";
|
|
44
|
+
import { type ClickHousePort, type HistoryLocation, type HistoryState, type HistoryStoreLike } from "../history/index.js";
|
|
45
|
+
import type { SqlDialect } from "../project.js";
|
|
46
|
+
import type { VersionedDriver } from "./driver.js";
|
|
47
|
+
import { type PgConnect } from "./postgres/driver.js";
|
|
48
|
+
import { type PendingMigration, type VersionedStatus } from "./state.js";
|
|
49
|
+
import { type LiveScope } from "./digest.js";
|
|
50
|
+
/** Exit code: a checksum mismatch, an applied migration gone from the directory, or an out-of-order migration without `--allow-out-of-order`. */
|
|
51
|
+
export declare const REFUSED = 4;
|
|
52
|
+
/** Everything a versioned verb needs about one environment. */
|
|
53
|
+
export interface VersionedEnv {
|
|
54
|
+
environment: string;
|
|
55
|
+
dialect: SqlDialect;
|
|
56
|
+
/** The dialect's lock, fingerprint and statement sending (`./driver.ts`). */
|
|
57
|
+
driver: VersionedDriver;
|
|
58
|
+
/** The ClickHouse server; absent on Postgres, whose connections the driver keeps. */
|
|
59
|
+
port?: ClickHousePort;
|
|
60
|
+
store: HistoryStoreLike;
|
|
61
|
+
/** Where the history is: on Postgres, `database` is the history's schema and the topology a single node. */
|
|
62
|
+
location: HistoryLocation;
|
|
63
|
+
/** ClickHouse: the databases the fingerprint covers. Postgres: `databases` is the profile's schemas. */
|
|
64
|
+
scope: LiveScope;
|
|
65
|
+
/** Identifies the server, for the local lock's file name. */
|
|
66
|
+
serverKey: string;
|
|
67
|
+
lockTtlMs: number;
|
|
68
|
+
/** The project directory: an Op step reads chant.config.ts from it. */
|
|
69
|
+
projectDir?: string;
|
|
70
|
+
/** The server, for the receipt store a backfill step keeps its receipts in. */
|
|
71
|
+
endpoint?: ClickHouseEndpoint;
|
|
72
|
+
/** What each statement is rendered for when it is sent (`../config/settings.ts`). Default: a single node. */
|
|
73
|
+
topology?: Topology;
|
|
74
|
+
/** Where {@link topology} came from: `yodel.config.ts`, `sql.profiles.<env>.topology`, `YODEL_TOPOLOGY` or `default`. */
|
|
75
|
+
topologySource?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Binds an environment's server (`sql.profiles.<env>`) and history. `port`
|
|
79
|
+
* (ClickHouse) and `pgConnect` (Postgres) stand in for the server in tests.
|
|
80
|
+
* Close it with {@link closeVersioned} when done: on Postgres it holds a
|
|
81
|
+
* connection.
|
|
82
|
+
*/
|
|
83
|
+
export declare function openVersioned(project: Project, env: Environment, processEnv?: NodeJS.ProcessEnv, port?: ClickHousePort, pgConnect?: PgConnect): Promise<VersionedEnv>;
|
|
84
|
+
/** Closes what {@link openVersioned} opened (a Postgres connection). Safe to call twice. */
|
|
85
|
+
export declare const closeVersioned: (venv: VersionedEnv) => Promise<void>;
|
|
86
|
+
/** The history and how the directory compares with it. Reads only; takes no lock. */
|
|
87
|
+
export declare function inspect(venv: VersionedEnv, ordered: readonly Migration[], options?: {
|
|
88
|
+
sync?: boolean;
|
|
89
|
+
}): Promise<{
|
|
90
|
+
state: HistoryState;
|
|
91
|
+
status: VersionedStatus;
|
|
92
|
+
}>;
|
|
93
|
+
/** The refusal for an integrity problem, or `undefined`. */
|
|
94
|
+
export declare function integrityRefusal(status: VersionedStatus, allowOutOfOrder: boolean): YodelError | undefined;
|
|
95
|
+
/** The refusal for the steps among `toApply` that apply does not run (manual steps, Ops it does not run), or `undefined`. */
|
|
96
|
+
export declare function stepRefusal(toApply: readonly PendingMigration[]): YodelError | undefined;
|
|
97
|
+
/** What a run would apply, in chain order, or the refusal. */
|
|
98
|
+
export declare function selectToApply(status: VersionedStatus, allowOutOfOrder: boolean): PendingMigration[];
|
|
99
|
+
/** The plan digest of applying `toApply` from the server's state now. */
|
|
100
|
+
export declare function currentDigest(venv: VersionedEnv, status: VersionedStatus, toApply: readonly PendingMigration[]): Promise<string>;
|
|
101
|
+
export declare class ApplyFailed extends YodelError {
|
|
102
|
+
readonly migration: string;
|
|
103
|
+
readonly statement: number;
|
|
104
|
+
readonly serverError: string;
|
|
105
|
+
constructor(migration: string, statement: number, serverError: string);
|
|
106
|
+
}
|
|
107
|
+
/** An Op step failed or stopped part way; the next run resumes it from its receipts. */
|
|
108
|
+
export declare class StepFailed extends YodelError {
|
|
109
|
+
readonly migration: string;
|
|
110
|
+
readonly step: number;
|
|
111
|
+
readonly detail: string;
|
|
112
|
+
constructor(migration: string, step: number, detail: string);
|
|
113
|
+
}
|
|
114
|
+
export interface ExecuteOptions {
|
|
115
|
+
/** The digest the Op's gate handed the Apply step. A run whose digest has moved since is refused. */
|
|
116
|
+
approvedDigest: string;
|
|
117
|
+
/**
|
|
118
|
+
* Checks the approval itself, given the digest computed under the lock;
|
|
119
|
+
* throws to refuse. `--execute` looks it up in chant's gate ledger
|
|
120
|
+
* (`./approval.ts`). Required: the digest alone is never an approval.
|
|
121
|
+
*/
|
|
122
|
+
approve: (digest: string) => Promise<unknown>;
|
|
123
|
+
allowOutOfOrder: boolean;
|
|
124
|
+
/** Progress, one line at a time. */
|
|
125
|
+
log: (line: string) => void;
|
|
126
|
+
/** The silences in force for a migration, as JSON text for the history's `silences` field. Default `"[]"`. */
|
|
127
|
+
silences?: (m: Migration) => string;
|
|
128
|
+
processEnv?: NodeJS.ProcessEnv;
|
|
129
|
+
/** Runs an Op step. Default: chant's local executor (`../steps/`). */
|
|
130
|
+
runStep?: StepRunner;
|
|
131
|
+
/** The receipt store a backfill step uses. Default: chant's, on the server. */
|
|
132
|
+
receipts?: ReceiptStore;
|
|
133
|
+
}
|
|
134
|
+
export interface ExecuteResult {
|
|
135
|
+
applied: string[];
|
|
136
|
+
/** Statements not sent because an earlier run had already succeeded with them. */
|
|
137
|
+
skipped: Array<{
|
|
138
|
+
id: string;
|
|
139
|
+
statement: number;
|
|
140
|
+
}>;
|
|
141
|
+
lock: {
|
|
142
|
+
kind: "keeper" | "local" | "advisory";
|
|
143
|
+
where: string;
|
|
144
|
+
notice?: string;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Applies the pending migrations: takes the lock, reads the history again
|
|
149
|
+
* under it, refuses on any integrity problem or a digest that moved since
|
|
150
|
+
* approval, then runs each statement not yet succeeded.
|
|
151
|
+
*/
|
|
152
|
+
export declare function executeApply(venv: VersionedEnv, ordered: readonly Migration[], options: ExecuteOptions): Promise<ExecuteResult>;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The versioned apply: pending migrations, statement by statement, each
|
|
3
|
+
* statement's start and outcome appended to the history, under the lock,
|
|
4
|
+
* after the plan digest the run was approved under is checked again.
|
|
5
|
+
*
|
|
6
|
+
* Statements are the `statement` steps of each migration's `migration.json`,
|
|
7
|
+
* written for a single node, each rendered for the environment's topology
|
|
8
|
+
* as it is sent (#9, `../topology/render.ts`). A statement whose latest
|
|
9
|
+
* history row is `succeeded` is never sent again, so a migration that failed
|
|
10
|
+
* part way resumes at the statement that failed. A statement whose latest
|
|
11
|
+
* row is `started` (the runner stopped while it ran) is sent again: the
|
|
12
|
+
* history cannot tell whether the server ran it.
|
|
13
|
+
*
|
|
14
|
+
* An Op step (#12) runs in its place among the statements: the statements
|
|
15
|
+
* before it first, the ones after it only once it succeeded. A
|
|
16
|
+
* `ClickHouseRebuildOp` step runs chant's rebuild Op, a backfill step the
|
|
17
|
+
* author's Op of `effect()` batches (`../steps/`), each in this process
|
|
18
|
+
* under the same lock, and behind the same approval: the step and any file
|
|
19
|
+
* it runs are in the migration's checksum, so in the plan digest. Its rows
|
|
20
|
+
* are `kind = 'step'`, with the step's summary (receipts skipped, work done)
|
|
21
|
+
* in `note`. A step that failed or was stopped runs again on the next run
|
|
22
|
+
* and resumes from its receipts: the partitions or batches that finished
|
|
23
|
+
* are skipped.
|
|
24
|
+
*
|
|
25
|
+
* On Postgres (#13) a `PostgresMigrationOp` step (a column rename or type
|
|
26
|
+
* change) runs the same way, as chant's expand-and-contract Op
|
|
27
|
+
* (`../steps/postgres-migration.ts`).
|
|
28
|
+
*
|
|
29
|
+
* What is the dialect's (the lock, the live-schema fingerprint, how a
|
|
30
|
+
* statement is sent and its `succeeded` row written) is the driver's
|
|
31
|
+
* (`./driver.ts`): on Postgres a transactional statement and its row commit
|
|
32
|
+
* in one transaction, under `lock_timeout`, retried on a lock timeout
|
|
33
|
+
* (`./postgres/executor.ts`).
|
|
34
|
+
*
|
|
35
|
+
* Manual steps, and Op steps apply does not run (a rebuild in app mode, a
|
|
36
|
+
* step of the other dialect), are refused before anything runs (exit 2).
|
|
37
|
+
*/
|
|
38
|
+
import { YodelError } from "../project.js";
|
|
39
|
+
import { PLAN_NEEDS_OP } from "../chant/plan.js";
|
|
40
|
+
import { environmentSettings } from "../config/settings.js";
|
|
41
|
+
import { defaultStepRunner, describeStep, planStep, stepSha, StepOpFailed } from "../steps/index.js";
|
|
42
|
+
import { HistoryRecorder, HistoryStore, httpPort, MIGRATION_ROW, newRunContext, SINGLE, statementDone, statementSha, withLock, appliedBy, } from "../history/index.js";
|
|
43
|
+
import { clickhouseDriver } from "./clickhouse.js";
|
|
44
|
+
import { openPostgres } from "./postgres/driver.js";
|
|
45
|
+
import { compareHistory, outOfOrder } from "./state.js";
|
|
46
|
+
import { digestSubject, planDigest, sameDigest } from "./digest.js";
|
|
47
|
+
/** Exit code: a checksum mismatch, an applied migration gone from the directory, or an out-of-order migration without `--allow-out-of-order`. */
|
|
48
|
+
export const REFUSED = 4;
|
|
49
|
+
/**
|
|
50
|
+
* Binds an environment's server (`sql.profiles.<env>`) and history. `port`
|
|
51
|
+
* (ClickHouse) and `pgConnect` (Postgres) stand in for the server in tests.
|
|
52
|
+
* Close it with {@link closeVersioned} when done: on Postgres it holds a
|
|
53
|
+
* connection.
|
|
54
|
+
*/
|
|
55
|
+
export async function openVersioned(project, env, processEnv = process.env, port, pgConnect) {
|
|
56
|
+
const settings = environmentSettings(project, env.name, processEnv);
|
|
57
|
+
if (project.dialect === "postgres") {
|
|
58
|
+
const pg = await openPostgres(project, env, processEnv, settings.historyDatabase, pgConnect);
|
|
59
|
+
return {
|
|
60
|
+
environment: env.name,
|
|
61
|
+
dialect: "postgres",
|
|
62
|
+
driver: pg.driver,
|
|
63
|
+
store: pg.store,
|
|
64
|
+
location: { database: settings.historyDatabase, topology: SINGLE },
|
|
65
|
+
scope: { databases: pg.schemas, historyDatabase: settings.historyDatabase },
|
|
66
|
+
serverKey: pg.endpoint.url,
|
|
67
|
+
lockTtlMs: settings.lockTtlMs,
|
|
68
|
+
projectDir: project.dir,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const { resolveClickHouseTarget, isUnresolvedTarget } = await import("@intentius/chant-lexicon-sql/clickhouse/live/bind");
|
|
72
|
+
const target = resolveClickHouseTarget({ environment: env.name, config: project.config, env: processEnv });
|
|
73
|
+
if (isUnresolvedTarget(target))
|
|
74
|
+
throw new YodelError(target.detail);
|
|
75
|
+
const endpoint = target.endpoint;
|
|
76
|
+
const location = { database: settings.historyDatabase, topology: settings.historyTopology };
|
|
77
|
+
const p = port ?? (await httpPort(endpoint));
|
|
78
|
+
const scope = { databases: target.databases, historyDatabase: settings.historyDatabase };
|
|
79
|
+
return {
|
|
80
|
+
environment: env.name,
|
|
81
|
+
dialect: "clickhouse",
|
|
82
|
+
driver: clickhouseDriver({ port: p, location, scope, serverKey: endpoint.url, lockTtlMs: settings.lockTtlMs, endpoint, topology: settings.topology }),
|
|
83
|
+
port: p,
|
|
84
|
+
store: new HistoryStore(p, location),
|
|
85
|
+
location,
|
|
86
|
+
scope,
|
|
87
|
+
serverKey: endpoint.url,
|
|
88
|
+
lockTtlMs: settings.lockTtlMs,
|
|
89
|
+
projectDir: project.dir,
|
|
90
|
+
endpoint,
|
|
91
|
+
topology: settings.topology,
|
|
92
|
+
topologySource: settings.topologySource,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Closes what {@link openVersioned} opened (a Postgres connection). Safe to call twice. */
|
|
96
|
+
export const closeVersioned = (venv) => venv.driver.close();
|
|
97
|
+
/** The history and how the directory compares with it. Reads only; takes no lock. */
|
|
98
|
+
export async function inspect(venv, ordered, options = {}) {
|
|
99
|
+
const state = await venv.store.state(options);
|
|
100
|
+
return { state, status: compareHistory(ordered, state) };
|
|
101
|
+
}
|
|
102
|
+
/** The refusal for an integrity problem, or `undefined`. */
|
|
103
|
+
export function integrityRefusal(status, allowOutOfOrder) {
|
|
104
|
+
if (status.mismatches.length > 0) {
|
|
105
|
+
return new YodelError(`refused: the history does not match the migrations directory; nothing was applied:\n${status.mismatches.map((m) => ` ${m.message}`).join("\n")}`, REFUSED);
|
|
106
|
+
}
|
|
107
|
+
const late = outOfOrder(status);
|
|
108
|
+
if (late.length > 0 && !allowOutOfOrder) {
|
|
109
|
+
const newest = status.applied.filter((a) => a.index !== undefined).at(-1)?.id ?? "";
|
|
110
|
+
return new YodelError(`refused: ${late.map((p) => p.id).join(", ")} ${late.length === 1 ? "is" : "are"} out of order: the chain puts ${late.length === 1 ? "it" : "them"} before ${newest}, which is already applied. ` +
|
|
111
|
+
`Nothing was applied. Pass --allow-out-of-order to apply ${late.length === 1 ? "it" : "them"} now (in chain order); ${late.length === 1 ? "it is" : "they are"} never skipped.`, REFUSED);
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
/** The refusal for the steps among `toApply` that apply does not run (manual steps, Ops it does not run), or `undefined`. */
|
|
116
|
+
export function stepRefusal(toApply) {
|
|
117
|
+
const lines = toApply.flatMap((p) => p.refusedSteps.map(({ index, reason }) => ` ${p.id} step ${String(index)}: ${reason}`));
|
|
118
|
+
if (lines.length === 0)
|
|
119
|
+
return undefined;
|
|
120
|
+
return new YodelError(`refused: a pending migration has a step yodel apply does not run: a person acts on a manual step, and yodel apply runs only ClickHouseRebuildOp (materialized-view mode) and backfill steps on ClickHouse, and PostgresMigrationOp on Postgres. Nothing was applied:\n${lines.join("\n")}`, PLAN_NEEDS_OP);
|
|
121
|
+
}
|
|
122
|
+
/** What a run would apply, in chain order, or the refusal. */
|
|
123
|
+
export function selectToApply(status, allowOutOfOrder) {
|
|
124
|
+
const refusal = integrityRefusal(status, allowOutOfOrder) ?? stepRefusal(status.pending);
|
|
125
|
+
if (refusal)
|
|
126
|
+
throw refusal;
|
|
127
|
+
return status.pending;
|
|
128
|
+
}
|
|
129
|
+
/** The plan digest of applying `toApply` from the server's state now. */
|
|
130
|
+
export async function currentDigest(venv, status, toApply) {
|
|
131
|
+
const live = await venv.driver.liveFingerprint();
|
|
132
|
+
return planDigest(digestSubject({ environment: venv.environment, historyDatabase: venv.location.database, topology: venv.topology, applied: status.applied, toApply, live }));
|
|
133
|
+
}
|
|
134
|
+
export class ApplyFailed extends YodelError {
|
|
135
|
+
migration;
|
|
136
|
+
statement;
|
|
137
|
+
serverError;
|
|
138
|
+
constructor(migration, statement, serverError) {
|
|
139
|
+
super(`${migration} failed at statement ${String(statement)}: ${serverError}\n` +
|
|
140
|
+
`Fix the cause (on the server, or that statement in the migration's files), then run yodel apply again: ` +
|
|
141
|
+
`it resumes at statement ${String(statement)} and never sends again the statements that succeeded.`, 1);
|
|
142
|
+
this.migration = migration;
|
|
143
|
+
this.statement = statement;
|
|
144
|
+
this.serverError = serverError;
|
|
145
|
+
this.name = "ApplyFailed";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** An Op step failed or stopped part way; the next run resumes it from its receipts. */
|
|
149
|
+
export class StepFailed extends YodelError {
|
|
150
|
+
migration;
|
|
151
|
+
step;
|
|
152
|
+
detail;
|
|
153
|
+
constructor(migration, step, detail) {
|
|
154
|
+
super(`${migration} failed at step ${String(step)}: ${detail}\n` +
|
|
155
|
+
`Fix the cause, then run yodel apply again: it resumes at step ${String(step)}, whose finished work (the partitions or batches with a receipt) is not done again, ` +
|
|
156
|
+
`and never sends again the statements that succeeded.`, 1);
|
|
157
|
+
this.migration = migration;
|
|
158
|
+
this.step = step;
|
|
159
|
+
this.detail = detail;
|
|
160
|
+
this.name = "StepFailed";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Applies the pending migrations: takes the lock, reads the history again
|
|
165
|
+
* under it, refuses on any integrity problem or a digest that moved since
|
|
166
|
+
* approval, then runs each statement not yet succeeded.
|
|
167
|
+
*/
|
|
168
|
+
export async function executeApply(venv, ordered, options) {
|
|
169
|
+
const lock = await venv.driver.acquireLock(appliedBy(options.processEnv));
|
|
170
|
+
if (lock.notice)
|
|
171
|
+
options.log(`lock: ${lock.notice}`);
|
|
172
|
+
else
|
|
173
|
+
options.log(`lock: held in ${lock.where}`);
|
|
174
|
+
return withLock(lock, venv.lockTtlMs, async (beat) => {
|
|
175
|
+
await venv.store.ensure();
|
|
176
|
+
const { state, status } = await inspect(venv, ordered);
|
|
177
|
+
const toApply = selectToApply(status, options.allowOutOfOrder);
|
|
178
|
+
const digest = await currentDigest(venv, status, toApply);
|
|
179
|
+
if (!(await sameDigest(digest, options.approvedDigest))) {
|
|
180
|
+
const { describeMove, explainAgainst } = await import("./explain.js");
|
|
181
|
+
const moved = await explainAgainst(venv, ordered, { approvedDigest: options.approvedDigest });
|
|
182
|
+
throw new YodelError(`refused: the plan moved since it was approved (approved ${options.approvedDigest}, now ${digest}): ${describeMove(moved)}. ` +
|
|
183
|
+
`Nothing was applied; run yodel apply ${venv.environment} again and approve the new plan.`, 1);
|
|
184
|
+
}
|
|
185
|
+
await options.approve(digest);
|
|
186
|
+
const recorder = new HistoryRecorder(venv.store, newRunContext(options.approvedDigest, options.processEnv), state.maxSeq);
|
|
187
|
+
const result = { applied: [], skipped: [], lock: { kind: lock.kind, where: lock.where, ...(lock.notice ? { notice: lock.notice } : {}) } };
|
|
188
|
+
for (const p of toApply) {
|
|
189
|
+
await applyMigration(venv, p, state.byId.get(p.id), recorder, beat, options, result);
|
|
190
|
+
result.applied.push(p.id);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async function applyMigration(venv, p, rec, recorder, beat, options, result) {
|
|
196
|
+
const m = p.migration;
|
|
197
|
+
const silences = options.silences?.(m) ?? "[]";
|
|
198
|
+
const base = { migration_id: m.id, parent: m.parent ?? "", checksum: p.checksum, silences };
|
|
199
|
+
options.log(`${m.id}: ${p.partWay ? `resuming at statement ${String(p.partWay.resumeAt)}` : "applying"}${p.outOfOrder ? " (out of order, allowed)" : ""}`);
|
|
200
|
+
await recorder.record({ ...base, kind: "migration", statement_index: MIGRATION_ROW, statement_sha: "", status: "started" });
|
|
201
|
+
for (const [i, step] of m.document.steps.entries()) {
|
|
202
|
+
if (statementDone(rec, i)) {
|
|
203
|
+
options.log(` statement ${String(i)}: succeeded in an earlier run; not sent`);
|
|
204
|
+
result.skipped.push({ id: m.id, statement: i });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (step.kind !== "statement") {
|
|
208
|
+
await beat();
|
|
209
|
+
await runOpStep(venv, p, i, recorder, base, options);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
await beat();
|
|
213
|
+
// The neutral statement's sha, so a resume matches on every topology; the note says what it was rendered for.
|
|
214
|
+
const sha = statementSha(step.sql);
|
|
215
|
+
const rowNote = venv.driver.rowNote ?? "";
|
|
216
|
+
const row = { ...base, kind: "statement", statement_index: i, statement_sha: sha, note: rowNote };
|
|
217
|
+
await recorder.record({ ...row, status: "started" });
|
|
218
|
+
try {
|
|
219
|
+
// The driver writes the succeeded row: on Postgres in the statement's own transaction, so the two commit together.
|
|
220
|
+
await venv.driver.runStatement(step, {
|
|
221
|
+
succeeded: (note) => recorder.record({ ...row, status: "succeeded", note: [rowNote, note].filter(Boolean).join("; ") }),
|
|
222
|
+
log: options.log,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
227
|
+
await recorder.record({ ...row, status: "failed", error });
|
|
228
|
+
await recorder.record({ ...base, kind: "migration", statement_index: MIGRATION_ROW, statement_sha: "", status: "failed", error: `statement ${String(i)}: ${error}` });
|
|
229
|
+
options.log(` statement ${String(i)}: failed: ${error.split("\n")[0] ?? ""}`);
|
|
230
|
+
throw new ApplyFailed(m.id, i, error.split("\n")[0] ?? error);
|
|
231
|
+
}
|
|
232
|
+
options.log(` statement ${String(i)}: ok (${step.rule} ${step.class}, ${step.name})`);
|
|
233
|
+
}
|
|
234
|
+
await recorder.record({ ...base, kind: "migration", statement_index: MIGRATION_ROW, statement_sha: "", status: "succeeded" });
|
|
235
|
+
options.log(`${m.id}: applied`);
|
|
236
|
+
}
|
|
237
|
+
/** Runs Op step `i` of `p`, recording its start and outcome. */
|
|
238
|
+
async function runOpStep(venv, p, i, recorder, base, options) {
|
|
239
|
+
const m = p.migration;
|
|
240
|
+
const step = m.document.steps[i];
|
|
241
|
+
if (!step)
|
|
242
|
+
throw new YodelError(`${m.id} has no step ${String(i)}`);
|
|
243
|
+
const plan = planStep(step);
|
|
244
|
+
if (plan.run === "statement" || plan.run === "refused" || p.refusedSteps.some((r) => r.index === i)) {
|
|
245
|
+
throw stepRefusal([p]) ?? new YodelError(`${m.id} step ${String(i)}: ${plan.run === "refused" ? plan.reason : "not a step yodel apply runs"}`, PLAN_NEEDS_OP);
|
|
246
|
+
}
|
|
247
|
+
const row = { ...base, kind: "step", statement_index: i, statement_sha: stepSha(step, m.files) };
|
|
248
|
+
const resumed = p.partWay !== undefined && p.partWay.resumeAt === i;
|
|
249
|
+
options.log(` step ${String(i)}: ${describeStep(step)}${resumed ? " (resuming from its receipts)" : ""}`);
|
|
250
|
+
await recorder.record({ ...row, status: "started" });
|
|
251
|
+
const run = options.runStep ?? defaultStepRunner;
|
|
252
|
+
let summary;
|
|
253
|
+
try {
|
|
254
|
+
({ summary } = await run({
|
|
255
|
+
environment: venv.environment,
|
|
256
|
+
projectDir: venv.projectDir ?? process.cwd(),
|
|
257
|
+
...venv.driver.stepContext(),
|
|
258
|
+
migration: m,
|
|
259
|
+
index: i,
|
|
260
|
+
plan,
|
|
261
|
+
log: options.log,
|
|
262
|
+
receipts: options.receipts,
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
267
|
+
const note = e instanceof StepOpFailed && e.summary !== undefined ? JSON.stringify(e.summary) : "";
|
|
268
|
+
await recorder.record({ ...row, status: "failed", error: detail, note });
|
|
269
|
+
await recorder.record({ ...base, kind: "migration", statement_index: MIGRATION_ROW, statement_sha: "", status: "failed", error: `step ${String(i)}: ${detail}` });
|
|
270
|
+
options.log(` step ${String(i)}: failed: ${detail.split("\n")[0] ?? ""}${note ? ` (${note})` : ""}`);
|
|
271
|
+
throw new StepFailed(m.id, i, detail.split("\n")[0] ?? detail);
|
|
272
|
+
}
|
|
273
|
+
const note = JSON.stringify(summary ?? {});
|
|
274
|
+
await recorder.record({ ...row, status: "succeeded", note });
|
|
275
|
+
options.log(` step ${String(i)}: ok ${note}`);
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=engine.js.map
|