orez 0.9.7 → 0.10.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 (249) hide show
  1. package/README.md +27 -19
  2. package/dist/config.d.ts +0 -4
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +0 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts +0 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +5 -43
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -71
  11. package/dist/cf-deploy/bundle.d.ts +0 -21
  12. package/dist/cf-deploy/bundle.d.ts.map +0 -1
  13. package/dist/cf-deploy/bundle.js +0 -347
  14. package/dist/cf-deploy/bundle.js.map +0 -1
  15. package/dist/cf-deploy/config.d.ts +0 -52
  16. package/dist/cf-deploy/config.d.ts.map +0 -1
  17. package/dist/cf-deploy/config.js +0 -23
  18. package/dist/cf-deploy/config.js.map +0 -1
  19. package/dist/cf-deploy/index.d.ts +0 -11
  20. package/dist/cf-deploy/index.d.ts.map +0 -1
  21. package/dist/cf-deploy/index.js +0 -17
  22. package/dist/cf-deploy/index.js.map +0 -1
  23. package/dist/cf-deploy/leaves.d.ts +0 -19
  24. package/dist/cf-deploy/leaves.d.ts.map +0 -1
  25. package/dist/cf-deploy/leaves.js +0 -109
  26. package/dist/cf-deploy/leaves.js.map +0 -1
  27. package/dist/cf-deploy/migration.d.ts +0 -40
  28. package/dist/cf-deploy/migration.d.ts.map +0 -1
  29. package/dist/cf-deploy/migration.js +0 -891
  30. package/dist/cf-deploy/migration.js.map +0 -1
  31. package/dist/cf-deploy/nativeMigrations.d.ts +0 -24
  32. package/dist/cf-deploy/nativeMigrations.d.ts.map +0 -1
  33. package/dist/cf-deploy/nativeMigrations.js +0 -185
  34. package/dist/cf-deploy/nativeMigrations.js.map +0 -1
  35. package/dist/cf-deploy/prune.d.ts +0 -14
  36. package/dist/cf-deploy/prune.d.ts.map +0 -1
  37. package/dist/cf-deploy/prune.js +0 -235
  38. package/dist/cf-deploy/prune.js.map +0 -1
  39. package/dist/cf-deploy/shims.d.ts +0 -13
  40. package/dist/cf-deploy/shims.d.ts.map +0 -1
  41. package/dist/cf-deploy/shims.js +0 -737
  42. package/dist/cf-deploy/shims.js.map +0 -1
  43. package/dist/cf-deploy/smoke.d.ts +0 -15
  44. package/dist/cf-deploy/smoke.d.ts.map +0 -1
  45. package/dist/cf-deploy/smoke.js +0 -141
  46. package/dist/cf-deploy/smoke.js.map +0 -1
  47. package/dist/cf-deploy/sources.d.ts +0 -9
  48. package/dist/cf-deploy/sources.d.ts.map +0 -1
  49. package/dist/cf-deploy/sources.js +0 -276
  50. package/dist/cf-deploy/sources.js.map +0 -1
  51. package/dist/cf-deploy/wrangler.d.ts +0 -6
  52. package/dist/cf-deploy/wrangler.d.ts.map +0 -1
  53. package/dist/cf-deploy/wrangler.js +0 -168
  54. package/dist/cf-deploy/wrangler.js.map +0 -1
  55. package/dist/cf-do/application-sql.d.ts +0 -49
  56. package/dist/cf-do/application-sql.d.ts.map +0 -1
  57. package/dist/cf-do/application-sql.js +0 -124
  58. package/dist/cf-do/application-sql.js.map +0 -1
  59. package/dist/cf-do/cdc.d.ts +0 -155
  60. package/dist/cf-do/cdc.d.ts.map +0 -1
  61. package/dist/cf-do/cdc.js +0 -719
  62. package/dist/cf-do/cdc.js.map +0 -1
  63. package/dist/cf-do/cursor-pull.d.ts +0 -27
  64. package/dist/cf-do/cursor-pull.d.ts.map +0 -1
  65. package/dist/cf-do/cursor-pull.js +0 -78
  66. package/dist/cf-do/cursor-pull.js.map +0 -1
  67. package/dist/cf-do/row-undo.d.ts +0 -36
  68. package/dist/cf-do/row-undo.d.ts.map +0 -1
  69. package/dist/cf-do/row-undo.js +0 -221
  70. package/dist/cf-do/row-undo.js.map +0 -1
  71. package/dist/cf-do/tx-journal.d.ts +0 -119
  72. package/dist/cf-do/tx-journal.d.ts.map +0 -1
  73. package/dist/cf-do/tx-journal.js +0 -718
  74. package/dist/cf-do/tx-journal.js.map +0 -1
  75. package/dist/cf-do/watermark.d.ts +0 -29
  76. package/dist/cf-do/watermark.d.ts.map +0 -1
  77. package/dist/cf-do/watermark.js +0 -105
  78. package/dist/cf-do/watermark.js.map +0 -1
  79. package/dist/cf-do/worker.d.ts +0 -287
  80. package/dist/cf-do/worker.d.ts.map +0 -1
  81. package/dist/cf-do/worker.js +0 -1961
  82. package/dist/cf-do/worker.js.map +0 -1
  83. package/dist/do-sql-tracking.d.ts +0 -104
  84. package/dist/do-sql-tracking.d.ts.map +0 -1
  85. package/dist/do-sql-tracking.js +0 -255
  86. package/dist/do-sql-tracking.js.map +0 -1
  87. package/dist/pg-proxy-do-backend.d.ts +0 -211
  88. package/dist/pg-proxy-do-backend.d.ts.map +0 -1
  89. package/dist/pg-proxy-do-backend.js +0 -7884
  90. package/dist/pg-proxy-do-backend.js.map +0 -1
  91. package/dist/pg-sqlite-compiler/catalog/seed.d.ts +0 -67
  92. package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +0 -1
  93. package/dist/pg-sqlite-compiler/catalog/seed.js +0 -436
  94. package/dist/pg-sqlite-compiler/catalog/seed.js.map +0 -1
  95. package/dist/pg-sqlite-compiler/compiler.d.ts +0 -17
  96. package/dist/pg-sqlite-compiler/compiler.d.ts.map +0 -1
  97. package/dist/pg-sqlite-compiler/compiler.js +0 -86
  98. package/dist/pg-sqlite-compiler/compiler.js.map +0 -1
  99. package/dist/pg-sqlite-compiler/index.d.ts +0 -2
  100. package/dist/pg-sqlite-compiler/index.d.ts.map +0 -1
  101. package/dist/pg-sqlite-compiler/index.js +0 -13
  102. package/dist/pg-sqlite-compiler/index.js.map +0 -1
  103. package/dist/pg-sqlite-compiler/passes/array.d.ts +0 -3
  104. package/dist/pg-sqlite-compiler/passes/array.d.ts.map +0 -1
  105. package/dist/pg-sqlite-compiler/passes/array.js +0 -223
  106. package/dist/pg-sqlite-compiler/passes/array.js.map +0 -1
  107. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +0 -48
  108. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +0 -1
  109. package/dist/pg-sqlite-compiler/passes/ast-utils.js +0 -93
  110. package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +0 -1
  111. package/dist/pg-sqlite-compiler/passes/catalog.d.ts +0 -34
  112. package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +0 -1
  113. package/dist/pg-sqlite-compiler/passes/catalog.js +0 -30
  114. package/dist/pg-sqlite-compiler/passes/catalog.js.map +0 -1
  115. package/dist/pg-sqlite-compiler/passes/datetime.d.ts +0 -23
  116. package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +0 -1
  117. package/dist/pg-sqlite-compiler/passes/datetime.js +0 -55
  118. package/dist/pg-sqlite-compiler/passes/datetime.js.map +0 -1
  119. package/dist/pg-sqlite-compiler/passes/dml-cte.d.ts +0 -22
  120. package/dist/pg-sqlite-compiler/passes/dml-cte.d.ts.map +0 -1
  121. package/dist/pg-sqlite-compiler/passes/dml-cte.js +0 -161
  122. package/dist/pg-sqlite-compiler/passes/dml-cte.js.map +0 -1
  123. package/dist/pg-sqlite-compiler/passes/index.d.ts +0 -24
  124. package/dist/pg-sqlite-compiler/passes/index.d.ts.map +0 -1
  125. package/dist/pg-sqlite-compiler/passes/index.js +0 -52
  126. package/dist/pg-sqlite-compiler/passes/index.js.map +0 -1
  127. package/dist/pg-sqlite-compiler/passes/json-functions.d.ts +0 -3
  128. package/dist/pg-sqlite-compiler/passes/json-functions.d.ts.map +0 -1
  129. package/dist/pg-sqlite-compiler/passes/json-functions.js +0 -57
  130. package/dist/pg-sqlite-compiler/passes/json-functions.js.map +0 -1
  131. package/dist/pg-sqlite-compiler/passes/row-json.d.ts +0 -3
  132. package/dist/pg-sqlite-compiler/passes/row-json.d.ts.map +0 -1
  133. package/dist/pg-sqlite-compiler/passes/row-json.js +0 -230
  134. package/dist/pg-sqlite-compiler/passes/row-json.js.map +0 -1
  135. package/dist/pg-sqlite-compiler/passes/schema.d.ts +0 -4
  136. package/dist/pg-sqlite-compiler/passes/schema.d.ts.map +0 -1
  137. package/dist/pg-sqlite-compiler/passes/schema.js +0 -128
  138. package/dist/pg-sqlite-compiler/passes/schema.js.map +0 -1
  139. package/dist/pg-sqlite-compiler/passes/string-functions.d.ts +0 -11
  140. package/dist/pg-sqlite-compiler/passes/string-functions.d.ts.map +0 -1
  141. package/dist/pg-sqlite-compiler/passes/string-functions.js +0 -37
  142. package/dist/pg-sqlite-compiler/passes/string-functions.js.map +0 -1
  143. package/dist/pg-sqlite-compiler/passes/types.d.ts +0 -41
  144. package/dist/pg-sqlite-compiler/passes/types.d.ts.map +0 -1
  145. package/dist/pg-sqlite-compiler/passes/types.js +0 -115
  146. package/dist/pg-sqlite-compiler/passes/types.js.map +0 -1
  147. package/dist/pg-sqlite-compiler/passes/unsupported.d.ts +0 -3
  148. package/dist/pg-sqlite-compiler/passes/unsupported.d.ts.map +0 -1
  149. package/dist/pg-sqlite-compiler/passes/unsupported.js +0 -57
  150. package/dist/pg-sqlite-compiler/passes/unsupported.js.map +0 -1
  151. package/dist/pg-sqlite-compiler/test/oracle.d.ts +0 -34
  152. package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +0 -1
  153. package/dist/pg-sqlite-compiler/test/oracle.js +0 -204
  154. package/dist/pg-sqlite-compiler/test/oracle.js.map +0 -1
  155. package/dist/pg-sqlite-compiler/types.d.ts +0 -66
  156. package/dist/pg-sqlite-compiler/types.d.ts.map +0 -1
  157. package/dist/pg-sqlite-compiler/types.js +0 -2
  158. package/dist/pg-sqlite-compiler/types.js.map +0 -1
  159. package/dist/sync-browser-host/generated/sqlite3-browser.wasm +0 -0
  160. package/dist/sync-browser-host/generated/sync_wasm.js +0 -922
  161. package/dist/sync-browser-host/generated/sync_wasm_bg.wasm +0 -0
  162. package/dist/sync-browser-host/host.d.ts +0 -10
  163. package/dist/sync-browser-host/host.d.ts.map +0 -1
  164. package/dist/sync-browser-host/host.js +0 -496
  165. package/dist/sync-browser-host/host.js.map +0 -1
  166. package/dist/sync-browser-host/idb-snapshot.d.ts +0 -11
  167. package/dist/sync-browser-host/idb-snapshot.d.ts.map +0 -1
  168. package/dist/sync-browser-host/idb-snapshot.js +0 -122
  169. package/dist/sync-browser-host/idb-snapshot.js.map +0 -1
  170. package/dist/sync-browser-host/index.d.ts +0 -5
  171. package/dist/sync-browser-host/index.d.ts.map +0 -1
  172. package/dist/sync-browser-host/index.js +0 -5
  173. package/dist/sync-browser-host/index.js.map +0 -1
  174. package/dist/sync-browser-host/message-port.d.ts +0 -7
  175. package/dist/sync-browser-host/message-port.d.ts.map +0 -1
  176. package/dist/sync-browser-host/message-port.js +0 -188
  177. package/dist/sync-browser-host/message-port.js.map +0 -1
  178. package/dist/sync-browser-host/sqlite-adapter.d.ts +0 -67
  179. package/dist/sync-browser-host/sqlite-adapter.d.ts.map +0 -1
  180. package/dist/sync-browser-host/sqlite-adapter.js +0 -145
  181. package/dist/sync-browser-host/sqlite-adapter.js.map +0 -1
  182. package/dist/sync-browser-host/types.d.ts +0 -52
  183. package/dist/sync-browser-host/types.d.ts.map +0 -1
  184. package/dist/sync-browser-host/types.js +0 -2
  185. package/dist/sync-browser-host/types.js.map +0 -1
  186. package/dist/sync-cf-host.d.ts +0 -2
  187. package/dist/sync-cf-host.d.ts.map +0 -1
  188. package/dist/sync-cf-host.js +0 -2
  189. package/dist/sync-cf-host.js.map +0 -1
  190. package/dist/sync-executor.d.ts +0 -2
  191. package/dist/sync-executor.d.ts.map +0 -1
  192. package/dist/sync-executor.js +0 -2
  193. package/dist/sync-executor.js.map +0 -1
  194. package/dist/wake.d.ts +0 -22
  195. package/dist/wake.d.ts.map +0 -1
  196. package/dist/wake.js +0 -114
  197. package/dist/wake.js.map +0 -1
  198. package/dist/worker/cf-do-shim.d.ts +0 -55
  199. package/dist/worker/cf-do-shim.d.ts.map +0 -1
  200. package/dist/worker/cf-do-shim.js +0 -58
  201. package/dist/worker/cf-do-shim.js.map +0 -1
  202. package/dist/worker/cf-patches.d.ts +0 -43
  203. package/dist/worker/cf-patches.d.ts.map +0 -1
  204. package/dist/worker/cf-patches.js +0 -840
  205. package/dist/worker/cf-patches.js.map +0 -1
  206. package/dist/worker/local-sql-backend.d.ts +0 -35
  207. package/dist/worker/local-sql-backend.d.ts.map +0 -1
  208. package/dist/worker/local-sql-backend.js +0 -217
  209. package/dist/worker/local-sql-backend.js.map +0 -1
  210. package/dist/worker/zero-cache-embed-cf.d.ts +0 -92
  211. package/dist/worker/zero-cache-embed-cf.d.ts.map +0 -1
  212. package/dist/worker/zero-cache-embed-cf.js +0 -764
  213. package/dist/worker/zero-cache-embed-cf.js.map +0 -1
  214. package/dist/worker/zero-sql-write-circuit.d.ts +0 -46
  215. package/dist/worker/zero-sql-write-circuit.d.ts.map +0 -1
  216. package/dist/worker/zero-sql-write-circuit.js +0 -161
  217. package/dist/worker/zero-sql-write-circuit.js.map +0 -1
  218. package/dist/zero-http/encrypted-column-codec.d.ts +0 -46
  219. package/dist/zero-http/encrypted-column-codec.d.ts.map +0 -1
  220. package/dist/zero-http/encrypted-column-codec.js +0 -676
  221. package/dist/zero-http/encrypted-column-codec.js.map +0 -1
  222. package/dist/zero-http/encrypted-column-conformance.d.ts +0 -7
  223. package/dist/zero-http/encrypted-column-conformance.d.ts.map +0 -1
  224. package/dist/zero-http/encrypted-column-conformance.js +0 -161
  225. package/dist/zero-http/encrypted-column-conformance.js.map +0 -1
  226. package/dist/zero-http/fixture-schema.d.ts +0 -127
  227. package/dist/zero-http/fixture-schema.d.ts.map +0 -1
  228. package/dist/zero-http/fixture-schema.js +0 -56
  229. package/dist/zero-http/fixture-schema.js.map +0 -1
  230. package/dist/zero-http/mount.d.ts +0 -71
  231. package/dist/zero-http/mount.d.ts.map +0 -1
  232. package/dist/zero-http/mount.js +0 -594
  233. package/dist/zero-http/mount.js.map +0 -1
  234. package/dist/zero-http/payload-codec.d.ts +0 -91
  235. package/dist/zero-http/payload-codec.d.ts.map +0 -1
  236. package/dist/zero-http/payload-codec.js +0 -10
  237. package/dist/zero-http/payload-codec.js.map +0 -1
  238. package/dist/zero-http/server.d.ts +0 -16
  239. package/dist/zero-http/server.d.ts.map +0 -1
  240. package/dist/zero-http/server.js +0 -270
  241. package/dist/zero-http/server.js.map +0 -1
  242. package/dist/zero-http/test-harness.d.ts +0 -32
  243. package/dist/zero-http/test-harness.d.ts.map +0 -1
  244. package/dist/zero-http/test-harness.js +0 -77
  245. package/dist/zero-http/test-harness.js.map +0 -1
  246. package/dist/zero-http/transport.d.ts +0 -71
  247. package/dist/zero-http/transport.d.ts.map +0 -1
  248. package/dist/zero-http/transport.js +0 -1106
  249. package/dist/zero-http/transport.js.map +0 -1
