@intentius/sql-yodeler 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +110 -2
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +14 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chant/apply.d.ts +51 -0
  7. package/dist/chant/apply.js +104 -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 +44 -0
  13. package/dist/chant/drift.js +91 -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/cli.d.ts +4 -0
  22. package/dist/cli.js +59 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/command.d.ts +17 -0
  25. package/dist/command.js +13 -0
  26. package/dist/command.js.map +1 -0
  27. package/dist/commands/apply.d.ts +3 -0
  28. package/dist/commands/apply.js +306 -0
  29. package/dist/commands/apply.js.map +1 -0
  30. package/dist/commands/drift.d.ts +3 -0
  31. package/dist/commands/drift.js +38 -0
  32. package/dist/commands/drift.js.map +1 -0
  33. package/dist/commands/index.d.ts +3 -0
  34. package/dist/commands/index.js +22 -0
  35. package/dist/commands/index.js.map +1 -0
  36. package/dist/commands/init.d.ts +3 -0
  37. package/dist/commands/init.js +135 -0
  38. package/dist/commands/init.js.map +1 -0
  39. package/dist/commands/lint.d.ts +3 -0
  40. package/dist/commands/lint.js +193 -0
  41. package/dist/commands/lint.js.map +1 -0
  42. package/dist/commands/new.d.ts +3 -0
  43. package/dist/commands/new.js +168 -0
  44. package/dist/commands/new.js.map +1 -0
  45. package/dist/commands/plan.d.ts +3 -0
  46. package/dist/commands/plan.js +171 -0
  47. package/dist/commands/plan.js.map +1 -0
  48. package/dist/commands/rebase.d.ts +3 -0
  49. package/dist/commands/rebase.js +167 -0
  50. package/dist/commands/rebase.js.map +1 -0
  51. package/dist/commands/repair.d.ts +3 -0
  52. package/dist/commands/repair.js +117 -0
  53. package/dist/commands/repair.js.map +1 -0
  54. package/dist/commands/status.d.ts +7 -0
  55. package/dist/commands/status.js +54 -0
  56. package/dist/commands/status.js.map +1 -0
  57. package/dist/config/define.d.ts +50 -0
  58. package/dist/config/define.js +20 -0
  59. package/dist/config/define.js.map +1 -0
  60. package/dist/config/index.d.ts +11 -0
  61. package/dist/config/index.js +12 -0
  62. package/dist/config/index.js.map +1 -0
  63. package/dist/config/load.d.ts +17 -0
  64. package/dist/config/load.js +102 -0
  65. package/dist/config/load.js.map +1 -0
  66. package/dist/config/settings.d.ts +40 -0
  67. package/dist/config/settings.js +88 -0
  68. package/dist/config/settings.js.map +1 -0
  69. package/dist/env-args.d.ts +18 -0
  70. package/dist/env-args.js +62 -0
  71. package/dist/env-args.js.map +1 -0
  72. package/dist/history/client.d.ts +17 -0
  73. package/dist/history/client.js +19 -0
  74. package/dist/history/client.js.map +1 -0
  75. package/dist/history/ddl.d.ts +58 -0
  76. package/dist/history/ddl.js +100 -0
  77. package/dist/history/ddl.js.map +1 -0
  78. package/dist/history/index.d.ts +25 -0
  79. package/dist/history/index.js +26 -0
  80. package/dist/history/index.js.map +1 -0
  81. package/dist/history/lock.d.ts +100 -0
  82. package/dist/history/lock.js +224 -0
  83. package/dist/history/lock.js.map +1 -0
  84. package/dist/history/postgres/ddl.d.ts +22 -0
  85. package/dist/history/postgres/ddl.js +47 -0
  86. package/dist/history/postgres/ddl.js.map +1 -0
  87. package/dist/history/postgres/index.d.ts +12 -0
  88. package/dist/history/postgres/index.js +13 -0
  89. package/dist/history/postgres/index.js.map +1 -0
  90. package/dist/history/postgres/lock.d.ts +48 -0
  91. package/dist/history/postgres/lock.js +95 -0
  92. package/dist/history/postgres/lock.js.map +1 -0
  93. package/dist/history/postgres/store.d.ts +39 -0
  94. package/dist/history/postgres/store.js +68 -0
  95. package/dist/history/postgres/store.js.map +1 -0
  96. package/dist/history/rows.d.ts +96 -0
  97. package/dist/history/rows.js +73 -0
  98. package/dist/history/rows.js.map +1 -0
  99. package/dist/history/store.d.ts +72 -0
  100. package/dist/history/store.js +134 -0
  101. package/dist/history/store.js.map +1 -0
  102. package/dist/history/topology.d.ts +41 -0
  103. package/dist/history/topology.js +54 -0
  104. package/dist/history/topology.js.map +1 -0
  105. package/dist/index.d.ts +10 -0
  106. package/dist/index.js +10 -0
  107. package/dist/index.js.map +1 -0
  108. package/dist/init/adopt.d.ts +70 -0
  109. package/dist/init/adopt.js +138 -0
  110. package/dist/init/adopt.js.map +1 -0
  111. package/dist/init/baseline.d.ts +61 -0
  112. package/dist/init/baseline.js +88 -0
  113. package/dist/init/baseline.js.map +1 -0
  114. package/dist/init/files.d.ts +23 -0
  115. package/dist/init/files.js +77 -0
  116. package/dist/init/files.js.map +1 -0
  117. package/dist/init/index.d.ts +16 -0
  118. package/dist/init/index.js +17 -0
  119. package/dist/init/index.js.map +1 -0
  120. package/dist/init/live.d.ts +28 -0
  121. package/dist/init/live.js +45 -0
  122. package/dist/init/live.js.map +1 -0
  123. package/dist/lint/check.d.ts +81 -0
  124. package/dist/lint/check.js +208 -0
  125. package/dist/lint/check.js.map +1 -0
  126. package/dist/lint/config.d.ts +21 -0
  127. package/dist/lint/config.js +43 -0
  128. package/dist/lint/config.js.map +1 -0
  129. package/dist/lint/index.d.ts +31 -0
  130. package/dist/lint/index.js +43 -0
  131. package/dist/lint/index.js.map +1 -0
  132. package/dist/lint/project-rules.d.ts +19 -0
  133. package/dist/lint/project-rules.js +32 -0
  134. package/dist/lint/project-rules.js.map +1 -0
  135. package/dist/lint/report.d.ts +7 -0
  136. package/dist/lint/report.js +48 -0
  137. package/dist/lint/report.js.map +1 -0
  138. package/dist/lint/rules.d.ts +53 -0
  139. package/dist/lint/rules.js +42 -0
  140. package/dist/lint/rules.js.map +1 -0
  141. package/dist/lint/silences.d.ts +49 -0
  142. package/dist/lint/silences.js +91 -0
  143. package/dist/lint/silences.js.map +1 -0
  144. package/dist/lint/statements.d.ts +38 -0
  145. package/dist/lint/statements.js +120 -0
  146. package/dist/lint/statements.js.map +1 -0
  147. package/dist/migrations/checksum.d.ts +56 -0
  148. package/dist/migrations/checksum.js +72 -0
  149. package/dist/migrations/checksum.js.map +1 -0
  150. package/dist/migrations/create.d.ts +73 -0
  151. package/dist/migrations/create.js +119 -0
  152. package/dist/migrations/create.js.map +1 -0
  153. package/dist/migrations/directory.d.ts +119 -0
  154. package/dist/migrations/directory.js +225 -0
  155. package/dist/migrations/directory.js.map +1 -0
  156. package/dist/migrations/document.d.ts +83 -0
  157. package/dist/migrations/document.js +82 -0
  158. package/dist/migrations/document.js.map +1 -0
  159. package/dist/migrations/index.d.ts +20 -0
  160. package/dist/migrations/index.js +21 -0
  161. package/dist/migrations/index.js.map +1 -0
  162. package/dist/pr/comment.d.ts +32 -0
  163. package/dist/pr/comment.js +99 -0
  164. package/dist/pr/comment.js.map +1 -0
  165. package/dist/pr/forge.d.ts +50 -0
  166. package/dist/pr/forge.js +105 -0
  167. package/dist/pr/forge.js.map +1 -0
  168. package/dist/pr/model.d.ts +91 -0
  169. package/dist/pr/model.js +113 -0
  170. package/dist/pr/model.js.map +1 -0
  171. package/dist/pr/render.d.ts +27 -0
  172. package/dist/pr/render.js +246 -0
  173. package/dist/pr/render.js.map +1 -0
  174. package/dist/project.d.ts +62 -0
  175. package/dist/project.js +92 -0
  176. package/dist/project.js.map +1 -0
  177. package/dist/rebase/index.d.ts +121 -0
  178. package/dist/rebase/index.js +390 -0
  179. package/dist/rebase/index.js.map +1 -0
  180. package/dist/rebase/repair.d.ts +41 -0
  181. package/dist/rebase/repair.js +68 -0
  182. package/dist/rebase/repair.js.map +1 -0
  183. package/dist/replay/clickhouse.d.ts +33 -0
  184. package/dist/replay/clickhouse.js +106 -0
  185. package/dist/replay/clickhouse.js.map +1 -0
  186. package/dist/replay/index.d.ts +36 -0
  187. package/dist/replay/index.js +54 -0
  188. package/dist/replay/index.js.map +1 -0
  189. package/dist/replay/postgres.d.ts +31 -0
  190. package/dist/replay/postgres.js +140 -0
  191. package/dist/replay/postgres.js.map +1 -0
  192. package/dist/replay/replay.d.ts +114 -0
  193. package/dist/replay/replay.js +98 -0
  194. package/dist/replay/replay.js.map +1 -0
  195. package/dist/replay/report.d.ts +18 -0
  196. package/dist/replay/report.js +72 -0
  197. package/dist/replay/report.js.map +1 -0
  198. package/dist/steps/backfill.d.ts +128 -0
  199. package/dist/steps/backfill.js +307 -0
  200. package/dist/steps/backfill.js.map +1 -0
  201. package/dist/steps/index.d.ts +52 -0
  202. package/dist/steps/index.js +52 -0
  203. package/dist/steps/index.js.map +1 -0
  204. package/dist/steps/kinds.d.ts +101 -0
  205. package/dist/steps/kinds.js +143 -0
  206. package/dist/steps/kinds.js.map +1 -0
  207. package/dist/steps/postgres-migration.d.ts +95 -0
  208. package/dist/steps/postgres-migration.js +149 -0
  209. package/dist/steps/postgres-migration.js.map +1 -0
  210. package/dist/steps/rebuild.d.ts +97 -0
  211. package/dist/steps/rebuild.js +162 -0
  212. package/dist/steps/rebuild.js.map +1 -0
  213. package/dist/steps/run.d.ts +45 -0
  214. package/dist/steps/run.js +72 -0
  215. package/dist/steps/run.js.map +1 -0
  216. package/dist/topology/render.d.ts +17 -0
  217. package/dist/topology/render.js +17 -0
  218. package/dist/topology/render.js.map +1 -0
  219. package/dist/version.d.ts +2 -0
  220. package/dist/version.js +11 -0
  221. package/dist/version.js.map +1 -0
  222. package/dist/versioned/approval.d.ts +35 -0
  223. package/dist/versioned/approval.js +53 -0
  224. package/dist/versioned/approval.js.map +1 -0
  225. package/dist/versioned/clickhouse.d.ts +24 -0
  226. package/dist/versioned/clickhouse.js +34 -0
  227. package/dist/versioned/clickhouse.js.map +1 -0
  228. package/dist/versioned/digest.d.ts +77 -0
  229. package/dist/versioned/digest.js +69 -0
  230. package/dist/versioned/digest.js.map +1 -0
  231. package/dist/versioned/driver.d.ts +41 -0
  232. package/dist/versioned/driver.js +13 -0
  233. package/dist/versioned/driver.js.map +1 -0
  234. package/dist/versioned/engine.d.ts +152 -0
  235. package/dist/versioned/engine.js +277 -0
  236. package/dist/versioned/engine.js.map +1 -0
  237. package/dist/versioned/explain.d.ts +123 -0
  238. package/dist/versioned/explain.js +289 -0
  239. package/dist/versioned/explain.js.map +1 -0
  240. package/dist/versioned/index.d.ts +37 -0
  241. package/dist/versioned/index.js +54 -0
  242. package/dist/versioned/index.js.map +1 -0
  243. package/dist/versioned/op.d.ts +62 -0
  244. package/dist/versioned/op.js +153 -0
  245. package/dist/versioned/op.js.map +1 -0
  246. package/dist/versioned/postgres/catalog.d.ts +33 -0
  247. package/dist/versioned/postgres/catalog.js +34 -0
  248. package/dist/versioned/postgres/catalog.js.map +1 -0
  249. package/dist/versioned/postgres/driver.d.ts +69 -0
  250. package/dist/versioned/postgres/driver.js +113 -0
  251. package/dist/versioned/postgres/driver.js.map +1 -0
  252. package/dist/versioned/postgres/executor.d.ts +104 -0
  253. package/dist/versioned/postgres/executor.js +172 -0
  254. package/dist/versioned/postgres/executor.js.map +1 -0
  255. package/dist/versioned/postgres/index.d.ts +11 -0
  256. package/dist/versioned/postgres/index.js +12 -0
  257. package/dist/versioned/postgres/index.js.map +1 -0
  258. package/dist/versioned/report.d.ts +22 -0
  259. package/dist/versioned/report.js +81 -0
  260. package/dist/versioned/report.js.map +1 -0
  261. package/dist/versioned/state.d.ts +108 -0
  262. package/dist/versioned/state.js +153 -0
  263. package/dist/versioned/state.js.map +1 -0
  264. package/package.json +51 -4
