@intentius/chant-lexicon-sql 0.124.54 → 0.124.56

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.
Files changed (61) hide show
  1. package/dist/capability-plugin.d.ts +160 -0
  2. package/dist/capability-plugin.d.ts.map +1 -0
  3. package/dist/capability-plugin.js +370 -0
  4. package/dist/change-set.d.ts +35 -0
  5. package/dist/change-set.d.ts.map +1 -1
  6. package/dist/change-set.js +43 -0
  7. package/dist/ci-setup.d.ts +34 -0
  8. package/dist/ci-setup.d.ts.map +1 -0
  9. package/dist/ci-setup.js +106 -0
  10. package/dist/clickhouse/access.d.ts +20 -0
  11. package/dist/clickhouse/access.d.ts.map +1 -1
  12. package/dist/clickhouse/access.js +42 -1
  13. package/dist/clickhouse/live/describe-resources.d.ts +3 -1
  14. package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
  15. package/dist/clickhouse/live/describe-resources.js +31 -3
  16. package/dist/clickhouse/plan/deep.d.ts +42 -11
  17. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  18. package/dist/clickhouse/plan/deep.js +104 -33
  19. package/dist/exit-codes.d.ts +69 -0
  20. package/dist/exit-codes.d.ts.map +1 -0
  21. package/dist/exit-codes.js +105 -0
  22. package/dist/index.d.ts +4 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +12 -0
  25. package/dist/integrity.json +2 -2
  26. package/dist/lock-grain.d.ts +27 -0
  27. package/dist/lock-grain.d.ts.map +1 -0
  28. package/dist/lock-grain.js +13 -0
  29. package/dist/manifest.json +1 -1
  30. package/dist/plugin.d.ts.map +1 -1
  31. package/dist/plugin.js +8 -0
  32. package/dist/postgres/testing/migration.d.ts +2 -0
  33. package/dist/postgres/testing/migration.d.ts.map +1 -1
  34. package/dist/token-source.d.ts +28 -0
  35. package/dist/token-source.d.ts.map +1 -1
  36. package/dist/token-source.js +70 -1
  37. package/dist/yodel-run.d.ts +58 -0
  38. package/dist/yodel-run.d.ts.map +1 -0
  39. package/dist/yodel-run.js +145 -0
  40. package/package.json +2 -2
  41. package/src/capability-plugin.test.ts +317 -0
  42. package/src/capability-plugin.ts +515 -0
  43. package/src/change-set.ts +74 -0
  44. package/src/ci-setup.test.ts +67 -0
  45. package/src/ci-setup.ts +116 -0
  46. package/src/clickhouse/access.ts +49 -2
  47. package/src/clickhouse/live/access.e2e.test.ts +8 -7
  48. package/src/clickhouse/live/describe-resources.ts +27 -3
  49. package/src/clickhouse/plan/deep-grants-drift.test.ts +151 -0
  50. package/src/clickhouse/plan/deep-grants.test.ts +10 -5
  51. package/src/clickhouse/plan/deep.ts +103 -32
  52. package/src/exit-codes.test.ts +46 -0
  53. package/src/exit-codes.ts +140 -0
  54. package/src/index.ts +28 -0
  55. package/src/lock-grain.test.ts +57 -0
  56. package/src/lock-grain.ts +37 -0
  57. package/src/plugin.ts +9 -0
  58. package/src/testdata/fake-yodel.mjs +58 -0
  59. package/src/token-source-traded.test.ts +75 -0
  60. package/src/token-source.ts +74 -1
  61. package/src/yodel-run.ts +175 -0
