@blulotus/trellis-core 0.7.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 (218) hide show
  1. package/LICENSE +235 -0
  2. package/dist/channel/api/assert.d.ts +3 -0
  3. package/dist/channel/api/assert.d.ts.map +1 -0
  4. package/dist/channel/api/assert.js +11 -0
  5. package/dist/channel/api/assert.js.map +1 -0
  6. package/dist/channel/api/context.d.ts +21 -0
  7. package/dist/channel/api/context.d.ts.map +1 -0
  8. package/dist/channel/api/context.js +99 -0
  9. package/dist/channel/api/context.js.map +1 -0
  10. package/dist/channel/api/create.d.ts +9 -0
  11. package/dist/channel/api/create.d.ts.map +1 -0
  12. package/dist/channel/api/create.js +104 -0
  13. package/dist/channel/api/create.js.map +1 -0
  14. package/dist/channel/api/inbox.d.ts +51 -0
  15. package/dist/channel/api/inbox.d.ts.map +1 -0
  16. package/dist/channel/api/inbox.js +176 -0
  17. package/dist/channel/api/inbox.js.map +1 -0
  18. package/dist/channel/api/interrupt.d.ts +31 -0
  19. package/dist/channel/api/interrupt.d.ts.map +1 -0
  20. package/dist/channel/api/interrupt.js +102 -0
  21. package/dist/channel/api/interrupt.js.map +1 -0
  22. package/dist/channel/api/post-thread.d.ts +14 -0
  23. package/dist/channel/api/post-thread.d.ts.map +1 -0
  24. package/dist/channel/api/post-thread.js +109 -0
  25. package/dist/channel/api/post-thread.js.map +1 -0
  26. package/dist/channel/api/read.d.ts +17 -0
  27. package/dist/channel/api/read.d.ts.map +1 -0
  28. package/dist/channel/api/read.js +44 -0
  29. package/dist/channel/api/read.js.map +1 -0
  30. package/dist/channel/api/resolve.d.ts +21 -0
  31. package/dist/channel/api/resolve.d.ts.map +1 -0
  32. package/dist/channel/api/resolve.js +28 -0
  33. package/dist/channel/api/resolve.js.map +1 -0
  34. package/dist/channel/api/runtime.d.ts +70 -0
  35. package/dist/channel/api/runtime.d.ts.map +1 -0
  36. package/dist/channel/api/runtime.js +11 -0
  37. package/dist/channel/api/runtime.js.map +1 -0
  38. package/dist/channel/api/send.d.ts +4 -0
  39. package/dist/channel/api/send.d.ts.map +1 -0
  40. package/dist/channel/api/send.js +52 -0
  41. package/dist/channel/api/send.js.map +1 -0
  42. package/dist/channel/api/spawn.d.ts +13 -0
  43. package/dist/channel/api/spawn.d.ts.map +1 -0
  44. package/dist/channel/api/spawn.js +54 -0
  45. package/dist/channel/api/spawn.js.map +1 -0
  46. package/dist/channel/api/title.d.ts +5 -0
  47. package/dist/channel/api/title.d.ts.map +1 -0
  48. package/dist/channel/api/title.js +40 -0
  49. package/dist/channel/api/title.js.map +1 -0
  50. package/dist/channel/api/types.d.ts +65 -0
  51. package/dist/channel/api/types.d.ts.map +1 -0
  52. package/dist/channel/api/types.js +2 -0
  53. package/dist/channel/api/types.js.map +1 -0
  54. package/dist/channel/api/watch-channels.d.ts +40 -0
  55. package/dist/channel/api/watch-channels.d.ts.map +1 -0
  56. package/dist/channel/api/watch-channels.js +143 -0
  57. package/dist/channel/api/watch-channels.js.map +1 -0
  58. package/dist/channel/api/watch.d.ts +11 -0
  59. package/dist/channel/api/watch.d.ts.map +1 -0
  60. package/dist/channel/api/watch.js +17 -0
  61. package/dist/channel/api/watch.js.map +1 -0
  62. package/dist/channel/api/workers.d.ts +69 -0
  63. package/dist/channel/api/workers.d.ts.map +1 -0
  64. package/dist/channel/api/workers.js +145 -0
  65. package/dist/channel/api/workers.js.map +1 -0
  66. package/dist/channel/index.d.ts +38 -0
  67. package/dist/channel/index.d.ts.map +1 -0
  68. package/dist/channel/index.js +23 -0
  69. package/dist/channel/index.js.map +1 -0
  70. package/dist/channel/internal/store/channel-metadata.d.ts +23 -0
  71. package/dist/channel/internal/store/channel-metadata.d.ts.map +1 -0
  72. package/dist/channel/internal/store/channel-metadata.js +94 -0
  73. package/dist/channel/internal/store/channel-metadata.js.map +1 -0
  74. package/dist/channel/internal/store/delivery.d.ts +27 -0
  75. package/dist/channel/internal/store/delivery.d.ts.map +1 -0
  76. package/dist/channel/internal/store/delivery.js +37 -0
  77. package/dist/channel/internal/store/delivery.js.map +1 -0
  78. package/dist/channel/internal/store/events.d.ts +215 -0
  79. package/dist/channel/internal/store/events.d.ts.map +1 -0
  80. package/dist/channel/internal/store/events.js +206 -0
  81. package/dist/channel/internal/store/events.js.map +1 -0
  82. package/dist/channel/internal/store/filter.d.ts +21 -0
  83. package/dist/channel/internal/store/filter.d.ts.map +1 -0
  84. package/dist/channel/internal/store/filter.js +75 -0
  85. package/dist/channel/internal/store/filter.js.map +1 -0
  86. package/dist/channel/internal/store/inbox.d.ts +17 -0
  87. package/dist/channel/internal/store/inbox.d.ts.map +1 -0
  88. package/dist/channel/internal/store/inbox.js +30 -0
  89. package/dist/channel/internal/store/inbox.js.map +1 -0
  90. package/dist/channel/internal/store/lock.d.ts +17 -0
  91. package/dist/channel/internal/store/lock.d.ts.map +1 -0
  92. package/dist/channel/internal/store/lock.js +88 -0
  93. package/dist/channel/internal/store/lock.js.map +1 -0
  94. package/dist/channel/internal/store/paths.d.ts +57 -0
  95. package/dist/channel/internal/store/paths.d.ts.map +1 -0
  96. package/dist/channel/internal/store/paths.js +264 -0
  97. package/dist/channel/internal/store/paths.js.map +1 -0
  98. package/dist/channel/internal/store/schema.d.ts +77 -0
  99. package/dist/channel/internal/store/schema.d.ts.map +1 -0
  100. package/dist/channel/internal/store/schema.js +127 -0
  101. package/dist/channel/internal/store/schema.js.map +1 -0
  102. package/dist/channel/internal/store/seq.d.ts +12 -0
  103. package/dist/channel/internal/store/seq.d.ts.map +1 -0
  104. package/dist/channel/internal/store/seq.js +133 -0
  105. package/dist/channel/internal/store/seq.js.map +1 -0
  106. package/dist/channel/internal/store/thread-state.d.ts +37 -0
  107. package/dist/channel/internal/store/thread-state.d.ts.map +1 -0
  108. package/dist/channel/internal/store/thread-state.js +206 -0
  109. package/dist/channel/internal/store/thread-state.js.map +1 -0
  110. package/dist/channel/internal/store/watch.d.ts +10 -0
  111. package/dist/channel/internal/store/watch.d.ts.map +1 -0
  112. package/dist/channel/internal/store/watch.js +122 -0
  113. package/dist/channel/internal/store/watch.js.map +1 -0
  114. package/dist/channel/internal/store/worker-state.d.ts +56 -0
  115. package/dist/channel/internal/store/worker-state.d.ts.map +1 -0
  116. package/dist/channel/internal/store/worker-state.js +220 -0
  117. package/dist/channel/internal/store/worker-state.js.map +1 -0
  118. package/dist/index.d.ts +3 -0
  119. package/dist/index.d.ts.map +1 -0
  120. package/dist/index.js +7 -0
  121. package/dist/index.js.map +1 -0
  122. package/dist/mem/adapters/claude.d.ts +22 -0
  123. package/dist/mem/adapters/claude.d.ts.map +1 -0
  124. package/dist/mem/adapters/claude.js +209 -0
  125. package/dist/mem/adapters/claude.js.map +1 -0
  126. package/dist/mem/adapters/codex.d.ts +55 -0
  127. package/dist/mem/adapters/codex.d.ts.map +1 -0
  128. package/dist/mem/adapters/codex.js +344 -0
  129. package/dist/mem/adapters/codex.js.map +1 -0
  130. package/dist/mem/adapters/grok.d.ts +41 -0
  131. package/dist/mem/adapters/grok.d.ts.map +1 -0
  132. package/dist/mem/adapters/grok.js +202 -0
  133. package/dist/mem/adapters/grok.js.map +1 -0
  134. package/dist/mem/adapters/opencode.d.ts +19 -0
  135. package/dist/mem/adapters/opencode.d.ts.map +1 -0
  136. package/dist/mem/adapters/opencode.js +25 -0
  137. package/dist/mem/adapters/opencode.js.map +1 -0
  138. package/dist/mem/adapters/pi.d.ts +24 -0
  139. package/dist/mem/adapters/pi.d.ts.map +1 -0
  140. package/dist/mem/adapters/pi.js +290 -0
  141. package/dist/mem/adapters/pi.js.map +1 -0
  142. package/dist/mem/adapters/zcode.d.ts +76 -0
  143. package/dist/mem/adapters/zcode.d.ts.map +1 -0
  144. package/dist/mem/adapters/zcode.js +434 -0
  145. package/dist/mem/adapters/zcode.js.map +1 -0
  146. package/dist/mem/context.d.ts +23 -0
  147. package/dist/mem/context.d.ts.map +1 -0
  148. package/dist/mem/context.js +120 -0
  149. package/dist/mem/context.js.map +1 -0
  150. package/dist/mem/dialogue.d.ts +32 -0
  151. package/dist/mem/dialogue.d.ts.map +1 -0
  152. package/dist/mem/dialogue.js +0 -0
  153. package/dist/mem/dialogue.js.map +1 -0
  154. package/dist/mem/filter.d.ts +31 -0
  155. package/dist/mem/filter.d.ts.map +1 -0
  156. package/dist/mem/filter.js +66 -0
  157. package/dist/mem/filter.js.map +1 -0
  158. package/dist/mem/index.d.ts +18 -0
  159. package/dist/mem/index.d.ts.map +1 -0
  160. package/dist/mem/index.js +17 -0
  161. package/dist/mem/index.js.map +1 -0
  162. package/dist/mem/internal/jsonl.d.ts +35 -0
  163. package/dist/mem/internal/jsonl.d.ts.map +1 -0
  164. package/dist/mem/internal/jsonl.js +125 -0
  165. package/dist/mem/internal/jsonl.js.map +1 -0
  166. package/dist/mem/internal/paths.d.ts +38 -0
  167. package/dist/mem/internal/paths.d.ts.map +1 -0
  168. package/dist/mem/internal/paths.js +126 -0
  169. package/dist/mem/internal/paths.js.map +1 -0
  170. package/dist/mem/internal/sqlite-readonly.d.ts +77 -0
  171. package/dist/mem/internal/sqlite-readonly.d.ts.map +1 -0
  172. package/dist/mem/internal/sqlite-readonly.js +797 -0
  173. package/dist/mem/internal/sqlite-readonly.js.map +1 -0
  174. package/dist/mem/phase.d.ts +45 -0
  175. package/dist/mem/phase.d.ts.map +1 -0
  176. package/dist/mem/phase.js +220 -0
  177. package/dist/mem/phase.js.map +1 -0
  178. package/dist/mem/projects.d.ts +13 -0
  179. package/dist/mem/projects.d.ts.map +1 -0
  180. package/dist/mem/projects.js +47 -0
  181. package/dist/mem/projects.js.map +1 -0
  182. package/dist/mem/search.d.ts +39 -0
  183. package/dist/mem/search.d.ts.map +1 -0
  184. package/dist/mem/search.js +133 -0
  185. package/dist/mem/search.js.map +1 -0
  186. package/dist/mem/sessions.d.ts +38 -0
  187. package/dist/mem/sessions.d.ts.map +1 -0
  188. package/dist/mem/sessions.js +313 -0
  189. package/dist/mem/sessions.js.map +1 -0
  190. package/dist/mem/types.d.ts +188 -0
  191. package/dist/mem/types.d.ts.map +1 -0
  192. package/dist/mem/types.js +10 -0
  193. package/dist/mem/types.js.map +1 -0
  194. package/dist/task/index.d.ts +9 -0
  195. package/dist/task/index.d.ts.map +1 -0
  196. package/dist/task/index.js +9 -0
  197. package/dist/task/index.js.map +1 -0
  198. package/dist/task/paths.d.ts +37 -0
  199. package/dist/task/paths.d.ts.map +1 -0
  200. package/dist/task/paths.js +49 -0
  201. package/dist/task/paths.js.map +1 -0
  202. package/dist/task/phase.d.ts +27 -0
  203. package/dist/task/phase.d.ts.map +1 -0
  204. package/dist/task/phase.js +24 -0
  205. package/dist/task/phase.js.map +1 -0
  206. package/dist/task/records.d.ts +39 -0
  207. package/dist/task/records.d.ts.map +1 -0
  208. package/dist/task/records.js +89 -0
  209. package/dist/task/records.js.map +1 -0
  210. package/dist/task/schema.d.ts +77 -0
  211. package/dist/task/schema.d.ts.map +1 -0
  212. package/dist/task/schema.js +220 -0
  213. package/dist/task/schema.js.map +1 -0
  214. package/dist/testing/index.d.ts +2 -0
  215. package/dist/testing/index.d.ts.map +1 -0
  216. package/dist/testing/index.js +4 -0
  217. package/dist/testing/index.js.map +1 -0
  218. package/package.json +77 -0
