orez 0.1.36 → 0.1.38

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 (130) hide show
  1. package/dist/cli-entry.js +0 -0
  2. package/dist/cli.js +7 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +1 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +14 -11
  10. package/dist/index.js.map +1 -1
  11. package/dist/pg-proxy.d.ts.map +1 -1
  12. package/dist/pg-proxy.js +8 -4
  13. package/dist/pg-proxy.js.map +1 -1
  14. package/dist/pglite-manager.d.ts +12 -0
  15. package/dist/pglite-manager.d.ts.map +1 -1
  16. package/dist/pglite-manager.js +81 -0
  17. package/dist/pglite-manager.js.map +1 -1
  18. package/dist/recovery.js +2 -2
  19. package/dist/recovery.js.map +1 -1
  20. package/dist/replication/change-tracker.js +9 -9
  21. package/dist/replication/change-tracker.js.map +1 -1
  22. package/dist/replication/handler.d.ts +12 -0
  23. package/dist/replication/handler.d.ts.map +1 -1
  24. package/dist/replication/handler.js +34 -6
  25. package/dist/replication/handler.js.map +1 -1
  26. package/dist/worker/browser-build-config.d.ts +59 -0
  27. package/dist/worker/browser-build-config.d.ts.map +1 -0
  28. package/dist/worker/browser-build-config.js +101 -0
  29. package/dist/worker/browser-build-config.js.map +1 -0
  30. package/dist/worker/browser-embed.d.ts +58 -0
  31. package/dist/worker/browser-embed.d.ts.map +1 -0
  32. package/dist/worker/browser-embed.js +195 -0
  33. package/dist/worker/browser-embed.js.map +1 -0
  34. package/dist/worker/cf-patches.d.ts +20 -0
  35. package/dist/worker/cf-patches.d.ts.map +1 -0
  36. package/dist/worker/cf-patches.js +94 -0
  37. package/dist/worker/cf-patches.js.map +1 -0
  38. package/dist/worker/index.d.ts +12 -0
  39. package/dist/worker/index.d.ts.map +1 -0
  40. package/dist/worker/index.js +105 -0
  41. package/dist/worker/index.js.map +1 -0
  42. package/dist/worker/shims/fastify.d.ts +80 -0
  43. package/dist/worker/shims/fastify.d.ts.map +1 -0
  44. package/dist/worker/shims/fastify.js +223 -0
  45. package/dist/worker/shims/fastify.js.map +1 -0
  46. package/dist/worker/shims/http-service.d.ts +104 -0
  47. package/dist/worker/shims/http-service.d.ts.map +1 -0
  48. package/dist/worker/shims/http-service.js +198 -0
  49. package/dist/worker/shims/http-service.js.map +1 -0
  50. package/dist/worker/shims/node-stub.d.ts +147 -0
  51. package/dist/worker/shims/node-stub.d.ts.map +1 -0
  52. package/dist/worker/shims/node-stub.js +204 -0
  53. package/dist/worker/shims/node-stub.js.map +1 -0
  54. package/dist/worker/shims/postgres.d.ts +115 -0
  55. package/dist/worker/shims/postgres.d.ts.map +1 -0
  56. package/dist/worker/shims/postgres.js +1181 -0
  57. package/dist/worker/shims/postgres.js.map +1 -0
  58. package/dist/worker/shims/sqlite-browser.d.ts +54 -0
  59. package/dist/worker/shims/sqlite-browser.d.ts.map +1 -0
  60. package/dist/worker/shims/sqlite-browser.js +144 -0
  61. package/dist/worker/shims/sqlite-browser.js.map +1 -0
  62. package/dist/worker/shims/sqlite.d.ts +126 -0
  63. package/dist/worker/shims/sqlite.d.ts.map +1 -0
  64. package/dist/worker/shims/sqlite.js +599 -0
  65. package/dist/worker/shims/sqlite.js.map +1 -0
  66. package/dist/worker/shims/stream-browser.d.ts +9 -0
  67. package/dist/worker/shims/stream-browser.d.ts.map +1 -0
  68. package/dist/worker/shims/stream-browser.js +13 -0
  69. package/dist/worker/shims/stream-browser.js.map +1 -0
  70. package/dist/worker/shims/ws-browser.d.ts +50 -0
  71. package/dist/worker/shims/ws-browser.d.ts.map +1 -0
  72. package/dist/worker/shims/ws-browser.js +105 -0
  73. package/dist/worker/shims/ws-browser.js.map +1 -0
  74. package/dist/worker/shims/ws.d.ts +62 -0
  75. package/dist/worker/shims/ws.d.ts.map +1 -0
  76. package/dist/worker/shims/ws.js +310 -0
  77. package/dist/worker/shims/ws.js.map +1 -0
  78. package/dist/worker/types.d.ts +57 -0
  79. package/dist/worker/types.d.ts.map +1 -0
  80. package/dist/worker/types.js +9 -0
  81. package/dist/worker/types.js.map +1 -0
  82. package/dist/worker/zero-cache-embed-cf.d.ts +63 -0
  83. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -0
  84. package/dist/worker/zero-cache-embed-cf.js +268 -0
  85. package/dist/worker/zero-cache-embed-cf.js.map +1 -0
  86. package/dist/worker/zero-cache-embed.d.ts +66 -0
  87. package/dist/worker/zero-cache-embed.d.ts.map +1 -0
  88. package/dist/worker/zero-cache-embed.js +200 -0
  89. package/dist/worker/zero-cache-embed.js.map +1 -0
  90. package/package.json +62 -3
  91. package/src/cli-entry.ts +0 -0
  92. package/src/cli.ts +8 -1
  93. package/src/config.ts +2 -0
  94. package/src/index.ts +15 -10
  95. package/src/integration/integration.test.ts +1 -1
  96. package/src/integration/restore-live-stress.test.ts +2 -2
  97. package/src/pg-proxy.ts +9 -4
  98. package/src/pglite-manager.ts +111 -0
  99. package/src/recovery.ts +2 -2
  100. package/src/replication/change-tracker.test.ts +1 -1
  101. package/src/replication/change-tracker.ts +9 -9
  102. package/src/replication/handler.test.ts +37 -0
  103. package/src/replication/handler.ts +46 -6
  104. package/src/wasm-sqlite.test.ts +2 -1
  105. package/src/worker/browser-build-config.test.ts +59 -0
  106. package/src/worker/browser-build-config.ts +105 -0
  107. package/src/worker/browser-embed.ts +306 -0
  108. package/src/worker/cf-patches.ts +114 -0
  109. package/src/worker/embed-integration.test.ts +321 -0
  110. package/src/worker/index.ts +138 -0
  111. package/src/worker/shims/fastify.test.ts +255 -0
  112. package/src/worker/shims/fastify.ts +292 -0
  113. package/src/worker/shims/http-service.test.ts +355 -0
  114. package/src/worker/shims/http-service.ts +293 -0
  115. package/src/worker/shims/node-stub.ts +223 -0
  116. package/src/worker/shims/postgres.test.ts +364 -0
  117. package/src/worker/shims/postgres.ts +1434 -0
  118. package/src/worker/shims/sqlite-browser.test.ts +233 -0
  119. package/src/worker/shims/sqlite-browser.ts +178 -0
  120. package/src/worker/shims/sqlite.test.ts +641 -0
  121. package/src/worker/shims/sqlite.ts +731 -0
  122. package/src/worker/shims/ws-browser.test.ts +184 -0
  123. package/src/worker/shims/ws-browser.ts +125 -0
  124. package/src/worker/shims/ws.test.ts +288 -0
  125. package/src/worker/shims/ws.ts +367 -0
  126. package/src/worker/types.ts +75 -0
  127. package/src/worker/worker-integration.test.ts +223 -0
  128. package/src/worker/worker.test.ts +136 -0
  129. package/src/worker/zero-cache-embed-cf.ts +367 -0
  130. package/src/worker/zero-cache-embed.ts +277 -0
