orez 0.2.20 → 0.2.25

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 (58) hide show
  1. package/dist/browser.d.ts +5 -0
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +1 -0
  4. package/dist/browser.js.map +1 -1
  5. package/dist/cf-do/test-protocol.d.ts +11 -0
  6. package/dist/cf-do/test-protocol.d.ts.map +1 -0
  7. package/dist/cf-do/test-protocol.js +137 -0
  8. package/dist/cf-do/test-protocol.js.map +1 -0
  9. package/dist/cf-do/worker.d.ts +65 -0
  10. package/dist/cf-do/worker.d.ts.map +1 -0
  11. package/dist/cf-do/worker.js +440 -0
  12. package/dist/cf-do/worker.js.map +1 -0
  13. package/dist/config.d.ts +4 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +1 -0
  16. package/dist/config.js.map +1 -1
  17. package/dist/index.d.ts +2 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +60 -28
  20. package/dist/index.js.map +1 -1
  21. package/dist/pg-proxy-do-backend.d.ts +49 -0
  22. package/dist/pg-proxy-do-backend.d.ts.map +1 -0
  23. package/dist/pg-proxy-do-backend.js +713 -0
  24. package/dist/pg-proxy-do-backend.js.map +1 -0
  25. package/dist/pglite-ipc.d.ts +3 -0
  26. package/dist/pglite-ipc.d.ts.map +1 -1
  27. package/dist/pglite-ipc.js +34 -12
  28. package/dist/pglite-ipc.js.map +1 -1
  29. package/dist/pglite-web-proxy.d.ts +3 -0
  30. package/dist/pglite-web-proxy.d.ts.map +1 -1
  31. package/dist/pglite-web-proxy.js +50 -7
  32. package/dist/pglite-web-proxy.js.map +1 -1
  33. package/dist/query-rewrites.d.ts +2 -0
  34. package/dist/query-rewrites.d.ts.map +1 -0
  35. package/dist/query-rewrites.js +140 -0
  36. package/dist/query-rewrites.js.map +1 -0
  37. package/dist/worker/browser-admin.d.ts +13 -0
  38. package/dist/worker/browser-admin.d.ts.map +1 -0
  39. package/dist/worker/browser-admin.js +33 -0
  40. package/dist/worker/browser-admin.js.map +1 -0
  41. package/dist/worker/browser-embed.d.ts +12 -12
  42. package/dist/worker/browser-embed.d.ts.map +1 -1
  43. package/dist/worker/browser-embed.js +7 -0
  44. package/dist/worker/browser-embed.js.map +1 -1
  45. package/package.json +2 -2
  46. package/src/browser.ts +7 -0
  47. package/src/config.ts +5 -0
  48. package/src/index.ts +66 -33
  49. package/src/pg-proxy-do-backend.ts +840 -0
  50. package/src/pglite-ipc.test.ts +17 -0
  51. package/src/pglite-ipc.ts +31 -12
  52. package/src/pglite-web-proxy.test.ts +57 -0
  53. package/src/pglite-web-proxy.ts +48 -7
  54. package/src/query-rewrites.test.ts +30 -0
  55. package/src/query-rewrites.ts +152 -0
  56. package/src/worker/browser-admin.ts +52 -0
  57. package/src/worker/browser-embed-admin.test.ts +75 -0
  58. package/src/worker/browser-embed.ts +21 -12