@@ -1,718 +0,0 @@
1
- /**
2
- * durable transaction journal for the DO SQL backend.
3
- *
4
- * the Durable Object refuses raw SQL BEGIN/COMMIT, so DoBackend emulates pg
5
- * transactions by applying writes eagerly. Parsed DML is rolled back from the
6
- * CDC row before-images captured by the same SQLite statement; unknown writes
7
- * fall back to a first-write table snapshot (`_orez_tx_<txID>_*`). Before this
8
- * journal, the rollback bookkeeping lived only in the client's memory: a DO eviction or
9
- * deploy upgrade-kill mid-transaction persisted the partial writes forever
10
- * (the 2026-06 poisoned cdc changeLog incident — zero's catchup replays
11
- * begin→data→begin and the replicator wedges permanently).
12
- *
13
- * the journal makes the snapshot bookkeeping durable and the commit point
14
- * atomic:
15
- *
16
- * - every row-journal marker or fallback snapshot is recorded in
17
- * `_orez_tx_manifest` atomically with its rollback state.
18
- * - COMMIT = one atomic storage transaction that drops rollback state and
19
- * deletes the manifest rows (plus promoting pending tracked changes in
20
- * ZeroDO). a tx is committed if and only if its manifest rows are gone.
21
- * - ROLLBACK = one atomic storage transaction that restores row before-images
22
- * plus any fallback table snapshots (in reverse order).
23
- * - RECOVERY (`recoverTxJournal`) rolls back every manifest tx for an
24
- * owner whose process generation is known dead (e.g. the zero-cache
25
- * embed at boot, before it opens any pg session), so a partial tx is
26
- * invisible on the next boot.
27
- *
28
- * all functions are synchronous over a minimal sql-exec interface so the
29
- * same core runs inside ZeroDO's ctx.storage.transaction() and inside the
30
- * embed-local backend's transactionSync().
31
- */
32
- import { restoreTriggers, suspendTriggers, writableColumns } from './cdc.js';
33
- export const TX_MANIFEST_TABLE = '_orez_tx_manifest';
34
- export const TX_SCHEMA_TABLE = '_orez_tx_schema';
35
- export const TX_MANIFEST_DDL = `CREATE TABLE IF NOT EXISTS "${TX_MANIFEST_TABLE}" (` +
36
- 'seq INTEGER PRIMARY KEY AUTOINCREMENT, ' +
37
- 'tx_id TEXT NOT NULL, ' +
38
- "owner TEXT NOT NULL DEFAULT 'default', " +
39
- 'original TEXT NOT NULL, ' +
40
- 'snapshot TEXT)';
41
- export const TX_SCHEMA_DDL = `CREATE TABLE IF NOT EXISTS "${TX_SCHEMA_TABLE}" (` +
42
- 'seq INTEGER PRIMARY KEY AUTOINCREMENT, ' +
43
- 'tx_id TEXT NOT NULL, ' +
44
- "owner TEXT NOT NULL DEFAULT 'default', " +
45
- 'type TEXT NOT NULL, ' +
46
- 'name TEXT NOT NULL, ' +
47
- 'tbl_name TEXT NOT NULL, ' +
48
- 'sql TEXT)';
49
- /**
50
- * The cdc registration table. It stays in INTERNAL_TABLES -- ordinary row-level
51
- * undo must not touch it -- and the schema restore carves it out explicitly
52
- * instead; see `snapshotCdcRegistration`.
53
- */
54
- const CDC_REGISTRATION_TABLE = '_orez_cdc_tables';
55
- const INTERNAL_TABLES = new Set([
56
- TX_MANIFEST_TABLE,
57
- TX_SCHEMA_TABLE,
58
- CDC_REGISTRATION_TABLE,
59
- '_orez_cdc_buffer',
60
- '_zero_pending_changes',
61
- '_zero_changes',
62
- '_zero_change_state',
63
- '_zero_schema_tables',
64
- ]);
65
- function isInternalObject(name) {
66
- return (INTERNAL_TABLES.has(name) ||
67
- name.startsWith('_orez_tx_') ||
68
- name.startsWith('sqlite_'));
69
- }
70
- function quoteIdent(name) {
71
- return `"${name.replace(/"/g, '""')}"`;
72
- }
73
- function manifestTableExists(sql) {
74
- return (sql
75
- .exec("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?", TX_MANIFEST_TABLE)
76
- .toArray().length > 0);
77
- }
78
- function tableExists(sql, name) {
79
- return (sql
80
- .exec("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?", name)
81
- .toArray().length > 0);
82
- }
83
- function schemaTableExists(sql) {
84
- return (sql
85
- .exec("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?", TX_SCHEMA_TABLE)
86
- .toArray().length > 0);
87
- }
88
- function schemaRows(sql, txID) {
89
- if (!schemaTableExists(sql))
90
- return [];
91
- return sql
92
- .exec(`SELECT seq, type, name, tbl_name, sql FROM "${TX_SCHEMA_TABLE}" WHERE tx_id = ? ORDER BY seq`, txID)
93
- .toArray()
94
- .map((row) => ({
95
- seq: Number(row.seq),
96
- type: String(row.type),
97
- name: String(row.name),
98
- table: String(row.tbl_name),
99
- sql: row.sql === null || row.sql === undefined ? null : String(row.sql),
100
- }));
101
- }
102
- function manifestRows(sql, txID) {
103
- return sql
104
- .exec(`SELECT seq, tx_id, original, snapshot FROM "${TX_MANIFEST_TABLE}" WHERE tx_id = ? ORDER BY seq`, txID)
105
- .toArray()
106
- .map((row) => ({
107
- seq: Number(row.seq),
108
- txId: String(row.tx_id),
109
- original: String(row.original),
110
- snapshot: row.snapshot === null || row.snapshot === undefined ? null : String(row.snapshot),
111
- }));
112
- }
113
- function dropTable(sql, table) {
114
- sql.exec(`DROP TABLE IF EXISTS ${quoteIdent(table)}`);
115
- }
116
- /** Parent snapshots must restore before children so FK cascades cannot erase a restored child. */
117
- function parentFirst(sql, rows) {
118
- const byTable = new Map(rows.map((row) => [row.original, row]));
119
- const edges = new Map(rows.map((row) => [row.original, new Set()]));
120
- const incoming = new Map(rows.map((row) => [row.original, 0]));
121
- for (const child of rows) {
122
- const parents = sql
123
- .exec(`PRAGMA foreign_key_list(${quoteIdent(child.original)})`)
124
- .toArray()
125
- .map((row) => String(row.table ?? ''));
126
- for (const parent of new Set(parents)) {
127
- if (!byTable.has(parent) || parent === child.original)
128
- continue;
129
- const children = edges.get(parent);
130
- if (children.has(child.original))
131
- continue;
132
- children.add(child.original);
133
- incoming.set(child.original, (incoming.get(child.original) ?? 0) + 1);
134
- }
135
- }
136
- const ready = rows.filter((row) => incoming.get(row.original) === 0);
137
- const ordered = [];
138
- while (ready.length > 0) {
139
- const row = ready.shift();
140
- ordered.push(row);
141
- for (const child of edges.get(row.original) ?? []) {
142
- const count = (incoming.get(child) ?? 0) - 1;
143
- incoming.set(child, count);
144
- if (count === 0)
145
- ready.push(byTable.get(child));
146
- }
147
- }
148
- // Cyclic foreign keys are checked at the surrounding transaction boundary;
149
- // retain deterministic manifest order for the strongly connected remainder.
150
- for (const row of rows)
151
- if (!ordered.includes(row))
152
- ordered.push(row);
153
- return ordered;
154
- }
155
- /**
156
- * true when restoreSchemaSnapshot will rebuild `table` during the rollback of
157
- * `txID`: the tx recorded a transactional-DDL schema marker and the table was
158
- * created, dropped, or DDL-changed since the schema snapshot. row-level undo
159
- * consults this to skip such tables — their pre-tx content comes back
160
- * wholesale from the table snapshot, and undoing rows against a dropped or
161
- * rebuilt table either throws or targets the wrong shape. before this,
162
- * recovery of a tx killed mid table rebuild threw on every boot and wedged
163
- * the namespace permanently (2026-07-24 prod fresh-project incident).
164
- */
165
- export function schemaRestoreOwnsTable(sql, txID, table) {
166
- if (!schemaTableExists(sql))
167
- return false;
168
- const rows = sql
169
- .exec(`SELECT type, name, sql FROM "${TX_SCHEMA_TABLE}" WHERE tx_id = ? AND type IN ('marker', 'table')`, txID)
170
- .toArray();
171
- if (!rows.some((row) => String(row.type) === 'marker'))
172
- return false;
173
- const original = rows.find((row) => String(row.type) === 'table' && String(row.name) === table);
174
- const current = sql
175
- .exec(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ? AND sql IS NOT NULL`, table)
176
- .toArray()[0];
177
- if (!original || !current)
178
- return true;
179
- return String(original.sql ?? '') !== String(current.sql ?? '');
180
- }
181
- /**
182
- * Capture the application schema before the first DDL statement and the data
183
- * of each table that a later DDL statement can destructively change. SQLite
184
- * can transact DDL, but the DO protocol spans requests, so rollback needs an
185
- * explicit durable image. The caller must wrap this in one storage transaction.
186
- */
187
- export function snapshotTxSchema(sql, txID, owner = 'default', affectedTables = []) {
188
- sql.exec(TX_SCHEMA_DDL);
189
- const exists = sql
190
- .exec(`SELECT 1 AS ok FROM "${TX_SCHEMA_TABLE}" WHERE tx_id = ? AND type = 'marker' LIMIT 1`, txID)
191
- .toArray();
192
- if (exists.length === 0) {
193
- const objects = sql
194
- .exec('SELECT type, name, tbl_name, sql FROM sqlite_master ' +
195
- "WHERE type IN ('table', 'index', 'trigger', 'view') AND sql IS NOT NULL " +
196
- 'ORDER BY rowid')
197
- .toArray()
198
- .map((row) => ({
199
- type: String(row.type ?? ''),
200
- name: String(row.name ?? ''),
201
- table: String(row.tbl_name ?? ''),
202
- sql: String(row.sql ?? ''),
203
- }))
204
- .filter((row) => row.name && !isInternalObject(row.name));
205
- // A marker makes even an empty pre-transaction schema recoverable after a
206
- // kill between CREATE TABLE and the client COMMIT/ROLLBACK.
207
- sql.exec(`INSERT INTO "${TX_SCHEMA_TABLE}" (tx_id, owner, type, name, tbl_name, sql) VALUES (?, ?, 'marker', '', '', NULL)`, txID, owner);
208
- for (const object of objects) {
209
- sql.exec(`INSERT INTO "${TX_SCHEMA_TABLE}" (tx_id, owner, type, name, tbl_name, sql) VALUES (?, ?, ?, ?, ?, ?)`, txID, owner, object.type, object.name, object.table, object.sql);
210
- }
211
- snapshotCdcRegistrations(sql, txID, owner);
212
- }
213
- for (const table of new Set(affectedTables)) {
214
- if (!table || isInternalObject(table))
215
- continue;
216
- const tableExists = sql
217
- .exec("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1", table)
218
- .toArray();
219
- if (tableExists.length > 0)
220
- upgradeToTableSnapshot(sql, txID, table, owner);
221
- }
222
- }
223
- /**
224
- * Capture every cdc registration alongside the schema snapshot.
225
- *
226
- * A cdc trigger is restored by the schema snapshot -- it is not an internal
227
- * object -- but the row in `_orez_cdc_tables` describing it IS internal and was
228
- * never restored. The two halves reverted on different rules, so a rollback
229
- * could leave a trigger with no registration. beginSchemaChange skips
230
- * unregistered tables, so capture was never suspended and the next
231
- * DROP/RENAME COLUMN on that table failed on the surviving trigger, forever
232
- * (production, 2026-07-25).
233
- *
234
- * Scope matches the schema snapshot exactly, and that is the whole point. The
235
- * snapshot is taken over ALL of sqlite_master, not over the statement's
236
- * affected tables -- only the row-data snapshots are per-table -- so the
237
- * restore drops any trigger absent from it, including one installed mid-session
238
- * for a table this transaction never touched. Capturing per affected table
239
- * instead would leave that table's row behind with its triggers gone: capture
240
- * silently off on a table the system believes is captured. Registration
241
- * therefore reverts wholesale, exactly as the triggers it describes do.
242
- *
243
- * Deliberately NOT extended to `_orez_cdc_buffer`, its sibling in
244
- * INTERNAL_TABLES. The buffer is drained and cleared by every statement (see
245
- * `drain`), so it holds in-flight changes rather than state describing a table;
246
- * restoring it would re-emit change records for writes the rollback just undid.
247
- */
248
- function snapshotCdcRegistrations(sql, txID, owner) {
249
- const registrationTableExists = sql
250
- .exec("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1", CDC_REGISTRATION_TABLE)
251
- .toArray();
252
- if (registrationTableExists.length === 0)
253
- return;
254
- const rows = sql
255
- .exec(`SELECT physical_table, table_name, columns_json, publish, schema_version ` +
256
- `FROM ${quoteIdent(CDC_REGISTRATION_TABLE)}`)
257
- .toArray();
258
- for (const row of rows) {
259
- sql.exec(`INSERT INTO "${TX_SCHEMA_TABLE}" (tx_id, owner, type, name, tbl_name, sql) VALUES (?, ?, 'cdc', ?, ?, ?)`, txID, owner, String(row.physical_table ?? ''), String(row.physical_table ?? ''), JSON.stringify(row));
260
- }
261
- }
262
- /** persist one cheap recovery marker only when a transaction first mutates data. */
263
- export function beginTxJournal(sql, txID, owner = 'default') {
264
- sql.exec(TX_SCHEMA_DDL);
265
- sql.exec(`INSERT INTO "${TX_SCHEMA_TABLE}" (tx_id, owner, type, name, tbl_name, sql) ` +
266
- `SELECT ?, ?, 'active', '', '', NULL WHERE NOT EXISTS (` +
267
- `SELECT 1 FROM "${TX_SCHEMA_TABLE}" WHERE tx_id = ? AND type = 'active')`, txID, owner, txID);
268
- }
269
- function restoreSchemaSnapshot(sql, txID, schema, manifest) {
270
- const current = sql
271
- .exec('SELECT type, name, tbl_name, sql FROM sqlite_master ' +
272
- "WHERE type IN ('table', 'index', 'trigger', 'view') AND sql IS NOT NULL " +
273
- 'ORDER BY rowid DESC')
274
- .toArray()
275
- .map((row) => ({
276
- type: String(row.type ?? ''),
277
- name: String(row.name ?? ''),
278
- table: String(row.tbl_name ?? ''),
279
- sql: String(row.sql ?? ''),
280
- }))
281
- .filter((row) => row.name && !isInternalObject(row.name));
282
- const original = schema.filter((row) => row.type !== 'marker' && row.sql);
283
- const key = (row) => `${row.type}\0${row.name}`;
284
- const originalByKey = new Map(original.map((row) => [key(row), row]));
285
- const currentByKey = new Map(current.map((row) => [key(row), row]));
286
- const originalTables = original.filter((row) => row.type === 'table');
287
- const currentTables = current.filter((row) => row.type === 'table');
288
- const changedOriginalTables = new Set(originalTables
289
- .filter((row) => currentByKey.get(key(row))?.sql !== row.sql)
290
- .map((row) => row.name));
291
- const createdTables = new Set(currentTables.filter((row) => !originalByKey.has(key(row))).map((row) => row.name));
292
- const tablesToDrop = new Set([...changedOriginalTables, ...createdTables]);
293
- const snapshotByTable = new Map(manifest
294
- .filter((row) => row.snapshot)
295
- .map((row) => [row.original, row.snapshot]));
296
- for (const table of changedOriginalTables) {
297
- if (!snapshotByTable.has(table)) {
298
- throw new Error(`transactional DDL rollback is missing table snapshot: ${table}`);
299
- }
300
- }
301
- // consumed on purpose: workerd executes a statement only as its cursor is
302
- // read, so a discarded PRAGMA cursor silently never runs.
303
- sql.exec('PRAGMA defer_foreign_keys = ON').toArray();
304
- const changedCurrentObject = (object) => {
305
- const before = originalByKey.get(key(object));
306
- return !before || before.sql !== object.sql || tablesToDrop.has(object.table);
307
- };
308
- // Views can depend on tables. Indexes and triggers disappear with their
309
- // table, while changed standalone objects are explicitly removed.
310
- for (const object of current.filter((object) => object.type === 'view' && changedCurrentObject(object))) {
311
- sql.exec(`DROP VIEW IF EXISTS ${quoteIdent(object.name)}`);
312
- }
313
- for (const object of current.filter((object) => object.type === 'trigger' &&
314
- !tablesToDrop.has(object.table) &&
315
- changedCurrentObject(object))) {
316
- sql.exec(`DROP TRIGGER IF EXISTS ${quoteIdent(object.name)}`);
317
- }
318
- for (const object of current.filter((object) => object.type === 'index' &&
319
- !tablesToDrop.has(object.table) &&
320
- changedCurrentObject(object))) {
321
- sql.exec(`DROP INDEX IF EXISTS ${quoteIdent(object.name)}`);
322
- }
323
- for (const table of tablesToDrop)
324
- sql.exec(`DROP TABLE IF EXISTS ${quoteIdent(table)}`);
325
- const tables = originalTables.filter((row) => changedOriginalTables.has(row.name));
326
- for (const row of tables)
327
- sql.exec(row.sql);
328
- for (const row of tables) {
329
- const snapshot = snapshotByTable.get(row.name);
330
- const hasRows = sql.exec(`SELECT 1 AS ok FROM ${quoteIdent(snapshot)} LIMIT 1`).toArray().length > 0;
331
- if (!hasRows)
332
- continue;
333
- const columns = writableColumns(sql, row.name);
334
- if (columns.length > 0) {
335
- const columnList = columns.map(quoteIdent).join(', ');
336
- sql.exec(`INSERT INTO ${quoteIdent(row.name)} (${columnList}) SELECT ${columnList} FROM ${quoteIdent(snapshot)}`);
337
- }
338
- }
339
- // Restore secondary objects only after all table rows are back. This keeps
340
- // business triggers from firing while the snapshot data is inserted.
341
- for (const type of ['index', 'trigger', 'view']) {
342
- for (const row of original.filter((item) => item.type === type)) {
343
- const after = currentByKey.get(key(row));
344
- if (!after || after.sql !== row.sql || changedOriginalTables.has(row.table)) {
345
- sql.exec(row.sql);
346
- }
347
- }
348
- }
349
- // Revert cdc registration in lockstep with the triggers the restore above
350
- // just put back or dropped, at the same whole-database scope the schema
351
- // snapshot uses. Clearing first is what handles the other direction: a table
352
- // registered during the transaction has had its triggers dropped by the
353
- // restore, and a row left behind would claim a captured table that nothing
354
- // captures. See `snapshotCdcRegistrations`.
355
- if (sql
356
- .exec("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1", CDC_REGISTRATION_TABLE)
357
- .toArray().length > 0) {
358
- sql.exec(`DELETE FROM ${quoteIdent(CDC_REGISTRATION_TABLE)}`);
359
- for (const row of schema.filter((item) => item.type === 'cdc' && item.sql)) {
360
- const registration = JSON.parse(row.sql);
361
- sql.exec(`INSERT INTO ${quoteIdent(CDC_REGISTRATION_TABLE)} ` +
362
- '(physical_table, table_name, columns_json, publish, schema_version) VALUES (?, ?, ?, ?, ?)', registration.physical_table, registration.table_name, registration.columns_json, registration.publish, registration.schema_version);
363
- }
364
- }
365
- // Consume only the snapshots used for schema restoration. Any remaining
366
- // manifest entries belong to ordinary DML in the same transaction and must
367
- // still run through the row/table rollback path below.
368
- if (manifestTableExists(sql)) {
369
- for (const table of changedOriginalTables) {
370
- const snapshot = snapshotByTable.get(table);
371
- if (snapshot)
372
- dropTable(sql, snapshot);
373
- sql.exec(`DELETE FROM "${TX_MANIFEST_TABLE}" WHERE tx_id = ? AND original = ?`, txID, table);
374
- }
375
- }
376
- sql.exec(`DELETE FROM "${TX_SCHEMA_TABLE}" WHERE tx_id = ?`, txID);
377
- rollbackTxJournal(sql, txID);
378
- }
379
- /**
380
- * commit a journaled transaction: drop its snapshot tables and delete its
381
- * manifest rows. the data writes were applied eagerly during the tx, so once
382
- * the manifest rows are gone the tx is durably committed. must run inside an
383
- * atomic storage transaction.
384
- */
385
- export function commitTxJournal(sql, txID) {
386
- if (manifestTableExists(sql)) {
387
- for (const row of manifestRows(sql, txID)) {
388
- if (row.snapshot)
389
- dropTable(sql, row.snapshot);
390
- }
391
- sql.exec(`DELETE FROM "${TX_MANIFEST_TABLE}" WHERE tx_id = ?`, txID);
392
- }
393
- if (schemaTableExists(sql)) {
394
- sql.exec(`DELETE FROM "${TX_SCHEMA_TABLE}" WHERE tx_id = ?`, txID);
395
- }
396
- }
397
- /**
398
- * roll back a journaled transaction: restore every snapshotted table to its
399
- * pre-tx contents (parents before children), drop tables that did not exist at
400
- * first write (null snapshot), then clean up. triggers on restored tables are
401
- * detached during the restore and re-created after, so restore DML doesn't
402
- * re-fire change tracking. must run inside an atomic storage transaction.
403
- */
404
- export function rollbackTxJournal(sql, txID) {
405
- const schema = schemaRows(sql, txID);
406
- const rows = manifestTableExists(sql) ? manifestRows(sql, txID).reverse() : [];
407
- if (schema.some((row) => row.type === 'marker')) {
408
- restoreSchemaSnapshot(sql, txID, schema, rows);
409
- return;
410
- }
411
- if (rows.length === 0) {
412
- if (schema.length > 0) {
413
- sql.exec(`DELETE FROM "${TX_SCHEMA_TABLE}" WHERE tx_id = ?`, txID);
414
- }
415
- return;
416
- }
417
- // A snapshotted table can be gone by the time its row rollback runs: a schema
418
- // restore in the same recovery drops created temp tables, and an earlier
419
- // partial recovery may already have dropped one. Its rows cannot and need not
420
- // be restored into a table that no longer exists — that restore is a no-op —
421
- // but its snapshot table and manifest row must still be cleaned up below.
422
- // Without this guard the DELETE/INSERT throws "no such table" and wedges the
423
- // durable object on every wake (prod token-usage rebuild, 2026-07-22).
424
- const allSnapshotRows = rows.filter((row) => row.snapshot !== null && row.snapshot !== '');
425
- const restorableRows = allSnapshotRows.filter((row) => tableExists(sql, row.original));
426
- const restoredTables = restorableRows.map((row) => row.original);
427
- const triggers = suspendTriggers(sql, restoredTables);
428
- // Defer constraint checks across the whole atomic restore. Delete every
429
- // snapshotted table before inserting any snapshot rows: interleaving those
430
- // phases lets a later DELETE in a cyclic cascade erase an earlier restore.
431
- // consumed on purpose: see the identical note in restoreSchemaSnapshot.
432
- sql.exec('PRAGMA defer_foreign_keys = ON').toArray();
433
- const snapshotRows = parentFirst(sql, restorableRows);
434
- for (const row of snapshotRows) {
435
- sql.exec(`DELETE FROM ${quoteIdent(row.original)}`);
436
- }
437
- for (const row of snapshotRows) {
438
- const quotedTable = quoteIdent(row.original);
439
- const quotedSnapshot = quoteIdent(row.snapshot);
440
- // `SELECT *` also selects generated columns, which SQLite refuses to let
441
- // any INSERT name, so restore the writable columns explicitly.
442
- const columns = writableColumns(sql, row.original);
443
- const columnList = columns.map(quoteIdent).join(', ');
444
- sql.exec(`INSERT OR REPLACE INTO ${quotedTable} (${columnList}) SELECT ${columnList} FROM ${quotedSnapshot}`);
445
- }
446
- // Drop every snapshot table, including those whose original was gone: the
447
- // snapshot is dead weight once the tx is being rolled back either way.
448
- for (const row of allSnapshotRows) {
449
- sql.exec(`DROP TABLE IF EXISTS ${quoteIdent(row.snapshot)}`);
450
- }
451
- for (const row of rows) {
452
- if (row.snapshot === null) {
453
- sql.exec(`DROP TABLE IF EXISTS ${quoteIdent(row.original)}`);
454
- }
455
- // Empty-string snapshots are row-journaled tables. Their before-images
456
- // were restored by the owner's CDC undo callback before this function.
457
- }
458
- restoreTriggers(sql, triggers);
459
- sql.exec(`DELETE FROM "${TX_MANIFEST_TABLE}" WHERE tx_id = ?`, txID);
460
- if (schema.length > 0) {
461
- sql.exec(`DELETE FROM "${TX_SCHEMA_TABLE}" WHERE tx_id = ?`, txID);
462
- }
463
- }
464
- /**
465
- * Point a transaction's journal entry for `table` at a real table snapshot.
466
- *
467
- * DoBackend marks a parsed write row-journaled (an empty snapshot) before the
468
- * DO has said whether it can capture that table. When CDC declines it, because
469
- * the table has no stable row identity, that empty marker promises a row-level
470
- * rollback nothing is able to perform. The owner therefore copies the table and
471
- * rewrites the marker in the same storage transaction as the write, which puts
472
- * the transaction back on the snapshot path recovery already knows how to
473
- * replay. Safe to call repeatedly: only the first write to a table in a
474
- * transaction takes the copy.
475
- */
476
- export function upgradeToTableSnapshot(sql, txID, table, owner = 'default') {
477
- sql.exec(TX_MANIFEST_DDL);
478
- const existing = sql
479
- .exec(`SELECT seq, snapshot FROM "${TX_MANIFEST_TABLE}" WHERE tx_id = ? AND original = ? ORDER BY seq`, txID, table)
480
- .toArray();
481
- // A null snapshot means the table did not exist at first write, so rollback
482
- // drops it and no copy is needed. A non-empty one is already a real snapshot.
483
- const snapshotted = existing.some((row) => row.snapshot === null || String(row.snapshot ?? '') !== '');
484
- if (snapshotted)
485
- return;
486
- // Name the snapshot after the manifest row's seq, which is an AUTOINCREMENT
487
- // primary key and therefore unique. Deriving the name from the table instead
488
- // would have to sanitize it into an identifier, and that is not injective:
489
- // `a-b` and `a_b` would collide on one snapshot table, so rolling the first
490
- // one back would restore the other's rows and then drop the table the second
491
- // still needs.
492
- const seq = existing.length > 0
493
- ? Number(existing[0].seq)
494
- : Number(sql
495
- .exec(`INSERT INTO "${TX_MANIFEST_TABLE}" (tx_id, owner, original, snapshot) VALUES (?, ?, ?, '') RETURNING seq`, txID, owner, table)
496
- .toArray()[0]?.seq);
497
- const snapshot = `_orez_tx_undo_${seq}`;
498
- sql.exec(`DROP TABLE IF EXISTS ${quoteIdent(snapshot)}`);
499
- sql.exec(`CREATE TABLE ${quoteIdent(snapshot)} AS SELECT * FROM ${quoteIdent(table)}`);
500
- sql.exec(`UPDATE "${TX_MANIFEST_TABLE}" SET snapshot = ? WHERE seq = ?`, snapshot, seq);
501
- }
502
- const TRIGGER_WRITE = /\b(?:INSERT(?:\s+OR\s+\w+)?\s+INTO|REPLACE(?:\s+OR\s+\w+)?\s+INTO|UPDATE(?:\s+OR\s+\w+)?|DELETE\s+FROM)\s+(?:"((?:[^"]|"")+)"|`((?:[^`]|``)+)`|\[([^\]]+)\]|([A-Za-z_][\w$]*))(?:\s*\.\s*(?:"((?:[^"]|"")+)"|`((?:[^`]|``)+)`|\[([^\]]+)\]|([A-Za-z_][\w$]*)))?/i;
503
- function triggerWriteTargets(sql) {
504
- if (!sql)
505
- return null;
506
- const begin = sql.search(/\bBEGIN\b/i);
507
- const end = sql.search(/\bEND\s*$/i);
508
- if (begin < 0 || end < begin)
509
- return null;
510
- const targets = [];
511
- for (const statement of sql.slice(begin + 5, end).split(';')) {
512
- if (!/\b(?:INSERT|REPLACE|UPDATE|DELETE)\b/i.test(statement))
513
- continue;
514
- const match = TRIGGER_WRITE.exec(statement);
515
- if (!match)
516
- return null;
517
- const target = match[5] ??
518
- match[6] ??
519
- match[7] ??
520
- match[8] ??
521
- match[1] ??
522
- match[2] ??
523
- match[3] ??
524
- match[4];
525
- if (!target)
526
- return null;
527
- targets.push(target.replaceAll('""', '"').replaceAll('``', '`'));
528
- }
529
- return targets;
530
- }
531
- // sqlite's built-in identifier comparison folds ASCII case only.
532
- function sqliteNoCase(identifier) {
533
- return identifier.replace(/[A-Z]/g, (character) => character.toLowerCase());
534
- }
535
- const ROW_OPERATIONS = ['INSERT', 'UPDATE', 'DELETE'];
536
- // The event keyword sits immediately before `ON <table>`, or before `OF
537
- // <columns>` for a column-scoped UPDATE trigger. Anchoring on that separator
538
- // distinguishes the event from a trigger *named* insert/update/delete.
539
- const TRIGGER_EVENT = /\b(DELETE|INSERT|UPDATE)\s+(?:OF\b|ON\b)/i;
540
- function triggerEvent(sql) {
541
- if (!sql)
542
- return null;
543
- const begin = sql.search(/\bBEGIN\b/i);
544
- const match = TRIGGER_EVENT.exec(begin < 0 ? sql : sql.slice(0, begin));
545
- return match ? match[1].toUpperCase() : null;
546
- }
547
- /**
548
- * snapshot the source table and the transitive targets of its business
549
- * triggers and cascading/SET foreign keys. trigger SQL and FK metadata name
550
- * every table SQLite can mutate implicitly, so copying unrelated published
551
- * tables only multiplies billable writes during zero-cache startup. if a
552
- * reachable trigger cannot be understood, retain the conservative all-table
553
- * fallback.
554
- *
555
- * reachability is per (table, operation), because SQLite fires implicit writes
556
- * per operation: `ON DELETE CASCADE` only fires when the parent row is deleted,
557
- * and `AFTER DELETE` triggers only fire on a delete. discovering side effects
558
- * from the table alone made every UPDATE pay for its children's delete
559
- * cascades. soot's five-second `sootAgent.runtimeClaimExpiresAt` renewal copied
560
- * the whole `agentEvent` table on each beat purely because `agentEvent`
561
- * references `sootAgent` `ON DELETE CASCADE`: 35 billable rows with an empty
562
- * event table, 1,035 with 1,000 retained events, and the rolling write budget
563
- * tripped from heartbeats alone once the table grew.
564
- */
565
- export function snapshotSideEffectWriteTables(sql, txID, sourceTable, operation) {
566
- const relations = sql
567
- .exec("SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') " +
568
- "AND name NOT GLOB 'sqlite_*' AND name NOT GLOB '_cf_*' ORDER BY name")
569
- .toArray()
570
- .map((row) => ({ name: String(row.name ?? ''), type: String(row.type ?? '') }))
571
- .filter((relation) => relation.name);
572
- const knownRelations = new Set(relations.map((relation) => sqliteNoCase(relation.name)));
573
- const snapshotTables = new Map(relations
574
- .filter((relation) => relation.type === 'table' &&
575
- !relation.name.startsWith('_orez_') &&
576
- !relation.name.startsWith('_zero_'))
577
- .map((relation) => [sqliteNoCase(relation.name), relation.name]));
578
- const node = (table, op) => `${sqliteNoCase(table)}\0${op}`;
579
- const edges = new Map();
580
- const unsafeNodes = new Set();
581
- const addEdge = (from, fromOp, to, toOps) => {
582
- const fromKey = node(from, fromOp);
583
- const targets = edges.get(fromKey) ?? new Set();
584
- for (const toOp of toOps)
585
- targets.add(node(to, toOp));
586
- edges.set(fromKey, targets);
587
- };
588
- const triggers = sql
589
- .exec("SELECT tbl_name, sql FROM sqlite_master WHERE type = 'trigger' " +
590
- "AND name NOT GLOB '_orez_cdc_*'")
591
- .toArray();
592
- for (const row of triggers) {
593
- const from = String(row.tbl_name ?? '');
594
- const definition = row.sql === null || row.sql === undefined ? null : String(row.sql);
595
- // an unreadable header hides which operation fires the trigger, so every
596
- // operation on its table has to assume it does.
597
- const event = triggerEvent(definition);
598
- const firedBy = event ? [event] : ROW_OPERATIONS;
599
- const targets = triggerWriteTargets(definition);
600
- const unknownTarget = !targets || targets.some((target) => !knownRelations.has(sqliteNoCase(target)));
601
- for (const firing of firedBy) {
602
- if (unknownTarget) {
603
- unsafeNodes.add(node(from, firing));
604
- continue;
605
- }
606
- // reading which operation a trigger body performs would have to model
607
- // REPLACE conflict resolution deleting rows, so assume all three on the
608
- // target. only whether the trigger fires at all is decided precisely.
609
- for (const target of targets)
610
- addEdge(from, firing, target, ROW_OPERATIONS);
611
- }
612
- }
613
- for (const child of relations) {
614
- if (child.type !== 'table')
615
- continue;
616
- for (const row of sql
617
- .exec(`PRAGMA foreign_key_list(${quoteIdent(child.name)})`)
618
- .toArray()) {
619
- const parent = String(row.table ?? '');
620
- if (!parent)
621
- continue;
622
- const onDelete = String(row.on_delete ?? 'NO ACTION').toUpperCase();
623
- const onUpdate = String(row.on_update ?? 'NO ACTION').toUpperCase();
624
- // deleting a parent row deletes cascaded children and rewrites SET ones.
625
- if (onDelete === 'CASCADE')
626
- addEdge(parent, 'DELETE', child.name, ['DELETE']);
627
- else if (onDelete === 'SET NULL' || onDelete === 'SET DEFAULT')
628
- addEdge(parent, 'DELETE', child.name, ['UPDATE']);
629
- // updating a parent key rewrites children under every non-restricting
630
- // action. which columns the statement touched is not known here, so any
631
- // parent update follows the ON UPDATE edge.
632
- if (onUpdate === 'CASCADE' || onUpdate === 'SET NULL' || onUpdate === 'SET DEFAULT')
633
- addEdge(parent, 'UPDATE', child.name, ['UPDATE']);
634
- }
635
- }
636
- // an upsert resolves to an insert or an update and only SQLite knows which,
637
- // so it seeds both. an insert fires no referential action.
638
- const seeded = operation === 'UPSERT' ? ['INSERT', 'UPDATE'] : [operation];
639
- const reachable = new Set();
640
- const reachableTables = new Set();
641
- const pending = seeded.map((op) => node(sourceTable, op));
642
- let hasSideEffect = false;
643
- let mustSnapshotAll = false;
644
- while (pending.length > 0) {
645
- const current = pending.pop();
646
- if (reachable.has(current))
647
- continue;
648
- reachable.add(current);
649
- reachableTables.add(current.slice(0, current.indexOf('\0')));
650
- if (unsafeNodes.has(current))
651
- mustSnapshotAll = true;
652
- for (const target of edges.get(current) ?? []) {
653
- hasSideEffect = true;
654
- pending.push(target);
655
- }
656
- }
657
- if (!hasSideEffect && !mustSnapshotAll)
658
- return false;
659
- const selected = mustSnapshotAll
660
- ? [...snapshotTables.values()]
661
- : [...reachableTables]
662
- .map((table) => snapshotTables.get(table))
663
- .filter((table) => table !== undefined);
664
- for (const table of selected.sort())
665
- upgradeToTableSnapshot(sql, txID, table);
666
- return true;
667
- }
668
- /**
669
- * roll back every journaled transaction belonging to `owner` (or every
670
- * transaction when owner is undefined), and sweep `_orez_tx_*` snapshot
671
- * tables no manifest row references (leftovers from pre-journal code).
672
- *
673
- * callers must guarantee none of the matched transactions can still be live
674
- * — i.e. the owning process generation is dead (embed boot before opening pg
675
- * sessions, or a store whose only client lives in the same isolate).
676
- *
677
- * returns the recovered transaction ids so callers can clean up associated
678
- * state (e.g. pending tracked changes).
679
- */
680
- export function recoverTxJournal(sql, owner, beforeRollback) {
681
- const recovered = [];
682
- const transactionIDs = new Set();
683
- for (const table of [TX_MANIFEST_TABLE, TX_SCHEMA_TABLE]) {
684
- const exists = table === TX_MANIFEST_TABLE ? manifestTableExists(sql) : schemaTableExists(sql);
685
- if (!exists)
686
- continue;
687
- const txRows = owner === undefined
688
- ? sql.exec(`SELECT DISTINCT tx_id FROM ${quoteIdent(table)}`).toArray()
689
- : sql
690
- .exec(`SELECT DISTINCT tx_id FROM ${quoteIdent(table)} WHERE owner = ?`, owner)
691
- .toArray();
692
- for (const row of txRows)
693
- transactionIDs.add(String(row.tx_id));
694
- }
695
- for (const txID of transactionIDs) {
696
- beforeRollback?.(txID);
697
- rollbackTxJournal(sql, txID);
698
- recovered.push(txID);
699
- }
700
- // sweep snapshot tables nothing references: pre-journal leftovers from a
701
- // killed tx. live transactions (any owner) are protected by their manifest
702
- // rows, which are created atomically with the snapshot table.
703
- const referenced = new Set(manifestTableExists(sql)
704
- ? sql
705
- .exec(`SELECT snapshot FROM "${TX_MANIFEST_TABLE}" WHERE snapshot IS NOT NULL`)
706
- .toArray()
707
- .map((row) => String(row.snapshot))
708
- : []);
709
- const orphanTables = sql
710
- .exec("SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE '\\_orez\\_tx\\_%' ESCAPE '\\' AND name != ?", TX_MANIFEST_TABLE)
711
- .toArray()
712
- .map((row) => String(row.name))
713
- .filter((name) => name !== TX_SCHEMA_TABLE && !referenced.has(name));
714
- for (const name of orphanTables)
715
- dropTable(sql, name);
716
- return recovered;
717
- }
718
- //# sourceMappingURL=tx-journal.js.map