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