pgserve 2.2.4 → 2.4.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.
- package/bin/pgserve-wrapper.cjs +5 -4
- package/bin/postgres-server.js +142 -631
- package/config/logrotate.d/pgserve +47 -0
- package/config/pgaudit.conf +31 -0
- package/package.json +2 -2
- package/scripts/test-npx.sh +32 -10
- package/src/cli-install.cjs +147 -77
- package/src/commands/uninstall.js +241 -0
- package/src/index.js +11 -44
- package/src/lib/admin-json.js +202 -0
- package/src/lib/pm2-args.js +119 -0
- package/src/lib/socket-dir.js +69 -0
- package/src/postgres.js +64 -5
- package/src/admin-client.js +0 -223
- package/src/audit.js +0 -168
- package/src/cluster.js +0 -654
- package/src/control-db.js +0 -330
- package/src/daemon-control.js +0 -468
- package/src/daemon-shared.js +0 -18
- package/src/daemon-tcp.js +0 -297
- package/src/daemon.js +0 -709
- package/src/dashboard.js +0 -217
- package/src/fingerprint.js +0 -479
- package/src/gc.js +0 -351
- package/src/pg-wire.js +0 -869
- package/src/protocol.js +0 -389
- package/src/restore.js +0 -574
- package/src/router.js +0 -546
- package/src/sdk.js +0 -137
- package/src/stats-collector.js +0 -453
- package/src/stats-dashboard.js +0 -401
- package/src/sync.js +0 -335
- package/src/tenancy.js +0 -75
- package/src/tokens.js +0 -102
package/src/pg-wire.js
DELETED
|
@@ -1,869 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PostgreSQL Wire Protocol Client
|
|
3
|
-
*
|
|
4
|
-
* Native Bun implementation for PostgreSQL connections using Bun.connect().
|
|
5
|
-
* Implements: Startup, Authentication (MD5/trust), Simple Query, COPY TO/FROM.
|
|
6
|
-
*
|
|
7
|
-
* Protocol Reference: https://www.postgresql.org/docs/current/protocol.html
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { createHash } from 'crypto';
|
|
11
|
-
|
|
12
|
-
// PostgreSQL Protocol Version 3.0
|
|
13
|
-
const PROTOCOL_VERSION = 196608;
|
|
14
|
-
|
|
15
|
-
// Frontend (client → server) message codes
|
|
16
|
-
const FE = {
|
|
17
|
-
Query: 0x51, // 'Q' - Simple query
|
|
18
|
-
Terminate: 0x58, // 'X' - Connection termination
|
|
19
|
-
PasswordMessage: 0x70, // 'p' - Password response
|
|
20
|
-
CopyData: 0x64, // 'd' - COPY data chunk
|
|
21
|
-
CopyDone: 0x63, // 'c' - COPY complete
|
|
22
|
-
CopyFail: 0x66, // 'f' - COPY failed
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Backend (server → client) message codes
|
|
26
|
-
const BE = {
|
|
27
|
-
AuthenticationRequest: 0x52, // 'R' - Authentication request/ok
|
|
28
|
-
BackendKeyData: 0x4b, // 'K' - Process ID and secret key
|
|
29
|
-
ParameterStatus: 0x53, // 'S' - Server parameter change
|
|
30
|
-
ReadyForQuery: 0x5a, // 'Z' - Ready for next query
|
|
31
|
-
RowDescription: 0x54, // 'T' - Column metadata
|
|
32
|
-
DataRow: 0x44, // 'D' - Row data
|
|
33
|
-
CommandComplete: 0x43, // 'C' - Command finished
|
|
34
|
-
ErrorResponse: 0x45, // 'E' - Error
|
|
35
|
-
NoticeResponse: 0x4e, // 'N' - Warning/notice
|
|
36
|
-
CopyOutResponse: 0x48, // 'H' - COPY TO started
|
|
37
|
-
CopyInResponse: 0x47, // 'G' - COPY FROM ready
|
|
38
|
-
CopyDone: 0x63, // 'c' - Server COPY complete
|
|
39
|
-
EmptyQueryResponse: 0x49, // 'I' - Empty query
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Authentication types
|
|
43
|
-
const AUTH = {
|
|
44
|
-
Ok: 0,
|
|
45
|
-
CleartextPassword: 3,
|
|
46
|
-
MD5Password: 5,
|
|
47
|
-
SASL: 10,
|
|
48
|
-
SASLContinue: 11,
|
|
49
|
-
SASLFinal: 12,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* PostgreSQL Wire Protocol Client
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* const client = new PgWireClient({
|
|
57
|
-
* hostname: '127.0.0.1',
|
|
58
|
-
* port: 5432,
|
|
59
|
-
* database: 'mydb',
|
|
60
|
-
* username: 'postgres',
|
|
61
|
-
* password: 'postgres'
|
|
62
|
-
* });
|
|
63
|
-
* await client.connect();
|
|
64
|
-
* const rows = await client.query('SELECT * FROM users');
|
|
65
|
-
* client.close();
|
|
66
|
-
*/
|
|
67
|
-
export class PgWireClient {
|
|
68
|
-
constructor(options = {}) {
|
|
69
|
-
this.hostname = options.hostname || '127.0.0.1';
|
|
70
|
-
this.port = options.port || 5432;
|
|
71
|
-
this.unix = options.unix || null;
|
|
72
|
-
this.database = options.database || 'postgres';
|
|
73
|
-
this.username = options.username || 'postgres';
|
|
74
|
-
this.password = options.password || 'postgres';
|
|
75
|
-
|
|
76
|
-
this.socket = null;
|
|
77
|
-
this.buffer = Buffer.alloc(0);
|
|
78
|
-
this.state = 'disconnected';
|
|
79
|
-
|
|
80
|
-
// Promise management for async operations
|
|
81
|
-
this._connectResolve = null;
|
|
82
|
-
this._connectReject = null;
|
|
83
|
-
this._queryResolve = null;
|
|
84
|
-
this._queryReject = null;
|
|
85
|
-
this._copyResolve = null;
|
|
86
|
-
this._copyReject = null;
|
|
87
|
-
|
|
88
|
-
// Query result accumulation
|
|
89
|
-
this._columns = [];
|
|
90
|
-
this._rows = [];
|
|
91
|
-
this._commandTag = '';
|
|
92
|
-
|
|
93
|
-
// COPY streaming
|
|
94
|
-
this._copyChunks = [];
|
|
95
|
-
this._copyCallback = null;
|
|
96
|
-
|
|
97
|
-
// Backend key data (for cancel requests)
|
|
98
|
-
this.processId = null;
|
|
99
|
-
this.secretKey = null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Connect to PostgreSQL server
|
|
104
|
-
* @returns {Promise<void>}
|
|
105
|
-
*/
|
|
106
|
-
async connect() {
|
|
107
|
-
return new Promise((resolve, reject) => {
|
|
108
|
-
this._connectResolve = resolve;
|
|
109
|
-
this._connectReject = reject;
|
|
110
|
-
|
|
111
|
-
const connectOpts = this.unix
|
|
112
|
-
? { unix: this.unix }
|
|
113
|
-
: { hostname: this.hostname, port: this.port };
|
|
114
|
-
|
|
115
|
-
const client = this;
|
|
116
|
-
|
|
117
|
-
Bun.connect({
|
|
118
|
-
...connectOpts,
|
|
119
|
-
socket: {
|
|
120
|
-
open(socket) {
|
|
121
|
-
client.socket = socket;
|
|
122
|
-
client.state = 'startup';
|
|
123
|
-
// Send startup message
|
|
124
|
-
const startup = client._buildStartupMessage();
|
|
125
|
-
socket.write(startup);
|
|
126
|
-
},
|
|
127
|
-
data(socket, data) {
|
|
128
|
-
client._onData(data);
|
|
129
|
-
},
|
|
130
|
-
close() {
|
|
131
|
-
client._onClose();
|
|
132
|
-
},
|
|
133
|
-
error(socket, err) {
|
|
134
|
-
client._onError(err);
|
|
135
|
-
},
|
|
136
|
-
drain() {
|
|
137
|
-
// Handle backpressure if needed
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}).catch(reject);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Build PostgreSQL startup message
|
|
146
|
-
* @private
|
|
147
|
-
*/
|
|
148
|
-
_buildStartupMessage() {
|
|
149
|
-
const params = `user\0${this.username}\0database\0${this.database}\0\0`;
|
|
150
|
-
const len = 4 + 4 + params.length;
|
|
151
|
-
const buf = Buffer.alloc(len);
|
|
152
|
-
buf.writeUInt32BE(len, 0);
|
|
153
|
-
buf.writeUInt32BE(PROTOCOL_VERSION, 4);
|
|
154
|
-
buf.write(params, 8);
|
|
155
|
-
return buf;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Handle incoming data
|
|
160
|
-
* @private
|
|
161
|
-
*/
|
|
162
|
-
_onData(data) {
|
|
163
|
-
// Append to buffer
|
|
164
|
-
this.buffer = Buffer.concat([this.buffer, Buffer.from(data)]);
|
|
165
|
-
|
|
166
|
-
// Process complete messages
|
|
167
|
-
this._processMessages();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Process buffered messages
|
|
172
|
-
* @private
|
|
173
|
-
*/
|
|
174
|
-
_processMessages() {
|
|
175
|
-
while (this.buffer.length >= 5) {
|
|
176
|
-
const type = this.buffer[0];
|
|
177
|
-
const length = this.buffer.readUInt32BE(1);
|
|
178
|
-
const totalLength = 1 + length;
|
|
179
|
-
|
|
180
|
-
if (this.buffer.length < totalLength) {
|
|
181
|
-
break; // Wait for more data
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const payload = this.buffer.subarray(5, totalLength);
|
|
185
|
-
this.buffer = this.buffer.subarray(totalLength);
|
|
186
|
-
|
|
187
|
-
this._handleMessage(type, payload);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Handle a single message
|
|
193
|
-
* @private
|
|
194
|
-
*/
|
|
195
|
-
_handleMessage(type, payload) {
|
|
196
|
-
switch (type) {
|
|
197
|
-
case BE.AuthenticationRequest:
|
|
198
|
-
this._handleAuth(payload);
|
|
199
|
-
break;
|
|
200
|
-
|
|
201
|
-
case BE.BackendKeyData:
|
|
202
|
-
this.processId = payload.readUInt32BE(0);
|
|
203
|
-
this.secretKey = payload.readUInt32BE(4);
|
|
204
|
-
break;
|
|
205
|
-
|
|
206
|
-
case BE.ParameterStatus:
|
|
207
|
-
// Server parameter - ignore for now
|
|
208
|
-
break;
|
|
209
|
-
|
|
210
|
-
case BE.ReadyForQuery:
|
|
211
|
-
this._handleReadyForQuery(payload);
|
|
212
|
-
break;
|
|
213
|
-
|
|
214
|
-
case BE.RowDescription:
|
|
215
|
-
this._handleRowDescription(payload);
|
|
216
|
-
break;
|
|
217
|
-
|
|
218
|
-
case BE.DataRow:
|
|
219
|
-
this._handleDataRow(payload);
|
|
220
|
-
break;
|
|
221
|
-
|
|
222
|
-
case BE.CommandComplete:
|
|
223
|
-
this._commandTag = payload.toString('utf8', 0, payload.indexOf(0));
|
|
224
|
-
break;
|
|
225
|
-
|
|
226
|
-
case BE.ErrorResponse:
|
|
227
|
-
this._handleError(payload);
|
|
228
|
-
break;
|
|
229
|
-
|
|
230
|
-
case BE.NoticeResponse:
|
|
231
|
-
// Warning - ignore for now
|
|
232
|
-
break;
|
|
233
|
-
|
|
234
|
-
case BE.EmptyQueryResponse:
|
|
235
|
-
// Empty query - no rows
|
|
236
|
-
break;
|
|
237
|
-
|
|
238
|
-
case BE.CopyOutResponse:
|
|
239
|
-
this._handleCopyOutResponse(payload);
|
|
240
|
-
break;
|
|
241
|
-
|
|
242
|
-
case BE.CopyInResponse:
|
|
243
|
-
this._handleCopyInResponse(payload);
|
|
244
|
-
break;
|
|
245
|
-
|
|
246
|
-
case BE.CopyDone:
|
|
247
|
-
// Server confirms COPY complete
|
|
248
|
-
break;
|
|
249
|
-
|
|
250
|
-
case FE.CopyData:
|
|
251
|
-
// This is actually BE CopyData (same code)
|
|
252
|
-
this._handleCopyData(payload);
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Handle authentication request
|
|
259
|
-
* @private
|
|
260
|
-
*/
|
|
261
|
-
_handleAuth(payload) {
|
|
262
|
-
const authType = payload.readUInt32BE(0);
|
|
263
|
-
|
|
264
|
-
switch (authType) {
|
|
265
|
-
case AUTH.Ok:
|
|
266
|
-
this.state = 'authenticated';
|
|
267
|
-
break;
|
|
268
|
-
|
|
269
|
-
case AUTH.CleartextPassword:
|
|
270
|
-
this._sendPassword(this.password);
|
|
271
|
-
break;
|
|
272
|
-
|
|
273
|
-
case AUTH.MD5Password:
|
|
274
|
-
const salt = payload.subarray(4, 8);
|
|
275
|
-
const hash = this._md5Auth(this.username, this.password, salt);
|
|
276
|
-
this._sendPassword('md5' + hash);
|
|
277
|
-
break;
|
|
278
|
-
|
|
279
|
-
case AUTH.SASL:
|
|
280
|
-
// SCRAM-SHA-256 not yet implemented
|
|
281
|
-
this._rejectConnect(new Error('SCRAM-SHA-256 authentication not yet implemented'));
|
|
282
|
-
break;
|
|
283
|
-
|
|
284
|
-
default:
|
|
285
|
-
this._rejectConnect(new Error(`Unsupported authentication type: ${authType}`));
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Compute MD5 password hash
|
|
291
|
-
* @private
|
|
292
|
-
*/
|
|
293
|
-
_md5Auth(user, password, salt) {
|
|
294
|
-
// md5(md5(password + user) + salt)
|
|
295
|
-
const inner = createHash('md5')
|
|
296
|
-
.update(password + user)
|
|
297
|
-
.digest('hex');
|
|
298
|
-
return createHash('md5')
|
|
299
|
-
.update(inner)
|
|
300
|
-
.update(salt)
|
|
301
|
-
.digest('hex');
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Send password message
|
|
306
|
-
* @private
|
|
307
|
-
*/
|
|
308
|
-
_sendPassword(password) {
|
|
309
|
-
const buf = Buffer.alloc(1 + 4 + password.length + 1);
|
|
310
|
-
buf[0] = FE.PasswordMessage;
|
|
311
|
-
buf.writeUInt32BE(4 + password.length + 1, 1);
|
|
312
|
-
buf.write(password + '\0', 5);
|
|
313
|
-
this.socket.write(buf);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Handle ReadyForQuery message
|
|
318
|
-
* @private
|
|
319
|
-
*/
|
|
320
|
-
_handleReadyForQuery(payload) {
|
|
321
|
-
const _txStatus = String.fromCharCode(payload[0]); // 'I', 'T', or 'E'
|
|
322
|
-
|
|
323
|
-
if (this.state === 'authenticated' || this.state === 'startup') {
|
|
324
|
-
this.state = 'ready';
|
|
325
|
-
this._resolveConnect();
|
|
326
|
-
} else if (this.state === 'query') {
|
|
327
|
-
this.state = 'ready';
|
|
328
|
-
this._resolveQuery({
|
|
329
|
-
rows: this._rows,
|
|
330
|
-
columns: this._columns,
|
|
331
|
-
command: this._commandTag
|
|
332
|
-
});
|
|
333
|
-
} else if (this.state === 'copyTo') {
|
|
334
|
-
this.state = 'ready';
|
|
335
|
-
this._resolveCopy();
|
|
336
|
-
} else if (this.state === 'copyFrom') {
|
|
337
|
-
this.state = 'ready';
|
|
338
|
-
this._resolveCopy();
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Handle RowDescription message
|
|
344
|
-
* @private
|
|
345
|
-
*/
|
|
346
|
-
_handleRowDescription(payload) {
|
|
347
|
-
const fieldCount = payload.readUInt16BE(0);
|
|
348
|
-
const columns = [];
|
|
349
|
-
let offset = 2;
|
|
350
|
-
|
|
351
|
-
for (let i = 0; i < fieldCount; i++) {
|
|
352
|
-
const nameEnd = payload.indexOf(0, offset);
|
|
353
|
-
const name = payload.toString('utf8', offset, nameEnd);
|
|
354
|
-
offset = nameEnd + 1;
|
|
355
|
-
|
|
356
|
-
const _tableOid = payload.readUInt32BE(offset);
|
|
357
|
-
offset += 4;
|
|
358
|
-
const _columnId = payload.readUInt16BE(offset);
|
|
359
|
-
offset += 2;
|
|
360
|
-
const typeOid = payload.readUInt32BE(offset);
|
|
361
|
-
offset += 4;
|
|
362
|
-
const _typeLen = payload.readInt16BE(offset);
|
|
363
|
-
offset += 2;
|
|
364
|
-
const _typeMod = payload.readInt32BE(offset);
|
|
365
|
-
offset += 4;
|
|
366
|
-
const format = payload.readUInt16BE(offset);
|
|
367
|
-
offset += 2;
|
|
368
|
-
|
|
369
|
-
columns.push({ name, typeOid, format });
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
this._columns = columns;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Handle DataRow message
|
|
377
|
-
* @private
|
|
378
|
-
*/
|
|
379
|
-
_handleDataRow(payload) {
|
|
380
|
-
const columnCount = payload.readUInt16BE(0);
|
|
381
|
-
const row = {};
|
|
382
|
-
let offset = 2;
|
|
383
|
-
|
|
384
|
-
for (let i = 0; i < columnCount; i++) {
|
|
385
|
-
const valueLen = payload.readInt32BE(offset);
|
|
386
|
-
offset += 4;
|
|
387
|
-
|
|
388
|
-
if (valueLen === -1) {
|
|
389
|
-
row[this._columns[i].name] = null;
|
|
390
|
-
} else {
|
|
391
|
-
const value = payload.toString('utf8', offset, offset + valueLen);
|
|
392
|
-
offset += valueLen;
|
|
393
|
-
row[this._columns[i].name] = this._parseValue(value, this._columns[i].typeOid);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
this._rows.push(row);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Parse value based on PostgreSQL type OID
|
|
402
|
-
* @private
|
|
403
|
-
*/
|
|
404
|
-
_parseValue(value, typeOid) {
|
|
405
|
-
// Basic type conversion
|
|
406
|
-
switch (typeOid) {
|
|
407
|
-
case 23: // int4
|
|
408
|
-
case 21: // int2
|
|
409
|
-
case 20: // int8
|
|
410
|
-
return parseInt(value, 10);
|
|
411
|
-
case 700: // float4
|
|
412
|
-
case 701: // float8
|
|
413
|
-
case 1700: // numeric
|
|
414
|
-
return parseFloat(value);
|
|
415
|
-
case 16: // bool
|
|
416
|
-
return value === 't' || value === 'true';
|
|
417
|
-
case 1114: // timestamp
|
|
418
|
-
case 1184: // timestamptz
|
|
419
|
-
return new Date(value);
|
|
420
|
-
default:
|
|
421
|
-
return value;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Handle error response
|
|
427
|
-
* @private
|
|
428
|
-
*/
|
|
429
|
-
_handleError(payload) {
|
|
430
|
-
const error = this._parseErrorFields(payload);
|
|
431
|
-
const err = new Error(error.message || 'PostgreSQL error');
|
|
432
|
-
err.code = error.code;
|
|
433
|
-
err.detail = error.detail;
|
|
434
|
-
err.severity = error.severity;
|
|
435
|
-
|
|
436
|
-
if (this.state === 'startup' || this.state === 'authenticated') {
|
|
437
|
-
this._rejectConnect(err);
|
|
438
|
-
} else if (this.state === 'query') {
|
|
439
|
-
this._rejectQuery(err);
|
|
440
|
-
} else if (this.state === 'copyTo' || this.state === 'copyFrom') {
|
|
441
|
-
this._rejectCopy(err);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Parse error response fields
|
|
447
|
-
* @private
|
|
448
|
-
*/
|
|
449
|
-
_parseErrorFields(payload) {
|
|
450
|
-
const fields = {};
|
|
451
|
-
let offset = 0;
|
|
452
|
-
|
|
453
|
-
while (offset < payload.length) {
|
|
454
|
-
const fieldType = String.fromCharCode(payload[offset]);
|
|
455
|
-
if (fieldType === '\0') break;
|
|
456
|
-
offset++;
|
|
457
|
-
|
|
458
|
-
const valueEnd = payload.indexOf(0, offset);
|
|
459
|
-
const value = payload.toString('utf8', offset, valueEnd);
|
|
460
|
-
offset = valueEnd + 1;
|
|
461
|
-
|
|
462
|
-
switch (fieldType) {
|
|
463
|
-
case 'S': fields.severity = value; break;
|
|
464
|
-
case 'C': fields.code = value; break;
|
|
465
|
-
case 'M': fields.message = value; break;
|
|
466
|
-
case 'D': fields.detail = value; break;
|
|
467
|
-
case 'H': fields.hint = value; break;
|
|
468
|
-
case 'P': fields.position = value; break;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return fields;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Handle CopyOutResponse (COPY TO started)
|
|
477
|
-
* @private
|
|
478
|
-
*/
|
|
479
|
-
_handleCopyOutResponse(_payload) {
|
|
480
|
-
this.state = 'copyTo';
|
|
481
|
-
this._copyChunks = [];
|
|
482
|
-
// Format and column info available in _payload but not needed for binary copy
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Handle CopyInResponse (COPY FROM ready)
|
|
487
|
-
* @private
|
|
488
|
-
*/
|
|
489
|
-
_handleCopyInResponse(_payload) {
|
|
490
|
-
this.state = 'copyFrom';
|
|
491
|
-
// Server is ready to receive COPY data
|
|
492
|
-
if (this._copyCallback) {
|
|
493
|
-
this._copyCallback();
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Handle CopyData message (for COPY TO)
|
|
499
|
-
* @private
|
|
500
|
-
*/
|
|
501
|
-
_handleCopyData(payload) {
|
|
502
|
-
if (this.state === 'copyTo') {
|
|
503
|
-
this._copyChunks.push(Buffer.from(payload));
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Handle connection close
|
|
509
|
-
* @private
|
|
510
|
-
*/
|
|
511
|
-
_onClose() {
|
|
512
|
-
this.state = 'disconnected';
|
|
513
|
-
this.socket = null;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Handle socket error
|
|
518
|
-
* @private
|
|
519
|
-
*/
|
|
520
|
-
_onError(err) {
|
|
521
|
-
if (this._connectReject) {
|
|
522
|
-
this._connectReject(err);
|
|
523
|
-
} else if (this._queryReject) {
|
|
524
|
-
this._queryReject(err);
|
|
525
|
-
} else if (this._copyReject) {
|
|
526
|
-
this._copyReject(err);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Promise resolution helpers
|
|
531
|
-
_resolveConnect() {
|
|
532
|
-
if (this._connectResolve) {
|
|
533
|
-
this._connectResolve();
|
|
534
|
-
this._connectResolve = null;
|
|
535
|
-
this._connectReject = null;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
_rejectConnect(err) {
|
|
540
|
-
if (this._connectReject) {
|
|
541
|
-
this._connectReject(err);
|
|
542
|
-
this._connectResolve = null;
|
|
543
|
-
this._connectReject = null;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
_resolveQuery(result) {
|
|
548
|
-
if (this._queryResolve) {
|
|
549
|
-
this._queryResolve(result);
|
|
550
|
-
this._queryResolve = null;
|
|
551
|
-
this._queryReject = null;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
_rejectQuery(err) {
|
|
556
|
-
if (this._queryReject) {
|
|
557
|
-
this._queryReject(err);
|
|
558
|
-
this._queryResolve = null;
|
|
559
|
-
this._queryReject = null;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
_resolveCopy() {
|
|
564
|
-
if (this._copyResolve) {
|
|
565
|
-
this._copyResolve();
|
|
566
|
-
this._copyResolve = null;
|
|
567
|
-
this._copyReject = null;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
_rejectCopy(err) {
|
|
572
|
-
if (this._copyReject) {
|
|
573
|
-
this._copyReject(err);
|
|
574
|
-
this._copyResolve = null;
|
|
575
|
-
this._copyReject = null;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Execute a simple query
|
|
581
|
-
* @param {string} sql - SQL query
|
|
582
|
-
* @returns {Promise<{rows: Object[], columns: Object[], command: string}>}
|
|
583
|
-
*/
|
|
584
|
-
async query(sql) {
|
|
585
|
-
if (this.state !== 'ready') {
|
|
586
|
-
throw new Error(`Cannot query: client is ${this.state}`);
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
return new Promise((resolve, reject) => {
|
|
590
|
-
this._queryResolve = resolve;
|
|
591
|
-
this._queryReject = reject;
|
|
592
|
-
this._columns = [];
|
|
593
|
-
this._rows = [];
|
|
594
|
-
this._commandTag = '';
|
|
595
|
-
this.state = 'query';
|
|
596
|
-
|
|
597
|
-
// Send Query message
|
|
598
|
-
const buf = Buffer.alloc(1 + 4 + Buffer.byteLength(sql, 'utf8') + 1);
|
|
599
|
-
buf[0] = FE.Query;
|
|
600
|
-
buf.writeUInt32BE(4 + Buffer.byteLength(sql, 'utf8') + 1, 1);
|
|
601
|
-
buf.write(sql + '\0', 5);
|
|
602
|
-
this.socket.write(buf);
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* Execute COPY TO STDOUT - returns async iterator of binary chunks
|
|
608
|
-
* @param {string} sql - COPY TO query
|
|
609
|
-
* @returns {AsyncGenerator<Buffer>}
|
|
610
|
-
*/
|
|
611
|
-
async *copyTo(sql) {
|
|
612
|
-
if (this.state !== 'ready') {
|
|
613
|
-
throw new Error(`Cannot copyTo: client is ${this.state}`);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// Reset copy state
|
|
617
|
-
this._copyChunks = [];
|
|
618
|
-
this.state = 'copyTo';
|
|
619
|
-
|
|
620
|
-
// Send Query message
|
|
621
|
-
const buf = Buffer.alloc(1 + 4 + Buffer.byteLength(sql, 'utf8') + 1);
|
|
622
|
-
buf[0] = FE.Query;
|
|
623
|
-
buf.writeUInt32BE(4 + Buffer.byteLength(sql, 'utf8') + 1, 1);
|
|
624
|
-
buf.write(sql + '\0', 5);
|
|
625
|
-
this.socket.write(buf);
|
|
626
|
-
|
|
627
|
-
// Yield chunks as they arrive
|
|
628
|
-
while (this.state === 'copyTo') {
|
|
629
|
-
// Wait for data or state change
|
|
630
|
-
await this._waitForData();
|
|
631
|
-
|
|
632
|
-
// Yield accumulated chunks
|
|
633
|
-
while (this._copyChunks.length > 0) {
|
|
634
|
-
yield this._copyChunks.shift();
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Wait for more data or state change
|
|
641
|
-
* @private
|
|
642
|
-
*/
|
|
643
|
-
_waitForData() {
|
|
644
|
-
return new Promise(resolve => {
|
|
645
|
-
if (this._copyChunks.length > 0 || this.state !== 'copyTo') {
|
|
646
|
-
resolve();
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Poll for changes (simple approach)
|
|
651
|
-
const check = () => {
|
|
652
|
-
if (this._copyChunks.length > 0 || this.state !== 'copyTo') {
|
|
653
|
-
resolve();
|
|
654
|
-
} else {
|
|
655
|
-
setTimeout(check, 1);
|
|
656
|
-
}
|
|
657
|
-
};
|
|
658
|
-
setTimeout(check, 1);
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Execute COPY FROM STDIN - accepts async iterator of binary chunks
|
|
664
|
-
* @param {string} sql - COPY FROM query
|
|
665
|
-
* @param {AsyncIterable<Buffer>} dataIterator - Iterator yielding data chunks
|
|
666
|
-
* @returns {Promise<void>}
|
|
667
|
-
*/
|
|
668
|
-
async copyFrom(sql, dataIterator) {
|
|
669
|
-
if (this.state !== 'ready') {
|
|
670
|
-
throw new Error(`Cannot copyFrom: client is ${this.state}`);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
return new Promise(async (resolve, reject) => {
|
|
674
|
-
this._copyResolve = resolve;
|
|
675
|
-
this._copyReject = reject;
|
|
676
|
-
|
|
677
|
-
// Wait for CopyInResponse
|
|
678
|
-
this._copyCallback = async () => {
|
|
679
|
-
try {
|
|
680
|
-
// Send CopyData messages
|
|
681
|
-
for await (const chunk of dataIterator) {
|
|
682
|
-
const msg = Buffer.alloc(1 + 4 + chunk.length);
|
|
683
|
-
msg[0] = FE.CopyData;
|
|
684
|
-
msg.writeUInt32BE(4 + chunk.length, 1);
|
|
685
|
-
chunk.copy(msg, 5);
|
|
686
|
-
this.socket.write(msg);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// Send CopyDone
|
|
690
|
-
const done = Buffer.alloc(5);
|
|
691
|
-
done[0] = FE.CopyDone;
|
|
692
|
-
done.writeUInt32BE(4, 1);
|
|
693
|
-
this.socket.write(done);
|
|
694
|
-
} catch (err) {
|
|
695
|
-
// Send CopyFail
|
|
696
|
-
const errMsg = err.message || 'Copy failed';
|
|
697
|
-
const fail = Buffer.alloc(1 + 4 + Buffer.byteLength(errMsg, 'utf8') + 1);
|
|
698
|
-
fail[0] = FE.CopyFail;
|
|
699
|
-
fail.writeUInt32BE(4 + Buffer.byteLength(errMsg, 'utf8') + 1, 1);
|
|
700
|
-
fail.write(errMsg + '\0', 5);
|
|
701
|
-
this.socket.write(fail);
|
|
702
|
-
reject(err);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
this._copyCallback = null;
|
|
706
|
-
};
|
|
707
|
-
|
|
708
|
-
this.state = 'copyFrom';
|
|
709
|
-
|
|
710
|
-
// Send Query message
|
|
711
|
-
const buf = Buffer.alloc(1 + 4 + Buffer.byteLength(sql, 'utf8') + 1);
|
|
712
|
-
buf[0] = FE.Query;
|
|
713
|
-
buf.writeUInt32BE(4 + Buffer.byteLength(sql, 'utf8') + 1, 1);
|
|
714
|
-
buf.write(sql + '\0', 5);
|
|
715
|
-
this.socket.write(buf);
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* Close the connection gracefully
|
|
721
|
-
*/
|
|
722
|
-
close() {
|
|
723
|
-
if (this.socket) {
|
|
724
|
-
// Send Terminate message
|
|
725
|
-
const terminate = Buffer.alloc(5);
|
|
726
|
-
terminate[0] = FE.Terminate;
|
|
727
|
-
terminate.writeUInt32BE(4, 1);
|
|
728
|
-
this.socket.write(terminate);
|
|
729
|
-
this.socket.end();
|
|
730
|
-
this.socket = null;
|
|
731
|
-
}
|
|
732
|
-
this.state = 'disconnected';
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Check if client is connected and ready
|
|
737
|
-
*/
|
|
738
|
-
get isReady() {
|
|
739
|
-
return this.state === 'ready';
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
/**
|
|
744
|
-
* PostgreSQL Connection Pool
|
|
745
|
-
*
|
|
746
|
-
* Manages a pool of PgWireClient connections.
|
|
747
|
-
*
|
|
748
|
-
* @example
|
|
749
|
-
* const pool = new PgWirePool({ hostname: '127.0.0.1', port: 5432, max: 8 });
|
|
750
|
-
* const client = await pool.connect();
|
|
751
|
-
* const result = await client.query('SELECT 1');
|
|
752
|
-
* pool.release(client);
|
|
753
|
-
* await pool.end();
|
|
754
|
-
*/
|
|
755
|
-
export class PgWirePool {
|
|
756
|
-
constructor(options = {}) {
|
|
757
|
-
this.options = options;
|
|
758
|
-
this.max = options.max || 10;
|
|
759
|
-
this.available = [];
|
|
760
|
-
this.inUse = new Set();
|
|
761
|
-
this.waiters = [];
|
|
762
|
-
this.closed = false;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
/**
|
|
766
|
-
* Acquire a connection from the pool
|
|
767
|
-
* @returns {Promise<PgWireClient>}
|
|
768
|
-
*/
|
|
769
|
-
async connect() {
|
|
770
|
-
if (this.closed) {
|
|
771
|
-
throw new Error('Pool is closed');
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// Return available connection
|
|
775
|
-
if (this.available.length > 0) {
|
|
776
|
-
const client = this.available.pop();
|
|
777
|
-
this.inUse.add(client);
|
|
778
|
-
return client;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Create new connection if under limit
|
|
782
|
-
if (this.inUse.size < this.max) {
|
|
783
|
-
const client = new PgWireClient(this.options);
|
|
784
|
-
await client.connect();
|
|
785
|
-
this.inUse.add(client);
|
|
786
|
-
return client;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Wait for available connection
|
|
790
|
-
return new Promise((resolve, reject) => {
|
|
791
|
-
this.waiters.push({ resolve, reject });
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Release a connection back to the pool
|
|
797
|
-
* @param {PgWireClient} client
|
|
798
|
-
*/
|
|
799
|
-
release(client) {
|
|
800
|
-
if (!this.inUse.has(client)) {
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
this.inUse.delete(client);
|
|
805
|
-
|
|
806
|
-
// Check for waiting requests
|
|
807
|
-
if (this.waiters.length > 0 && client.isReady) {
|
|
808
|
-
const waiter = this.waiters.shift();
|
|
809
|
-
this.inUse.add(client);
|
|
810
|
-
waiter.resolve(client);
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Return to pool if still usable
|
|
815
|
-
if (client.isReady && !this.closed) {
|
|
816
|
-
this.available.push(client);
|
|
817
|
-
} else {
|
|
818
|
-
client.close();
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Execute a query using a pooled connection
|
|
824
|
-
* @param {string} sql - SQL query
|
|
825
|
-
* @returns {Promise<{rows: Object[], columns: Object[], command: string}>}
|
|
826
|
-
*/
|
|
827
|
-
async query(sql) {
|
|
828
|
-
const client = await this.connect();
|
|
829
|
-
try {
|
|
830
|
-
return await client.query(sql);
|
|
831
|
-
} finally {
|
|
832
|
-
this.release(client);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
/**
|
|
837
|
-
* Close all connections and the pool
|
|
838
|
-
*/
|
|
839
|
-
async end() {
|
|
840
|
-
this.closed = true;
|
|
841
|
-
|
|
842
|
-
// Reject waiting requests
|
|
843
|
-
for (const waiter of this.waiters) {
|
|
844
|
-
waiter.reject(new Error('Pool is closing'));
|
|
845
|
-
}
|
|
846
|
-
this.waiters = [];
|
|
847
|
-
|
|
848
|
-
// Close all connections
|
|
849
|
-
for (const client of [...this.available, ...this.inUse]) {
|
|
850
|
-
client.close();
|
|
851
|
-
}
|
|
852
|
-
this.available = [];
|
|
853
|
-
this.inUse.clear();
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Get pool statistics
|
|
858
|
-
*/
|
|
859
|
-
get stats() {
|
|
860
|
-
return {
|
|
861
|
-
available: this.available.length,
|
|
862
|
-
inUse: this.inUse.size,
|
|
863
|
-
waiting: this.waiters.length,
|
|
864
|
-
max: this.max
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
export { FE, BE, AUTH };
|