@intentius/sql-yodeler 0.0.0-stage → 0.2.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.
Files changed (324) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +112 -2
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +29 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chant/apply.d.ts +59 -0
  7. package/dist/chant/apply.js +116 -0
  8. package/dist/chant/apply.js.map +1 -0
  9. package/dist/chant/build.d.ts +34 -0
  10. package/dist/chant/build.js +48 -0
  11. package/dist/chant/build.js.map +1 -0
  12. package/dist/chant/drift.d.ts +112 -0
  13. package/dist/chant/drift.js +238 -0
  14. package/dist/chant/drift.js.map +1 -0
  15. package/dist/chant/plan.d.ts +45 -0
  16. package/dist/chant/plan.js +61 -0
  17. package/dist/chant/plan.js.map +1 -0
  18. package/dist/chant/process.d.ts +41 -0
  19. package/dist/chant/process.js +87 -0
  20. package/dist/chant/process.js.map +1 -0
  21. package/dist/chant/runnable.d.ts +19 -0
  22. package/dist/chant/runnable.js +47 -0
  23. package/dist/chant/runnable.js.map +1 -0
  24. package/dist/cli.d.ts +8 -0
  25. package/dist/cli.js +99 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/command.d.ts +23 -0
  28. package/dist/command.js +13 -0
  29. package/dist/command.js.map +1 -0
  30. package/dist/commands/apply.d.ts +3 -0
  31. package/dist/commands/apply.js +426 -0
  32. package/dist/commands/apply.js.map +1 -0
  33. package/dist/commands/approve.d.ts +5 -0
  34. package/dist/commands/approve.js +111 -0
  35. package/dist/commands/approve.js.map +1 -0
  36. package/dist/commands/drift.d.ts +14 -0
  37. package/dist/commands/drift.js +267 -0
  38. package/dist/commands/drift.js.map +1 -0
  39. package/dist/commands/index.d.ts +3 -0
  40. package/dist/commands/index.js +28 -0
  41. package/dist/commands/index.js.map +1 -0
  42. package/dist/commands/init.d.ts +6 -0
  43. package/dist/commands/init.js +139 -0
  44. package/dist/commands/init.js.map +1 -0
  45. package/dist/commands/lint.d.ts +3 -0
  46. package/dist/commands/lint.js +202 -0
  47. package/dist/commands/lint.js.map +1 -0
  48. package/dist/commands/mcp.d.ts +3 -0
  49. package/dist/commands/mcp.js +50 -0
  50. package/dist/commands/mcp.js.map +1 -0
  51. package/dist/commands/new.d.ts +5 -0
  52. package/dist/commands/new.js +292 -0
  53. package/dist/commands/new.js.map +1 -0
  54. package/dist/commands/override.d.ts +3 -0
  55. package/dist/commands/override.js +118 -0
  56. package/dist/commands/override.js.map +1 -0
  57. package/dist/commands/plan.d.ts +3 -0
  58. package/dist/commands/plan.js +183 -0
  59. package/dist/commands/plan.js.map +1 -0
  60. package/dist/commands/rebase.d.ts +3 -0
  61. package/dist/commands/rebase.js +167 -0
  62. package/dist/commands/rebase.js.map +1 -0
  63. package/dist/commands/repair.d.ts +14 -0
  64. package/dist/commands/repair.js +125 -0
  65. package/dist/commands/repair.js.map +1 -0
  66. package/dist/commands/status.d.ts +7 -0
  67. package/dist/commands/status.js +54 -0
  68. package/dist/commands/status.js.map +1 -0
  69. package/dist/config/define.d.ts +148 -0
  70. package/dist/config/define.js +20 -0
  71. package/dist/config/define.js.map +1 -0
  72. package/dist/config/index.d.ts +11 -0
  73. package/dist/config/index.js +12 -0
  74. package/dist/config/index.js.map +1 -0
  75. package/dist/config/load.d.ts +17 -0
  76. package/dist/config/load.js +162 -0
  77. package/dist/config/load.js.map +1 -0
  78. package/dist/config/settings.d.ts +53 -0
  79. package/dist/config/settings.js +116 -0
  80. package/dist/config/settings.js.map +1 -0
  81. package/dist/env-args.d.ts +24 -0
  82. package/dist/env-args.js +92 -0
  83. package/dist/env-args.js.map +1 -0
  84. package/dist/exit.d.ts +20 -0
  85. package/dist/exit.js +28 -0
  86. package/dist/exit.js.map +1 -0
  87. package/dist/history/client.d.ts +25 -0
  88. package/dist/history/client.js +42 -0
  89. package/dist/history/client.js.map +1 -0
  90. package/dist/history/ddl.d.ts +58 -0
  91. package/dist/history/ddl.js +100 -0
  92. package/dist/history/ddl.js.map +1 -0
  93. package/dist/history/index.d.ts +25 -0
  94. package/dist/history/index.js +26 -0
  95. package/dist/history/index.js.map +1 -0
  96. package/dist/history/lock.d.ts +104 -0
  97. package/dist/history/lock.js +230 -0
  98. package/dist/history/lock.js.map +1 -0
  99. package/dist/history/postgres/ddl.d.ts +22 -0
  100. package/dist/history/postgres/ddl.js +47 -0
  101. package/dist/history/postgres/ddl.js.map +1 -0
  102. package/dist/history/postgres/index.d.ts +12 -0
  103. package/dist/history/postgres/index.js +13 -0
  104. package/dist/history/postgres/index.js.map +1 -0
  105. package/dist/history/postgres/lock.d.ts +48 -0
  106. package/dist/history/postgres/lock.js +95 -0
  107. package/dist/history/postgres/lock.js.map +1 -0
  108. package/dist/history/postgres/store.d.ts +39 -0
  109. package/dist/history/postgres/store.js +68 -0
  110. package/dist/history/postgres/store.js.map +1 -0
  111. package/dist/history/rows.d.ts +96 -0
  112. package/dist/history/rows.js +73 -0
  113. package/dist/history/rows.js.map +1 -0
  114. package/dist/history/store.d.ts +72 -0
  115. package/dist/history/store.js +134 -0
  116. package/dist/history/store.js.map +1 -0
  117. package/dist/history/topology.d.ts +41 -0
  118. package/dist/history/topology.js +54 -0
  119. package/dist/history/topology.js.map +1 -0
  120. package/dist/index.d.ts +10 -0
  121. package/dist/index.js +10 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/init/adopt.d.ts +70 -0
  124. package/dist/init/adopt.js +143 -0
  125. package/dist/init/adopt.js.map +1 -0
  126. package/dist/init/baseline.d.ts +61 -0
  127. package/dist/init/baseline.js +88 -0
  128. package/dist/init/baseline.js.map +1 -0
  129. package/dist/init/files.d.ts +23 -0
  130. package/dist/init/files.js +77 -0
  131. package/dist/init/files.js.map +1 -0
  132. package/dist/init/index.d.ts +16 -0
  133. package/dist/init/index.js +17 -0
  134. package/dist/init/index.js.map +1 -0
  135. package/dist/init/live.d.ts +39 -0
  136. package/dist/init/live.js +68 -0
  137. package/dist/init/live.js.map +1 -0
  138. package/dist/lint/check.d.ts +88 -0
  139. package/dist/lint/check.js +227 -0
  140. package/dist/lint/check.js.map +1 -0
  141. package/dist/lint/config.d.ts +21 -0
  142. package/dist/lint/config.js +43 -0
  143. package/dist/lint/config.js.map +1 -0
  144. package/dist/lint/index.d.ts +31 -0
  145. package/dist/lint/index.js +43 -0
  146. package/dist/lint/index.js.map +1 -0
  147. package/dist/lint/project-rules.d.ts +19 -0
  148. package/dist/lint/project-rules.js +32 -0
  149. package/dist/lint/project-rules.js.map +1 -0
  150. package/dist/lint/report.d.ts +7 -0
  151. package/dist/lint/report.js +48 -0
  152. package/dist/lint/report.js.map +1 -0
  153. package/dist/lint/rules.d.ts +53 -0
  154. package/dist/lint/rules.js +42 -0
  155. package/dist/lint/rules.js.map +1 -0
  156. package/dist/lint/silences.d.ts +49 -0
  157. package/dist/lint/silences.js +91 -0
  158. package/dist/lint/silences.js.map +1 -0
  159. package/dist/lint/statements.d.ts +38 -0
  160. package/dist/lint/statements.js +120 -0
  161. package/dist/lint/statements.js.map +1 -0
  162. package/dist/mcp/server.d.ts +40 -0
  163. package/dist/mcp/server.js +106 -0
  164. package/dist/mcp/server.js.map +1 -0
  165. package/dist/mcp/tools.d.ts +72 -0
  166. package/dist/mcp/tools.js +178 -0
  167. package/dist/mcp/tools.js.map +1 -0
  168. package/dist/migrations/checks.d.ts +28 -0
  169. package/dist/migrations/checks.js +97 -0
  170. package/dist/migrations/checks.js.map +1 -0
  171. package/dist/migrations/checksum.d.ts +56 -0
  172. package/dist/migrations/checksum.js +72 -0
  173. package/dist/migrations/checksum.js.map +1 -0
  174. package/dist/migrations/create.d.ts +75 -0
  175. package/dist/migrations/create.js +120 -0
  176. package/dist/migrations/create.js.map +1 -0
  177. package/dist/migrations/directory.d.ts +119 -0
  178. package/dist/migrations/directory.js +229 -0
  179. package/dist/migrations/directory.js.map +1 -0
  180. package/dist/migrations/document.d.ts +106 -0
  181. package/dist/migrations/document.js +83 -0
  182. package/dist/migrations/document.js.map +1 -0
  183. package/dist/migrations/index.d.ts +21 -0
  184. package/dist/migrations/index.js +22 -0
  185. package/dist/migrations/index.js.map +1 -0
  186. package/dist/pr/comment.d.ts +32 -0
  187. package/dist/pr/comment.js +99 -0
  188. package/dist/pr/comment.js.map +1 -0
  189. package/dist/pr/forge.d.ts +54 -0
  190. package/dist/pr/forge.js +119 -0
  191. package/dist/pr/forge.js.map +1 -0
  192. package/dist/pr/issue.d.ts +68 -0
  193. package/dist/pr/issue.js +107 -0
  194. package/dist/pr/issue.js.map +1 -0
  195. package/dist/pr/model.d.ts +99 -0
  196. package/dist/pr/model.js +137 -0
  197. package/dist/pr/model.js.map +1 -0
  198. package/dist/pr/render.d.ts +29 -0
  199. package/dist/pr/render.js +279 -0
  200. package/dist/pr/render.js.map +1 -0
  201. package/dist/project.d.ts +62 -0
  202. package/dist/project.js +92 -0
  203. package/dist/project.js.map +1 -0
  204. package/dist/rebase/index.d.ts +121 -0
  205. package/dist/rebase/index.js +390 -0
  206. package/dist/rebase/index.js.map +1 -0
  207. package/dist/rebase/repair.d.ts +41 -0
  208. package/dist/rebase/repair.js +68 -0
  209. package/dist/rebase/repair.js.map +1 -0
  210. package/dist/replay/clickhouse.d.ts +33 -0
  211. package/dist/replay/clickhouse.js +106 -0
  212. package/dist/replay/clickhouse.js.map +1 -0
  213. package/dist/replay/index.d.ts +39 -0
  214. package/dist/replay/index.js +57 -0
  215. package/dist/replay/index.js.map +1 -0
  216. package/dist/replay/postgres.d.ts +31 -0
  217. package/dist/replay/postgres.js +140 -0
  218. package/dist/replay/postgres.js.map +1 -0
  219. package/dist/replay/replay.d.ts +114 -0
  220. package/dist/replay/replay.js +98 -0
  221. package/dist/replay/replay.js.map +1 -0
  222. package/dist/replay/report.d.ts +18 -0
  223. package/dist/replay/report.js +72 -0
  224. package/dist/replay/report.js.map +1 -0
  225. package/dist/replay/shared.d.ts +28 -0
  226. package/dist/replay/shared.js +77 -0
  227. package/dist/replay/shared.js.map +1 -0
  228. package/dist/schemas.d.ts +34 -0
  229. package/dist/schemas.js +38 -0
  230. package/dist/schemas.js.map +1 -0
  231. package/dist/server.d.ts +40 -0
  232. package/dist/server.js +87 -0
  233. package/dist/server.js.map +1 -0
  234. package/dist/steps/backfill.d.ts +132 -0
  235. package/dist/steps/backfill.js +307 -0
  236. package/dist/steps/backfill.js.map +1 -0
  237. package/dist/steps/index.d.ts +52 -0
  238. package/dist/steps/index.js +52 -0
  239. package/dist/steps/index.js.map +1 -0
  240. package/dist/steps/kinds.d.ts +101 -0
  241. package/dist/steps/kinds.js +143 -0
  242. package/dist/steps/kinds.js.map +1 -0
  243. package/dist/steps/postgres-migration.d.ts +95 -0
  244. package/dist/steps/postgres-migration.js +149 -0
  245. package/dist/steps/postgres-migration.js.map +1 -0
  246. package/dist/steps/rebuild.d.ts +97 -0
  247. package/dist/steps/rebuild.js +155 -0
  248. package/dist/steps/rebuild.js.map +1 -0
  249. package/dist/steps/run.d.ts +45 -0
  250. package/dist/steps/run.js +72 -0
  251. package/dist/steps/run.js.map +1 -0
  252. package/dist/topology/render.d.ts +17 -0
  253. package/dist/topology/render.js +17 -0
  254. package/dist/topology/render.js.map +1 -0
  255. package/dist/version.d.ts +2 -0
  256. package/dist/version.js +11 -0
  257. package/dist/version.js.map +1 -0
  258. package/dist/versioned/approval.d.ts +35 -0
  259. package/dist/versioned/approval.js +53 -0
  260. package/dist/versioned/approval.js.map +1 -0
  261. package/dist/versioned/checks.d.ts +29 -0
  262. package/dist/versioned/checks.js +54 -0
  263. package/dist/versioned/checks.js.map +1 -0
  264. package/dist/versioned/clickhouse.d.ts +24 -0
  265. package/dist/versioned/clickhouse.js +42 -0
  266. package/dist/versioned/clickhouse.js.map +1 -0
  267. package/dist/versioned/digest.d.ts +77 -0
  268. package/dist/versioned/digest.js +69 -0
  269. package/dist/versioned/digest.js.map +1 -0
  270. package/dist/versioned/driver.d.ts +51 -0
  271. package/dist/versioned/driver.js +13 -0
  272. package/dist/versioned/driver.js.map +1 -0
  273. package/dist/versioned/engine.d.ts +206 -0
  274. package/dist/versioned/engine.js +370 -0
  275. package/dist/versioned/engine.js.map +1 -0
  276. package/dist/versioned/explain.d.ts +123 -0
  277. package/dist/versioned/explain.js +294 -0
  278. package/dist/versioned/explain.js.map +1 -0
  279. package/dist/versioned/index.d.ts +46 -0
  280. package/dist/versioned/index.js +77 -0
  281. package/dist/versioned/index.js.map +1 -0
  282. package/dist/versioned/op.d.ts +72 -0
  283. package/dist/versioned/op.js +185 -0
  284. package/dist/versioned/op.js.map +1 -0
  285. package/dist/versioned/policy.d.ts +67 -0
  286. package/dist/versioned/policy.js +161 -0
  287. package/dist/versioned/policy.js.map +1 -0
  288. package/dist/versioned/postgres/catalog.d.ts +33 -0
  289. package/dist/versioned/postgres/catalog.js +34 -0
  290. package/dist/versioned/postgres/catalog.js.map +1 -0
  291. package/dist/versioned/postgres/driver.d.ts +71 -0
  292. package/dist/versioned/postgres/driver.js +140 -0
  293. package/dist/versioned/postgres/driver.js.map +1 -0
  294. package/dist/versioned/postgres/executor.d.ts +104 -0
  295. package/dist/versioned/postgres/executor.js +172 -0
  296. package/dist/versioned/postgres/executor.js.map +1 -0
  297. package/dist/versioned/postgres/index.d.ts +11 -0
  298. package/dist/versioned/postgres/index.js +12 -0
  299. package/dist/versioned/postgres/index.js.map +1 -0
  300. package/dist/versioned/postgres/waits.d.ts +46 -0
  301. package/dist/versioned/postgres/waits.js +78 -0
  302. package/dist/versioned/postgres/waits.js.map +1 -0
  303. package/dist/versioned/report.d.ts +25 -0
  304. package/dist/versioned/report.js +84 -0
  305. package/dist/versioned/report.js.map +1 -0
  306. package/dist/versioned/stand-down.d.ts +30 -0
  307. package/dist/versioned/stand-down.js +56 -0
  308. package/dist/versioned/stand-down.js.map +1 -0
  309. package/dist/versioned/state.d.ts +108 -0
  310. package/dist/versioned/state.js +153 -0
  311. package/dist/versioned/state.js.map +1 -0
  312. package/package.json +61 -4
  313. package/schemas/apply.schema.json +269 -0
  314. package/schemas/common.schema.json +213 -0
  315. package/schemas/drift.schema.json +36 -0
  316. package/schemas/init.schema.json +31 -0
  317. package/schemas/lint-rules.schema.json +54 -0
  318. package/schemas/lint.schema.json +290 -0
  319. package/schemas/mcp.schema.json +70 -0
  320. package/schemas/new.schema.json +71 -0
  321. package/schemas/plan.schema.json +371 -0
  322. package/schemas/rebase.schema.json +54 -0
  323. package/schemas/repair.schema.json +20 -0
  324. package/schemas/status.schema.json +310 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * The approval: a versioned apply runs as a chant Op, behind a gate bound to
