@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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silences: a line in `migration.sql`, above the statement it is for,
|
|
3
|
+
*
|
|
4
|
+
* -- yodel:allow <rule> <reason>
|
|
5
|
+
*
|
|
6
|
+
* silences that statement rule for that one step. The reason is required
|
|
7
|
+
* and is recorded, with the rule and the step, in every history row the
|
|
8
|
+
* migration writes when it applies (the `silences` column), so the history
|
|
9
|
+
* says who accepted what and why. Because `migration.sql` is in the
|
|
10
|
+
* migration's checksum, a silence is part of what an approval covers.
|
|
11
|
+
*
|
|
12
|
+
* Which step a line is "above": `migration.sql` holds each step in the
|
|
13
|
+
* order `migration.json` lists them, a statement as its SQL followed by
|
|
14
|
+
* `;`, an Op or manual step as a comment starting `-- <object> (<name>):`
|
|
15
|
+
* (`renderStatements`). Each step is found in that order; a silence belongs
|
|
16
|
+
* to the first step found after it. A statement `migration.sql` does not
|
|
17
|
+
* hold as `migration.json` records it is reported (`migration-sql`): apply
|
|
18
|
+
* runs `migration.json`'s, so an edit to the SQL alone changes nothing.
|
|
19
|
+
*/
|
|
20
|
+
import type { MigrationStep } from "@intentius/chant-lexicon-sql/migration-statements";
|
|
21
|
+
/** One `-- yodel:allow` line, read. */
|
|
22
|
+
export interface Silence {
|
|
23
|
+
/** The step it is above, an index into `migration.json`'s `steps`. */
|
|
24
|
+
step: number;
|
|
25
|
+
rule: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
/** Its line in migration.sql, from 1. */
|
|
28
|
+
line: number;
|
|
29
|
+
}
|
|
30
|
+
/** What was wrong with a migration.sql. */
|
|
31
|
+
export interface SqlProblem {
|
|
32
|
+
rule: "migration-sql" | "silence";
|
|
33
|
+
message: string;
|
|
34
|
+
step?: number;
|
|
35
|
+
line?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface SqlMap {
|
|
38
|
+
silences: Silence[];
|
|
39
|
+
problems: SqlProblem[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reads the silences in `sql` and assigns each to its step. `known` is the
|
|
43
|
+
* set of rule ids a silence may name. Problems: a silence that is
|
|
44
|
+
* malformed, names an unknown rule or one that cannot be silenced, has no
|
|
45
|
+
* reason, or follows the last step; a step migration.sql does not hold.
|
|
46
|
+
*/
|
|
47
|
+
export declare function mapMigrationSql(sql: string, steps: readonly MigrationStep[], silenceable: ReadonlySet<string>): SqlMap;
|
|
48
|
+
/** The silences as the history's `silences` field holds them: JSON, `[{"step":0,"rule":"...","reason":"..."}]`. */
|
|
49
|
+
export declare const silencesJson: (silences: readonly Silence[]) => string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silences: a line in `migration.sql`, above the statement it is for,
|
|
3
|
+
*
|
|
4
|
+
* -- yodel:allow <rule> <reason>
|
|
5
|
+
*
|
|
6
|
+
* silences that statement rule for that one step. The reason is required
|
|
7
|
+
* and is recorded, with the rule and the step, in every history row the
|
|
8
|
+
* migration writes when it applies (the `silences` column), so the history
|
|
9
|
+
* says who accepted what and why. Because `migration.sql` is in the
|
|
10
|
+
* migration's checksum, a silence is part of what an approval covers.
|
|
11
|
+
*
|
|
12
|
+
* Which step a line is "above": `migration.sql` holds each step in the
|
|
13
|
+
* order `migration.json` lists them, a statement as its SQL followed by
|
|
14
|
+
* `;`, an Op or manual step as a comment starting `-- <object> (<name>):`
|
|
15
|
+
* (`renderStatements`). Each step is found in that order; a silence belongs
|
|
16
|
+
* to the first step found after it. A statement `migration.sql` does not
|
|
17
|
+
* hold as `migration.json` records it is reported (`migration-sql`): apply
|
|
18
|
+
* runs `migration.json`'s, so an edit to the SQL alone changes nothing.
|
|
19
|
+
*/
|
|
20
|
+
import { normalizeLineEndings } from "../migrations/document.js";
|
|
21
|
+
const DIRECTIVE = /yodel:\s*allow\b/i;
|
|
22
|
+
const SILENCE = /^\s*--\s*yodel:allow(?:\s+(\S+))?(?:\s+(.*?))?\s*$/;
|
|
23
|
+
/** The text that marks a step in migration.sql. */
|
|
24
|
+
function anchorOf(step) {
|
|
25
|
+
return step.kind === "statement" ? `${step.sql};` : `-- ${step.object} (${step.name}):`;
|
|
26
|
+
}
|
|
27
|
+
const describe = (step, i) => `step ${String(i)} (${step.kind === "statement" ? `${step.rule} ${step.class}` : step.kind}, ${step.name})`;
|
|
28
|
+
/**
|
|
29
|
+
* Reads the silences in `sql` and assigns each to its step. `known` is the
|
|
30
|
+
* set of rule ids a silence may name. Problems: a silence that is
|
|
31
|
+
* malformed, names an unknown rule or one that cannot be silenced, has no
|
|
32
|
+
* reason, or follows the last step; a step migration.sql does not hold.
|
|
33
|
+
*/
|
|
34
|
+
export function mapMigrationSql(sql, steps, silenceable) {
|
|
35
|
+
const text = normalizeLineEndings(sql);
|
|
36
|
+
const lineAt = (offset) => text.slice(0, offset).split("\n").length;
|
|
37
|
+
const out = { silences: [], problems: [] };
|
|
38
|
+
const readRegion = (from, to, step) => {
|
|
39
|
+
const region = text.slice(from, to);
|
|
40
|
+
let offset = from;
|
|
41
|
+
for (const raw of region.split("\n")) {
|
|
42
|
+
const line = lineAt(offset);
|
|
43
|
+
offset += raw.length + 1;
|
|
44
|
+
if (!DIRECTIVE.test(raw) || !raw.trimStart().startsWith("--"))
|
|
45
|
+
continue;
|
|
46
|
+
const m = SILENCE.exec(raw);
|
|
47
|
+
const rule = m?.[1];
|
|
48
|
+
const reason = m?.[2]?.trim() ?? "";
|
|
49
|
+
if (!m || rule === undefined) {
|
|
50
|
+
out.problems.push({ rule: "silence", line, message: `migration.sql line ${String(line)}: expected -- yodel:allow <rule> <reason>` });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!silenceable.has(rule)) {
|
|
54
|
+
out.problems.push({ rule: "silence", line, message: `migration.sql line ${String(line)}: ${rule} is not a rule a statement can silence (${[...silenceable].join(", ")})` });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (reason === "") {
|
|
58
|
+
out.problems.push({ rule: "silence", line, message: `migration.sql line ${String(line)}: silencing ${rule} needs a reason: -- yodel:allow ${rule} <why this is safe>` });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (step === undefined) {
|
|
62
|
+
out.problems.push({ rule: "silence", line, message: `migration.sql line ${String(line)}: yodel:allow ${rule} comes after the last step, so it silences nothing; put it above the statement` });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
out.silences.push({ step, rule, reason, line });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
let cursor = 0;
|
|
69
|
+
steps.forEach((step, i) => {
|
|
70
|
+
const anchor = anchorOf(step);
|
|
71
|
+
const at = text.indexOf(anchor, cursor);
|
|
72
|
+
if (at === -1) {
|
|
73
|
+
if (step.kind === "statement") {
|
|
74
|
+
out.problems.push({
|
|
75
|
+
rule: "migration-sql",
|
|
76
|
+
step: i,
|
|
77
|
+
message: `${describe(step, i)}: migration.sql does not hold its statement as migration.json records it (${step.sql.split("\n")[0] ?? ""}); yodel apply runs migration.json's`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// An Op or manual step's comment is for people; a missing one moves nothing.
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
readRegion(cursor, at, i);
|
|
84
|
+
cursor = at + anchor.length;
|
|
85
|
+
});
|
|
86
|
+
readRegion(cursor, text.length, undefined);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/** The silences as the history's `silences` field holds them: JSON, `[{"step":0,"rule":"...","reason":"..."}]`. */
|
|
90
|
+
export const silencesJson = (silences) => JSON.stringify(silences.map((s) => ({ step: s.step, rule: s.rule, reason: s.reason })));
|
|
91
|
+
//# sourceMappingURL=silences.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"silences.js","sourceRoot":"","sources":["../../src/lint/silences.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The statement rules: each step of a migration against the class chant's
|
|
3
|
+
* classifier gave it (`migration.json` records the rule and class of every
|
|
4
|
+
* statement, and the changes an Op or manual step makes). Pure.
|
|
5
|
+
*
|
|
6
|
+
* - `destructive`: a statement marked destructive (a table or column drop),
|
|
7
|
+
* or an Op step one of whose changes is.
|
|
8
|
+
* - `ch-mutation`: a ClickHouse statement of class `rewrite`, or one that
|
|
9
|
+
* waits for its mutation (a column type or TTL change).
|
|
10
|
+
* - `ch-rebuild`: a `ClickHouseRebuildOp` step.
|
|
11
|
+
* - `pg-rewrite`: a Postgres statement of class `rewrite` (ACCESS EXCLUSIVE
|
|
12
|
+
* for a rewrite or a full scan).
|
|
13
|
+
* - `pg-lock`: a Postgres statement that blocks writes for a whole build or
|
|
14
|
+
* scan: an index created or dropped on an existing table without
|
|
15
|
+
* `CONCURRENTLY` (SQLPG240 to SQLPG243), a primary key or unique
|
|
16
|
+
* constraint built in place rather than `USING INDEX` (SQLPG221), a
|
|
17
|
+
* foreign key or domain constraint validated in place rather than added
|
|
18
|
+
* `NOT VALID` (SQLPG219, SQLPG262).
|
|
19
|
+
*
|
|
20
|
+
* `COMMENT ON` statements are skipped: chant writes one after a `CREATE`
|
|
21
|
+
* with the same rule, and a comment is catalog only.
|
|
22
|
+
*
|
|
23
|
+
* A statement whose rule is not one of the lexicon's, or a step of a kind
|
|
24
|
+
* it does not know, is `unreadable-object`: lint cannot tell what it does,
|
|
25
|
+
* and never passes what it cannot read.
|
|
26
|
+
*/
|
|
27
|
+
import type { MigrationStep } from "@intentius/chant-lexicon-sql/migration-statements";
|
|
28
|
+
export type Dialect = "clickhouse" | "postgres";
|
|
29
|
+
export interface StepFinding {
|
|
30
|
+
rule: string;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
/** What lint cannot read in a step, or `undefined`. */
|
|
34
|
+
export declare function unreadableStep(dialect: Dialect, step: MigrationStep): string | undefined;
|
|
35
|
+
/** The statement-rule findings for one step, before levels and silences. */
|
|
36
|
+
export declare function stepFindings(dialect: Dialect, step: MigrationStep): StepFinding[];
|
|
37
|
+
/** Why yodel apply refuses a step, or `undefined` for one it runs. */
|
|
38
|
+
export declare function refusedStep(step: MigrationStep): string | undefined;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The statement rules: each step of a migration against the class chant's
|
|
3
|
+
* classifier gave it (`migration.json` records the rule and class of every
|
|
4
|
+
* statement, and the changes an Op or manual step makes). Pure.
|
|
5
|
+
*
|
|
6
|
+
* - `destructive`: a statement marked destructive (a table or column drop),
|
|
7
|
+
* or an Op step one of whose changes is.
|
|
8
|
+
* - `ch-mutation`: a ClickHouse statement of class `rewrite`, or one that
|
|
9
|
+
* waits for its mutation (a column type or TTL change).
|
|
10
|
+
* - `ch-rebuild`: a `ClickHouseRebuildOp` step.
|
|
11
|
+
* - `pg-rewrite`: a Postgres statement of class `rewrite` (ACCESS EXCLUSIVE
|
|
12
|
+
* for a rewrite or a full scan).
|
|
13
|
+
* - `pg-lock`: a Postgres statement that blocks writes for a whole build or
|
|
14
|
+
* scan: an index created or dropped on an existing table without
|
|
15
|
+
* `CONCURRENTLY` (SQLPG240 to SQLPG243), a primary key or unique
|
|
16
|
+
* constraint built in place rather than `USING INDEX` (SQLPG221), a
|
|
17
|
+
* foreign key or domain constraint validated in place rather than added
|
|
18
|
+
* `NOT VALID` (SQLPG219, SQLPG262).
|
|
19
|
+
*
|
|
20
|
+
* `COMMENT ON` statements are skipped: chant writes one after a `CREATE`
|
|
21
|
+
* with the same rule, and a comment is catalog only.
|
|
22
|
+
*
|
|
23
|
+
* A statement whose rule is not one of the lexicon's, or a step of a kind
|
|
24
|
+
* it does not know, is `unreadable-object`: lint cannot tell what it does,
|
|
25
|
+
* and never passes what it cannot read.
|
|
26
|
+
*/
|
|
27
|
+
import { CLASSIFIER_RULES } from "@intentius/chant-lexicon-sql/clickhouse/plan/rules";
|
|
28
|
+
import { PG_CLASSIFIER_RULES } from "@intentius/chant-lexicon-sql/postgres/plan/rules";
|
|
29
|
+
import { planStep, REBUILD_OP } from "../steps/kinds.js";
|
|
30
|
+
const catalog = (dialect) => (dialect === "clickhouse" ? CLASSIFIER_RULES : PG_CLASSIFIER_RULES);
|
|
31
|
+
/** The classifier rule's id and title, e.g. `SQLCH202 Drop a column`. */
|
|
32
|
+
function titled(dialect, rule) {
|
|
33
|
+
const r = catalog(dialect)[rule];
|
|
34
|
+
return r ? `${rule} ${r.title}` : rule;
|
|
35
|
+
}
|
|
36
|
+
const COMMENT = /^\s*COMMENT\s+ON\b/i;
|
|
37
|
+
const INDEX_DDL = /^\s*(?:CREATE\s+(?:UNIQUE\s+)?INDEX|DROP\s+INDEX|REINDEX)\b/i;
|
|
38
|
+
const CONCURRENTLY = /\bCONCURRENTLY\b/i;
|
|
39
|
+
const NOT_VALID = /\bNOT\s+VALID\b/i;
|
|
40
|
+
const USING_INDEX = /\bUSING\s+INDEX\b/i;
|
|
41
|
+
const ALTER_TABLE = /^\s*ALTER\s+TABLE\b/i;
|
|
42
|
+
/** Why a Postgres statement blocks writes for a whole build or scan, or `undefined`. */
|
|
43
|
+
function pgLock(step) {
|
|
44
|
+
const sql = step.sql;
|
|
45
|
+
if (step.class === "create")
|
|
46
|
+
return undefined; // an index on a table created in the same migration blocks nobody
|
|
47
|
+
if (["SQLPG221", "SQLPG240", "SQLPG241", "SQLPG242", "SQLPG243"].includes(step.rule) && INDEX_DDL.test(sql) && !CONCURRENTLY.test(sql)) {
|
|
48
|
+
return /^\s*DROP/i.test(sql)
|
|
49
|
+
? "drops an index without CONCURRENTLY: ACCESS EXCLUSIVE on the table, blocking reads and writes behind it; use DROP INDEX CONCURRENTLY"
|
|
50
|
+
: "builds an index without CONCURRENTLY: SHARE on the table, blocking every write until the build ends; declare the index CONCURRENTLY";
|
|
51
|
+
}
|
|
52
|
+
if (step.rule === "SQLPG221" && ALTER_TABLE.test(sql) && !USING_INDEX.test(sql)) {
|
|
53
|
+
return "builds the constraint's index under ACCESS EXCLUSIVE; build a unique index CONCURRENTLY first, then ADD CONSTRAINT ... USING INDEX";
|
|
54
|
+
}
|
|
55
|
+
if (step.rule === "SQLPG219" && !NOT_VALID.test(sql)) {
|
|
56
|
+
return "reads every row under SHARE ROW EXCLUSIVE on both tables, blocking writes to them for the scan; add it NOT VALID and validate it in a later migration";
|
|
57
|
+
}
|
|
58
|
+
if (step.rule === "SQLPG262" && !NOT_VALID.test(sql)) {
|
|
59
|
+
return "checks every column of the domain's type in every table while it holds its lock; add it NOT VALID and validate it later";
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/** What lint cannot read in a step, or `undefined`. */
|
|
64
|
+
export function unreadableStep(dialect, step) {
|
|
65
|
+
const kind = step.kind;
|
|
66
|
+
if (kind !== "statement" && kind !== "op" && kind !== "manual")
|
|
67
|
+
return `a step of kind ${JSON.stringify(kind)}, which is not statement, op or manual`;
|
|
68
|
+
if (step.kind === "statement") {
|
|
69
|
+
if (typeof step.sql !== "string" || step.sql.trim() === "")
|
|
70
|
+
return "a statement step with no SQL";
|
|
71
|
+
const known = catalog(dialect)[step.rule];
|
|
72
|
+
return known ? undefined : `a statement whose rule ${JSON.stringify(step.rule)} is not one of the ${dialect} classifier's, so what it does cannot be checked`;
|
|
73
|
+
}
|
|
74
|
+
for (const c of step.changes) {
|
|
75
|
+
if (!catalog(dialect)[c.rule])
|
|
76
|
+
return `a ${step.kind} step whose change ${JSON.stringify(c.rule)} is not one of the ${dialect} classifier's rules`;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/** The statement-rule findings for one step, before levels and silences. */
|
|
81
|
+
export function stepFindings(dialect, step) {
|
|
82
|
+
const out = [];
|
|
83
|
+
if (step.kind === "statement") {
|
|
84
|
+
if (COMMENT.test(step.sql))
|
|
85
|
+
return out;
|
|
86
|
+
const what = `${titled(dialect, step.rule)} on ${step.name}`;
|
|
87
|
+
if (step.destructive)
|
|
88
|
+
out.push({ rule: "destructive", message: `${what}: removes data that cannot be recovered` });
|
|
89
|
+
if (dialect === "clickhouse" && (step.class === "rewrite" || step.waitsForMutation === true)) {
|
|
90
|
+
out.push({ rule: "ch-mutation", message: `${what}: a mutation that rewrites existing parts in the background, with no rollback` });
|
|
91
|
+
}
|
|
92
|
+
if (dialect === "postgres") {
|
|
93
|
+
if (step.class === "rewrite") {
|
|
94
|
+
const r = catalog(dialect)[step.rule];
|
|
95
|
+
out.push({ rule: "pg-rewrite", message: `${what}: blocks reads and writes on the table while it runs${r ? ` (${r.restriction})` : ""}` });
|
|
96
|
+
}
|
|
97
|
+
const lock = pgLock(step);
|
|
98
|
+
if (lock)
|
|
99
|
+
out.push({ rule: "pg-lock", message: `${what}: ${lock}` });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
if (step.kind === "op") {
|
|
104
|
+
const destructive = step.changes.filter((c) => c.destructive);
|
|
105
|
+
if (destructive.length > 0) {
|
|
106
|
+
out.push({ rule: "destructive", message: `${step.op} for ${step.name} removes data that cannot be recovered: ${destructive.map((c) => `${titled(dialect, c.rule)} (${c.field})`).join(", ")}` });
|
|
107
|
+
}
|
|
108
|
+
if (step.op === REBUILD_OP) {
|
|
109
|
+
const why = step.changes.filter((c) => c.class === "rebuild").map((c) => `${titled(dialect, c.rule)} (${c.field})`);
|
|
110
|
+
out.push({ rule: "ch-rebuild", message: `${REBUILD_OP} rebuilds ${step.name}: every row is copied into a new table and swapped in${why.length > 0 ? `, for ${why.join(", ")}` : ""}` });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** Why yodel apply refuses a step, or `undefined` for one it runs. */
|
|
116
|
+
export function refusedStep(step) {
|
|
117
|
+
const plan = planStep(step);
|
|
118
|
+
return plan.run === "refused" ? plan.reason : undefined;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=statements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statements.js","sourceRoot":"","sources":["../../src/lint/statements.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A migration's checksum: covers that migration's own files and nothing
|
|
3
|
+
* else, so editing, adding or removing another migration never changes it.
|
|
4
|
+
*
|
|
5
|
+
* Exactly:
|
|
6
|
+
*
|
|
7
|
+
* sql = migration.sql, with CRLF and lone CR turned into LF
|
|
8
|
+
* json = migration.json parsed, its top-level "checksum" removed, then
|
|
9
|
+
* written as JSON with every object's keys sorted and no whitespace
|
|
10
|
+
* checksum = "sha256:" + lowercase hex of SHA-256 over the UTF-8 bytes of
|
|
11
|
+
* "yodel-migration-checksum-v1\n"
|
|
12
|
+
* + "migration.sql " + byteLength(sql) + "\n" + sql
|
|
13
|
+
* + "migration.json " + byteLength(json) + "\n" + json
|
|
14
|
+
* + for each other file in the migration's directory, by path:
|
|
15
|
+
* path + " " + byteLength(text) + "\n" + text
|
|
16
|
+
*
|
|
17
|
+
* The other files are a migration's own besides the two, such as a backfill
|
|
18
|
+
* step's module (`backfill.ts`, see `../steps/backfill.ts`): every regular
|
|
19
|
+
* file under its directory, at any depth, except `migration.sql`,
|
|
20
|
+
* `migration.json` and anything whose name starts with `.`. `path` is
|
|
21
|
+
* relative to the directory with `/` separators; paths are taken in code
|
|
22
|
+
* point order, and each file's text has its line endings normalized as
|
|
23
|
+
* migration.sql's are. A migration with no other files hashes exactly as it
|
|
24
|
+
* did before they counted, so no existing checksum changes.
|
|
25
|
+
*
|
|
26
|
+
* The lengths frame each file, so no text can move from one file into the
|
|
27
|
+
* other and keep the hash. Because migration.json is hashed as parsed JSON,
|
|
28
|
+
* its whitespace, key order and line endings do not count; every value in it
|
|
29
|
+
* does, the parent id and the recorded schema included. Whitespace in
|
|
30
|
+
* migration.sql counts, except line endings.
|
|
31
|
+
*/
|
|
32
|
+
import { type MigrationDocument, type MigrationFiles } from "./document.js";
|
|
33
|
+
export declare const CHECKSUM_PREFIX = "sha256:";
|
|
34
|
+
/** The canonical JSON of a migration document, without its checksum: what the checksum hashes for `migration.json`. */
|
|
35
|
+
export declare function canonicalDocument(doc: MigrationDocument | Record<string, unknown>): string;
|
|
36
|
+
/**
|
|
37
|
+
* The checksum of a migration's files. `json` may be the file's text or the
|
|
38
|
+
* document already parsed; whatever `checksum` it holds is ignored. `extra`
|
|
39
|
+
* is the migration's other files, by path (see above).
|
|
40
|
+
*/
|
|
41
|
+
export declare function computeChecksum(files: {
|
|
42
|
+
sql: string;
|
|
43
|
+
json: string | MigrationDocument | Record<string, unknown>;
|
|
44
|
+
extra?: Readonly<Record<string, string>> | undefined;
|
|
45
|
+
}): string;
|
|
46
|
+
export interface ChecksumCheck {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
/** The checksum `migration.json` records. */
|
|
49
|
+
recorded: string;
|
|
50
|
+
/** The checksum of the files as they are. */
|
|
51
|
+
actual: string;
|
|
52
|
+
}
|
|
53
|
+
/** Whether a migration's files still hash to the checksum its `migration.json` records. */
|
|
54
|
+
export declare function verifyChecksum(m: {
|
|
55
|
+
files: MigrationFiles;
|
|
56
|
+
} | MigrationFiles): ChecksumCheck;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A migration's checksum: covers that migration's own files and nothing
|
|
3
|
+
* else, so editing, adding or removing another migration never changes it.
|
|
4
|
+
*
|
|
5
|
+
* Exactly:
|
|
6
|
+
*
|
|
7
|
+
* sql = migration.sql, with CRLF and lone CR turned into LF
|
|
8
|
+
* json = migration.json parsed, its top-level "checksum" removed, then
|
|
9
|
+
* written as JSON with every object's keys sorted and no whitespace
|
|
10
|
+
* checksum = "sha256:" + lowercase hex of SHA-256 over the UTF-8 bytes of
|
|
11
|
+
* "yodel-migration-checksum-v1\n"
|
|
12
|
+
* + "migration.sql " + byteLength(sql) + "\n" + sql
|
|
13
|
+
* + "migration.json " + byteLength(json) + "\n" + json
|
|
14
|
+
* + for each other file in the migration's directory, by path:
|
|
15
|
+
* path + " " + byteLength(text) + "\n" + text
|
|
16
|
+
*
|
|
17
|
+
* The other files are a migration's own besides the two, such as a backfill
|
|
18
|
+
* step's module (`backfill.ts`, see `../steps/backfill.ts`): every regular
|
|
19
|
+
* file under its directory, at any depth, except `migration.sql`,
|
|
20
|
+
* `migration.json` and anything whose name starts with `.`. `path` is
|
|
21
|
+
* relative to the directory with `/` separators; paths are taken in code
|
|
22
|
+
* point order, and each file's text has its line endings normalized as
|
|
23
|
+
* migration.sql's are. A migration with no other files hashes exactly as it
|
|
24
|
+
* did before they counted, so no existing checksum changes.
|
|
25
|
+
*
|
|
26
|
+
* The lengths frame each file, so no text can move from one file into the
|
|
27
|
+
* other and keep the hash. Because migration.json is hashed as parsed JSON,
|
|
28
|
+
* its whitespace, key order and line endings do not count; every value in it
|
|
29
|
+
* does, the parent id and the recorded schema included. Whitespace in
|
|
30
|
+
* migration.sql counts, except line endings.
|
|
31
|
+
*/
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
33
|
+
import { MIGRATION_JSON, MIGRATION_SQL, normalizeLineEndings, sortKeys } from "./document.js";
|
|
34
|
+
export const CHECKSUM_PREFIX = "sha256:";
|
|
35
|
+
const DOMAIN = "yodel-migration-checksum-v1\n";
|
|
36
|
+
/** The canonical JSON of a migration document, without its checksum: what the checksum hashes for `migration.json`. */
|
|
37
|
+
export function canonicalDocument(doc) {
|
|
38
|
+
const rest = { ...doc };
|
|
39
|
+
delete rest.checksum;
|
|
40
|
+
return JSON.stringify(sortKeys(rest));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The checksum of a migration's files. `json` may be the file's text or the
|
|
44
|
+
* document already parsed; whatever `checksum` it holds is ignored. `extra`
|
|
45
|
+
* is the migration's other files, by path (see above).
|
|
46
|
+
*/
|
|
47
|
+
export function computeChecksum(files) {
|
|
48
|
+
const sql = normalizeLineEndings(files.sql);
|
|
49
|
+
const doc = typeof files.json === "string" ? JSON.parse(files.json) : files.json;
|
|
50
|
+
const json = canonicalDocument(doc);
|
|
51
|
+
const hash = createHash("sha256");
|
|
52
|
+
hash.update(DOMAIN, "utf8");
|
|
53
|
+
hash.update(`${MIGRATION_SQL} ${String(Buffer.byteLength(sql, "utf8"))}\n`, "utf8");
|
|
54
|
+
hash.update(sql, "utf8");
|
|
55
|
+
hash.update(`${MIGRATION_JSON} ${String(Buffer.byteLength(json, "utf8"))}\n`, "utf8");
|
|
56
|
+
hash.update(json, "utf8");
|
|
57
|
+
for (const path of Object.keys(files.extra ?? {}).sort()) {
|
|
58
|
+
const text = normalizeLineEndings(files.extra?.[path] ?? "");
|
|
59
|
+
hash.update(`${path} ${String(Buffer.byteLength(text, "utf8"))}\n`, "utf8");
|
|
60
|
+
hash.update(text, "utf8");
|
|
61
|
+
}
|
|
62
|
+
return `${CHECKSUM_PREFIX}${hash.digest("hex")}`;
|
|
63
|
+
}
|
|
64
|
+
/** Whether a migration's files still hash to the checksum its `migration.json` records. */
|
|
65
|
+
export function verifyChecksum(m) {
|
|
66
|
+
const files = "files" in m ? m.files : m;
|
|
67
|
+
const doc = JSON.parse(files.json);
|
|
68
|
+
const recorded = typeof doc.checksum === "string" ? doc.checksum : "";
|
|
69
|
+
const actual = computeChecksum(files);
|
|
70
|
+
return { ok: recorded === actual, recorded, actual };
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=checksum.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checksum.js","sourceRoot":"","sources":["../../src/migrations/checksum.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Making the next migration, offline: the newest migration's recorded
|
|
3
|
+
* schema (or an empty schema, for the first) diffed against the current
|
|
4
|
+
* build with chant's `diffStatements`. No server is read. The statements
|
|
5
|
+
* are written for a single node and the migration records no topology: it
|
|
6
|
+
* is rendered for each environment's topology when it is applied (#9). Pure: nothing
|
|
7
|
+
* here touches the disk; `yodel new` writes what it returns with
|
|
8
|
+
* `writeMigration`.
|
|
9
|
+
*/
|
|
10
|
+
import { type DiffStatementsOptions, type SchemaStatements } from "@intentius/chant-lexicon-sql/migration-statements";
|
|
11
|
+
import type { BuildOutput } from "../chant/build.js";
|
|
12
|
+
import { type MigrationDocument, type MigrationFiles, type MigrationStep } from "./document.js";
|
|
13
|
+
import { type Migration } from "./directory.js";
|
|
14
|
+
export interface CreateMigrationOptions {
|
|
15
|
+
/** The migration's name, the part of its id after the timestamp. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** The current `chant build` output: the schema after the migration. */
|
|
18
|
+
build: BuildOutput;
|
|
19
|
+
/** The newest migration, which the new one follows; `undefined` for the first. */
|
|
20
|
+
previous?: Migration | undefined;
|
|
21
|
+
/** When it is made. Default: now. Its UTC minute is the id's timestamp. */
|
|
22
|
+
now?: Date;
|
|
23
|
+
/** The ownership marker each `CREATE` carries. Default: chant's, `managed-by=chant`. */
|
|
24
|
+
marker?: DiffStatementsOptions["marker"];
|
|
25
|
+
/** ClickHouse: the database a bare name means. Default: chant's, `default`. */
|
|
26
|
+
defaultDatabase?: string;
|
|
27
|
+
/** Postgres: the schema a bare name means. Default: chant's, `public`. */
|
|
28
|
+
defaultSchema?: string;
|
|
29
|
+
/**
|
|
30
|
+
* A backfill step to end the migration with (#12): the module's text,
|
|
31
|
+
* written into the migration's directory as `file` (default `backfill.ts`)
|
|
32
|
+
* and counted in its checksum. With one, a migration is made even when the
|
|
33
|
+
* build has no schema change.
|
|
34
|
+
*/
|
|
35
|
+
backfill?: {
|
|
36
|
+
text: string;
|
|
37
|
+
file?: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface CreatedMigration {
|
|
41
|
+
id: string;
|
|
42
|
+
document: MigrationDocument;
|
|
43
|
+
files: MigrationFiles;
|
|
44
|
+
statements: SchemaStatements;
|
|
45
|
+
}
|
|
46
|
+
/** What {@link createMigration} returns when the build has no change from the recorded schema. */
|
|
47
|
+
export interface NoChange {
|
|
48
|
+
id: undefined;
|
|
49
|
+
statements: SchemaStatements;
|
|
50
|
+
}
|
|
51
|
+
/** `YYYYMMDDTHHMM` in UTC. */
|
|
52
|
+
export declare function migrationTimestamp(now: Date): string;
|
|
53
|
+
/** `<YYYYMMDDTHHMM>-<name>`, or an error for a name that is not lowercase words joined by `-` or `_`. */
|
|
54
|
+
export declare function migrationId(name: string, now: Date): string;
|
|
55
|
+
/**
|
|
56
|
+
* The schema before the first migration: a build of the same dialect with no
|
|
57
|
+
* objects. `diffStatements` takes two build outputs and has no form for
|
|
58
|
+
* "nothing", so this is one. A Postgres build's `postgresMajor` is kept, so
|
|
59
|
+
* both sides classify for the same major.
|
|
60
|
+
*/
|
|
61
|
+
export declare function emptyBuild(like: BuildOutput): BuildOutput;
|
|
62
|
+
/** The steps that are not statements: each Op or manual step, which a person has to act on. */
|
|
63
|
+
export declare const refusedSteps: (steps: readonly MigrationStep[]) => Exclude<MigrationStep, {
|
|
64
|
+
kind: "statement";
|
|
65
|
+
}>[];
|
|
66
|
+
/** `migration.sql`: a header naming the migration, its parent, any Op or manual step and the diff's hints, then chant's rendered statements. */
|
|
67
|
+
export declare function renderMigrationSql(doc: Pick<MigrationDocument, "id" | "parent" | "hints">, statements: SchemaStatements): string;
|
|
68
|
+
/**
|
|
69
|
+
* The next migration, or a {@link NoChange} when the build declares the
|
|
70
|
+
* schema the previous migration recorded. Throws a YodelError when the
|
|
71
|
+
* recorded schema and the build are different dialects.
|
|
72
|
+
*/
|
|
73
|
+
export declare function createMigration(options: CreateMigrationOptions): CreatedMigration | NoChange;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Making the next migration, offline: the newest migration's recorded
|
|
3
|
+
* schema (or an empty schema, for the first) diffed against the current
|
|
4
|
+
* build with chant's `diffStatements`. No server is read. The statements
|
|
5
|
+
* are written for a single node and the migration records no topology: it
|
|
6
|
+
* is rendered for each environment's topology when it is applied (#9). Pure: nothing
|
|
7
|
+
* here touches the disk; `yodel new` writes what it returns with
|
|
8
|
+
* `writeMigration`.
|
|
9
|
+
*/
|
|
10
|
+
import { diffStatements, renderStatements } from "@intentius/chant-lexicon-sql/migration-statements";
|
|
11
|
+
import { YodelError } from "../project.js";
|
|
12
|
+
import { computeChecksum } from "./checksum.js";
|
|
13
|
+
import { MIGRATION_FORMAT, serializeDocument } from "./document.js";
|
|
14
|
+
import { MIGRATION_NAME } from "./directory.js";
|
|
15
|
+
import { asBackfill, BACKFILL_FILE } from "../steps/kinds.js";
|
|
16
|
+
import { backfillStep } from "../steps/backfill.js";
|
|
17
|
+
/** `YYYYMMDDTHHMM` in UTC. */
|
|
18
|
+
export function migrationTimestamp(now) {
|
|
19
|
+
const iso = now.toISOString(); // 2026-10-09T14:12:34.567Z
|
|
20
|
+
return `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}T${iso.slice(11, 13)}${iso.slice(14, 16)}`;
|
|
21
|
+
}
|
|
22
|
+
/** `<YYYYMMDDTHHMM>-<name>`, or an error for a name that is not lowercase words joined by `-` or `_`. */
|
|
23
|
+
export function migrationId(name, now) {
|
|
24
|
+
if (!MIGRATION_NAME.test(name)) {
|
|
25
|
+
throw new YodelError(`migration name "${name}": use lowercase letters and digits, words joined by - or _ (e.g. add-country)`);
|
|
26
|
+
}
|
|
27
|
+
return `${migrationTimestamp(now)}-${name}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The schema before the first migration: a build of the same dialect with no
|
|
31
|
+
* objects. `diffStatements` takes two build outputs and has no form for
|
|
32
|
+
* "nothing", so this is one. A Postgres build's `postgresMajor` is kept, so
|
|
33
|
+
* both sides classify for the same major.
|
|
34
|
+
*/
|
|
35
|
+
export function emptyBuild(like) {
|
|
36
|
+
return {
|
|
37
|
+
dialect: like.dialect,
|
|
38
|
+
objects: [],
|
|
39
|
+
...(typeof like.postgresMajor === "number" ? { postgresMajor: like.postgresMajor } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** The steps that are not statements: each Op or manual step, which a person has to act on. */
|
|
43
|
+
export const refusedSteps = (steps) => steps.filter((s) => s.kind !== "statement");
|
|
44
|
+
function describeRefused(s) {
|
|
45
|
+
const backfill = asBackfill(s);
|
|
46
|
+
if (backfill)
|
|
47
|
+
return `a backfill: the Op ${backfill.options.file} exports, its effect() batches resumable from their receipts`;
|
|
48
|
+
const rules = s.changes.map((c) => c.rule).join(", ");
|
|
49
|
+
return s.kind === "op"
|
|
50
|
+
? `${s.op} for ${s.object} (${s.name}), ${rules}: not SQL; migration.json holds its declaration`
|
|
51
|
+
: `a manual step for ${s.object} (${s.name}), ${rules}: no statement and no Op makes it`;
|
|
52
|
+
}
|
|
53
|
+
/** `migration.sql`: a header naming the migration, its parent, any Op or manual step and the diff's hints, then chant's rendered statements. */
|
|
54
|
+
export function renderMigrationSql(doc, statements) {
|
|
55
|
+
const head = [`-- yodel migration ${doc.id}`, `-- parent: ${doc.parent ?? "none (the first migration)"}`];
|
|
56
|
+
const refused = refusedSteps(statements.steps);
|
|
57
|
+
if (refused.length > 0) {
|
|
58
|
+
head.push(`-- This migration contains ${refused.some((s) => s.kind === "op") ? "an Op step" : "a manual step"}, not only SQL:`);
|
|
59
|
+
for (const s of refused)
|
|
60
|
+
head.push(`-- ${describeRefused(s)}`);
|
|
61
|
+
}
|
|
62
|
+
for (const h of doc.hints)
|
|
63
|
+
head.push(`-- hint: ${h.replace(/\r?\n/g, " ")}`);
|
|
64
|
+
const body = statements.steps
|
|
65
|
+
.map((s) => {
|
|
66
|
+
const backfill = asBackfill(s);
|
|
67
|
+
return backfill ? `-- ${backfill.object} (${backfill.name}): made by the Op ${backfill.options.file} exports, not a statement\n` : renderStatements({ ...statements, steps: [s] });
|
|
68
|
+
})
|
|
69
|
+
.join("\n")
|
|
70
|
+
.replace(/\n+$/, "");
|
|
71
|
+
return `${head.join("\n")}\n\n${body}\n`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The next migration, or a {@link NoChange} when the build declares the
|
|
75
|
+
* schema the previous migration recorded. Throws a YodelError when the
|
|
76
|
+
* recorded schema and the build are different dialects.
|
|
77
|
+
*/
|
|
78
|
+
export function createMigration(options) {
|
|
79
|
+
const now = options.now ?? new Date();
|
|
80
|
+
const id = migrationId(options.name, now);
|
|
81
|
+
const before = options.previous?.document.schema ?? emptyBuild(options.build);
|
|
82
|
+
let statements;
|
|
83
|
+
try {
|
|
84
|
+
statements = diffStatements(before, options.build, {
|
|
85
|
+
...(options.marker ? { marker: options.marker } : {}),
|
|
86
|
+
...(options.defaultDatabase !== undefined ? { defaultDatabase: options.defaultDatabase } : {}),
|
|
87
|
+
...(options.defaultSchema !== undefined ? { defaultSchema: options.defaultSchema } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
throw new YodelError(e instanceof Error ? e.message : String(e));
|
|
92
|
+
}
|
|
93
|
+
if (options.backfill) {
|
|
94
|
+
const file = options.backfill.file ?? BACKFILL_FILE;
|
|
95
|
+
statements = { ...statements, refused: true, steps: [...statements.steps, backfillStep(options.name, file)] };
|
|
96
|
+
}
|
|
97
|
+
if (statements.steps.length === 0)
|
|
98
|
+
return { id: undefined, statements };
|
|
99
|
+
const unsigned = {
|
|
100
|
+
format: MIGRATION_FORMAT,
|
|
101
|
+
id,
|
|
102
|
+
parent: options.previous?.id ?? null,
|
|
103
|
+
checksum: "",
|
|
104
|
+
createdAt: `${now.toISOString().slice(0, 19)}Z`,
|
|
105
|
+
dialect: statements.dialect,
|
|
106
|
+
...(statements.defaultDatabase !== undefined ? { defaultDatabase: statements.defaultDatabase } : {}),
|
|
107
|
+
...(statements.defaultSchema !== undefined ? { defaultSchema: statements.defaultSchema } : {}),
|
|
108
|
+
...(statements.major !== undefined ? { major: statements.major } : {}),
|
|
109
|
+
refused: statements.refused,
|
|
110
|
+
hints: statements.hints,
|
|
111
|
+
steps: statements.steps,
|
|
112
|
+
schema: options.build,
|
|
113
|
+
};
|
|
114
|
+
const sql = renderMigrationSql(unsigned, statements);
|
|
115
|
+
const extra = options.backfill ? { [options.backfill.file ?? BACKFILL_FILE]: options.backfill.text } : undefined;
|
|
116
|
+
const document = { ...unsigned, checksum: computeChecksum({ sql, json: unsigned, extra }) };
|
|
117
|
+
return { id, document, files: { sql, json: serializeDocument(document), ...(extra ? { extra } : {}) }, statements };
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=create.js.map
|