@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,58 @@
1
+ /**
2
+ * The history's DDL, per topology. Written out here rather than rendered by
3
+ * chant's topology renderer, so the history does not wait on a chant release
4
+ * and the four forms are plain to read and test:
5
+ *
6
+ * | topology | database | history table |
7
+ * |------------|--------------------------------------------|-------------------------------------------------|
8
+ * | single | `CREATE DATABASE` | `MergeTree` |
9
+ * | cluster | `CREATE DATABASE ... ON CLUSTER c` | `ReplicatedMergeTree('<path>', '<replica>')` `ON CLUSTER c` |
10
+ * | replicated | `ENGINE = Replicated(...)`, `ON CLUSTER` when the topology names one | `ReplicatedMergeTree` with no arguments (the database supplies them) |
11
+ * | cloud | `CREATE DATABASE` | `MergeTree` (Cloud makes it `SharedMergeTree`) |
12
+ *
13
+ * The lock table is a `KeeperMap` (see `./lock.ts`); on a cluster it is
14
+ * created `ON CLUSTER`, every host's table naming the same Keeper path, so
15
+ * there is one lock for the cluster.
16
+ */
17
+ import { type HistoryTopology } from "./topology.js";
18
+ /** The database the history lives in when the environment names none. */
19
+ export declare const DEFAULT_HISTORY_DATABASE = "yodeler";
20
+ export declare const HISTORY_TABLE = "history";
21
+ export declare const LOCK_TABLE = "lock";
22
+ /** A ClickHouse identifier, back-quoted. */
23
+ export declare const ident: (name: string) => string;
24
+ /** A ClickHouse string literal. */
25
+ export declare const literal: (value: string) => string;
26
+ /**
27
+ * The history table's columns. One row per event, never updated or deleted:
28
+ *
29
+ * - a migration row (`kind = 'migration'`, `statement_index = -1`) when a
30
+ * migration starts, succeeds or fails;
31
+ * - a statement row (`kind = 'statement'`, `statement_index` its index in
32
+ * the migration's `steps`) when a statement starts, succeeds or fails;
33
+ * - a step row (`kind = 'step'`, the same) when an Op step (a rebuild, a
34
+ * backfill) starts, succeeds or fails, its `note` the step's summary as
35
+ * JSON (the receipts it skipped and the work it ran).
36
+ *
37
+ * The state of a migration or a statement is its row with the highest
38
+ * `seq`. `seq` is taken under the lock, one more than the highest in the
39
+ * table, so it orders rows across runs without trusting anyone's clock;
40
+ * `recorded_at` is the server's time, for people.
41
+ */
42
+ export declare const HISTORY_COLUMNS: readonly (readonly [name: string, type: string])[];
43
+ export interface HistoryDdlOptions {
44
+ database: string;
45
+ topology: HistoryTopology;
46
+ }
47
+ /** `CREATE DATABASE IF NOT EXISTS` for the history's database. */
48
+ export declare function databaseDdl({ database, topology }: HistoryDdlOptions): string;
49
+ /** The history table's engine clause for a topology. */
50
+ export declare function historyEngine({ database, topology }: HistoryDdlOptions): string;
51
+ /** `CREATE TABLE IF NOT EXISTS <database>.history`. */
52
+ export declare function historyTableDdl(options: HistoryDdlOptions): string;
53
+ /** The Keeper path of an environment's lock, under the server's `keeper_map_path_prefix`. */
54
+ export declare const lockKeeperPath: (database: string) => string;
55
+ /** `CREATE TABLE IF NOT EXISTS <database>.lock`, a KeeperMap. */
56
+ export declare function lockTableDdl(options: HistoryDdlOptions): string;
57
+ /** Every statement that makes the history ready, in order: the database, then the table. */
58
+ export declare const historyDdl: (options: HistoryDdlOptions) => string[];
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The history's DDL, per topology. Written out here rather than rendered by
3
+ * chant's topology renderer, so the history does not wait on a chant release
4
+ * and the four forms are plain to read and test:
5
+ *
6
+ * | topology | database | history table |
7
+ * |------------|--------------------------------------------|-------------------------------------------------|
8
+ * | single | `CREATE DATABASE` | `MergeTree` |
9
+ * | cluster | `CREATE DATABASE ... ON CLUSTER c` | `ReplicatedMergeTree('<path>', '<replica>')` `ON CLUSTER c` |
10
+ * | replicated | `ENGINE = Replicated(...)`, `ON CLUSTER` when the topology names one | `ReplicatedMergeTree` with no arguments (the database supplies them) |
11
+ * | cloud | `CREATE DATABASE` | `MergeTree` (Cloud makes it `SharedMergeTree`) |
12
+ *
13
+ * The lock table is a `KeeperMap` (see `./lock.ts`); on a cluster it is
14
+ * created `ON CLUSTER`, every host's table naming the same Keeper path, so
15
+ * there is one lock for the cluster.
16
+ */
17
+ import { defaultReplicaPath, DEFAULT_HISTORY_REPLICA_NAME } from "./topology.js";
18
+ /** The database the history lives in when the environment names none. */
19
+ export const DEFAULT_HISTORY_DATABASE = "yodeler";
20
+ export const HISTORY_TABLE = "history";
21
+ export const LOCK_TABLE = "lock";
22
+ /** A ClickHouse identifier, back-quoted. */
23
+ export const ident = (name) => `\`${name.replace(/\\/g, "\\\\").replace(/`/g, "\\`")}\``;
24
+ /** A ClickHouse string literal. */
25
+ export const literal = (value) => `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
26
+ const onCluster = (t) => (t.kind === "cluster" ? ` ON CLUSTER ${ident(t.cluster)}` : "");
27
+ /**
28
+ * The history table's columns. One row per event, never updated or deleted:
29
+ *
30
+ * - a migration row (`kind = 'migration'`, `statement_index = -1`) when a
31
+ * migration starts, succeeds or fails;
32
+ * - a statement row (`kind = 'statement'`, `statement_index` its index in
33
+ * the migration's `steps`) when a statement starts, succeeds or fails;
34
+ * - a step row (`kind = 'step'`, the same) when an Op step (a rebuild, a
35
+ * backfill) starts, succeeds or fails, its `note` the step's summary as
36
+ * JSON (the receipts it skipped and the work it ran).
37
+ *
38
+ * The state of a migration or a statement is its row with the highest
39
+ * `seq`. `seq` is taken under the lock, one more than the highest in the
40
+ * table, so it orders rows across runs without trusting anyone's clock;
41
+ * `recorded_at` is the server's time, for people.
42
+ */
43
+ export const HISTORY_COLUMNS = [
44
+ ["seq", "UInt64"],
45
+ ["run_id", "String"],
46
+ ["kind", "LowCardinality(String)"],
47
+ ["migration_id", "String"],
48
+ ["parent", "String"],
49
+ ["checksum", "String"],
50
+ ["statement_index", "Int32"],
51
+ ["statement_sha", "String"],
52
+ ["status", "LowCardinality(String)"],
53
+ ["error", "String"],
54
+ ["applied_by", "String"],
55
+ ["plan_digest", "String"],
56
+ ["silences", "String DEFAULT '[]'"],
57
+ ["note", "String"],
58
+ ["recorded_at", "DateTime64(6, 'UTC') DEFAULT now64(6)"],
59
+ ];
60
+ /** `CREATE DATABASE IF NOT EXISTS` for the history's database. */
61
+ export function databaseDdl({ database, topology }) {
62
+ if (topology.kind === "replicated") {
63
+ const cluster = topology.cluster ? ` ON CLUSTER ${ident(topology.cluster)}` : "";
64
+ return `CREATE DATABASE IF NOT EXISTS ${ident(database)}${cluster} ENGINE = Replicated(${literal(`/clickhouse/databases/${database}`)}, '{shard}', '{replica}')`;
65
+ }
66
+ return `CREATE DATABASE IF NOT EXISTS ${ident(database)}${onCluster(topology)}`;
67
+ }
68
+ /** The history table's engine clause for a topology. */
69
+ export function historyEngine({ database, topology }) {
70
+ switch (topology.kind) {
71
+ case "cluster": {
72
+ const path = topology.replicaPath ?? defaultReplicaPath(database, HISTORY_TABLE);
73
+ const replica = topology.replicaName ?? DEFAULT_HISTORY_REPLICA_NAME;
74
+ return `ReplicatedMergeTree(${literal(path)}, ${literal(replica)})`;
75
+ }
76
+ case "replicated":
77
+ return "ReplicatedMergeTree";
78
+ case "single":
79
+ case "cloud":
80
+ return "MergeTree";
81
+ }
82
+ }
83
+ /** `CREATE TABLE IF NOT EXISTS <database>.history`. */
84
+ export function historyTableDdl(options) {
85
+ const columns = HISTORY_COLUMNS.map(([name, type]) => ` ${name} ${type}`).join(",\n");
86
+ return (`CREATE TABLE IF NOT EXISTS ${ident(options.database)}.${HISTORY_TABLE}${onCluster(options.topology)}\n(\n${columns}\n)\n` +
87
+ `ENGINE = ${historyEngine(options)}\nORDER BY (migration_id, statement_index, seq)\n` +
88
+ `COMMENT 'yodel migration history: append-only, one row per migration and per statement event'`);
89
+ }
90
+ /** The Keeper path of an environment's lock, under the server's `keeper_map_path_prefix`. */
91
+ export const lockKeeperPath = (database) => `/yodeler/${database}/lock`;
92
+ /** `CREATE TABLE IF NOT EXISTS <database>.lock`, a KeeperMap. */
93
+ export function lockTableDdl(options) {
94
+ return (`CREATE TABLE IF NOT EXISTS ${ident(options.database)}.${LOCK_TABLE}${onCluster(options.topology)}\n` +
95
+ `(\n name String,\n owner String,\n acquired_at DateTime64(3, 'UTC'),\n expires_at DateTime64(3, 'UTC')\n)\n` +
96
+ `ENGINE = KeeperMap(${literal(lockKeeperPath(options.database))})\nPRIMARY KEY name`);
97
+ }
98
+ /** Every statement that makes the history ready, in order: the database, then the table. */
99
+ export const historyDdl = (options) => [databaseDdl(options), historyTableDdl(options)];
100
+ //# sourceMappingURL=ddl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ddl.js","sourceRoot":"","sources":["../../src/history/ddl.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The migration history (#8): an append-only ClickHouse table in the
3
+ * database the environment names (default `yodeler`), and the apply lock.
4
+ *
5
+ * historyDdl({ database, topology }) CREATE DATABASE / TABLE for single, cluster, replicated, cloud
6
+ * lockTableDdl({ database, topology }) the KeeperMap lock table
7
+ * new HistoryStore(port, location) exists(), ensure(), rows(), state(), append(rows)
8
+ * new HistoryRecorder(store, run, maxSeq) record(row): appends with the next seq
9
+ * deriveState(rows) the latest row per migration and per statement
10
+ * isApplied(record), statementDone(record, i)
11
+ * acquireLock(port, options), withLock(lock, ttl, body)
12
+ * parseHistoryTopology("cluster:main")
13
+ *
14
+ * Rows are never updated or deleted; a state is always the row with the
15
+ * highest seq for its key. #10 (`init --from` records a baseline), #12 (Op
16
+ * steps record their receipts' progress), #13 (the Postgres twin), #15
17
+ * (`repair` appends a migration row with the new checksum and a `note`) and
18
+ * #16 (the digest) build on this.
19
+ */
20
+ export * from "./client.js";
21
+ export * from "./ddl.js";
22
+ export * from "./lock.js";
23
+ export * from "./rows.js";
24
+ export * from "./store.js";
25
+ export * from "./topology.js";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The migration history (#8): an append-only ClickHouse table in the
3
+ * database the environment names (default `yodeler`), and the apply lock.
4
+ *
5
+ * historyDdl({ database, topology }) CREATE DATABASE / TABLE for single, cluster, replicated, cloud
6
+ * lockTableDdl({ database, topology }) the KeeperMap lock table
7
+ * new HistoryStore(port, location) exists(), ensure(), rows(), state(), append(rows)
8
+ * new HistoryRecorder(store, run, maxSeq) record(row): appends with the next seq
9
+ * deriveState(rows) the latest row per migration and per statement
10
+ * isApplied(record), statementDone(record, i)
11
+ * acquireLock(port, options), withLock(lock, ttl, body)
12
+ * parseHistoryTopology("cluster:main")
13
+ *
14
+ * Rows are never updated or deleted; a state is always the row with the
15
+ * highest seq for its key. #10 (`init --from` records a baseline), #12 (Op
16
+ * steps record their receipts' progress), #13 (the Postgres twin), #15
17
+ * (`repair` appends a migration row with the new checksum and a `note`) and
18
+ * #16 (the digest) build on this.
19
+ */
20
+ export * from "./client.js";
21
+ export * from "./ddl.js";
22
+ export * from "./lock.js";
23
+ export * from "./rows.js";
24
+ export * from "./store.js";
25
+ export * from "./topology.js";
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * One apply at a time per environment.
3
+ *
4
+ * ## Keeper: a KeeperMap row
5
+ *
6
+ * The lock is a row in `<history database>.lock`, a `KeeperMap` table, so
7
+ * it lives in Keeper. Inserted with `keeper_map_strict_mode = 1`, an insert
8
+ * of a key that is there already fails ("Node exists") instead of
9
+ * overwriting it: create-if-absent, which is a lock. Every runner, on any
10
+ * host, sees the same row, because every host's `lock` table names the same
11
+ * Keeper path. It was picked over the other routes ClickHouse SQL offers:
12
+ *
13
+ * - an insert into a Replicated table that deduplicates: the losing insert is
14
+ * dropped silently, inside a time window, so the loser cannot tell it lost
15
+ * without reading back, and a later insert is not refused at all;
16
+ * - `INSERT INTO system.zookeeper`: no `DELETE`, so a lock could never be
17
+ * released or expired from SQL.
18
+ *
19
+ * The row names its owner and an expiry. A runner that holds it extends the
20
+ * expiry while it works (a heartbeat every third of the TTL) and deletes the
21
+ * row when done. A runner that finds an expired row deletes that row (by its
22
+ * owner, so it never deletes a newer one) and inserts its own; two runners
23
+ * racing for an expired lock both delete, and only one insert succeeds. A
24
+ * heartbeat that finds the row gone or someone else's stops the apply
25
+ * before its next statement.
26
+ *
27
+ * KeeperMap needs Keeper and the server setting `keeper_map_path_prefix`.
28
+ *
29
+ * ## No Keeper: a local lock file
30
+ *
31
+ * A single node with no Keeper (or no `keeper_map_path_prefix`) gets a lock
32
+ * file under the system temp directory instead, created exclusively, with
33
+ * the same owner and expiry. It serializes applies from one machine only, and
34
+ * the run says so: cross-runner locking needs Keeper. A cluster, a
35
+ * Replicated database or Cloud never fall back: they refuse without the
36
+ * KeeperMap lock.
37
+ *
38
+ * ## Clearing a lock by hand
39
+ *
40
+ * A dead runner's lock expires after its TTL (`YODEL_LOCK_TTL`, seconds,
41
+ * default 900). To clear it sooner, when you know no apply is running:
42
+ *
43
+ * Keeper: ALTER TABLE <history database>.lock DELETE WHERE name = 'apply'
44
+ * local: rm the lock file the refusal names
45
+ */
46
+ import { YodelError } from "../project.js";
47
+ import { type ClickHousePort } from "./client.js";
48
+ import { type HistoryTopology } from "./topology.js";
49
+ /** Exit code: another apply holds the lock. */
50
+ export declare const LOCKED = 5;
51
+ /** The lock row's key: one apply lock per history database. */
52
+ export declare const LOCK_NAME = "apply";
53
+ /** Default lock TTL. */
54
+ export declare const DEFAULT_LOCK_TTL_MS: number;
55
+ export interface Lock {
56
+ /** `advisory`: a Postgres session advisory lock (`./postgres/lock.ts`). */
57
+ kind: "keeper" | "local" | "advisory";
58
+ owner: string;
59
+ /** Where it is: the KeeperMap table, or the lock file. */
60
+ where: string;
61
+ /** Set when the lock is local: why, and that cross-runner locking needs Keeper. */
62
+ notice?: string;
63
+ /** Extends the expiry. Throws a {@link LockLostError} when the lock is no longer this owner's. */
64
+ heartbeat(): Promise<void>;
65
+ /** Deletes the lock, when it is still this owner's. */
66
+ release(): Promise<void>;
67
+ }
68
+ export declare class LockHeldError extends YodelError {
69
+ readonly holder: string;
70
+ readonly expiresAt: string;
71
+ readonly where: string;
72
+ readonly clear: string;
73
+ /** Set when the run waited for it (`--lock-wait`): how long, in words. */
74
+ readonly waited?: string | undefined;
75
+ constructor(holder: string, expiresAt: string, where: string, clear: string,
76
+ /** Set when the run waited for it (`--lock-wait`): how long, in words. */
77
+ waited?: string | undefined);
78
+ }
79
+ export declare class LockLostError extends YodelError {
80
+ constructor(where: string);
81
+ }
82
+ export interface LockOptions {
83
+ database: string;
84
+ topology: HistoryTopology;
85
+ /** Who holds it, for the refusal another runner sees. */
86
+ holder: string;
87
+ ttlMs?: number;
88
+ /** Identifies the server, for the local lock file's name. */
89
+ serverKey: string;
90
+ /** Where local lock files go. Default: `<tmpdir>/yodel-locks`. */
91
+ localDir?: string;
92
+ }
93
+ /** A lock owner: who, plus a random part so two runs by the same user never share one. */
94
+ export declare const lockOwner: (holder: string) => string;
95
+ /** Acquires the environment's apply lock, or throws {@link LockHeldError}. */
96
+ export declare function acquireLock(port: ClickHousePort, options: LockOptions): Promise<Lock>;
97
+ /** The local lock's file for a server and history database. */
98
+ export declare function localLockPath(options: Pick<LockOptions, "serverKey" | "database" | "localDir">): string;
99
+ /**
100
+ * Runs `body` holding the lock: a heartbeat every third of the TTL while it
101
+ * runs, the lock released after, whatever happens. `beat` lets the body
102
+ * extend the lock (and learn it was lost) between statements.
103
+ */
104
+ export declare function withLock<T>(lock: Lock, ttlMs: number, body: (beat: () => Promise<void>) => Promise<T>): Promise<T>;
@@ -0,0 +1,230 @@
1
+ /**
2
+ * One apply at a time per environment.
3
+ *
4
+ * ## Keeper: a KeeperMap row
5
+ *
6
+ * The lock is a row in `<history database>.lock`, a `KeeperMap` table, so
7
+ * it lives in Keeper. Inserted with `keeper_map_strict_mode = 1`, an insert
8
+ * of a key that is there already fails ("Node exists") instead of
9
+ * overwriting it: create-if-absent, which is a lock. Every runner, on any
10
+ * host, sees the same row, because every host's `lock` table names the same
11
+ * Keeper path. It was picked over the other routes ClickHouse SQL offers:
12
+ *
13
+ * - an insert into a Replicated table that deduplicates: the losing insert is
14
+ * dropped silently, inside a time window, so the loser cannot tell it lost
15
+ * without reading back, and a later insert is not refused at all;
16
+ * - `INSERT INTO system.zookeeper`: no `DELETE`, so a lock could never be
17
+ * released or expired from SQL.
18
+ *
19
+ * The row names its owner and an expiry. A runner that holds it extends the
20
+ * expiry while it works (a heartbeat every third of the TTL) and deletes the
21
+ * row when done. A runner that finds an expired row deletes that row (by its
22
+ * owner, so it never deletes a newer one) and inserts its own; two runners
23
+ * racing for an expired lock both delete, and only one insert succeeds. A
24
+ * heartbeat that finds the row gone or someone else's stops the apply
25
+ * before its next statement.
26
+ *
27
+ * KeeperMap needs Keeper and the server setting `keeper_map_path_prefix`.
28
+ *
29
+ * ## No Keeper: a local lock file
30
+ *
31
+ * A single node with no Keeper (or no `keeper_map_path_prefix`) gets a lock
32
+ * file under the system temp directory instead, created exclusively, with
33
+ * the same owner and expiry. It serializes applies from one machine only, and
34
+ * the run says so: cross-runner locking needs Keeper. A cluster, a
35
+ * Replicated database or Cloud never fall back: they refuse without the
36
+ * KeeperMap lock.
37
+ *
38
+ * ## Clearing a lock by hand
39
+ *
40
+ * A dead runner's lock expires after its TTL (`YODEL_LOCK_TTL`, seconds,
41
+ * default 900). To clear it sooner, when you know no apply is running:
42
+ *
43
+ * Keeper: ALTER TABLE <history database>.lock DELETE WHERE name = 'apply'
44
+ * local: rm the lock file the refusal names
45
+ */
46
+ import { createHash, randomUUID } from "node:crypto";
47
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
48
+ import { tmpdir } from "node:os";
49
+ import { join } from "node:path";
50
+ import { YodelError } from "../project.js";
51
+ import { serverMessage } from "./client.js";
52
+ import { databaseDdl, ident, literal, LOCK_TABLE, lockTableDdl } from "./ddl.js";
53
+ import { describeTopology } from "./topology.js";
54
+ /** Exit code: another apply holds the lock. */
55
+ export const LOCKED = 5;
56
+ /** The lock row's key: one apply lock per history database. */
57
+ export const LOCK_NAME = "apply";
58
+ /** Default lock TTL. */
59
+ export const DEFAULT_LOCK_TTL_MS = 15 * 60 * 1000;
60
+ export class LockHeldError extends YodelError {
61
+ holder;
62
+ expiresAt;
63
+ where;
64
+ clear;
65
+ waited;
66
+ constructor(holder, expiresAt, where, clear,
67
+ /** Set when the run waited for it (`--lock-wait`): how long, in words. */
68
+ waited) {
69
+ super(`another apply holds the lock (${where}): owner ${holder}, ${where.startsWith("Postgres advisory lock") ? "since" : "until"} ${expiresAt} UTC. ` +
70
+ (waited ? `This run ${waited}. ` : "") +
71
+ `Wait for it, or once you know it is not running, clear it with: ${clear}`, LOCKED);
72
+ this.holder = holder;
73
+ this.expiresAt = expiresAt;
74
+ this.where = where;
75
+ this.clear = clear;
76
+ this.waited = waited;
77
+ this.name = "LockHeldError";
78
+ }
79
+ }
80
+ export class LockLostError extends YodelError {
81
+ constructor(where) {
82
+ super(`the apply lock (${where}) is no longer held by this run (it expired and another run took it); stopped before the next statement`, LOCKED);
83
+ this.name = "LockLostError";
84
+ }
85
+ }
86
+ /** A lock owner: who, plus a random part so two runs by the same user never share one. */
87
+ export const lockOwner = (holder) => `${holder}#${String(process.pid)}-${randomUUID().slice(0, 8)}`;
88
+ const isNodeExists = (e) => /Node exists|already exists/i.test(serverMessage(e));
89
+ const keeperMapUnavailable = (e) => /keeper_map_path_prefix|ZooKeeper|Keeper is not|KEEPER_EXCEPTION.*(not configured|connection)|No such engine|UNKNOWN_STORAGE/i.test(serverMessage(e));
90
+ const STRICT = { keeper_map_strict_mode: "1", async_insert: "0" };
91
+ /** Acquires the environment's apply lock, or throws {@link LockHeldError}. */
92
+ export async function acquireLock(port, options) {
93
+ const ttlMs = options.ttlMs ?? DEFAULT_LOCK_TTL_MS;
94
+ await port.query(databaseDdl(options));
95
+ try {
96
+ await port.query(lockTableDdl(options));
97
+ }
98
+ catch (e) {
99
+ if (!keeperMapUnavailable(e))
100
+ throw e;
101
+ const why = (serverMessage(e).split("\n")[0] ?? "").replace(/^Code: \d+\. DB::Exception: /, "").replace(/\s*\([A-Z_]+\)\s*\(version [^)]*\)\s*$/, "");
102
+ if (options.topology.kind !== "single") {
103
+ throw new YodelError(`topology ${describeTopology(options.topology)} needs the Keeper lock (a KeeperMap table), and the server cannot make one: ${why}. ` +
104
+ `Configure Keeper and keeper_map_path_prefix on the server.`);
105
+ }
106
+ return localLock(options, ttlMs, `no KeeperMap on this server (${why}); the lock is a file on this machine, so cross-runner locking needs Keeper (and keeper_map_path_prefix)`);
107
+ }
108
+ return keeperLock(port, options, ttlMs);
109
+ }
110
+ async function keeperLock(port, options, ttlMs) {
111
+ const table = `${ident(options.database)}.${LOCK_TABLE}`;
112
+ const owner = lockOwner(options.holder);
113
+ const where = `KeeperMap ${options.database}.${LOCK_TABLE}`;
114
+ const clear = `ALTER TABLE ${table} DELETE WHERE name = '${LOCK_NAME}'`;
115
+ const until = `now64(3) + toIntervalMillisecond(${String(ttlMs)})`;
116
+ const read = async () => (await port.query(
117
+ // Not aliased as expires_at: an alias shadows the column in ClickHouse, and the comparison would read the string.
118
+ `SELECT owner, toString(expires_at) AS until, expires_at < now64(3) AS expired FROM ${table} WHERE name = ${literal(LOCK_NAME)}`))[0];
119
+ for (let attempt = 0;; attempt++) {
120
+ try {
121
+ await port.query(`INSERT INTO ${table} (name, owner, acquired_at, expires_at) SELECT ${literal(LOCK_NAME)}, ${literal(owner)}, now64(3), ${until}`, { settings: STRICT });
122
+ break;
123
+ }
124
+ catch (e) {
125
+ if (!isNodeExists(e))
126
+ throw e;
127
+ const held = await read();
128
+ if (held && Number(held.expired) !== 1)
129
+ throw new LockHeldError(held.owner, held.until, where, clear);
130
+ if (attempt >= 3)
131
+ throw new LockHeldError(held?.owner ?? "(unknown)", held?.until ?? "(unknown)", where, clear);
132
+ // Expired (or released between the insert and the read): delete that owner's row, never a newer one, and try again.
133
+ if (held)
134
+ await port.query(`ALTER TABLE ${table} DELETE WHERE name = ${literal(LOCK_NAME)} AND owner = ${literal(held.owner)}`, { settings: STRICT });
135
+ }
136
+ }
137
+ return {
138
+ kind: "keeper",
139
+ owner,
140
+ where,
141
+ async heartbeat() {
142
+ await port.query(`ALTER TABLE ${table} UPDATE expires_at = ${until} WHERE name = ${literal(LOCK_NAME)} AND owner = ${literal(owner)}`, { settings: STRICT });
143
+ const held = await read();
144
+ if (held?.owner !== owner)
145
+ throw new LockLostError(where);
146
+ },
147
+ async release() {
148
+ await port.query(`ALTER TABLE ${table} DELETE WHERE name = ${literal(LOCK_NAME)} AND owner = ${literal(owner)}`, { settings: STRICT });
149
+ },
150
+ };
151
+ }
152
+ /** The local lock's file for a server and history database. */
153
+ export function localLockPath(options) {
154
+ const key = createHash("sha256").update(`${options.serverKey}\n${options.database}`).digest("hex").slice(0, 16);
155
+ return join(options.localDir ?? join(tmpdir(), "yodel-locks"), `${options.database}-${key}.lock`);
156
+ }
157
+ function readLockFile(path) {
158
+ try {
159
+ return JSON.parse(readFileSync(path, "utf8"));
160
+ }
161
+ catch {
162
+ return undefined;
163
+ }
164
+ }
165
+ function localLock(options, ttlMs, notice) {
166
+ const path = localLockPath(options);
167
+ mkdirSync(join(path, ".."), { recursive: true });
168
+ const owner = lockOwner(options.holder);
169
+ const where = `local lock file ${path}`;
170
+ const body = () => JSON.stringify({ owner, acquiredAt: new Date().toISOString(), expiresAt: new Date(Date.now() + ttlMs).toISOString() });
171
+ for (let attempt = 0;; attempt++) {
172
+ try {
173
+ writeFileSync(path, body(), { flag: "wx" });
174
+ break;
175
+ }
176
+ catch (e) {
177
+ if (e.code !== "EEXIST")
178
+ throw e;
179
+ const held = readLockFile(path);
180
+ const expired = !held || Date.parse(held.expiresAt) < Date.now();
181
+ if (!expired || attempt >= 3)
182
+ throw new LockHeldError(held?.owner ?? "(unreadable)", held?.expiresAt ?? "(unknown)", where, `rm ${path}`);
183
+ rmSync(path, { force: true });
184
+ }
185
+ }
186
+ return {
187
+ kind: "local",
188
+ owner,
189
+ where,
190
+ notice,
191
+ heartbeat() {
192
+ if (readLockFile(path)?.owner !== owner)
193
+ return Promise.reject(new LockLostError(where));
194
+ writeFileSync(path, body());
195
+ return Promise.resolve();
196
+ },
197
+ release() {
198
+ if (readLockFile(path)?.owner === owner)
199
+ rmSync(path, { force: true });
200
+ return Promise.resolve();
201
+ },
202
+ };
203
+ }
204
+ /**
205
+ * Runs `body` holding the lock: a heartbeat every third of the TTL while it
206
+ * runs, the lock released after, whatever happens. `beat` lets the body
207
+ * extend the lock (and learn it was lost) between statements.
208
+ */
209
+ export async function withLock(lock, ttlMs, body) {
210
+ let lost;
211
+ const timer = setInterval(() => {
212
+ lock.heartbeat().catch((e) => {
213
+ lost ??= e;
214
+ });
215
+ }, Math.max(1000, Math.floor(ttlMs / 3)));
216
+ timer.unref();
217
+ const beat = async () => {
218
+ if (lost !== undefined)
219
+ throw lost instanceof Error ? lost : new LockLostError(lock.where);
220
+ await lock.heartbeat();
221
+ };
222
+ try {
223
+ return await body(beat);
224
+ }
225
+ finally {
226
+ clearInterval(timer);
227
+ await lock.release().catch(() => undefined);
228
+ }
229
+ }
230
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/history/lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The Postgres history's DDL: `<schema>.history` in a schema of its own
3
+ * (default `yodeler`, the environment's history database setting), the same
4
+ * columns as the ClickHouse table (`../ddl.ts`), so a row reads the same in
5
+ * both and `deriveState` adds them up the same way.
6
+ *
7
+ * `seq` is the primary key: two runs can never write the same one (a run
8
+ * takes it under the advisory lock, one more than the highest), and the key
9
+ * says so if one ever tried. Rows are only ever inserted.
10
+ */
11
+ /** A Postgres identifier, double-quoted. */
12
+ export declare const pgIdent: (name: string) => string;
13
+ /** A Postgres string literal (standard_conforming_strings, the default since 9.1). */
14
+ export declare const pgLiteral: (value: string) => string;
15
+ /** The history table's columns, in the order {@link HISTORY_COLUMNS} (ClickHouse) has them. */
16
+ export declare const PG_HISTORY_COLUMNS: readonly (readonly [name: string, type: string])[];
17
+ /** The columns a row write names: all but `recorded_at`, which is the server's. */
18
+ export declare const PG_WRITTEN_COLUMNS: string[];
19
+ /** `<schema>.history`, quoted. */
20
+ export declare const pgHistoryTable: (schema: string) => string;
21
+ /** Every statement that makes the history ready, in order: the schema, the table, its comment. */
22
+ export declare function pgHistoryDdl(schema: string): string[];
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The Postgres history's DDL: `<schema>.history` in a schema of its own
3
+ * (default `yodeler`, the environment's history database setting), the same
4
+ * columns as the ClickHouse table (`../ddl.ts`), so a row reads the same in
5
+ * both and `deriveState` adds them up the same way.
6
+ *
7
+ * `seq` is the primary key: two runs can never write the same one (a run
8
+ * takes it under the advisory lock, one more than the highest), and the key
9
+ * says so if one ever tried. Rows are only ever inserted.
10
+ */
11
+ import { HISTORY_TABLE } from "../ddl.js";
12
+ /** A Postgres identifier, double-quoted. */
13
+ export const pgIdent = (name) => `"${name.replace(/"/g, '""')}"`;
14
+ /** A Postgres string literal (standard_conforming_strings, the default since 9.1). */
15
+ export const pgLiteral = (value) => `'${value.replace(/'/g, "''")}'`;
16
+ /** The history table's columns, in the order {@link HISTORY_COLUMNS} (ClickHouse) has them. */
17
+ export const PG_HISTORY_COLUMNS = [
18
+ ["seq", "bigint NOT NULL PRIMARY KEY"],
19
+ ["run_id", "text NOT NULL"],
20
+ ["kind", "text NOT NULL CHECK (kind IN ('migration', 'statement', 'step'))"],
21
+ ["migration_id", "text NOT NULL"],
22
+ ["parent", "text NOT NULL DEFAULT ''"],
23
+ ["checksum", "text NOT NULL DEFAULT ''"],
24
+ ["statement_index", "integer NOT NULL"],
25
+ ["statement_sha", "text NOT NULL DEFAULT ''"],
26
+ ["status", "text NOT NULL CHECK (status IN ('started', 'succeeded', 'failed'))"],
27
+ ["error", "text NOT NULL DEFAULT ''"],
28
+ ["applied_by", "text NOT NULL DEFAULT ''"],
29
+ ["plan_digest", "text NOT NULL DEFAULT ''"],
30
+ ["silences", "text NOT NULL DEFAULT '[]'"],
31
+ ["note", "text NOT NULL DEFAULT ''"],
32
+ ["recorded_at", "timestamptz NOT NULL DEFAULT pg_catalog.clock_timestamp()"],
33
+ ];
34
+ /** The columns a row write names: all but `recorded_at`, which is the server's. */
35
+ export const PG_WRITTEN_COLUMNS = PG_HISTORY_COLUMNS.map(([name]) => name).filter((n) => n !== "recorded_at");
36
+ /** `<schema>.history`, quoted. */
37
+ export const pgHistoryTable = (schema) => `${pgIdent(schema)}.${pgIdent(HISTORY_TABLE)}`;
38
+ /** Every statement that makes the history ready, in order: the schema, the table, its comment. */
39
+ export function pgHistoryDdl(schema) {
40
+ const columns = PG_HISTORY_COLUMNS.map(([name, type]) => ` ${name} ${type}`).join(",\n");
41
+ return [
42
+ `CREATE SCHEMA IF NOT EXISTS ${pgIdent(schema)}`,
43
+ `CREATE TABLE IF NOT EXISTS ${pgHistoryTable(schema)} (\n${columns}\n)`,
44
+ `COMMENT ON TABLE ${pgHistoryTable(schema)} IS 'yodel migration history: append-only, one row per migration, statement and step event'`,
45
+ ];
46
+ }
47
+ //# sourceMappingURL=ddl.js.map