@@ -0,0 +1,840 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * DoBackend: a PGlite-compatible adapter that forwards SQL to Cloudflare Durable Objects.
4
+ *
5
+ * Translates PG wire protocol messages → SQL → DO HTTP API → PG wire protocol responses.
6
+ *
7
+ * Handles PG transactions transparently: BEGIN/COMMIT/ROLLBACK are intercepted
8
+ * and managed with in-memory write buffering. Writes are flushed to the DO
9
+ * atomically via ctx.storage.transaction() on COMMIT.
10
+ */
11
+
12
+ const textEncoder = new TextEncoder()
13
+ const textDecoder = new TextDecoder()
14
+
15
+ // ── PG wire protocol constants ────────────────────────────────────────────
16
+
17
+ const FT_QUERY = 0x51
18
+ const FT_PARSE = 0x50
19
+ const FT_BIND = 0x42
20
+ const FT_DESCRIBE = 0x44
21
+ const FT_EXECUTE = 0x45
22
+ const FT_SYNC = 0x53
23
+ const FT_CLOSE = 0x43
24
+ const FT_TERMINATE = 0x58
25
+ const FT_FLUSH = 0x48
26
+
27
+ const STATUS_IDLE = 0x49
28
+ const PG_TYPE_TEXT = 25
29
+ const PG_TYPE_INT4 = 23
30
+ const PG_TYPE_INT8 = 20
31
+ const PG_TYPE_BOOL = 16
32
+ const PG_TYPE_FLOAT8 = 701
33
+ const PG_TYPE_VARCHAR = 1043
34
+ const PG_TYPE_JSON = 114
35
+ const PG_TYPE_NUMERIC = 1700
36
+ const PG_TYPE_TIMESTAMP = 1114
37
+ const PG_TYPE_BYTEA = 17
38
+ const PG_TYPE_INT2 = 21
39
+
40
+ // ── Utilities ─────────────────────────────────────────────────────────────
41
+
42
+ function concat(...parts: Uint8Array[]): Uint8Array {
43
+ let total = 0
44
+ for (const p of parts) total += p.length
45
+ const result = new Uint8Array(total)
46
+ let offset = 0
47
+ for (const p of parts) {
48
+ result.set(p, offset)
49
+ offset += p.length
50
+ }
51
+ return result
52
+ }
53
+
54
+ function i16(v: number, buf = new ArrayBuffer(2)): Uint8Array {
55
+ new DataView(buf).setInt16(0, v)
56
+ return new Uint8Array(buf)
57
+ }
58
+
59
+ function i32(v: number, buf = new ArrayBuffer(4)): Uint8Array {
60
+ new DataView(buf).setInt32(0, v)
61
+ return new Uint8Array(buf)
62
+ }
63
+
64
+ function int4(v: number, buf = new ArrayBuffer(4)): Uint8Array {
65
+ new DataView(buf).setInt32(0, v)
66
+ return new Uint8Array(buf)
67
+ }
68
+
69
+ function uint4(v: number, buf = new ArrayBuffer(4)): Uint8Array {
70
+ new DataView(buf).setUint32(0, v)
71
+ return new Uint8Array(buf)
72
+ }
73
+
74
+ function cstr(s: string): Uint8Array {
75
+ const encoded = textEncoder.encode(s)
76
+ const result = new Uint8Array(encoded.length + 1)
77
+ result.set(encoded)
78
+ return result
79
+ }
80
+
81
+ function msg(ty: number, payload: Uint8Array): Uint8Array {
82
+ const out = new Uint8Array(1 + 4 + payload.length)
83
+ out[0] = ty
84
+ new DataView(out.buffer, 1, 4).setUint32(0, 4 + payload.length)
85
+ out.set(payload, 5)
86
+ return out
87
+ }
88
+
89
+ const zero4 = new Uint8Array(4)
90
+ const zero2 = new Uint8Array(2)
91
+
92
+ // ── PG response builders ─────────────────────────────────────────────────
93
+
94
+ function buildRowDescription(fields: { name: string; oid?: number }[]): Uint8Array {
95
+ if (fields.length === 0) return buildNoData()
96
+ const colParts: Uint8Array[] = []
97
+ for (const f of fields) {
98
+ colParts.push(cstr(f.name))
99
+ const col = new Uint8Array(18)
100
+ const v = new DataView(col.buffer)
101
+ v.setUint32(0, 0)
102
+ v.setInt16(4, 0)
103
+ v.setUint32(6, f.oid ?? PG_TYPE_TEXT)
104
+ v.setInt16(10, -1)
105
+ v.setInt32(12, -1)
106
+ v.setInt16(16, 0)
107
+ colParts.push(col)
108
+ }
109
+ return msg(0x54, concat(i16(fields.length), ...colParts))
110
+ }
111
+
112
+ function buildDataRow(row: Record<string, unknown>, fields: string[]): Uint8Array {
113
+ const colParts: Uint8Array[] = []
114
+ for (const name of fields) {
115
+ const val = row[name]
116
+ if (val === null || val === undefined) {
117
+ colParts.push(int4(-1))
118
+ } else {
119
+ const str = typeof val === 'object' ? JSON.stringify(val) : String(val)
120
+ const encoded = textEncoder.encode(str)
121
+ colParts.push(concat(uint4(encoded.length), encoded))
122
+ }
123
+ }
124
+ return msg(0x44, concat(i16(fields.length), ...colParts))
125
+ }
126
+
127
+ function buildCommandComplete(tag: string): Uint8Array {
128
+ return msg(0x43, cstr(tag))
129
+ }
130
+
131
+ function buildReadyForQuery(status: number = STATUS_IDLE): Uint8Array {
132
+ return msg(0x5a, new Uint8Array([status]))
133
+ }
134
+
135
+ function buildErrorResponse(message: string): Uint8Array {
136
+ return msg(
137
+ 0x45,
138
+ concat(
139
+ cstr('S'),
140
+ cstr('ERROR'),
141
+ cstr('C'),
142
+ cstr('XX000'),
143
+ cstr('M'),
144
+ cstr(message),
145
+ new Uint8Array([0])
146
+ )
147
+ )
148
+ }
149
+
150
+ function buildParseComplete(): Uint8Array {
151
+ return msg(0x31, zero4)
152
+ }
153
+ function buildBindComplete(): Uint8Array {
154
+ return msg(0x32, zero4)
155
+ }
156
+ function buildCloseComplete(): Uint8Array {
157
+ return msg(0x33, zero4)
158
+ }
159
+ function buildNoData(): Uint8Array {
160
+ return msg(0x6e, zero4)
161
+ }
162
+ function buildParameterDescription(oids: number[]): Uint8Array {
163
+ return msg(0x74, concat(i16(oids.length), ...oids.map(uint4)))
164
+ }
165
+ function buildParameterStatus(name: string, value: string): Uint8Array {
166
+ return msg(0x53, concat(cstr(name), cstr(value)))
167
+ }
168
+ function buildNotificationResponse(
169
+ pid: number,
170
+ channel: string,
171
+ payload: string
172
+ ): Uint8Array {
173
+ return msg(0x41, concat(uint4(pid), cstr(channel), cstr(payload)))
174
+ }
175
+
176
+ // ── PG message parsers ────────────────────────────────────────────────────
177
+
178
+ function extractQueryText(data: Uint8Array): string | null {
179
+ if (data[0] !== 0x51) return null
180
+ const len = new DataView(data.buffer, data.byteOffset, 4).getInt32(1)
181
+ return textDecoder.decode(data.subarray(5, 1 + len - 1)).replace(/\0$/, '')
182
+ }
183
+
184
+ function extractParseQuery(data: Uint8Array): string | null {
185
+ if (data[0] !== 0x50) return null
186
+ let offset = 5
187
+ while (offset < data.length && data[offset] !== 0) offset++
188
+ offset++
189
+ const qStart = offset
190
+ while (offset < data.length && data[offset] !== 0) offset++
191
+ return textDecoder.decode(data.subarray(qStart, offset))
192
+ }
193
+
194
+ function extractParseStatementName(data: Uint8Array): string {
195
+ let offset = 5
196
+ const start = offset
197
+ while (offset < data.length && data[offset] !== 0) offset++
198
+ return textDecoder.decode(data.subarray(start, offset))
199
+ }
200
+
201
+ function extractBindStatementName(data: Uint8Array): string {
202
+ let offset = 5
203
+ while (offset < data.length && data[offset] !== 0) offset++
204
+ offset++
205
+ const start = offset
206
+ while (offset < data.length && data[offset] !== 0) offset++
207
+ return textDecoder.decode(data.subarray(start, offset))
208
+ }
209
+
210
+ function extractBindParams(data: Uint8Array): any[] {
211
+ const params: any[] = []
212
+ let offset = 5
213
+ while (offset < data.length && data[offset] !== 0) offset++
214
+ offset++
215
+ while (offset < data.length && data[offset] !== 0) offset++
216
+ offset++
217
+ if (offset + 2 > data.length) return params
218
+ const nfc = new DataView(data.buffer, data.byteOffset + offset, 2).getInt16(0)
219
+ offset += 2 + nfc * 2
220
+ if (offset + 2 > data.length) return params
221
+ const np = new DataView(data.buffer, data.byteOffset + offset, 2).getInt16(0)
222
+ offset += 2
223
+ for (let i = 0; i < np; i++) {
224
+ if (offset + 4 > data.length) break
225
+ const plen = new DataView(data.buffer, data.byteOffset + offset, 4).getInt32(0)
226
+ offset += 4
227
+ if (plen === -1) {
228
+ params.push(null)
229
+ continue
230
+ }
231
+ const str = textDecoder.decode(data.subarray(offset, offset + plen))
232
+ offset += plen
233
+ params.push(
234
+ /^-?\d+(\.\d+)?$/.test(str)
235
+ ? str.includes('.')
236
+ ? parseFloat(str)
237
+ : Number(str)
238
+ : str
239
+ )
240
+ }
241
+ return params
242
+ }
243
+
244
+ function extractDescribeType(data: Uint8Array): 'S' | 'P' {
245
+ return data[5] === 0x53 ? 'S' : 'P'
246
+ }
247
+ function extractDescribeName(data: Uint8Array): string {
248
+ const start = 6
249
+ let off = start
250
+ while (off < data.length && data[off] !== 0) off++
251
+ return textDecoder.decode(data.subarray(start, off))
252
+ }
253
+
254
+ // ── Catalog query interception ────────────────────────────────────────────
255
+
256
+ function isCatalogQuery(sql: string): boolean {
257
+ const n = sql.replace(/\s+/g, ' ').trim().toLowerCase()
258
+ if (n.includes('current_setting(')) return true
259
+ if (n.includes('pg_advisory_xact_lock') || n.includes('pg_advisory_lock')) return true
260
+ if (
261
+ n.startsWith('select') &&
262
+ (n.includes('information_schema.') ||
263
+ n.includes('pg_catalog.') ||
264
+ n.includes('pg_tables') ||
265
+ n.includes('pg_namespace') ||
266
+ n.includes('pg_type') ||
267
+ n.includes('pg_class') ||
268
+ n.includes('pg_attribute') ||
269
+ n.includes('pg_stat_') ||
270
+ n.includes('pg_index') ||
271
+ n.includes('pg_depend') ||
272
+ n.includes('pg_constraint') ||
273
+ n.includes('pg_inherits') ||
274
+ n.includes('pg_cast') ||
275
+ n.includes('pg_opfamily') ||
276
+ n.includes('pg_am ') ||
277
+ n.includes('pg_operator') ||
278
+ n.includes('pg_aggregate') ||
279
+ n.includes('pg_language') ||
280
+ n.includes('pg_extension') ||
281
+ n.includes('pg_foreign_data') ||
282
+ n.includes('pg_foreign_server') ||
283
+ n.includes('pg_range') ||
284
+ n.includes('pg_enum') ||
285
+ n.includes('pg_rewrite') ||
286
+ n.includes('pg_proc') ||
287
+ n.includes('pg_roles') ||
288
+ n.includes('pg_user ') ||
289
+ n.includes('pg_authid') ||
290
+ n.includes('pg_settings') ||
291
+ n.includes('pg_collation') ||
292
+ n.includes('pg_trigger') ||
293
+ n.includes('pg_get_expr') ||
294
+ n.includes('pg_get_functiondef') ||
295
+ n.includes('pg_get_constraintdef') ||
296
+ n.includes('pg_describe_object') ||
297
+ n.includes('has_') ||
298
+ n.includes('obj_description') ||
299
+ n.includes('format_type'))
300
+ )
301
+ return true
302
+ if (
303
+ n.includes('information_schema.') &&
304
+ (n.includes('schemata') ||
305
+ n.includes('views') ||
306
+ n.includes('view_') ||
307
+ n.includes('_pg_') ||
308
+ n.includes('table_privileges') ||
309
+ n.includes('column_udt_usage') ||
310
+ n.includes('routine_') ||
311
+ n.includes('parameters') ||
312
+ n.includes('check_constraints') ||
313
+ n.includes('referential_constraints') ||
314
+ n.includes('key_column_usage'))
315
+ )
316
+ return true
317
+ return false
318
+ }
319
+
320
+ // ── SQL rewriting ─────────────────────────────────────────────────────────
321
+
322
+ function rewriteSQL(sql: string): string {
323
+ let result = sql.trim()
324
+
325
+ // Strip PG type casts
326
+ result = result.replace(/::\w+(\[\])?\b/g, '')
327
+
328
+ // Schema-qualified names: "schema"."table" or schema.table → flat
329
+ result = result.replace(/"(\w+)"\s*\.\s*"(\w+)"/g, '"$1_$2"')
330
+ result = result.replace(/_orez\._zero_changes\b/g, '_zero_changes')
331
+ result = result.replace(
332
+ /_orez\._zero_replication_slots\b/g,
333
+ '_orez__zero_replication_slots'
334
+ )
335
+ result = result.replace(/(\b)_orez\.(\w+)/g, '$1_orez__$2')
336
+
337
+ // nextval → 1
338
+ result = result.replace(/nextval\s*\([^)]*\)/gi, '1')
339
+
340
+ // CREATE SEQUENCE → CREATE TABLE IF NOT EXISTS
341
+ if (/^\s*create\s+sequence\s+/i.test(result)) {
342
+ const m = /create\s+sequence\s+(\S+)/i.exec(result)
343
+ if (m)
344
+ result = `CREATE TABLE IF NOT EXISTS _${m[1].replace(/"/g, '')}_seq (val INTEGER DEFAULT 1, dummy INTEGER PRIMARY KEY DEFAULT 1)`
345
+ }
346
+
347
+ // Skipped PG features
348
+ if (/^\s*create\s+(or\s+replace\s+)?(function|trigger)\s+/i.test(result)) return ''
349
+ if (/^\s*cluster\s+/i.test(result)) return ''
350
+ if (/^\s*(grant|revoke)\s+/i.test(result)) return ''
351
+ if (/^\s*alter\s+default\s+privileges/i.test(result)) return ''
352
+ if (/^\s*comment\s+on\s+/i.test(result)) return ''
353
+ if (/^\s*(create|alter|drop)\s+publication\s+/i.test(result)) return ''
354
+ if (/^\s*alter\s+table\s+.+replica\s+identity/i.test(result)) return ''
355
+ // CLOSE cursor — pg-specific
356
+ if (/^\s*close\s+/i.test(result)) return ''
357
+
358
+ // DDL schema flattening
359
+ if (/^\s*(create|alter|drop)\s+(table|index|view|schema|sequence)\s+/i.test(result)) {
360
+ result = result.replace(/(\w+)\.(\w+)/g, '$1_$2')
361
+ }
362
+
363
+ // DEALLOCATE / DISCARD / RESET
364
+ if (/^(deallocate|discard|reset\s+all)/i.test(result)) return ''
365
+ // LISTEN / UNLISTEN
366
+ if (/^(listen|unlisten)/i.test(result)) return ''
367
+ // SHOW
368
+ if (/^show\s+/i.test(result)) return ''
369
+
370
+ return result
371
+ }
372
+
373
+ // ── DoBackend class ───────────────────────────────────────────────────────
374
+
375
+ export class DoBackend {
376
+ readonly waitReady: Promise<void>
377
+ ready = false
378
+ closed = false
379
+ private doUrl: string
380
+ private dbName: string
381
+ private httpClient: HttpClient
382
+ private preparedStatements = new Map<string, { sql: string }>()
383
+ private sqlToExecute: { sql: string; params: any[] } | null = null
384
+
385
+ // Transaction state
386
+ private inTransaction = false
387
+ private txnBuffer: string[] = []
388
+ private txnReadOnly = false
389
+
390
+ constructor(doUrl: string, dbName: string = 'postgres') {
391
+ this.doUrl = doUrl.replace(/\/+$/, '')
392
+ this.dbName = dbName
393
+ this.httpClient = new HttpClient()
394
+ this.waitReady = this.init()
395
+ }
396
+
397
+ private async init() {
398
+ try {
399
+ await this.httpClient.post(
400
+ `${this.doUrl}/exec?db=${encodeURIComponent(this.dbName)}`,
401
+ JSON.stringify({ sql: 'SELECT 1' })
402
+ )
403
+ } catch {}
404
+ this.ready = true
405
+ }
406
+
407
+ async close(): Promise<void> {
408
+ this.closed = true
409
+ }
410
+
411
+ async execProtocolRaw(
412
+ message: Uint8Array,
413
+ options?: { syncToFs?: boolean; throwOnError?: boolean }
414
+ ): Promise<Uint8Array> {
415
+ const msgType = message[0]
416
+ try {
417
+ switch (msgType) {
418
+ case FT_QUERY:
419
+ return await this.handleSimpleQuery(message)
420
+ case FT_PARSE:
421
+ return this.handleParse(message)
422
+ case FT_BIND:
423
+ return this.handleBind(message)
424
+ case FT_DESCRIBE:
425
+ return this.handleDescribe(message)
426
+ case FT_EXECUTE:
427
+ return await this.handleExecute(message)
428
+ case FT_SYNC:
429
+ return this.handleSync()
430
+ case FT_CLOSE:
431
+ return buildCloseComplete()
432
+ case FT_FLUSH:
433
+ return new Uint8Array(0)
434
+ case FT_TERMINATE:
435
+ return new Uint8Array(0)
436
+ default:
437
+ return new Uint8Array(0)
438
+ }
439
+ } catch (err: any) {
440
+ if (options?.throwOnError !== false) throw err
441
+ return buildErrorResponse(err.message || String(err))
442
+ }
443
+ }
444
+
445
+ // ── Transaction-aware query handling ──────────────────────────────────────
446
+
447
+ private async handleSimpleQuery(data: Uint8Array): Promise<Uint8Array> {
448
+ const sql = extractQueryText(data)
449
+ if (!sql) return concat(buildCommandComplete('OK'), buildReadyForQuery())
450
+
451
+ const normalized = sql.trimStart().toLowerCase()
452
+
453
+ // BEGIN / START TRANSACTION
454
+ if (
455
+ normalized === 'begin' ||
456
+ normalized === 'begin;' ||
457
+ normalized === 'begin work' ||
458
+ normalized === 'begin transaction' ||
459
+ normalized === 'start transaction'
460
+ ) {
461
+ this.inTransaction = true
462
+ this.txnBuffer = []
463
+ this.txnReadOnly = false
464
+ return concat(buildCommandComplete('BEGIN'), buildReadyForQuery())
465
+ }
466
+
467
+ // COMMIT / END
468
+ if (
469
+ normalized === 'commit' ||
470
+ normalized === 'commit;' ||
471
+ normalized === 'commit work' ||
472
+ normalized === 'end' ||
473
+ normalized === 'end;'
474
+ ) {
475
+ if (this.inTransaction && this.txnBuffer.length > 0) {
476
+ await this.flushTransactionBuffer()
477
+ }
478
+ this.inTransaction = false
479
+ this.txnBuffer = []
480
+ return concat(buildCommandComplete('COMMIT'), buildReadyForQuery())
481
+ }
482
+
483
+ // ROLLBACK / ABORT
484
+ if (
485
+ normalized === 'rollback' ||
486
+ normalized === 'rollback;' ||
487
+ normalized === 'rollback work' ||
488
+ normalized === 'abort' ||
489
+ normalized === 'abort;'
490
+ ) {
491
+ this.inTransaction = false
492
+ this.txnBuffer = []
493
+ return concat(buildCommandComplete('ROLLBACK'), buildReadyForQuery())
494
+ }
495
+
496
+ // SET (local) — skip
497
+ if (normalized.startsWith('set '))
498
+ return concat(buildCommandComplete('SET'), buildReadyForQuery())
499
+ if (normalized.startsWith('show '))
500
+ return concat(buildCommandComplete('SHOW'), buildReadyForQuery())
501
+ if (normalized === 'show' || normalized === 'show;')
502
+ return concat(buildCommandComplete('SHOW'), buildReadyForQuery())
503
+
504
+ // SAVEPOINT — skip
505
+ if (
506
+ normalized.startsWith('savepoint ') ||
507
+ normalized.startsWith('release savepoint') ||
508
+ normalized.startsWith('release ') ||
509
+ normalized.startsWith('rollback to savepoint') ||
510
+ normalized.startsWith('rollback to ')
511
+ ) {
512
+ return concat(buildCommandComplete('SAVEPOINT'), buildReadyForQuery())
513
+ }
514
+
515
+ // DEALLOCATE, DISCARD, RESET → skip
516
+ if (/^(deallocate|discard|reset)\b/.test(normalized)) {
517
+ return concat(buildCommandComplete('OK'), buildReadyForQuery())
518
+ }
519
+
520
+ // LOCK TABLE → skip
521
+ if (normalized.startsWith('lock table') || normalized.startsWith('lock ')) {
522
+ return concat(buildCommandComplete('LOCK TABLE'), buildReadyForQuery())
523
+ }
524
+
525
+ // Prepare query
526
+ const rewritten = rewriteSQL(sql)
527
+ if (rewritten === '' || rewritten.startsWith('--'))
528
+ return concat(buildCommandComplete('OK'), buildReadyForQuery())
529
+
530
+ // Catalog queries — check before forwarding
531
+ if (isCatalogQuery(rewritten)) {
532
+ const result = this.handleCatalogQuery(rewritten)
533
+ return this.buildSelectResponse(result.rows, result.fields)
534
+ }
535
+
536
+ // SELECT reads — execute immediately even in transaction
537
+ const isWrite = this.isWriteQuery(rewritten)
538
+ const isDDL = this.isDDLQuery(rewritten)
539
+
540
+ if (this.inTransaction && (isWrite || isDDL)) {
541
+ // Buffer writes until COMMIT
542
+ this.txnBuffer.push(rewritten)
543
+ if (isDDL) return concat(buildCommandComplete('CREATE TABLE'), buildReadyForQuery())
544
+ const isInsert = /^\s*insert\b/i.test(rewritten)
545
+ const isUpdate = /^\s*update\b/i.test(rewritten)
546
+ const isDelete = /^\s*delete\b/i.test(rewritten)
547
+ const tag = isInsert
548
+ ? 'INSERT 0 1'
549
+ : isUpdate
550
+ ? 'UPDATE 1'
551
+ : isDelete
552
+ ? 'DELETE 1'
553
+ : 'OK'
554
+ return concat(buildCommandComplete(tag), buildReadyForQuery())
555
+ }
556
+
557
+ // Execute SQL
558
+ try {
559
+ const rows = await this.doExec(rewritten)
560
+ return this.buildSQLResponse(rewritten, rows)
561
+ } catch (err: any) {
562
+ return concat(buildErrorResponse(err.message), buildReadyForQuery())
563
+ }
564
+ }
565
+
566
+ private async flushTransactionBuffer(): Promise<void> {
567
+ if (this.txnBuffer.length === 0) return
568
+ await this.doBatchExec(this.txnBuffer)
569
+ this.txnBuffer = []
570
+ }
571
+
572
+ private isWriteQuery(sql: string): boolean {
573
+ return /^\s*(insert|update|delete|upsert|merge|truncate|copy)\b/i.test(sql)
574
+ }
575
+
576
+ private isDDLQuery(sql: string): boolean {
577
+ return /^\s*(create|alter|drop|grant|revoke)\s+(table|index|view|schema|sequence|function|trigger|publication)/i.test(
578
+ sql
579
+ )
580
+ }
581
+
582
+ // ── Extended protocol handlers ──────────────────────────────────────────
583
+
584
+ private handleParse(data: Uint8Array): Uint8Array {
585
+ const sql = extractParseQuery(data)
586
+ const stmtName = extractParseStatementName(data)
587
+ if (sql) {
588
+ const rewritten = rewriteSQL(sql)
589
+ if (rewritten && !rewritten.startsWith('--') && !isCatalogQuery(rewritten)) {
590
+ this.preparedStatements.set(stmtName, { sql: rewritten })
591
+ }
592
+ }
593
+ return buildParseComplete()
594
+ }
595
+
596
+ private handleBind(data: Uint8Array): Uint8Array {
597
+ const stmtName = extractBindStatementName(data)
598
+ const params = extractBindParams(data)
599
+ const stmt = this.preparedStatements.get(stmtName)
600
+ if (stmt) (stmt as any)._params = params
601
+ return buildBindComplete()
602
+ }
603
+
604
+ private async handleExecute(_data: Uint8Array): Promise<Uint8Array> {
605
+ let stmt: any
606
+ for (const [, s] of this.preparedStatements) {
607
+ if ((s as any)._params !== undefined) {
608
+ stmt = s
609
+ break
610
+ }
611
+ }
612
+ if (!stmt || !stmt.sql?.trim()) return new Uint8Array(0)
613
+
614
+ const params = stmt._params || []
615
+ delete stmt._params
616
+ const sql = this.inlineParams(stmt.sql, params)
617
+
618
+ const normalized = sql.trimStart().toLowerCase()
619
+
620
+ // Handle transaction markers in extended protocol
621
+ if (normalized === 'begin' || normalized.startsWith('begin ')) {
622
+ this.inTransaction = true
623
+ this.txnBuffer = []
624
+ this.txnReadOnly = false
625
+ return new Uint8Array(0)
626
+ }
627
+ if (normalized === 'commit' || normalized.startsWith('commit ')) {
628
+ if (this.inTransaction && this.txnBuffer.length > 0)
629
+ await this.flushTransactionBuffer()
630
+ this.inTransaction = false
631
+ this.txnBuffer = []
632
+ return buildCommandComplete('COMMIT')
633
+ }
634
+ if (
635
+ normalized === 'rollback' ||
636
+ normalized.startsWith('rollback ') ||
637
+ normalized === 'abort'
638
+ ) {
639
+ this.inTransaction = false
640
+ this.txnBuffer = []
641
+ return buildCommandComplete('ROLLBACK')
642
+ }
643
+
644
+ this.sqlToExecute = { sql, params }
645
+
646
+ try {
647
+ const rows = await this.doExec(sql)
648
+ if (rows.length > 0) {
649
+ const fns = Object.keys(rows[0])
650
+ return concat(
651
+ buildRowDescription(fns.map((n) => ({ name: n }))),
652
+ ...rows.map((r) => buildDataRow(r, fns)),
653
+ buildCommandComplete(`SELECT ${rows.length}`)
654
+ )
655
+ }
656
+ const isSelect = /^\s*select\b/i.test(sql) || /^\s*with\b/i.test(sql)
657
+ return buildCommandComplete(isSelect ? 'SELECT 0' : 'OK')
658
+ } catch (err: any) {
659
+ return buildErrorResponse(err.message)
660
+ }
661
+ }
662
+
663
+ private handleSync(): Uint8Array {
664
+ this.sqlToExecute = null
665
+ return buildReadyForQuery()
666
+ }
667
+
668
+ private handleDescribe(data: Uint8Array): Uint8Array {
669
+ const stmt = this.preparedStatements.get(extractDescribeName(data))
670
+ if (stmt && stmt.paramOIDs?.length) return buildParameterDescription(stmt.paramOIDs!)
671
+ return buildNoData()
672
+ }
673
+
674
+ // ── High-level API ──────────────────────────────────────────────────────
675
+
676
+ async exec(sql: string): Promise<any[]> {
677
+ const rewritten = rewriteSQL(sql)
678
+ if (!rewritten) return []
679
+ if (isCatalogQuery(rewritten)) return []
680
+ return this.doExec(rewritten)
681
+ }
682
+
683
+ async query<T = Record<string, unknown>>(
684
+ sql: string,
685
+ _params?: any[]
686
+ ): Promise<{ rows: T[] }> {
687
+ const rewritten = rewriteSQL(sql)
688
+ if (!rewritten) return { rows: [] }
689
+ if (isCatalogQuery(rewritten)) return { rows: [] }
690
+ const rows = await this.doExec(rewritten)
691
+ return { rows: rows as T[] }
692
+ }
693
+
694
+ // ── Internal helpers ─────────────────────────────────────────────────────
695
+
696
+ private async doExec(sql: string): Promise<Record<string, unknown>[]> {
697
+ if (!sql.trim()) return []
698
+ for (let attempt = 0; attempt < 2; attempt++) {
699
+ try {
700
+ const resp = await this.httpClient.post(
701
+ `${this.doUrl}/exec?db=${encodeURIComponent(this.dbName)}`,
702
+ JSON.stringify({ sql }),
703
+ { 'Content-Type': 'application/json' }
704
+ )
705
+ const result = JSON.parse(resp)
706
+ return result.rows ?? result ?? []
707
+ } catch {}
708
+ }
709
+
710
+ return []
711
+ }
712
+
713
+ private async doBatchExec(statements: string[]): Promise<void> {
714
+ await this.httpClient.post(
715
+ `${this.doUrl}/batch?db=${encodeURIComponent(this.dbName)}`,
716
+ JSON.stringify({ statements }),
717
+ { 'Content-Type': 'application/json' }
718
+ )
719
+ }
720
+
721
+ private inlineParams(sql: string, params: any[]): string {
722
+ let result = sql
723
+ for (let i = params.length; i >= 1; i--) {
724
+ const val = params[i - 1]
725
+ const esc =
726
+ val === null
727
+ ? 'NULL'
728
+ : typeof val === 'string'
729
+ ? `'${val.replace(/'/g, "''")}'`
730
+ : String(val)
731
+ result = result.replace(new RegExp(`\\$${i}\\b`, 'g'), esc)
732
+ }
733
+ return result
734
+ }
735
+
736
+ private buildSQLResponse(
737
+ originalSql: string,
738
+ rows: Record<string, unknown>[]
739
+ ): Uint8Array {
740
+ const isSelect = /^\s*select\b/i.test(originalSql) || /^\s*with\b/i.test(originalSql)
741
+ if (rows.length > 0) {
742
+ const fns = Object.keys(rows[0])
743
+ const tag = isSelect
744
+ ? `SELECT ${rows.length}`
745
+ : /^\s*insert\b/i.test(originalSql)
746
+ ? 'INSERT 0 1'
747
+ : /^\s*update\b/i.test(originalSql)
748
+ ? 'UPDATE 1'
749
+ : /^\s*delete\b/i.test(originalSql)
750
+ ? 'DELETE 1'
751
+ : 'OK'
752
+ return concat(
753
+ buildRowDescription(fns.map((n) => ({ name: n }))),
754
+ ...rows.map((r) => buildDataRow(r, fns)),
755
+ buildCommandComplete(tag),
756
+ buildReadyForQuery()
757
+ )
758
+ }
759
+ const tag = isSelect
760
+ ? 'SELECT 0'
761
+ : /^\s*insert\b/i.test(originalSql)
762
+ ? 'INSERT 0 0'
763
+ : /^\s*update\b/i.test(originalSql)
764
+ ? 'UPDATE 0'
765
+ : /^\s*delete\b/i.test(originalSql)
766
+ ? 'DELETE 0'
767
+ : 'OK'
768
+ return concat(buildCommandComplete(tag), buildReadyForQuery())
769
+ }
770
+
771
+ private buildSelectResponse(
772
+ rows: Record<string, unknown>[],
773
+ fields: { name: string; oid?: number }[]
774
+ ): Uint8Array {
775
+ const fns = fields.map((f) => f.name)
776
+ if (rows.length === 0)
777
+ return concat(
778
+ buildRowDescription(fields),
779
+ buildCommandComplete('SELECT 0'),
780
+ buildReadyForQuery()
781
+ )
782
+ return concat(
783
+ buildRowDescription(fields),
784
+ ...rows.map((r) => buildDataRow(r, fns)),
785
+ buildCommandComplete(`SELECT ${rows.length}`),
786
+ buildReadyForQuery()
787
+ )
788
+ }
789
+
790
+ private handleCatalogQuery(sql: string): {
791
+ rows: Record<string, unknown>[]
792
+ fields: { name: string; oid?: number }[]
793
+ } {
794
+ const n = sql.replace(/\s+/g, ' ').trim().toLowerCase()
795
+
796
+ // current_setting('server_version') etc.
797
+ const csMatch = /current_setting\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(n)
798
+ if (csMatch) {
799
+ const vals: Record<string, string> = {
800
+ server_version: '16.0',
801
+ server_encoding: 'UTF8',
802
+ client_encoding: 'UTF8',
803
+ standard_conforming_strings: 'on',
804
+ TimeZone: 'UTC',
805
+ integer_datetimes: 'on',
806
+ IntervalStyle: 'postgres',
807
+ DateStyle: 'ISO, MDY',
808
+ lc_messages: 'en_US.UTF-8',
809
+ lc_monetary: 'en_US.UTF-8',
810
+ lc_numeric: 'en_US.UTF-8',
811
+ lc_time: 'en_US.UTF-8',
812
+ }
813
+ return {
814
+ rows: [{ current_setting: vals[csMatch[1]] ?? '' }],
815
+ fields: [{ name: 'current_setting' }],
816
+ }
817
+ }
818
+
819
+ return { rows: [], fields: [] }
820
+ }
821
+ }
822
+
823
+ class HttpClient {
824
+ async post(
825
+ url: string,
826
+ body: string,
827
+ headers?: Record<string, string>
828
+ ): Promise<string> {
829
+ const resp = await fetch(url, {
830
+ method: 'POST',
831
+ headers: headers ?? { 'Content-Type': 'application/json' },
832
+ body,
833
+ })
834
+ if (!resp.ok) {
835
+ const text = await resp.text().catch(() => '')
836
+ throw new Error(`HTTP ${resp.status}: ${text.slice(0, 200)}`)
837
+ }
838
+ return resp.text()
839
+ }
840
+ }