orez 0.2.27 → 0.2.30

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 (157) hide show
  1. package/dist/cf-do/worker.d.ts +3 -0
  2. package/dist/cf-do/worker.d.ts.map +1 -1
  3. package/dist/cf-do/worker.js +37 -15
  4. package/dist/cf-do/worker.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +8 -0
  7. package/dist/index.js.map +1 -1
  8. package/package.json +3 -4
  9. package/src/admin/admin-data.test.ts +0 -348
  10. package/src/admin/http-proxy.ts +0 -252
  11. package/src/admin/log-store.ts +0 -192
  12. package/src/admin/server.ts +0 -471
  13. package/src/admin/ui.ts +0 -1322
  14. package/src/bench/proxy-throughput.bench.ts +0 -343
  15. package/src/bench/serial-mutations.bench.ts +0 -270
  16. package/src/browser.ts +0 -203
  17. package/src/cf-do/.wrangler/cache/cf.json +0 -1
  18. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
  19. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
  20. package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
  21. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
  22. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
  23. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
  24. package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
  25. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +0 -11
  26. package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +0 -134
  27. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +0 -11
  28. package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +0 -134
  29. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +0 -1059
  30. package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +0 -8
  31. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +0 -1059
  32. package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +0 -8
  33. package/src/cf-do/ARCHITECTURE.md +0 -93
  34. package/src/cf-do/CHAT_E2E.md +0 -213
  35. package/src/cf-do/watermark.test.ts +0 -103
  36. package/src/cf-do/watermark.ts +0 -118
  37. package/src/cf-do/worker.ts +0 -1041
  38. package/src/cf-do/wrangler.toml +0 -11
  39. package/src/cf-pglite/README.md +0 -19
  40. package/src/change-tracking.ts +0 -25
  41. package/src/child-process.test.ts +0 -147
  42. package/src/child-process.ts +0 -90
  43. package/src/cli-entry.ts +0 -72
  44. package/src/cli.test.ts +0 -40
  45. package/src/cli.ts +0 -1214
  46. package/src/config.ts +0 -150
  47. package/src/do-sql-tracking.test.ts +0 -19
  48. package/src/do-sql-tracking.ts +0 -19
  49. package/src/index.ts +0 -1215
  50. package/src/integration/integration.test.ts +0 -517
  51. package/src/integration/native-binary.guard.test.ts +0 -13
  52. package/src/integration/native-startup.test.ts +0 -44
  53. package/src/integration/replication-latency.test.ts +0 -428
  54. package/src/integration/restore-live-stress.test.ts +0 -433
  55. package/src/integration/restore-reset.test.ts +0 -400
  56. package/src/integration/restore.test.ts +0 -274
  57. package/src/integration/test-permissions.ts +0 -147
  58. package/src/load-config.ts +0 -46
  59. package/src/log.ts +0 -96
  60. package/src/mutex.ts +0 -47
  61. package/src/pg-proxy-browser.singledb.test.ts +0 -233
  62. package/src/pg-proxy-browser.ts +0 -2022
  63. package/src/pg-proxy-do-backend.test.ts +0 -3890
  64. package/src/pg-proxy-do-backend.ts +0 -7191
  65. package/src/pg-proxy.ts +0 -1087
  66. package/src/pg-sqlite-compiler/README.md +0 -53
  67. package/src/pg-sqlite-compiler/catalog/seed.ts +0 -524
  68. package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +0 -307
  69. package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +0 -377
  70. package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +0 -12
  71. package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +0 -447
  72. package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +0 -32
  73. package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +0 -397
  74. package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +0 -337
  75. package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +0 -337
  76. package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +0 -537
  77. package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +0 -1837
  78. package/src/pg-sqlite-compiler/index.ts +0 -73
  79. package/src/pg-sqlite-compiler/integration.test.ts +0 -136
  80. package/src/pg-sqlite-compiler/passes/ast-utils.ts +0 -113
  81. package/src/pg-sqlite-compiler/passes/catalog.ts +0 -65
  82. package/src/pg-sqlite-compiler/passes/datetime.ts +0 -74
  83. package/src/pg-sqlite-compiler/passes/index.ts +0 -49
  84. package/src/pg-sqlite-compiler/passes/types.ts +0 -156
  85. package/src/pg-sqlite-compiler/smoke.test.ts +0 -69
  86. package/src/pg-sqlite-compiler/test/catalog.test.ts +0 -171
  87. package/src/pg-sqlite-compiler/test/corpus.test.ts +0 -161
  88. package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +0 -102
  89. package/src/pg-sqlite-compiler/test/oracle.ts +0 -237
  90. package/src/pg-sqlite-compiler/test/types.test.ts +0 -109
  91. package/src/pg-sqlite-compiler/types.ts +0 -63
  92. package/src/pglite-ipc.test.ts +0 -116
  93. package/src/pglite-ipc.ts +0 -266
  94. package/src/pglite-manager.ts +0 -557
  95. package/src/pglite-web-proxy.test.ts +0 -57
  96. package/src/pglite-web-proxy.ts +0 -221
  97. package/src/pglite-web-worker.ts +0 -152
  98. package/src/pglite-worker-thread.ts +0 -253
  99. package/src/port.ts +0 -25
  100. package/src/process-title.ts +0 -9
  101. package/src/recovery.ts +0 -155
  102. package/src/replication/change-tracker.test.ts +0 -357
  103. package/src/replication/change-tracker.ts +0 -279
  104. package/src/replication/handler.test.ts +0 -511
  105. package/src/replication/handler.ts +0 -1190
  106. package/src/replication/pgoutput-encoder.test.ts +0 -697
  107. package/src/replication/pgoutput-encoder.ts +0 -373
  108. package/src/replication/tcp-replication.test.ts +0 -876
  109. package/src/replication/zero-compat.test.ts +0 -1150
  110. package/src/restore-stress.test.ts +0 -188
  111. package/src/s3-local.ts +0 -203
  112. package/src/shim/hooks.mjs +0 -120
  113. package/src/shim/register.mjs +0 -4
  114. package/src/sqlite-mode/apply-mode.ts +0 -224
  115. package/src/sqlite-mode/index.ts +0 -15
  116. package/src/sqlite-mode/native-binary.ts +0 -89
  117. package/src/sqlite-mode/package-resolve.ts +0 -17
  118. package/src/sqlite-mode/resolve-mode.ts +0 -80
  119. package/src/sqlite-mode/shim-template.ts +0 -159
  120. package/src/sqlite-mode/sqlite-mode.test.ts +0 -427
  121. package/src/sqlite-mode/types.ts +0 -30
  122. package/src/vite-plugin.ts +0 -67
  123. package/src/wasm-sqlite.test.ts +0 -537
  124. package/src/worker/browser-admin.ts +0 -52
  125. package/src/worker/browser-build-config.test.ts +0 -71
  126. package/src/worker/browser-build-config.ts +0 -109
  127. package/src/worker/browser-embed-admin.test.ts +0 -75
  128. package/src/worker/browser-embed.ts +0 -345
  129. package/src/worker/cf-patches.ts +0 -384
  130. package/src/worker/embed-integration.test.ts +0 -321
  131. package/src/worker/index.ts +0 -138
  132. package/src/worker/shims/fastify.test.ts +0 -255
  133. package/src/worker/shims/fastify.ts +0 -306
  134. package/src/worker/shims/http-service.test.ts +0 -355
  135. package/src/worker/shims/http-service.ts +0 -293
  136. package/src/worker/shims/node-stub.ts +0 -290
  137. package/src/worker/shims/oxfmt.ts +0 -3
  138. package/src/worker/shims/postgres-browser.ts +0 -59
  139. package/src/worker/shims/postgres-socket.test.ts +0 -576
  140. package/src/worker/shims/postgres-socket.ts +0 -310
  141. package/src/worker/shims/postgres.test.ts +0 -364
  142. package/src/worker/shims/postgres.ts +0 -1454
  143. package/src/worker/shims/sqlite-browser.test.ts +0 -233
  144. package/src/worker/shims/sqlite-browser.ts +0 -175
  145. package/src/worker/shims/sqlite.test.ts +0 -786
  146. package/src/worker/shims/sqlite.ts +0 -978
  147. package/src/worker/shims/stream-browser.ts +0 -15
  148. package/src/worker/shims/ws-browser.test.ts +0 -205
  149. package/src/worker/shims/ws-browser.ts +0 -248
  150. package/src/worker/shims/ws.test.ts +0 -288
  151. package/src/worker/shims/ws.ts +0 -467
  152. package/src/worker/shims/zero-process-env.ts +0 -11
  153. package/src/worker/types.ts +0 -75
  154. package/src/worker/worker-integration.test.ts +0 -223
  155. package/src/worker/worker.test.ts +0 -136
  156. package/src/worker/zero-cache-embed-cf.ts +0 -463
  157. package/src/worker/zero-cache-embed.ts +0 -277
