orez 0.1.36 → 0.1.37

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 +32 -4
  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 +1167 -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 -4
  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 +1419 -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,1419 @@
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
+ * postgres shim for cloudflare workers.
6
+ *
7
+ * wraps a PGlite instance to implement the `postgres` npm package API
8
+ * that zero-cache uses. enables bundler aliasing so zero-cache talks to
9
+ * PGlite instead of a real postgres server.
10
+ *
11
+ * usage with bundler alias:
12
+ * alias: { 'postgres': './src/worker/shims/postgres.js' }
13
+ *
14
+ * usage directly:
15
+ * import { createPostgresShim } from 'orez/worker/shims/postgres'
16
+ * const sql = createPostgresShim(pglite)
17
+ * const rows = await sql`SELECT * FROM users WHERE id = ${id}`
18
+ */
19
+
20
+ import { PassThrough } from 'stream'
21
+
22
+ import { Mutex } from '../../mutex.js'
23
+ import {
24
+ handleStartReplication,
25
+ signalReplicationChange,
26
+ } from '../../replication/handler.js'
27
+
28
+ import type { PGlite, Results, Transaction } from '@electric-sql/pglite'
29
+
30
+ // -- PostgresError --
31
+
32
+ export class PostgresError extends Error {
33
+ name = 'PostgresError' as const
34
+ severity_local: string
35
+ severity: string
36
+ code: string
37
+ position: string
38
+ file: string
39
+ line: string
40
+ routine: string
41
+ detail?: string
42
+ hint?: string
43
+ schema_name?: string
44
+ table_name?: string
45
+ column_name?: string
46
+ constraint_name?: string
47
+ query: string
48
+ parameters: unknown[]
49
+
50
+ constructor(info: {
51
+ message?: string
52
+ code?: string
53
+ severity?: string
54
+ detail?: string
55
+ hint?: string
56
+ [key: string]: unknown
57
+ }) {
58
+ super(info.message || 'postgres error')
59
+ this.severity_local = (info.severity as string) || 'ERROR'
60
+ this.severity = (info.severity as string) || 'ERROR'
61
+ this.code = (info.code as string) || '00000'
62
+ this.position = (info.position as string) || ''
63
+ this.file = (info.file as string) || ''
64
+ this.line = (info.line as string) || ''
65
+ this.routine = (info.routine as string) || ''
66
+ this.detail = info.detail as string | undefined
67
+ this.hint = info.hint as string | undefined
68
+ this.schema_name = info.schema_name as string | undefined
69
+ this.table_name = info.table_name as string | undefined
70
+ this.column_name = info.column_name as string | undefined
71
+ this.constraint_name = info.constraint_name as string | undefined
72
+ this.query = (info.query as string) || ''
73
+ this.parameters = (info.parameters as unknown[]) || []
74
+ Object.assign(this, info)
75
+ }
76
+ }
77
+
78
+ // -- Identifier --
79
+ // returned by sql(string) for dynamic identifier escaping
80
+
81
+ class Identifier {
82
+ value: string
83
+ constructor(value: string) {
84
+ this.value = escapeIdentifier(value)
85
+ }
86
+ }
87
+
88
+ function escapeIdentifier(str: string): string {
89
+ return '"' + str.replace(/"/g, '""').replace(/\./g, '"."') + '"'
90
+ }
91
+
92
+ // -- result array --
93
+ // creates an array of rows that also has metadata properties (count, command, columns, statement, state)
94
+ // matching the RowList type from postgres
95
+
96
+ interface ResultMeta {
97
+ count: number
98
+ command: string
99
+ state: { status: string; pid: number; secret: number }
100
+ statement: { name: string; string: string; types: number[]; columns: ColumnMeta[] }
101
+ columns: ColumnMeta[]
102
+ }
103
+
104
+ interface ColumnMeta {
105
+ name: string
106
+ type: number
107
+ table: number
108
+ number: number
109
+ parser?: ((raw: string) => unknown) | undefined
110
+ }
111
+
112
+ type ResultArray<T = Record<string, unknown>> = T[] & ResultMeta
113
+
114
+ function createResultArray<T extends Record<string, unknown>>(
115
+ pgliteResult: Results<T>,
116
+ queryString: string
117
+ ): ResultArray<T> {
118
+ // guard against undefined/null results (e.g. DDL on PGlite proxy)
119
+ if (!pgliteResult) {
120
+ const empty = [] as unknown as ResultArray<T>
121
+ empty.count = 0
122
+ empty.command = detectCommand(queryString)
123
+ empty.state = { status: 'idle', pid: 0, secret: 0 }
124
+ empty.statement = { name: '', string: queryString, types: [], columns: [] }
125
+ empty.columns = []
126
+ return empty
127
+ }
128
+ const rows = pgliteResult.rows
129
+ const columns: ColumnMeta[] = (pgliteResult.fields || []).map((f, i) => ({
130
+ name: f.name,
131
+ type: f.dataTypeID,
132
+ table: 0,
133
+ number: i,
134
+ }))
135
+
136
+ // create a proper array with rows as elements
137
+ const result = [...rows] as ResultArray<T>
138
+
139
+ // attach metadata
140
+ const command = detectCommand(queryString)
141
+ // for SELECT queries affectedRows is 0, use row count instead
142
+ result.count =
143
+ command === 'SELECT' || !pgliteResult.affectedRows
144
+ ? rows.length
145
+ : pgliteResult.affectedRows
146
+ result.command = command
147
+ result.state = { status: 'idle', pid: 0, secret: 0 }
148
+ result.statement = {
149
+ name: '',
150
+ string: queryString,
151
+ types: [],
152
+ columns,
153
+ }
154
+ result.columns = columns
155
+
156
+ return result
157
+ }
158
+
159
+ function detectCommand(sql: string): string {
160
+ const trimmed = sql.trimStart().toUpperCase()
161
+ if (trimmed.startsWith('SELECT')) return 'SELECT'
162
+ if (trimmed.startsWith('INSERT')) return 'INSERT'
163
+ if (trimmed.startsWith('UPDATE')) return 'UPDATE'
164
+ if (trimmed.startsWith('DELETE')) return 'DELETE'
165
+ if (trimmed.startsWith('CREATE')) return 'CREATE'
166
+ if (trimmed.startsWith('DROP')) return 'DROP'
167
+ if (trimmed.startsWith('ALTER')) return 'ALTER'
168
+ return trimmed.split(/\s/)[0] || 'SELECT'
169
+ }
170
+
171
+ // -- multi-statement detection --
172
+ // detects if a query string contains multiple SQL statements.
173
+ // strips string literals and comments first to avoid false positives
174
+ // from semicolons inside quoted strings.
175
+
176
+ function hasMultipleStatements(sql: string): boolean {
177
+ // strip dollar-quoted strings ($$ ... $$)
178
+ let stripped = sql.replace(/(\$[a-zA-Z_]*\$)([\s\S]*?)\1/g, '')
179
+ // strip string literals (single-quoted, with '' escape)
180
+ stripped = stripped.replace(/'(?:[^']|'')*'/g, '')
181
+ // strip double-quoted identifiers
182
+ stripped = stripped.replace(/"(?:[^"]|"")*"/g, '')
183
+ // strip -- line comments
184
+ stripped = stripped.replace(/--[^\n]*/g, '')
185
+ // strip /* block comments */
186
+ stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '')
187
+
188
+ // check if there are multiple non-empty statements
189
+ const statements = stripped
190
+ .split(';')
191
+ .map((s) => s.trim())
192
+ .filter((s) => s.length > 0)
193
+
194
+ return statements.length > 1
195
+ }
196
+
197
+ // -- parameter serialization --
198
+ // convert js values to postgres-compatible parameter values
199
+
200
+ function serializeParam(value: unknown): unknown {
201
+ if (value === null || value === undefined) return null
202
+ if (value instanceof Identifier) return value // handled in template assembly
203
+ if (typeof value === 'bigint') return value.toString()
204
+ if (
205
+ typeof value === 'object' &&
206
+ !(value instanceof Date) &&
207
+ !Array.isArray(value) &&
208
+ !ArrayBuffer.isView(value)
209
+ ) {
210
+ return JSON.stringify(value)
211
+ }
212
+ return value
213
+ }
214
+
215
+ // -- template tag to parameterized query conversion --
216
+ // sql`SELECT * FROM foo WHERE id = ${id} AND name = ${name}`
217
+ // becomes: { text: 'SELECT * FROM foo WHERE id = $1 AND name = $2', params: [id, name] }
218
+
219
+ function buildQuery(
220
+ strings: TemplateStringsArray,
221
+ values: unknown[]
222
+ ): { text: string; params: unknown[] } {
223
+ const params: unknown[] = []
224
+ let text = ''
225
+
226
+ for (let i = 0; i < strings.length; i++) {
227
+ text += strings[i]
228
+ if (i < values.length) {
229
+ const val = values[i]
230
+ if (val instanceof Identifier) {
231
+ // identifiers are inlined (already escaped)
232
+ text += val.value
233
+ } else if (
234
+ val &&
235
+ typeof val === 'object' &&
236
+ '_isHelper' in val &&
237
+ (val as any)._isHelper
238
+ ) {
239
+ // sql(object) helper — expand based on preceding SQL context
240
+ const helper = val as {
241
+ _isHelper: true
242
+ _data: Record<string, any>
243
+ toInsert: () => any
244
+ toUpdate: () => any
245
+ }
246
+ const before = text.trimEnd().toUpperCase()
247
+ if (
248
+ /\)\s*$/.test(before) ||
249
+ /SET\s*$/i.test(before) ||
250
+ /DO\s+UPDATE\s+SET\s*$/i.test(before)
251
+ ) {
252
+ // UPDATE SET context: col1 = $1, col2 = $2
253
+ const { set, params: updateParams } = helper.toUpdate()
254
+ // rebase placeholder indices
255
+ const rebasedSet = set.replace(
256
+ /\$(\d+)/g,
257
+ (_: string, n: string) => `$${params.length + Number(n)}`
258
+ )
259
+ text += rebasedSet
260
+ params.push(...updateParams.map(serializeParam))
261
+ } else {
262
+ // INSERT context: (col1, col2) VALUES ($1, $2)
263
+ const {
264
+ columns,
265
+ values: placeholders,
266
+ params: insertParams,
267
+ } = helper.toInsert()
268
+ // rebase placeholder indices
269
+ const rebasedPlaceholders = placeholders.replace(
270
+ /\$(\d+)/g,
271
+ (_: string, n: string) => `$${params.length + Number(n)}`
272
+ )
273
+ text += `(${columns}) VALUES (${rebasedPlaceholders})`
274
+ params.push(...insertParams.map(serializeParam))
275
+ }
276
+ } else if (
277
+ val &&
278
+ typeof val === 'object' &&
279
+ '_isArrayExpansion' in val &&
280
+ (val as any)._isArrayExpansion
281
+ ) {
282
+ // sql([1,2,3]) — expand for IN clauses: ($1, $2, $3)
283
+ const arr = (val as any)._values as unknown[]
284
+ const placeholders = arr.map((_, j) => `$${params.length + j + 1}`).join(', ')
285
+ text += `(${placeholders})`
286
+ params.push(...arr.map(serializeParam))
287
+ } else if (Array.isArray(val)) {
288
+ // raw array in template tag
289
+ // check context: json_to_recordset etc. need JSON, everything else needs PG array
290
+ const before = text.trimEnd()
291
+ const needsJson =
292
+ /json_to_record(?:set)?|json_(?:array|build|each|populate)\s*\(\s*$/i.test(
293
+ before
294
+ ) ||
295
+ (/\(\s*$/.test(before) &&
296
+ /json/i.test(before.slice(Math.max(0, before.length - 40))))
297
+ if (needsJson) {
298
+ params.push(JSON.stringify(val))
299
+ text += `$${params.length}::json`
300
+ } else {
301
+ // PostgreSQL array literal: {val1,val2,...}
302
+ const pgArray = `{${val
303
+ .map((v: any) => {
304
+ if (v === null || v === undefined) return 'NULL'
305
+ const s = String(v)
306
+ // quote if contains special chars
307
+ if (
308
+ s.includes(',') ||
309
+ s.includes('"') ||
310
+ s.includes('{') ||
311
+ s.includes('}') ||
312
+ s.includes(' ')
313
+ ) {
314
+ return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
315
+ }
316
+ return s
317
+ })
318
+ .join(',')}}`
319
+ params.push(pgArray)
320
+ text += `$${params.length}`
321
+ }
322
+ } else {
323
+ const serialized = serializeParam(val)
324
+ params.push(serialized)
325
+ // add ::json cast for JSON values (PGlite needs explicit type for json_to_recordset etc.)
326
+ const needsJsonCast =
327
+ typeof serialized === 'string' &&
328
+ typeof val === 'object' &&
329
+ val !== null &&
330
+ (serialized.startsWith('[') || serialized.startsWith('{'))
331
+ text += `$${params.length}${needsJsonCast ? '::json' : ''}`
332
+ }
333
+ }
334
+ }
335
+
336
+ return { text, params }
337
+ }
338
+
339
+ // -- pending query --
340
+ // wraps a promise to add .simple(), .readable(), .writable(), .execute(), .describe(), .values(), .raw()
341
+
342
+ function createPendingQuery<T>(
343
+ promise: Promise<T>
344
+ ): T extends any[]
345
+ ? Promise<T> & PendingQueryModifiers
346
+ : Promise<T> & PendingQueryModifiers {
347
+ const pending = promise as any
348
+
349
+ pending.simple = () => pending
350
+ pending.execute = () => pending
351
+ pending.cancel = () => {}
352
+
353
+ pending.describe = () =>
354
+ Promise.reject(new Error('describe() not supported in worker mode'))
355
+ pending.values = () =>
356
+ promise.then((rows: any) => {
357
+ if (!Array.isArray(rows)) return []
358
+ return rows.map((row: any) => Object.values(row))
359
+ })
360
+ pending.raw = () => Promise.reject(new Error('raw() not supported in worker mode'))
361
+
362
+ pending.readable = () => {
363
+ throw new Error('readable() not supported in worker mode')
364
+ }
365
+ pending.writable = () => {
366
+ throw new Error('writable() not supported in worker mode')
367
+ }
368
+
369
+ pending.forEach = (cb: (row: any, result: any) => void) =>
370
+ promise.then((rows: any) => {
371
+ const result = { count: rows.length, command: rows.command || 'SELECT' }
372
+ for (const row of rows) cb(row, result)
373
+ return result
374
+ })
375
+
376
+ // cursor: returns async iterable yielding batches of rows
377
+ pending.cursor = (batchSize: number = 100) => ({
378
+ [Symbol.asyncIterator]() {
379
+ let allRows: any[] | null = null
380
+ let offset = 0
381
+ return {
382
+ async next() {
383
+ if (!allRows) {
384
+ const result = await promise
385
+ allRows = Array.isArray(result) ? result : []
386
+ }
387
+ if (offset >= allRows.length) return { done: true as const, value: undefined }
388
+ const batch = allRows.slice(offset, offset + batchSize)
389
+ offset += batchSize
390
+ return { done: false as const, value: batch }
391
+ },
392
+ }
393
+ },
394
+ })
395
+
396
+ pending.stream = () => {
397
+ throw new Error('stream() is deprecated, use forEach()')
398
+ }
399
+
400
+ return pending
401
+ }
402
+
403
+ interface PendingQueryModifiers {
404
+ simple(): this
405
+ readable(): never
406
+ writable(): never
407
+ execute(): this
408
+ cancel(): void
409
+ describe(): Promise<never>
410
+ values(): Promise<never>
411
+ raw(): Promise<never>
412
+ forEach(cb: (row: any, result: any) => void): Promise<any>
413
+ cursor(...args: unknown[]): never
414
+ }
415
+
416
+ type ReplicationCapableDb = {
417
+ query<T>(sql: string, params?: unknown[]): Promise<Results<T>>
418
+ exec(sql: string): Promise<Array<Results> | void>
419
+ listen?: (channel: string, cb: () => void) => Promise<() => Promise<void>>
420
+ closed?: boolean
421
+ }
422
+
423
+ function getSharedMutex(target: object): Mutex {
424
+ const existing = (target as any).__orez_mutex as Mutex | undefined
425
+ if (existing) return existing
426
+ const mutex = new Mutex()
427
+ ;(target as any).__orez_mutex = mutex
428
+ return mutex
429
+ }
430
+
431
+ // -- raw wire protocol helper --
432
+ // bypasses PGlite's JS mutexes (Fe2/ke2) by calling execProtocolRawSync directly.
433
+ // used for replication protocol queries and external queries that would otherwise
434
+ // deadlock against zero-cache's long-running transaction pool.
435
+ function rawQuery(pglite: PGlite, sql: string): any[] {
436
+ const pg = pglite as any
437
+ if (!pg.execProtocolRawSync) return []
438
+ const enc = new TextEncoder()
439
+ const sqlBytes = enc.encode(sql + '\0')
440
+ const len = 4 + sqlBytes.length
441
+ const msg = new Uint8Array(1 + len)
442
+ msg[0] = 0x51 // Q message
443
+ new DataView(msg.buffer).setInt32(1, len)
444
+ msg.set(sqlBytes, 5)
445
+ const result = pg.execProtocolRawSync(msg) as Uint8Array
446
+ // parse wire protocol response
447
+ const rows: any[] = []
448
+ const fields: string[] = []
449
+ let pos = 0
450
+ const dv = new DataView(result.buffer, result.byteOffset, result.byteLength)
451
+ while (pos < result.length) {
452
+ const type = result[pos]
453
+ const msgLen = dv.getInt32(pos + 1) + 1
454
+ if (type === 0x54) {
455
+ // RowDescription
456
+ const nFields = dv.getInt16(pos + 5)
457
+ let fpos = pos + 7
458
+ for (let i = 0; i < nFields; i++) {
459
+ let end = fpos
460
+ while (result[end] !== 0) end++
461
+ fields.push(new TextDecoder().decode(result.subarray(fpos, end)))
462
+ fpos = end + 1 + 18
463
+ }
464
+ } else if (type === 0x44) {
465
+ // DataRow
466
+ const nCols = dv.getInt16(pos + 5)
467
+ const row: any = {}
468
+ let cpos = pos + 7
469
+ for (let i = 0; i < nCols; i++) {
470
+ const colLen = dv.getInt32(cpos)
471
+ cpos += 4
472
+ if (colLen === -1) {
473
+ row[fields[i]] = null
474
+ } else {
475
+ row[fields[i]] = new TextDecoder().decode(result.subarray(cpos, cpos + colLen))
476
+ cpos += colLen
477
+ }
478
+ }
479
+ rows.push(row)
480
+ } else if (type === 0x45) {
481
+ // ErrorResponse
482
+ break
483
+ }
484
+ pos += msgLen
485
+ }
486
+ return rows
487
+ }
488
+
489
+ function rawExec(pglite: PGlite, sql: string): void {
490
+ const pg = pglite as any
491
+ if (!pg.execProtocolRawSync) return
492
+ const enc = new TextEncoder()
493
+ const sqlBytes = enc.encode(sql + '\0')
494
+ const len = 4 + sqlBytes.length
495
+ const msg = new Uint8Array(1 + len)
496
+ msg[0] = 0x51
497
+ new DataView(msg.buffer).setInt32(1, len)
498
+ msg.set(sqlBytes, 5)
499
+ pg.execProtocolRawSync(msg)
500
+ }
501
+
502
+ // create a proxy around PGlite that routes query/exec through raw wire protocol
503
+ // this is needed because the replication handler runs continuously and would
504
+ // deadlock against zero-cache's transaction pool if it used the normal PGlite API
505
+ function createRawDbProxy(pg: PGlite): PGlite {
506
+ if (!(pg as any).execProtocolRawSync) return pg
507
+ return new Proxy(pg, {
508
+ get(target, prop) {
509
+ if (prop === 'query') {
510
+ return async (sql: string, params?: any[]) => {
511
+ // for parameterized queries, fall back to normal query
512
+ // (raw protocol doesn't support parameters easily)
513
+ if (params?.length) return target.query(sql, params)
514
+ const rows = rawQuery(target, sql)
515
+ return { rows, fields: [], affectedRows: 0 }
516
+ }
517
+ }
518
+ if (prop === 'exec') {
519
+ return async (sql: string) => {
520
+ rawExec(target, sql)
521
+ return []
522
+ }
523
+ }
524
+ if (prop === 'listen') {
525
+ // skip listen in browser — it's a secondary signal mechanism
526
+ return async () => async () => {}
527
+ }
528
+ if (prop === 'closed') return target.closed
529
+ return (target as any)[prop]
530
+ },
531
+ })
532
+ }
533
+
534
+ // -- execute a query, routing multi-statement to exec() --
535
+ // PGlite.query() only handles single statements. multi-statement DDL
536
+ // (schema migrations, etc.) must use exec(). when params are present
537
+ // AND the query is multi-statement, we split and run each individually.
538
+
539
+ // intercept replication-related queries that PGlite can't handle natively.
540
+ // these are sent by zero-cache during initialization (wal_level check,
541
+ // replication slot management, etc.) and need fake responses.
542
+ // uses rawQuery/rawExec to bypass PGlite's transaction mutex.
543
+ async function interceptReplicationQuery(
544
+ text: string,
545
+ pglite: PGlite
546
+ ): Promise<ResultArray<any> | null> {
547
+ const upper = text.trimStart().toUpperCase()
548
+
549
+ // wal_level check: zero-cache verifies logical replication is enabled
550
+ if (
551
+ upper.includes('WAL_LEVEL') &&
552
+ (upper.includes('CURRENT_SETTING') || upper.startsWith('SHOW'))
553
+ ) {
554
+ if (upper.includes('VERSION')) {
555
+ return fakeResult([{ walLevel: 'logical', version: '170004' }], text)
556
+ }
557
+ return fakeResult([{ walLevel: 'logical' }], text)
558
+ }
559
+
560
+ // CREATE_REPLICATION_SLOT: zero-cache creates a slot during initial sync
561
+ if (upper.includes('CREATE_REPLICATION_SLOT')) {
562
+ const match = text.match(/CREATE_REPLICATION_SLOT\s+(?:"([^"]+)"|'([^']+)'|(\S+))/i)
563
+ const slotName = match?.[1] || match?.[2] || match?.[3] || 'zero_slot'
564
+ const lsn = '0/1000100'
565
+ try {
566
+ await pglite.exec(`
567
+ CREATE TABLE IF NOT EXISTS _orez._zero_replication_slots (
568
+ slot_name TEXT PRIMARY KEY, restart_lsn TEXT,
569
+ confirmed_flush_lsn TEXT, wal_status TEXT DEFAULT 'reserved'
570
+ )
571
+ `)
572
+ await pglite.exec(
573
+ `INSERT INTO _orez._zero_replication_slots (slot_name, restart_lsn, confirmed_flush_lsn)
574
+ VALUES ('${slotName.replace(/'/g, "''")}', '${lsn}', '${lsn}')
575
+ ON CONFLICT (slot_name) DO UPDATE SET restart_lsn = '${lsn}'`
576
+ )
577
+ } catch {}
578
+ return fakeResult(
579
+ [
580
+ {
581
+ slot_name: slotName,
582
+ consistent_point: lsn,
583
+ snapshot_name: '00000003-00000001-1',
584
+ output_plugin: 'pgoutput',
585
+ },
586
+ ],
587
+ text
588
+ )
589
+ }
590
+
591
+ // DROP_REPLICATION_SLOT
592
+ if (upper.startsWith('DROP_REPLICATION_SLOT')) {
593
+ return fakeResult([], text, 'DROP_REPLICATION_SLOT')
594
+ }
595
+
596
+ // pg_replication_slots query
597
+ if (upper.includes('PG_REPLICATION_SLOTS') && upper.includes('SELECT')) {
598
+ try {
599
+ const result = await pglite.query(
600
+ `SELECT slot_name, restart_lsn as "restartLSN", wal_status as "walStatus"
601
+ FROM _orez._zero_replication_slots`
602
+ )
603
+ return createResultArray(result as any, text)
604
+ } catch {
605
+ return fakeResult([], text)
606
+ }
607
+ }
608
+
609
+ // IDENTIFY_SYSTEM
610
+ if (upper === 'IDENTIFY_SYSTEM' || upper === 'IDENTIFY_SYSTEM;') {
611
+ return fakeResult(
612
+ [
613
+ {
614
+ systemid: '1234567890',
615
+ timeline: '1',
616
+ xlogpos: '0/1000100',
617
+ dbname: 'template1',
618
+ },
619
+ ],
620
+ text
621
+ )
622
+ }
623
+
624
+ // ALTER ROLE ... REPLICATION
625
+ if (upper.startsWith('ALTER ROLE') && upper.includes('REPLICATION')) {
626
+ return fakeResult([], text, 'ALTER ROLE')
627
+ }
628
+
629
+ // SET TRANSACTION / SET SESSION / SET LOCAL — PGlite doesn't support SET LOCAL
630
+ if (
631
+ upper.startsWith('SET TRANSACTION') ||
632
+ upper.startsWith('SET SESSION') ||
633
+ upper.startsWith('SET LOCAL')
634
+ ) {
635
+ return fakeResult([], text, 'SET')
636
+ }
637
+
638
+ // pg_settings query (wal_sender_timeout etc.) — not available in PGlite
639
+ if (upper.includes('PG_SETTINGS') && upper.includes('WAL_SENDER_TIMEOUT')) {
640
+ return fakeResult([{ walSenderTimeoutMs: 60000 }], text)
641
+ }
642
+
643
+ // event triggers: PGlite doesn't support them (requires superuser),
644
+ // and DDL detection isn't needed in browser mode
645
+ if (
646
+ upper.includes('EVENT TRIGGER') &&
647
+ (upper.startsWith('DROP') || upper.startsWith('CREATE'))
648
+ ) {
649
+ return fakeResult([], text, upper.startsWith('DROP') ? 'DROP' : 'CREATE')
650
+ }
651
+
652
+ return null
653
+ }
654
+
655
+ function fakeResult(
656
+ rows: Record<string, unknown>[],
657
+ queryString: string,
658
+ command?: string
659
+ ): ResultArray<any> {
660
+ const columns: ColumnMeta[] =
661
+ rows.length > 0
662
+ ? Object.keys(rows[0]).map((name, i) => ({ name, type: 25, table: 0, number: i }))
663
+ : []
664
+ const result = [...rows] as ResultArray<any>
665
+ result.count = rows.length
666
+ result.command = command || detectCommand(queryString)
667
+ result.state = { status: 'idle', pid: 0, secret: 0 }
668
+ result.statement = { name: '', string: queryString, types: [], columns }
669
+ result.columns = columns
670
+ return result
671
+ }
672
+
673
+ async function executeQuery(
674
+ executor: {
675
+ query<T>(sql: string, params?: unknown[]): Promise<Results<T>>
676
+ exec(sql: string): Promise<Array<Results>>
677
+ },
678
+ text: string,
679
+ params: unknown[],
680
+ pglite?: PGlite
681
+ ): Promise<ResultArray<any>> {
682
+ // intercept replication-related queries before they reach PGlite
683
+ if (pglite) {
684
+ const intercepted = await interceptReplicationQuery(text, pglite)
685
+ if (intercepted) return intercepted
686
+ }
687
+
688
+ // strip FK constraints from CREATE TABLE in browser mode.
689
+ // without a wrapping transaction, DEFERRABLE can't defer across separate
690
+ // INSERT statements — zero-cache inserts desires before queries exist.
691
+ // single-connection browser dev preview doesn't need FK enforcement.
692
+ if (/FOREIGN\s+KEY/i.test(text) && /CREATE\s+TABLE/i.test(text)) {
693
+ text = text.replace(
694
+ /,?\s*(?:CONSTRAINT\s+\w+\s+)?FOREIGN\s+KEY\s*\([^)]*\)\s*REFERENCES\s+[^,(]+(?:\s*\([^)]*\))?(?:\s+ON\s+(?:DELETE|UPDATE)\s+(?:CASCADE|SET\s+NULL|SET\s+DEFAULT|RESTRICT|NO\s+ACTION))*(?:\s+DEFERRABLE[^,)]*)?/gi,
695
+ ''
696
+ )
697
+ }
698
+
699
+ const isMulti = hasMultipleStatements(text)
700
+
701
+ if (!isMulti) {
702
+ // use normal PGlite query — rawQuery breaks transaction state
703
+ const r = await (params.length > 0
704
+ ? executor.query(text, params)
705
+ : executor.query(text))
706
+ const result = createResultArray(r as Results<any>, text)
707
+ if (isWriteCommand(text)) signalReplicationChange()
708
+ return result
709
+ }
710
+
711
+ // multi-statement: ALWAYS split and run individually
712
+ if (params.length === 0) {
713
+ const stmts = splitStatements(text)
714
+ let lastResult: Results<any> = { rows: [], fields: [], affectedRows: 0 } as any
715
+ for (const stmt of stmts) {
716
+ lastResult = (await executor.query(stmt)) as Results<any>
717
+ }
718
+ const result = createResultArray(lastResult, text)
719
+ if (isWriteCommand(text)) signalReplicationChange()
720
+ return result
721
+ }
722
+
723
+ // multi-statement WITH params — split and run each statement,
724
+ // distributing $N params to the correct statement
725
+ const statements = splitStatements(text)
726
+ let lastResult: Results<any> = { rows: [], fields: [], affectedRows: 0 } as any
727
+
728
+ for (const stmt of statements) {
729
+ // find which $N params this statement references
730
+ const paramRefs = [...stmt.matchAll(/\$(\d+)/g)].map((m) => Number(m[1]))
731
+
732
+ if (paramRefs.length > 0) {
733
+ // remap $N to $1, $2, ... for this statement's params
734
+ const stmtParams = paramRefs.map((n) => params[n - 1])
735
+ let remapped = stmt
736
+ paramRefs.forEach((origN, i) => {
737
+ remapped = remapped.replace(new RegExp(`\\$${origN}\\b`), `$${i + 1}`)
738
+ })
739
+ lastResult = (await executor.query(remapped, stmtParams)) as Results<any>
740
+ } else {
741
+ // no params in this statement — can use query() directly
742
+ lastResult = (await executor.query(stmt)) as Results<any>
743
+ }
744
+ }
745
+
746
+ const result = createResultArray(lastResult, text)
747
+ if (isWriteCommand(text)) signalReplicationChange()
748
+ return result
749
+ }
750
+
751
+ const WRITE_COMMANDS = new Set(['INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER'])
752
+
753
+ function isWriteCommand(sql: string): boolean {
754
+ return WRITE_COMMANDS.has(detectCommand(sql))
755
+ }
756
+
757
+ // split SQL into individual statements, respecting string literals
758
+ function splitStatements(sql: string): string[] {
759
+ // strip string literals to find real semicolons
760
+ const literals: string[] = []
761
+ // dollar-quoted strings first ($$ ... $$ or $tag$ ... $tag$)
762
+ let stripped = sql.replace(/(\$[a-zA-Z_]*\$)([\s\S]*?)\1/g, (match) => {
763
+ literals.push(match)
764
+ return `__LIT${literals.length - 1}__`
765
+ })
766
+ stripped = stripped.replace(/'(?:[^']|'')*'/g, (match) => {
767
+ literals.push(match)
768
+ return `__LIT${literals.length - 1}__`
769
+ })
770
+ stripped = stripped.replace(/"(?:[^"]|"")*"/g, (match) => {
771
+ literals.push(match)
772
+ return `__LIT${literals.length - 1}__`
773
+ })
774
+ // strip -- comments
775
+ stripped = stripped.replace(/--[^\n]*/g, '')
776
+ // strip /* block comments */
777
+ stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '')
778
+
779
+ // split on semicolons
780
+ const parts = stripped
781
+ .split(';')
782
+ .map((s) => s.trim())
783
+ .filter((s) => s.length > 0)
784
+
785
+ // restore literals
786
+ return parts.map((part) => part.replace(/__LIT(\d+)__/g, (_, i) => literals[Number(i)]))
787
+ }
788
+
789
+ // -- sql function factory for a given executor --
790
+ // used both for the top-level sql and for transaction sql
791
+
792
+ function createSqlFunction(
793
+ executor: {
794
+ query<T>(sql: string, params?: unknown[]): Promise<Results<T>>
795
+ exec(sql: string): Promise<Array<Results>>
796
+ },
797
+ rootPglite?: PGlite
798
+ ) {
799
+ function sql(first: any, ...rest: any[]): any {
800
+ // tagged template: sql`SELECT ...`
801
+ if (first && Array.isArray(first.raw)) {
802
+ const { text, params } = buildQuery(first as TemplateStringsArray, rest)
803
+ const promise = executeQuery(executor, text, params, rootPglite)
804
+ return createPendingQuery(promise)
805
+ }
806
+
807
+ // function call with string: sql('identifier') => Identifier
808
+ if (typeof first === 'string' && rest.length === 0) {
809
+ return new Identifier(first)
810
+ }
811
+
812
+ // sql(object) — helper for dynamic INSERT/UPDATE
813
+ if (typeof first === 'object' && first !== null && !Array.isArray(first)) {
814
+ return {
815
+ _isHelper: true,
816
+ _data: first,
817
+ toInsert() {
818
+ const keys = Object.keys(first)
819
+ const columns = keys.map((k) => '"' + k.replace(/"/g, '""') + '"').join(', ')
820
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ')
821
+ return { columns, values: placeholders, params: keys.map((k) => first[k]) }
822
+ },
823
+ toUpdate() {
824
+ const keys = Object.keys(first)
825
+ const set = keys
826
+ .map((k, i) => `"${k.replace(/"/g, '""')}" = $${i + 1}`)
827
+ .join(', ')
828
+ return { set, params: keys.map((k) => first[k]) }
829
+ },
830
+ }
831
+ }
832
+
833
+ // sql(array) — parameter expansion for IN clauses
834
+ // wrap in marker object so buildQuery knows to expand (not serialize as JSON)
835
+ if (Array.isArray(first)) {
836
+ return { _isArrayExpansion: true, _values: first }
837
+ }
838
+
839
+ throw new Error('postgres shim: unsupported sql() call')
840
+ }
841
+
842
+ return sql
843
+ }
844
+
845
+ // -- COPY TO STDOUT support --
846
+
847
+ function createCopyPendingQuery(
848
+ copyQuery: string,
849
+ executor: { query: (sql: string, params?: unknown[]) => Promise<Results<any>> }
850
+ ): any {
851
+ // extract the query from COPY (SELECT ...) TO STDOUT or COPY table TO STDOUT
852
+ let selectQuery: string
853
+ const parenMatch = copyQuery.match(/COPY\s*\(([\s\S]+)\)\s*TO\s+STDOUT/i)
854
+ if (parenMatch) {
855
+ selectQuery = parenMatch[1].trim()
856
+ } else {
857
+ const tableMatch = copyQuery.match(
858
+ /COPY\s+("(?:[^"]|"")*"(?:\."(?:[^"]|"")*")*|\S+)\s+TO\s+STDOUT/i
859
+ )
860
+ selectQuery = tableMatch ? `SELECT * FROM ${tableMatch[1]}` : 'SELECT 1 WHERE false'
861
+ }
862
+
863
+ // returns a Node.js Readable stream (via PassThrough) compatible with pipeline()
864
+ const readablePromise = (async () => {
865
+ const result = await executor.query(selectQuery)
866
+ const rows = result.rows as any[]
867
+ const pt = new PassThrough()
868
+
869
+ // write all rows as TSV-encoded COPY output, using Buffer for stream compatibility
870
+ const encoder = typeof Buffer !== 'undefined' ? null : new TextEncoder()
871
+ for (const row of rows) {
872
+ const values = Object.values(row).map((v: any) => {
873
+ if (v === null || v === undefined) return '\\N'
874
+ if (typeof v === 'boolean') return v ? 't' : 'f'
875
+ if (typeof v === 'object')
876
+ return JSON.stringify(v)
877
+ .replace(/\\/g, '\\\\')
878
+ .replace(/\t/g, '\\t')
879
+ .replace(/\n/g, '\\n')
880
+ return String(v)
881
+ .replace(/\\/g, '\\\\')
882
+ .replace(/\t/g, '\\t')
883
+ .replace(/\n/g, '\\n')
884
+ })
885
+ const line = values.join('\t') + '\n'
886
+ const chunk = encoder ? encoder.encode(line) : Buffer.from(line)
887
+ pt.push(chunk)
888
+ }
889
+ // signal end of stream
890
+ pt.push(null)
891
+ return pt
892
+ })()
893
+
894
+ const pending = readablePromise as any
895
+ pending.execute = () => pending
896
+ pending.simple = () => pending
897
+ pending.cancel = () => {}
898
+ pending.readable = () => readablePromise
899
+ pending.writable = () => Promise.resolve(new PassThrough())
900
+ pending.describe = () => Promise.reject(new Error('not supported'))
901
+ pending.values = () => Promise.reject(new Error('not supported'))
902
+ pending.raw = () => Promise.reject(new Error('not supported'))
903
+ pending.forEach = () => Promise.reject(new Error('not supported'))
904
+ pending.cursor = () => {
905
+ throw new Error('not supported')
906
+ }
907
+ pending.stream = () => {
908
+ throw new Error('not supported')
909
+ }
910
+ return pending
911
+ }
912
+
913
+ function createReplicationPendingQuery(
914
+ replicationQuery: string,
915
+ db: ReplicationCapableDb
916
+ ): any {
917
+ const mutex = getSharedMutex(db as object)
918
+ const writable = new PassThrough()
919
+ let started = false
920
+ let startPromise: Promise<void> | null = null
921
+ let destroyed = false
922
+
923
+ // use PassThrough — AND expose a direct callback for the pipe to use.
924
+ // stream-browserify's PassThrough + Duplexify stop flowing after idle,
925
+ // so we also push data via a callback that the patched pipe() can use.
926
+ const readable = new PassThrough()
927
+ // direct data callback — bypasses broken stream plumbing entirely.
928
+ // registered on globalThis so the patched pipe() can find it regardless
929
+ // of how many stream wrappers (Duplexify, etc.) sit between.
930
+ const dataListeners: Array<(chunk: Buffer) => void> = []
931
+ ;(globalThis as any).__orez_repl_data_push = (fn: (chunk: Buffer) => void) => {
932
+ dataListeners.push(fn)
933
+ }
934
+
935
+ const start = async () => {
936
+ if (started) return
937
+ started = true
938
+ startPromise = handleStartReplication(
939
+ replicationQuery,
940
+ {
941
+ write(chunk: Uint8Array) {
942
+ if (destroyed) return
943
+ // skip CopyBothResponse, unwrap CopyData
944
+ if (chunk[0] === 0x57) return
945
+ const data =
946
+ chunk[0] === 0x64 && chunk.length >= 6
947
+ ? Buffer.from(chunk.subarray(5))
948
+ : Buffer.from(chunk)
949
+ readable.write(data)
950
+ // also call pipe handlers directly — stream polyfills are broken in browser
951
+ const handlers = (globalThis as any).__orez_pipe_handlers as
952
+ | Array<(chunk: Buffer) => void>
953
+ | undefined
954
+ if (handlers && handlers.length > 0) {
955
+ ;(globalThis as any).__orez_repl_bypass_count =
956
+ ((globalThis as any).__orez_repl_bypass_count || 0) + 1
957
+ }
958
+ if (handlers) {
959
+ for (const fn of handlers) {
960
+ try {
961
+ fn(data)
962
+ } catch {}
963
+ }
964
+ }
965
+ },
966
+ get closed() {
967
+ return destroyed || writable.destroyed || !!db.closed
968
+ },
969
+ },
970
+ db as PGlite,
971
+ mutex
972
+ ).catch((err) => {
973
+ // don't destroy the readable on handler errors — the change-streamer
974
+ // would reconnect with a new subscription, losing the reference the
975
+ // producer holds. just log the error and let the handler restart.
976
+ console.warn(
977
+ '[orez:repl] handler error:',
978
+ err instanceof Error ? err.message : String(err)
979
+ )
980
+ })
981
+ return Promise.resolve()
982
+ }
983
+
984
+ const pending = Promise.resolve() as any
985
+ pending.execute = () => pending
986
+ pending.simple = () => pending
987
+ pending.cancel = () => {
988
+ destroyed = true
989
+ readable.destroy()
990
+ writable.destroy()
991
+ }
992
+ pending.readable = async () => {
993
+ await start()
994
+ return readable
995
+ }
996
+ pending.writable = async () => {
997
+ await start()
998
+ return writable
999
+ }
1000
+ pending.describe = () => Promise.reject(new Error('not supported'))
1001
+ pending.values = () => Promise.reject(new Error('not supported'))
1002
+ pending.raw = () => Promise.reject(new Error('not supported'))
1003
+ pending.forEach = () => Promise.reject(new Error('not supported'))
1004
+ pending.cursor = () => {
1005
+ throw new Error('not supported')
1006
+ }
1007
+ pending.stream = () => {
1008
+ throw new Error('not supported')
1009
+ }
1010
+ return pending
1011
+ }
1012
+
1013
+ // -- type parsers --
1014
+ // pre-populated parsers matching the postgres npm package's type registry.
1015
+
1016
+ function identity(x: string) {
1017
+ return x
1018
+ }
1019
+ function parseFloat_(x: string) {
1020
+ return parseFloat(x)
1021
+ }
1022
+ function parseInt_(x: string) {
1023
+ return parseInt(x, 10)
1024
+ }
1025
+ function parseBool(x: string) {
1026
+ return x === 't' || x === 'true'
1027
+ }
1028
+ function parseJSON(x: string) {
1029
+ try {
1030
+ return JSON.parse(x)
1031
+ } catch {
1032
+ return x
1033
+ }
1034
+ }
1035
+
1036
+ function makeArrayParser(elementParser: (x: string) => unknown) {
1037
+ const fn = (x: string) => {
1038
+ if (!x || x === '{}') return []
1039
+ const inner = x.slice(1, -1)
1040
+ return inner.split(',').map((v) => {
1041
+ if (v === 'NULL') return null
1042
+ return elementParser(v.replace(/^"|"$/g, ''))
1043
+ })
1044
+ }
1045
+ ;(fn as any).array = true
1046
+ return fn
1047
+ }
1048
+
1049
+ function buildDefaultParsers(): Record<number, (value: string) => unknown> {
1050
+ const p: Record<number, any> = {}
1051
+ // scalar types
1052
+ p[16] = parseBool // bool
1053
+ p[17] = identity // bytea
1054
+ p[20] = identity // int8 (bigint as string)
1055
+ p[21] = parseInt_ // int2
1056
+ p[23] = parseInt_ // int4
1057
+ p[25] = identity // text
1058
+ p[26] = parseInt_ // oid
1059
+ p[114] = parseJSON // json
1060
+ p[700] = parseFloat_ // float4
1061
+ p[701] = parseFloat_ // float8
1062
+ p[1042] = identity // bpchar
1063
+ p[1043] = identity // varchar
1064
+ p[1082] = identity // date
1065
+ p[1083] = identity // time
1066
+ // timestamps → epoch ms (zero-cache expects number, not string)
1067
+ const parseTimestamp = (val: string) => {
1068
+ if (typeof val === 'number') return val
1069
+ const d = Date.parse(val)
1070
+ return isNaN(d) ? val : d
1071
+ }
1072
+ p[1114] = parseTimestamp // timestamp
1073
+ p[1184] = parseTimestamp // timestamptz
1074
+ p[1266] = identity // timetz
1075
+ p[1700] = identity // numeric
1076
+ p[2950] = identity // uuid
1077
+ p[3802] = parseJSON // jsonb
1078
+ // array types
1079
+ p[1000] = makeArrayParser(parseBool) // bool[]
1080
+ p[1005] = makeArrayParser(parseInt_) // int2[]
1081
+ p[1007] = makeArrayParser(parseInt_) // int4[]
1082
+ p[1009] = makeArrayParser(identity) // text[]
1083
+ p[1016] = makeArrayParser(identity) // int8[]
1084
+ p[1021] = makeArrayParser(parseFloat_) // float4[]
1085
+ p[1022] = makeArrayParser(parseFloat_) // float8[]
1086
+ p[1015] = makeArrayParser(identity) // varchar[]
1087
+ p[1182] = makeArrayParser(identity) // date[]
1088
+ p[1115] = makeArrayParser(identity) // timestamp[]
1089
+ p[1185] = makeArrayParser(identity) // timestamptz[]
1090
+ p[2951] = makeArrayParser(identity) // uuid[]
1091
+ p[199] = makeArrayParser(parseJSON) // json[]
1092
+ p[3807] = makeArrayParser(parseJSON) // jsonb[]
1093
+ return p
1094
+ }
1095
+
1096
+ // -- main export --
1097
+
1098
+ export interface PostgresShimOptions {
1099
+ max?: number
1100
+ max_lifetime?: number
1101
+ idle_timeout?: number
1102
+ fetch_types?: boolean
1103
+ ssl?: unknown
1104
+ onnotice?: (notice: unknown) => void
1105
+ connection?: Record<string, unknown>
1106
+ types?: Record<string, unknown>
1107
+ }
1108
+
1109
+ export function createPostgresShim(pglite: PGlite, opts?: PostgresShimOptions) {
1110
+ const sqlFn = createSqlFunction(pglite, pglite)
1111
+
1112
+ function sql(first: any, ...rest: any[]): any {
1113
+ return sqlFn(first, ...rest)
1114
+ }
1115
+
1116
+ // sql.unsafe(queryString, params?) — raw SQL execution
1117
+ sql.unsafe = (queryString: string, params?: unknown[]) => {
1118
+ const upper = queryString.trimStart().toUpperCase()
1119
+
1120
+ // START_REPLICATION — expose an in-process duplex stream backed by
1121
+ // orez's replication handler instead of the TCP protocol adapter.
1122
+ if (upper.startsWith('START_REPLICATION')) {
1123
+ return createReplicationPendingQuery(queryString, pglite as ReplicationCapableDb)
1124
+ }
1125
+
1126
+ // COPY TO STDOUT — returns readable stream of rows
1127
+ if (upper.startsWith('COPY') && upper.includes('TO STDOUT')) {
1128
+ return createCopyPendingQuery(queryString, pglite)
1129
+ }
1130
+
1131
+ // strip FK constraints from CREATE TABLE (see executeQuery for why)
1132
+ if (/FOREIGN\s+KEY/i.test(queryString) && /CREATE\s+TABLE/i.test(queryString)) {
1133
+ queryString = queryString.replace(
1134
+ /,?\s*(?:CONSTRAINT\s+\w+\s+)?FOREIGN\s+KEY\s*\([^)]*\)\s*REFERENCES\s+[^,(]+(?:\s*\([^)]*\))?(?:\s+ON\s+(?:DELETE|UPDATE)\s+(?:CASCADE|SET\s+NULL|SET\s+DEFAULT|RESTRICT|NO\s+ACTION))*(?:\s+DEFERRABLE[^,)]*)?/gi,
1135
+ ''
1136
+ )
1137
+ }
1138
+
1139
+ const serializedParams = (params ?? []).map(serializeParam)
1140
+
1141
+ // multi-statement with no params: split and run each individually
1142
+ // PGliteWorker's execProtocol rejects multi-statement, so always split upfront
1143
+ if (hasMultipleStatements(queryString) && serializedParams.length === 0) {
1144
+ const promise = (async () => {
1145
+ const intercepted = await interceptReplicationQuery(queryString, pglite)
1146
+ if (intercepted) return intercepted
1147
+ {
1148
+ const statements = splitStatements(queryString)
1149
+ const resultArrays = []
1150
+ for (const stmt of statements) {
1151
+ const r = await pglite.query(stmt)
1152
+ resultArrays.push(createResultArray(r as Results<any>, stmt))
1153
+ }
1154
+ const combined = resultArrays as any
1155
+ combined.count = resultArrays.length
1156
+ combined.command = 'SELECT'
1157
+ combined.state = { status: 'idle', pid: 0, secret: 0 }
1158
+ combined.statement = { name: '', string: queryString, types: [], columns: [] }
1159
+ combined.columns = []
1160
+ return combined
1161
+ }
1162
+ })()
1163
+ return createPendingQuery(promise)
1164
+ }
1165
+
1166
+ const promise = executeQuery(pglite, queryString, serializedParams, pglite)
1167
+ return createPendingQuery(promise)
1168
+ }
1169
+
1170
+ // sql.begin(options?, callback) — transactions
1171
+ //
1172
+ // BROWSER FIX: don't use pglite.transaction() for the top-level callback.
1173
+ // zero-cache's transaction pool holds the callback open indefinitely
1174
+ // (await dequeue() loop), which permanently holds PGlite's Fe2 mutex
1175
+ // and deadlocks ALL external queries (push handler, pg-query, etc.).
1176
+ //
1177
+ // instead, route queries directly through PGlite — each query acquires
1178
+ // and releases Fe2 independently. this trades transaction atomicity for
1179
+ // Fe2 availability, which is acceptable in the browser dev preview
1180
+ // (PGlite is single-connection so operations are serialized anyway).
1181
+ //
1182
+ // nested begin/savepoint calls still use real pglite.transaction()
1183
+ // since those are short-lived (task-scoped, not pool-scoped).
1184
+ sql.begin = async (
1185
+ optionsOrCb: string | ((tx: any) => any),
1186
+ maybeCb?: (tx: any) => any
1187
+ ) => {
1188
+ const cb = typeof optionsOrCb === 'function' ? optionsOrCb : maybeCb!
1189
+
1190
+ // create sql function backed by pglite directly (not a Transaction)
1191
+ // each query acquires/releases Fe2 independently
1192
+ const txSql = createSqlFunction(pglite, pglite)
1193
+
1194
+ function txSqlFn(first: any, ...rest: any[]): any {
1195
+ return txSql(first, ...rest)
1196
+ }
1197
+
1198
+ // unsafe: routes through pglite directly
1199
+ txSqlFn.unsafe = (queryString: string, params?: unknown[]) => {
1200
+ const upper = queryString.trimStart().toUpperCase()
1201
+ if (upper.startsWith('COPY') && upper.includes('TO STDOUT')) {
1202
+ return createCopyPendingQuery(queryString, pglite)
1203
+ }
1204
+
1205
+ const serializedParams = (params ?? []).map(serializeParam)
1206
+
1207
+ if (hasMultipleStatements(queryString) && serializedParams.length === 0) {
1208
+ const promise = (async () => {
1209
+ const stmts = splitStatements(queryString)
1210
+ const resultArrays = []
1211
+ for (const stmt of stmts) {
1212
+ const r = await pglite.query(stmt)
1213
+ resultArrays.push(createResultArray(r as Results<any>, stmt))
1214
+ }
1215
+ const combined = resultArrays as any
1216
+ combined.count = resultArrays.length
1217
+ combined.command = 'SELECT'
1218
+ combined.state = { status: 'idle', pid: 0, secret: 0 }
1219
+ combined.statement = {
1220
+ name: '',
1221
+ string: queryString,
1222
+ types: [],
1223
+ columns: [],
1224
+ }
1225
+ combined.columns = []
1226
+ return combined
1227
+ })()
1228
+ return createPendingQuery(promise)
1229
+ }
1230
+
1231
+ const promise = executeQuery(pglite, queryString, serializedParams, pglite)
1232
+ return createPendingQuery(promise)
1233
+ }
1234
+
1235
+ // nested begin: use real pglite.transaction() for atomicity (short-lived)
1236
+ // nested begin: non-transactional (same as top level)
1237
+ // real pglite.transaction() holds Fe2 during async SQLite work in the syncer,
1238
+ // which deadlocks any concurrent PGlite queries. non-transactional avoids this.
1239
+ txSqlFn.begin = async (
1240
+ innerOptOrCb: string | ((tx: any) => any),
1241
+ innerMaybeCb?: (tx: any) => any
1242
+ ) => {
1243
+ const innerCb = typeof innerOptOrCb === 'function' ? innerOptOrCb : innerMaybeCb!
1244
+ const innerTxSql = createSqlFunction(pglite, pglite)
1245
+ function innerTxSqlFn(first: any, ...rest: any[]): any {
1246
+ return innerTxSql(first, ...rest)
1247
+ }
1248
+ innerTxSqlFn.unsafe = txSqlFn.unsafe
1249
+ innerTxSqlFn.begin = txSqlFn.begin
1250
+ innerTxSqlFn.savepoint = txSqlFn.savepoint
1251
+ innerTxSqlFn.end = async () => {}
1252
+ innerTxSqlFn.options = sql.options
1253
+ innerTxSqlFn.PostgresError = PostgresError
1254
+ try {
1255
+ const result = await innerCb(innerTxSqlFn)
1256
+ return Array.isArray(result) ? await Promise.all(result) : result
1257
+ } finally {
1258
+ signalReplicationChange()
1259
+ }
1260
+ }
1261
+
1262
+ // savepoint at top level: DON'T use pglite.transaction() — same reasoning
1263
+ // as sql.begin(). the callback might be long-running (e.g. setupTriggers).
1264
+ // just run the callback with PGlite-backed sql (each query acquires/releases Fe2).
1265
+ let _savepointIdx = 0
1266
+ txSqlFn.savepoint = async (nameOrFn: any, maybeFn?: any) => {
1267
+ const fn = typeof nameOrFn === 'function' ? nameOrFn : maybeFn
1268
+ return fn(txSqlFn)
1269
+ }
1270
+
1271
+ txSqlFn.end = async () => {}
1272
+ txSqlFn.options = sql.options
1273
+ txSqlFn.PostgresError = PostgresError
1274
+
1275
+ // use explicit BEGIN/COMMIT/ROLLBACK SQL instead of pglite.transaction().
1276
+ // each statement acquires/releases Fe2 independently, so the mutex is
1277
+ // NOT held during async gaps in the callback — avoiding the deadlock
1278
+ // that pglite.transaction() causes with zero-cache's long-running pool.
1279
+ await pglite.exec('BEGIN')
1280
+ try {
1281
+ const result = await cb(txSqlFn)
1282
+ await pglite.exec('COMMIT')
1283
+ return Array.isArray(result) ? await Promise.all(result) : result
1284
+ } catch (err) {
1285
+ await pglite.exec('ROLLBACK')
1286
+ throw err
1287
+ } finally {
1288
+ signalReplicationChange()
1289
+ }
1290
+ }
1291
+
1292
+ // sql.end() — no-op (PGlite lifecycle managed elsewhere)
1293
+ sql.end = async (_opts?: { timeout?: number }) => {}
1294
+
1295
+ // sql.close() — alias for end
1296
+ sql.close = sql.end
1297
+
1298
+ // sql.options — connection metadata
1299
+ sql.options = {
1300
+ host: ['localhost'],
1301
+ port: [5432],
1302
+ database: 'pglite',
1303
+ user: 'pglite',
1304
+ max: opts?.max ?? 1,
1305
+ parsers: buildDefaultParsers(),
1306
+ fetch_types: opts?.fetch_types ?? true,
1307
+ connection: opts?.connection ?? {},
1308
+ ssl: opts?.ssl ?? false,
1309
+ types: opts?.types ?? {},
1310
+ transform: {
1311
+ undefined: undefined,
1312
+ column: { from: undefined, to: undefined },
1313
+ value: { from: undefined, to: undefined },
1314
+ row: { from: undefined, to: undefined },
1315
+ },
1316
+ serializers: {} as Record<number, (value: unknown) => unknown>,
1317
+ }
1318
+
1319
+ // sql.PostgresError — error class
1320
+ sql.PostgresError = PostgresError
1321
+
1322
+ // sql.CLOSE / sql.END — sentinel objects
1323
+ sql.CLOSE = {} as Record<string, never>
1324
+ sql.END = sql.CLOSE
1325
+
1326
+ // sql.parameters — server parameters
1327
+ sql.parameters = {
1328
+ application_name: 'pglite-shim',
1329
+ server_version: '17.0',
1330
+ }
1331
+
1332
+ // sql.types / sql.typed — type helpers
1333
+ sql.typed = (value: unknown, oid: number) => ({ value, type: oid })
1334
+ sql.types = sql.typed
1335
+
1336
+ // sql.json — json parameter helper
1337
+ sql.json = (value: unknown) => JSON.stringify(value)
1338
+
1339
+ // sql.array — array parameter helper
1340
+ sql.array = (value: unknown[], type?: number) => ({ value, type, array: true })
1341
+
1342
+ // sql.listen — not supported
1343
+ sql.listen = () => {
1344
+ throw new Error('listen() not supported in worker mode')
1345
+ }
1346
+
1347
+ // sql.notify — not supported
1348
+ sql.notify = () => {
1349
+ throw new Error('notify() not supported in worker mode')
1350
+ }
1351
+
1352
+ // sql.subscribe — not supported
1353
+ sql.subscribe = () => {
1354
+ throw new Error('subscribe() not supported in worker mode')
1355
+ }
1356
+
1357
+ // sql.reserve — not supported
1358
+ sql.reserve = () => {
1359
+ throw new Error('reserve() not supported in worker mode')
1360
+ }
1361
+
1362
+ // sql.file — not supported
1363
+ sql.file = () => {
1364
+ throw new Error('file() not supported in worker mode')
1365
+ }
1366
+
1367
+ // sql.largeObject — not supported
1368
+ sql.largeObject = () => {
1369
+ throw new Error('largeObject() not supported in worker mode')
1370
+ }
1371
+
1372
+ return sql
1373
+ }
1374
+
1375
+ // -- default export --
1376
+ // matches the `postgres` package's default export shape:
1377
+ // import postgres from 'postgres'
1378
+ // const sql = postgres(url, options)
1379
+ //
1380
+ // when used as a bundler alias, zero-cache calls postgres(connectionURI, options).
1381
+ // we intercept by reading the PGlite instance from globalThis.__orez_pglite.
1382
+
1383
+ function postgres(
1384
+ _urlOrOpts?: string | PostgresShimOptions,
1385
+ opts?: PostgresShimOptions
1386
+ ): ReturnType<typeof createPostgresShim> {
1387
+ // multi-instance routing: if __orez_pglite_instances is set, route by URL
1388
+ const instances = (globalThis as any).__orez_pglite_instances as
1389
+ | { postgres: PGlite; cvr: PGlite; cdb: PGlite }
1390
+ | undefined
1391
+ let pglite: PGlite | undefined
1392
+ if (instances && typeof _urlOrOpts === 'string') {
1393
+ if (_urlOrOpts.includes('/zero_cvr')) pglite = instances.cvr
1394
+ else if (_urlOrOpts.includes('/zero_cdb')) pglite = instances.cdb
1395
+ else pglite = instances.postgres
1396
+ } else {
1397
+ pglite = (globalThis as any).__orez_pglite as PGlite | undefined
1398
+ }
1399
+ if (!pglite) {
1400
+ throw new Error(
1401
+ 'postgres shim: no PGlite instance found. ' +
1402
+ 'set globalThis.__orez_pglite or __orez_pglite_instances.'
1403
+ )
1404
+ }
1405
+
1406
+ const resolvedOpts = typeof _urlOrOpts === 'object' ? _urlOrOpts : opts
1407
+ return createPostgresShim(pglite, resolvedOpts)
1408
+ }
1409
+
1410
+ // attach PostgresError and BigInt to the default export (matches postgres package)
1411
+ postgres.PostgresError = PostgresError
1412
+ postgres.BigInt = {
1413
+ to: 20,
1414
+ from: [20],
1415
+ parse: (x: string) => globalThis.BigInt(x),
1416
+ serialize: (x: bigint) => x.toString(),
1417
+ }
1418
+
1419
+ export default postgres