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