orez 0.2.25 → 0.2.27

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 (175) hide show
  1. package/dist/cf-do/watermark.d.ts +21 -0
  2. package/dist/cf-do/watermark.d.ts.map +1 -0
  3. package/dist/cf-do/watermark.js +93 -0
  4. package/dist/cf-do/watermark.js.map +1 -0
  5. package/dist/cf-do/worker.d.ts +48 -22
  6. package/dist/cf-do/worker.d.ts.map +1 -1
  7. package/dist/cf-do/worker.js +650 -269
  8. package/dist/cf-do/worker.js.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/do-sql-tracking.d.ts +6 -0
  12. package/dist/do-sql-tracking.d.ts.map +1 -0
  13. package/dist/do-sql-tracking.js +14 -0
  14. package/dist/do-sql-tracking.js.map +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +28 -14
  17. package/dist/index.js.map +1 -1
  18. package/dist/pg-proxy-browser.js +6 -6
  19. package/dist/pg-proxy-browser.js.map +1 -1
  20. package/dist/pg-proxy-do-backend.d.ts +98 -17
  21. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  22. package/dist/pg-proxy-do-backend.js +6075 -454
  23. package/dist/pg-proxy-do-backend.js.map +1 -1
  24. package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
  25. package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
  26. package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
  27. package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
  28. package/dist/pg-sqlite-compiler/index.d.ts +12 -0
  29. package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
  30. package/dist/pg-sqlite-compiler/index.js +59 -0
  31. package/dist/pg-sqlite-compiler/index.js.map +1 -0
  32. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
  33. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
  34. package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
  35. package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
  36. package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
  37. package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
  38. package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
  39. package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
  40. package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
  41. package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
  42. package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
  43. package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
  44. package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
  45. package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
  46. package/dist/pg-sqlite-compiler/passes/index.js +39 -0
  47. package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
  48. package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
  49. package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
  50. package/dist/pg-sqlite-compiler/passes/types.js +103 -0
  51. package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
  52. package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
  53. package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
  54. package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
  55. package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
  56. package/dist/pg-sqlite-compiler/types.d.ts +55 -0
  57. package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
  58. package/dist/pg-sqlite-compiler/types.js +2 -0
  59. package/dist/pg-sqlite-compiler/types.js.map +1 -0
  60. package/dist/replication/change-tracker.d.ts.map +1 -1
  61. package/dist/replication/change-tracker.js +18 -1
  62. package/dist/replication/change-tracker.js.map +1 -1
  63. package/dist/replication/handler.d.ts.map +1 -1
  64. package/dist/replication/handler.js +7 -2
  65. package/dist/replication/handler.js.map +1 -1
  66. package/dist/replication/pgoutput-encoder.d.ts.map +1 -1
  67. package/dist/replication/pgoutput-encoder.js +72 -30
  68. package/dist/replication/pgoutput-encoder.js.map +1 -1
  69. package/dist/worker/browser-build-config.d.ts.map +1 -1
  70. package/dist/worker/browser-build-config.js +2 -1
  71. package/dist/worker/browser-build-config.js.map +1 -1
  72. package/dist/worker/cf-patches.d.ts +5 -2
  73. package/dist/worker/cf-patches.d.ts.map +1 -1
  74. package/dist/worker/cf-patches.js +238 -4
  75. package/dist/worker/cf-patches.js.map +1 -1
  76. package/dist/worker/shims/node-stub.d.ts +35 -0
  77. package/dist/worker/shims/node-stub.d.ts.map +1 -1
  78. package/dist/worker/shims/node-stub.js +53 -1
  79. package/dist/worker/shims/node-stub.js.map +1 -1
  80. package/dist/worker/shims/oxfmt.d.ts +4 -0
  81. package/dist/worker/shims/oxfmt.d.ts.map +1 -0
  82. package/dist/worker/shims/oxfmt.js +4 -0
  83. package/dist/worker/shims/oxfmt.js.map +1 -0
  84. package/dist/worker/shims/postgres-socket.js +1 -1
  85. package/dist/worker/shims/postgres-socket.js.map +1 -1
  86. package/dist/worker/shims/sqlite.d.ts +1 -0
  87. package/dist/worker/shims/sqlite.d.ts.map +1 -1
  88. package/dist/worker/shims/sqlite.js +229 -9
  89. package/dist/worker/shims/sqlite.js.map +1 -1
  90. package/dist/worker/shims/ws.d.ts.map +1 -1
  91. package/dist/worker/shims/ws.js +45 -0
  92. package/dist/worker/shims/ws.js.map +1 -1
  93. package/dist/worker/shims/zero-process-env.d.ts +2 -0
  94. package/dist/worker/shims/zero-process-env.d.ts.map +1 -0
  95. package/dist/worker/shims/zero-process-env.js +9 -0
  96. package/dist/worker/shims/zero-process-env.js.map +1 -0
  97. package/dist/worker/zero-cache-embed-cf.d.ts +29 -12
  98. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
  99. package/dist/worker/zero-cache-embed-cf.js +83 -14
  100. package/dist/worker/zero-cache-embed-cf.js.map +1 -1
  101. package/package.json +11 -2
  102. package/src/cf-do/.wrangler/cache/cf.json +1 -0
  103. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  104. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  105. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  106. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  107. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  108. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  109. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  110. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +11 -0
  111. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +134 -0
  112. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +11 -0
  113. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +134 -0
  114. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +1059 -0
  115. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +8 -0
  116. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +1059 -0
  117. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +8 -0
  118. package/src/cf-do/ARCHITECTURE.md +93 -0
  119. package/src/cf-do/CHAT_E2E.md +213 -0
  120. package/src/cf-do/watermark.test.ts +103 -0
  121. package/src/cf-do/watermark.ts +118 -0
  122. package/src/cf-do/worker.ts +1041 -0
  123. package/src/cf-do/wrangler.toml +11 -0
  124. package/src/cli.test.ts +3 -1
  125. package/src/config.ts +1 -1
  126. package/src/do-sql-tracking.test.ts +19 -0
  127. package/src/do-sql-tracking.ts +19 -0
  128. package/src/index.ts +29 -14
  129. package/src/pg-proxy-browser.ts +6 -6
  130. package/src/pg-proxy-do-backend.test.ts +3890 -0
  131. package/src/pg-proxy-do-backend.ts +6833 -482
  132. package/src/pg-sqlite-compiler/README.md +53 -0
  133. package/src/pg-sqlite-compiler/catalog/seed.ts +524 -0
  134. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +307 -0
  135. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +377 -0
  136. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +12 -0
  137. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +447 -0
  138. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +32 -0
  139. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +397 -0
  140. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +337 -0
  141. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +337 -0
  142. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +537 -0
  143. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +1837 -0
  144. package/src/pg-sqlite-compiler/index.ts +73 -0
  145. package/src/pg-sqlite-compiler/integration.test.ts +136 -0
  146. package/src/pg-sqlite-compiler/passes/ast-utils.ts +113 -0
  147. package/src/pg-sqlite-compiler/passes/catalog.ts +65 -0
  148. package/src/pg-sqlite-compiler/passes/datetime.ts +74 -0
  149. package/src/pg-sqlite-compiler/passes/index.ts +49 -0
  150. package/src/pg-sqlite-compiler/passes/types.ts +156 -0
  151. package/src/pg-sqlite-compiler/smoke.test.ts +69 -0
  152. package/src/pg-sqlite-compiler/test/catalog.test.ts +171 -0
  153. package/src/pg-sqlite-compiler/test/corpus.test.ts +161 -0
  154. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +102 -0
  155. package/src/pg-sqlite-compiler/test/oracle.ts +237 -0
  156. package/src/pg-sqlite-compiler/test/types.test.ts +109 -0
  157. package/src/pg-sqlite-compiler/types.ts +63 -0
  158. package/src/replication/change-tracker.ts +16 -1
  159. package/src/replication/handler.test.ts +35 -0
  160. package/src/replication/handler.ts +7 -2
  161. package/src/replication/pgoutput-encoder.test.ts +71 -2
  162. package/src/replication/pgoutput-encoder.ts +65 -30
  163. package/src/worker/browser-build-config.test.ts +12 -0
  164. package/src/worker/browser-build-config.ts +2 -1
  165. package/src/worker/cf-patches.ts +274 -4
  166. package/src/worker/shims/node-stub.ts +53 -1
  167. package/src/worker/shims/oxfmt.ts +3 -0
  168. package/src/worker/shims/postgres-socket.ts +1 -1
  169. package/src/worker/shims/sqlite.test.ts +145 -0
  170. package/src/worker/shims/sqlite.ts +256 -9
  171. package/src/worker/shims/ws.ts +45 -0
  172. package/src/worker/shims/zero-process-env.ts +11 -0
  173. package/src/worker/zero-cache-embed-cf.ts +114 -18
  174. package/src/query-rewrites.test.ts +0 -30
  175. package/src/query-rewrites.ts +0 -152