@@ -1,1454 +0,0 @@
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
- // if query includes pg_terminate_backend, it's stopExistingReplicationSlotSubscribers
610
- // — PGlite has no real replication slots or active_pid, return empty result
611
- if (upper.includes('PG_TERMINATE_BACKEND')) {
612
- return fakeResult([], text)
613
- }
614
- try {
615
- const result = await pglite.query(
616
- `SELECT slot_name, restart_lsn as "restartLSN", wal_status as "walStatus"
617
- FROM _orez._zero_replication_slots`
618
- )
619
- return createResultArray(result as any, text)
620
- } catch {
621
- return fakeResult([], text)
622
- }
623
- }
624
-
625
- // IDENTIFY_SYSTEM
626
- if (upper === 'IDENTIFY_SYSTEM' || upper === 'IDENTIFY_SYSTEM;') {
627
- return fakeResult(
628
- [
629
- {
630
- systemid: '1234567890',
631
- timeline: '1',
632
- xlogpos: '0/1000100',
633
- dbname: 'template1',
634
- },
635
- ],
636
- text
637
- )
638
- }
639
-
640
- // ALTER ROLE ... REPLICATION
641
- if (upper.startsWith('ALTER ROLE') && upper.includes('REPLICATION')) {
642
- return fakeResult([], text, 'ALTER ROLE')
643
- }
644
-
645
- // SET TRANSACTION / SET SESSION / SET LOCAL — PGlite doesn't support SET LOCAL
646
- if (
647
- upper.startsWith('SET TRANSACTION') ||
648
- upper.startsWith('SET SESSION') ||
649
- upper.startsWith('SET LOCAL')
650
- ) {
651
- return fakeResult([], text, 'SET')
652
- }
653
-
654
- // pg_settings query (wal_sender_timeout etc.) — not available in PGlite
655
- if (upper.includes('PG_SETTINGS') && upper.includes('WAL_SENDER_TIMEOUT')) {
656
- return fakeResult([{ walSenderTimeoutMs: 60000 }], text)
657
- }
658
-
659
- // event triggers: PGlite doesn't support them (requires superuser),
660
- // and DDL detection isn't needed in browser mode
661
- if (
662
- upper.includes('EVENT TRIGGER') &&
663
- (upper.startsWith('DROP') || upper.startsWith('CREATE'))
664
- ) {
665
- return fakeResult([], text, upper.startsWith('DROP') ? 'DROP' : 'CREATE')
666
- }
667
-
668
- return null
669
- }
670
-
671
- function fakeResult(
672
- rows: Record<string, unknown>[],
673
- queryString: string,
674
- command?: string
675
- ): ResultArray<any> {
676
- const columns: ColumnMeta[] =
677
- rows.length > 0
678
- ? Object.keys(rows[0]).map((name, i) => ({ name, type: 25, table: 0, number: i }))
679
- : []
680
- const result = [...rows] as ResultArray<any>
681
- result.count = rows.length
682
- result.command = command || detectCommand(queryString)
683
- result.state = { status: 'idle', pid: 0, secret: 0 }
684
- result.statement = { name: '', string: queryString, types: [], columns }
685
- result.columns = columns
686
- return result
687
- }
688
-
689
- async function executeQuery(
690
- executor: {
691
- query<T>(sql: string, params?: unknown[]): Promise<Results<T>>
692
- exec(sql: string): Promise<Array<Results>>
693
- },
694
- text: string,
695
- params: unknown[],
696
- pglite?: PGlite
697
- ): Promise<ResultArray<any>> {
698
- // intercept replication-related queries before they reach PGlite
699
- if (pglite) {
700
- const intercepted = await interceptReplicationQuery(text, pglite)
701
- if (intercepted) return intercepted
702
- }
703
-
704
- // strip FK constraints — PGlite doesn't support cross-schema FKs,
705
- // and browser single-process mode doesn't need FK enforcement.
706
- // covers CREATE TABLE inline FKs and ALTER TABLE ADD CONSTRAINT FKs.
707
- if (/FOREIGN\s+KEY/i.test(text)) {
708
- if (/CREATE\s+TABLE/i.test(text)) {
709
- text = text.replace(
710
- /,?\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,
711
- ''
712
- )
713
- }
714
- if (/ALTER\s+TABLE/i.test(text) && /ADD\s+CONSTRAINT/i.test(text)) {
715
- text = text.replace(
716
- /ALTER\s+TABLE\s+[^\s]+\s+ADD\s+CONSTRAINT\s+[^\s]+\s*\n?\s*FOREIGN\s+KEY\s*\([^)]*\)\s*\n?\s*REFERENCES\s+[^;]+/gi,
717
- 'SELECT 1'
718
- )
719
- }
720
- }
721
-
722
- const isMulti = hasMultipleStatements(text)
723
-
724
- if (!isMulti) {
725
- // use normal PGlite query — rawQuery breaks transaction state
726
- const r = await (params.length > 0
727
- ? executor.query(text, params)
728
- : executor.query(text))
729
- const result = createResultArray(r as Results<any>, text)
730
- if (isWriteCommand(text)) {
731
- signalReplicationChange()
732
- debouncedSignal() // also fire debounced in case immediate signal is consumed
733
- }
734
- return result
735
- }
736
-
737
- // multi-statement: ALWAYS split and run individually
738
- if (params.length === 0) {
739
- const stmts = splitStatements(text)
740
- let lastResult: Results<any> = { rows: [], fields: [], affectedRows: 0 } as any
741
- for (const stmt of stmts) {
742
- lastResult = (await executor.query(stmt)) as Results<any>
743
- }
744
- const result = createResultArray(lastResult, text)
745
- if (isWriteCommand(text)) signalReplicationChange()
746
- return result
747
- }
748
-
749
- // multi-statement WITH params — split and run each statement,
750
- // distributing $N params to the correct statement
751
- const statements = splitStatements(text)
752
- let lastResult: Results<any> = { rows: [], fields: [], affectedRows: 0 } as any
753
-
754
- for (const stmt of statements) {
755
- // find which $N params this statement references
756
- const paramRefs = [...stmt.matchAll(/\$(\d+)/g)].map((m) => Number(m[1]))
757
-
758
- if (paramRefs.length > 0) {
759
- // remap $N to $1, $2, ... for this statement's params
760
- const stmtParams = paramRefs.map((n) => params[n - 1])
761
- let remapped = stmt
762
- paramRefs.forEach((origN, i) => {
763
- remapped = remapped.replace(new RegExp(`\\$${origN}\\b`), `$${i + 1}`)
764
- })
765
- lastResult = (await executor.query(remapped, stmtParams)) as Results<any>
766
- } else {
767
- // no params in this statement — can use query() directly
768
- lastResult = (await executor.query(stmt)) as Results<any>
769
- }
770
- }
771
-
772
- const result = createResultArray(lastResult, text)
773
- if (isWriteCommand(text)) signalReplicationChange()
774
- return result
775
- }
776
-
777
- const WRITE_COMMANDS = new Set(['INSERT', 'UPDATE', 'DELETE', 'CREATE', 'DROP', 'ALTER'])
778
-
779
- function isWriteCommand(sql: string): boolean {
780
- return WRITE_COMMANDS.has(detectCommand(sql))
781
- }
782
-
783
- // split SQL into individual statements, respecting string literals
784
- function splitStatements(sql: string): string[] {
785
- // strip string literals to find real semicolons
786
- const literals: string[] = []
787
- // dollar-quoted strings first ($$ ... $$ or $tag$ ... $tag$)
788
- let stripped = sql.replace(/(\$[a-zA-Z_]*\$)([\s\S]*?)\1/g, (match) => {
789
- literals.push(match)
790
- return `__LIT${literals.length - 1}__`
791
- })
792
- stripped = stripped.replace(/'(?:[^']|'')*'/g, (match) => {
793
- literals.push(match)
794
- return `__LIT${literals.length - 1}__`
795
- })
796
- stripped = stripped.replace(/"(?:[^"]|"")*"/g, (match) => {
797
- literals.push(match)
798
- return `__LIT${literals.length - 1}__`
799
- })
800
- // strip -- comments
801
- stripped = stripped.replace(/--[^\n]*/g, '')
802
- // strip /* block comments */
803
- stripped = stripped.replace(/\/\*[\s\S]*?\*\//g, '')
804
-
805
- // split on semicolons
806
- const parts = stripped
807
- .split(';')
808
- .map((s) => s.trim())
809
- .filter((s) => s.length > 0)
810
-
811
- // restore literals
812
- return parts.map((part) => part.replace(/__LIT(\d+)__/g, (_, i) => literals[Number(i)]))
813
- }
814
-
815
- // -- sql function factory for a given executor --
816
- // used both for the top-level sql and for transaction sql
817
-
818
- function createSqlFunction(
819
- executor: {
820
- query<T>(sql: string, params?: unknown[]): Promise<Results<T>>
821
- exec(sql: string): Promise<Array<Results>>
822
- },
823
- rootPglite?: PGlite
824
- ) {
825
- function sql(first: any, ...rest: any[]): any {
826
- // tagged template: sql`SELECT ...`
827
- if (first && Array.isArray(first.raw)) {
828
- const { text, params } = buildQuery(first as TemplateStringsArray, rest)
829
- const promise = executeQuery(executor, text, params, rootPglite)
830
- return createPendingQuery(promise)
831
- }
832
-
833
- // function call with string: sql('identifier') => Identifier
834
- if (typeof first === 'string' && rest.length === 0) {
835
- return new Identifier(first)
836
- }
837
-
838
- // sql(object) — helper for dynamic INSERT/UPDATE
839
- if (typeof first === 'object' && first !== null && !Array.isArray(first)) {
840
- return {
841
- _isHelper: true,
842
- _data: first,
843
- toInsert() {
844
- const keys = Object.keys(first)
845
- const columns = keys.map((k) => '"' + k.replace(/"/g, '""') + '"').join(', ')
846
- const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ')
847
- return { columns, values: placeholders, params: keys.map((k) => first[k]) }
848
- },
849
- toUpdate() {
850
- const keys = Object.keys(first)
851
- const set = keys
852
- .map((k, i) => `"${k.replace(/"/g, '""')}" = $${i + 1}`)
853
- .join(', ')
854
- return { set, params: keys.map((k) => first[k]) }
855
- },
856
- }
857
- }
858
-
859
- // sql(array) — parameter expansion for IN clauses
860
- // wrap in marker object so buildQuery knows to expand (not serialize as JSON)
861
- if (Array.isArray(first)) {
862
- return { _isArrayExpansion: true, _values: first }
863
- }
864
-
865
- throw new Error('postgres shim: unsupported sql() call')
866
- }
867
-
868
- return sql
869
- }
870
-
871
- // -- COPY TO STDOUT support --
872
-
873
- function createCopyPendingQuery(
874
- copyQuery: string,
875
- executor: { query: (sql: string, params?: unknown[]) => Promise<Results<any>> }
876
- ): any {
877
- // extract the query from COPY (SELECT ...) TO STDOUT or COPY table TO STDOUT
878
- let selectQuery: string
879
- const parenMatch = copyQuery.match(/COPY\s*\(([\s\S]+)\)\s*TO\s+STDOUT/i)
880
- if (parenMatch) {
881
- selectQuery = parenMatch[1].trim()
882
- } else {
883
- const tableMatch = copyQuery.match(
884
- /COPY\s+("(?:[^"]|"")*"(?:\."(?:[^"]|"")*")*|\S+)\s+TO\s+STDOUT/i
885
- )
886
- selectQuery = tableMatch ? `SELECT * FROM ${tableMatch[1]}` : 'SELECT 1 WHERE false'
887
- }
888
-
889
- // returns a Node.js Readable stream (via PassThrough) compatible with pipeline()
890
- const readablePromise = (async () => {
891
- const result = await executor.query(selectQuery)
892
- const rows = result.rows as any[]
893
- const pt = new PassThrough()
894
-
895
- // write all rows as TSV-encoded COPY output, using Buffer for stream compatibility
896
- const encoder = typeof Buffer !== 'undefined' ? null : new TextEncoder()
897
- for (const row of rows) {
898
- const values = Object.values(row).map((v: any) => {
899
- if (v === null || v === undefined) return '\\N'
900
- if (typeof v === 'boolean') return v ? 't' : 'f'
901
- if (typeof v === 'object')
902
- return JSON.stringify(v)
903
- .replace(/\\/g, '\\\\')
904
- .replace(/\t/g, '\\t')
905
- .replace(/\n/g, '\\n')
906
- return String(v)
907
- .replace(/\\/g, '\\\\')
908
- .replace(/\t/g, '\\t')
909
- .replace(/\n/g, '\\n')
910
- })
911
- const line = values.join('\t') + '\n'
912
- const chunk = encoder ? encoder.encode(line) : Buffer.from(line)
913
- pt.push(chunk)
914
- }
915
- // signal end of stream
916
- pt.push(null)
917
- return pt
918
- })()
919
-
920
- const pending = readablePromise as any
921
- pending.execute = () => pending
922
- pending.simple = () => pending
923
- pending.cancel = () => {}
924
- pending.readable = () => readablePromise
925
- pending.writable = () => Promise.resolve(new PassThrough())
926
- pending.describe = () => Promise.reject(new Error('not supported'))
927
- pending.values = () => Promise.reject(new Error('not supported'))
928
- pending.raw = () => Promise.reject(new Error('not supported'))
929
- pending.forEach = () => Promise.reject(new Error('not supported'))
930
- pending.cursor = () => {
931
- throw new Error('not supported')
932
- }
933
- pending.stream = () => {
934
- throw new Error('not supported')
935
- }
936
- return pending
937
- }
938
-
939
- function createReplicationPendingQuery(
940
- replicationQuery: string,
941
- db: ReplicationCapableDb
942
- ): any {
943
- const mutex = getSharedMutex(db as object)
944
- const writable = new PassThrough()
945
- let started = false
946
- let startPromise: Promise<void> | null = null
947
- let destroyed = false
948
-
949
- // use PassThrough — AND expose a direct callback for the pipe to use.
950
- // stream-browserify's PassThrough + Duplexify stop flowing after idle,
951
- // so we also push data via a callback that the patched pipe() can use.
952
- const readable = new PassThrough()
953
- // direct data callback — bypasses broken stream plumbing entirely.
954
- // registered on globalThis so the patched pipe() can find it regardless
955
- // of how many stream wrappers (Duplexify, etc.) sit between.
956
- const dataListeners: Array<(chunk: Buffer) => void> = []
957
- ;(globalThis as any).__orez_repl_data_push = (fn: (chunk: Buffer) => void) => {
958
- dataListeners.push(fn)
959
- }
960
-
961
- const start = async () => {
962
- if (started) return
963
- started = true
964
- startPromise = handleStartReplication(
965
- replicationQuery,
966
- {
967
- write(chunk: Uint8Array) {
968
- if (destroyed) return
969
- // skip CopyBothResponse, unwrap CopyData
970
- if (chunk[0] === 0x57) return
971
- const data =
972
- chunk[0] === 0x64 && chunk.length >= 6
973
- ? Buffer.from(chunk.subarray(5))
974
- : Buffer.from(chunk)
975
- readable.write(data)
976
- // also call pipe handlers directly — stream polyfills are broken in browser
977
- const handlers = (globalThis as any).__orez_pipe_handlers as
978
- | Array<(chunk: Buffer) => void>
979
- | undefined
980
- if (handlers && handlers.length > 0) {
981
- ;(globalThis as any).__orez_repl_bypass_count =
982
- ((globalThis as any).__orez_repl_bypass_count || 0) + 1
983
- }
984
- if (handlers) {
985
- for (const fn of handlers) {
986
- try {
987
- fn(data)
988
- } catch {}
989
- }
990
- }
991
- },
992
- get closed() {
993
- return destroyed || writable.destroyed || !!db.closed
994
- },
995
- },
996
- db as PGlite,
997
- mutex
998
- ).catch((err) => {
999
- // don't destroy the readable on handler errors — the change-streamer
1000
- // would reconnect with a new subscription, losing the reference the
1001
- // producer holds. just log the error and let the handler restart.
1002
- console.warn(
1003
- '[orez:repl] handler error:',
1004
- err instanceof Error ? err.message : String(err)
1005
- )
1006
- })
1007
- return Promise.resolve()
1008
- }
1009
-
1010
- const pending = Promise.resolve() as any
1011
- pending.execute = () => pending
1012
- pending.simple = () => pending
1013
- pending.cancel = () => {
1014
- destroyed = true
1015
- readable.destroy()
1016
- writable.destroy()
1017
- }
1018
- pending.readable = async () => {
1019
- await start()
1020
- return readable
1021
- }
1022
- pending.writable = async () => {
1023
- await start()
1024
- return writable
1025
- }
1026
- pending.describe = () => Promise.reject(new Error('not supported'))
1027
- pending.values = () => Promise.reject(new Error('not supported'))
1028
- pending.raw = () => Promise.reject(new Error('not supported'))
1029
- pending.forEach = () => Promise.reject(new Error('not supported'))
1030
- pending.cursor = () => {
1031
- throw new Error('not supported')
1032
- }
1033
- pending.stream = () => {
1034
- throw new Error('not supported')
1035
- }
1036
- return pending
1037
- }
1038
-
1039
- // -- type parsers --
1040
- // pre-populated parsers matching the postgres npm package's type registry.
1041
-
1042
- function identity(x: string) {
1043
- return x
1044
- }
1045
- function parseFloat_(x: string) {
1046
- return parseFloat(x)
1047
- }
1048
- function parseInt_(x: string) {
1049
- return parseInt(x, 10)
1050
- }
1051
- function parseBool(x: string) {
1052
- return x === 't' || x === 'true'
1053
- }
1054
- function parseJSON(x: string) {
1055
- try {
1056
- return JSON.parse(x)
1057
- } catch {
1058
- return x
1059
- }
1060
- }
1061
-
1062
- function makeArrayParser(elementParser: (x: string) => unknown) {
1063
- const fn = (x: string) => {
1064
- if (!x || x === '{}') return []
1065
- const inner = x.slice(1, -1)
1066
- return inner.split(',').map((v) => {
1067
- if (v === 'NULL') return null
1068
- return elementParser(v.replace(/^"|"$/g, ''))
1069
- })
1070
- }
1071
- ;(fn as any).array = true
1072
- return fn
1073
- }
1074
-
1075
- function buildDefaultParsers(): Record<number, (value: string) => unknown> {
1076
- const p: Record<number, any> = {}
1077
- // scalar types
1078
- p[16] = parseBool // bool
1079
- p[17] = identity // bytea
1080
- p[20] = identity // int8 (bigint as string)
1081
- p[21] = parseInt_ // int2
1082
- p[23] = parseInt_ // int4
1083
- p[25] = identity // text
1084
- p[26] = parseInt_ // oid
1085
- p[114] = parseJSON // json
1086
- p[700] = parseFloat_ // float4
1087
- p[701] = parseFloat_ // float8
1088
- p[1042] = identity // bpchar
1089
- p[1043] = identity // varchar
1090
- p[1082] = identity // date
1091
- p[1083] = identity // time
1092
- // timestamps → epoch ms (zero-cache expects number, not string)
1093
- const parseTimestamp = (val: string) => {
1094
- if (typeof val === 'number') return val
1095
- const d = Date.parse(val)
1096
- return isNaN(d) ? val : d
1097
- }
1098
- p[1114] = parseTimestamp // timestamp
1099
- p[1184] = parseTimestamp // timestamptz
1100
- p[1266] = identity // timetz
1101
- p[1700] = identity // numeric
1102
- p[2950] = identity // uuid
1103
- p[3802] = parseJSON // jsonb
1104
- // array types
1105
- p[1000] = makeArrayParser(parseBool) // bool[]
1106
- p[1005] = makeArrayParser(parseInt_) // int2[]
1107
- p[1007] = makeArrayParser(parseInt_) // int4[]
1108
- p[1009] = makeArrayParser(identity) // text[]
1109
- p[1016] = makeArrayParser(identity) // int8[]
1110
- p[1021] = makeArrayParser(parseFloat_) // float4[]
1111
- p[1022] = makeArrayParser(parseFloat_) // float8[]
1112
- p[1015] = makeArrayParser(identity) // varchar[]
1113
- p[1182] = makeArrayParser(identity) // date[]
1114
- p[1115] = makeArrayParser(identity) // timestamp[]
1115
- p[1185] = makeArrayParser(identity) // timestamptz[]
1116
- p[2951] = makeArrayParser(identity) // uuid[]
1117
- p[199] = makeArrayParser(parseJSON) // json[]
1118
- p[3807] = makeArrayParser(parseJSON) // jsonb[]
1119
- return p
1120
- }
1121
-
1122
- // -- main export --
1123
-
1124
- export interface PostgresShimOptions {
1125
- max?: number
1126
- max_lifetime?: number
1127
- idle_timeout?: number
1128
- fetch_types?: boolean
1129
- ssl?: unknown
1130
- onnotice?: (notice: unknown) => void
1131
- connection?: Record<string, unknown>
1132
- types?: Record<string, unknown>
1133
- }
1134
-
1135
- export function createPostgresShim(pglite: PGlite, opts?: PostgresShimOptions) {
1136
- const sqlFn = createSqlFunction(pglite, pglite)
1137
-
1138
- function sql(first: any, ...rest: any[]): any {
1139
- return sqlFn(first, ...rest)
1140
- }
1141
-
1142
- // sql.unsafe(queryString, params?) — raw SQL execution
1143
- sql.unsafe = (queryString: string, params?: unknown[]) => {
1144
- const upper = queryString.trimStart().toUpperCase()
1145
-
1146
- // START_REPLICATION — expose an in-process duplex stream backed by
1147
- // orez's replication handler instead of the TCP protocol adapter.
1148
- if (upper.startsWith('START_REPLICATION')) {
1149
- return createReplicationPendingQuery(queryString, pglite as ReplicationCapableDb)
1150
- }
1151
-
1152
- // COPY TO STDOUT — returns readable stream of rows
1153
- if (upper.startsWith('COPY') && upper.includes('TO STDOUT')) {
1154
- return createCopyPendingQuery(queryString, pglite)
1155
- }
1156
-
1157
- // strip FK constraints (see executeQuery for why)
1158
- if (/FOREIGN\s+KEY/i.test(queryString)) {
1159
- if (/CREATE\s+TABLE/i.test(queryString)) {
1160
- queryString = queryString.replace(
1161
- /,?\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,
1162
- ''
1163
- )
1164
- }
1165
- if (/ALTER\s+TABLE/i.test(queryString) && /ADD\s+CONSTRAINT/i.test(queryString)) {
1166
- queryString = queryString.replace(
1167
- /ALTER\s+TABLE\s+[^\s]+\s+ADD\s+CONSTRAINT\s+[^\s]+\s*\n?\s*FOREIGN\s+KEY\s*\([^)]*\)\s*\n?\s*REFERENCES\s+[^;]+/gi,
1168
- 'SELECT 1'
1169
- )
1170
- }
1171
- }
1172
-
1173
- const serializedParams = (params ?? []).map(serializeParam)
1174
-
1175
- // multi-statement with no params: split and run each individually
1176
- // PGliteWorker's execProtocol rejects multi-statement, so always split upfront
1177
- if (hasMultipleStatements(queryString) && serializedParams.length === 0) {
1178
- const promise = (async () => {
1179
- const intercepted = await interceptReplicationQuery(queryString, pglite)
1180
- if (intercepted) return intercepted
1181
- {
1182
- const statements = splitStatements(queryString)
1183
- const resultArrays = []
1184
- for (const stmt of statements) {
1185
- const r = await pglite.query(stmt)
1186
- resultArrays.push(createResultArray(r as Results<any>, stmt))
1187
- }
1188
- const combined = resultArrays as any
1189
- combined.count = resultArrays.length
1190
- combined.command = 'SELECT'
1191
- combined.state = { status: 'idle', pid: 0, secret: 0 }
1192
- combined.statement = { name: '', string: queryString, types: [], columns: [] }
1193
- combined.columns = []
1194
- return combined
1195
- }
1196
- })()
1197
- return createPendingQuery(promise)
1198
- }
1199
-
1200
- const promise = executeQuery(pglite, queryString, serializedParams, pglite)
1201
- return createPendingQuery(promise)
1202
- }
1203
-
1204
- // sql.begin(options?, callback) — transactions
1205
- //
1206
- // BROWSER FIX: don't use pglite.transaction() for the top-level callback.
1207
- // zero-cache's transaction pool holds the callback open indefinitely
1208
- // (await dequeue() loop), which permanently holds PGlite's Fe2 mutex
1209
- // and deadlocks ALL external queries (push handler, pg-query, etc.).
1210
- //
1211
- // instead, route queries directly through PGlite — each query acquires
1212
- // and releases Fe2 independently. this trades transaction atomicity for
1213
- // Fe2 availability, which is acceptable in the browser dev preview
1214
- // (PGlite is single-connection so operations are serialized anyway).
1215
- //
1216
- // nested begin/savepoint calls still use real pglite.transaction()
1217
- // since those are short-lived (task-scoped, not pool-scoped).
1218
- sql.begin = async (
1219
- optionsOrCb: string | ((tx: any) => any),
1220
- maybeCb?: (tx: any) => any
1221
- ) => {
1222
- const cb = typeof optionsOrCb === 'function' ? optionsOrCb : maybeCb!
1223
-
1224
- // create sql function backed by pglite directly (not a Transaction)
1225
- // each query acquires/releases Fe2 independently
1226
- const txSql = createSqlFunction(pglite, pglite)
1227
-
1228
- function txSqlFn(first: any, ...rest: any[]): any {
1229
- return txSql(first, ...rest)
1230
- }
1231
-
1232
- // unsafe: routes through pglite directly
1233
- txSqlFn.unsafe = (queryString: string, params?: unknown[]) => {
1234
- const upper = queryString.trimStart().toUpperCase()
1235
- if (upper.startsWith('COPY') && upper.includes('TO STDOUT')) {
1236
- return createCopyPendingQuery(queryString, pglite)
1237
- }
1238
-
1239
- const serializedParams = (params ?? []).map(serializeParam)
1240
-
1241
- if (hasMultipleStatements(queryString) && serializedParams.length === 0) {
1242
- const promise = (async () => {
1243
- const stmts = splitStatements(queryString)
1244
- const resultArrays = []
1245
- for (const stmt of stmts) {
1246
- const r = await pglite.query(stmt)
1247
- resultArrays.push(createResultArray(r as Results<any>, stmt))
1248
- }
1249
- const combined = resultArrays as any
1250
- combined.count = resultArrays.length
1251
- combined.command = 'SELECT'
1252
- combined.state = { status: 'idle', pid: 0, secret: 0 }
1253
- combined.statement = {
1254
- name: '',
1255
- string: queryString,
1256
- types: [],
1257
- columns: [],
1258
- }
1259
- combined.columns = []
1260
- return combined
1261
- })()
1262
- return createPendingQuery(promise)
1263
- }
1264
-
1265
- const promise = executeQuery(pglite, queryString, serializedParams, pglite)
1266
- return createPendingQuery(promise)
1267
- }
1268
-
1269
- // nested begin: use real pglite.transaction() for atomicity (short-lived)
1270
- // nested begin: non-transactional (same as top level)
1271
- // real pglite.transaction() holds Fe2 during async SQLite work in the syncer,
1272
- // which deadlocks any concurrent PGlite queries. non-transactional avoids this.
1273
- txSqlFn.begin = async (
1274
- innerOptOrCb: string | ((tx: any) => any),
1275
- innerMaybeCb?: (tx: any) => any
1276
- ) => {
1277
- const innerCb = typeof innerOptOrCb === 'function' ? innerOptOrCb : innerMaybeCb!
1278
- const innerTxSql = createSqlFunction(pglite, pglite)
1279
- function innerTxSqlFn(first: any, ...rest: any[]): any {
1280
- return innerTxSql(first, ...rest)
1281
- }
1282
- innerTxSqlFn.unsafe = txSqlFn.unsafe
1283
- innerTxSqlFn.begin = txSqlFn.begin
1284
- innerTxSqlFn.savepoint = txSqlFn.savepoint
1285
- innerTxSqlFn.end = async () => {}
1286
- innerTxSqlFn.options = sql.options
1287
- innerTxSqlFn.PostgresError = PostgresError
1288
- try {
1289
- const result = await innerCb(innerTxSqlFn)
1290
- return Array.isArray(result) ? await Promise.all(result) : result
1291
- } finally {
1292
- signalReplicationChange()
1293
- }
1294
- }
1295
-
1296
- // savepoint at top level: DON'T use pglite.transaction() — same reasoning
1297
- // as sql.begin(). the callback might be long-running (e.g. setupTriggers).
1298
- // just run the callback with PGlite-backed sql (each query acquires/releases Fe2).
1299
- let _savepointIdx = 0
1300
- txSqlFn.savepoint = async (nameOrFn: any, maybeFn?: any) => {
1301
- const fn = typeof nameOrFn === 'function' ? nameOrFn : maybeFn
1302
- return fn(txSqlFn)
1303
- }
1304
-
1305
- txSqlFn.end = async () => {}
1306
- txSqlFn.options = sql.options
1307
- txSqlFn.PostgresError = PostgresError
1308
-
1309
- // use explicit BEGIN/COMMIT/ROLLBACK SQL instead of pglite.transaction().
1310
- // each statement acquires/releases Fe2 independently, so the mutex is
1311
- // NOT held during async gaps in the callback — avoiding the deadlock
1312
- // that pglite.transaction() causes with zero-cache's long-running pool.
1313
- await pglite.exec('BEGIN')
1314
- try {
1315
- const result = await cb(txSqlFn)
1316
- await pglite.exec('COMMIT')
1317
- signalReplicationChange() // signal immediately after commit, don't wait for finally
1318
- return Array.isArray(result) ? await Promise.all(result) : result
1319
- } catch (err) {
1320
- await pglite.exec('ROLLBACK')
1321
- throw err
1322
- } finally {
1323
- signalReplicationChange()
1324
- }
1325
- }
1326
-
1327
- // sql.end() — no-op (PGlite lifecycle managed elsewhere)
1328
- sql.end = async (_opts?: { timeout?: number }) => {}
1329
-
1330
- // sql.close() — alias for end
1331
- sql.close = sql.end
1332
-
1333
- // sql.options — connection metadata
1334
- sql.options = {
1335
- host: ['localhost'],
1336
- port: [5432],
1337
- database: 'pglite',
1338
- user: 'pglite',
1339
- max: opts?.max ?? 1,
1340
- parsers: buildDefaultParsers(),
1341
- fetch_types: opts?.fetch_types ?? true,
1342
- connection: opts?.connection ?? {},
1343
- ssl: opts?.ssl ?? false,
1344
- types: opts?.types ?? {},
1345
- transform: {
1346
- undefined: undefined,
1347
- column: { from: undefined, to: undefined },
1348
- value: { from: undefined, to: undefined },
1349
- row: { from: undefined, to: undefined },
1350
- },
1351
- serializers: {} as Record<number, (value: unknown) => unknown>,
1352
- }
1353
-
1354
- // sql.PostgresError — error class
1355
- sql.PostgresError = PostgresError
1356
-
1357
- // sql.CLOSE / sql.END — sentinel objects
1358
- sql.CLOSE = {} as Record<string, never>
1359
- sql.END = sql.CLOSE
1360
-
1361
- // sql.parameters — server parameters
1362
- sql.parameters = {
1363
- application_name: 'pglite-shim',
1364
- server_version: '17.0',
1365
- }
1366
-
1367
- // sql.types / sql.typed — type helpers
1368
- sql.typed = (value: unknown, oid: number) => ({ value, type: oid })
1369
- sql.types = sql.typed
1370
-
1371
- // sql.json — json parameter helper
1372
- sql.json = (value: unknown) => JSON.stringify(value)
1373
-
1374
- // sql.array — array parameter helper
1375
- sql.array = (value: unknown[], type?: number) => ({ value, type, array: true })
1376
-
1377
- // sql.listen — not supported
1378
- sql.listen = () => {
1379
- throw new Error('listen() not supported in worker mode')
1380
- }
1381
-
1382
- // sql.notify — not supported
1383
- sql.notify = () => {
1384
- throw new Error('notify() not supported in worker mode')
1385
- }
1386
-
1387
- // sql.subscribe — not supported
1388
- sql.subscribe = () => {
1389
- throw new Error('subscribe() not supported in worker mode')
1390
- }
1391
-
1392
- // sql.reserve — not supported
1393
- sql.reserve = () => {
1394
- throw new Error('reserve() not supported in worker mode')
1395
- }
1396
-
1397
- // sql.file — not supported
1398
- sql.file = () => {
1399
- throw new Error('file() not supported in worker mode')
1400
- }
1401
-
1402
- // sql.largeObject — not supported
1403
- sql.largeObject = () => {
1404
- throw new Error('largeObject() not supported in worker mode')
1405
- }
1406
-
1407
- return sql
1408
- }
1409
-
1410
- // -- default export --
1411
- // matches the `postgres` package's default export shape:
1412
- // import postgres from 'postgres'
1413
- // const sql = postgres(url, options)
1414
- //
1415
- // when used as a bundler alias, zero-cache calls postgres(connectionURI, options).
1416
- // we intercept by reading the PGlite instance from globalThis.__orez_pglite.
1417
-
1418
- function postgres(
1419
- _urlOrOpts?: string | PostgresShimOptions,
1420
- opts?: PostgresShimOptions
1421
- ): ReturnType<typeof createPostgresShim> {
1422
- // multi-instance routing: if __orez_pglite_instances is set, route by URL
1423
- const instances = (globalThis as any).__orez_pglite_instances as
1424
- | { postgres: PGlite; cvr: PGlite; cdb: PGlite }
1425
- | undefined
1426
- let pglite: PGlite | undefined
1427
- if (instances && typeof _urlOrOpts === 'string') {
1428
- if (_urlOrOpts.includes('/zero_cvr')) pglite = instances.cvr
1429
- else if (_urlOrOpts.includes('/zero_cdb')) pglite = instances.cdb
1430
- else pglite = instances.postgres
1431
- } else {
1432
- pglite = (globalThis as any).__orez_pglite as PGlite | undefined
1433
- }
1434
- if (!pglite) {
1435
- throw new Error(
1436
- 'postgres shim: no PGlite instance found. ' +
1437
- 'set globalThis.__orez_pglite or __orez_pglite_instances.'
1438
- )
1439
- }
1440
-
1441
- const resolvedOpts = typeof _urlOrOpts === 'object' ? _urlOrOpts : opts
1442
- return createPostgresShim(pglite, resolvedOpts)
1443
- }
1444
-
1445
- // attach PostgresError and BigInt to the default export (matches postgres package)
1446
- postgres.PostgresError = PostgresError
1447
- postgres.BigInt = {
1448
- to: 20,
1449
- from: [20],
1450
- parse: (x: string) => globalThis.BigInt(x),
1451
- serialize: (x: bigint) => x.toString(),
1452
- }
1453
-
1454
- export default postgres