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