@@ -139,6 +139,7 @@ describe('Database', () => {
139
139
  })
140
140
 
141
141
  afterEach(() => {
142
+ db.close()
142
143
  mock._nativeDb.close()
143
144
  })
144
145
 
@@ -189,6 +190,7 @@ describe('Database.exec', () => {
189
190
  })
190
191
 
191
192
  afterEach(() => {
193
+ db.close()
192
194
  mock._nativeDb.close()
193
195
  })
194
196
 
@@ -229,6 +231,7 @@ describe('Database.prepare / Statement', () => {
229
231
  })
230
232
 
231
233
  afterEach(() => {
234
+ db.close()
232
235
  mock._nativeDb.close()
233
236
  })
234
237
 
@@ -552,6 +555,148 @@ describe('StatementRunner', () => {
552
555
  })
553
556
  })
554
557
 
558
+ describe('DO snapshot transactions', () => {
559
+ let mock: SqlStorageLike & { _nativeDb: any; transactionSync: <T>(fn: () => T) => T }
560
+ let live: Database
561
+ let snapshot: Database
562
+
563
+ beforeEach(() => {
564
+ mock = createMockSqlStorage() as SqlStorageLike & {
565
+ _nativeDb: any
566
+ transactionSync: <T>(fn: () => T) => T
567
+ }
568
+ mock.transactionSync = (fn) => fn()
569
+ live = new Database(mock)
570
+ snapshot = new Database(mock)
571
+ live.exec('CREATE TABLE todo (id TEXT PRIMARY KEY, title TEXT, _0_version TEXT)')
572
+ live.prepare('INSERT INTO todo VALUES (?, ?, ?)').run('1', 'old', '01')
573
+ })
574
+
575
+ afterEach(() => {
576
+ live.close()
577
+ snapshot.close()
578
+ mock._nativeDb.close()
579
+ })
580
+
581
+ it('keeps BEGIN CONCURRENT reads stable until rollback', () => {
582
+ snapshot.prepare('BEGIN CONCURRENT').run()
583
+ expect(snapshot.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
584
+ title: 'old',
585
+ })
586
+
587
+ live
588
+ .prepare('UPDATE todo SET title = ?, _0_version = ? WHERE id = ?')
589
+ .run('new', '02', '1')
590
+
591
+ expect(live.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
592
+ title: 'new',
593
+ })
594
+ expect(snapshot.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
595
+ title: 'old',
596
+ })
597
+
598
+ snapshot.prepare('ROLLBACK').run()
599
+ expect(snapshot.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
600
+ title: 'new',
601
+ })
602
+ })
603
+
604
+ it('hides snapshot tables from sqlite catalog queries', () => {
605
+ snapshot.prepare('BEGIN CONCURRENT').run()
606
+
607
+ expect(
608
+ live
609
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name")
610
+ .all()
611
+ ).toEqual([{ name: 'todo' }])
612
+ })
613
+
614
+ it('does not rewrite table names inside string literals during snapshots', () => {
615
+ live.exec('CREATE TABLE log (id TEXT PRIMARY KEY, table_name TEXT, _0_version TEXT)')
616
+ live.prepare('INSERT INTO log VALUES (?, ?, ?)').run('1', 'todo', '01')
617
+ live.prepare('INSERT INTO log VALUES (?, ?, ?)').run('2', '"todo"', '02')
618
+
619
+ snapshot.prepare('BEGIN CONCURRENT').run()
620
+
621
+ expect(
622
+ snapshot.prepare("SELECT table_name FROM log WHERE table_name = 'todo'").get()
623
+ ).toEqual({ table_name: 'todo' })
624
+ expect(
625
+ snapshot.prepare('SELECT table_name FROM log WHERE table_name = ?').get('"todo"')
626
+ ).toEqual({ table_name: '"todo"' })
627
+ })
628
+
629
+ it('cleans inactive snapshot tables when opening a database', () => {
630
+ mock.exec('CREATE TABLE _orez_snapshot_1_todo AS SELECT * FROM todo')
631
+ expect(
632
+ mock
633
+ .exec("SELECT name FROM sqlite_master WHERE name = '_orez_snapshot_1_todo'")
634
+ .toArray()
635
+ ).toEqual([{ name: '_orez_snapshot_1_todo' }])
636
+
637
+ const reopened = new Database(mock)
638
+ try {
639
+ expect(
640
+ mock
641
+ .exec("SELECT name FROM sqlite_master WHERE name = '_orez_snapshot_1_todo'")
642
+ .toArray()
643
+ ).toEqual([])
644
+ } finally {
645
+ reopened.close()
646
+ }
647
+ })
648
+
649
+ it('does not remove another open connection snapshot', () => {
650
+ snapshot.prepare('BEGIN CONCURRENT').run()
651
+ const snapshotTables = mock
652
+ .exec("SELECT name FROM sqlite_master WHERE name LIKE '_orez_snapshot_%'")
653
+ .toArray()
654
+ expect(snapshotTables).toHaveLength(1)
655
+
656
+ const other = new Database(mock)
657
+ try {
658
+ live
659
+ .prepare('UPDATE todo SET title = ?, _0_version = ? WHERE id = ?')
660
+ .run('new', '02', '1')
661
+
662
+ expect(snapshot.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
663
+ title: 'old',
664
+ })
665
+ } finally {
666
+ other.close()
667
+ }
668
+ })
669
+
670
+ it('persists BEGIN CONCURRENT writes for the zero-cache replica writer', () => {
671
+ const globalObject = globalThis as any
672
+ const previousRole = globalObject.__orez_zero_sqlite_role
673
+ globalObject.__orez_zero_sqlite_role = 'replica-writer'
674
+ let writer: Database
675
+ try {
676
+ writer = new Database(mock)
677
+ } finally {
678
+ if (previousRole === undefined) {
679
+ delete globalObject.__orez_zero_sqlite_role
680
+ } else {
681
+ globalObject.__orez_zero_sqlite_role = previousRole
682
+ }
683
+ }
684
+
685
+ writer.prepare('BEGIN CONCURRENT').run()
686
+ writer.prepare('INSERT INTO todo VALUES (?, ?, ?)').run('2', 'writer row', '02')
687
+ writer.prepare('COMMIT').run()
688
+
689
+ expect(live.prepare('SELECT title FROM todo WHERE id = ?').get('2')).toEqual({
690
+ title: 'writer row',
691
+ })
692
+ expect(
693
+ live
694
+ .prepare("SELECT name FROM sqlite_master WHERE name LIKE '_orez_snapshot_%'")
695
+ .all()
696
+ ).toEqual([])
697
+ })
698
+ })
699
+
555
700
  describe('StatementRunner: zero-cache replicator pattern', () => {
556
701
  let mock: SqlStorageLike & { _nativeDb: any }
557
702
  let db: Database
@@ -39,6 +39,9 @@ export interface SqlStorageLike {
39
39
  transactionSync?<T>(fn: () => T): T
40
40
  }
41
41
 
42
+ type SqliteConnectionRole = 'default' | 'replica-writer'
43
+ const activeSnapshotPrefixes = new Set<string>()
44
+
42
45
  // -- SqliteError --
43
46
 
44
47
  export class SqliteError extends Error {
@@ -109,6 +112,178 @@ function serializeSqliteParams(params: unknown[]): SqlStorageValue[] {
109
112
  return params.map(serializeValue)
110
113
  }
111
114
 
115
+ function quoteIdentifier(value: string): string {
116
+ return `"${value.replace(/"/g, '""')}"`
117
+ }
118
+
119
+ function snapshotTableName(prefix: string, table: string): string {
120
+ return `${prefix}_${table.replace(/[^A-Za-z0-9_]/g, '_')}`
121
+ }
122
+
123
+ function isSnapshotInternalTable(name: string): boolean {
124
+ return name.startsWith('_orez_snapshot_')
125
+ }
126
+
127
+ function createSnapshotPrefix(): string {
128
+ const uuid =
129
+ typeof globalThis.crypto?.randomUUID === 'function'
130
+ ? globalThis.crypto.randomUUID().replace(/-/g, '').slice(0, 16)
131
+ : Math.random().toString(36).slice(2, 18)
132
+ return `_orez_snapshot_${Date.now().toString(36)}_${uuid}`
133
+ }
134
+
135
+ function isActiveSnapshotTable(name: string): boolean {
136
+ for (const prefix of activeSnapshotPrefixes) {
137
+ if (name.startsWith(`${prefix}_`)) return true
138
+ }
139
+ return false
140
+ }
141
+
142
+ function cleanupInactiveSnapshotTables(sql: SqlStorageLike): void {
143
+ try {
144
+ const rows = sql
145
+ .exec(
146
+ `SELECT name FROM sqlite_master
147
+ WHERE type = 'table'
148
+ AND name LIKE '_orez_snapshot_%'`
149
+ )
150
+ .toArray()
151
+ for (const row of rows) {
152
+ const name = String(row.name ?? '')
153
+ if (!isSnapshotInternalTable(name) || isActiveSnapshotTable(name)) continue
154
+ try {
155
+ sql.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(name)}`)
156
+ } catch {}
157
+ }
158
+ } catch {}
159
+ }
160
+
161
+ function shouldSnapshotTable(name: string): boolean {
162
+ return (
163
+ name !== '__miniflare_do_name' &&
164
+ name !== 'storage' &&
165
+ name !== 'sqlite_stat1' &&
166
+ !isSnapshotInternalTable(name)
167
+ )
168
+ }
169
+
170
+ function currentConnectionRole(): SqliteConnectionRole {
171
+ return (globalThis as any).__orez_zero_sqlite_role === 'replica-writer'
172
+ ? 'replica-writer'
173
+ : 'default'
174
+ }
175
+
176
+ function isSqliteCatalogQuery(sql: string): boolean {
177
+ return /\bsqlite_(?:master|schema)\b/i.test(sql)
178
+ }
179
+
180
+ function hasSnapshotCatalogName(row: Record<string, unknown>): boolean {
181
+ for (const key of ['name', 'tbl_name', 'table', 'tableName']) {
182
+ const value = row[key]
183
+ if (typeof value === 'string' && isSnapshotInternalTable(value)) return true
184
+ }
185
+ return false
186
+ }
187
+
188
+ function filterSnapshotCatalogRows<T>(sql: string, rows: T[]): T[] {
189
+ if (!isSqliteCatalogQuery(sql)) return rows
190
+ return rows.filter((row) => !hasSnapshotCatalogName(row as Record<string, unknown>))
191
+ }
192
+
193
+ function replaceIdentifierOutsideLiterals(
194
+ sql: string,
195
+ identifier: string,
196
+ replacement: string
197
+ ): string {
198
+ let out = ''
199
+ let i = 0
200
+
201
+ while (i < sql.length) {
202
+ const ch = sql[i]
203
+ const next = sql[i + 1]
204
+
205
+ if (ch === "'") {
206
+ const start = i
207
+ i++
208
+ while (i < sql.length) {
209
+ if (sql[i] === "'" && sql[i + 1] === "'") {
210
+ i += 2
211
+ continue
212
+ }
213
+ if (sql[i] === "'") {
214
+ i++
215
+ break
216
+ }
217
+ i++
218
+ }
219
+ out += sql.slice(start, i)
220
+ continue
221
+ }
222
+
223
+ if (ch === '"') {
224
+ const start = i
225
+ let value = ''
226
+ i++
227
+ while (i < sql.length) {
228
+ if (sql[i] === '"' && sql[i + 1] === '"') {
229
+ value += '"'
230
+ i += 2
231
+ continue
232
+ }
233
+ if (sql[i] === '"') {
234
+ i++
235
+ break
236
+ }
237
+ value += sql[i]
238
+ i++
239
+ }
240
+ out += value === identifier ? quoteIdentifier(replacement) : sql.slice(start, i)
241
+ continue
242
+ }
243
+
244
+ if (ch === '-' && next === '-') {
245
+ const start = i
246
+ i += 2
247
+ while (i < sql.length && sql[i] !== '\n') i++
248
+ out += sql.slice(start, i)
249
+ continue
250
+ }
251
+
252
+ if (ch === '/' && next === '*') {
253
+ const start = i
254
+ i += 2
255
+ while (i < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) i++
256
+ i = Math.min(sql.length, i + 2)
257
+ out += sql.slice(start, i)
258
+ continue
259
+ }
260
+
261
+ if (/[A-Za-z_]/.test(ch)) {
262
+ const start = i
263
+ i++
264
+ while (i < sql.length && /[A-Za-z0-9_]/.test(sql[i])) i++
265
+ const word = sql.slice(start, i)
266
+ out += word === identifier ? replacement : word
267
+ continue
268
+ }
269
+
270
+ out += ch
271
+ i++
272
+ }
273
+
274
+ return out
275
+ }
276
+
277
+ function rewriteSQLTables(sql: string, tables: Map<string, string>): string {
278
+ let rewritten = sql
279
+ const names = [...tables.keys()].sort((a, b) => b.length - a.length)
280
+ for (const name of names) {
281
+ const snapshot = tables.get(name)!
282
+ rewritten = replaceIdentifierOutsideLiterals(rewritten, name, snapshot)
283
+ }
284
+ return rewritten
285
+ }
286
+
112
287
  // -- Statement --
113
288
 
114
289
  export class Statement<T = Record<string, SqlStorageValue>> {
@@ -193,7 +368,9 @@ export class Statement<T = Record<string, SqlStorageValue>> {
193
368
  upper.startsWith('ROLLBACK') ||
194
369
  upper === 'END' ||
195
370
  upper.startsWith('END ')
196
- const { sql, values } = this.#resolveParams(params)
371
+ const resolved = this.#resolveParams(params)
372
+ const sql = this.#db._rewriteForSnapshot(resolved.sql)
373
+ const values = resolved.values
197
374
  const cursor =
198
375
  isTxCmd && values.length === 0
199
376
  ? this.#db._execTransactionAware(sql, this.#sql)
@@ -213,9 +390,11 @@ export class Statement<T = Record<string, SqlStorageValue>> {
213
390
  return pragma.result.toArray()[0] as T
214
391
  }
215
392
 
216
- const { sql, values } = this.#resolveParams(params)
393
+ const resolved = this.#resolveParams(params)
394
+ const sql = this.#db._rewriteForSnapshot(resolved.sql)
395
+ const values = resolved.values
217
396
  const cursor = this.#sql.exec(sql, ...values)
218
- const rows = cursor.toArray()
397
+ const rows = filterSnapshotCatalogRows(sql, cursor.toArray())
219
398
  return (rows[0] as T) ?? undefined
220
399
  }
221
400
 
@@ -228,9 +407,11 @@ export class Statement<T = Record<string, SqlStorageValue>> {
228
407
  return pragma.result.toArray() as T[]
229
408
  }
230
409
 
231
- const { sql, values } = this.#resolveParams(params)
410
+ const resolved = this.#resolveParams(params)
411
+ const sql = this.#db._rewriteForSnapshot(resolved.sql)
412
+ const values = resolved.values
232
413
  const cursor = this.#sql.exec(sql, ...values)
233
- return cursor.toArray() as T[]
414
+ return filterSnapshotCatalogRows(sql, cursor.toArray()) as T[]
234
415
  }
235
416
 
236
417
  iterate(...params: unknown[]): IterableIterator<T> {
@@ -271,7 +452,7 @@ export class Statement<T = Record<string, SqlStorageValue>> {
271
452
  /** columns() returns column name metadata */
272
453
  columns(): Array<{ name: string; column: string | null; table: string | null }> {
273
454
  // execute a dummy query to get column names
274
- const cursor = this.#sql.exec(this.source)
455
+ const cursor = this.#sql.exec(this.#db._rewriteForSnapshot(this.source))
275
456
  return cursor.columnNames.map((name) => ({
276
457
  name,
277
458
  column: null,
@@ -295,6 +476,10 @@ export class Database {
295
476
  #sql: SqlStorageLike
296
477
  #open: boolean
297
478
  #inTransaction: boolean
479
+ #snapshotTables: Map<string, string> | null
480
+ #snapshotPrefix: string
481
+ #snapshotCounter: number
482
+ #connectionRole: SqliteConnectionRole
298
483
 
299
484
  constructor(sqlOrFilename: SqlStorageLike | string, _options?: { readonly?: boolean }) {
300
485
  if (typeof sqlOrFilename === 'string') {
@@ -316,6 +501,12 @@ export class Database {
316
501
  }
317
502
  this.#open = true
318
503
  this.#inTransaction = false
504
+ this.#snapshotTables = null
505
+ this.#snapshotPrefix = createSnapshotPrefix()
506
+ this.#snapshotCounter = 0
507
+ this.#connectionRole = currentConnectionRole()
508
+ activeSnapshotPrefixes.add(this.#snapshotPrefix)
509
+ cleanupInactiveSnapshotTables(this.#sql)
319
510
 
320
511
  // expose storage for StatementRunner to access transactionSync
321
512
  ;(this as any).__orez_sql = this.#sql
@@ -348,6 +539,15 @@ export class Database {
348
539
 
349
540
  // CF DO: all transaction control is no-op (DO coalesces writes automatically)
350
541
  if (sqlStorage.transactionSync) {
542
+ if (upper.startsWith('BEGIN CONCURRENT')) {
543
+ if (this.#connectionRole === 'replica-writer') {
544
+ shared.__txDepth = (shared.__txDepth || 0) + 1
545
+ } else {
546
+ this.#beginSnapshot()
547
+ }
548
+ this.#inTransaction = true
549
+ return noopCursor
550
+ }
351
551
  if (upper.startsWith('BEGIN')) {
352
552
  shared.__txDepth = (shared.__txDepth || 0) + 1
353
553
  this.#inTransaction = true
@@ -359,6 +559,7 @@ export class Database {
359
559
  upper.startsWith('END ') ||
360
560
  upper.startsWith('ROLLBACK')
361
561
  ) {
562
+ this.#dropSnapshot()
362
563
  shared.__txDepth = Math.max(0, (shared.__txDepth || 0) - 1)
363
564
  if (shared.__txDepth === 0) this.#inTransaction = false
364
565
  return noopCursor
@@ -404,6 +605,47 @@ export class Database {
404
605
  return sqlStorage.exec(sql)
405
606
  }
406
607
 
608
+ #beginSnapshot(): void {
609
+ this.#dropSnapshot()
610
+ const prefix = `${this.#snapshotPrefix}_${++this.#snapshotCounter}`
611
+ const tables = new Map<string, string>()
612
+ const rows = this.#sql
613
+ .exec(
614
+ `SELECT name FROM sqlite_master
615
+ WHERE type = 'table'
616
+ ORDER BY name`
617
+ )
618
+ .toArray()
619
+
620
+ for (const row of rows) {
621
+ const name = String(row.name ?? '')
622
+ if (!shouldSnapshotTable(name)) continue
623
+ const snapshot = snapshotTableName(prefix, name)
624
+ this.#sql.exec(
625
+ `CREATE TABLE ${quoteIdentifier(snapshot)} AS SELECT * FROM ${quoteIdentifier(name)}`
626
+ )
627
+ tables.set(name, snapshot)
628
+ }
629
+
630
+ this.#snapshotTables = tables
631
+ }
632
+
633
+ #dropSnapshot(): void {
634
+ const tables = this.#snapshotTables
635
+ if (!tables) return
636
+ this.#snapshotTables = null
637
+ for (const snapshot of tables.values()) {
638
+ try {
639
+ this.#sql.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(snapshot)}`)
640
+ } catch {}
641
+ }
642
+ }
643
+
644
+ _rewriteForSnapshot(sql: string): string {
645
+ if (!this.#snapshotTables) return sql
646
+ return rewriteSQLTables(sql, this.#snapshotTables)
647
+ }
648
+
407
649
  /** prepare a statement */
408
650
  prepare<T = Record<string, SqlStorageValue>>(sql: string): Statement<T> {
409
651
  if (!this.#open) {
@@ -535,7 +777,7 @@ export class Database {
535
777
  if (isTxCmd) {
536
778
  this._execTransactionAware(stmt, this.#sql)
537
779
  } else {
538
- this.#sql.exec(stmt)
780
+ this.#sql.exec(this._rewriteForSnapshot(stmt))
539
781
  }
540
782
  }
541
783
 
@@ -603,6 +845,8 @@ export class Database {
603
845
 
604
846
  /** close the database connection */
605
847
  close(): this {
848
+ this.#dropSnapshot()
849
+ activeSnapshotPrefixes.delete(this.#snapshotPrefix)
606
850
  this.#open = false
607
851
  return this
608
852
  }
@@ -697,6 +941,9 @@ export class StatementRunner {
697
941
  }
698
942
 
699
943
  beginConcurrent(): RunResult {
944
+ if (this.#storage?.transactionSync) {
945
+ return this.run('BEGIN CONCURRENT')
946
+ }
700
947
  return this.begin()
701
948
  }
702
949
 
@@ -709,14 +956,14 @@ export class StatementRunner {
709
956
 
710
957
  commit(): RunResult {
711
958
  if (this.#storage?.transactionSync) {
712
- return this.#txnResult
959
+ return this.run('COMMIT')
713
960
  }
714
961
  return this.run('COMMIT')
715
962
  }
716
963
 
717
964
  rollback(): RunResult {
718
965
  if (this.#storage?.transactionSync) {
719
- return this.#txnResult
966
+ return this.run('ROLLBACK')
720
967
  }
721
968
  return this.run('ROLLBACK')
722
969
  }
@@ -31,6 +31,35 @@ interface CFWebSocket {
31
31
  accept?(): void
32
32
  }
33
33
 
34
+ function debugWS(event: Record<string, unknown>): void {
35
+ try {
36
+ const log = (globalThis as any).__orez_ws_debug_log
37
+ if (typeof log === 'function') log(event)
38
+ } catch {
39
+ // debug logging must never affect websocket behavior
40
+ }
41
+ }
42
+
43
+ function summarizeWSData(data: unknown): Record<string, unknown> {
44
+ if (typeof data === 'string') {
45
+ return {
46
+ dataType: 'string',
47
+ bytes: data.length,
48
+ text: data.length > 300 ? `${data.slice(0, 300)}...` : data,
49
+ }
50
+ }
51
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
52
+ return { dataType: 'buffer', bytes: data.byteLength }
53
+ }
54
+ if (data instanceof ArrayBuffer) {
55
+ return { dataType: 'arraybuffer', bytes: data.byteLength }
56
+ }
57
+ if (ArrayBuffer.isView(data)) {
58
+ return { dataType: data.constructor.name, bytes: data.byteLength }
59
+ }
60
+ return { dataType: typeof data }
61
+ }
62
+
34
63
  // -- WebSocket shim --
35
64
  // wraps a CF WebSocket to match the ws package WebSocket API
36
65
 
@@ -198,6 +227,7 @@ class WebSocket extends EventEmitter {
198
227
  } else {
199
228
  this.#ws = urlOrSocket
200
229
  this.#url = ''
230
+ debugWS({ event: 'wrap-cf-socket', readyState: this.#ws.readyState })
201
231
  this.#setupListeners()
202
232
  }
203
233
  }
@@ -215,6 +245,7 @@ class WebSocket extends EventEmitter {
215
245
  cb?: (err?: Error) => void
216
246
  ): void {
217
247
  try {
248
+ debugWS({ event: 'send', url: this.#url, ...summarizeWSData(data) })
218
249
  if (typeof data === 'string') {
219
250
  this.#ws.send(data)
220
251
  } else if (Buffer.isBuffer(data)) {
@@ -299,15 +330,28 @@ class WebSocket extends EventEmitter {
299
330
  // error: (err: Error)
300
331
  const onMessage = (event: any) => {
301
332
  const data = event.data
333
+ debugWS({ event: 'message', url: this.#url, ...summarizeWSData(data) })
302
334
  this.emit('message', data, typeof data !== 'string')
303
335
  }
304
336
  const onClose = (event: any) => {
337
+ debugWS({
338
+ event: 'close',
339
+ url: this.#url,
340
+ code: event.code ?? 1000,
341
+ reason: event.reason ?? '',
342
+ })
305
343
  this.emit('close', event.code ?? 1000, event.reason ?? '')
306
344
  }
307
345
  const onError = (event: any) => {
346
+ debugWS({
347
+ event: 'error',
348
+ url: this.#url,
349
+ message: event?.message ?? event?.error?.message ?? 'WebSocket error',
350
+ })
308
351
  this.emit('error', event.error ?? new Error(event.message ?? 'WebSocket error'))
309
352
  }
310
353
  const onOpen = () => {
354
+ debugWS({ event: 'open', url: this.#url })
311
355
  this.emit('open')
312
356
  }
313
357
 
@@ -352,6 +396,7 @@ class WebSocketServer extends EventEmitter {
352
396
  callback: (ws: WebSocket) => void
353
397
  ): void {
354
398
  // wrap the CF WebSocket in our shim
399
+ debugWS({ event: 'handle-upgrade' })
355
400
  const ws = new WebSocket(socket as CFWebSocket)
356
401
  callback(ws)
357
402
  }
@@ -0,0 +1,11 @@
1
+ const globalProcess = ((globalThis as any).process ??= {})
2
+
3
+ globalProcess.env ??= {}
4
+ globalProcess.pid ??= 1
5
+ globalProcess.argv ??= []
6
+ globalProcess.kill ??= () => true
7
+
8
+ globalProcess.env.SINGLE_PROCESS = '1'
9
+ globalProcess.env.NODE_ENV ??= 'development'
10
+
11
+ export {}