orez 0.0.47 → 0.0.49
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/admin/http-proxy.d.ts.map +1 -1
- package/dist/admin/http-proxy.js.map +1 -1
- package/dist/admin/log-store.d.ts.map +1 -1
- package/dist/admin/log-store.js.map +1 -1
- package/dist/admin/server.d.ts +2 -2
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js.map +1 -1
- package/dist/admin/ui.d.ts.map +1 -1
- package/dist/admin/ui.js +2 -2
- package/dist/admin/ui.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -112
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -249
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +0 -9
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +1 -24
- package/dist/log.js.map +1 -1
- package/dist/mutex.d.ts.map +1 -1
- package/dist/mutex.js +2 -13
- package/dist/mutex.js.map +1 -1
- package/dist/pg-proxy.d.ts +2 -3
- package/dist/pg-proxy.d.ts.map +1 -1
- package/dist/pg-proxy.js +167 -377
- package/dist/pg-proxy.js.map +1 -1
- package/dist/pglite-manager.d.ts +0 -1
- package/dist/pglite-manager.d.ts.map +1 -1
- package/dist/pglite-manager.js +1 -1
- package/dist/pglite-manager.js.map +1 -1
- package/dist/replication/change-tracker.d.ts +0 -6
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +0 -74
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +5 -47
- package/dist/replication/handler.js.map +1 -1
- package/dist/vite-plugin.d.ts +0 -3
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +0 -24
- package/dist/vite-plugin.js.map +1 -1
- package/package.json +5 -4
- package/src/admin/http-proxy.ts +5 -1
- package/src/admin/log-store.ts +4 -1
- package/src/admin/server.ts +7 -3
- package/src/admin/ui.ts +682 -680
- package/src/cli.ts +6 -111
- package/src/config.ts +0 -10
- package/src/index.ts +92 -262
- package/src/integration/integration.test.ts +264 -133
- package/src/log.ts +1 -25
- package/src/mutex.ts +2 -12
- package/src/pg-proxy.ts +187 -449
- package/src/pglite-manager.ts +1 -1
- package/src/replication/change-tracker.ts +0 -92
- package/src/replication/handler.ts +4 -50
- package/src/shim/hooks.mjs +34 -1
- package/src/vite-plugin.ts +0 -28
- package/src/wasm-sqlite.test.ts +1 -2
package/src/pg-proxy.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tcp proxy that makes pglite speak postgresql wire protocol.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* connections with large responses.
|
|
4
|
+
* uses pg-gateway to handle protocol lifecycle for regular connections,
|
|
5
|
+
* and directly handles the raw socket for replication connections.
|
|
7
6
|
*
|
|
8
7
|
* regular connections: forwarded to pglite via execProtocolRaw()
|
|
9
8
|
* replication connections: intercepted, replication protocol faked
|
|
@@ -15,6 +14,8 @@
|
|
|
15
14
|
|
|
16
15
|
import { createServer, type Server, type Socket } from 'node:net'
|
|
17
16
|
|
|
17
|
+
import { fromNodeSocket } from 'pg-gateway/node'
|
|
18
|
+
|
|
18
19
|
import { log } from './log.js'
|
|
19
20
|
import { Mutex } from './mutex.js'
|
|
20
21
|
import { handleReplicationQuery, handleStartReplication } from './replication/handler.js'
|
|
@@ -66,7 +67,6 @@ const QUERY_REWRITES: Array<{ match: RegExp; replace: string }> = [
|
|
|
66
67
|
// parameter status messages sent during connection handshake
|
|
67
68
|
// pg_restore and other tools read these to determine server capabilities
|
|
68
69
|
const SERVER_PARAMS: [string, string][] = [
|
|
69
|
-
['server_version', '16.4'],
|
|
70
70
|
['server_encoding', 'UTF8'],
|
|
71
71
|
['client_encoding', 'UTF8'],
|
|
72
72
|
['DateStyle', 'ISO, MDY'],
|
|
@@ -76,12 +76,7 @@ const SERVER_PARAMS: [string, string][] = [
|
|
|
76
76
|
['IntervalStyle', 'postgres'],
|
|
77
77
|
]
|
|
78
78
|
|
|
79
|
-
//
|
|
80
|
-
// pglite rejects SET TRANSACTION if any query (e.g. SET search_path) ran first
|
|
81
|
-
const NOOP_QUERY_PATTERNS: RegExp[] = [/^\s*SET\s+TRANSACTION\b/i, /^\s*SET\s+SESSION\b/i]
|
|
82
|
-
|
|
83
|
-
// ── wire protocol helpers ──
|
|
84
|
-
|
|
79
|
+
// build a ParameterStatus wire protocol message (type 'S', 0x53)
|
|
85
80
|
function buildParameterStatus(name: string, value: string): Uint8Array {
|
|
86
81
|
const encoder = new TextEncoder()
|
|
87
82
|
const nameBytes = encoder.encode(name)
|
|
@@ -100,64 +95,13 @@ function buildParameterStatus(name: string, value: string): Uint8Array {
|
|
|
100
95
|
return buf
|
|
101
96
|
}
|
|
102
97
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
new DataView(buf.buffer).setInt32(1, 8)
|
|
107
|
-
new DataView(buf.buffer).setInt32(5, 0) // auth ok
|
|
108
|
-
return buf
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function buildAuthCleartextPassword(): Uint8Array {
|
|
112
|
-
const buf = new Uint8Array(9)
|
|
113
|
-
buf[0] = 0x52 // 'R'
|
|
114
|
-
new DataView(buf.buffer).setInt32(1, 8)
|
|
115
|
-
new DataView(buf.buffer).setInt32(5, 3) // cleartext password
|
|
116
|
-
return buf
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function buildBackendKeyData(): Uint8Array {
|
|
120
|
-
const buf = new Uint8Array(13)
|
|
121
|
-
buf[0] = 0x4b // 'K'
|
|
122
|
-
new DataView(buf.buffer).setInt32(1, 12)
|
|
123
|
-
new DataView(buf.buffer).setInt32(5, process.pid)
|
|
124
|
-
new DataView(buf.buffer).setInt32(9, 0)
|
|
125
|
-
return buf
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function buildReadyForQuery(status: number = 0x49): Uint8Array {
|
|
129
|
-
const buf = new Uint8Array(6)
|
|
130
|
-
buf[0] = 0x5a // 'Z'
|
|
131
|
-
new DataView(buf.buffer).setInt32(1, 5)
|
|
132
|
-
buf[5] = status // 'I' = idle
|
|
133
|
-
return buf
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function buildErrorResponse(message: string): Uint8Array {
|
|
137
|
-
const encoder = new TextEncoder()
|
|
138
|
-
const msgBytes = encoder.encode(message)
|
|
139
|
-
// S(ERROR) + C(code) + M(message) + terminator
|
|
140
|
-
const sField = new Uint8Array([0x53, ...encoder.encode('ERROR'), 0])
|
|
141
|
-
const cField = new Uint8Array([0x43, ...encoder.encode('08006'), 0])
|
|
142
|
-
const mField = new Uint8Array([0x4d, ...msgBytes, 0])
|
|
143
|
-
const terminator = new Uint8Array([0])
|
|
144
|
-
const bodyLen = 4 + sField.length + cField.length + mField.length + terminator.length
|
|
145
|
-
const buf = new Uint8Array(1 + bodyLen)
|
|
146
|
-
buf[0] = 0x45 // 'E'
|
|
147
|
-
new DataView(buf.buffer).setInt32(1, bodyLen)
|
|
148
|
-
let pos = 5
|
|
149
|
-
buf.set(sField, pos)
|
|
150
|
-
pos += sField.length
|
|
151
|
-
buf.set(cField, pos)
|
|
152
|
-
pos += cField.length
|
|
153
|
-
buf.set(mField, pos)
|
|
154
|
-
pos += mField.length
|
|
155
|
-
buf.set(terminator, pos)
|
|
156
|
-
return buf
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ── query helpers ──
|
|
98
|
+
// queries to intercept and return no-op success (synthetic SET response)
|
|
99
|
+
// pglite rejects SET TRANSACTION if any query (e.g. SET search_path) ran first
|
|
100
|
+
const NOOP_QUERY_PATTERNS: RegExp[] = [/^\s*SET\s+TRANSACTION\b/i, /^\s*SET\s+SESSION\b/i]
|
|
160
101
|
|
|
102
|
+
/**
|
|
103
|
+
* extract query text from a Parse message (0x50).
|
|
104
|
+
*/
|
|
161
105
|
function extractParseQuery(data: Uint8Array): string | null {
|
|
162
106
|
if (data[0] !== 0x50) return null
|
|
163
107
|
let offset = 5
|
|
@@ -168,6 +112,9 @@ function extractParseQuery(data: Uint8Array): string | null {
|
|
|
168
112
|
return new TextDecoder().decode(data.subarray(queryStart, offset))
|
|
169
113
|
}
|
|
170
114
|
|
|
115
|
+
/**
|
|
116
|
+
* rebuild a Parse message with a modified query string.
|
|
117
|
+
*/
|
|
171
118
|
function rebuildParseMessage(data: Uint8Array, newQuery: string): Uint8Array {
|
|
172
119
|
let offset = 5
|
|
173
120
|
while (offset < data.length && data[offset] !== 0) offset++
|
|
@@ -197,6 +144,9 @@ function rebuildParseMessage(data: Uint8Array, newQuery: string): Uint8Array {
|
|
|
197
144
|
return result
|
|
198
145
|
}
|
|
199
146
|
|
|
147
|
+
/**
|
|
148
|
+
* rebuild a Simple Query message with a modified query string.
|
|
149
|
+
*/
|
|
200
150
|
function rebuildSimpleQuery(newQuery: string): Uint8Array {
|
|
201
151
|
const encoder = new TextEncoder()
|
|
202
152
|
const queryBytes = encoder.encode(newQuery + '\0')
|
|
@@ -207,6 +157,9 @@ function rebuildSimpleQuery(newQuery: string): Uint8Array {
|
|
|
207
157
|
return buf
|
|
208
158
|
}
|
|
209
159
|
|
|
160
|
+
/**
|
|
161
|
+
* intercept and rewrite query messages to make pglite look like real postgres.
|
|
162
|
+
*/
|
|
210
163
|
function interceptQuery(data: Uint8Array): Uint8Array {
|
|
211
164
|
const msgType = data[0]
|
|
212
165
|
|
|
@@ -250,6 +203,9 @@ function interceptQuery(data: Uint8Array): Uint8Array {
|
|
|
250
203
|
return data
|
|
251
204
|
}
|
|
252
205
|
|
|
206
|
+
/**
|
|
207
|
+
* check if a query should be intercepted as a no-op.
|
|
208
|
+
*/
|
|
253
209
|
function isNoopQuery(data: Uint8Array): boolean {
|
|
254
210
|
let query: string | null = null
|
|
255
211
|
if (data[0] === 0x51) {
|
|
@@ -263,6 +219,9 @@ function isNoopQuery(data: Uint8Array): boolean {
|
|
|
263
219
|
return NOOP_QUERY_PATTERNS.some((p) => p.test(query!))
|
|
264
220
|
}
|
|
265
221
|
|
|
222
|
+
/**
|
|
223
|
+
* build a synthetic "SET" command complete response.
|
|
224
|
+
*/
|
|
266
225
|
function buildSetCompleteResponse(): Uint8Array {
|
|
267
226
|
const encoder = new TextEncoder()
|
|
268
227
|
const tag = encoder.encode('SET\0')
|
|
@@ -282,6 +241,9 @@ function buildSetCompleteResponse(): Uint8Array {
|
|
|
282
241
|
return result
|
|
283
242
|
}
|
|
284
243
|
|
|
244
|
+
/**
|
|
245
|
+
* build a synthetic ParseComplete response for extended protocol no-ops.
|
|
246
|
+
*/
|
|
285
247
|
function buildParseCompleteResponse(): Uint8Array {
|
|
286
248
|
const pc = new Uint8Array(5)
|
|
287
249
|
pc[0] = 0x31 // ParseComplete
|
|
@@ -289,6 +251,9 @@ function buildParseCompleteResponse(): Uint8Array {
|
|
|
289
251
|
return pc
|
|
290
252
|
}
|
|
291
253
|
|
|
254
|
+
/**
|
|
255
|
+
* strip ReadyForQuery messages from a response buffer.
|
|
256
|
+
*/
|
|
292
257
|
function stripReadyForQuery(data: Uint8Array): Uint8Array {
|
|
293
258
|
if (data.length === 0) return data
|
|
294
259
|
|
|
@@ -320,299 +285,184 @@ function stripReadyForQuery(data: Uint8Array): Uint8Array {
|
|
|
320
285
|
return result
|
|
321
286
|
}
|
|
322
287
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
function socketWrite(socket: Socket, data: Uint8Array): Promise<void> {
|
|
326
|
-
if (data.length === 0 || socket.destroyed) return Promise.resolve()
|
|
327
|
-
return new Promise<void>((resolve, reject) => {
|
|
328
|
-
const ok = socket.write(data as any, (err) => (err ? reject(err) : resolve()))
|
|
329
|
-
// if buffer is full, the callback still fires when flushed
|
|
330
|
-
if (!ok) void 0
|
|
331
|
-
})
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// ── startup handshake ──
|
|
335
|
-
|
|
336
|
-
// parse startup message from raw bytes.
|
|
337
|
-
// handles SSLRequest (8 bytes, code 80877103) and StartupMessage.
|
|
338
|
-
function parseStartupMessage(buf: Buffer): {
|
|
339
|
-
isSSL: boolean
|
|
340
|
-
params: Record<string, string>
|
|
341
|
-
} {
|
|
342
|
-
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
343
|
-
const len = dv.getInt32(0)
|
|
344
|
-
const code = dv.getInt32(4)
|
|
345
|
-
|
|
346
|
-
// SSL request: length=8, code=80877103
|
|
347
|
-
if (len === 8 && code === 80877103) {
|
|
348
|
-
return { isSSL: true, params: {} }
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// startup message: length, protocol(196608=3.0), then key=value pairs
|
|
352
|
-
const params: Record<string, string> = {}
|
|
353
|
-
let offset = 8
|
|
354
|
-
while (offset < len) {
|
|
355
|
-
const keyStart = offset
|
|
356
|
-
while (offset < buf.length && buf[offset] !== 0) offset++
|
|
357
|
-
const key = buf.subarray(keyStart, offset).toString()
|
|
358
|
-
offset++
|
|
359
|
-
if (!key) break // double-null = end of params
|
|
360
|
-
const valStart = offset
|
|
361
|
-
while (offset < buf.length && buf[offset] !== 0) offset++
|
|
362
|
-
params[key] = buf.subarray(valStart, offset).toString()
|
|
363
|
-
offset++
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return { isSSL: false, params }
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// read exactly `n` bytes from socket
|
|
370
|
-
function readBytes(socket: Socket, n: number): Promise<Buffer> {
|
|
371
|
-
return new Promise((resolve, reject) => {
|
|
372
|
-
let collected = Buffer.alloc(0)
|
|
373
|
-
|
|
374
|
-
const onData = (chunk: Buffer) => {
|
|
375
|
-
collected = Buffer.concat([collected, chunk])
|
|
376
|
-
if (collected.length >= n) {
|
|
377
|
-
socket.removeListener('data', onData)
|
|
378
|
-
socket.removeListener('error', onError)
|
|
379
|
-
socket.removeListener('close', onClose)
|
|
380
|
-
socket.pause()
|
|
381
|
-
resolve(collected)
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
const onError = (err: Error) => {
|
|
385
|
-
socket.removeListener('data', onData)
|
|
386
|
-
socket.removeListener('close', onClose)
|
|
387
|
-
reject(err)
|
|
388
|
-
}
|
|
389
|
-
const onClose = () => {
|
|
390
|
-
socket.removeListener('data', onData)
|
|
391
|
-
socket.removeListener('error', onError)
|
|
392
|
-
reject(new Error('socket closed'))
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
socket.on('data', onData)
|
|
396
|
-
socket.on('error', onError)
|
|
397
|
-
socket.on('close', onClose)
|
|
398
|
-
socket.resume()
|
|
399
|
-
})
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// perform the startup handshake (SSL negotiation, auth, parameter status)
|
|
403
|
-
async function performHandshake(
|
|
404
|
-
socket: Socket,
|
|
288
|
+
export async function startPgProxy(
|
|
289
|
+
dbInput: PGlite | PGliteInstances,
|
|
405
290
|
config: ZeroLiteConfig
|
|
406
|
-
): Promise<
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (startup.isSSL) {
|
|
413
|
-
// reject SSL, client will reconnect without it
|
|
414
|
-
socket.write(Buffer.from('N'))
|
|
415
|
-
buf = await readBytes(socket, 8)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// now we have startup message header - read the rest if needed
|
|
419
|
-
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
420
|
-
const msgLen = dv.getInt32(0)
|
|
421
|
-
if (buf.length < msgLen) {
|
|
422
|
-
const rest = await readBytes(socket, msgLen - buf.length)
|
|
423
|
-
buf = Buffer.concat([buf, rest])
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const { params } = parseStartupMessage(buf)
|
|
427
|
-
|
|
428
|
-
// request cleartext password
|
|
429
|
-
socket.write(buildAuthCleartextPassword())
|
|
291
|
+
): Promise<Server> {
|
|
292
|
+
// normalize input: single PGlite instance = use it for all databases (backwards compat for tests)
|
|
293
|
+
const instances: PGliteInstances =
|
|
294
|
+
'postgres' in dbInput
|
|
295
|
+
? (dbInput as PGliteInstances)
|
|
296
|
+
: { postgres: dbInput as PGlite, cvr: dbInput as PGlite, cdb: dbInput as PGlite }
|
|
430
297
|
|
|
431
|
-
//
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (fullPwBuf.length < 1 + pwLen) {
|
|
437
|
-
const rest = await readBytes(socket, 1 + pwLen - fullPwBuf.length)
|
|
438
|
-
fullPwBuf = Buffer.concat([fullPwBuf, rest])
|
|
439
|
-
}
|
|
440
|
-
const password = fullPwBuf.subarray(5, 1 + pwLen - 1).toString()
|
|
441
|
-
|
|
442
|
-
// validate credentials
|
|
443
|
-
if (params.user !== config.pgUser || password !== config.pgPassword) {
|
|
444
|
-
socket.write(buildErrorResponse('authentication failed'))
|
|
445
|
-
socket.write(buildReadyForQuery())
|
|
446
|
-
socket.destroy()
|
|
447
|
-
throw new Error('auth failed')
|
|
298
|
+
// per-instance mutexes for serializing pglite access
|
|
299
|
+
const mutexes = {
|
|
300
|
+
postgres: new Mutex(),
|
|
301
|
+
cvr: new Mutex(),
|
|
302
|
+
cdb: new Mutex(),
|
|
448
303
|
}
|
|
449
304
|
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
socket.write(buildParameterStatus(name, value))
|
|
305
|
+
// helper to get instance + mutex for a database name
|
|
306
|
+
function getDbContext(dbName: string): { db: PGlite; mutex: Mutex } {
|
|
307
|
+
if (dbName === 'zero_cvr') return { db: instances.cvr, mutex: mutexes.cvr }
|
|
308
|
+
if (dbName === 'zero_cdb') return { db: instances.cdb, mutex: mutexes.cdb }
|
|
309
|
+
return { db: instances.postgres, mutex: mutexes.postgres }
|
|
456
310
|
}
|
|
457
311
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
socket.write(buildReadyForQuery())
|
|
463
|
-
|
|
464
|
-
return { params }
|
|
465
|
-
}
|
|
312
|
+
const server = createServer(async (socket: Socket) => {
|
|
313
|
+
// prevent idle timeouts from killing connections
|
|
314
|
+
socket.setKeepAlive(true, 30000)
|
|
315
|
+
socket.setTimeout(0)
|
|
466
316
|
|
|
467
|
-
|
|
317
|
+
let dbName = 'postgres'
|
|
318
|
+
let isReplicationConnection = false
|
|
468
319
|
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
320
|
+
// clean up pglite transaction state when a client disconnects
|
|
321
|
+
socket.on('close', async () => {
|
|
322
|
+
const { db, mutex } = getDbContext(dbName)
|
|
323
|
+
await mutex.acquire()
|
|
324
|
+
try {
|
|
325
|
+
await db.exec('ROLLBACK')
|
|
326
|
+
} catch {
|
|
327
|
+
// no transaction to rollback
|
|
328
|
+
} finally {
|
|
329
|
+
mutex.release()
|
|
330
|
+
}
|
|
331
|
+
})
|
|
474
332
|
|
|
475
|
-
|
|
333
|
+
try {
|
|
334
|
+
const connection = await fromNodeSocket(socket, {
|
|
335
|
+
serverVersion: '16.4',
|
|
336
|
+
auth: {
|
|
337
|
+
method: 'password',
|
|
338
|
+
getClearTextPassword() {
|
|
339
|
+
return config.pgPassword
|
|
340
|
+
},
|
|
341
|
+
validateCredentials(credentials: {
|
|
342
|
+
username: string
|
|
343
|
+
password: string
|
|
344
|
+
clearTextPassword: string
|
|
345
|
+
}) {
|
|
346
|
+
return (
|
|
347
|
+
credentials.password === credentials.clearTextPassword &&
|
|
348
|
+
credentials.username === config.pgUser
|
|
349
|
+
)
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
// send ParameterStatus messages that standard postgres tools expect
|
|
354
|
+
// pg-gateway sends server_version via the serverVersion option above,
|
|
355
|
+
// but tools like pg_restore also need encoding, datestyle, etc.
|
|
356
|
+
onAuthenticated() {
|
|
357
|
+
for (const [name, value] of SERVER_PARAMS) {
|
|
358
|
+
socket.write(buildParameterStatus(name, value))
|
|
359
|
+
}
|
|
360
|
+
},
|
|
476
361
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
let buffer: Buffer = Buffer.alloc(0)
|
|
490
|
-
let processing = false
|
|
491
|
-
|
|
492
|
-
async function processBuffer() {
|
|
493
|
-
if (processing) return
|
|
494
|
-
processing = true
|
|
495
|
-
socket.pause()
|
|
362
|
+
async onStartup(state) {
|
|
363
|
+
const params = state.clientParams
|
|
364
|
+
if (params?.replication === 'database') {
|
|
365
|
+
isReplicationConnection = true
|
|
366
|
+
}
|
|
367
|
+
dbName = params?.database || 'postgres'
|
|
368
|
+
log.debug.proxy(
|
|
369
|
+
`connection: db=${dbName} user=${params?.user} replication=${params?.replication || 'none'}`
|
|
370
|
+
)
|
|
371
|
+
const { db } = getDbContext(dbName)
|
|
372
|
+
await db.waitReady
|
|
373
|
+
},
|
|
496
374
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const msgType = buffer[0]
|
|
500
|
-
const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
|
|
501
|
-
const msgLen = dv.getInt32(1)
|
|
502
|
-
const totalLen = 1 + msgLen
|
|
375
|
+
async onMessage(data, state) {
|
|
376
|
+
if (!state.isAuthenticated) return
|
|
503
377
|
|
|
504
|
-
|
|
378
|
+
// handle replication connections (always go to postgres instance)
|
|
379
|
+
if (isReplicationConnection) {
|
|
380
|
+
if (data[0] === 0x51) {
|
|
381
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength)
|
|
382
|
+
const len = view.getInt32(1)
|
|
383
|
+
const query = new TextDecoder()
|
|
384
|
+
.decode(data.subarray(5, 1 + len - 1))
|
|
385
|
+
.replace(/\0$/, '')
|
|
386
|
+
log.debug.proxy(`repl query: ${query.slice(0, 200)}`)
|
|
387
|
+
}
|
|
388
|
+
return handleReplicationMessage(
|
|
389
|
+
data,
|
|
390
|
+
socket,
|
|
391
|
+
instances.postgres,
|
|
392
|
+
mutexes.postgres,
|
|
393
|
+
connection
|
|
394
|
+
)
|
|
395
|
+
}
|
|
505
396
|
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
397
|
+
// check for no-op queries
|
|
398
|
+
if (isNoopQuery(data)) {
|
|
399
|
+
if (data[0] === 0x51) {
|
|
400
|
+
return buildSetCompleteResponse()
|
|
401
|
+
} else if (data[0] === 0x50) {
|
|
402
|
+
return buildParseCompleteResponse()
|
|
403
|
+
}
|
|
404
|
+
}
|
|
511
405
|
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
406
|
+
// intercept and rewrite queries
|
|
407
|
+
data = interceptQuery(data)
|
|
408
|
+
|
|
409
|
+
// message-level locking on the connection's pglite instance
|
|
410
|
+
const { db, mutex } = getDbContext(dbName)
|
|
411
|
+
await mutex.acquire()
|
|
412
|
+
|
|
413
|
+
let result: Uint8Array
|
|
414
|
+
try {
|
|
415
|
+
result = await db.execProtocolRaw(data, {
|
|
416
|
+
throwOnError: false,
|
|
417
|
+
})
|
|
418
|
+
} catch (err) {
|
|
419
|
+
mutex.release()
|
|
420
|
+
throw err
|
|
516
421
|
}
|
|
517
422
|
|
|
518
|
-
//
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
continue
|
|
423
|
+
// strip ReadyForQuery from non-Sync/non-SimpleQuery responses
|
|
424
|
+
if (data[0] !== 0x53 && data[0] !== 0x51) {
|
|
425
|
+
result = stripReadyForQuery(result)
|
|
522
426
|
}
|
|
523
427
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
|
|
428
|
+
mutex.release()
|
|
429
|
+
return result
|
|
430
|
+
},
|
|
431
|
+
})
|
|
432
|
+
} catch (err) {
|
|
433
|
+
if (!socket.destroyed) {
|
|
434
|
+
socket.destroy()
|
|
530
435
|
}
|
|
531
|
-
|
|
532
|
-
processing = false
|
|
533
|
-
socket.resume()
|
|
534
436
|
}
|
|
437
|
+
})
|
|
535
438
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
server.listen(config.pgPort, '127.0.0.1', () => {
|
|
441
|
+
log.debug.proxy(`listening on port ${config.pgPort}`)
|
|
442
|
+
resolve(server)
|
|
539
443
|
})
|
|
540
|
-
|
|
541
|
-
socket.on('end', () => resolve())
|
|
542
|
-
socket.on('error', (err) => reject(err))
|
|
543
|
-
socket.on('close', () => resolve())
|
|
544
|
-
|
|
545
|
-
socket.resume()
|
|
444
|
+
server.on('error', reject)
|
|
546
445
|
})
|
|
547
446
|
}
|
|
548
447
|
|
|
549
|
-
async function
|
|
550
|
-
data: Uint8Array,
|
|
551
|
-
socket: Socket,
|
|
552
|
-
db: PGlite,
|
|
553
|
-
mutex: Mutex
|
|
554
|
-
): Promise<void> {
|
|
555
|
-
// check for no-op queries
|
|
556
|
-
if (isNoopQuery(data)) {
|
|
557
|
-
if (data[0] === 0x51) {
|
|
558
|
-
await socketWrite(socket, buildSetCompleteResponse())
|
|
559
|
-
return
|
|
560
|
-
} else if (data[0] === 0x50) {
|
|
561
|
-
await socketWrite(socket, buildParseCompleteResponse())
|
|
562
|
-
return
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// intercept and rewrite queries
|
|
567
|
-
data = interceptQuery(data)
|
|
568
|
-
|
|
569
|
-
// serialize pglite access
|
|
570
|
-
await mutex.acquire()
|
|
571
|
-
let result: Uint8Array
|
|
572
|
-
try {
|
|
573
|
-
result = await db.execProtocolRaw(data, { throwOnError: false })
|
|
574
|
-
} catch (err: any) {
|
|
575
|
-
mutex.release()
|
|
576
|
-
// send error response instead of killing the connection — PGlite internal
|
|
577
|
-
// errors shouldn't terminate the client's tcp session
|
|
578
|
-
log.debug.proxy(`execProtocolRaw error: ${err?.message || err}`)
|
|
579
|
-
const errMsg = err?.message || 'internal error'
|
|
580
|
-
const errResp = buildErrorResponse(errMsg)
|
|
581
|
-
const rfq = buildReadyForQuery(0x45) // 'E' = failed transaction
|
|
582
|
-
const combined = new Uint8Array(errResp.length + rfq.length)
|
|
583
|
-
combined.set(errResp, 0)
|
|
584
|
-
combined.set(rfq, errResp.length)
|
|
585
|
-
await socketWrite(socket, combined)
|
|
586
|
-
return
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// strip ReadyForQuery from non-Sync/non-SimpleQuery responses
|
|
590
|
-
if (data[0] !== 0x53 && data[0] !== 0x51) {
|
|
591
|
-
result = stripReadyForQuery(result)
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
mutex.release()
|
|
595
|
-
|
|
596
|
-
// write response directly to socket
|
|
597
|
-
await socketWrite(socket, result)
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
async function handleReplicationMsg(
|
|
448
|
+
async function handleReplicationMessage(
|
|
601
449
|
data: Uint8Array,
|
|
602
450
|
socket: Socket,
|
|
603
451
|
db: PGlite,
|
|
604
|
-
mutex: Mutex
|
|
605
|
-
|
|
606
|
-
|
|
452
|
+
mutex: Mutex,
|
|
453
|
+
connection: Awaited<ReturnType<typeof fromNodeSocket>>
|
|
454
|
+
): Promise<Uint8Array | undefined> {
|
|
455
|
+
if (data[0] !== 0x51) return undefined
|
|
607
456
|
|
|
608
457
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength)
|
|
609
458
|
const len = view.getInt32(1)
|
|
610
459
|
const query = new TextDecoder().decode(data.subarray(5, 1 + len - 1)).replace(/\0$/, '')
|
|
611
460
|
const upper = query.trim().toUpperCase()
|
|
612
461
|
|
|
613
|
-
|
|
614
|
-
|
|
462
|
+
// check if this is a START_REPLICATION command
|
|
615
463
|
if (upper.startsWith('START_REPLICATION')) {
|
|
464
|
+
await connection.detach()
|
|
465
|
+
|
|
616
466
|
const writer = {
|
|
617
467
|
write(chunk: Uint8Array) {
|
|
618
468
|
if (!socket.destroyed) {
|
|
@@ -623,143 +473,31 @@ async function handleReplicationMsg(
|
|
|
623
473
|
|
|
624
474
|
// drain incoming standby status updates
|
|
625
475
|
socket.on('data', (_chunk: Buffer) => {})
|
|
626
|
-
socket.on('close', () => socket.destroy())
|
|
627
476
|
|
|
628
|
-
|
|
629
|
-
|
|
477
|
+
socket.on('close', () => {
|
|
478
|
+
socket.destroy()
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
handleStartReplication(query, writer, db, mutex).catch((err) => {
|
|
630
482
|
log.debug.proxy(`replication stream ended: ${err}`)
|
|
631
483
|
})
|
|
632
|
-
return
|
|
484
|
+
return undefined
|
|
633
485
|
}
|
|
634
486
|
|
|
635
|
-
// handle replication queries + fallthrough to pglite
|
|
487
|
+
// handle replication queries + fallthrough to pglite, all under mutex
|
|
636
488
|
await mutex.acquire()
|
|
637
489
|
try {
|
|
638
490
|
const response = await handleReplicationQuery(query, db)
|
|
639
|
-
if (response)
|
|
640
|
-
await socketWrite(socket, response)
|
|
641
|
-
return
|
|
642
|
-
}
|
|
491
|
+
if (response) return response
|
|
643
492
|
|
|
644
493
|
// apply query rewrites before forwarding
|
|
645
494
|
data = interceptQuery(data)
|
|
646
495
|
|
|
647
|
-
|
|
648
|
-
await
|
|
496
|
+
// fall through to pglite for unrecognized queries
|
|
497
|
+
return await db.execProtocolRaw(data, {
|
|
498
|
+
throwOnError: false,
|
|
499
|
+
})
|
|
649
500
|
} finally {
|
|
650
501
|
mutex.release()
|
|
651
502
|
}
|
|
652
503
|
}
|
|
653
|
-
|
|
654
|
-
// ── main entry point ──
|
|
655
|
-
|
|
656
|
-
export async function startPgProxy(
|
|
657
|
-
dbInput: PGlite | PGliteInstances,
|
|
658
|
-
config: ZeroLiteConfig
|
|
659
|
-
): Promise<Server> {
|
|
660
|
-
// normalize input: single PGlite instance = use it for all databases (backwards compat for tests)
|
|
661
|
-
const instances: PGliteInstances =
|
|
662
|
-
'postgres' in dbInput
|
|
663
|
-
? (dbInput as PGliteInstances)
|
|
664
|
-
: { postgres: dbInput as PGlite, cvr: dbInput as PGlite, cdb: dbInput as PGlite }
|
|
665
|
-
|
|
666
|
-
// per-instance mutexes for serializing pglite access
|
|
667
|
-
const mutexes = {
|
|
668
|
-
postgres: new Mutex(),
|
|
669
|
-
cvr: new Mutex(),
|
|
670
|
-
cdb: new Mutex(),
|
|
671
|
-
}
|
|
672
|
-
function getDbContext(dbName: string): { db: PGlite; mutex: Mutex } {
|
|
673
|
-
if (dbName === 'zero_cvr') return { db: instances.cvr, mutex: mutexes.cvr }
|
|
674
|
-
if (dbName === 'zero_cdb') return { db: instances.cdb, mutex: mutexes.cdb }
|
|
675
|
-
return { db: instances.postgres, mutex: mutexes.postgres }
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const server = createServer(async (socket: Socket) => {
|
|
679
|
-
socket.setKeepAlive(true, 30000)
|
|
680
|
-
socket.setTimeout(0)
|
|
681
|
-
socket.setNoDelay(true)
|
|
682
|
-
|
|
683
|
-
let dbName = 'postgres'
|
|
684
|
-
let isReplicationConnection = false
|
|
685
|
-
const connId = ++connCounter
|
|
686
|
-
|
|
687
|
-
try {
|
|
688
|
-
// perform startup handshake
|
|
689
|
-
const { params } = await performHandshake(socket, config)
|
|
690
|
-
|
|
691
|
-
dbName = params.database || 'postgres'
|
|
692
|
-
isReplicationConnection = params.replication === 'database'
|
|
693
|
-
|
|
694
|
-
// track active connections per database
|
|
695
|
-
activeConns[dbName] = (activeConns[dbName] || 0) + 1
|
|
696
|
-
|
|
697
|
-
console.info(`[orez-proxy#${connId}] connect db=${dbName} repl=${params.replication || 'none'}`)
|
|
698
|
-
|
|
699
|
-
const { db } = getDbContext(dbName)
|
|
700
|
-
await db.waitReady
|
|
701
|
-
|
|
702
|
-
// clean up pglite session state when client disconnects.
|
|
703
|
-
// pglite is single-session — all connections share one session.
|
|
704
|
-
// only ROLLBACK + reset when this is the LAST connection for this db,
|
|
705
|
-
// to avoid killing another connection's active transaction.
|
|
706
|
-
socket.on('close', async () => {
|
|
707
|
-
activeConns[dbName] = Math.max(0, (activeConns[dbName] || 1) - 1)
|
|
708
|
-
const remaining = activeConns[dbName]
|
|
709
|
-
const shouldRollback = remaining === 0
|
|
710
|
-
|
|
711
|
-
console.info(
|
|
712
|
-
`[orez-proxy#${connId}] close [${dbName}] (remaining=${remaining}, shouldRollback=${shouldRollback})`
|
|
713
|
-
)
|
|
714
|
-
|
|
715
|
-
if (!shouldRollback) return
|
|
716
|
-
|
|
717
|
-
const { db: closeDb, mutex: closeMutex } = getDbContext(dbName)
|
|
718
|
-
await closeMutex.acquire()
|
|
719
|
-
try {
|
|
720
|
-
await closeDb.exec('ROLLBACK')
|
|
721
|
-
} catch {
|
|
722
|
-
// no transaction to rollback
|
|
723
|
-
}
|
|
724
|
-
try {
|
|
725
|
-
await closeDb.exec(`SET search_path TO public`)
|
|
726
|
-
await closeDb.exec(`RESET statement_timeout`)
|
|
727
|
-
await closeDb.exec(`RESET lock_timeout`)
|
|
728
|
-
await closeDb.exec(`RESET idle_in_transaction_session_timeout`)
|
|
729
|
-
} catch {
|
|
730
|
-
// best-effort reset
|
|
731
|
-
} finally {
|
|
732
|
-
closeMutex.release()
|
|
733
|
-
}
|
|
734
|
-
})
|
|
735
|
-
|
|
736
|
-
// enter message processing loop
|
|
737
|
-
const { db: msgDb, mutex: msgMutex } = getDbContext(dbName)
|
|
738
|
-
await messageLoop(
|
|
739
|
-
socket,
|
|
740
|
-
msgDb,
|
|
741
|
-
msgMutex,
|
|
742
|
-
isReplicationConnection,
|
|
743
|
-
instances.postgres,
|
|
744
|
-
mutexes.postgres
|
|
745
|
-
)
|
|
746
|
-
} catch (err: any) {
|
|
747
|
-
const msg = err?.message || err
|
|
748
|
-
// suppress expected errors (client disconnected, auth failures)
|
|
749
|
-
if (msg !== 'auth failed' && msg !== 'socket closed') {
|
|
750
|
-
log.debug.proxy(`connection error: ${msg}`)
|
|
751
|
-
}
|
|
752
|
-
if (!socket.destroyed) {
|
|
753
|
-
socket.destroy()
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
})
|
|
757
|
-
|
|
758
|
-
return new Promise((resolve, reject) => {
|
|
759
|
-
server.listen(config.pgPort, '127.0.0.1', () => {
|
|
760
|
-
log.debug.proxy(`listening on port ${config.pgPort}`)
|
|
761
|
-
resolve(server)
|
|
762
|
-
})
|
|
763
|
-
server.on('error', reject)
|
|
764
|
-
})
|
|
765
|
-
}
|