@@ -0,0 +1,160 @@
1
+ import type { Capability } from "@intentius/chant/components/capability";
2
+ import { type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
3
+ import { type ChangeSetHole } from "@intentius/chant/change-set";
4
+ import { type ChantExitCode } from "@intentius/chant/exit-codes";
5
+ import { type SqlDeclarativeChange } from "./change-set.js";
6
+ import { type YodelRunner } from "./yodel-run.js";
7
+ /** A plan that moved since it was decided: nothing was applied (core's `isPlanMovedError`). */
8
+ export declare class SqlPlanMovedError extends Error {
9
+ readonly member: string;
10
+ readonly planMoved = true;
11
+ constructor(member: string, message: string);
12
+ }
13
+ /** A yodel run that did not succeed, with the code chant's table gives it. */
14
+ export declare class YodelRunError extends Error {
15
+ /** The table's code for the run (`CHANT_EXIT`). Named so core never reads it as an exit code. */
16
+ readonly chantExit: ChantExitCode;
17
+ readonly yodelExit: number | null;
18
+ constructor(message: string,
19
+ /** The table's code for the run (`CHANT_EXIT`). Named so core never reads it as an exit code. */
20
+ chantExit: ChantExitCode, yodelExit: number | null);
21
+ }
22
+ /** The input of a `sql-migrate` step. */
23
+ export interface SqlMigrateStepInput {
24
+ /** The yodel environment (`{target}` in an op wave), a tenant (`<env>/<tenant>`) or a slot of a tenant set. */
25
+ environment: string;
26
+ /** The yodel project directory, relative to the job's working directory. Default: the working directory. */
27
+ dir?: string;
28
+ /** The migrations Op, when more than one applies the environment. */
29
+ op?: string;
30
+ allowOutOfOrder?: boolean;
31
+ /**
32
+ * The op-waves spec file the wave runs (`yodel-waves.json`), which the
33
+ * engine reads chant's decision for the wave by. Set: plan the wave plan
34
+ * and allow `run`. Absent: plan the migrations only, as a pull request's
35
+ * plan does; `run` refuses.
36
+ */
37
+ spec?: string;
38
+ /** The yodel predates sql-yodeler#288 and exits by its own table. */
39
+ legacyExit?: boolean;
40
+ }
41
+ /** What `sql-migrate`'s plan hands its run. */
42
+ export interface SqlMigrateArtifact {
43
+ kind: "sql-migrate";
44
+ environment: string;
45
+ planDigest: string;
46
+ /** The wave plan file `--wave-plan` wrote, relative to the working directory. */
47
+ wavePlan?: string;
48
+ spec?: string;
49
+ /** The migrations' own digest, as `yodel apply --digest` prints it. */
50
+ migrations?: string;
51
+ pending: string[];
52
+ }
53
+ export interface SqlMigrateStepOutput {
54
+ environment: string;
55
+ planDigest: string;
56
+ /** What yodel printed on standard output: `Applied: ...`, `Nothing to apply.` or the stand-down line. */
57
+ summary: string;
58
+ }
59
+ /** The parts of the wave plan file `yodel apply --wave-plan` writes that the capability reads. */
60
+ interface YodelWavePlanFile {
61
+ planDigest: string;
62
+ migrations?: string;
63
+ environment?: string;
64
+ empty?: boolean;
65
+ pending?: string[];
66
+ destructiveSteps?: Array<{
67
+ migration?: string;
68
+ step?: number;
69
+ why?: string;
70
+ }>;
71
+ }
72
+ /** What `sql-migrate` is built with. Tests pass a fake runner. */
73
+ export interface SqlMigrateDeps {
74
+ run?: YodelRunner;
75
+ /** The job's working directory. Default `process.cwd()`. */
76
+ cwd?: () => string;
77
+ /** The job's environment. Default `process.env`. */
78
+ env?: () => NodeJS.ProcessEnv;
79
+ }
80
+ /** Where the capability keeps a run's yodel files, relative to the working directory. */
81
+ export declare function sqlMigrateFiles(environment: string): {
82
+ part: string;
83
+ wavePlan: string;
84
+ };
85
+ /** The destructive classes of a wave plan: each step's rules, as the engine's lint names them, sorted. */
86
+ export declare function wavePlanDestructive(plan: Pick<YodelWavePlanFile, "destructiveSteps">): string[];
87
+ /** The `sql-migrate` capability, with its runner and environment injectable. */
88
+ export declare function createSqlMigrateCapability(deps?: SqlMigrateDeps): Capability<SqlMigrateStepInput, SqlMigrateStepOutput>;
89
+ export declare const sqlMigrateCapability: Capability<SqlMigrateStepInput, SqlMigrateStepOutput>;
90
+ /** The input of a `sql-apply` step. */
91
+ export interface SqlApplyStepInput {
92
+ /** The chant environment, which selects `sql.profiles.<environment>`. */
93
+ environment: string;
94
+ /** The build's output (`chant build src --lexicon sql -o dist/schema.json`). */
95
+ build: string;
96
+ /** Drop this project's objects the build no longer declares. Destructive, so off by default. */
97
+ prune?: boolean;
98
+ stack?: string;
99
+ ownershipEnv?: string;
100
+ /** Postgres: `lock_timeout` per statement, in ms. */
101
+ lockTimeoutMs?: number;
102
+ /** Postgres: `statement_timeout` for a catalog-only statement, in ms. */
103
+ statementTimeoutMs?: number;
104
+ /** Postgres: `statement_timeout` for a statement that reads or rewrites rows, in ms. */
105
+ scanTimeoutMs?: number;
106
+ /** ClickHouse: how long to wait for one table's background rewrite, in ms. */
107
+ mutationTimeoutMs?: number;
108
+ /** Project directory for `chant.config.ts`. Default: the working directory. */
109
+ cwd?: string;
110
+ }
111
+ /** A declarative plan, as `sql-apply` reads it from the dialect's planner. */
112
+ export interface SqlDeclarativePlan {
113
+ dialect: "clickhouse" | "postgres";
114
+ changes: SqlDeclarativeChange[];
115
+ /** Changes the applier will not make in place, each with the Op to run instead when the planner names one. */
116
+ refused: Array<{
117
+ object: string;
118
+ field: string;
119
+ op?: string;
120
+ }>;
121
+ holes: ChangeSetHole[];
122
+ /** Why the plan cannot be applied besides (a named collection missing on the server). */
123
+ problems: string[];
124
+ }
125
+ export interface SqlApplyArtifact {
126
+ kind: "sql-apply";
127
+ environment: string;
128
+ dialect: "clickhouse" | "postgres";
129
+ planDigest: string;
130
+ }
131
+ export interface SqlApplyStepOutput {
132
+ environment: string;
133
+ dialect: "clickhouse" | "postgres";
134
+ planDigest: string;
135
+ applied: string[];
136
+ pruned: string[];
137
+ }
138
+ /** What `sql-apply` is built with. Tests pass fakes for the planner and the applier. */
139
+ export interface SqlApplyDeps {
140
+ plan?: (input: SqlApplyStepInput, dialect: "clickhouse" | "postgres") => Promise<SqlDeclarativePlan>;
141
+ apply?: (input: SqlApplyStepInput, dialect: "clickhouse" | "postgres") => Promise<{
142
+ applied: string[];
143
+ pruned: string[];
144
+ notAttempted: Array<{
145
+ name: string;
146
+ reason: string;
147
+ }>;
148
+ }>;
149
+ }
150
+ /** The dialect a build output names: `postgres`, else `clickhouse`. */
151
+ export declare function buildDialect(path: string): "clickhouse" | "postgres";
152
+ /** The `sql-apply` capability, with its planner and applier injectable. */
153
+ export declare function createSqlApplyCapability(deps?: SqlApplyDeps): Capability<SqlApplyStepInput, SqlApplyStepOutput>;
154
+ export declare const sqlApplyCapability: Capability<SqlApplyStepInput, SqlApplyStepOutput>;
155
+ export declare const SQL_VERB_FAMILIES: {
156
+ readonly apply: readonly ["sql-apply", "sql-migrate"];
157
+ };
158
+ export declare const sqlCapabilityPlugin: CapabilityPlugin;
159
+ export {};
160
+ //# sourceMappingURL=capability-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../src/capability-plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,370 @@
1
+ /**
2
+ * The sql lexicon's capability plugin (#4569): `sql-migrate` and
3
+ * `sql-apply`, so op waves (`step: { kind, ...input }`, #4545) and
4
+ * `chant components pr-plan` plan and apply a SQL member through chant, as
5
+ * they do a Terraform root (`terraform-apply`). A project that lists `sql`
6
+ * among its lexicons gets both registered when its registry is built
7
+ * (`loadCapabilityPluginsFromLexicons`).
8
+ *
9
+ * ## sql-migrate: the versioned path
10
+ *
11
+ * The versioned migration engine stays in sql-yodeler (ruling A on
12
+ * sql-yodeler#285), and this capability runs it through the fixed argv list
13
+ * in `./yodel-run.ts`:
14
+ *
15
+ * ```json
16
+ * { "step": { "kind": "sql-migrate", "environment": "{target}", "spec": "yodel-waves.json" } }
17
+ * ```
18
+ *
19
+ * - `plan` runs `yodel plan <env> --part <file>` for the change-set part,
20
+ * one entry per object the pending migrations touch, with real counts.
21
+ * With `spec` (an op wave) it also runs `yodel apply <env> --wave-plan
22
+ * <file> --spec <spec>`, and the wave plan's digest is the member's: the
23
+ * value the wave's gate binds and the engine checks again under its lock.
24
+ * The wave plan's destructive steps come back as
25
+ * {@link CapabilityPlan.destructive}, each the lint rule that classes it,
26
+ * so `on-destructive` waits on what the engine calls destructive. Without
27
+ * `spec` (a pull request's plan) the digest is the migrations' own, the
28
+ * one `yodel approve` binds. A plan the engine would refuse fails the
29
+ * plan, naming the refusal.
30
+ * - `run` runs `yodel apply <env> --wave-apply <file> --spec <spec>` with
31
+ * the plan the wave decided, which `DeployContext.plans` hands back. The
32
+ * engine plans again under its lock and refuses a plan that moved; that
33
+ * refusal is a moved plan here ({@link SqlPlanMovedError}), and the wave
34
+ * exits 4. The engine also checks chant's decision for the wave and the
35
+ * gate ledger's approval itself (ruling A on sql-yodeler#300), so an
36
+ * apply that chant let through and the ledger does not hold still refuses.
37
+ * `run` applies only a plan an op wave made with `spec`: an apply happens
38
+ * on the merged branch, never before the merge (ruling E on
39
+ * sql-yodeler#285).
40
+ *
41
+ * yodel's exit status reads through `yodelExitOf` (`./exit-codes.ts`,
42
+ * #4570). A yodel before sql-yodeler#288 exits by its own table; set
43
+ * `legacyExit: true` for one, and its moved plan is read from the line it
44
+ * prints.
45
+ *
46
+ * Both kinds take the `target` lock grain, the environment, and refuse
47
+ * `resource` by name (`./lock-grain.ts`, #4571).
48
+ *
49
+ * The planning commands run without the writer's variables; only the apply
50
+ * gets them (`readerEnv` in `./yodel-run.ts`).
51
+ *
52
+ * ## sql-apply: the declarative path
53
+ *
54
+ * `sql-apply` plans a build (`chant build -o dist/schema.json`) against the
55
+ * environment's server, as `chant sql plan` does, and applies it with the
56
+ * lexicon's applier, `clickhouseApply` or `postgresApply`, by the build's
57
+ * dialect:
58
+ *
59
+ * ```json
60
+ * { "kind": "sql-apply", "environment": "prod", "build": "dist/schema.json" }
61
+ * ```
62
+ *
63
+ * The part has one entry per object the classified changes touch. A change
64
+ * the applier refuses (a ClickHouse rebuild, a Postgres expand and contract),
65
+ * an object the plan cannot read, or a named collection the server lacks
66
+ * fails the plan, naming the migration Op to run instead. `run` plans again
67
+ * and applies only when the digest is still the one decided; otherwise it
68
+ * refuses as moved and sends nothing.
69
+ */
70
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
71
+ import { dirname, join } from "node:path";
72
+ import { ownPackageVersion } from "@intentius/chant/components/capability-plugin";
73
+ import { readChangeSetPartFile } from "@intentius/chant/change-set";
74
+ import { CHANT_EXIT } from "@intentius/chant/exit-codes";
75
+ import { sqlDeclarativePlanPart } from "./change-set.js";
76
+ import { yodelExitOf } from "./exit-codes.js";
77
+ import { sqlDefaultLockGrain, sqlLockGrainRefusal } from "./lock-grain.js";
78
+ import { defaultYodelRunner, readerEnv, runAllowedYodel, yodelPlanArgs, yodelWaveApplyArgs, yodelWavePlanArgs, } from "./yodel-run.js";
79
+ /** A plan that moved since it was decided: nothing was applied (core's `isPlanMovedError`). */
80
+ export class SqlPlanMovedError extends Error {
81
+ constructor(member, message) {
82
+ super(message);
83
+ this.member = member;
84
+ this.planMoved = true;
85
+ this.name = "SqlPlanMovedError";
86
+ }
87
+ }
88
+ /** A yodel run that did not succeed, with the code chant's table gives it. */
89
+ export class YodelRunError extends Error {
90
+ constructor(message,
91
+ /** The table's code for the run (`CHANT_EXIT`). Named so core never reads it as an exit code. */
92
+ chantExit, yodelExit) {
93
+ super(message);
94
+ this.chantExit = chantExit;
95
+ this.yodelExit = yodelExit;
96
+ this.name = "YodelRunError";
97
+ }
98
+ }
99
+ /** Where the capability keeps a run's yodel files, relative to the working directory. */
100
+ export function sqlMigrateFiles(environment) {
101
+ const key = environment.replace(/[^A-Za-z0-9._-]/g, "_");
102
+ return { part: `.chant/sql/yodel/${key}.part.json`, wavePlan: `.chant/sql/yodel/${key}.wave-plan.json` };
103
+ }
104
+ function requireEnvironment(input) {
105
+ if (typeof input?.environment !== "string" || input.environment.trim() === "" || input.environment.startsWith("-")) {
106
+ throw new Error("sql-migrate: the step needs `environment`, the yodel environment to migrate (\"{target}\" in an op wave)");
107
+ }
108
+ if (input.spec !== undefined && (typeof input.spec !== "string" || input.spec === "" || input.spec.startsWith("-"))) {
109
+ throw new Error("sql-migrate: `spec` must name the op-waves spec file, such as \"yodel-waves.json\"");
110
+ }
111
+ }
112
+ const commonOf = (input) => ({
113
+ ...(input.dir ? { dir: input.dir } : {}),
114
+ ...(input.op ? { op: input.op } : {}),
115
+ ...(input.allowOutOfOrder ? { allowOutOfOrder: true } : {}),
116
+ });
117
+ /** The last few lines a failed run printed, for a message. */
118
+ function tail(run) {
119
+ const text = (run.stderr || run.stdout).trim();
120
+ return text.split("\n").slice(-4).join("\n");
121
+ }
122
+ /** A failed yodel run as an error, its exit read through chant's table. */
123
+ function runError(what, run, command, legacy) {
124
+ if (run.missingBinary)
125
+ return new YodelRunError(`${what}: the yodel binary was not found (the project's node_modules/.bin/yodel, or yodel on PATH); install @intentius/sql-yodeler`, CHANT_EXIT.failed, null);
126
+ const chantExit = yodelExitOf({ code: run.code, command, legacy });
127
+ return new YodelRunError(`${what} exited ${run.code === null ? "without a status" : String(run.code)}: ${tail(run)}`, chantExit, run.code);
128
+ }
129
+ function readJson(path) {
130
+ return JSON.parse(readFileSync(path, "utf-8"));
131
+ }
132
+ /** The destructive classes of a wave plan: each step's rules, as the engine's lint names them, sorted. */
133
+ export function wavePlanDestructive(plan) {
134
+ const out = new Set();
135
+ for (const s of plan.destructiveSteps ?? [])
136
+ for (const why of (s.why ?? "").split(","))
137
+ if (why.trim())
138
+ out.add(why.trim());
139
+ return [...out].sort();
140
+ }
141
+ function isSqlMigrateArtifact(value) {
142
+ const v = value;
143
+ return v?.kind === "sql-migrate" && typeof v.environment === "string" && typeof v.planDigest === "string";
144
+ }
145
+ /** A legacy yodel's moved wave plan, read from the line it prints (it exits 1, as for any error). */
146
+ const LEGACY_MOVED = /wave plan moved since the wave was decided/;
147
+ /** The `sql-migrate` capability, with its runner and environment injectable. */
148
+ export function createSqlMigrateCapability(deps = {}) {
149
+ const run = deps.run ?? defaultYodelRunner;
150
+ const cwdOf = deps.cwd ?? (() => process.cwd());
151
+ const envOf = deps.env ?? (() => process.env);
152
+ return {
153
+ kind: "sql-migrate",
154
+ // A migration has no undo short of a revert, which is a change of its own (`yodel revert`).
155
+ rollbackPolicy: "needs-opt-out",
156
+ targets: { refuse: "sql-migrate applies an environment's pending migrations in chain order, so it cannot narrow to some objects" },
157
+ // #4571: the environment's history is the grain; resource is refused by name (./lock-grain.ts).
158
+ defaultLockGrain: () => sqlDefaultLockGrain(),
159
+ lockGrainRefusal: (_input, grain) => sqlLockGrainRefusal("sql-migrate", grain),
160
+ async plan(_ctx, input) {
161
+ requireEnvironment(input);
162
+ const cwd = cwdOf();
163
+ const env = input.environment;
164
+ const files = sqlMigrateFiles(env);
165
+ const legacy = input.legacyExit === true;
166
+ const reader = readerEnv(envOf());
167
+ const opts = { cwd, env: reader, ...(input.dir ? { dir: input.dir } : {}) };
168
+ mkdirSync(dirname(join(cwd, files.part)), { recursive: true });
169
+ // A file an earlier plan left must not pass for this one's.
170
+ rmSync(join(cwd, files.part), { force: true });
171
+ rmSync(join(cwd, files.wavePlan), { force: true });
172
+ // The part: real counts, one entry per object the pending migrations touch.
173
+ const planned = await runAllowedYodel(run, yodelPlanArgs(env, files.part, commonOf(input)), opts);
174
+ if (!existsSync(join(cwd, files.part)))
175
+ throw runError(`sql-migrate: yodel plan ${env}`, planned, "plan", legacy);
176
+ const part = readChangeSetPartFile(readJson(join(cwd, files.part)), "sql", env, "sql");
177
+ if (part.member.status === "failed") {
178
+ // A refusal no approval fixes (a checksum, a fork, the directory): `failed` on chant's table (#4570).
179
+ throw new YodelRunError(`sql-migrate: yodel would refuse to apply ${env}: ${part.member.error ?? tail(planned)}`, CHANT_EXIT.failed, planned.code);
180
+ }
181
+ // 2: a pending step the engine does not run; the part still says which, and the wave plan refuses it.
182
+ if (planned.code !== 0 && planned.code !== 2)
183
+ throw runError(`sql-migrate: yodel plan ${env}`, planned, "plan", legacy);
184
+ if (input.spec === undefined) {
185
+ // A pull request's plan: the migrations' digest, the one `yodel approve` binds.
186
+ const planDigest = part.member.planDigest;
187
+ const artifact = { kind: "sql-migrate", environment: env, planDigest, migrations: planDigest, pending: [] };
188
+ return { member: env, planDigest, artifact, changeSet: part };
189
+ }
190
+ if (!existsSync(join(cwd, input.spec)))
191
+ throw new Error(`sql-migrate: the op-waves spec ${input.spec} is not in ${cwd}; the engine reads chant's decision for the wave by it`);
192
+ const waved = await runAllowedYodel(run, yodelWavePlanArgs(env, files.wavePlan, input.spec, commonOf(input)), opts);
193
+ if (waved.code !== 0 || !existsSync(join(cwd, files.wavePlan)))
194
+ throw runError(`sql-migrate: yodel apply ${env} --wave-plan`, waved, "apply", legacy);
195
+ const wave = readJson(join(cwd, files.wavePlan));
196
+ if (typeof wave.planDigest !== "string" || wave.planDigest === "")
197
+ throw new Error(`sql-migrate: ${files.wavePlan} has no planDigest; it is not a plan yodel apply --wave-plan wrote`);
198
+ const destructive = wavePlanDestructive(wave);
199
+ const artifact = {
200
+ kind: "sql-migrate",
201
+ environment: env,
202
+ planDigest: wave.planDigest,
203
+ wavePlan: files.wavePlan,
204
+ spec: input.spec,
205
+ ...(wave.migrations ? { migrations: wave.migrations } : {}),
206
+ pending: wave.pending ?? [],
207
+ };
208
+ // The member's digest is the wave plan's, so a gate on the member alone and the wave bind one value.
209
+ const changeSet = { ...part, member: { ...part.member, planDigest: wave.planDigest } };
210
+ return { member: env, planDigest: wave.planDigest, artifact, changeSet, ...(destructive.length > 0 ? { destructive } : {}) };
211
+ },
212
+ async run(ctx, input) {
213
+ requireEnvironment(input);
214
+ const env = input.environment;
215
+ const decided = ctx.plans?.[env];
216
+ if (!isSqlMigrateArtifact(decided) || decided.wavePlan === undefined || decided.spec === undefined) {
217
+ throw new Error(`sql-migrate: ${env} applies only the plan an op wave decided, planned with \`spec\`; migrations apply on the merged branch, never before the merge (sql-yodeler#285, ruling E). Nothing was applied`);
218
+ }
219
+ const cwd = cwdOf();
220
+ const legacy = input.legacyExit === true;
221
+ const file = join(cwd, decided.wavePlan);
222
+ if (!existsSync(file))
223
+ throw new Error(`sql-migrate: the wave plan of ${env} is gone from ${decided.wavePlan}; nothing was applied. Run the wave again`);
224
+ const onDisk = readJson(file);
225
+ if (onDisk.planDigest !== decided.planDigest) {
226
+ throw new SqlPlanMovedError(env, `sql-migrate: ${decided.wavePlan} holds plan ${String(onDisk.planDigest)}, and the wave decided ${decided.planDigest}; nothing was applied`);
227
+ }
228
+ // The apply alone runs with the job's environment, so the writer reaches it and nothing else.
229
+ const applied = await runAllowedYodel(run, yodelWaveApplyArgs(env, decided.wavePlan, decided.spec, commonOf(input)), { cwd, env: envOf(), ...(input.dir ? { dir: input.dir } : {}) });
230
+ if (applied.code === 0)
231
+ return { environment: env, planDigest: decided.planDigest, summary: applied.stdout.trim() };
232
+ const error = runError(`sql-migrate: yodel apply ${env} --wave-apply`, applied, "apply", legacy);
233
+ if (error.chantExit === CHANT_EXIT.moved || (legacy && LEGACY_MOVED.test(`${applied.stderr}\n${applied.stdout}`)))
234
+ throw new SqlPlanMovedError(env, error.message);
235
+ throw error;
236
+ },
237
+ };
238
+ }
239
+ export const sqlMigrateCapability = createSqlMigrateCapability();
240
+ /** The dialect a build output names: `postgres`, else `clickhouse`. */
241
+ export function buildDialect(path) {
242
+ let raw;
243
+ try {
244
+ raw = JSON.parse(readFileSync(path, "utf-8"));
245
+ }
246
+ catch (err) {
247
+ throw new Error(`sql-apply: the build ${path} cannot be read: ${err instanceof Error ? err.message : String(err)}. Run chant build first`);
248
+ }
249
+ const d = raw.sql && typeof raw.sql === "object" ? raw.sql.dialect : raw.dialect;
250
+ return d === "postgres" ? "postgres" : "clickhouse";
251
+ }
252
+ const pick = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
253
+ async function defaultPlan(input, dialect) {
254
+ const cwd = input.cwd ? { cwd: input.cwd } : {};
255
+ if (dialect === "postgres") {
256
+ const { planPgAgainstServer } = await import("./postgres/plan/commands.js");
257
+ const diff = await planPgAgainstServer(input.environment, input.build, cwd);
258
+ const { HANDED_OFF } = await import("./postgres/migrate/handoff.js");
259
+ return {
260
+ dialect,
261
+ changes: diff.changes,
262
+ // A column rename or a type change across kinds is a PostgresMigrationOp's; the other expand and contract changes are made by hand.
263
+ refused: diff.refused.map((c) => ({ object: c.object, field: c.field, ...(HANDED_OFF.has(c.rule) ? { op: "PostgresMigrationOp" } : {}) })),
264
+ holes: [],
265
+ problems: [],
266
+ };
267
+ }
268
+ const { planAgainstServer } = await import("./clickhouse/plan/commands.js");
269
+ const { namedCollectionsFail } = await import("./clickhouse/live/named-collections.js");
270
+ const diff = await planAgainstServer(input.environment, input.build, cwd);
271
+ const missing = namedCollectionsFail(diff.namedCollections) ? diff.namedCollections.missing : [];
272
+ return {
273
+ dialect,
274
+ changes: diff.changes,
275
+ refused: diff.rebuilds.map((c) => ({ object: c.object, field: c.field, op: "ClickHouseRebuildOp" })),
276
+ holes: (diff.unreadable ?? []).map((u) => ({ address: u.object, type: u.type, reason: u.reason })),
277
+ problems: missing.length > 0 ? [`the build references named collection(s) the server does not have: ${missing.join(", ")}`] : [],
278
+ };
279
+ }
280
+ async function defaultApply(input, dialect) {
281
+ const args = pick({ buildPath: input.build, environment: input.environment, prune: input.prune, stack: input.stack, ownershipEnv: input.ownershipEnv, cwd: input.cwd });
282
+ const outcome = dialect === "postgres"
283
+ ? await (await import("./op/activities/postgres-apply.js")).postgresApply({
284
+ ...args,
285
+ buildPath: input.build,
286
+ ...pick({ lockTimeoutMs: input.lockTimeoutMs, statementTimeoutMs: input.statementTimeoutMs, scanTimeoutMs: input.scanTimeoutMs }),
287
+ })
288
+ : await (await import("./op/activities/clickhouse-apply.js")).clickhouseApply({ ...args, buildPath: input.build, ...pick({ mutationTimeoutMs: input.mutationTimeoutMs }) });
289
+ return {
290
+ applied: outcome.applied.map((a) => `${a.kind}/${a.name}`),
291
+ pruned: outcome.pruned.map((p) => `${p.kind}/${p.name}`),
292
+ notAttempted: outcome.notAttempted.map((n) => ({ name: `${n.kind}/${n.name}`, reason: `${n.reason}${n.detail ? `: ${n.detail}` : ""}` })),
293
+ };
294
+ }
295
+ function requireApplyInput(input) {
296
+ if (typeof input?.environment !== "string" || input.environment.trim() === "")
297
+ throw new Error("sql-apply: the step needs `environment`, which selects sql.profiles.<environment>");
298
+ if (typeof input.build !== "string" || input.build.trim() === "")
299
+ throw new Error("sql-apply: the step needs `build`, the chant build output to apply (dist/schema.json)");
300
+ }
301
+ /** Plan `input` and say why it cannot be applied, if it cannot. */
302
+ async function planDeclarative(plan, input) {
303
+ const dialect = buildDialect(input.build);
304
+ const planned = await plan(input, dialect);
305
+ const why = [...planned.problems];
306
+ if (planned.refused.length > 0) {
307
+ why.push(`the applier does not make ${planned.refused.map((r) => `${r.object} (${r.field}${r.op ? `, run a ${r.op}` : ""})`).join(", ")} in place`);
308
+ }
309
+ if (planned.holes.length > 0)
310
+ why.push(`the plan cannot read ${planned.holes.map((h) => `${h.address}: ${h.reason}`).join("; ")}`);
311
+ if (why.length > 0)
312
+ throw new Error(`sql-apply: ${input.environment} cannot apply this build: ${why.join("; and ")}`);
313
+ const part = sqlDeclarativePlanPart({ member: input.environment, dialect, environment: input.environment, changes: planned.changes });
314
+ return { part, dialect };
315
+ }
316
+ /** The `sql-apply` capability, with its planner and applier injectable. */
317
+ export function createSqlApplyCapability(deps = {}) {
318
+ const plan = deps.plan ?? defaultPlan;
319
+ const apply = deps.apply ?? defaultApply;
320
+ return {
321
+ kind: "sql-apply",
322
+ // An apply has no undo short of applying the previous build, which is a change of its own.
323
+ rollbackPolicy: "needs-opt-out",
324
+ targets: { refuse: "sql-apply applies a whole build, since its objects depend on each other in creation order" },
325
+ // #4571: the environment is the grain, as for sql-migrate.
326
+ defaultLockGrain: () => sqlDefaultLockGrain(),
327
+ lockGrainRefusal: (_input, grain) => sqlLockGrainRefusal("sql-apply", grain),
328
+ async plan(_ctx, input) {
329
+ requireApplyInput(input);
330
+ const { part, dialect } = await planDeclarative(plan, input);
331
+ const planDigest = part.member.planDigest;
332
+ const artifact = { kind: "sql-apply", environment: input.environment, dialect, planDigest };
333
+ return { member: input.environment, planDigest, artifact, changeSet: part };
334
+ },
335
+ async run(ctx, input) {
336
+ requireApplyInput(input);
337
+ const decided = ctx.plans?.[input.environment];
338
+ if (decided?.kind !== "sql-apply" || typeof decided.planDigest !== "string") {
339
+ throw new Error(`sql-apply: ${input.environment} applies only a plan that was planned and decided first (DeployContext.plans); nothing was applied`);
340
+ }
341
+ // Planned again just before the apply: a server or build that moved since the decision applies nothing.
342
+ const { part, dialect } = await planDeclarative(plan, input);
343
+ if (part.member.planDigest !== decided.planDigest) {
344
+ throw new SqlPlanMovedError(input.environment, `sql-apply: ${input.environment}'s plan moved since it was decided (decided ${decided.planDigest}, now ${String(part.member.planDigest)}); nothing was applied`);
345
+ }
346
+ const outcome = await apply(input, dialect);
347
+ if (outcome.notAttempted.length > 0 && outcome.applied.length === 0 && outcome.pruned.length === 0 && part.entries.length > 0) {
348
+ throw new Error(`sql-apply: nothing was applied to ${input.environment}: ${outcome.notAttempted.map((n) => `${n.name} (${n.reason})`).join(", ")}`);
349
+ }
350
+ return { environment: input.environment, dialect, planDigest: decided.planDigest, applied: outcome.applied, pruned: outcome.pruned };
351
+ },
352
+ };
353
+ }
354
+ export const sqlApplyCapability = createSqlApplyCapability();
355
+ // ── the plugin ────────────────────────────────────────────────────────────────
356
+ export const SQL_VERB_FAMILIES = {
357
+ apply: ["sql-apply", "sql-migrate"],
358
+ };
359
+ export const sqlCapabilityPlugin = {
360
+ name: "sql",
361
+ get version() {
362
+ return ownPackageVersion(import.meta.url);
363
+ },
364
+ capabilities() {
365
+ return [sqlMigrateCapability, sqlApplyCapability];
366
+ },
367
+ families() {
368
+ return SQL_VERB_FAMILIES;
369
+ },
370
+ };
@@ -11,6 +11,8 @@
11
11
  * - the plan step of the lexicon's own migration Ops, `ClickHouseRebuildOp`
12
12
  * and `PostgresMigrationOp`. {@link clickhouseRebuildPart} and
13
13
  * {@link postgresMigrationPart}.
14
+ * - a declarative plan's classified changes, as the `sql-apply` capability
15
+ * plans them (#4569). {@link sqlDeclarativePlanPart}.
14
16
  *
15
17
  * Each returns a part with planner `sql`, so it composes with every other
16
18
  * member's in `composeChangeSet`, and a gate that binds the document gets its
@@ -170,5 +172,38 @@ export declare function clickhouseRebuildPart(input: SqlOpPlanPartInput): Change
170
172
  * propose nothing.
171
173
  */
172
174
  export declare function postgresMigrationPart(input: SqlOpPlanPartInput): ChangeSetPart;
175
+ /**
176
+ * One classified change of a declarative plan (`chant sql plan`): a build
177
+ * against the environment's server, as the dialects' classifiers write it.
178
+ */
179
+ export interface SqlDeclarativeChange {
180
+ /** The object, as the plan labels it. */
181
+ object: string;
182
+ field: string;
183
+ rule: string;
184
+ class: string;
185
+ before?: string;
186
+ after?: string;
187
+ destructive?: boolean;
188
+ }
189
+ export interface SqlDeclarativePlanPartInput {
190
+ member: string;
191
+ dialect: "clickhouse" | "postgres";
192
+ /** The environment it plans into: the member's and every entry's `scope`. */
193
+ environment: string;
194
+ changes: readonly SqlDeclarativeChange[];
195
+ /** What the plan could not read (#3653), as holes. */
196
+ holes?: ChangeSetHole[];
197
+ /** Default `"sql"`. */
198
+ lexicon?: string;
199
+ }
200
+ /** The digest of a declarative plan: the dialect, the environment and every change, sorted. */
201
+ export declare function sqlDeclarativePlanDigest(input: Pick<SqlDeclarativePlanPartInput, "dialect" | "environment" | "changes">): string;
202
+ /**
203
+ * A declarative plan's part (#4569): one entry per object the changes touch,
204
+ * the action and disruption from their classes as for a versioned plan, the
205
+ * fields as attributes. Its digest is {@link sqlDeclarativePlanDigest}.
206
+ */
207
+ export declare function sqlDeclarativePlanPart(input: SqlDeclarativePlanPartInput): ChangeSetPart;
173
208
  export {};
174
209
  //# sourceMappingURL=change-set.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"change-set.d.ts","sourceRoot":"","sources":["../src/change-set.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EAEnB,aAAa,EACb,aAAa,EACd,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAExE,wDAAwD;AACxD,eAAO,MAAM,sBAAsB,QAAQ,CAAC;AAE5C;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAU9E,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG;IACxC,uEAAuE;IACvE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,yEAAyE;IACzE,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,uGAAuG;IACvG,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;CAC7B,CAAC;AAEF,yHAAyH;AACzH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,aAAa,CAAC,WAAW,GAAG;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC3E;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC7C,6EAA6E;IAC7E,OAAO,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC,uEAAuE;IACvE,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,gBAAgB,CAAC;IACvB,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAMD,iFAAiF;AACjF,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,mCAAmC;IACnC,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;CACtC;AAmDD,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC,GAAG;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,UAAU,CAAC,EAAE,mBAAmB,CAAA;CAAE,CAwBpK;AAeD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,aAAa,CA4BpF;AAID;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oHAAoH;IACpH,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;IAChB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAgCD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAI9E;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAK9E"}
1
+ {"version":3,"file":"change-set.d.ts","sourceRoot":"","sources":["../src/change-set.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,8 @@
11
11
  * - the plan step of the lexicon's own migration Ops, `ClickHouseRebuildOp`
12
12
  * and `PostgresMigrationOp`. {@link clickhouseRebuildPart} and
13
13
  * {@link postgresMigrationPart}.
14
+ * - a declarative plan's classified changes, as the `sql-apply` capability
15
+ * plans them (#4569). {@link sqlDeclarativePlanPart}.
14
16
  *
15
17
  * Each returns a part with planner `sql`, so it composes with every other
16
18
  * member's in `composeChangeSet`, and a gate that binds the document gets its
@@ -249,3 +251,44 @@ export function postgresMigrationPart(input) {
249
251
  const table = migration.split(".").slice(0, -1).join(".") || migration;
250
252
  return opPart(input, "PostgresMigrationOp", "migrate", table, "Postgres::Table");
251
253
  }
254
+ /** The entry type a declarative change gets: the classified change names no object kind. */
255
+ const DECLARATIVE_TYPE = { clickhouse: "ClickHouse::Object", postgres: "Postgres::Object" };
256
+ /** The digest of a declarative plan: the dialect, the environment and every change, sorted. */
257
+ export function sqlDeclarativePlanDigest(input) {
258
+ const changes = input.changes
259
+ .map((c) => ({ object: c.object, field: c.field, rule: c.rule, class: c.class, before: c.before ?? null, after: c.after ?? null, destructive: c.destructive === true }))
260
+ .sort((a, b) => (a.object === b.object ? (a.field < b.field ? -1 : a.field > b.field ? 1 : 0) : a.object < b.object ? -1 : 1));
261
+ return computePlanDigest("sql-declarative-plan", { dialect: input.dialect, environment: input.environment, changes });
262
+ }
263
+ /**
264
+ * A declarative plan's part (#4569): one entry per object the changes touch,
265
+ * the action and disruption from their classes as for a versioned plan, the
266
+ * fields as attributes. Its digest is {@link sqlDeclarativePlanDigest}.
267
+ */
268
+ export function sqlDeclarativePlanPart(input) {
269
+ const lexicon = input.lexicon ?? SQL_CHANGE_SET_LEXICON;
270
+ const byAddress = new Map();
271
+ for (const c of input.changes) {
272
+ let o = byAddress.get(c.object);
273
+ if (!o) {
274
+ o = { address: c.object, type: DECLARATIVE_TYPE[input.dialect], classes: [], actions: [], destructive: false, ops: [], manual: [], sql: [], fields: new Map() };
275
+ byAddress.set(c.object, o);
276
+ }
277
+ o.classes.push(c.class);
278
+ if (c.destructive)
279
+ o.destructive = true;
280
+ o.fields.set(c.field, { rule: c.rule, class: c.class, field: c.field, ...(c.before !== undefined ? { before: c.before } : {}), ...(c.after !== undefined ? { after: c.after } : {}), ...(c.destructive ? { destructive: true } : {}) });
281
+ }
282
+ return {
283
+ member: {
284
+ member: input.member,
285
+ lexicon,
286
+ planner: "sql",
287
+ scope: input.environment,
288
+ status: "planned",
289
+ planDigest: sqlDeclarativePlanDigest(input),
290
+ holes: input.holes ?? [],
291
+ },
292
+ entries: entriesOf(input.member, lexicon, input.environment, [...byAddress.values()]),
293
+ };
294
+ }