@@ -0,0 +1,797 @@
1
+ /**
2
+ * Zero-dependency read-only SQLite file parser.
3
+ *
4
+ * `@blulotus/trellis-core` is intentionally free of runtime dependencies —
5
+ * `better-sqlite3` was tried for the OpenCode adapter and reverted because its
6
+ * native build chain broke `npm install` on Windows + restricted networks. This
7
+ * parser is the minimum viable reader needed by the ZCode `mem` adapter: list
8
+ * tables, full-scan one table, return rows as `{ [columnName]: value }`. It is
9
+ * NOT a SQL engine: no WHERE pushdown, no index lookup, no writes.
10
+ *
11
+ * Scope:
12
+ * - Database header (page size, db size, text encoding, WAL flag)
13
+ * - Table b-tree leaf (0x0d) + interior (0x05) traversal
14
+ * - Record decode for TEXT / INTEGER / NULL / float64 / BLOB(serial only)
15
+ * - Overflow page chains (long TEXT rows like ZCode `message.data`)
16
+ * - WAL replay: stable main/WAL snapshot, WAL-index `mxFrame` end mark, and
17
+ * cumulative header/frame checksum verification.
18
+ *
19
+ * Out of scope:
20
+ * - WITHOUT ROWID tables (return [] — their rows live in index b-trees,
21
+ * which this parser doesn't walk). ZCode's tables are normal rowid tables.
22
+ *
23
+ * Format references:
24
+ * - https://www.sqlite.org/fileformat2.html
25
+ * - https://www.sqlite.org/walformat.html
26
+ *
27
+ * All multi-byte integers are big-endian (SQLite's native byte order).
28
+ */
29
+ import * as fs from "node:fs";
30
+ // ---------- errors ----------
31
+ /** Thrown when the SQLite file is structurally invalid. Adapters are expected
32
+ * to catch this and degrade to an empty result rather than crash the CLI. */
33
+ export class SqliteParseError extends Error {
34
+ cause;
35
+ constructor(message, cause) {
36
+ super(message);
37
+ this.cause = cause;
38
+ this.name = "SqliteParseError";
39
+ }
40
+ }
41
+ /**
42
+ * Thrown when the database files do not remain stable across all snapshot
43
+ * capture attempts. This is deliberately distinct from a malformed database:
44
+ * callers must not parse a snapshot whose consistency they could not verify.
45
+ */
46
+ export class SqliteSnapshotUnstableError extends SqliteParseError {
47
+ constructor(mainPath) {
48
+ super(`SQLite main/WAL files changed while capturing a read snapshot: ${mainPath}`);
49
+ this.name = "SqliteSnapshotUnstableError";
50
+ }
51
+ }
52
+ // ---------- low-level byte readers (big-endian) ----------
53
+ /** Read one byte at `off`, treating out-of-range reads as 0. Avoids the
54
+ * `no-non-null-assertion` lint rule while keeping byte access concise. */
55
+ function byteAt(buf, off) {
56
+ return off >= 0 && off < buf.length ? buf[off] : 0;
57
+ }
58
+ function readUint32BE(buf, off) {
59
+ return ((byteAt(buf, off) * 0x1000000 +
60
+ ((byteAt(buf, off + 1) << 16) |
61
+ (byteAt(buf, off + 2) << 8) |
62
+ byteAt(buf, off + 3))) >>>
63
+ 0);
64
+ }
65
+ function readUint16BE(buf, off) {
66
+ return ((byteAt(buf, off) << 8) | byteAt(buf, off + 1)) >>> 0;
67
+ }
68
+ function readUint8(buf, off) {
69
+ return byteAt(buf, off);
70
+ }
71
+ /**
72
+ * Decode a SQLite varint (1-9 bytes, big-endian, high bit = continuation).
73
+ * Returns the integer value and the offset past the varint. The 9th byte
74
+ * contributes all 8 bits.
75
+ *
76
+ * Uses multiplication (`* 128` / `* 256`) rather than left-shift (`<< 7`):
77
+ * `<<` coerces to int32 and wraps past 2³¹, which would corrupt large
78
+ * varints (rowids > 2³¹, or TEXT/BLOB serial types > ~2⁴⁰). Multiplication
79
+ * preserves full precision up to 2⁵³ (Number.MAX_SAFE_INTEGER), which covers
80
+ * every value SQLite can store.
81
+ */
82
+ function readVarint(buf, off) {
83
+ let result = 0;
84
+ for (let i = 0; i < 8; i++) {
85
+ const byte = byteAt(buf, off + i);
86
+ if (byte < 0x80) {
87
+ result = result * 128 + byte;
88
+ return { value: result, next: off + i + 1 };
89
+ }
90
+ result = result * 128 + (byte & 0x7f);
91
+ }
92
+ // 9th byte: all 8 bits
93
+ const ninth = byteAt(buf, off + 8);
94
+ result = result * 256 + ninth;
95
+ return { value: result, next: off + 9 };
96
+ }
97
+ /**
98
+ * Decode a big-endian signed integer of `n` bytes (1,2,3,4,6,8) into a
99
+ * JS number. Used for INTEGER column serial types.
100
+ *
101
+ * Like `readVarint`, this accumulates via multiplication (`* 256`) rather than
102
+ * `<<`, which would wrap to int32 past 2³¹. For n=8 (serial type 6, the full
103
+ * 64-bit INTEGER) values beyond 2⁵³ still lose precision — this is inherent to
104
+ * JS numbers and acceptable here (ZCode never stores such large integers).
105
+ */
106
+ function readSignedBE(buf, off, n) {
107
+ let val = 0;
108
+ for (let i = 0; i < n; i++)
109
+ val = val * 256 + byteAt(buf, off + i);
110
+ // Sign-extend if the high bit is set.
111
+ const bits = n * 8;
112
+ if (val >= 2 ** (bits - 1))
113
+ val -= 2 ** bits;
114
+ return val;
115
+ }
116
+ const DB_HEADER_SIZE = 100;
117
+ const WAL_HEADER_SIZE = 32;
118
+ const WAL_FRAME_HEADER_SIZE = 24;
119
+ const WAL_INDEX_HEADER_SIZE = 96;
120
+ const SNAPSHOT_ATTEMPTS = 3;
121
+ function parseDbHeader(buf) {
122
+ // Magic "SQLite format 3\0" (note: mixed-case, lowercase 'i').
123
+ if (buf[0] !== 0x53 || // S
124
+ buf[1] !== 0x51 || // Q
125
+ buf[2] !== 0x4c || // l
126
+ buf[3] !== 0x69 || // i
127
+ buf[4] !== 0x74 || // t
128
+ buf[5] !== 0x65 // e
129
+ ) {
130
+ throw new SqliteParseError("not a SQLite database (bad magic)");
131
+ }
132
+ const ps = readUint16BE(buf, 16);
133
+ const pageSize = ps === 1 ? 65536 : ps;
134
+ if (pageSize < 512 || (pageSize & (pageSize - 1)) !== 0) {
135
+ throw new SqliteParseError(`invalid page size ${pageSize}`);
136
+ }
137
+ const textEncoding = readUint32BE(buf, 56);
138
+ const reservedBytes = readUint8(buf, 20);
139
+ const dbSizePages = readUint32BE(buf, 28);
140
+ return {
141
+ pageSize,
142
+ dbSizePages,
143
+ textEncoding: textEncoding || 1,
144
+ reservedBytes,
145
+ };
146
+ }
147
+ const HOST_IS_LITTLE_ENDIAN = (() => {
148
+ const bytes = new Uint8Array(4);
149
+ new Uint32Array(bytes.buffer)[0] = 1;
150
+ return bytes[0] === 1;
151
+ })();
152
+ function readUint32LE(buf, off) {
153
+ return ((byteAt(buf, off) |
154
+ (byteAt(buf, off + 1) << 8) |
155
+ (byteAt(buf, off + 2) << 16) |
156
+ (byteAt(buf, off + 3) * 0x1000000)) >>>
157
+ 0);
158
+ }
159
+ function readUint32Native(buf, off) {
160
+ return HOST_IS_LITTLE_ENDIAN
161
+ ? readUint32LE(buf, off)
162
+ : readUint32BE(buf, off);
163
+ }
164
+ function readUint16Native(buf, off) {
165
+ return HOST_IS_LITTLE_ENDIAN
166
+ ? (byteAt(buf, off) | (byteAt(buf, off + 1) << 8)) >>> 0
167
+ : readUint16BE(buf, off);
168
+ }
169
+ function equalBytes(a, b) {
170
+ if (a === null || b === null)
171
+ return a === b;
172
+ if (a.length !== b.length)
173
+ return false;
174
+ for (let i = 0; i < a.length; i++) {
175
+ if (a[i] !== b[i])
176
+ return false;
177
+ }
178
+ return true;
179
+ }
180
+ function fileStamp(path) {
181
+ try {
182
+ const stat = fs.statSync(path);
183
+ return {
184
+ size: stat.size,
185
+ mtimeMs: stat.mtimeMs,
186
+ ctimeMs: stat.ctimeMs,
187
+ dev: stat.dev,
188
+ ino: stat.ino,
189
+ };
190
+ }
191
+ catch (error) {
192
+ const code = error.code;
193
+ if (code === "ENOENT")
194
+ return null;
195
+ throw error;
196
+ }
197
+ }
198
+ function sameStamp(a, b) {
199
+ if (a === null || b === null)
200
+ return a === b;
201
+ return (a.size === b.size &&
202
+ a.mtimeMs === b.mtimeMs &&
203
+ a.ctimeMs === b.ctimeMs &&
204
+ a.dev === b.dev &&
205
+ a.ino === b.ino);
206
+ }
207
+ function readOptionalFile(path) {
208
+ try {
209
+ return fs.readFileSync(path);
210
+ }
211
+ catch (error) {
212
+ const code = error.code;
213
+ if (code === "ENOENT")
214
+ return null;
215
+ throw error;
216
+ }
217
+ }
218
+ function readWalIndexPrefix(path) {
219
+ const bytes = readOptionalFile(path);
220
+ return bytes === null
221
+ ? null
222
+ : bytes.subarray(0, Math.min(bytes.length, WAL_INDEX_HEADER_SIZE));
223
+ }
224
+ function walChecksum(bytes, off, length, bigEndian, seed0, seed1) {
225
+ if (length % 8 !== 0) {
226
+ throw new SqliteParseError("WAL checksum input is not 8-byte aligned");
227
+ }
228
+ const readWord = bigEndian ? readUint32BE : readUint32LE;
229
+ let s0 = seed0 >>> 0;
230
+ let s1 = seed1 >>> 0;
231
+ for (let i = off; i < off + length; i += 8) {
232
+ s0 = (s0 + readWord(bytes, i) + s1) >>> 0;
233
+ s1 = (s1 + readWord(bytes, i + 4) + s0) >>> 0;
234
+ }
235
+ return [s0, s1];
236
+ }
237
+ function parseWalIndexHeader(bytes) {
238
+ if (bytes === null)
239
+ return null;
240
+ if (bytes.length < WAL_INDEX_HEADER_SIZE) {
241
+ throw new SqliteParseError("WAL-index header is truncated");
242
+ }
243
+ const first = bytes.subarray(0, 48);
244
+ const second = bytes.subarray(48, 96);
245
+ if (!equalBytes(first, second)) {
246
+ throw new SqliteParseError("WAL-index header copies disagree");
247
+ }
248
+ if (byteAt(first, 12) !== 1) {
249
+ throw new SqliteParseError("WAL-index is not initialized");
250
+ }
251
+ const [checksum1, checksum2] = walChecksum(first, 0, 40, !HOST_IS_LITTLE_ENDIAN, 0, 0);
252
+ if (checksum1 !== readUint32Native(first, 40) ||
253
+ checksum2 !== readUint32Native(first, 44)) {
254
+ throw new SqliteParseError("WAL-index header checksum mismatch");
255
+ }
256
+ const encodedPageSize = readUint16Native(first, 14);
257
+ return {
258
+ pageSize: encodedPageSize === 1 ? 65536 : encodedPageSize,
259
+ mxFrame: readUint32Native(first, 16),
260
+ nPage: readUint32Native(first, 20),
261
+ frameChecksum1: readUint32Native(first, 24),
262
+ frameChecksum2: readUint32Native(first, 28),
263
+ salt1: readUint32BE(first, 32),
264
+ salt2: readUint32BE(first, 36),
265
+ bigEndianChecksum: byteAt(first, 13) !== 0,
266
+ };
267
+ }
268
+ function captureSnapshot(mainPath) {
269
+ const walPath = mainPath + "-wal";
270
+ const shmPath = mainPath + "-shm";
271
+ let lastError;
272
+ for (let attempt = 0; attempt < SNAPSHOT_ATTEMPTS; attempt++) {
273
+ try {
274
+ const shmBefore = readWalIndexPrefix(shmPath);
275
+ const mainBefore = fileStamp(mainPath);
276
+ const walBefore = fileStamp(walPath);
277
+ if (mainBefore === null) {
278
+ throw new SqliteParseError(`cannot read db file: ${mainPath}`);
279
+ }
280
+ const mainBytes = fs.readFileSync(mainPath);
281
+ const walBytes = readOptionalFile(walPath);
282
+ const mainAfter = fileStamp(mainPath);
283
+ const walAfter = fileStamp(walPath);
284
+ const shmAfter = readWalIndexPrefix(shmPath);
285
+ if (!sameStamp(mainBefore, mainAfter) ||
286
+ !sameStamp(walBefore, walAfter) ||
287
+ !equalBytes(shmBefore, shmAfter)) {
288
+ lastError = new SqliteSnapshotUnstableError(mainPath);
289
+ continue;
290
+ }
291
+ const walIndex = walBytes === null ? null : parseWalIndexHeader(shmAfter);
292
+ return { mainBytes, walBytes, walIndex };
293
+ }
294
+ catch (error) {
295
+ lastError = error;
296
+ }
297
+ }
298
+ throw lastError instanceof SqliteParseError
299
+ ? lastError
300
+ : new SqliteParseError(`cannot capture SQLite snapshot: ${mainPath}`, lastError);
301
+ }
302
+ /**
303
+ * Validate and replay captured WAL bytes. Returns a page-overrides map or
304
+ * `null` when the WAL is absent.
305
+ *
306
+ * Algorithm (per https://www.sqlite.org/walformat.html):
307
+ * 1. Validate the 32-byte header checksum, salts, and page size.
308
+ * 2. Use WAL-index `mxFrame` as the committed end mark when available;
309
+ * otherwise stop recovery at the first invalid/stale frame.
310
+ * 3. Validate every frame's cumulative checksum seeded by the header.
311
+ * 4. Replay pages through the committed end mark, with later frames winning.
312
+ */
313
+ function loadWal(walBytes, expectedPageSize, walIndex) {
314
+ if (walBytes === null)
315
+ return null;
316
+ if (walBytes.length < WAL_HEADER_SIZE)
317
+ return null;
318
+ const magic = readUint32BE(walBytes, 0);
319
+ if (magic !== 0x377f0682 && magic !== 0x377f0683) {
320
+ throw new SqliteParseError("invalid WAL magic");
321
+ }
322
+ const bigEndianChecksum = magic === 0x377f0683;
323
+ const walPageSize = readUint32BE(walBytes, 8);
324
+ if (walPageSize !== expectedPageSize) {
325
+ throw new SqliteParseError("WAL page size does not match database");
326
+ }
327
+ const salt1 = readUint32BE(walBytes, 16);
328
+ const salt2 = readUint32BE(walBytes, 20);
329
+ let [checksum1, checksum2] = walChecksum(walBytes, 0, 24, bigEndianChecksum, 0, 0);
330
+ if (checksum1 !== readUint32BE(walBytes, 24) ||
331
+ checksum2 !== readUint32BE(walBytes, 28)) {
332
+ throw new SqliteParseError("WAL header checksum mismatch");
333
+ }
334
+ if (walIndex) {
335
+ if (walIndex.pageSize !== expectedPageSize ||
336
+ walIndex.salt1 !== salt1 ||
337
+ walIndex.salt2 !== salt2 ||
338
+ walIndex.bigEndianChecksum !== bigEndianChecksum) {
339
+ throw new SqliteParseError("WAL-index does not match WAL header");
340
+ }
341
+ }
342
+ const frameSize = WAL_FRAME_HEADER_SIZE + expectedPageSize;
343
+ const frameCount = Math.floor((walBytes.length - WAL_HEADER_SIZE) / frameSize);
344
+ const frameLimit = walIndex?.mxFrame ?? frameCount;
345
+ if (frameLimit > frameCount) {
346
+ throw new SqliteParseError("WAL-index end mark exceeds WAL frame count");
347
+ }
348
+ let lastCommitFrame = -1;
349
+ let validatedFrameCount = 0;
350
+ for (let i = 0; i < frameLimit; i++) {
351
+ const base = WAL_HEADER_SIZE + i * frameSize;
352
+ const fSalt1 = readUint32BE(walBytes, base + 8);
353
+ const fSalt2 = readUint32BE(walBytes, base + 12);
354
+ if (fSalt1 !== salt1 || fSalt2 !== salt2) {
355
+ if (walIndex) {
356
+ throw new SqliteParseError(`WAL frame ${i + 1} salt mismatch`);
357
+ }
358
+ break;
359
+ }
360
+ [checksum1, checksum2] = walChecksum(walBytes, base, 8, bigEndianChecksum, checksum1, checksum2);
361
+ [checksum1, checksum2] = walChecksum(walBytes, base + WAL_FRAME_HEADER_SIZE, expectedPageSize, bigEndianChecksum, checksum1, checksum2);
362
+ if (checksum1 !== readUint32BE(walBytes, base + 16) ||
363
+ checksum2 !== readUint32BE(walBytes, base + 20)) {
364
+ if (walIndex) {
365
+ throw new SqliteParseError(`WAL frame ${i + 1} checksum mismatch`);
366
+ }
367
+ break;
368
+ }
369
+ validatedFrameCount = i + 1;
370
+ const dbSizeAfterCommit = readUint32BE(walBytes, base + 4);
371
+ if (dbSizeAfterCommit !== 0)
372
+ lastCommitFrame = i;
373
+ }
374
+ if (walIndex) {
375
+ if (checksum1 !== walIndex.frameChecksum1 ||
376
+ checksum2 !== walIndex.frameChecksum2) {
377
+ throw new SqliteParseError("WAL end-mark checksum disagrees with WAL-index");
378
+ }
379
+ if (frameLimit > 0 && lastCommitFrame !== frameLimit - 1) {
380
+ throw new SqliteParseError("WAL-index end mark is not a commit frame");
381
+ }
382
+ }
383
+ if (lastCommitFrame < 0)
384
+ return { pageMap: new Map() };
385
+ const pageMap = new Map();
386
+ const replayFrameCount = walIndex
387
+ ? frameLimit
388
+ : Math.min(validatedFrameCount, lastCommitFrame + 1);
389
+ for (let i = 0; i < replayFrameCount; i++) {
390
+ const base = WAL_HEADER_SIZE + i * frameSize;
391
+ const pgno = readUint32BE(walBytes, base);
392
+ const pageStart = base + WAL_FRAME_HEADER_SIZE;
393
+ pageMap.set(pgno, walBytes.subarray(pageStart, pageStart + expectedPageSize));
394
+ }
395
+ return { pageMap };
396
+ }
397
+ function makePageSource(mainBytes, header, wal) {
398
+ return {
399
+ pageSize: header.pageSize,
400
+ getPage(pgno) {
401
+ // WAL override first
402
+ if (wal?.pageMap.has(pgno)) {
403
+ const walPage = wal.pageMap.get(pgno);
404
+ if (walPage)
405
+ return walPage;
406
+ }
407
+ const start = (pgno - 1) * header.pageSize;
408
+ const end = start + header.pageSize;
409
+ if (end > mainBytes.length) {
410
+ // Page lives beyond the main file and has no WAL copy — treat as empty.
411
+ return new Uint8Array(header.pageSize);
412
+ }
413
+ return mainBytes.subarray(start, end);
414
+ },
415
+ };
416
+ }
417
+ /** Decode a record body (the payload of a table-leaf cell) into an array of
418
+ * JS values. Serial type reference: https://www.sqlite.org/fileformat2.html
419
+ * section 2.1. Only TEXT / INTEGER / NULL / float / BLOB are handled. The
420
+ * caller supplies a reused `TextDecoder` (hoisted per scan) so we don't
421
+ * allocate one per cell. */
422
+ function decodeRecord(payload, td) {
423
+ const headerLenInfo = readVarint(payload, 0);
424
+ const headerLen = headerLenInfo.value;
425
+ const headerEnd = headerLenInfo.next;
426
+ if (headerLen > payload.length) {
427
+ throw new SqliteParseError("record header length exceeds payload");
428
+ }
429
+ // Collect serial types.
430
+ const serialTypes = [];
431
+ let p = headerEnd;
432
+ while (p < headerLen) {
433
+ const st = readVarint(payload, p);
434
+ serialTypes.push(st.value);
435
+ p = st.next;
436
+ }
437
+ // Decode body values.
438
+ const values = [];
439
+ let bodyOff = headerLen;
440
+ for (const st of serialTypes) {
441
+ if (st === 0) {
442
+ values.push(null);
443
+ }
444
+ else if (st <= 4) {
445
+ values.push(readSignedBE(payload, bodyOff, st));
446
+ bodyOff += st;
447
+ }
448
+ else if (st === 5) {
449
+ values.push(readSignedBE(payload, bodyOff, 6));
450
+ bodyOff += 6;
451
+ }
452
+ else if (st === 6) {
453
+ values.push(readSignedBE(payload, bodyOff, 8));
454
+ bodyOff += 8;
455
+ }
456
+ else if (st === 7) {
457
+ // float64 big-endian
458
+ const view = new DataView(payload.buffer, payload.byteOffset + bodyOff, 8);
459
+ values.push(view.getFloat64(0));
460
+ bodyOff += 8;
461
+ }
462
+ else if (st === 8) {
463
+ values.push(0);
464
+ }
465
+ else if (st === 9) {
466
+ values.push(1);
467
+ }
468
+ else if (st >= 12 && st % 2 === 0) {
469
+ // BLOB
470
+ const len = (st - 12) / 2;
471
+ values.push(payload.subarray(bodyOff, bodyOff + len));
472
+ bodyOff += len;
473
+ }
474
+ else if (st >= 13 && st % 2 === 1) {
475
+ // TEXT
476
+ const len = (st - 13) / 2;
477
+ values.push(td.decode(payload.subarray(bodyOff, bodyOff + len)));
478
+ bodyOff += len;
479
+ }
480
+ else {
481
+ // st === 10 or 11 are reserved for internal use; skip defensively.
482
+ values.push(null);
483
+ }
484
+ }
485
+ return { values };
486
+ }
487
+ function decodeTextDecoderEncoding(enc) {
488
+ if (enc === 2)
489
+ return "utf-16le";
490
+ if (enc === 3)
491
+ return "utf-16be";
492
+ return "utf-8";
493
+ }
494
+ // ---------- b-tree traversal ----------
495
+ const PAGE_LEAF_TABLE = 0x0d;
496
+ const PAGE_INTERIOR_TABLE = 0x05;
497
+ /**
498
+ * Visit every row from the table b-tree rooted at `rootPgno`, in b-tree key
499
+ * (rowid) order. The visitor form lets callers filter rows without retaining
500
+ * a decoded copy of the entire table first.
501
+ */
502
+ function visitTableBtree(src, rootPgno, textEncoding, header, visit) {
503
+ // Hoist one decoder per scan instead of allocating one per cell.
504
+ const td = new TextDecoder(decodeTextDecoderEncoding(textEncoding));
505
+ // Visited-set guards against corrupted interior pages whose child pointer
506
+ // forms a cycle (ancestor → self), which would otherwise recurse until the
507
+ // JS stack overflows. Valid SQLite b-trees are acyclic, so this never trims
508
+ // real data.
509
+ const visited = new Set();
510
+ walk(rootPgno);
511
+ function walk(pgno) {
512
+ if (pgno <= 0 || visited.has(pgno))
513
+ return;
514
+ visited.add(pgno);
515
+ const page = src.getPage(pgno);
516
+ // Page 1 has a 100-byte db header before its b-tree header.
517
+ const hdrOff = pgno === 1 ? DB_HEADER_SIZE : 0;
518
+ const pageType = byteAt(page, hdrOff);
519
+ if (pageType === PAGE_INTERIOR_TABLE) {
520
+ walkInterior(page, hdrOff);
521
+ }
522
+ else if (pageType === PAGE_LEAF_TABLE) {
523
+ walkLeaf(page, hdrOff);
524
+ }
525
+ else {
526
+ // Not a table b-tree page (could be index/freelist). Silently skip — a
527
+ // corrupted root page will just yield no rows rather than crash.
528
+ }
529
+ }
530
+ function walkInterior(page, hdrOff) {
531
+ const ncells = readUint16BE(page, hdrOff + 3);
532
+ const cellPtrStart = hdrOff + 12; // interior header is 12 bytes
533
+ for (let i = 0; i < ncells; i++) {
534
+ const cellOff = readUint16BE(page, cellPtrStart + i * 2);
535
+ const childPgno = readUint32BE(page, cellOff);
536
+ walk(childPgno);
537
+ }
538
+ // Right-most pointer (header bytes 8-11).
539
+ const rightMost = readUint32BE(page, hdrOff + 8);
540
+ if (rightMost !== 0)
541
+ walk(rightMost);
542
+ }
543
+ function walkLeaf(page, hdrOff) {
544
+ const ncells = readUint16BE(page, hdrOff + 3);
545
+ const cellPtrStart = hdrOff + 8; // leaf header is 8 bytes
546
+ for (let i = 0; i < ncells; i++) {
547
+ const cellOff = readUint16BE(page, cellPtrStart + i * 2);
548
+ const { rowid, values } = decodeLeafCell(page, cellOff, src, header, td);
549
+ visit({ rowid, values });
550
+ }
551
+ }
552
+ }
553
+ /** Compute the overflow thresholds and reassemble a cell payload, following
554
+ * overflow page chains when the payload spills past the leaf page. */
555
+ function decodeLeafCell(page, cellOff, src, header, td) {
556
+ // leaf cell: payloadLen (varint) | rowid (varint) | payload [| overflow pgno]
557
+ let cur = cellOff;
558
+ const payloadLenInfo = readVarint(page, cur);
559
+ cur = payloadLenInfo.next;
560
+ const rowidInfo = readVarint(page, cur);
561
+ cur = rowidInfo.next;
562
+ const payloadLen = payloadLenInfo.value;
563
+ // Overflow math (https://www.sqlite.org/fileformat2.html §1.6).
564
+ const usableSize = header.pageSize - header.reservedBytes;
565
+ const maxLocal = usableSize - 35;
566
+ const minLocal = Math.floor(((usableSize - 12) * 32) / 255) - 23;
567
+ let localBytes;
568
+ let overflowPgno = null;
569
+ if (payloadLen <= maxLocal) {
570
+ localBytes = payloadLen;
571
+ }
572
+ else {
573
+ const k = minLocal + ((payloadLen - minLocal) % (usableSize - 4));
574
+ localBytes = k <= maxLocal ? k : minLocal;
575
+ overflowPgno = readUint32BE(page, cur + localBytes);
576
+ }
577
+ // Build the full payload buffer.
578
+ let payload;
579
+ if (overflowPgno === null) {
580
+ payload = page.subarray(cur, cur + localBytes);
581
+ }
582
+ else {
583
+ const full = new Uint8Array(payloadLen);
584
+ full.set(page.subarray(cur, cur + localBytes), 0);
585
+ let written = localBytes;
586
+ let nextPgno = overflowPgno;
587
+ // Guard against pathological chains.
588
+ let guard = 0;
589
+ while (nextPgno !== 0 && written < payloadLen && guard < 100000) {
590
+ guard++;
591
+ const ovPage = src.getPage(nextPgno);
592
+ const nextPage = readUint32BE(ovPage, 0);
593
+ const chunkLen = Math.min(payloadLen - written, usableSize - 4);
594
+ full.set(ovPage.subarray(4, 4 + chunkLen), written);
595
+ written += chunkLen;
596
+ nextPgno = nextPage;
597
+ }
598
+ payload = full;
599
+ }
600
+ const { values } = decodeRecord(payload, td);
601
+ return { rowid: rowidInfo.value, values };
602
+ }
603
+ // ---------- column-name recovery from CREATE TABLE sql ----------
604
+ /**
605
+ * Best-effort column-name extractor for `CREATE TABLE name (col1 TYPE, col2, ...)`.
606
+ * Returns the list of column names in order, or `null` when parsing fails (the
607
+ * caller then falls back to `col0`, `col1`, ...).
608
+ *
609
+ * Strategy: split the parenthesised body on top-level commas, then take the
610
+ * first identifier of each segment as the column name. Segments whose first
611
+ * identifier is a table-constraint keyword (PRIMARY KEY / UNIQUE / CHECK /
612
+ * FOREIGN KEY / CONSTRAINT) are dropped — they are table-level constraints,
613
+ * not columns.
614
+ */
615
+ function parseColumnNames(sql) {
616
+ const open = sql.indexOf("(");
617
+ const close = sql.lastIndexOf(")");
618
+ if (open < 0 || close < 0 || close <= open)
619
+ return null;
620
+ const body = sql.slice(open + 1, close);
621
+ const segments = splitTopLevelCommas(body);
622
+ const cols = [];
623
+ for (const seg of segments) {
624
+ const trimmed = seg.trim();
625
+ if (!trimmed)
626
+ continue;
627
+ const ident = firstIdentifier(trimmed);
628
+ if (!ident)
629
+ continue;
630
+ if (isConstraintKeyword(ident))
631
+ continue;
632
+ cols.push(ident);
633
+ }
634
+ return cols.length ? cols : null;
635
+ }
636
+ /** Split on commas that are not inside any parenthesised group. */
637
+ function splitTopLevelCommas(body) {
638
+ const out = [];
639
+ let depth = 0;
640
+ let start = 0;
641
+ for (let i = 0; i <= body.length; i++) {
642
+ const ch = body[i];
643
+ if (ch === "(")
644
+ depth++;
645
+ else if (ch === ")")
646
+ depth = Math.max(0, depth - 1);
647
+ else if ((ch === "," && depth === 0) || i === body.length) {
648
+ out.push(body.slice(start, i));
649
+ start = i + 1;
650
+ }
651
+ }
652
+ return out;
653
+ }
654
+ /** First identifier of a column-definition piece, handling quoted forms. */
655
+ function firstIdentifier(piece) {
656
+ const m = piece.match(/^\s*(?:"([^"]+)"|`([^`]+)`|\[([^\]]+)\]|([A-Za-z_][A-Za-z0-9_$]*))/);
657
+ if (!m)
658
+ return null;
659
+ return m[1] ?? m[2] ?? m[3] ?? m[4] ?? null;
660
+ }
661
+ function isConstraintKeyword(id) {
662
+ return /^(primary|unique|check|foreign|constraint)$/i.test(id);
663
+ }
664
+ // ---------- public entry ----------
665
+ /**
666
+ * Open a SQLite database file for read-only access. Automatically loads the
667
+ * sibling `<db>-wal` file when present so active WAL-mode databases (e.g.
668
+ * ZCode's running store) return their latest committed data.
669
+ *
670
+ * Throws `SqliteParseError` if `mainPath` is not a SQLite file or is too
671
+ * corrupted to read the header. Adapters should catch and degrade.
672
+ */
673
+ export function openSqliteReadOnly(mainPath) {
674
+ const snapshot = captureSnapshot(mainPath);
675
+ const { mainBytes } = snapshot;
676
+ if (mainBytes.length < DB_HEADER_SIZE) {
677
+ throw new SqliteParseError(`db file too small: ${mainPath}`);
678
+ }
679
+ const header = parseDbHeader(mainBytes);
680
+ const wal = loadWal(snapshot.walBytes, header.pageSize, snapshot.walIndex);
681
+ const src = makePageSource(mainBytes, header, wal);
682
+ // Column-name cache: tableName → column list (or null).
683
+ const columnCache = new Map();
684
+ function readSqliteMaster() {
685
+ const tables = [];
686
+ visitTableBtree(src, 1, header.textEncoding, header, ({ values }) => {
687
+ // sqlite_master columns: type, name, tbl_name, rootpage, sql
688
+ const [type, name, _tbl, rootpage, sql] = values;
689
+ if (type === "table" &&
690
+ typeof name === "string" &&
691
+ typeof rootpage === "number") {
692
+ tables.push({ name, rootPgno: rootpage, sql: sql ?? "" });
693
+ }
694
+ });
695
+ return tables;
696
+ }
697
+ function columnsFor(table) {
698
+ const cached = columnCache.get(table.name);
699
+ if (cached !== undefined)
700
+ return cached;
701
+ const parsed = table.sql ? parseColumnNames(table.sql) : null;
702
+ columnCache.set(table.name, parsed);
703
+ return parsed;
704
+ }
705
+ /** Find the column index that is `INTEGER PRIMARY KEY` (the rowid alias), or
706
+ * -1 if none. SQLite stores NULL in that column's record slot and the real
707
+ * value in the cell rowid; we splice it back so callers see correct values. */
708
+ function rowidAliasIndex(table, columns) {
709
+ if (!table.sql || !columns)
710
+ return -1;
711
+ const open = table.sql.indexOf("(");
712
+ const close = table.sql.lastIndexOf(")");
713
+ if (open < 0 || close < 0)
714
+ return -1;
715
+ const body = table.sql.slice(open + 1, close);
716
+ const segments = splitTopLevelCommas(body);
717
+ let idx = 0;
718
+ for (const seg of segments) {
719
+ const trimmed = seg.trim();
720
+ const ident = firstIdentifier(trimmed);
721
+ if (!ident || isConstraintKeyword(ident))
722
+ continue;
723
+ // Match "INTEGER PRIMARY KEY" anywhere in this column definition (case-
724
+ // insensitive). The trailing \b guards against a false positive on
725
+ // something like `PRIMARY KEYDESC`. This is a heuristic; WITHOUT ROWID
726
+ // and other edge cases are out of scope (ZCode tables don't use them).
727
+ if (/integer\s+primary\s+key\b/i.test(trimmed)) {
728
+ return idx;
729
+ }
730
+ idx++;
731
+ }
732
+ return -1;
733
+ }
734
+ return {
735
+ dbPath: mainPath,
736
+ listTables() {
737
+ return readSqliteMaster();
738
+ },
739
+ scanTable(tableName, predicate) {
740
+ let table;
741
+ try {
742
+ table = readSqliteMaster().find((t) => t.name === tableName);
743
+ if (!table || table.rootPgno <= 0)
744
+ return [];
745
+ const columns = columnsFor(table);
746
+ const aliasIdx = rowidAliasIndex(table, columns);
747
+ const rows = [];
748
+ visitTableBtree(src, table.rootPgno, header.textEncoding, header, ({ rowid, values }) => {
749
+ const row = {};
750
+ for (let i = 0; i < values.length; i++) {
751
+ const key = columns?.[i] ?? `col${i}`;
752
+ // INTEGER PRIMARY KEY columns store NULL in the record; the real
753
+ // value is the cell rowid. Splice it in so callers see the integer.
754
+ if (i === aliasIdx && values[i] === null) {
755
+ row[key] = rowid;
756
+ }
757
+ else {
758
+ row[key] = values[i];
759
+ }
760
+ }
761
+ if (!predicate || predicate(row))
762
+ rows.push(row);
763
+ });
764
+ return rows;
765
+ }
766
+ catch (e) {
767
+ // Wrap any non-SqliteParseError (e.g. RangeError from a truncated
768
+ // float64, TypeError from a bad cell pointer) so the adapter's single
769
+ // catch on SqliteParseError degrades cleanly instead of crashing CLI.
770
+ throw e instanceof SqliteParseError
771
+ ? e
772
+ : new SqliteParseError(`failed reading table "${tableName}"`, e);
773
+ }
774
+ },
775
+ close() {
776
+ // Nothing to release — we read the whole file into memory at open time.
777
+ },
778
+ };
779
+ }
780
+ /** Convenience: open + scan + close in one call. Returns [] on any parse error
781
+ * or missing table, never throws. Useful for adapters that already decided to
782
+ * degrade silently. */
783
+ export function scanTableSafe(mainPath, tableName) {
784
+ try {
785
+ const db = openSqliteReadOnly(mainPath);
786
+ try {
787
+ return db.scanTable(tableName);
788
+ }
789
+ finally {
790
+ db.close();
791
+ }
792
+ }
793
+ catch {
794
+ return [];
795
+ }
796
+ }
797
+ //# sourceMappingURL=sqlite-readonly.js.map