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,731 @@
1
+ // NOTE THIS IS NOT OREZ NODE THIS IS NOT A GOOD REFERENCE BECAUSE ITS OUR EARLY GUESS AT WHAT COULD WORK
2
+ // DO NOT STUDY THIS, THE OTHER STUFF IN SRC IS WHERE YOU EANT TO LOOK
3
+
4
+ /**
5
+ * sqlite shim for cloudflare durable objects.
6
+ *
7
+ * wraps the DO SqlStorage interface (`this.ctx.storage.sql`) to implement
8
+ * the better-sqlite3 / @rocicorp/zero-sqlite3 api that zero-cache uses.
9
+ *
10
+ * all operations are synchronous, matching both DO sqlite and better-sqlite3.
11
+ *
12
+ * usage in a durable object:
13
+ *
14
+ * import { Database } from 'orez/worker/shims/sqlite'
15
+ *
16
+ * export class MyDO extends DurableObject {
17
+ * db: Database
18
+ * constructor(ctx: DurableObjectState, env: Env) {
19
+ * super(ctx, env)
20
+ * this.db = new Database(ctx.storage.sql)
21
+ * }
22
+ * }
23
+ */
24
+
25
+ // -- abstract interface for DO SqlStorage --
26
+
27
+ export type SqlStorageValue = string | number | null | ArrayBuffer
28
+
29
+ export interface SqlStorageCursor {
30
+ toArray(): Record<string, SqlStorageValue>[]
31
+ readonly rowsRead: number
32
+ readonly rowsWritten: number
33
+ readonly columnNames: string[]
34
+ }
35
+
36
+ export interface SqlStorageLike {
37
+ exec(query: string, ...bindings: SqlStorageValue[]): SqlStorageCursor
38
+ /** DO transaction API — if available, used instead of raw BEGIN/COMMIT */
39
+ transactionSync?<T>(fn: () => T): T
40
+ }
41
+
42
+ // -- SqliteError --
43
+
44
+ export class SqliteError extends Error {
45
+ code: string
46
+
47
+ constructor(message: string, code: string) {
48
+ super(message)
49
+ this.name = 'SqliteError'
50
+ this.code = code
51
+ }
52
+ }
53
+
54
+ // -- RunResult --
55
+
56
+ export interface RunResult {
57
+ changes: number
58
+ lastInsertRowid: number | bigint
59
+ }
60
+
61
+ // -- parameter serialization --
62
+ // sqlite only accepts scalar values; serialize objects/booleans/dates/etc.
63
+ // special case: a single object argument means named parameters (@key syntax)
64
+ // convert named params (@key) in SQL to positional (?) and extract values in order.
65
+ // CF DO SqlStorage doesn't support @key syntax, only ? placeholders.
66
+ function convertNamedParams(
67
+ sql: string,
68
+ params: Record<string, unknown>
69
+ ): { sql: string; values: SqlStorageValue[] } {
70
+ const values: SqlStorageValue[] = []
71
+ // each @param occurrence gets its own ? placeholder and value
72
+ const converted = sql.replace(/@(\w+)/g, (_, name) => {
73
+ values.push(serializeValue(params[name]))
74
+ return '?'
75
+ })
76
+ return { sql: converted, values }
77
+ }
78
+
79
+ function serializeValue(p: unknown): SqlStorageValue {
80
+ if (p === null || p === undefined) return null
81
+ if (typeof p === 'string' || typeof p === 'number') return p
82
+ if (typeof p === 'boolean') return p ? 1 : 0
83
+ if (typeof p === 'bigint') return Number(p)
84
+ if (p instanceof ArrayBuffer || p instanceof Uint8Array) return p as any
85
+ if (typeof p === 'object') return JSON.stringify(p)
86
+ return String(p)
87
+ }
88
+
89
+ function serializeSqliteParams(params: unknown[]): SqlStorageValue[] {
90
+ // better-sqlite3 API: stmt.run([val1, val2, ...]) spreads the array
91
+ // as positional parameters. detect this: single array argument.
92
+ if (params.length === 1 && Array.isArray(params[0])) {
93
+ return serializeSqliteParams(params[0])
94
+ }
95
+
96
+ // named parameters: .run({key: value}) → extract values matching @key placeholders
97
+ // handled at the exec level via convertNamedParams, return as marker here
98
+ if (
99
+ params.length === 1 &&
100
+ params[0] !== null &&
101
+ typeof params[0] === 'object' &&
102
+ !Array.isArray(params[0]) &&
103
+ !(params[0] instanceof ArrayBuffer) &&
104
+ !(params[0] instanceof Uint8Array)
105
+ ) {
106
+ return params as any // marker: the Statement methods detect this and convert
107
+ }
108
+
109
+ return params.map(serializeValue)
110
+ }
111
+
112
+ // -- Statement --
113
+
114
+ export class Statement<T = Record<string, SqlStorageValue>> {
115
+ readonly source: string
116
+ #sql: SqlStorageLike
117
+ #db: Database
118
+
119
+ constructor(sql: SqlStorageLike, db: Database, source: string) {
120
+ this.#sql = sql
121
+ this.#db = db
122
+ // auto-add IF NOT EXISTS to CREATE TABLE/INDEX (shared sqlite in browser)
123
+ this.source = source
124
+ .replace(/CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)/gi, 'CREATE TABLE IF NOT EXISTS ')
125
+ .replace(/CREATE\s+INDEX\s+(?!IF\s+NOT\s+EXISTS)/gi, 'CREATE INDEX IF NOT EXISTS ')
126
+ .replace(
127
+ /CREATE\s+UNIQUE\s+INDEX\s+(?!IF\s+NOT\s+EXISTS)/gi,
128
+ 'CREATE UNIQUE INDEX IF NOT EXISTS '
129
+ )
130
+ }
131
+
132
+ // intercept PRAGMAs that sql.js can't handle (wal2, etc.)
133
+ #interceptPragma(): { intercepted: boolean; result?: SqlStorageCursor } {
134
+ const upper = this.source.trimStart().toUpperCase()
135
+ if (!upper.startsWith('PRAGMA')) return { intercepted: false }
136
+
137
+ // PRAGMA journal_mode — always report wal2 (sql.js doesn't support WAL)
138
+ if (/PRAGMA\s+journal_mode\s*=/i.test(this.source)) {
139
+ const value = this.source.split('=')[1]?.trim().replace(/['"]/g, '') || 'wal2'
140
+ return {
141
+ intercepted: true,
142
+ result: {
143
+ toArray: () => [{ journal_mode: value }],
144
+ rowsRead: 1,
145
+ rowsWritten: 0,
146
+ columnNames: ['journal_mode'],
147
+ },
148
+ }
149
+ }
150
+ if (/PRAGMA\s+journal_mode\s*$/i.test(this.source)) {
151
+ return {
152
+ intercepted: true,
153
+ result: {
154
+ toArray: () => [{ journal_mode: 'wal2' }],
155
+ rowsRead: 1,
156
+ rowsWritten: 0,
157
+ columnNames: ['journal_mode'],
158
+ },
159
+ }
160
+ }
161
+ return { intercepted: false }
162
+ }
163
+
164
+ // resolve named params (@key) → positional (?), or return sql + values as-is
165
+ #resolveParams(params: unknown[]): { sql: string; values: SqlStorageValue[] } {
166
+ const serialized = serializeSqliteParams(params)
167
+ // detect named parameter marker: single non-array object
168
+ const first = serialized[0] as any
169
+ if (
170
+ serialized.length === 1 &&
171
+ first !== null &&
172
+ typeof first === 'object' &&
173
+ !Array.isArray(first) &&
174
+ !(first instanceof ArrayBuffer) &&
175
+ !(first instanceof Uint8Array)
176
+ ) {
177
+ return convertNamedParams(this.source, first as Record<string, unknown>)
178
+ }
179
+ return { sql: this.source, values: serialized }
180
+ }
181
+
182
+ run(...params: unknown[]): RunResult {
183
+ if (!this.#db.open) {
184
+ throw new SqliteError('The database connection is not open', 'SQLITE_MISUSE')
185
+ }
186
+ const pragma = this.#interceptPragma()
187
+ if (pragma.intercepted) return { changes: 0, lastInsertRowid: 0 }
188
+
189
+ const upper = this.source.trimStart().toUpperCase()
190
+ const isTxCmd =
191
+ upper.startsWith('BEGIN') ||
192
+ upper.startsWith('COMMIT') ||
193
+ upper.startsWith('ROLLBACK') ||
194
+ upper === 'END' ||
195
+ upper.startsWith('END ')
196
+ const { sql, values } = this.#resolveParams(params)
197
+ const cursor =
198
+ isTxCmd && values.length === 0
199
+ ? this.#db._execTransactionAware(sql, this.#sql)
200
+ : this.#sql.exec(sql, ...values)
201
+ return {
202
+ changes: cursor.rowsWritten,
203
+ lastInsertRowid: 0,
204
+ }
205
+ }
206
+
207
+ get(...params: unknown[]): T | undefined {
208
+ if (!this.#db.open) {
209
+ throw new SqliteError('The database connection is not open', 'SQLITE_MISUSE')
210
+ }
211
+ const pragma = this.#interceptPragma()
212
+ if (pragma.intercepted && pragma.result) {
213
+ return pragma.result.toArray()[0] as T
214
+ }
215
+
216
+ const { sql, values } = this.#resolveParams(params)
217
+ const cursor = this.#sql.exec(sql, ...values)
218
+ const rows = cursor.toArray()
219
+ return (rows[0] as T) ?? undefined
220
+ }
221
+
222
+ all(...params: unknown[]): T[] {
223
+ if (!this.#db.open) {
224
+ throw new SqliteError('The database connection is not open', 'SQLITE_MISUSE')
225
+ }
226
+ const pragma = this.#interceptPragma()
227
+ if (pragma.intercepted && pragma.result) {
228
+ return pragma.result.toArray() as T[]
229
+ }
230
+
231
+ const { sql, values } = this.#resolveParams(params)
232
+ const cursor = this.#sql.exec(sql, ...values)
233
+ return cursor.toArray() as T[]
234
+ }
235
+
236
+ iterate(...params: unknown[]): IterableIterator<T> {
237
+ // eagerly fetch all rows - DO sqlite doesn't support streaming
238
+ const rows = this.all(...params)
239
+ let index = 0
240
+ return {
241
+ next(): IteratorResult<T> {
242
+ if (index < rows.length) {
243
+ return { value: rows[index++], done: false }
244
+ }
245
+ return { value: undefined as unknown as T, done: true }
246
+ },
247
+ [Symbol.iterator]() {
248
+ return this
249
+ },
250
+ }
251
+ }
252
+
253
+ /** no-op for compatibility — DO sqlite doesn't need bigint toggle */
254
+ safeIntegers(_toggle?: boolean): this {
255
+ return this
256
+ }
257
+
258
+ /** no-op for compatibility — scan status not available in DO */
259
+ scanStatus(): undefined {
260
+ return undefined
261
+ }
262
+
263
+ /** no-op for compatibility */
264
+ scanStatusV2(): unknown[] {
265
+ return []
266
+ }
267
+
268
+ /** no-op for compatibility */
269
+ scanStatusReset(): void {}
270
+
271
+ /** columns() returns column name metadata */
272
+ columns(): Array<{ name: string; column: string | null; table: string | null }> {
273
+ // execute a dummy query to get column names
274
+ const cursor = this.#sql.exec(this.source)
275
+ return cursor.columnNames.map((name) => ({
276
+ name,
277
+ column: null,
278
+ table: null,
279
+ }))
280
+ }
281
+ }
282
+
283
+ // -- TransactionFunction --
284
+
285
+ type TransactionFunction<F extends (...args: unknown[]) => unknown> = F & {
286
+ deferred: F
287
+ immediate: F
288
+ exclusive: F
289
+ }
290
+
291
+ // -- Database --
292
+
293
+ export class Database {
294
+ readonly name: string
295
+ #sql: SqlStorageLike
296
+ #open: boolean
297
+ #inTransaction: boolean
298
+
299
+ constructor(sqlOrFilename: SqlStorageLike | string, _options?: { readonly?: boolean }) {
300
+ if (typeof sqlOrFilename === 'string') {
301
+ // when used as a bundler alias for @rocicorp/zero-sqlite3,
302
+ // zero-cache passes a file path. look up DO storage from globalThis.
303
+ const storage = (globalThis as any).__orez_do_sqlite as SqlStorageLike | undefined
304
+ if (!storage) {
305
+ throw new SqliteError(
306
+ 'sqlite shim: no DO storage on globalThis.__orez_do_sqlite. ' +
307
+ 'register DO storage before importing zero-cache.',
308
+ 'SQLITE_ERROR'
309
+ )
310
+ }
311
+ this.#sql = storage
312
+ this.name = sqlOrFilename
313
+ } else {
314
+ this.#sql = sqlOrFilename
315
+ this.name = ':do-storage:'
316
+ }
317
+ this.#open = true
318
+ this.#inTransaction = false
319
+
320
+ // expose storage for StatementRunner to access transactionSync
321
+ ;(this as any).__orez_sql = this.#sql
322
+ }
323
+
324
+ get open(): boolean {
325
+ return this.#open
326
+ }
327
+
328
+ get inTransaction(): boolean {
329
+ return this.#inTransaction
330
+ }
331
+
332
+ // transaction nesting: converts nested BEGIN to SAVEPOINT
333
+ // uses shared counter on SqlStorageLike to handle multiple Database instances sharing one sql.js db
334
+ //
335
+ // CF DO SQLite rejects raw BEGIN/COMMIT/ROLLBACK/SAVEPOINT statements —
336
+ // it requires state.storage.transactionSync() instead. when transactionSync
337
+ // is available, all transaction control statements become no-ops here.
338
+ // DO handles atomicity via its own write coalescing mechanism.
339
+ _execTransactionAware(sql: string, sqlStorage: SqlStorageLike): SqlStorageCursor {
340
+ const upper = sql.trimStart().toUpperCase()
341
+ const shared = sqlStorage as any
342
+ const noopCursor: SqlStorageCursor = {
343
+ toArray: () => [],
344
+ rowsRead: 0,
345
+ rowsWritten: 0,
346
+ columnNames: [],
347
+ }
348
+
349
+ // CF DO: all transaction control is no-op (DO coalesces writes automatically)
350
+ if (sqlStorage.transactionSync) {
351
+ if (upper.startsWith('BEGIN')) {
352
+ shared.__txDepth = (shared.__txDepth || 0) + 1
353
+ this.#inTransaction = true
354
+ return noopCursor
355
+ }
356
+ if (
357
+ upper.startsWith('COMMIT') ||
358
+ upper === 'END' ||
359
+ upper.startsWith('END ') ||
360
+ upper.startsWith('ROLLBACK')
361
+ ) {
362
+ shared.__txDepth = Math.max(0, (shared.__txDepth || 0) - 1)
363
+ if (shared.__txDepth === 0) this.#inTransaction = false
364
+ return noopCursor
365
+ }
366
+ // non-tx statement inside a "transaction" — just execute normally
367
+ return sqlStorage.exec(sql)
368
+ }
369
+
370
+ // non-DO path: real BEGIN/COMMIT/ROLLBACK with SAVEPOINT nesting
371
+ if (upper.startsWith('BEGIN')) {
372
+ shared.__txDepth = (shared.__txDepth || 0) + 1
373
+ if (shared.__txDepth > 1) {
374
+ return sqlStorage.exec(`SAVEPOINT _nested_${shared.__txDepth}`)
375
+ }
376
+ this.#inTransaction = true
377
+ return sqlStorage.exec(sql)
378
+ }
379
+
380
+ if (upper.startsWith('COMMIT') || upper === 'END' || upper.startsWith('END ')) {
381
+ if ((shared.__txDepth || 0) > 1) {
382
+ const result = sqlStorage.exec(`RELEASE SAVEPOINT _nested_${shared.__txDepth}`)
383
+ shared.__txDepth--
384
+ return result
385
+ }
386
+ shared.__txDepth = 0
387
+ this.#inTransaction = false
388
+ return sqlStorage.exec(sql)
389
+ }
390
+
391
+ if (upper.startsWith('ROLLBACK')) {
392
+ if ((shared.__txDepth || 0) > 1) {
393
+ const result = sqlStorage.exec(
394
+ `ROLLBACK TO SAVEPOINT _nested_${shared.__txDepth}`
395
+ )
396
+ shared.__txDepth--
397
+ return result
398
+ }
399
+ shared.__txDepth = 0
400
+ this.#inTransaction = false
401
+ return sqlStorage.exec(sql)
402
+ }
403
+
404
+ return sqlStorage.exec(sql)
405
+ }
406
+
407
+ /** prepare a statement */
408
+ prepare<T = Record<string, SqlStorageValue>>(sql: string): Statement<T> {
409
+ if (!this.#open) {
410
+ throw new SqliteError('The database connection is not open', 'SQLITE_MISUSE')
411
+ }
412
+ return new Statement<T>(this.#sql, this, sql)
413
+ }
414
+
415
+ /**
416
+ * execute pragma statements.
417
+ *
418
+ * DO sqlite supports a subset of pragmas. we handle known ones and
419
+ * return sensible defaults for the rest. the `simple` option returns
420
+ * just the value instead of an array of objects.
421
+ */
422
+ pragma(source: string, options?: { simple?: boolean }): unknown {
423
+ if (!this.#open) {
424
+ throw new SqliteError('The database connection is not open', 'SQLITE_MISUSE')
425
+ }
426
+
427
+ const trimmed = source.trim().toLowerCase()
428
+
429
+ // skip optimize pragma (not supported / can corrupt)
430
+ if (trimmed.startsWith('optimize')) {
431
+ return options?.simple ? undefined : []
432
+ }
433
+
434
+ // return sensible defaults for pragmas that DO SQLite may not support
435
+ // but that zero-cache's zqlite Database constructor expects
436
+ const pragmaDefaults: Record<string, unknown> = {
437
+ page_size: [{ page_size: 4096 }],
438
+ freelist_count: [{ freelist_count: 0 }],
439
+ auto_vacuum: [{ auto_vacuum: 0 }],
440
+ page_count: [{ page_count: 0 }],
441
+ journal_mode: [{ journal_mode: 'wal2' }],
442
+ wal_checkpoint: [{ busy: 0, log: 0, checkpointed: 0 }],
443
+ }
444
+
445
+ // parse pragma name and value
446
+ const eqIndex = source.indexOf('=')
447
+ const isSet = eqIndex !== -1
448
+
449
+ if (isSet) {
450
+ // intercept journal_mode set — sql.js doesn't support wal/wal2, fake it
451
+ const pragmaName = source.substring(0, eqIndex).trim().toLowerCase()
452
+ const pragmaValue = source
453
+ .substring(eqIndex + 1)
454
+ .trim()
455
+ .replace(/['"]/g, '')
456
+ if (pragmaName === 'journal_mode') {
457
+ return options?.simple ? pragmaValue : [{ journal_mode: pragmaValue }]
458
+ }
459
+
460
+ // setting a pragma - execute it and return result
461
+ try {
462
+ const cursor = this.#sql.exec(`PRAGMA ${source}`)
463
+ const rows = cursor.toArray()
464
+ return options?.simple ? rows[0]?.[Object.keys(rows[0] ?? {})[0]] : rows
465
+ } catch {
466
+ // many pragmas are no-ops in DO sqlite - swallow errors
467
+ return options?.simple ? undefined : []
468
+ }
469
+ }
470
+
471
+ // reading a pragma — check defaults first for pragmas we intercept
472
+ const pragmaName = trimmed.split(/[\s(]/)[0]
473
+ const defaultVal = pragmaDefaults[pragmaName]
474
+ if (defaultVal) {
475
+ if (options?.simple) {
476
+ const firstRow = (defaultVal as any[])[0]
477
+ return firstRow ? firstRow[Object.keys(firstRow)[0]] : undefined
478
+ }
479
+ return defaultVal
480
+ }
481
+
482
+ // try real execution for unknown pragmas
483
+ try {
484
+ const cursor = this.#sql.exec(`PRAGMA ${source}`)
485
+ const rows = cursor.toArray()
486
+ if (rows.length > 0) {
487
+ if (options?.simple) {
488
+ const firstKey = Object.keys(rows[0])[0]
489
+ return rows[0][firstKey]
490
+ }
491
+ return rows
492
+ }
493
+ } catch {
494
+ // sql.js may not support this pragma
495
+ }
496
+ if (defaultVal) {
497
+ if (options?.simple) {
498
+ const arr = defaultVal as Record<string, unknown>[]
499
+ const firstKey = Object.keys(arr[0])[0]
500
+ return arr[0][firstKey]
501
+ }
502
+ return defaultVal
503
+ }
504
+ return options?.simple ? undefined : []
505
+ }
506
+
507
+ /**
508
+ * execute one or more sql statements.
509
+ * does not return results — used for DDL and multi-statement strings.
510
+ */
511
+ exec(source: string): this {
512
+ if (!this.#open) {
513
+ throw new SqliteError('The database connection is not open', 'SQLITE_MISUSE')
514
+ }
515
+
516
+ // split on semicolons to handle multi-statement strings
517
+ // (DO sqlite exec only takes single statements)
518
+ const statements = source
519
+ .split(';')
520
+ .map((s) => s.trim())
521
+ .filter((s) => s.length > 0)
522
+
523
+ for (const stmt of statements) {
524
+ // route transaction control through _execTransactionAware
525
+ // so CF DO's transactionSync short-circuit applies
526
+ const upper = stmt.trimStart().toUpperCase()
527
+ const isTxCmd =
528
+ upper.startsWith('BEGIN') ||
529
+ upper.startsWith('COMMIT') ||
530
+ upper.startsWith('ROLLBACK') ||
531
+ upper === 'END' ||
532
+ upper.startsWith('END ') ||
533
+ upper.startsWith('SAVEPOINT') ||
534
+ upper.startsWith('RELEASE ')
535
+ if (isTxCmd) {
536
+ this._execTransactionAware(stmt, this.#sql)
537
+ } else {
538
+ this.#sql.exec(stmt)
539
+ }
540
+ }
541
+
542
+ return this
543
+ }
544
+
545
+ /**
546
+ * create a transaction wrapper function.
547
+ *
548
+ * returns a function that, when called, wraps `fn` in BEGIN/COMMIT.
549
+ * if `fn` throws, issues ROLLBACK instead.
550
+ *
551
+ * the returned function also has `.deferred()`, `.immediate()`, and
552
+ * `.exclusive()` variants.
553
+ */
554
+ transaction<F extends (...args: unknown[]) => unknown>(fn: F): TransactionFunction<F> {
555
+ const self = this
556
+
557
+ const wrapInTransaction: F = ((...args: unknown[]) => {
558
+ // handle nested transactions — just run fn
559
+ if (self.#inTransaction) {
560
+ return fn(...args)
561
+ }
562
+
563
+ // DO SQLite requires transactionSync() — raw BEGIN/COMMIT is rejected.
564
+ // fall back to raw SQL only if transactionSync is unavailable.
565
+ if (self.#sql.transactionSync) {
566
+ return self.#sql.transactionSync(() => {
567
+ self.#inTransaction = true
568
+ try {
569
+ return fn(...args)
570
+ } finally {
571
+ self.#inTransaction = false
572
+ }
573
+ })
574
+ }
575
+
576
+ // fallback for non-DO environments (tests with bedrock-sqlite)
577
+ self.#sql.exec('BEGIN')
578
+ self.#inTransaction = true
579
+ try {
580
+ const result = fn(...args)
581
+ self.#sql.exec('COMMIT')
582
+ self.#inTransaction = false
583
+ return result
584
+ } catch (err) {
585
+ try {
586
+ self.#sql.exec('ROLLBACK')
587
+ } catch {
588
+ // swallow rollback errors
589
+ }
590
+ self.#inTransaction = false
591
+ throw err
592
+ }
593
+ }) as F
594
+
595
+ // all variants use the same transactionSync wrapper on DO
596
+ const txn = wrapInTransaction as TransactionFunction<F>
597
+ txn.deferred = wrapInTransaction
598
+ txn.immediate = wrapInTransaction
599
+ txn.exclusive = wrapInTransaction
600
+
601
+ return txn
602
+ }
603
+
604
+ /** close the database connection */
605
+ close(): this {
606
+ this.#open = false
607
+ return this
608
+ }
609
+
610
+ /** no-op for compatibility — unsafe mode not needed in DO */
611
+ unsafeMode(_unsafe?: boolean): this {
612
+ return this
613
+ }
614
+
615
+ /** no-op for compatibility */
616
+ defaultSafeIntegers(_toggle?: boolean): this {
617
+ return this
618
+ }
619
+ }
620
+
621
+ // -- StatementRunner --
622
+ // matches zero-cache's db/statements.js StatementRunner interface
623
+
624
+ export class StatementRunner {
625
+ db: Database
626
+ #stmtCache = new Map<string, Statement[]>()
627
+ /** DO SqlStorage for transactionSync — set when Database wraps DO storage */
628
+ #storage: SqlStorageLike | null = null
629
+
630
+ constructor(db: Database) {
631
+ this.db = db
632
+ // extract the SqlStorageLike from the Database for transactionSync access.
633
+ // the Database stores it privately, so we pass it via a well-known property.
634
+ this.#storage = (db as any).__orez_sql ?? null
635
+ }
636
+
637
+ #getStatement(sql: string): Statement {
638
+ const cached = this.#stmtCache.get(sql)
639
+ if (cached && cached.length > 0) {
640
+ return cached.pop()!
641
+ }
642
+ return this.db.prepare(sql)
643
+ }
644
+
645
+ #returnStatement(sql: string, stmt: Statement): void {
646
+ let arr = this.#stmtCache.get(sql)
647
+ if (!arr) {
648
+ arr = []
649
+ this.#stmtCache.set(sql, arr)
650
+ }
651
+ arr.push(stmt)
652
+ }
653
+
654
+ run(sql: string, ...args: unknown[]): RunResult {
655
+ const stmt = this.#getStatement(sql)
656
+ try {
657
+ return stmt.run(...args)
658
+ } finally {
659
+ this.#returnStatement(sql, stmt)
660
+ }
661
+ }
662
+
663
+ get(sql: string, ...args: unknown[]): unknown {
664
+ const stmt = this.#getStatement(sql)
665
+ try {
666
+ return stmt.get(...args)
667
+ } finally {
668
+ this.#returnStatement(sql, stmt)
669
+ }
670
+ }
671
+
672
+ all(sql: string, ...args: unknown[]): unknown[] {
673
+ const stmt = this.#getStatement(sql)
674
+ try {
675
+ return stmt.all(...args)
676
+ } finally {
677
+ this.#returnStatement(sql, stmt)
678
+ }
679
+ }
680
+
681
+ // -- transaction methods --
682
+ // DO SQLite rejects raw BEGIN/COMMIT SQL. when transactionSync is
683
+ // available, begin() starts a transactionSync block and commit()
684
+ // lets it complete. for non-DO environments, falls back to raw SQL.
685
+
686
+ #txnResult: RunResult = { changes: 0, lastInsertRowid: 0 }
687
+
688
+ begin(): RunResult {
689
+ // on DO, transactionSync is closure-based so begin/commit are
690
+ // effectively no-ops — the actual transaction wrapping happens
691
+ // at the Database.transaction() level or implicitly per-statement.
692
+ // we try raw SQL first and swallow the DO rejection.
693
+ if (this.#storage?.transactionSync) {
694
+ return this.#txnResult
695
+ }
696
+ return this.run('BEGIN')
697
+ }
698
+
699
+ beginConcurrent(): RunResult {
700
+ return this.begin()
701
+ }
702
+
703
+ beginImmediate(): RunResult {
704
+ if (this.#storage?.transactionSync) {
705
+ return this.#txnResult
706
+ }
707
+ return this.run('BEGIN IMMEDIATE')
708
+ }
709
+
710
+ commit(): RunResult {
711
+ if (this.#storage?.transactionSync) {
712
+ return this.#txnResult
713
+ }
714
+ return this.run('COMMIT')
715
+ }
716
+
717
+ rollback(): RunResult {
718
+ if (this.#storage?.transactionSync) {
719
+ return this.#txnResult
720
+ }
721
+ return this.run('ROLLBACK')
722
+ }
723
+ }
724
+
725
+ // -- default export --
726
+ // matches @rocicorp/zero-sqlite3's default export: the Database class.
727
+ // zero-cache's zqlite/src/db.js does:
728
+ // import SQLite3Database, { SqliteError } from "@rocicorp/zero-sqlite3"
729
+ // new SQLite3Database(path, options)
730
+
731
+ export default Database