@@ -0,0 +1,641 @@
1
+ /**
2
+ * sqlite shim tests.
3
+ *
4
+ * uses a mock SqlStorageLike backed by better-sqlite3 to validate that our
5
+ * shim correctly bridges between the better-sqlite3 api and DO SqlStorage.
6
+ */
7
+
8
+ // @ts-expect-error - CJS module
9
+ import BedrockSqlite from 'bedrock-sqlite'
10
+ const BetterSqlite3 = BedrockSqlite.Database
11
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
12
+
13
+ import {
14
+ Database,
15
+ Statement,
16
+ StatementRunner,
17
+ SqliteError,
18
+ type SqlStorageLike,
19
+ type SqlStorageCursor,
20
+ type SqlStorageValue,
21
+ } from './sqlite.js'
22
+
23
+ // -- mock SqlStorageLike backed by better-sqlite3 --
24
+
25
+ function createMockSqlStorage(): SqlStorageLike {
26
+ const nativeDb = new BetterSqlite3(':memory:')
27
+
28
+ return {
29
+ exec(query: string, ...bindings: SqlStorageValue[]): SqlStorageCursor {
30
+ const trimmed = query.trim()
31
+
32
+ // handle statements that don't return rows
33
+ const isWrite =
34
+ /^(INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|BEGIN|COMMIT|ROLLBACK|PRAGMA|SAVEPOINT|RELEASE|VACUUM)/i.test(
35
+ trimmed
36
+ )
37
+
38
+ if (isWrite && !trimmed.toUpperCase().startsWith('PRAGMA')) {
39
+ const stmt = nativeDb.prepare(query)
40
+ const result = stmt.run(...bindings)
41
+ return {
42
+ toArray: () => [],
43
+ get rowsRead() {
44
+ return 0
45
+ },
46
+ get rowsWritten() {
47
+ return result.changes
48
+ },
49
+ get columnNames() {
50
+ return []
51
+ },
52
+ }
53
+ }
54
+
55
+ // for pragmas — some are SET (contain =), some are GET
56
+ if (trimmed.toUpperCase().startsWith('PRAGMA')) {
57
+ try {
58
+ const stmt = nativeDb.prepare(query)
59
+ const rows = stmt.all(...bindings)
60
+ return {
61
+ toArray: () => rows as Record<string, SqlStorageValue>[],
62
+ get rowsRead() {
63
+ return rows.length
64
+ },
65
+ get rowsWritten() {
66
+ return 0
67
+ },
68
+ get columnNames() {
69
+ if (rows.length > 0) return Object.keys(rows[0] as object)
70
+ return []
71
+ },
72
+ }
73
+ } catch {
74
+ // pragma that modifies state (e.g., journal_mode = WAL)
75
+ nativeDb.pragma(query.replace(/^PRAGMA\s+/i, ''))
76
+ return {
77
+ toArray: () => [],
78
+ get rowsRead() {
79
+ return 0
80
+ },
81
+ get rowsWritten() {
82
+ return 0
83
+ },
84
+ get columnNames() {
85
+ return []
86
+ },
87
+ }
88
+ }
89
+ }
90
+
91
+ // select / other read queries
92
+ const stmt = nativeDb.prepare(query)
93
+ const rows = stmt.all(...bindings)
94
+ const columns = stmt.columns().map((c: { name: string }) => c.name)
95
+ return {
96
+ toArray: () => rows as Record<string, SqlStorageValue>[],
97
+ get rowsRead() {
98
+ return rows.length
99
+ },
100
+ get rowsWritten() {
101
+ return 0
102
+ },
103
+ get columnNames() {
104
+ return columns
105
+ },
106
+ }
107
+ },
108
+ // expose native db for cleanup
109
+ _nativeDb: nativeDb,
110
+ } as SqlStorageLike & { _nativeDb: typeof nativeDb }
111
+ }
112
+
113
+ // -- tests --
114
+
115
+ describe('SqliteError', () => {
116
+ it('creates error with message and code', () => {
117
+ const err = new SqliteError('table not found', 'SQLITE_ERROR')
118
+ expect(err).toBeInstanceOf(Error)
119
+ expect(err).toBeInstanceOf(SqliteError)
120
+ expect(err.message).toBe('table not found')
121
+ expect(err.code).toBe('SQLITE_ERROR')
122
+ expect(err.name).toBe('SqliteError')
123
+ })
124
+
125
+ it('captures stack trace', () => {
126
+ const err = new SqliteError('test', 'SQLITE_MISUSE')
127
+ expect(err.stack).toBeDefined()
128
+ expect(err.stack).toContain('SqliteError')
129
+ })
130
+ })
131
+
132
+ describe('Database', () => {
133
+ let mock: SqlStorageLike & { _nativeDb: any }
134
+ let db: Database
135
+
136
+ beforeEach(() => {
137
+ mock = createMockSqlStorage() as SqlStorageLike & { _nativeDb: any }
138
+ db = new Database(mock)
139
+ })
140
+
141
+ afterEach(() => {
142
+ mock._nativeDb.close()
143
+ })
144
+
145
+ it('constructs with SqlStorageLike', () => {
146
+ expect(db.open).toBe(true)
147
+ expect(db.name).toBe(':do-storage:')
148
+ expect(db.inTransaction).toBe(false)
149
+ })
150
+
151
+ it('throws when constructed with a string and no globalThis storage', () => {
152
+ // clear any global DO storage
153
+ const prev = (globalThis as any).__orez_do_sqlite
154
+ delete (globalThis as any).__orez_do_sqlite
155
+ try {
156
+ expect(() => new Database('/path/to/db' as unknown as SqlStorageLike)).toThrow(
157
+ 'no DO storage on globalThis.__orez_do_sqlite'
158
+ )
159
+ } finally {
160
+ if (prev) (globalThis as any).__orez_do_sqlite = prev
161
+ }
162
+ })
163
+
164
+ it('close sets open to false', () => {
165
+ expect(db.open).toBe(true)
166
+ const ret = db.close()
167
+ expect(db.open).toBe(false)
168
+ expect(ret).toBe(db) // chainable
169
+ })
170
+
171
+ it('unsafeMode is a no-op that returns this', () => {
172
+ expect(db.unsafeMode(true)).toBe(db)
173
+ expect(db.unsafeMode(false)).toBe(db)
174
+ expect(db.unsafeMode()).toBe(db)
175
+ })
176
+
177
+ it('defaultSafeIntegers is a no-op that returns this', () => {
178
+ expect(db.defaultSafeIntegers(true)).toBe(db)
179
+ })
180
+ })
181
+
182
+ describe('Database.exec', () => {
183
+ let mock: SqlStorageLike & { _nativeDb: any }
184
+ let db: Database
185
+
186
+ beforeEach(() => {
187
+ mock = createMockSqlStorage() as SqlStorageLike & { _nativeDb: any }
188
+ db = new Database(mock)
189
+ })
190
+
191
+ afterEach(() => {
192
+ mock._nativeDb.close()
193
+ })
194
+
195
+ it('executes single statement', () => {
196
+ const ret = db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY)')
197
+ expect(ret).toBe(db) // chainable
198
+ })
199
+
200
+ it('executes multiple statements separated by semicolons', () => {
201
+ db.exec(
202
+ 'CREATE TABLE t (id INTEGER PRIMARY KEY); CREATE TABLE t2 (id INTEGER PRIMARY KEY)'
203
+ )
204
+ // verify both tables exist
205
+ const r1 = db
206
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='t'")
207
+ .get()
208
+ const r2 = db
209
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='t2'")
210
+ .get()
211
+ expect(r1).toEqual({ name: 't' })
212
+ expect(r2).toEqual({ name: 't2' })
213
+ })
214
+
215
+ it('throws on closed database', () => {
216
+ db.close()
217
+ expect(() => db.exec('SELECT 1')).toThrow('not open')
218
+ })
219
+ })
220
+
221
+ describe('Database.prepare / Statement', () => {
222
+ let mock: SqlStorageLike & { _nativeDb: any }
223
+ let db: Database
224
+
225
+ beforeEach(() => {
226
+ mock = createMockSqlStorage() as SqlStorageLike & { _nativeDb: any }
227
+ db = new Database(mock)
228
+ db.exec('CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT, val INTEGER)')
229
+ })
230
+
231
+ afterEach(() => {
232
+ mock._nativeDb.close()
233
+ })
234
+
235
+ it('prepare returns a Statement', () => {
236
+ const stmt = db.prepare('SELECT 1 as x')
237
+ expect(stmt).toBeInstanceOf(Statement)
238
+ expect(stmt.source).toBe('SELECT 1 as x')
239
+ })
240
+
241
+ it('throws prepare on closed db', () => {
242
+ db.close()
243
+ expect(() => db.prepare('SELECT 1')).toThrow('not open')
244
+ })
245
+
246
+ it('run executes write and returns RunResult', () => {
247
+ const result = db
248
+ .prepare('INSERT INTO items (name, val) VALUES (?, ?)')
249
+ .run('foo', 42)
250
+ expect(result).toHaveProperty('changes')
251
+ expect(result).toHaveProperty('lastInsertRowid')
252
+ expect(result.changes).toBe(1)
253
+ })
254
+
255
+ it('get returns first row or undefined', () => {
256
+ db.prepare('INSERT INTO items (name, val) VALUES (?, ?)').run('a', 1)
257
+ db.prepare('INSERT INTO items (name, val) VALUES (?, ?)').run('b', 2)
258
+
259
+ const row = db.prepare('SELECT * FROM items WHERE name = ?').get('a')
260
+ expect(row).toEqual({ id: 1, name: 'a', val: 1 })
261
+
262
+ const missing = db.prepare('SELECT * FROM items WHERE name = ?').get('zzz')
263
+ expect(missing).toBeUndefined()
264
+ })
265
+
266
+ it('all returns all rows', () => {
267
+ db.prepare('INSERT INTO items (name, val) VALUES (?, ?)').run('x', 10)
268
+ db.prepare('INSERT INTO items (name, val) VALUES (?, ?)').run('y', 20)
269
+
270
+ const rows = db.prepare('SELECT * FROM items ORDER BY id').all()
271
+ expect(rows).toEqual([
272
+ { id: 1, name: 'x', val: 10 },
273
+ { id: 2, name: 'y', val: 20 },
274
+ ])
275
+ })
276
+
277
+ it('all returns empty array when no rows', () => {
278
+ const rows = db.prepare('SELECT * FROM items').all()
279
+ expect(rows).toEqual([])
280
+ })
281
+
282
+ it('iterate yields rows lazily', () => {
283
+ db.prepare('INSERT INTO items (name, val) VALUES (?, ?)').run('a', 1)
284
+ db.prepare('INSERT INTO items (name, val) VALUES (?, ?)').run('b', 2)
285
+
286
+ const iter = db.prepare('SELECT * FROM items ORDER BY id').iterate()
287
+ const results: unknown[] = []
288
+ for (const row of iter) {
289
+ results.push(row)
290
+ }
291
+ expect(results).toEqual([
292
+ { id: 1, name: 'a', val: 1 },
293
+ { id: 2, name: 'b', val: 2 },
294
+ ])
295
+ })
296
+
297
+ it('safeIntegers is a no-op returning this', () => {
298
+ const stmt = db.prepare('SELECT 1 as x')
299
+ expect(stmt.safeIntegers(true)).toBe(stmt)
300
+ expect(stmt.safeIntegers()).toBe(stmt)
301
+ })
302
+
303
+ it('statement methods throw on closed db', () => {
304
+ const stmt = db.prepare('SELECT 1 as x')
305
+ db.close()
306
+ expect(() => stmt.run()).toThrow('not open')
307
+ expect(() => stmt.get()).toThrow('not open')
308
+ expect(() => stmt.all()).toThrow('not open')
309
+ })
310
+ })
311
+
312
+ describe('Database.pragma', () => {
313
+ let mock: SqlStorageLike & { _nativeDb: any }
314
+ let db: Database
315
+
316
+ beforeEach(() => {
317
+ mock = createMockSqlStorage() as SqlStorageLike & { _nativeDb: any }
318
+ db = new Database(mock)
319
+ })
320
+
321
+ afterEach(() => {
322
+ mock._nativeDb.close()
323
+ })
324
+
325
+ it('reads a pragma and returns array of objects', () => {
326
+ const result = db.pragma('journal_mode')
327
+ expect(Array.isArray(result)).toBe(true)
328
+ expect((result as unknown[]).length).toBeGreaterThan(0)
329
+ })
330
+
331
+ it('reads pragma with simple option', () => {
332
+ const result = db.pragma('journal_mode', { simple: true })
333
+ // should return a scalar value, not an array
334
+ expect(Array.isArray(result)).toBe(false)
335
+ expect(typeof result).toBe('string')
336
+ })
337
+
338
+ it('sets a pragma', () => {
339
+ const result = db.pragma('cache_size = 2000')
340
+ // set pragmas return the new value or empty
341
+ expect(Array.isArray(result)).toBe(true)
342
+ })
343
+
344
+ it('skips optimize pragma', () => {
345
+ const result = db.pragma('optimize')
346
+ expect(result).toEqual([])
347
+
348
+ const simple = db.pragma('optimize', { simple: true })
349
+ expect(simple).toBeUndefined()
350
+ })
351
+
352
+ it('throws on closed db', () => {
353
+ db.close()
354
+ expect(() => db.pragma('journal_mode')).toThrow('not open')
355
+ })
356
+ })
357
+
358
+ describe('Database.transaction', () => {
359
+ let mock: SqlStorageLike & { _nativeDb: any }
360
+ let db: Database
361
+
362
+ beforeEach(() => {
363
+ mock = createMockSqlStorage() as SqlStorageLike & { _nativeDb: any }
364
+ db = new Database(mock)
365
+ db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY)')
366
+ })
367
+
368
+ afterEach(() => {
369
+ mock._nativeDb.close()
370
+ })
371
+
372
+ it('commits on success', () => {
373
+ const txn = db.transaction(() => {
374
+ db.prepare('INSERT INTO t VALUES (1)').run()
375
+ db.prepare('INSERT INTO t VALUES (2)').run()
376
+ })
377
+
378
+ txn()
379
+
380
+ const rows = db.prepare('SELECT * FROM t').all()
381
+ expect(rows).toEqual([{ id: 1 }, { id: 2 }])
382
+ })
383
+
384
+ it('rolls back on error', () => {
385
+ const txn = db.transaction(() => {
386
+ db.prepare('INSERT INTO t VALUES (1)').run()
387
+ throw new Error('boom')
388
+ })
389
+
390
+ expect(() => txn()).toThrow('boom')
391
+
392
+ const rows = db.prepare('SELECT * FROM t').all()
393
+ expect(rows).toEqual([])
394
+ })
395
+
396
+ it('inTransaction reflects state', () => {
397
+ expect(db.inTransaction).toBe(false)
398
+
399
+ db.transaction(() => {
400
+ expect(db.inTransaction).toBe(true)
401
+ })()
402
+
403
+ expect(db.inTransaction).toBe(false)
404
+ })
405
+
406
+ it('inTransaction is false after rollback', () => {
407
+ try {
408
+ db.transaction(() => {
409
+ throw new Error('fail')
410
+ })()
411
+ } catch {}
412
+
413
+ expect(db.inTransaction).toBe(false)
414
+ })
415
+
416
+ it('has deferred/immediate/exclusive variants', () => {
417
+ const txn = db.transaction(() => {
418
+ db.prepare('INSERT INTO t VALUES (99)').run()
419
+ })
420
+
421
+ expect(typeof txn.deferred).toBe('function')
422
+ expect(typeof txn.immediate).toBe('function')
423
+ expect(typeof txn.exclusive).toBe('function')
424
+ })
425
+
426
+ it('immediate variant works', () => {
427
+ const txn = db.transaction(() => {
428
+ db.prepare('INSERT INTO t VALUES (42)').run()
429
+ })
430
+
431
+ txn.immediate()
432
+
433
+ const rows = db.prepare('SELECT * FROM t').all()
434
+ expect(rows).toEqual([{ id: 42 }])
435
+ })
436
+
437
+ it('nested transaction calls run fn without extra BEGIN', () => {
438
+ const inner = db.transaction(() => {
439
+ db.prepare('INSERT INTO t VALUES (2)').run()
440
+ })
441
+
442
+ const outer = db.transaction(() => {
443
+ db.prepare('INSERT INTO t VALUES (1)').run()
444
+ inner() // should NOT issue another BEGIN
445
+ db.prepare('INSERT INTO t VALUES (3)').run()
446
+ })
447
+
448
+ outer()
449
+
450
+ const rows = db.prepare('SELECT * FROM t ORDER BY id').all()
451
+ expect(rows).toEqual([{ id: 1 }, { id: 2 }, { id: 3 }])
452
+ })
453
+
454
+ it('passes arguments through to wrapped function', () => {
455
+ const txn = db.transaction((id: unknown, val: unknown) => {
456
+ db.prepare('INSERT INTO t VALUES (?)').run(id)
457
+ return val
458
+ })
459
+
460
+ const result = txn(7, 'hello')
461
+ expect(result).toBe('hello')
462
+ expect(db.prepare('SELECT * FROM t').all()).toEqual([{ id: 7 }])
463
+ })
464
+ })
465
+
466
+ describe('StatementRunner', () => {
467
+ let mock: SqlStorageLike & { _nativeDb: any }
468
+ let db: Database
469
+ let runner: StatementRunner
470
+
471
+ beforeEach(() => {
472
+ mock = createMockSqlStorage() as SqlStorageLike & { _nativeDb: any }
473
+ db = new Database(mock)
474
+ db.exec('CREATE TABLE foo (id INTEGER PRIMARY KEY, name TEXT)')
475
+ runner = new StatementRunner(db)
476
+ })
477
+
478
+ afterEach(() => {
479
+ mock._nativeDb.close()
480
+ })
481
+
482
+ it('run inserts data', () => {
483
+ const result = runner.run('INSERT INTO foo (name) VALUES (?)', 'bar')
484
+ expect(result).toHaveProperty('changes')
485
+ expect(result.changes).toBe(1)
486
+ })
487
+
488
+ it('get returns single row', () => {
489
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'baz')
490
+ const row = runner.get('SELECT * FROM foo WHERE name = ?', 'baz')
491
+ expect(row).toEqual({ id: 1, name: 'baz' })
492
+ })
493
+
494
+ it('get returns undefined for no match', () => {
495
+ const row = runner.get('SELECT * FROM foo WHERE name = ?', 'nope')
496
+ expect(row).toBeUndefined()
497
+ })
498
+
499
+ it('all returns all rows', () => {
500
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'a')
501
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'b')
502
+ const rows = runner.all('SELECT * FROM foo ORDER BY id')
503
+ expect(rows).toEqual([
504
+ { id: 1, name: 'a' },
505
+ { id: 2, name: 'b' },
506
+ ])
507
+ })
508
+
509
+ it('begin/commit transaction', () => {
510
+ runner.begin()
511
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'x')
512
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'y')
513
+ runner.commit()
514
+
515
+ expect(runner.all('SELECT * FROM foo ORDER BY id')).toEqual([
516
+ { id: 1, name: 'x' },
517
+ { id: 2, name: 'y' },
518
+ ])
519
+ })
520
+
521
+ it('begin/rollback discards changes', () => {
522
+ runner.begin()
523
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'x')
524
+ runner.rollback()
525
+
526
+ expect(runner.all('SELECT * FROM foo')).toEqual([])
527
+ })
528
+
529
+ it('beginConcurrent maps to regular BEGIN', () => {
530
+ runner.beginConcurrent()
531
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'concurrent')
532
+ runner.commit()
533
+
534
+ expect(runner.all('SELECT * FROM foo')).toEqual([{ id: 1, name: 'concurrent' }])
535
+ })
536
+
537
+ it('beginImmediate works', () => {
538
+ runner.beginImmediate()
539
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'immediate')
540
+ runner.commit()
541
+
542
+ expect(runner.all('SELECT * FROM foo')).toEqual([{ id: 1, name: 'immediate' }])
543
+ })
544
+
545
+ it('caches prepared statements', () => {
546
+ // run same SQL multiple times — statements should be reused
547
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'a')
548
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'b')
549
+ runner.run('INSERT INTO foo (name) VALUES (?)', 'c')
550
+
551
+ expect(runner.all('SELECT count(*) as c FROM foo')).toEqual([{ c: 3 }])
552
+ })
553
+ })
554
+
555
+ describe('StatementRunner: zero-cache replicator pattern', () => {
556
+ let mock: SqlStorageLike & { _nativeDb: any }
557
+ let db: Database
558
+ let runner: StatementRunner
559
+
560
+ beforeEach(() => {
561
+ mock = createMockSqlStorage() as SqlStorageLike & { _nativeDb: any }
562
+ db = new Database(mock)
563
+ db.exec(`
564
+ CREATE TABLE issues (
565
+ issueID INTEGER PRIMARY KEY,
566
+ title TEXT,
567
+ _0_version TEXT
568
+ )
569
+ `)
570
+ runner = new StatementRunner(db)
571
+ })
572
+
573
+ afterEach(() => {
574
+ mock._nativeDb.close()
575
+ })
576
+
577
+ it('simulates zero-cache batch processing', () => {
578
+ // batch 1
579
+ runner.begin()
580
+ runner.run(
581
+ 'INSERT INTO issues (issueID, title, _0_version) VALUES (?, ?, ?)',
582
+ 1,
583
+ 'bug',
584
+ '01'
585
+ )
586
+ runner.run(
587
+ 'INSERT INTO issues (issueID, title, _0_version) VALUES (?, ?, ?)',
588
+ 2,
589
+ 'feat',
590
+ '01'
591
+ )
592
+ runner.commit()
593
+
594
+ expect(runner.all('SELECT * FROM issues ORDER BY issueID')).toEqual([
595
+ { issueID: 1, title: 'bug', _0_version: '01' },
596
+ { issueID: 2, title: 'feat', _0_version: '01' },
597
+ ])
598
+
599
+ // batch 2
600
+ runner.begin()
601
+ runner.run(
602
+ 'INSERT OR REPLACE INTO issues (issueID, title, _0_version) VALUES (?, ?, ?)',
603
+ 1,
604
+ 'bug fix',
605
+ '02'
606
+ )
607
+ runner.commit()
608
+
609
+ expect(runner.get('SELECT title FROM issues WHERE issueID = ?', 1)).toEqual({
610
+ title: 'bug fix',
611
+ })
612
+ })
613
+
614
+ it('simulates rollback on conflict', () => {
615
+ runner.begin()
616
+ runner.run(
617
+ 'INSERT INTO issues (issueID, title, _0_version) VALUES (?, ?, ?)',
618
+ 1,
619
+ 'ok',
620
+ '01'
621
+ )
622
+ runner.commit()
623
+
624
+ runner.begin()
625
+ try {
626
+ runner.run(
627
+ 'INSERT INTO issues (issueID, title, _0_version) VALUES (?, ?, ?)',
628
+ 1,
629
+ 'dupe',
630
+ '02'
631
+ )
632
+ } catch {
633
+ runner.rollback()
634
+ }
635
+
636
+ // original row should still be there
637
+ expect(runner.get('SELECT title FROM issues WHERE issueID = ?', 1)).toEqual({
638
+ title: 'ok',
639
+ })
640
+ })
641
+ })