jsql-neo 5.2.0 → 5.2.1
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/README.md +29 -1
- package/lib/database.js +20 -1
- package/lib/mongo_server.js +483 -0
- package/lib/multiserver.js +141 -0
- package/lib/mysql_server.js +1 -1
- package/lib/pg_server.js +864 -0
- package/lib/redis_server.js +24 -15
- package/package.json +1 -1
package/lib/pg_server.js
ADDED
|
@@ -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 };
|