3
+ * the plan digest, the same machinery #6's declarative apply runs through.
4
+ *
5
+ * The project declares one Op per environment:
6
+ *
7
+ * Plan shell("yodel apply <env> --digest", { id: "plan" })
8
+ * prints the digest of the pending migrations and the live schema
9
+ * Approve gate(..., { plan: stepOutput(plan, "stdout") })
10
+ * a resolution counts only for that digest (`chant approve <op> <gate> --plan <digest>`)
11
+ * Apply shell("yodel apply <env> --execute", { env: { YODEL_APPROVED_PLAN: stepOutput(plan, "stdout") } })
12
+ * takes the lock, computes the digest again, refuses if it moved, applies
13
+ *
14
+ * `yodel apply <env>` runs it with `chant run <op>`, as #6 runs an ApplyOp.
15
+ * The gate ledger is chant's (the `chant/lifecycle` branch); yodel keeps no
16
+ * approval store of its own. The digest is checked twice: by the gate, on
17
+ * every run, against the approval; and by the Apply step, under the lock,
18
+ * against the digest the gate passed, so a change between the two (another
19
+ * runner applying, a table altered out of band) refuses too.
20
+ *
21
+ * `chant run <op>` run directly (in CI) needs `yodel` on PATH, e.g.
22
+ * `PATH="$PWD/node_modules/.bin:$PATH"`. When `yodel apply` runs it, yodel
23
+ * puts a `yodel` that runs itself first on the child's PATH.
24
+ */
25
+ import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
26
+ import { tmpdir } from "node:os";
27
+ import { delimiter, extname, join } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+ import { YodelError } from "../project.js";
30
+ import { chantInstall, runChant } from "../chant/process.js";
31
+ import { discoveryError, GATED, readApplyRun } from "../chant/apply.js";
32
+ /** The env var the Apply step reads the approved digest from. */
33
+ export const APPROVED_PLAN_ENV = "YODEL_APPROVED_PLAN";
34
+ /** Passes `--allow-out-of-order` through `chant run` to the Op's steps. */
35
+ export const ALLOW_OUT_OF_ORDER_ENV = "YODEL_ALLOW_OUT_OF_ORDER";
36
+ const command = (verb) => new RegExp(`^(?:npx\\s+)?yodel\\s+apply\\s+(\\S+)\\s+${verb}(?:\\s|$)`);
37
+ const DIGEST = command("--digest");
38
+ const EXECUTE = command("--execute");
39
+ const steps = (config) => config.phases.flatMap((p) => p.steps);
40
+ function shellCmd(step) {
41
+ if (step.kind !== "activity" || step.fn !== "shellCmd")
42
+ return undefined;
43
+ const cmd = step.args?.cmd;
44
+ return typeof cmd === "string" ? { cmd: cmd.trim(), ...(step.id ? { id: step.id } : {}) } : undefined;
45
+ }
46
+ /** The parts of a migrations Op for `env`, when `config` is one. */
47
+ export function migrateOpParts(config, env) {
48
+ const all = steps(config);
49
+ const plan = all.map(shellCmd).find((s) => s && DIGEST.exec(s.cmd)?.[1] === env);
50
+ const execute = all.map(shellCmd).find((s) => s && EXECUTE.exec(s.cmd)?.[1] === env);
51
+ if (!plan || !execute)
52
+ return undefined;
53
+ const gate = all.find((s) => {
54
+ if (s.kind !== "gate" || s.plan === undefined || typeof s.plan === "string")
55
+ return false;
56
+ return s.plan.step === plan.id;
57
+ });
58
+ return gate?.kind === "gate" ? { gate: gate.gate, quorum: gate.approval !== undefined } : { gate: undefined, quorum: false };
59
+ }
60
+ /** How to declare the Op `yodel apply` looks for. */
61
+ export function migrateOpExample(env) {
62
+ return [
63
+ `// ops/migrate-${env}.op.ts`,
64
+ `import { Op, phase, gate, shell, stepOutput } from "@intentius/chant/op";`,
65
+ ``,
66
+ `const plan = shell("yodel apply ${env} --digest", { id: "plan" });`,
67
+ `const digest = stepOutput(plan, "stdout");`,
68
+ ``,
69
+ `export const op = Op({`,
70
+ ` name: "migrate-${env}",`,
71
+ ` overview: "Apply the pending migrations to ${env}, behind an approval bound to their plan digest",`,
72
+ ` phases: [`,
73
+ ` phase("Plan", [plan]),`,
74
+ ` phase("Approve", [gate("approve-migrate-${env}", { plan: digest })]),`,
75
+ ` phase("Apply", [shell("yodel apply ${env} --execute", { env: { ${APPROVED_PLAN_ENV}: digest }, timeout: "2h" })]),`,
76
+ ` ],`,
77
+ `});`,
78
+ ].join("\n");
79
+ }
80
+ /** Picks the migrations Op for `env` from discovered Ops. Pure. */
81
+ export function selectMigrateOp(ops, project, env, name) {
82
+ const found = [...ops.values()].flatMap((o) => {
83
+ const parts = migrateOpParts(o.config, env.name);
84
+ return parts ? [{ ...o, parts }] : [];
85
+ });
86
+ const chosen = name ? found.filter((o) => o.config.name === name) : found;
87
+ if (chosen.length === 0) {
88
+ if (name && ops.has(name))
89
+ throw new YodelError(`Op "${name}" is not a migrations Op for environment ${env.name} (a Plan step running "yodel apply ${env.name} --digest", a gate bound to it, an Apply step running "yodel apply ${env.name} --execute")`);
90
+ throw new YodelError(name
91
+ ? `no Op named "${name}" in ${project.dir}`
92
+ : `no Op applies the migrations to environment ${env.name} in ${project.dir}; declare one, for example:\n\n${migrateOpExample(env.name)}`);
93
+ }
94
+ if (chosen.length > 1)
95
+ throw new YodelError(`${String(chosen.length)} Ops apply the migrations to ${env.name} (${chosen.map((o) => o.config.name).join(", ")}); pick one with --op <name>`);
96
+ const [op] = chosen;
97
+ if (!op.parts.gate) {
98
+ throw new YodelError(`Op "${op.config.name}" (${op.filePath}) has no approval gate bound to its Plan step's digest, and yodel only applies behind one; give it gate(..., { plan: stepOutput(plan, "stdout") })`);
99
+ }
100
+ return { name: op.config.name, filePath: op.filePath, gate: op.parts.gate, quorum: op.parts.quorum };
101
+ }
102
+ export async function findMigrateOp(project, env, name) {
103
+ const { discoverOps } = await import("@intentius/chant/op/discover");
104
+ let found;
105
+ try {
106
+ found = await discoverOps({ cwd: project.dir });
107
+ }
108
+ catch (e) {
109
+ throw discoveryError(project.dir, e);
110
+ }
111
+ if (found.errors.length > 0 && found.ops.size === 0)
112
+ throw new YodelError(`could not discover Ops in ${project.dir}:\n${found.errors.join("\n")}`);
113
+ return selectMigrateOp(found.ops, project, env, name);
114
+ }
115
+ /** This yodel's own entry point: `bin.js` when built, `bin.ts` from source. */
116
+ function selfEntry() {
117
+ const here = fileURLToPath(import.meta.url);
118
+ return fileURLToPath(new URL(`../bin${extname(here)}`, import.meta.url));
119
+ }
120
+ /** A directory holding a `yodel` that runs this yodel, for a child's PATH. */
121
+ export function yodelShim() {
122
+ const dir = mkdtempSync(join(tmpdir(), "yodel-shim-"));
123
+ const q = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
124
+ const script = `#!/bin/sh\nexec ${q(process.execPath)} --import ${q(chantInstall().loader)} ${q(selfEntry())} "$@"\n`;
125
+ const path = join(dir, "yodel");
126
+ writeFileSync(path, script);
127
+ chmodSync(path, 0o755);
128
+ return { dir, dispose: () => { rmSync(dir, { recursive: true, force: true }); } };
129
+ }
130
+ /**
131
+ * Where `yodel apply --execute` (the Op's Apply step) writes its exit code,
132
+ * so the outer `yodel apply` exits with it (5 for the lock, 4 for a refusal)
133
+ * rather than with `chant run`'s 1.
134
+ */
135
+ export const STEP_EXIT_ENV = "YODEL_STEP_EXIT_FILE";
136
+ /** Records an Op step's exit code where the outer `yodel apply` reads it. Never fails the step. */
137
+ export function recordStepExit(code, env = process.env) {
138
+ const file = env[STEP_EXIT_ENV];
139
+ if (!file)
140
+ return;
141
+ try {
142
+ writeFileSync(file, `${String(code)}\n`);
143
+ }
144
+ catch {
145
+ // The outer run then reports chant run's own exit.
146
+ }
147
+ }
148
+ /** The exit code the Op's Apply step recorded, when it ran. */
149
+ export function readStepExit(file) {
150
+ try {
151
+ const n = Number(readFileSync(file, "utf8").trim());
152
+ return Number.isInteger(n) ? n : undefined;
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ }
158
+ /** Runs the migrations Op with `chant run`, with this yodel on PATH and `--allow-out-of-order` passed through. */
159
+ export async function runMigrateOp(project, op, options) {
160
+ const shim = yodelShim();
161
+ try {
162
+ const base = options.env ?? process.env;
163
+ // The approved digest only ever comes from the gate, never from the caller's environment.
164
+ const rest = Object.fromEntries(Object.entries(base).filter(([k]) => k !== APPROVED_PLAN_ENV && k !== ALLOW_OUT_OF_ORDER_ENV));
165
+ const exitFile = join(shim.dir, "step-exit");
166
+ const env = {
167
+ ...rest,
168
+ [STEP_EXIT_ENV]: exitFile,
169
+ PATH: [shim.dir, base.PATH ?? ""].filter(Boolean).join(delimiter),
170
+ ...(options.allowOutOfOrder ? { [ALLOW_OUT_OF_ORDER_ENV]: "1" } : {}),
171
+ };
172
+ const result = await runChant(["run", op.name, "--gated-exit", String(GATED)], {
173
+ cwd: project.dir,
174
+ env,
175
+ ...(options.onOutput ? { onStdout: options.onOutput, onStderr: options.onOutput } : {}),
176
+ });
177
+ const run = readApplyRun(result.code, `${result.stdout}\n${result.stderr}`);
178
+ const stepExit = readStepExit(exitFile);
179
+ return stepExit === undefined ? run : { ...run, stepExit };
180
+ }
181
+ finally {
182
+ shim.dispose();
183
+ }
184
+ }
185
+ //# sourceMappingURL=op.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"op.js","sourceRoot":"","sources":["../../src/versioned/op.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Policy over the plan (#53): the rules `yodel.config.ts` `policy` declares
3
+ * (`../config/define.ts`), evaluated over the plan `yodel apply` would run,
4
+ * and the overrides recorded against them.
5
+ *
6
+ * `yodel plan` reports each rule's outcome; `yodel apply` refuses (exit 4)
7
+ * before the gate, and its Apply step again under the lock, a plan a rule
8
+ * denies. An override is a person's decision to run that plan anyway:
9
+ * `yodel override <env> --rule <id> --reason <text>` records it on chant's
10
+ * gate ledger (the `chant/lifecycle` branch) as an approval of the gate
11
+ * `override-<id>` of the migrations Op, for exactly the current plan digest,
12
+ * with the reason as its note. It counts only when the plan digest is the
13
+ * same, so it is refused once the plan moves, and only when
14
+ * `policy.overriders` lists who recorded it. The Apply step writes the
15
+ * overrides it relied on into the history (the `note` of each migration's
16
+ * `started` row).
17
+ */
18
+ import type { GateLedgerPort } from "@intentius/chant/op/gate";
19
+ import type { PolicyConfig, PolicyPlan } from "../config/define.js";
20
+ import type { SqlDialect } from "../project.js";
21
+ import { YodelError } from "../project.js";
22
+ import type { MigrateOpRef } from "./op.js";
23
+ import type { PendingMigration } from "./state.js";
24
+ /** The gate an override of `rule` is recorded as an approval of. */
25
+ export declare const overrideGate: (rule: string) => string;
26
+ /** An override that counts: who recorded it, when, why, and the digest it is for. */
27
+ export interface PolicyOverride {
28
+ rule: string;
29
+ by: string;
30
+ at: string;
31
+ reason: string;
32
+ digest: string;
33
+ }
34
+ export interface RuleResult {
35
+ id: string;
36
+ description?: string | undefined;
37
+ /** `denied` stands; `overridden` was denied, and an override counts. */
38
+ outcome: "allowed" | "denied" | "overridden";
39
+ /** Why it denies the plan, one line each. */
40
+ reasons: string[];
41
+ override?: PolicyOverride | undefined;
42
+ }
43
+ /** The plan a rule sees. */
44
+ export declare function policyPlan(environment: string, dialect: SqlDialect, toApply: readonly PendingMigration[], now?: Date): PolicyPlan;
45
+ /** Each rule that applies to the plan's environment, allowed or denied. No overrides yet. */
46
+ export declare function evaluatePolicy(policy: PolicyConfig | undefined, plan: PolicyPlan): RuleResult[];
47
+ /** The overrides on the ledger that count for `digest`: of a rule's override gate, for that digest, with a reason, by someone `overriders` lists. The newest per rule. */
48
+ export declare function readOverrides(port: GateLedgerPort, op: MigrateOpRef, digest: string, overriders: readonly string[] | undefined): Promise<PolicyOverride[]>;
49
+ /** The results with each denied rule an override counts for marked overridden. */
50
+ export declare function withOverrides(results: readonly RuleResult[], overrides: readonly PolicyOverride[]): RuleResult[];
51
+ /** The refusal for the rules that deny the plan with no override, or `undefined`. */
52
+ export declare function policyRefusal(results: readonly RuleResult[], environment: string, digest: string | undefined, when: string): YodelError | undefined;
53
+ /** The whole evaluation: the rules over the plan, with the overrides on the ledger for `digest`. Empty when the config declares no policy. */
54
+ export declare function judgePlan(input: {
55
+ policy: PolicyConfig | undefined;
56
+ environment: string;
57
+ dialect: SqlDialect;
58
+ toApply: readonly PendingMigration[];
59
+ digest: string | undefined;
60
+ ledger: () => Promise<{
61
+ port: GateLedgerPort;
62
+ op: MigrateOpRef;
63
+ } | undefined>;
64
+ now?: Date;
65
+ }): Promise<RuleResult[]>;
66
+ /** One line per rule, for text output. */
67
+ export declare function describeResults(results: readonly RuleResult[]): string[];
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Policy over the plan (#53): the rules `yodel.config.ts` `policy` declares
3
+ * (`../config/define.ts`), evaluated over the plan `yodel apply` would run,
4
+ * and the overrides recorded against them.
5
+ *
6
+ * `yodel plan` reports each rule's outcome; `yodel apply` refuses (exit 4)
7
+ * before the gate, and its Apply step again under the lock, a plan a rule
8
+ * denies. An override is a person's decision to run that plan anyway:
9
+ * `yodel override <env> --rule <id> --reason <text>` records it on chant's
10
+ * gate ledger (the `chant/lifecycle` branch) as an approval of the gate
11
+ * `override-<id>` of the migrations Op, for exactly the current plan digest,
12
+ * with the reason as its note. It counts only when the plan digest is the
13
+ * same, so it is refused once the plan moves, and only when
14
+ * `policy.overriders` lists who recorded it. The Apply step writes the
15
+ * overrides it relied on into the history (the `note` of each migration's
16
+ * `started` row).
17
+ */
18
+ import { YodelError } from "../project.js";
19
+ import { EXIT_REFUSED } from "../exit.js";
20
+ import { silencesOf } from "../lint/index.js";
21
+ import { sameDigest } from "./digest.js";
22
+ /** The gate an override of `rule` is recorded as an approval of. */
23
+ export const overrideGate = (rule) => `override-${rule}`;
24
+ /** The plan a rule sees. */
25
+ export function policyPlan(environment, dialect, toApply, now = new Date()) {
26
+ return {
27
+ environment,
28
+ dialect,
29
+ now: now.toISOString(),
30
+ pending: toApply.map((p) => ({
31
+ id: p.id,
32
+ checksum: p.checksum,
33
+ outOfOrder: p.outOfOrder,
34
+ steps: p.migration.document.steps.map((s, index) => {
35
+ if (s.kind === "statement") {
36
+ return { index, kind: "statement", object: s.object, name: s.name, classes: [s.class], destructive: s.destructive === true, rules: [s.rule], sql: s.sql };
37
+ }
38
+ return {
39
+ index,
40
+ kind: s.kind,
41
+ object: s.object,
42
+ name: s.name,
43
+ classes: [...new Set(s.changes.map((c) => c.class))],
44
+ destructive: s.changes.some((c) => c.destructive === true),
45
+ rules: [...new Set(s.changes.map((c) => c.rule))],
46
+ ...(s.kind === "op" ? { op: s.op } : {}),
47
+ };
48
+ }),
49
+ silences: silencesOf(p.migration).map(({ step, rule, reason }) => ({ step, rule, reason })),
50
+ })),
51
+ };
52
+ }
53
+ const applies = (rule, environment) => rule.environments === undefined || rule.environments.includes(environment);
54
+ function denials(rule, plan) {
55
+ const out = [];
56
+ if (rule.refuse) {
57
+ const { classes = [], destructive = false } = rule.refuse;
58
+ for (const m of plan.pending) {
59
+ for (const s of m.steps) {
60
+ const cls = s.classes.filter((c) => classes.includes(c));
61
+ if (cls.length > 0)
62
+ out.push(`${m.id} step ${String(s.index)} (${s.name}) is ${cls.join(", ")}`);
63
+ else if (destructive && s.destructive)
64
+ out.push(`${m.id} step ${String(s.index)} (${s.name}) is destructive`);
65
+ }
66
+ }
67
+ }
68
+ if (rule.silences) {
69
+ const { rules, reason } = rule.silences;
70
+ for (const m of plan.pending) {
71
+ for (const s of m.silences) {
72
+ if (rules && !rules.includes(s.rule))
73
+ continue;
74
+ reason.lastIndex = 0;
75
+ if (!reason.test(s.reason))
76
+ out.push(`${m.id} step ${String(s.step)} silences ${s.rule} with a reason that does not match ${String(reason)}: ${JSON.stringify(s.reason)}`);
77
+ }
78
+ }
79
+ }
80
+ if (rule.check) {
81
+ let said;
82
+ try {
83
+ said = rule.check(plan);
84
+ }
85
+ catch (e) {
86
+ said = `its check threw: ${e instanceof Error ? e.message : String(e)}`;
87
+ }
88
+ if (said === false)
89
+ out.push(rule.description ?? "its check denies the plan");
90
+ else if (typeof said === "string")
91
+ out.push(said);
92
+ else if (Array.isArray(said))
93
+ out.push(...said);
94
+ }
95
+ return out;
96
+ }
97
+ /** Each rule that applies to the plan's environment, allowed or denied. No overrides yet. */
98
+ export function evaluatePolicy(policy, plan) {
99
+ return (policy?.rules ?? [])
100
+ .filter((r) => applies(r, plan.environment))
101
+ .map((r) => {
102
+ const reasons = denials(r, plan);
103
+ return { id: r.id, ...(r.description ? { description: r.description } : {}), outcome: reasons.length > 0 ? "denied" : "allowed", reasons };
104
+ });
105
+ }
106
+ /** The overrides on the ledger that count for `digest`: of a rule's override gate, for that digest, with a reason, by someone `overriders` lists. The newest per rule. */
107
+ export async function readOverrides(port, op, digest, overriders) {
108
+ const { resolutions } = await port.read(op.name);
109
+ const byRule = new Map();
110
+ for (const r of resolutions) {
111
+ if (!r.gate.startsWith("override-") || !r.note || !(overriders ?? []).includes(r.resolvedBy))
112
+ continue;
113
+ if (!(await sameDigest(r.planDigest, digest)))
114
+ continue;
115
+ const rule = r.gate.slice("override-".length);
116
+ const prior = byRule.get(rule);
117
+ if (!prior || prior.at < r.timestamp)
118
+ byRule.set(rule, { rule, by: r.resolvedBy, at: r.timestamp, reason: r.note, digest });
119
+ }
120
+ return [...byRule.values()];
121
+ }
122
+ /** The results with each denied rule an override counts for marked overridden. */
123
+ export function withOverrides(results, overrides) {
124
+ return results.map((r) => {
125
+ const o = r.outcome === "denied" ? overrides.find((x) => x.rule === r.id) : undefined;
126
+ return o ? { ...r, outcome: "overridden", override: o } : r;
127
+ });
128
+ }
129
+ /** The refusal for the rules that deny the plan with no override, or `undefined`. */
130
+ export function policyRefusal(results, environment, digest, when) {
131
+ const denied = results.filter((r) => r.outcome === "denied");
132
+ if (denied.length === 0)
133
+ return undefined;
134
+ const lines = denied.flatMap((r) => [` ${r.id}${r.description ? ` (${r.description})` : ""}:`, ...r.reasons.map((x) => ` ${x}`)]);
135
+ const ids = denied.map((r) => r.id).join(", ");
136
+ return new YodelError(`refused by policy ${when}: ${denied.length === 1 ? "rule" : "rules"} ${ids} ${denied.length === 1 ? "denies" : "deny"} this plan${digest ? ` (${digest})` : ""}. Nothing was applied:\n${lines.join("\n")}\n` +
137
+ `To run it anyway, someone policy.overriders lists records why, for exactly this plan: ${denied.map((r) => `yodel override ${environment} --rule ${r.id} --reason "<why>"`).join("; ")}`, EXIT_REFUSED);
138
+ }
139
+ /** The whole evaluation: the rules over the plan, with the overrides on the ledger for `digest`. Empty when the config declares no policy. */
140
+ export async function judgePlan(input) {
141
+ if (!input.policy || input.toApply.length === 0)
142
+ return [];
143
+ const results = evaluatePolicy(input.policy, policyPlan(input.environment, input.dialect, input.toApply, input.now));
144
+ if (!input.digest || !results.some((r) => r.outcome === "denied"))
145
+ return results;
146
+ const at = await input.ledger();
147
+ if (!at)
148
+ return results;
149
+ return withOverrides(results, await readOverrides(at.port, at.op, input.digest, input.policy.overriders));
150
+ }
151
+ /** One line per rule, for text output. */
152
+ export function describeResults(results) {
153
+ return results.map((r) => {
154
+ if (r.outcome === "allowed")
155
+ return `policy ${r.id}: allowed`;
156
+ if (r.outcome === "overridden" && r.override)
157
+ return `policy ${r.id}: denied, overridden by ${r.override.by} at ${r.override.at}: ${r.override.reason}`;
158
+ return `policy ${r.id}: denied: ${r.reasons.join("; ")}`;
159
+ });
160
+ }
161
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../src/versioned/policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The live-schema fingerprint a Postgres plan digest covers: every object
3
+ * chant's catalog reader sees in the environment's schemas
4
+ * (`sql.profiles.<env>.schemas`; every schema but the server's own when it
5
+ * names none), by its type, schema, name, the statements that create it as
6
+ * the server prints them, and its comment. The history schema is left out,
7
+ * so recording a run does not move the digest, and so are chant's own
8
+ * working objects (a migration's new column, its receipts), which the reader
9
+ * skips.
10
+ *
11
+ * It is the same reader `chant sql plan` and the applier diff against, so a
12
+ * change the plan would see (a column added out of band, an index dropped, a
13
+ * default changed) is a new fingerprint, and an approval given before it
14
+ * does not carry over.
15
+ */
16
+ import type { PgClient } from "../../history/postgres/index.js";
17
+ export interface PgLiveScope {
18
+ /** The schemas in scope; every schema but the server's own when undefined. */
19
+ schemas: string[] | undefined;
20
+ /** Left out of the fingerprint. */
21
+ historySchema: string;
22
+ }
23
+ export interface PgCatalogEntry {
24
+ type: string;
25
+ schema: string;
26
+ name: string;
27
+ statement: string;
28
+ comment: string;
29
+ }
30
+ /** Reads what the fingerprint covers, in a stable order. */
31
+ export declare function readPgCatalog(client: PgClient, scope: PgLiveScope): Promise<PgCatalogEntry[]>;
32
+ /** `sha256:<hex>` over the catalog's canonical JSON. */
33
+ export declare function pgCatalogFingerprint(entries: readonly PgCatalogEntry[]): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The live-schema fingerprint a Postgres plan digest covers: every object
3
+ * chant's catalog reader sees in the environment's schemas
4
+ * (`sql.profiles.<env>.schemas`; every schema but the server's own when it
5
+ * names none), by its type, schema, name, the statements that create it as
6
+ * the server prints them, and its comment. The history schema is left out,
7
+ * so recording a run does not move the digest, and so are chant's own
8
+ * working objects (a migration's new column, its receipts), which the reader
9
+ * skips.
10
+ *
11
+ * It is the same reader `chant sql plan` and the applier diff against, so a
12
+ * change the plan would see (a column added out of band, an index dropped, a
13
+ * default changed) is a new fingerprint, and an approval given before it
14
+ * does not carry over.
15
+ */
16
+ import { createHash } from "node:crypto";
17
+ /** Byte order, so the order never depends on the machine's locale. */
18
+ const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
19
+ /** Reads what the fingerprint covers, in a stable order. */
20
+ export async function readPgCatalog(client, scope) {
21
+ const { readLiveSchema } = await import("@intentius/chant-lexicon-sql/postgres/live/catalog");
22
+ const schemas = scope.schemas?.filter((s) => s !== scope.historySchema);
23
+ const objects = await readLiveSchema(client, schemas ? { schemas } : {});
24
+ return objects
25
+ .filter((o) => o.schema !== scope.historySchema && !(o.type === "Postgres::Schema" && o.name === scope.historySchema))
26
+ .map((o) => ({ type: o.type, schema: o.schema ?? "", name: o.name, statement: o.statement, comment: o.comment ?? "" }))
27
+ .sort((a, b) => cmp(a.type, b.type) || cmp(a.schema, b.schema) || cmp(a.name, b.name));
28
+ }
29
+ /** `sha256:<hex>` over the catalog's canonical JSON. */
30
+ export function pgCatalogFingerprint(entries) {
31
+ const canonical = entries.map((e) => [e.type, e.schema, e.name, e.statement, e.comment]);
32
+ return `sha256:${createHash("sha256").update(JSON.stringify(canonical), "utf8").digest("hex")}`;
33
+ }
34
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../../src/versioned/postgres/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAkBzC,sEAAsE;AACtE,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE3E,4DAA4D;AAC5D,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAgB,EAAE,KAAkB;IACtE,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,oDAAoD,CAAC,CAAC;IAC9F,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC,aAAa,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,MAA8C,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACjH,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,kBAAkB,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,aAAa,CAAC,CAAC;SACrH,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC;SACtH,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3F,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,oBAAoB,CAAC,OAAkC;IACrE,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACzF,OAAO,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AAClG,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The versioned apply on Postgres (#13): binding an environment's server,
3
+ * its history, its lock and its statements.
4
+ *
5
+ * Settings:
6
+ *
7
+ * the environment's history database (`yodel.config.ts`
8
+ * `environments.<env>.history.database`, else YODEL_HISTORY_DATABASE,
9
+ * default `yodeler`) is the history's schema: `<schema>.history`, and the lock's key
10
+ * YODEL_LOCK_RETRIES how many more times a statement that hit the lock timeout is tried (default 5)
11
+ *
12
+ * and from `sql.profiles.<env>`, as chant's applier reads them:
13
+ *
14
+ * lockTimeoutMs lock_timeout for every statement, and for the live-schema read (default 5000)
15
+ * statementTimeoutMs statement_timeout for a catalog change (default 60000)
16
+ * scanTimeoutMs statement_timeout for a statement that reads or rewrites rows (default none)
17
+ * schemas the schemas the plan digest's fingerprint covers (default every schema but the server's own)
18
+ *
19
+ * Two connections: the work connection, opened on first use, on which the
20
+ * history is read and written and the statements run; and the lock's,
21
+ * opened when the lock is taken and closed when it is released.
22
+ */
23
+ import type { PostgresEndpoint } from "@intentius/chant-lexicon-sql/postgres/live/client";
24
+ import { type Environment, type Project } from "../../project.js";
25
+ import { PostgresHistoryStore, type PgLockConnection } from "../../history/postgres/index.js";
26
+ import type { VersionedDriver } from "../driver.js";
27
+ import { type PgTimeouts } from "./executor.js";
28
+ /** Opens a connection to the server: chant's `connectPostgres`, or a fake in the tests. */
29
+ export type PgConnect = (endpoint: PostgresEndpoint, applicationName: string) => Promise<PgLockConnection>;
30
+ export interface PgSettings {
31
+ historySchema: string;
32
+ timeouts: PgTimeouts;
33
+ lockRetries: number;
34
+ }
35
+ export declare function pgSettings(target: {
36
+ timeouts?: Partial<PgTimeouts>;
37
+ }, env?: NodeJS.ProcessEnv, historySchema?: string): PgSettings;
38
+ /** A connection opened on its first query, so a verb that reads nothing opens none. */
39
+ export declare class LazyConnection implements PgLockConnection {
40
+ private readonly open;
41
+ private conn;
42
+ constructor(open: () => Promise<PgLockConnection>);
43
+ query<T = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<T[]>;
44
+ end(): Promise<void>;
45
+ }
46
+ /** Everything the versioned verbs need about one Postgres environment. */
47
+ export interface PostgresEnv {
48
+ endpoint: PostgresEndpoint;
49
+ /** `sql.profiles.<env>.schemas`. */
50
+ schemas: string[] | undefined;
51
+ defaultSchema: string;
52
+ settings: PgSettings;
53
+ client: LazyConnection;
54
+ store: PostgresHistoryStore;
55
+ driver: VersionedDriver;
56
+ }
57
+ /** Binds `env`'s Postgres server (`sql.profiles.<env>`) and its history. Connects on first use. */
58
+ export declare function openPostgres(project: Project, env: Environment, processEnv?: NodeJS.ProcessEnv, historySchema?: string, connect?: PgConnect): Promise<PostgresEnv>;
59
+ export interface PostgresDriverInput {
60
+ client: LazyConnection;
61
+ /** Opens the lock's own connection. */
62
+ connect: () => Promise<PgLockConnection>;
63
+ settings: PgSettings;
64
+ schemas: string[] | undefined;
65
+ store: PostgresHistoryStore;
66
+ sleep?: (ms: number) => Promise<void>;
67
+ retryBaseMs?: number;
68
+ /** Where a read held up by another session's lock says so; default stderr. */
69
+ log?: (line: string) => void;
70
+ }
71
+ export declare function postgresDriver(input: PostgresDriverInput): VersionedDriver;