@@ -0,0 +1,224 @@
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
+ constructor(holder, expiresAt, where, clear) {
66
+ super(`another apply holds the lock (${where}): owner ${holder}, until ${expiresAt} UTC. Wait for it, or once you know it is not running, clear it with: ${clear}`, LOCKED);
67
+ this.holder = holder;
68
+ this.expiresAt = expiresAt;
69
+ this.where = where;
70
+ this.clear = clear;
71
+ this.name = "LockHeldError";
72
+ }
73
+ }
74
+ export class LockLostError extends YodelError {
75
+ constructor(where) {
76
+ 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);
77
+ this.name = "LockLostError";
78
+ }
79
+ }
80
+ /** A lock owner: who, plus a random part so two runs by the same user never share one. */
81
+ export const lockOwner = (holder) => `${holder}#${String(process.pid)}-${randomUUID().slice(0, 8)}`;
82
+ const isNodeExists = (e) => /Node exists|already exists/i.test(serverMessage(e));
83
+ 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));
84
+ const STRICT = { keeper_map_strict_mode: "1", async_insert: "0" };
85
+ /** Acquires the environment's apply lock, or throws {@link LockHeldError}. */
86
+ export async function acquireLock(port, options) {
87
+ const ttlMs = options.ttlMs ?? DEFAULT_LOCK_TTL_MS;
88
+ await port.query(databaseDdl(options));
89
+ try {
90
+ await port.query(lockTableDdl(options));
91
+ }
92
+ catch (e) {
93
+ if (!keeperMapUnavailable(e))
94
+ throw e;
95
+ const why = (serverMessage(e).split("\n")[0] ?? "").replace(/^Code: \d+\. DB::Exception: /, "").replace(/\s*\([A-Z_]+\)\s*\(version [^)]*\)\s*$/, "");
96
+ if (options.topology.kind !== "single") {
97
+ throw new YodelError(`topology ${describeTopology(options.topology)} needs the Keeper lock (a KeeperMap table), and the server cannot make one: ${why}. ` +
98
+ `Configure Keeper and keeper_map_path_prefix on the server.`);
99
+ }
100
+ 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)`);
101
+ }
102
+ return keeperLock(port, options, ttlMs);
103
+ }
104
+ async function keeperLock(port, options, ttlMs) {
105
+ const table = `${ident(options.database)}.${LOCK_TABLE}`;
106
+ const owner = lockOwner(options.holder);
107
+ const where = `KeeperMap ${options.database}.${LOCK_TABLE}`;
108
+ const clear = `ALTER TABLE ${table} DELETE WHERE name = '${LOCK_NAME}'`;
109
+ const until = `now64(3) + toIntervalMillisecond(${String(ttlMs)})`;
110
+ const read = async () => (await port.query(
111
+ // Not aliased as expires_at: an alias shadows the column in ClickHouse, and the comparison would read the string.
112
+ `SELECT owner, toString(expires_at) AS until, expires_at < now64(3) AS expired FROM ${table} WHERE name = ${literal(LOCK_NAME)}`))[0];
113
+ for (let attempt = 0;; attempt++) {
114
+ try {
115
+ await port.query(`INSERT INTO ${table} (name, owner, acquired_at, expires_at) SELECT ${literal(LOCK_NAME)}, ${literal(owner)}, now64(3), ${until}`, { settings: STRICT });
116
+ break;
117
+ }
118
+ catch (e) {
119
+ if (!isNodeExists(e))
120
+ throw e;
121
+ const held = await read();
122
+ if (held && Number(held.expired) !== 1)
123
+ throw new LockHeldError(held.owner, held.until, where, clear);
124
+ if (attempt >= 3)
125
+ throw new LockHeldError(held?.owner ?? "(unknown)", held?.until ?? "(unknown)", where, clear);
126
+ // Expired (or released between the insert and the read): delete that owner's row, never a newer one, and try again.
127
+ if (held)
128
+ await port.query(`ALTER TABLE ${table} DELETE WHERE name = ${literal(LOCK_NAME)} AND owner = ${literal(held.owner)}`, { settings: STRICT });
129
+ }
130
+ }
131
+ return {
132
+ kind: "keeper",
133
+ owner,
134
+ where,
135
+ async heartbeat() {
136
+ await port.query(`ALTER TABLE ${table} UPDATE expires_at = ${until} WHERE name = ${literal(LOCK_NAME)} AND owner = ${literal(owner)}`, { settings: STRICT });
137
+ const held = await read();
138
+ if (held?.owner !== owner)
139
+ throw new LockLostError(where);
140
+ },
141
+ async release() {
142
+ await port.query(`ALTER TABLE ${table} DELETE WHERE name = ${literal(LOCK_NAME)} AND owner = ${literal(owner)}`, { settings: STRICT });
143
+ },
144
+ };
145
+ }
146
+ /** The local lock's file for a server and history database. */
147
+ export function localLockPath(options) {
148
+ const key = createHash("sha256").update(`${options.serverKey}\n${options.database}`).digest("hex").slice(0, 16);
149
+ return join(options.localDir ?? join(tmpdir(), "yodel-locks"), `${options.database}-${key}.lock`);
150
+ }
151
+ function readLockFile(path) {
152
+ try {
153
+ return JSON.parse(readFileSync(path, "utf8"));
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ }
159
+ function localLock(options, ttlMs, notice) {
160
+ const path = localLockPath(options);
161
+ mkdirSync(join(path, ".."), { recursive: true });
162
+ const owner = lockOwner(options.holder);
163
+ const where = `local lock file ${path}`;
164
+ const body = () => JSON.stringify({ owner, acquiredAt: new Date().toISOString(), expiresAt: new Date(Date.now() + ttlMs).toISOString() });
165
+ for (let attempt = 0;; attempt++) {
166
+ try {
167
+ writeFileSync(path, body(), { flag: "wx" });
168
+ break;
169
+ }
170
+ catch (e) {
171
+ if (e.code !== "EEXIST")
172
+ throw e;
173
+ const held = readLockFile(path);
174
+ const expired = !held || Date.parse(held.expiresAt) < Date.now();
175
+ if (!expired || attempt >= 3)
176
+ throw new LockHeldError(held?.owner ?? "(unreadable)", held?.expiresAt ?? "(unknown)", where, `rm ${path}`);
177
+ rmSync(path, { force: true });
178
+ }
179
+ }
180
+ return {
181
+ kind: "local",
182
+ owner,
183
+ where,
184
+ notice,
185
+ heartbeat() {
186
+ if (readLockFile(path)?.owner !== owner)
187
+ return Promise.reject(new LockLostError(where));
188
+ writeFileSync(path, body());
189
+ return Promise.resolve();
190
+ },
191
+ release() {
192
+ if (readLockFile(path)?.owner === owner)
193
+ rmSync(path, { force: true });
194
+ return Promise.resolve();
195
+ },
196
+ };
197
+ }
198
+ /**
199
+ * Runs `body` holding the lock: a heartbeat every third of the TTL while it
200
+ * runs, the lock released after, whatever happens. `beat` lets the body
201
+ * extend the lock (and learn it was lost) between statements.
202
+ */
203
+ export async function withLock(lock, ttlMs, body) {
204
+ let lost;
205
+ const timer = setInterval(() => {
206
+ lock.heartbeat().catch((e) => {
207
+ lost ??= e;
208
+ });
209
+ }, Math.max(1000, Math.floor(ttlMs / 3)));
210
+ timer.unref();
211
+ const beat = async () => {
212
+ if (lost !== undefined)
213
+ throw lost instanceof Error ? lost : new LockLostError(lock.where);
214
+ await lock.heartbeat();
215
+ };
216
+ try {
217
+ return await body(beat);
218
+ }
219
+ finally {
220
+ clearInterval(timer);
221
+ await lock.release().catch(() => undefined);
222
+ }
223
+ }
224
+ //# 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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ddl.js","sourceRoot":"","sources":["../../../src/history/postgres/ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE1C,4CAA4C;AAC5C,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;AAEjF,sFAAsF;AACtF,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;AAErF,+FAA+F;AAC/F,MAAM,CAAC,MAAM,kBAAkB,GAAuD;IACpF,CAAC,KAAK,EAAE,6BAA6B,CAAC;IACtC,CAAC,QAAQ,EAAE,eAAe,CAAC;IAC3B,CAAC,MAAM,EAAE,kEAAkE,CAAC;IAC5E,CAAC,cAAc,EAAE,eAAe,CAAC;IACjC,CAAC,QAAQ,EAAE,0BAA0B,CAAC;IACtC,CAAC,UAAU,EAAE,0BAA0B,CAAC;IACxC,CAAC,iBAAiB,EAAE,kBAAkB,CAAC;IACvC,CAAC,eAAe,EAAE,0BAA0B,CAAC;IAC7C,CAAC,QAAQ,EAAE,oEAAoE,CAAC;IAChF,CAAC,OAAO,EAAE,0BAA0B,CAAC;IACrC,CAAC,YAAY,EAAE,0BAA0B,CAAC;IAC1C,CAAC,aAAa,EAAE,0BAA0B,CAAC;IAC3C,CAAC,UAAU,EAAE,4BAA4B,CAAC;IAC1C,CAAC,MAAM,EAAE,0BAA0B,CAAC;IACpC,CAAC,aAAa,EAAE,2DAA2D,CAAC;CAC7E,CAAC;AAEF,mFAAmF;AACnF,MAAM,CAAC,MAAM,kBAAkB,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC;AAE9G,kCAAkC;AAClC,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAc,EAAU,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;AAEzG,kGAAkG;AAClG,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,OAAO,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1F,OAAO;QACL,+BAA+B,OAAO,CAAC,MAAM,CAAC,EAAE;QAChD,8BAA8B,cAAc,CAAC,MAAM,CAAC,OAAO,OAAO,KAAK;QACvE,oBAAoB,cAAc,CAAC,MAAM,CAAC,6FAA6F;KACxI,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The Postgres history (#13): `<schema>.history` (default `yodeler.history`),
3
+ * the same rows as ClickHouse's, and the session advisory lock.
4
+ *
5
+ * pgHistoryDdl(schema) CREATE SCHEMA / TABLE
6
+ * new PostgresHistoryStore(client, {schema}) exists(), ensure(), rows(), state(), append(rows)
7
+ * acquireAdvisoryLock(conn, {schema, holder}) the apply lock, on a connection of its own
8
+ * lockKeys(schema) the lock's two keys
9
+ */
10
+ export * from "./ddl.js";
11
+ export * from "./lock.js";
12
+ export * from "./store.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The Postgres history (#13): `<schema>.history` (default `yodeler.history`),
3
+ * the same rows as ClickHouse's, and the session advisory lock.
4
+ *
5
+ * pgHistoryDdl(schema) CREATE SCHEMA / TABLE
6
+ * new PostgresHistoryStore(client, {schema}) exists(), ensure(), rows(), state(), append(rows)
7
+ * acquireAdvisoryLock(conn, {schema, holder}) the apply lock, on a connection of its own
8
+ * lockKeys(schema) the lock's two keys
9
+ */
10
+ export * from "./ddl.js";
11
+ export * from "./lock.js";
12
+ export * from "./store.js";
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/history/postgres/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * One apply at a time per environment, on Postgres: a session advisory lock.
3
+ *
4
+ * `pg_try_advisory_lock(<yodel>, <history schema>)` on a connection of its
5
+ * own, opened for the lock and held for the whole apply. The two keys are
6
+ * fixed: the first is yodel's ({@link LOCK_CLASS}), the second a hash of the
7
+ * history schema's name ({@link lockKeys}), so every runner of the same
8
+ * environment asks for the same lock, and two environments with their own
9
+ * history schemas in one database do not share one. A session lock is
10
+ * Postgres's own: it is released when the session ends, however it ends
11
+ * (the runner exits, crashes, or its connection drops), so it needs no
12
+ * expiry, and a dead runner never leaves one behind.
13
+ *
14
+ * The statements run on another connection (the work connection), so the
15
+ * lock's connection is never inside a transaction a statement rolls back.
16
+ *
17
+ * A runner that finds the lock taken refuses with exit 5, as on ClickHouse,
18
+ * naming the session that holds it (its pid, its `application_name`, which
19
+ * says who, and since when). The heartbeat checks that the lock's session
20
+ * still holds it; a dropped connection stops the apply before its next
21
+ * statement.
22
+ *
23
+ * Clearing it by hand, when you know the holder is stuck: end that session,
24
+ * `SELECT pg_terminate_backend(<pid>)`.
25
+ */
26
+ import { type Lock } from "../lock.js";
27
+ import type { PgClient } from "./store.js";
28
+ /** The first key of every yodel lock: "YODL" as an int4. */
29
+ export declare const LOCK_CLASS = 1498367052;
30
+ /** The lock's two int4 keys for a history schema. Both non-negative, so `pg_locks.objid` (an oid) reads them back as they are. */
31
+ export declare function lockKeys(schema: string): [number, number];
32
+ /** A connection the lock holds for itself. */
33
+ export interface PgLockConnection extends PgClient {
34
+ end(): Promise<void>;
35
+ }
36
+ export interface PgLockOptions {
37
+ /** The history schema: the lock's second key. */
38
+ schema: string;
39
+ /** Who holds it, for the refusal another runner sees. */
40
+ holder: string;
41
+ }
42
+ /**
43
+ * Takes the advisory lock on `conn`, a connection opened for it, or throws
44
+ * {@link LockHeldError} (exit 5). On a refusal the connection is closed.
45
+ * The lock is released with the returned {@link Lock}'s `release`, which
46
+ * also closes the connection.
47
+ */
48
+ export declare function acquireAdvisoryLock(conn: PgLockConnection, options: PgLockOptions): Promise<Lock>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * One apply at a time per environment, on Postgres: a session advisory lock.
3
+ *
4
+ * `pg_try_advisory_lock(<yodel>, <history schema>)` on a connection of its
5
+ * own, opened for the lock and held for the whole apply. The two keys are
6
+ * fixed: the first is yodel's ({@link LOCK_CLASS}), the second a hash of the
7
+ * history schema's name ({@link lockKeys}), so every runner of the same
8
+ * environment asks for the same lock, and two environments with their own
9
+ * history schemas in one database do not share one. A session lock is
10
+ * Postgres's own: it is released when the session ends, however it ends
11
+ * (the runner exits, crashes, or its connection drops), so it needs no
12
+ * expiry, and a dead runner never leaves one behind.
13
+ *
14
+ * The statements run on another connection (the work connection), so the
15
+ * lock's connection is never inside a transaction a statement rolls back.
16
+ *
17
+ * A runner that finds the lock taken refuses with exit 5, as on ClickHouse,
18
+ * naming the session that holds it (its pid, its `application_name`, which
19
+ * says who, and since when). The heartbeat checks that the lock's session
20
+ * still holds it; a dropped connection stops the apply before its next
21
+ * statement.
22
+ *
23
+ * Clearing it by hand, when you know the holder is stuck: end that session,
24
+ * `SELECT pg_terminate_backend(<pid>)`.
25
+ */
26
+ import { createHash } from "node:crypto";
27
+ import { lockOwner, LockHeldError, LockLostError } from "../lock.js";
28
+ /** The first key of every yodel lock: "YODL" as an int4. */
29
+ export const LOCK_CLASS = 0x594f444c;
30
+ /** The lock's two int4 keys for a history schema. Both non-negative, so `pg_locks.objid` (an oid) reads them back as they are. */
31
+ export function lockKeys(schema) {
32
+ const digest = createHash("sha256").update(`yodel apply lock\n${schema}`, "utf8").digest();
33
+ return [LOCK_CLASS, digest.readUInt32BE(0) & 0x7fffffff];
34
+ }
35
+ /** `application_name` is 63 bytes at most; the lock's says it is yodel's and whose. */
36
+ const applicationName = (owner) => `yodel apply ${owner}`.slice(0, 63);
37
+ /**
38
+ * Takes the advisory lock on `conn`, a connection opened for it, or throws
39
+ * {@link LockHeldError} (exit 5). On a refusal the connection is closed.
40
+ * The lock is released with the returned {@link Lock}'s `release`, which
41
+ * also closes the connection.
42
+ */
43
+ export async function acquireAdvisoryLock(conn, options) {
44
+ const [k1, k2] = lockKeys(options.schema);
45
+ const owner = lockOwner(options.holder);
46
+ const where = `Postgres advisory lock (${String(k1)}, ${String(k2)}) for history schema ${options.schema}`;
47
+ try {
48
+ await conn.query("SELECT pg_catalog.set_config('application_name', $1, false)", [applicationName(owner)]);
49
+ const [row] = await conn.query("SELECT pg_catalog.pg_try_advisory_lock($1::int4, $2::int4) AS ok", [k1, k2]);
50
+ if (row?.ok !== true) {
51
+ const [held] = await conn.query(`SELECT l.pid, a.application_name AS app, pg_catalog.to_char(a.backend_start AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS') AS since
52
+ FROM pg_catalog.pg_locks l LEFT JOIN pg_catalog.pg_stat_activity a ON a.pid = l.pid
53
+ WHERE l.locktype = 'advisory' AND l.granted AND l.classid = $1::oid AND l.objid = $2::oid AND l.objsubid = 2
54
+ LIMIT 1`, [k1, k2]);
55
+ const who = held ? `${held.app || "(no application_name)"}, pid ${String(held.pid)}` : "(another session)";
56
+ throw new LockHeldError(who, held?.since ?? "(unknown)", where, held ? `SELECT pg_terminate_backend(${String(held.pid)})` : "end the session that holds it");
57
+ }
58
+ }
59
+ catch (e) {
60
+ await conn.end().catch(() => undefined);
61
+ throw e;
62
+ }
63
+ let released = false;
64
+ return {
65
+ kind: "advisory",
66
+ owner,
67
+ where,
68
+ async heartbeat() {
69
+ if (released)
70
+ throw new LockLostError(where);
71
+ let holds;
72
+ try {
73
+ const [row] = await conn.query("SELECT count(*) AS n FROM pg_catalog.pg_locks WHERE locktype = 'advisory' AND granted AND pid = pg_catalog.pg_backend_pid() AND classid = $1::oid AND objid = $2::oid AND objsubid = 2", [k1, k2]);
74
+ holds = Number(row?.n ?? 0) > 0;
75
+ }
76
+ catch {
77
+ holds = false;
78
+ }
79
+ if (!holds)
80
+ throw new LockLostError(where);
81
+ },
82
+ async release() {
83
+ if (released)
84
+ return;
85
+ released = true;
86
+ try {
87
+ await conn.query("SELECT pg_catalog.pg_advisory_unlock($1::int4, $2::int4)", [k1, k2]);
88
+ }
89
+ finally {
90
+ await conn.end().catch(() => undefined);
91
+ }
92
+ },
93
+ };
94
+ }
95
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/history/postgres/lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Reading and appending the Postgres history table. Nothing here updates or
3
+ * deletes a row.
4
+ *
5
+ * The store works on one connection, the apply's work connection, so a row
6
+ * appended while that connection has a transaction open commits or rolls
7
+ * back with it: that is how a statement and its `succeeded` row commit
8
+ * together (`../../versioned/postgres/executor.ts`).
9
+ */
10
+ import { type HistoryRow, type HistoryState } from "../rows.js";
11
+ import { type HistoryStoreLike } from "../store.js";
12
+ /** What the history needs from a Postgres connection: chant's `PostgresClient`. */
13
+ export interface PgClient {
14
+ query<T = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<T[]>;
15
+ }
16
+ export interface PgHistoryLocation {
17
+ /** The schema the history table lives in. */
18
+ schema: string;
19
+ }
20
+ export declare class PostgresHistoryStore implements HistoryStoreLike {
21
+ readonly client: PgClient;
22
+ readonly location: PgHistoryLocation;
23
+ constructor(client: PgClient, location: PgHistoryLocation);
24
+ get table(): string;
25
+ /** Whether the history table is there. A history that was never made has no rows. */
26
+ exists(): Promise<boolean>;
27
+ /** Creates the schema and table when they are not there. */
28
+ ensure(): Promise<void>;
29
+ /** Every row, in seq order. Postgres reads are consistent, so the `sync` option the ClickHouse store takes changes nothing here. */
30
+ rows(): Promise<HistoryRow[]>;
31
+ state(): Promise<HistoryState>;
32
+ /** Appends rows, as written; `recorded_at` is the server's. On the store's connection, so inside its open transaction, if any. */
33
+ append(rows: readonly HistoryRow[]): Promise<void>;
34
+ }
35
+ /** The parameterized `INSERT` of `rows`. */
36
+ export declare function insertRows(schema: string, rows: readonly HistoryRow[]): {
37
+ sql: string;
38
+ params: unknown[];
39
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Reading and appending the Postgres history table. Nothing here updates or
3
+ * deletes a row.
4
+ *
5
+ * The store works on one connection, the apply's work connection, so a row
6
+ * appended while that connection has a transaction open commits or rolls
7
+ * back with it: that is how a statement and its `succeeded` row commit
8
+ * together (`../../versioned/postgres/executor.ts`).
9
+ */
10
+ import { deriveState } from "../rows.js";
11
+ import { fromRaw } from "../store.js";
12
+ import { HISTORY_TABLE } from "../ddl.js";
13
+ import { pgHistoryDdl, pgHistoryTable, PG_HISTORY_COLUMNS, PG_WRITTEN_COLUMNS } from "./ddl.js";
14
+ /** The history's rows read back, `recorded_at` in the ClickHouse table's form (`YYYY-MM-DD hh:mm:ss.ffffff`, UTC). */
15
+ const SELECT = PG_HISTORY_COLUMNS.map(([name]) => name === "recorded_at" ? `pg_catalog.to_char(recorded_at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS.US') AS recorded_at` : name === "seq" ? "seq::text AS seq" : name).join(", ");
16
+ export class PostgresHistoryStore {
17
+ client;
18
+ location;
19
+ constructor(client, location) {
20
+ this.client = client;
21
+ this.location = location;
22
+ }
23
+ get table() {
24
+ return pgHistoryTable(this.location.schema);
25
+ }
26
+ /** Whether the history table is there. A history that was never made has no rows. */
27
+ async exists() {
28
+ const [row] = await this.client.query("SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = $1 AND c.relname = $2 AND c.relkind = 'r') AS present", [this.location.schema, HISTORY_TABLE]);
29
+ return row?.present === true;
30
+ }
31
+ /** Creates the schema and table when they are not there. */
32
+ async ensure() {
33
+ if (await this.exists())
34
+ return;
35
+ for (const sql of pgHistoryDdl(this.location.schema))
36
+ await this.client.query(sql);
37
+ }
38
+ /** Every row, in seq order. Postgres reads are consistent, so the `sync` option the ClickHouse store takes changes nothing here. */
39
+ async rows() {
40
+ if (!(await this.exists()))
41
+ return [];
42
+ const raw = await this.client.query(`SELECT ${SELECT} FROM ${this.table} ORDER BY seq`);
43
+ return raw.map(fromRaw);
44
+ }
45
+ async state() {
46
+ return deriveState(await this.rows());
47
+ }
48
+ /** Appends rows, as written; `recorded_at` is the server's. On the store's connection, so inside its open transaction, if any. */
49
+ async append(rows) {
50
+ if (rows.length === 0)
51
+ return;
52
+ const { sql, params } = insertRows(this.location.schema, rows);
53
+ await this.client.query(sql, params);
54
+ }
55
+ }
56
+ /** The parameterized `INSERT` of `rows`. */
57
+ export function insertRows(schema, rows) {
58
+ const params = [];
59
+ const tuples = rows.map((r) => {
60
+ const values = PG_WRITTEN_COLUMNS.map((c) => {
61
+ params.push(c === "seq" ? String(r.seq) : r[c]);
62
+ return `$${String(params.length)}`;
63
+ });
64
+ return `(${values.join(", ")})`;
65
+ });
66
+ return { sql: `INSERT INTO ${pgHistoryTable(schema)} (${PG_WRITTEN_COLUMNS.join(", ")}) VALUES ${tuples.join(", ")}`, params };
67
+ }
68
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/history/postgres/store.ts"],"names":[],"mappings":"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"}