jsql-neo 5.2.0 → 5.3.0

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.
@@ -0,0 +1,864 @@
1
+ /*
2
+ * PostgreSQL wire protocol v3 server for JSQL-NEO.
3
+ *
4
+ * Speaks the PostgreSQL frontend/backend protocol (v3) so that standard
5
+ * PostgreSQL clients (node-postgres, psql, pgAdmin, ...) can connect.
6
+ *
7
+ * const { PgServer } = require('jsql-neo');
8
+ * const srv = new PgServer({ port: 5432, dataDir: './data' });
9
+ * srv.listen();
10
+ *
11
+ * Supported:
12
+ * - Startup / SSLRequest decline / auth (SCRAM-SHA-256, MD5, cleartext)
13
+ * - Simple query protocol ('Q') with multiple statements
14
+ * - Extended query protocol ('P' parse / 'B' bind / 'D' describe / 'E' execute / 'S' sync)
15
+ * - Transactions (BEGIN/COMMIT/ROLLBACK), prepared statements
16
+ * - Type OIDs: int4/8, float4/8, bool, text/varchar, date, timestamp, json, uuid
17
+ * - Multiple databases (schemas) with per-database routing (db.table)
18
+ * - Multi-user auth: users map + per-database ACL (shared with MysqlServer)
19
+ */
20
+ const net = require('net');
21
+ const crypto = require('crypto');
22
+ const path = require('path');
23
+ const fs = require('fs');
24
+ const Database = require('./database');
25
+ const { executeSQL } = require('./sql');
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Protocol constants
29
+ // ---------------------------------------------------------------------------
30
+ const AUTH_OK = 0;
31
+ const AUTH_CLEARTEXT = 3;
32
+ const AUTH_MD5 = 5;
33
+ const AUTH_SCRAM = 10;
34
+
35
+ const TYPE_OIDS = {
36
+ bool: 16, bytea: 17, char: 18, int8: 20, int2: 21, int4: 23, text: 25,
37
+ json: 114, jsonb: 3802, float4: 700, float8: 701, numeric: 1700,
38
+ date: 1082, timestamp: 1114, timestamptz: 1184, time: 1083,
39
+ varchar: 1043, bpchar: 1042, uuid: 2950, serial: 23, bigserial: 20, name: 19
40
+ };
41
+
42
+ function pgTypeOid(sqlType, def) {
43
+ const t = String(sqlType || '').toLowerCase();
44
+ if (t === 'integer') return 23;
45
+ if (t === 'number') return 701;
46
+ if (t === 'boolean') return 16;
47
+ if (t === 'date') return 1082;
48
+ if (t === 'datetime' || t === 'timestamp') return 1114;
49
+ if (t === 'object') return 3802;
50
+ if (t === 'array') return 114;
51
+ return 1043;
52
+ }
53
+
54
+ function pgFormatValue(value, sqlType) {
55
+ if (value === null || value === undefined) return null;
56
+ const t = String(sqlType || '').toLowerCase();
57
+ if (t === 'boolean') return value === true || value === 1 || value === 'true' || value === 't' ? 't' : 'f';
58
+ if (t === 'object') {
59
+ if (typeof value === 'string') return value;
60
+ try { return JSON.stringify(value); } catch (e) { return String(value); }
61
+ }
62
+ if (value instanceof Date) {
63
+ return value.toISOString().replace('T', ' ').replace('Z', '') + (t === 'timestamp' ? '' : '');
64
+ }
65
+ if (t === 'date') {
66
+ const d = value instanceof Date ? value : new Date(value);
67
+ if (isNaN(d.getTime())) return String(value);
68
+ return d.toISOString().slice(0, 10);
69
+ }
70
+ if (t === 'datetime' || t === 'timestamp') {
71
+ const d = value instanceof Date ? value : new Date(value);
72
+ if (isNaN(d.getTime())) return String(value);
73
+ return d.toISOString().replace('T', ' ').slice(0, 19);
74
+ }
75
+ if (typeof value === 'boolean') return value ? 't' : 'f';
76
+ return String(value);
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Message writer
81
+ // ---------------------------------------------------------------------------
82
+ class PgBuffer {
83
+ constructor() {
84
+ this.chunks = [];
85
+ }
86
+ raw(buf) { this.chunks.push(buf); return this; }
87
+ c(v) { this.chunks.push(Buffer.from([v & 0xff])); return this; }
88
+ i32(v) {
89
+ const b = Buffer.alloc(4);
90
+ b.writeInt32BE(v, 0);
91
+ this.chunks.push(b);
92
+ return this;
93
+ }
94
+ i16(v) {
95
+ const b = Buffer.alloc(2);
96
+ b.writeInt16BE(v, 0);
97
+ this.chunks.push(b);
98
+ return this;
99
+ }
100
+ str(s) {
101
+ this.chunks.push(Buffer.from(String(s), 'utf8'));
102
+ return this;
103
+ }
104
+ nul() { this.chunks.push(Buffer.from([0])); return this; }
105
+ build() { return Buffer.concat(this.chunks); }
106
+ }
107
+
108
+ /** 帧消息: type + int32 length(含自身4字节) + payload */
109
+ function frame(type, payload) {
110
+ const len = 4 + payload.length;
111
+ const head = Buffer.alloc(5);
112
+ head[0] = type;
113
+ head.writeInt32BE(len, 1);
114
+ return Buffer.concat([head, payload]);
115
+ }
116
+
117
+ function buildDataRow(values, types) {
118
+ const p = new PgBuffer();
119
+ p.i16(values.length);
120
+ for (let i = 0; i < values.length; i++) {
121
+ const s = pgFormatValue(values[i], types[i]);
122
+ if (s === null) {
123
+ p.i32(-1);
124
+ } else {
125
+ const b = Buffer.from(s, 'utf8');
126
+ p.i32(b.length);
127
+ p.raw(b);
128
+ }
129
+ }
130
+ return frame(0x44, p.build()); // 'D'
131
+ }
132
+
133
+ function buildRowDescription(columns, types) {
134
+ const p = new PgBuffer();
135
+ p.i16(columns.length);
136
+ for (let i = 0; i < columns.length; i++) {
137
+ const c = columns[i];
138
+ p.str(c);
139
+ p.nul();
140
+ p.i32(0); // table oid
141
+ p.i16(0); // column attnum
142
+ p.i32(TYPE_OIDS[types[i]] || 1043);
143
+ p.i16(types[i] === 'integer' ? 4 : -1); // typlen
144
+ p.i32(-1); // typmod
145
+ p.i16(0); // format code (text)
146
+ }
147
+ return frame(0x54, p.build()); // 'T'
148
+ }
149
+
150
+ function buildCommandComplete(tag) {
151
+ const p = new PgBuffer();
152
+ p.str(tag).nul();
153
+ return frame(0x43, p.build()); // 'C'
154
+ }
155
+
156
+ function buildEmptyQuery() {
157
+ return frame(0x49, Buffer.alloc(0)); // 'I'
158
+ }
159
+
160
+ function buildErrorResponse(code, message, severity = 'ERROR') {
161
+ const p = new PgBuffer();
162
+ p.c(0x53); p.str(severity).nul(); // S
163
+ p.c(0x56); p.str(severity).nul(); // V
164
+ p.c(0x43); p.str(code).nul(); // C
165
+ p.c(0x4d); p.str(message).nul(); // M
166
+ p.c(0x00);
167
+ return frame(0x45, p.build()); // 'E'
168
+ }
169
+
170
+ function buildNotice(message) {
171
+ const p = new PgBuffer();
172
+ p.c(0x53); p.str('NOTICE').nul();
173
+ p.c(0x56); p.str('NOTICE').nul();
174
+ p.c(0x43); p.str('00000').nul();
175
+ p.c(0x4d); p.str(message).nul();
176
+ p.c(0x00);
177
+ return frame(0x4e, p.build()); // 'N'
178
+ }
179
+
180
+ function buildParameterStatus(name, value) {
181
+ const p = new PgBuffer();
182
+ p.str(name).nul();
183
+ p.str(value).nul();
184
+ return frame(0x53, p.build()); // 'S'
185
+ }
186
+
187
+ function buildReadyForQuery(status = 'I') {
188
+ return frame(0x5a, Buffer.from([status.charCodeAt(0)])); // 'Z'
189
+ }
190
+
191
+ function buildBackendKeyData(pid, secret) {
192
+ const p = new PgBuffer();
193
+ p.i32(pid);
194
+ p.i32(secret);
195
+ return frame(0x4b, p.build()); // 'K'
196
+ }
197
+
198
+ function buildParseComplete() { return frame(0x31, Buffer.alloc(0)); } // '1'
199
+ function buildBindComplete() { return frame(0x32, Buffer.alloc(0)); } // '2'
200
+ function buildCloseComplete() { return frame(0x33, Buffer.alloc(0)); } // '3'
201
+ function buildNoData() { return frame(0x6e, Buffer.alloc(0)); } // 'n'
202
+ function buildParameterDesc(paramTypes) {
203
+ const p = new PgBuffer();
204
+ p.i16(paramTypes.length);
205
+ for (const t of paramTypes) p.i32(t);
206
+ return frame(0x74, p.build()); // 't'
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // SCRAM-SHA-256 (RFC 5802 / RFC 7677) — server side
211
+ // ---------------------------------------------------------------------------
212
+ function base64encode(buf) { return Buffer.from(buf).toString('base64'); }
213
+ function base64decode(s) { return Buffer.from(s, 'base64'); }
214
+
215
+ function parseScramMessage(str) {
216
+ const out = {};
217
+ for (const part of String(str).split(',')) {
218
+ const idx = part.indexOf('=');
219
+ if (idx < 0) continue;
220
+ out[part.slice(0, idx)] = part.slice(idx + 1);
221
+ }
222
+ return out;
223
+ }
224
+
225
+ function scramPrepare(password) {
226
+ const salt = crypto.randomBytes(16);
227
+ const iterations = 4096;
228
+ // StoredKey / ServerKey (RFC 5802 §3)
229
+ const saltedPassword = crypto.pbkdf2Sync(Buffer.from(password, 'utf8'), salt, iterations, 32, 'sha256');
230
+ const clientKey = crypto.createHmac('sha256', saltedPassword).update('Client Key').digest();
231
+ const storedKey = crypto.createHash('sha256').update(clientKey).digest();
232
+ const serverKey = crypto.createHmac('sha256', saltedPassword).update('Server Key').digest();
233
+ return { salt, iterations, storedKey, serverKey };
234
+ }
235
+
236
+ /** 生成 server-first 消息(server nonce = client nonce + 随机后缀) */
237
+ function scramServerFirst(clientNonce, cred) {
238
+ const serverNonce = clientNonce + crypto.randomBytes(18).toString('base64url');
239
+ return { serverNonce, sf: `r=${serverNonce},s=${base64encode(cred.salt)},i=${cred.iterations}` };
240
+ }
241
+
242
+ /** 验证 client-final 消息,返回 server-final 消息 */
243
+ function scramVerify(clientFirstBare, serverNonce, clientFinal, cred) {
244
+ const cf = parseScramMessage(clientFinal);
245
+ const nonce = String(cf.r).split(',')[0];
246
+ if (nonce !== serverNonce) {
247
+ throw new Error('invalid-proof: nonce mismatch');
248
+ }
249
+ const proof = base64decode(cf.p);
250
+ // client-first bare: 去掉 GS2 header(gs2-header + authzid 两段)
251
+ const bare = String(clientFirstBare).split(',').slice(2).join(',');
252
+ const authMessage = bare + ',' + `r=${serverNonce},s=${base64encode(cred.salt)},i=${cred.iterations}` + ',' + clientFinal.slice(0, clientFinal.indexOf(',p='));
253
+ const clientSignature = crypto.createHmac('sha256', cred.storedKey).update(Buffer.from(authMessage, 'utf8')).digest();
254
+ const clientKey = Buffer.alloc(32);
255
+ for (let i = 0; i < 32; i++) clientKey[i] = proof[i] ^ clientSignature[i];
256
+ if (crypto.createHash('sha256').update(clientKey).digest().toString('base64') !== cred.storedKey.toString('base64')) {
257
+ throw new Error('invalid-proof');
258
+ }
259
+ const serverSignature = crypto.createHmac('sha256', cred.serverKey).update(Buffer.from(authMessage, 'utf8')).digest();
260
+ return { verifier: base64encode(serverSignature), nonce };
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Connection
265
+ // ---------------------------------------------------------------------------
266
+ class PgConnection {
267
+ constructor(socket, server) {
268
+ this.socket = socket;
269
+ this.server = server;
270
+ this.buffer = Buffer.alloc(0);
271
+ this.authenticated = false;
272
+ this.user = null;
273
+ this.database = null;
274
+ this._scram = null;
275
+ this._scramClientFirst = null;
276
+ this._scramServerFirst = null;
277
+ this._scramCred = null;
278
+ this._prepared = new Map(); // name -> { sql, paramTypes }
279
+ this._portal = new Map(); // name -> { values }
280
+ this._inTransaction = false;
281
+ this._txId = null;
282
+ this._pid = 0;
283
+ this._secret = 0;
284
+ this._startupDone = false;
285
+ this._waitingSslRestart = false;
286
+ this._msgQueue = [];
287
+ this._processing = false;
288
+ this.socket.on('data', (chunk) => this._onData(chunk));
289
+ this.socket.on('error', () => {});
290
+ this.socket.on('close', () => this._onClose());
291
+ }
292
+
293
+ _onClose() {
294
+ this.server._sockets.delete(this.socket);
295
+ }
296
+
297
+ _send(buf) {
298
+ if (!this.socket.destroyed) this.socket.write(buf);
299
+ }
300
+
301
+ _onData(chunk) {
302
+ this.buffer = Buffer.concat([this.buffer, chunk]);
303
+ if (!this._startupDone || this._waitingSslRestart) {
304
+ // 首消息: 长度 + 协议号(SSLRequest 拒绝后客户端重发的 startup 也是无类型消息)
305
+ if (this.buffer.length < 4) return;
306
+ const len = this.buffer.readInt32BE(0);
307
+ if (this.buffer.length < len) return;
308
+ const body = this.buffer.slice(0, len);
309
+ this.buffer = this.buffer.slice(len);
310
+ this._startupDone = true;
311
+ this._handleStartup(body);
312
+ return;
313
+ }
314
+ for (;;) {
315
+ if (this.buffer.length < 5) break;
316
+ // PG 帧: type(1) + int32 length(含自身4字节, 不含type) + payload
317
+ const len = this.buffer.readInt32BE(1);
318
+ if (len < 4 || this.buffer.length < 1 + len) break;
319
+ const type = this.buffer[0];
320
+ const body = this.buffer.slice(5, 1 + len);
321
+ this.buffer = this.buffer.slice(1 + len);
322
+ if (!this.authenticated) {
323
+ // 认证阶段消息(SASLInitialResponse 'p' / SASLResponse 'p')
324
+ if (type === 0x70) {
325
+ this._handleAuthMessage(body);
326
+ } else {
327
+ this._authError();
328
+ this.socket.end();
329
+ return;
330
+ }
331
+ continue;
332
+ }
333
+ this._pending = (this._pending || Promise.resolve()).then(async () => {
334
+ try {
335
+ if (type === 0x51) { // 'Q' simple query(async)
336
+ await this._handleSimpleQuery(body.toString('utf8').replace(/\0$/, ''));
337
+ } else {
338
+ await this._handleMessage(type, body);
339
+ }
340
+ } catch (e) {
341
+ this._send(buildErrorResponse('XX000', e && e.message ? e.message : String(e)));
342
+ this._send(buildReadyForQuery(this._inTransaction ? 'T' : 'I'));
343
+ }
344
+ });
345
+ }
346
+ if (this._pending) this._pending.catch(() => {});
347
+ }
348
+
349
+ _authError() {
350
+ this._send(buildErrorResponse('28000', 'pg_hba.conf rejects connection: no authentication attempted'));
351
+ this.socket.end();
352
+ }
353
+
354
+ _handleStartup(body) {
355
+ const version = body.readInt32BE(4);
356
+ if (version === 80877103) {
357
+ // SSLRequest: 拒绝后客户端重发 startup
358
+ this._send(Buffer.from([0x4e]));
359
+ this._startupDone = false;
360
+ this._waitingSslRestart = true;
361
+ return;
362
+ }
363
+ this._waitingSslRestart = false;
364
+ if (version !== 196608) {
365
+ this._send(buildErrorResponse('0A000', `unsupported protocol version ${version}`));
366
+ this.socket.end();
367
+ return;
368
+ }
369
+ let pos = 8;
370
+ const params = {};
371
+ while (pos < body.length) {
372
+ const name = body.toString('utf8', pos, body.indexOf(0, pos));
373
+ pos = body.indexOf(0, pos) + 1;
374
+ if (name === '') break;
375
+ const val = body.toString('utf8', pos, body.indexOf(0, pos));
376
+ pos = body.indexOf(0, pos) + 1;
377
+ params[name] = val;
378
+ }
379
+ this.user = params.user || 'postgres';
380
+ this.database = params.database || this.user;
381
+ this._pid = 10000 + (this.server._connSeq++ % 8000);
382
+ this._secret = Math.floor(Math.random() * 0x7fffffff);
383
+ this._startAuth();
384
+ }
385
+
386
+ _startAuth() {
387
+ const password = this.server._userPassword(this.user);
388
+ if (password === undefined) {
389
+ // 用户不存在
390
+ this._send(buildErrorResponse('28000', `password authentication failed for user "${this.user}"`));
391
+ this.socket.end();
392
+ return;
393
+ }
394
+ if (password === null || password === '') {
395
+ // 无密码 → trust
396
+ this._completeAuth();
397
+ return;
398
+ }
399
+ // SCRAM-SHA-256(PG 13+ 默认)
400
+ this._scramCred = scramPrepare(password);
401
+ // AuthenticationSASL (code 10): int32 code + mechanism list (each nul-terminated) + extra nul
402
+ const p = new PgBuffer();
403
+ p.i32(AUTH_SCRAM);
404
+ p.str('SCRAM-SHA-256').nul();
405
+ p.c(0x00);
406
+ this._send(frame(0x52, p.build())); // 'R'
407
+ this._scramStage = 'first';
408
+ }
409
+
410
+ _handleAuthMessage(body) {
411
+ if (this._scramStage === 'first') {
412
+ // SASLInitialResponse: mechanism(nul) + int32 dataLen + client-first
413
+ const mechEnd = body.indexOf(0);
414
+ let dataStart = mechEnd + 1;
415
+ let clientFirst = '';
416
+ let dataLen = 0;
417
+ if (dataStart + 4 <= body.length) {
418
+ dataLen = body.readInt32BE(dataStart);
419
+ clientFirst = body.toString('utf8', dataStart + 4, dataStart + 4 + dataLen);
420
+ }
421
+ this._scramClientFirst = clientFirst;
422
+ // client-first 中提取 client nonce:`n,,n=user,r=<nonce>`
423
+ const cfm = parseScramMessage(clientFirst);
424
+ const sf = scramServerFirst(String(cfm.r || ''), this._scramCred);
425
+ this._scramServerFirst = sf.sf;
426
+ this._scramNonce = sf.serverNonce;
427
+ this._scramStage = 'final';
428
+ // AuthenticationSASLContinue (code 11): int32 code + server-first-message
429
+ // 注意:data 不带尾随 nul(真实 PG 用 pq_sendbytes,node-pg 按 length-8 读取)
430
+ const sb = new PgBuffer();
431
+ sb.i32(11);
432
+ sb.str(this._scramServerFirst);
433
+ this._send(frame(0x52, sb.build())); // 'R'
434
+ return;
435
+ }
436
+ // SASLResponse: client-final-message (纯数据)
437
+ const clientFinal = body.toString('utf8');
438
+ try {
439
+ const res = scramVerify(this._scramClientFirst, this._scramNonce, clientFinal, this._scramCred);
440
+ // AuthenticationSASLFinal (code 12): int32 code + server-final-message(无尾随 nul)
441
+ const sf = new PgBuffer();
442
+ sf.i32(12);
443
+ sf.str('v=' + res.verifier);
444
+ this._send(frame(0x52, sf.build())); // 'R'
445
+ this._completeAuth();
446
+ } catch (e) {
447
+ this._send(buildErrorResponse('28000', 'password authentication failed for user "' + this.user + '"'));
448
+ this.socket.end();
449
+ }
450
+ }
451
+
452
+ _completeAuth() {
453
+ if (!this.server._canAccessDb(this.user, this.database)) {
454
+ this._send(buildErrorResponse('28000', `database "${this.database}" does not exist`));
455
+ this.socket.end();
456
+ return;
457
+ }
458
+ this.authenticated = true;
459
+ this._send(frame(0x52, (() => { const p = new PgBuffer(); p.i32(AUTH_OK); return p.build(); })()));
460
+ this._send(buildParameterStatus('server_version', '16.4 (jsql-neo ' + this.server.version + ')'));
461
+ this._send(buildParameterStatus('server_encoding', 'UTF8'));
462
+ this._send(buildParameterStatus('client_encoding', 'UTF8'));
463
+ this._send(buildParameterStatus('DateStyle', 'ISO, MDY'));
464
+ this._send(buildParameterStatus('integer_datetimes', 'on'));
465
+ this._send(buildParameterStatus('standard_conforming_strings', 'on'));
466
+ this._send(buildBackendKeyData(this._pid, this._secret));
467
+ this._send(buildReadyForQuery('I'));
468
+ }
469
+
470
+ _getEngine() {
471
+ return this.server._getEngine(this.database);
472
+ }
473
+
474
+ async _handleMessage(type, body) {
475
+ switch (type) {
476
+ case 0x50: { // 'P' parse
477
+ const parts = this._splitCStrings(body);
478
+ const name = parts[0], sql = parts[1];
479
+ const nParams = body.readInt16BE(this._cstringLen(body, 0) + this._cstringLen(body, this._cstringLen(body, 0)));
480
+ const paramTypes = [];
481
+ let off = this._cstringLen(body, 0) + this._cstringLen(body, this._cstringLen(body, 0)) + 2;
482
+ for (let i = 0; i < nParams; i++) { paramTypes.push(body.readInt32BE(off)); off += 4; }
483
+ this._prepared.set(name, { sql, paramTypes });
484
+ this._send(buildParseComplete());
485
+ break;
486
+ }
487
+ case 0x42: { // 'B' bind
488
+ const n0 = this._cstringLen(body, 0);
489
+ const portal = body.toString('utf8', 0, n0 - 1);
490
+ const stmtName = body.toString('utf8', n0, n0 + this._cstringLen(body, n0) - 1);
491
+ const stmt = this._prepared.get(stmtName);
492
+ if (!stmt) throw new Error(`prepared statement "${stmtName}" does not exist`);
493
+ let off = n0 + this._cstringLen(body, n0);
494
+ const nFormats = body.readInt16BE(off); off += 2;
495
+ const formats = [];
496
+ for (let i = 0; i < nFormats; i++) { formats.push(body.readInt16BE(off)); off += 2; }
497
+ const nValues = body.readInt16BE(off); off += 2;
498
+ const values = [];
499
+ for (let i = 0; i < nValues; i++) {
500
+ const vlen = body.readInt32BE(off); off += 4;
501
+ if (vlen === -1) { values.push(null); continue; }
502
+ const fmt = formats.length > 0 ? formats[Math.min(i, formats.length - 1)] : 0;
503
+ values.push(body.toString('utf8', off, off + vlen));
504
+ off += vlen;
505
+ }
506
+ this._portal.set(portal, { values, stmt });
507
+ this._send(buildBindComplete()); break;
508
+ }
509
+ case 0x44: // 'D' describe
510
+ this._handleDescribe(body);
511
+ break;
512
+ case 0x45: { // 'E' execute
513
+ const portalName = body.toString('utf8', 0, this._cstringLen(body, 0) - 1);
514
+ const portal = this._portal.get(portalName);
515
+ if (!portal) throw new Error(`portal "${portalName}" does not exist`);
516
+ return this._execute(portal);
517
+ }
518
+ case 0x43: // 'C' close
519
+ this._handleClose(body);
520
+ break;
521
+ case 0x53: // 'S' sync
522
+ this._send(buildReadyForQuery(this._inTransaction ? 'T' : 'I'));
523
+ break;
524
+ case 0x46: // 'F' function call
525
+ this._send(buildErrorResponse('0A000', 'function call not supported'));
526
+ break;
527
+ case 0x58: // 'X' terminate
528
+ this.socket.end();
529
+ break;
530
+ default:
531
+ this._send(buildErrorResponse('0A000', `unsupported message type ${String.fromCharCode(type)}`));
532
+ break;
533
+ }
534
+ }
535
+
536
+ _cstringLen(buf, start) {
537
+ const idx = buf.indexOf(0, start);
538
+ return idx === -1 ? buf.length - start : idx - start + 1;
539
+ }
540
+
541
+ _splitCStrings(buf) {
542
+ const parts = [];
543
+ let off = 0;
544
+ while (off < buf.length) {
545
+ const idx = buf.indexOf(0, off);
546
+ if (idx === -1) { parts.push(buf.toString('utf8', off)); break; }
547
+ parts.push(buf.toString('utf8', off, idx));
548
+ off = idx + 1;
549
+ }
550
+ return parts;
551
+ }
552
+
553
+ _handleDescribe(body) {
554
+ const kind = body[0];
555
+ const name = body.toString('utf8', 1, this._cstringLen(body, 1) - 1);
556
+ if (kind === 0x53) { // 'S' statement
557
+ const stmt = this._prepared.get(name);
558
+ if (!stmt) throw new Error(`prepared statement "${name}" does not exist`);
559
+ this._send(buildParameterDesc(stmt.paramTypes.length > 0 ? stmt.paramTypes : [0]));
560
+ const cols = this._describeCols(stmt.sql);
561
+ if (cols && cols.length > 0) {
562
+ this._send(buildRowDescription(cols.columns, cols.types));
563
+ } else {
564
+ this._send(buildNoData());
565
+ }
566
+ } else if (kind === 0x50) { // 'P' portal
567
+ const portal = this._portal.get(name);
568
+ if (!portal) throw new Error(`portal "${name}" does not exist`);
569
+ const cols = this._describeCols(portal.stmt.sql);
570
+ if (cols && cols.length > 0) {
571
+ this._send(buildRowDescription(cols.columns, cols.types));
572
+ } else {
573
+ this._send(buildNoData());
574
+ }
575
+ }
576
+ }
577
+
578
+ _handleClose(body) {
579
+ const kind = body[0];
580
+ const name = body.toString('utf8', 1, this._cstringLen(body, 1) - 1);
581
+ if (kind === 0x53) this._prepared.delete(name);
582
+ else if (kind === 0x50) this._portal.delete(name);
583
+ this._send(buildCloseComplete());
584
+ }
585
+
586
+ /** 预描述 SELECT 结果列(不执行) */
587
+ _describeCols(sql) {
588
+ try {
589
+ const stmt = this.server._parseForDescribe(sql, this.database);
590
+ if (!stmt || stmt.type !== 'select' || !stmt.from || !stmt.from.tables || stmt.from.tables.length === 0) return null;
591
+ const t = stmt.from.tables[0];
592
+ if (!t || !t.table) return null;
593
+ const schema = this.server._tableSchema(this.database, t.table);
594
+ if (!schema) return null;
595
+ const columns = [];
596
+ const types = [];
597
+ const push = (name, type) => {
598
+ if (!name || name.startsWith('_')) return;
599
+ columns.push(name);
600
+ types.push(type);
601
+ };
602
+ for (const c of stmt.columns) {
603
+ if (c.scalar) {
604
+ push(c.scalar.name, this._astType(c.scalar, schema));
605
+ } else if (c.expr === '*') {
606
+ for (const [name, def] of Object.entries(schema)) push(name, def.type);
607
+ } else if (c.expr) {
608
+ push(String(c.expr).includes('.') ? String(c.expr).slice(String(c.expr).lastIndexOf('.') + 1) : c.expr, schema[c.expr] ? schema[c.expr].type : 'string');
609
+ }
610
+ }
611
+ return { columns, types };
612
+ } catch (e) {
613
+ return null;
614
+ }
615
+ }
616
+
617
+ _astType(node, schema) {
618
+ if (!node) return 'string';
619
+ if (node.type === 'column') return schema[node.name] ? schema[node.name].type : 'string';
620
+ if (node.type === 'cast') return String(node.to).includes('int') ? 'integer' : 'string';
621
+ if (node.type === 'func') {
622
+ const n = String(node.name).toUpperCase();
623
+ if (n === 'COUNT' || n === 'SUM' || n === 'AVG' || n === 'MIN' || n === 'MAX') return 'number';
624
+ return 'string';
625
+ }
626
+ if (node.type === 'value') return typeof node.value === 'number' ? 'number' : 'string';
627
+ return 'string';
628
+ }
629
+
630
+ async _handleSimpleQuery(sql) {
631
+ const trimmed = sql.trim();
632
+ if (!trimmed) {
633
+ this._send(buildEmptyQuery());
634
+ this._send(buildReadyForQuery(this._inTransaction ? 'T' : 'I'));
635
+ return;
636
+ }
637
+ if (trimmed.startsWith('SET ')) {
638
+ this._send(buildCommandComplete('SET'));
639
+ this._send(buildReadyForQuery(this._inTransaction ? 'T' : 'I'));
640
+ return;
641
+ }
642
+ try {
643
+ await this._executeSql(sql, null);
644
+ } catch (e) {
645
+ this._send(buildErrorResponse(this._pgErrorCode(e), e && e.message ? e.message : String(e)));
646
+ this._send(buildReadyForQuery(this._inTransaction ? 'T' : 'I'));
647
+ return;
648
+ }
649
+ this._send(buildReadyForQuery(this._inTransaction ? 'T' : 'I'));
650
+ }
651
+
652
+ async _execute(portal) {
653
+ try {
654
+ const { sql } = portal.stmt;
655
+ await this._executeSql(sql, portal.values || []);
656
+ } catch (e) {
657
+ this._send(buildErrorResponse(this._pgErrorCode(e), e && e.message ? e.message : String(e)));
658
+ }
659
+ }
660
+
661
+ async _executeSql(sql, values) {
662
+ const engine = await this._getEngine();
663
+ const ctx = {
664
+ session: { currentDb: this.database, connectionId: this._pid },
665
+ params: values || []
666
+ };
667
+ let results;
668
+ if (values && values.length > 0) {
669
+ // 参数化:将 $n 解析为 AST 后求值,或直接注入
670
+ const prepared = sql.replace(/\$\d+/g, (m) => {
671
+ const idx = parseInt(m.slice(1), 10) - 1;
672
+ const v = values[idx];
673
+ if (v === null || v === undefined) return 'NULL';
674
+ if (typeof v === 'number') return String(v);
675
+ return "'" + String(v).replace(/\\/g, '\\\\').replace(/'/g, "''") + "'";
676
+ });
677
+ results = await executeSQL(engine, prepared, { dialect: 'pg', safety: false, session: ctx.session });
678
+ } else {
679
+ results = await executeSQL(engine, sql, { dialect: 'pg', safety: false, session: ctx.session });
680
+ }
681
+ const list = Array.isArray(results) ? results : [results];
682
+ for (const res of list) {
683
+ this._emitResult(res);
684
+ }
685
+ }
686
+
687
+ _emitResult(res) {
688
+ if (!res) return;
689
+ if (res.type === 'select') {
690
+ const types = this._resultTypes(res);
691
+ if (res.columns && res.columns.length > 0) {
692
+ this._send(buildRowDescription(res.columns, types));
693
+ }
694
+ for (const row of res.rows || []) {
695
+ this._send(buildDataRow(row, types));
696
+ }
697
+ const tag = `SELECT ${(res.rows || []).length}`;
698
+ this._send(buildCommandComplete(tag));
699
+ } else if (res.type === 'insert' || res.type === 'update' || res.type === 'delete') {
700
+ const tag = `${String(res.type).toUpperCase()} ${res.affectedRows != null ? res.affectedRows : 0}`;
701
+ this._send(buildCommandComplete(tag));
702
+ } else if (res.type === 'begin' || res.type === 'commit' || res.type === 'rollback') {
703
+ this._inTransaction = res.type === 'begin';
704
+ this._send(buildCommandComplete(String(res.type).toUpperCase()));
705
+ } else if (res.type === 'createTable' || res.type === 'dropTable' || res.type === 'createDatabase' || res.type === 'dropDatabase') {
706
+ this._send(buildCommandComplete('OK'));
707
+ } else if (res.type === 'showTables' || res.type === 'showDatabases' || res.type === 'showColumns') {
708
+ const types = (res.columns || []).map(() => 'string');
709
+ this._send(buildRowDescription(res.columns || [], types));
710
+ for (const row of res.rows || []) this._send(buildDataRow(row, types));
711
+ this._send(buildCommandComplete(`SELECT ${(res.rows || []).length}`));
712
+ } else {
713
+ this._send(buildCommandComplete('OK'));
714
+ }
715
+ }
716
+
717
+ _resultTypes(res) {
718
+ const t = {};
719
+ try {
720
+ const schema = this.server._tableSchema(this.database, res.table);
721
+ if (schema) {
722
+ for (const [name, def] of Object.entries(schema)) t[name] = def.type;
723
+ }
724
+ } catch (e) {}
725
+ return (res.columns || []).map(c => t[c] || 'string');
726
+ }
727
+
728
+ _pgErrorCode(e) {
729
+ if (!e) if (/Table .*(doesn't exist|does not exist)/i.test(m)) return '42P01';
730
+ return 'XX000';
731
+ const codeKey = e.code;
732
+ const m = String(e.message || '');
733
+ // Debug write to /tmp/pg_error_debug.txt
734
+ const fs = require('fs');
735
+ fs.writeFileSync('/tmp/pg_error_debug.txt', 'codeKey=' + codeKey + '\\nmessage=' + m + '\\n');
736
+ // Map JSQL error keys to PG SQLSTATE codes
737
+ if (codeKey === 'ER_NO_SUCH_TABLE' || /Table .* doesn\\'t exist/i.test(m)) return '42P01';
738
+ if (codeKey === 'ER_TABLE_EXISTS' || /already exists/i.test(m)) return '42P07';
739
+ if (codeKey === 'ER_DUP_ENTRY' || m.includes('ER_DUP_ENTRY') || /Duplicate entry/i.test(m)) return '23505';
740
+ if (codeKey === 'ER_BAD_FIELD_ERROR' || /Unknown column/i.test(m)) return '42703';
741
+ if (codeKey === 'ER_CANT_DROP_FIELD' || /Cannot drop column/i.test(m)) return '428NF';
742
+ if (codeKey === 'ER_DUP_FIELDNAME' || /Duplicate column name/i.test(m)) return '42711';
743
+ if (codeKey === 'ER_BAD_NULL_ERROR' || /cannot be null/i.test(m)) return '23502';
744
+ if (codeKey === 'ER_CHECK_CONSTRAINT' || /Check constraint/i.test(m)) return '23514';
745
+ if (codeKey === 'ER_DATA_TOO_LONG' || /Data too long/i.test(m)) return '22001';
746
+ if (codeKey === 'ER_OUT_OF_RANGE' || /Out of range/i.test(m)) return '22003';
747
+ if (codeKey === 'ER_NO_REFERENCED_ROW' || /foreign key/i.test(m)) return '23503';
748
+ if (codeKey === 'ER_SYNTAX_ERROR' || /syntax|Unexpected token|Expected/i.test(m)) return '42601';
749
+ if (/Table .*(doesn't exist|does not exist)/i.test(m)) return '42P01';
750
+ return 'XX000';
751
+ }
752
+ }
753
+
754
+ // ---------------------------------------------------------------------------
755
+ // Server
756
+ // ---------------------------------------------------------------------------
757
+ class PgServer {
758
+ constructor(options = {}) {
759
+ this.options = options;
760
+ this.port = options.port || 5432;
761
+ this.host = options.host || '127.0.0.1';
762
+ this.auth = options.auth || null; // { user: { password, databases: [...] } }
763
+ this.noAuth = options.noAuth === true;
764
+ this.dataDir = options.dataDir || null;
765
+ this.version = '5.1.3';
766
+ this._server = null;
767
+ this._sockets = new Set();
768
+ this._connSeq = 0;
769
+ this._databases = new Map();
770
+ this._engines = new Map();
771
+ }
772
+
773
+ _userPassword(user) {
774
+ if (this.auth) {
775
+ const entry = this.auth[user];
776
+ if (entry === undefined) return undefined;
777
+ return entry && typeof entry === 'object' ? (entry.password || '') : String(entry);
778
+ }
779
+ if (this.noAuth) return '';
780
+ if (this.options.user) {
781
+ if (user !== this.options.user) return undefined;
782
+ return this.options.password || '';
783
+ }
784
+ return '';
785
+ }
786
+
787
+ _canAccessDb(user, dbName) {
788
+ if (this.noAuth) return true;
789
+ if (!this.auth) {
790
+ // 单用户模式下仅允许该用户访问其默认库
791
+ return true;
792
+ }
793
+ const entry = this.auth[user];
794
+ if (!entry) return false;
795
+ if (entry && typeof entry === 'object' && Array.isArray(entry.databases)) {
796
+ return entry.databases.includes(dbName);
797
+ }
798
+ return true;
799
+ }
800
+
801
+ _dbDir() {
802
+ return this.dataDir && this.dataDir !== ':memory:' ? path.resolve(this.dataDir) : null;
803
+ }
804
+
805
+ async _getEngine(dbName) {
806
+ const key = dbName || 'default';
807
+ if (this._engines.has(key)) return this._engines.get(key);
808
+ let engine;
809
+ if (this._dbDir()) {
810
+ const dbPath = path.join(this._dbDir(), key);
811
+ engine = new Database(dbPath, { autoSave: true });
812
+ } else {
813
+ engine = new Database(':memory:', { autoSave: false });
814
+ }
815
+ this._engines.set(key, engine);
816
+ return engine;
817
+ }
818
+
819
+ _parseForDescribe(sql, database) {
820
+ const { parseSQL } = require('./sql');
821
+ return parseSQL(sql, 'pg');
822
+ }
823
+
824
+ _tableSchema(database, table) {
825
+ const engine = this._engines.get(database || 'default');
826
+ if (!engine) return null;
827
+ try {
828
+ return engine.getTableSchema ? engine.getTableSchema(table) : (engine._schemas ? engine._schemas[table] : null);
829
+ } catch (e) {
830
+ return null;
831
+ }
832
+ }
833
+
834
+ listen(cb) {
835
+ this._server = net.createServer((socket) => {
836
+ this._sockets.add(socket);
837
+ new PgConnection(socket, this);
838
+ });
839
+ this._server.listen(this.port, this.host, cb || (() => {}));
840
+ this._server.on('error', (err) => {
841
+ if (this.options.onError) this.options.onError(err);
842
+ else throw err;
843
+ });
844
+ return this;
845
+ }
846
+
847
+ get address() {
848
+ return this._server ? this._server.address() : null;
849
+ }
850
+
851
+ close(cb) {
852
+ for (const s of this._sockets) s.destroy();
853
+ for (const engine of this._engines.values()) {
854
+ if (typeof engine.stop === 'function') engine.stop();
855
+ }
856
+ this._engines.clear();
857
+ if (this._server) {
858
+ this._server.close(cb || (() => {}));
859
+ this._server = null;
860
+ } else if (cb) cb();
861
+ }
862
+ }
863
+
864
+ module.exports = { PgServer, PgConnection, buildRowDescription, buildDataRow, buildCommandComplete, buildErrorResponse, buildReadyForQuery, buildParameterStatus, buildBackendKeyData, buildParseComplete, buildBindComplete, buildCloseComplete, buildNoData, buildParameterDesc, TYPE_OIDS, pgTypeOid, pgFormatValue };