postgrejs 3.1.3 → 3.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/CHANGELOG.md +79 -1
- package/README.md +79 -13
- package/connection/connection.d.ts +97 -0
- package/connection/connection.js +141 -2
- package/connection/cursor.d.ts +2 -1
- package/connection/cursor.js +6 -6
- package/connection/intl-connection.d.ts +113 -3
- package/connection/intl-connection.js +530 -45
- package/connection/prepared-statement.d.ts +64 -1
- package/connection/prepared-statement.js +97 -6
- package/data-types/bool-type.js +0 -1
- package/data-types/box-type.js +0 -1
- package/data-types/bytea-type.js +5 -3
- package/data-types/char-type.js +2 -2
- package/data-types/circle-type.js +0 -1
- package/data-types/date-type.js +1 -2
- package/data-types/float4-type.js +4 -5
- package/data-types/float8-type.js +4 -6
- package/data-types/int2-type.js +2 -2
- package/data-types/int2-vector-type.js +3 -2
- package/data-types/int4-type.js +2 -2
- package/data-types/int8-type.js +2 -2
- package/data-types/json-type.js +2 -2
- package/data-types/jsonb-type.js +6 -5
- package/data-types/lseg-type.js +0 -1
- package/data-types/numeric-type.js +9 -4
- package/data-types/oid-type.js +2 -2
- package/data-types/oid-vector-type.js +3 -2
- package/data-types/point-type.js +0 -1
- package/data-types/time-type.js +1 -2
- package/data-types/timestamp-type.js +13 -3
- package/data-types/timestamptz-type.js +27 -10
- package/data-types/uuid-type.js +1 -2
- package/data-types/varchar-type.js +2 -2
- package/index.d.ts +4 -0
- package/index.js +4 -0
- package/interfaces/batch-result.d.ts +42 -0
- package/interfaces/batch-result.js +1 -0
- package/interfaces/command-result.d.ts +3 -2
- package/interfaces/copy-from-rows-options.d.ts +58 -0
- package/interfaces/copy-from-rows-options.js +1 -0
- package/interfaces/data-type.d.ts +0 -1
- package/interfaces/database-connection-params.d.ts +25 -0
- package/interfaces/pipeline-request.d.ts +16 -0
- package/interfaces/pipeline-request.js +1 -0
- package/interfaces/query-options.d.ts +18 -0
- package/interfaces/script-execute-options.d.ts +12 -0
- package/package.json +18 -3
- package/protocol/backend.js +6 -6
- package/protocol/buffer-reader.d.ts +7 -12
- package/protocol/buffer-reader.js +11 -58
- package/protocol/database-error.d.ts +16 -0
- package/protocol/database-error.js +15 -0
- package/protocol/frontend.js +86 -96
- package/protocol/pg-socket.d.ts +82 -33
- package/protocol/pg-socket.js +143 -86
- package/protocol/pgoutput.js +2 -2
- package/protocol/smart-buffer.d.ts +11 -29
- package/protocol/smart-buffer.js +19 -162
- package/types.d.ts +13 -1
- package/util/assert-integer.d.ts +43 -0
- package/util/assert-integer.js +58 -0
- package/util/bigint-methods.d.ts +0 -1
- package/util/bigint-methods.js +0 -22
- package/util/copy-from-rows.d.ts +56 -0
- package/util/copy-from-rows.js +95 -0
- package/util/decode-binaryarray.d.ts +1 -1
- package/util/decode-binaryarray.js +7 -7
- package/util/encode-binaryarray.js +6 -6
- package/util/encode-copy-binary.d.ts +48 -0
- package/util/encode-copy-binary.js +112 -0
- package/util/fast-parsefloat.d.ts +15 -0
- package/util/fast-parsefloat.js +141 -0
- package/util/get-parsers.js +4 -8
- package/util/parse-datetime.js +7 -0
- package/util/parse-pg-timestamp.d.ts +31 -0
- package/util/parse-pg-timestamp.js +199 -0
- package/util/parse-row.js +120 -0
- package/util/row-decoder.d.ts +49 -0
- package/util/row-decoder.js +63 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
2
|
import { ConnectionState, DEFAULT_COLUMN_FORMAT } from '../constants.js';
|
|
3
3
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
4
|
+
import { DatabaseError } from '../protocol/database-error.js';
|
|
4
5
|
import { PgSocket } from '../protocol/pg-socket.js';
|
|
5
6
|
import { Protocol } from '../protocol/protocol.js';
|
|
6
7
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
7
8
|
import { getConnectionConfig } from '../util/connection-config.js';
|
|
9
|
+
import { buildCopySql, buildProbeSql, writeCopyBinaryRows, } from '../util/copy-from-rows.js';
|
|
8
10
|
import { escapeLiteral } from '../util/escape-literal.js';
|
|
9
11
|
import { getParsers } from '../util/get-parsers.js';
|
|
10
|
-
import {
|
|
12
|
+
import { resolveRowDecoder, resolveRowType } from '../util/row-decoder.js';
|
|
11
13
|
import { wrapRowDescription } from '../util/wrap-row-description.js';
|
|
12
14
|
import { CopyFromStream, CopyToStream } from './copy-stream.js';
|
|
13
15
|
const DataFormat = Protocol.DataFormat;
|
|
@@ -25,7 +27,36 @@ function columnFormatsEqual(a, b) {
|
|
|
25
27
|
return false;
|
|
26
28
|
return true;
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Uses of the same SQL before it is worth preparing.
|
|
32
|
+
*
|
|
33
|
+
* Preparing on first sight, as postgres.js does, pays Parse plus Describe
|
|
34
|
+
* and leaves a named statement behind for SQL that may never run again;
|
|
35
|
+
* waiting for the second use keeps a genuinely one-shot query at exactly
|
|
36
|
+
* today's cost. The repeated query pays one extra unprepared call before
|
|
37
|
+
* it starts saving, which it makes back immediately.
|
|
38
|
+
*/
|
|
39
|
+
const PREPARE_AFTER_USES = 2;
|
|
40
|
+
/**
|
|
41
|
+
* SQL that manages transaction boundaries itself, and so must never be
|
|
42
|
+
* wrapped in a savepoint or an implicit BEGIN/COMMIT of ours.
|
|
43
|
+
*/
|
|
44
|
+
const TRANSACTION_COMMAND_PATTERN = /^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i;
|
|
28
45
|
export class IntlConnection extends SafeEventEmitter {
|
|
46
|
+
/**
|
|
47
|
+
* Server-side prepared statements this connection has built, keyed by
|
|
48
|
+
* the SQL text and its parameter OIDs - both, because the OIDs are part
|
|
49
|
+
* of what was parsed, not just how it is bound.
|
|
50
|
+
*
|
|
51
|
+
* A Map, relied on for its insertion order: re-inserting on every hit
|
|
52
|
+
* makes the first entry the least recently used, which is what gets
|
|
53
|
+
* closed when the cache is full. Statements belong to the session, so
|
|
54
|
+
* this is per connection and dies with it.
|
|
55
|
+
*/
|
|
56
|
+
_preparedCache = new Map();
|
|
57
|
+
/** SQL seen once but not yet prepared - see PREPARE_AFTER_USES. */
|
|
58
|
+
_preparedCandidates = new Map();
|
|
59
|
+
_preparedCounter = 0;
|
|
29
60
|
_executeReusedParserCache = new WeakMap();
|
|
30
61
|
_refCount = 0;
|
|
31
62
|
_transactionDepth = 0;
|
|
@@ -116,7 +147,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
116
147
|
}
|
|
117
148
|
async execute(sql, options = {}, cb) {
|
|
118
149
|
this.assertConnected();
|
|
119
|
-
const transactionCommand =
|
|
150
|
+
const transactionCommand = TRANSACTION_COMMAND_PATTERN.test(sql);
|
|
120
151
|
let beginFirst = false;
|
|
121
152
|
let commitLast = false;
|
|
122
153
|
const { autoCommit } = options;
|
|
@@ -368,6 +399,69 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
368
399
|
* Runs a COPY ... FROM STDIN and hands back a stream to feed it.
|
|
369
400
|
* Resolves once the server is ready for data.
|
|
370
401
|
*/
|
|
402
|
+
/**
|
|
403
|
+
* Streams rows into a table with `COPY ... FROM STDIN (FORMAT binary)`,
|
|
404
|
+
* encoding each value with its own type's binary encoder.
|
|
405
|
+
*
|
|
406
|
+
* Binary COPY performs no conversion server-side, so the destination
|
|
407
|
+
* column types have to be known exactly. They are read from the server
|
|
408
|
+
* unless the caller supplies them: a Describe of `select <columns> from
|
|
409
|
+
* <table> where false` against the unnamed statement returns a
|
|
410
|
+
* RowDescription carrying each column's OID, which costs one round trip
|
|
411
|
+
* and leaves nothing to close (the next Parse replaces the unnamed
|
|
412
|
+
* statement). Letting PostgreSQL resolve the name is the reason to do it
|
|
413
|
+
* this way rather than reading the catalog here - schemas, quoting,
|
|
414
|
+
* search_path and views then behave exactly as they will for the COPY.
|
|
415
|
+
*/
|
|
416
|
+
async copyFromRows(table, source, options = {}) {
|
|
417
|
+
this.assertConnected();
|
|
418
|
+
let columns = options.columns;
|
|
419
|
+
let dataTypeIds = options.columnTypes;
|
|
420
|
+
if (!dataTypeIds || !columns) {
|
|
421
|
+
const { fields } = await this.prepareOnce(buildProbeSql(table, columns), undefined, '');
|
|
422
|
+
if (!fields?.length)
|
|
423
|
+
throw new Error(`Cannot determine columns of "${table}" - it returned no column descriptions`);
|
|
424
|
+
columns = columns || fields.map(f => f.fieldName);
|
|
425
|
+
dataTypeIds = dataTypeIds || fields.map(f => f.dataTypeId);
|
|
426
|
+
}
|
|
427
|
+
if (columns.length !== dataTypeIds.length)
|
|
428
|
+
throw new Error(`columnTypes has ${dataTypeIds.length} entries but ${columns.length} columns were given`);
|
|
429
|
+
const stream = await this.copyFrom(buildCopySql(table, columns));
|
|
430
|
+
// The CopyFail sent below comes back as an ErrorResponse, which the
|
|
431
|
+
// stream emits as 'error'. Node turns an 'error' with no listener into
|
|
432
|
+
// an uncaught exception and takes the process down - so a value this
|
|
433
|
+
// method already rejected cleanly would kill the caller instead.
|
|
434
|
+
// Recorded rather than ignored, since the same listener covers a
|
|
435
|
+
// socket that dies mid-copy.
|
|
436
|
+
let streamError;
|
|
437
|
+
stream.on('error', e => {
|
|
438
|
+
streamError = streamError || e;
|
|
439
|
+
});
|
|
440
|
+
try {
|
|
441
|
+
const result = await writeCopyBinaryRows(source, {
|
|
442
|
+
columns,
|
|
443
|
+
dataTypeIds,
|
|
444
|
+
options,
|
|
445
|
+
write: chunk => new Promise((resolve, reject) => {
|
|
446
|
+
const accepted = this.socket.sendCopyData(chunk, err => err ? reject(err) : resolve());
|
|
447
|
+
// Taken without buffering - no reason to wait for the write
|
|
448
|
+
// callback, which would add a turn of latency per chunk.
|
|
449
|
+
if (accepted)
|
|
450
|
+
resolve();
|
|
451
|
+
}),
|
|
452
|
+
});
|
|
453
|
+
await new Promise(resolve => stream.end(resolve));
|
|
454
|
+
if (streamError)
|
|
455
|
+
throw streamError;
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
catch (e) {
|
|
459
|
+
// Tell the server to abandon the copy rather than leaving it waiting
|
|
460
|
+
// for a stream that will never arrive.
|
|
461
|
+
this.socket.sendCopyFail(e instanceof Error ? e.message : 'copyFromRows() failed');
|
|
462
|
+
throw e;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
371
465
|
async copyFrom(sql) {
|
|
372
466
|
this.assertConnected();
|
|
373
467
|
this.ref();
|
|
@@ -398,6 +492,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
398
492
|
let fields;
|
|
399
493
|
let error;
|
|
400
494
|
const typeMap = options.typeMap || GlobalTypeMap;
|
|
495
|
+
const rowDecoder = resolveRowDecoder(options);
|
|
401
496
|
this.runningQueryCount++;
|
|
402
497
|
return await this.socket.sendQueryMessage(sql, (code, msg, done) => {
|
|
403
498
|
switch (code) {
|
|
@@ -426,9 +521,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
426
521
|
break;
|
|
427
522
|
case Protocol.BackendMessageCode.DataRow:
|
|
428
523
|
{
|
|
429
|
-
const row =
|
|
430
|
-
? parseObjectRow(parsers, msg.data, msg.columnCount, options, current.fields)
|
|
431
|
-
: parseRow(parsers, msg.data, msg.columnCount, options);
|
|
524
|
+
const row = rowDecoder.decode(parsers, msg.data, msg.columnCount, options, current.fields);
|
|
432
525
|
if (cb)
|
|
433
526
|
cb('row', row);
|
|
434
527
|
current.rows = current.rows || [];
|
|
@@ -446,8 +539,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
446
539
|
if (timingEnabled)
|
|
447
540
|
current.executeTime = performance.now() - currentStart;
|
|
448
541
|
if (current.rows)
|
|
449
|
-
current.rowType =
|
|
450
|
-
options.objectRows && current.fields ? 'object' : 'array';
|
|
542
|
+
current.rowType = resolveRowType(options);
|
|
451
543
|
result.results.push(current);
|
|
452
544
|
if (cb)
|
|
453
545
|
cb('command-complete', current);
|
|
@@ -484,7 +576,137 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
484
576
|
* +close()'s 7 round trips whenever there's no cursor and no explicit/
|
|
485
577
|
* active transaction to wrap (see Connection.query() for that gating).
|
|
486
578
|
*/
|
|
487
|
-
|
|
579
|
+
/**
|
|
580
|
+
* Runs a one-shot extended query, reusing a server-side prepared
|
|
581
|
+
* statement when this connection has run the same SQL before.
|
|
582
|
+
*
|
|
583
|
+
* A repeated query then costs Bind/Execute instead of
|
|
584
|
+
* Parse/Bind/Describe/Execute, which the server can answer without
|
|
585
|
+
* planning again - fifty concurrent calls of one statement measured
|
|
586
|
+
* 0.97ms against 3.13ms, and socket reads dropped from 38 to 8.
|
|
587
|
+
*
|
|
588
|
+
* Nothing is prepared on first sight: SQL has to be seen
|
|
589
|
+
* PREPARE_AFTER_USES times before it earns a name, so a query that runs
|
|
590
|
+
* once costs exactly what it costs today.
|
|
591
|
+
*
|
|
592
|
+
* A cached statement can be invalidated under us - `alter table` between
|
|
593
|
+
* two calls makes PostgreSQL answer 0A000, "cached plan must not change
|
|
594
|
+
* result type". That is caught here: the entry is dropped and the query
|
|
595
|
+
* runs again unprepared, so a migration against a live connection
|
|
596
|
+
* recovers instead of failing every call from then on.
|
|
597
|
+
*/
|
|
598
|
+
async queryCached(sql, paramTypes, params, options) {
|
|
599
|
+
const savepoint = this._inlineSavepointFor(sql, options);
|
|
600
|
+
try {
|
|
601
|
+
return await this._queryCached(sql, paramTypes, params, options, savepoint);
|
|
602
|
+
}
|
|
603
|
+
catch (e) {
|
|
604
|
+
// PostgreSQL throws away everything between a failed statement and
|
|
605
|
+
// the Sync, so an inlined RELEASE never ran and the savepoint is
|
|
606
|
+
// still standing. Rolling back to it leaves the transaction usable,
|
|
607
|
+
// which is the whole point of rollbackOnError - and it is left
|
|
608
|
+
// standing afterwards, as the separate-round-trip wrapper left it.
|
|
609
|
+
if (savepoint)
|
|
610
|
+
await this._execute('ROLLBACK TO ' + savepoint);
|
|
611
|
+
throw e;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* The savepoint rollbackOnError calls for, or undefined when this call
|
|
616
|
+
* needs none: outside a transaction there is nothing to roll back to,
|
|
617
|
+
* and a statement that is itself a transaction command manages its own
|
|
618
|
+
* boundaries (wrapping COMMIT in a savepoint would be nonsense).
|
|
619
|
+
*/
|
|
620
|
+
_inlineSavepointFor(sql, options) {
|
|
621
|
+
if (!this.inTransaction || TRANSACTION_COMMAND_PATTERN.test(sql))
|
|
622
|
+
return undefined;
|
|
623
|
+
const on = options.rollbackOnError ?? this.config.rollbackOnError ?? true;
|
|
624
|
+
return on ? this._onErrorSavePoint : undefined;
|
|
625
|
+
}
|
|
626
|
+
async _queryCached(sql, paramTypes, params, options, savepoint) {
|
|
627
|
+
const enabled = options.prepare ?? this.config.prepare ?? true;
|
|
628
|
+
if (!enabled)
|
|
629
|
+
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
630
|
+
const key = paramTypes?.length
|
|
631
|
+
? sql + '\u0000' + paramTypes.join(',')
|
|
632
|
+
: sql;
|
|
633
|
+
const cached = this._preparedCache.get(key);
|
|
634
|
+
if (cached) {
|
|
635
|
+
// Re-insert to mark it most recently used.
|
|
636
|
+
this._preparedCache.delete(key);
|
|
637
|
+
this._preparedCache.set(key, cached);
|
|
638
|
+
try {
|
|
639
|
+
return await this.executeReused(cached.name, cached.fields, paramTypes, params, options, savepoint);
|
|
640
|
+
}
|
|
641
|
+
catch (e) {
|
|
642
|
+
if (e?.code !== '0A000')
|
|
643
|
+
throw e;
|
|
644
|
+
this._preparedCache.delete(key);
|
|
645
|
+
// ROLLBACK TO leaves the savepoint itself in place, so the retry
|
|
646
|
+
// must not open a second one under the same name - it runs bare
|
|
647
|
+
// and releases the one already held.
|
|
648
|
+
if (savepoint)
|
|
649
|
+
await this._execute('ROLLBACK TO ' + savepoint);
|
|
650
|
+
const result = await this.queryOnce(sql, paramTypes, params, options);
|
|
651
|
+
if (savepoint)
|
|
652
|
+
await this._execute('RELEASE ' + savepoint);
|
|
653
|
+
return result;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const uses = (this._preparedCandidates.get(key) || 0) + 1;
|
|
657
|
+
if (uses < PREPARE_AFTER_USES) {
|
|
658
|
+
this._preparedCandidates.set(key, uses);
|
|
659
|
+
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
660
|
+
}
|
|
661
|
+
this._preparedCandidates.delete(key);
|
|
662
|
+
const name = 'C_' + ++this._preparedCounter;
|
|
663
|
+
// A Parse that fails caches nothing and reports itself; the statement
|
|
664
|
+
// is simply never reused. It runs outside the savepoint, as the
|
|
665
|
+
// prepare() the transaction path used to do ahead of its own wrapper.
|
|
666
|
+
const { fields } = await this.prepareOnce(sql, paramTypes, name);
|
|
667
|
+
const entry = { name, fields };
|
|
668
|
+
await this._evictPreparedIfFull();
|
|
669
|
+
this._preparedCache.set(key, entry);
|
|
670
|
+
return this.executeReused(name, entry.fields, paramTypes, params, options, savepoint);
|
|
671
|
+
}
|
|
672
|
+
/** Closes the least recently used statement once the cache is full. */
|
|
673
|
+
async _evictPreparedIfFull() {
|
|
674
|
+
const max = this.config.preparedStatementCacheSize ?? 64;
|
|
675
|
+
while (this._preparedCache.size >= max) {
|
|
676
|
+
const oldest = this._preparedCache.keys().next();
|
|
677
|
+
if (oldest.done)
|
|
678
|
+
return;
|
|
679
|
+
const entry = this._preparedCache.get(oldest.value);
|
|
680
|
+
this._preparedCache.delete(oldest.value);
|
|
681
|
+
// A statement that cannot be closed is already gone as far as this
|
|
682
|
+
// connection is concerned; losing the cache entry is the part that
|
|
683
|
+
// matters, and failing the caller's query over it would be worse.
|
|
684
|
+
await this._closePreparedStatement(entry.name).catch(() => undefined);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Closes an evicted statement server-side. Close answers CloseComplete
|
|
689
|
+
* but no ReadyForQuery, so the Sync goes separately and both are awaited
|
|
690
|
+
* together - the same shape PreparedStatement._close() uses.
|
|
691
|
+
*/
|
|
692
|
+
async _closePreparedStatement(name) {
|
|
693
|
+
const closed = this.socket.sendCloseMessage({ type: 'S', name }, (code, msg, done) => {
|
|
694
|
+
if (code === Protocol.BackendMessageCode.CloseComplete)
|
|
695
|
+
done(undefined);
|
|
696
|
+
else if (code === Protocol.BackendMessageCode.ErrorResponse)
|
|
697
|
+
done(msg);
|
|
698
|
+
});
|
|
699
|
+
const synced = this.socket.sendSyncMessage((code, msg, done) => {
|
|
700
|
+
if (code === Protocol.BackendMessageCode.ReadyForQuery) {
|
|
701
|
+
this.transactionStatus = msg.status;
|
|
702
|
+
done(undefined);
|
|
703
|
+
}
|
|
704
|
+
else if (code === Protocol.BackendMessageCode.ErrorResponse)
|
|
705
|
+
done(msg);
|
|
706
|
+
});
|
|
707
|
+
await Promise.all([closed, synced]);
|
|
708
|
+
}
|
|
709
|
+
async queryOnce(sql, paramTypes, params, options, savepoint) {
|
|
488
710
|
this.assertConnected();
|
|
489
711
|
this.ref();
|
|
490
712
|
try {
|
|
@@ -497,6 +719,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
497
719
|
let resultFields;
|
|
498
720
|
let commandTag;
|
|
499
721
|
let error;
|
|
722
|
+
const rowDecoder = resolveRowDecoder(options);
|
|
723
|
+
// See executeReused() for what a savepoint riding along does to the
|
|
724
|
+
// CommandComplete stream.
|
|
725
|
+
let leadingCommandTags = savepoint ? 1 : 0;
|
|
500
726
|
this.runningQueryCount++;
|
|
501
727
|
await this.socket
|
|
502
728
|
.sendExtendedQueryMessages({
|
|
@@ -504,6 +730,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
504
730
|
bind: { typeMap, paramTypes, params, queryOptions: options },
|
|
505
731
|
describe: { type: 'P' },
|
|
506
732
|
execute: { fetchCount: options.fetchCount || 100 },
|
|
733
|
+
before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
|
|
734
|
+
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
507
735
|
}, (code, msg, done) => {
|
|
508
736
|
switch (code) {
|
|
509
737
|
case Protocol.BackendMessageCode.ParseComplete:
|
|
@@ -516,13 +744,16 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
516
744
|
parsers = getParsers(typeMap, msg.fields);
|
|
517
745
|
resultFields = wrapRowDescription(typeMap, msg.fields, options.columnFormat || DEFAULT_COLUMN_FORMAT);
|
|
518
746
|
result.fields = resultFields;
|
|
519
|
-
result.rowType = options
|
|
747
|
+
result.rowType = resolveRowType(options);
|
|
520
748
|
break;
|
|
521
749
|
case Protocol.BackendMessageCode.DataRow:
|
|
522
750
|
rows.push(msg);
|
|
523
751
|
break;
|
|
524
752
|
case Protocol.BackendMessageCode.CommandComplete:
|
|
525
|
-
|
|
753
|
+
if (leadingCommandTags)
|
|
754
|
+
leadingCommandTags--;
|
|
755
|
+
else if (!commandTag)
|
|
756
|
+
commandTag = msg;
|
|
526
757
|
break;
|
|
527
758
|
case Protocol.BackendMessageCode.ErrorResponse:
|
|
528
759
|
error = msg;
|
|
@@ -547,9 +778,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
547
778
|
const l = rows.length;
|
|
548
779
|
let i;
|
|
549
780
|
for (i = 0; i < l; i++) {
|
|
550
|
-
rows[i] = options
|
|
551
|
-
? parseObjectRow(parsers, rows[i].data, rows[i].columnCount, options, resultFields)
|
|
552
|
-
: parseRow(parsers, rows[i].data, rows[i].columnCount, options);
|
|
781
|
+
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
553
782
|
}
|
|
554
783
|
}
|
|
555
784
|
if (result.command === 'DELETE' ||
|
|
@@ -623,7 +852,272 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
623
852
|
* the RowDescription fields prepareOnce() already fetched instead of
|
|
624
853
|
* re-Describing a fresh portal on every call.
|
|
625
854
|
*/
|
|
626
|
-
|
|
855
|
+
/**
|
|
856
|
+
* Parsers and wrapped field descriptions for a statement whose
|
|
857
|
+
* RowDescription prepare() already fetched, memoised per RowDescription
|
|
858
|
+
* so repeated executes of the same statement don't rebuild them.
|
|
859
|
+
* Shared by executeReused() and executeBatchReused().
|
|
860
|
+
*/
|
|
861
|
+
_resolveReusedParsers(cachedFields, typeMap, options) {
|
|
862
|
+
const columnFormat = options.columnFormat != null
|
|
863
|
+
? options.columnFormat
|
|
864
|
+
: DEFAULT_COLUMN_FORMAT;
|
|
865
|
+
const cached = this._executeReusedParserCache.get(cachedFields);
|
|
866
|
+
if (cached &&
|
|
867
|
+
cached.typeMap === typeMap &&
|
|
868
|
+
columnFormatsEqual(cached.columnFormat, columnFormat)) {
|
|
869
|
+
return { parsers: cached.parsers, resultFields: cached.resultFields };
|
|
870
|
+
}
|
|
871
|
+
const fields = cachedFields.map((f, i) => ({
|
|
872
|
+
...f,
|
|
873
|
+
format: Array.isArray(columnFormat) ? columnFormat[i] : columnFormat,
|
|
874
|
+
}));
|
|
875
|
+
const parsers = getParsers(typeMap, fields);
|
|
876
|
+
const resultFields = wrapRowDescription(typeMap, fields, columnFormat);
|
|
877
|
+
this._executeReusedParserCache.set(cachedFields, {
|
|
878
|
+
typeMap,
|
|
879
|
+
columnFormat,
|
|
880
|
+
parsers,
|
|
881
|
+
resultFields,
|
|
882
|
+
});
|
|
883
|
+
return { parsers, resultFields };
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Executes one prepared statement once per parameter set, as a single
|
|
887
|
+
* Bind/Execute stream closed by one Sync (see
|
|
888
|
+
* PgSocket.sendBatchBindExecuteMessages() for the wire shape and what
|
|
889
|
+
* the single Sync implies).
|
|
890
|
+
*
|
|
891
|
+
* Sets map to results positionally: the server answers each Execute with
|
|
892
|
+
* its own CommandComplete, and rows that arrive before one belong to the
|
|
893
|
+
* set it closes. When the server rejects a set, everything behind it is
|
|
894
|
+
* discarded unexecuted - the completed prefix is attached to the thrown
|
|
895
|
+
* error as `batchResults`, and the rejected set's index as `failedIndex`,
|
|
896
|
+
* so a caller can tell how far the batch actually got.
|
|
897
|
+
*/
|
|
898
|
+
/**
|
|
899
|
+
* Runs several different statements as one Parse/Bind/Describe/Execute
|
|
900
|
+
* stream closed by a single Sync (see PgSocket.sendPipelineMessages() for
|
|
901
|
+
* the wire shape and what that framing implies).
|
|
902
|
+
*
|
|
903
|
+
* The counterpart to executeBatchReused(): that one runs a single
|
|
904
|
+
* prepared statement over many parameter sets, this one runs many
|
|
905
|
+
* statements once each. Results map to statements positionally - the
|
|
906
|
+
* server answers each Execute with its own CommandComplete, and any rows
|
|
907
|
+
* that arrive before one belong to the statement it closes.
|
|
908
|
+
*/
|
|
909
|
+
async executePipeline(requests, options) {
|
|
910
|
+
this.assertConnected();
|
|
911
|
+
this.ref();
|
|
912
|
+
try {
|
|
913
|
+
const typeMap = options.typeMap || GlobalTypeMap;
|
|
914
|
+
const timingEnabled = options.timing ?? this.config.timing ?? false;
|
|
915
|
+
const startTime = timingEnabled ? performance.now() : 0;
|
|
916
|
+
const rowDecoder = resolveRowDecoder(options);
|
|
917
|
+
const columnFormat = options.columnFormat != null
|
|
918
|
+
? options.columnFormat
|
|
919
|
+
: DEFAULT_COLUMN_FORMAT;
|
|
920
|
+
const results = [];
|
|
921
|
+
let pendingFields;
|
|
922
|
+
let pendingRows;
|
|
923
|
+
let error;
|
|
924
|
+
const statements = requests.map(r => ({
|
|
925
|
+
// Per statement, not per pipeline: the statements differ, so a
|
|
926
|
+
// single shared list of parameter OIDs could only ever be right
|
|
927
|
+
// for one of them.
|
|
928
|
+
parse: { sql: r.sql, paramTypes: r.paramTypes },
|
|
929
|
+
bind: {
|
|
930
|
+
typeMap,
|
|
931
|
+
paramTypes: r.paramTypes,
|
|
932
|
+
params: r.params,
|
|
933
|
+
queryOptions: options,
|
|
934
|
+
},
|
|
935
|
+
describe: { type: 'P' },
|
|
936
|
+
// Unlimited, never options.fetchCount: a suspended portal would
|
|
937
|
+
// break the positional statement-to-result mapping.
|
|
938
|
+
execute: { fetchCount: 0 },
|
|
939
|
+
}));
|
|
940
|
+
this.runningQueryCount++;
|
|
941
|
+
await this.socket
|
|
942
|
+
.sendPipelineMessages({ statements }, (code, msg, done) => {
|
|
943
|
+
switch (code) {
|
|
944
|
+
case Protocol.BackendMessageCode.ParseComplete:
|
|
945
|
+
case Protocol.BackendMessageCode.BindComplete:
|
|
946
|
+
case Protocol.BackendMessageCode.NoticeResponse:
|
|
947
|
+
break;
|
|
948
|
+
case Protocol.BackendMessageCode.NoData:
|
|
949
|
+
pendingFields = undefined;
|
|
950
|
+
break;
|
|
951
|
+
case Protocol.BackendMessageCode.RowDescription:
|
|
952
|
+
pendingFields = msg.fields;
|
|
953
|
+
break;
|
|
954
|
+
case Protocol.BackendMessageCode.DataRow:
|
|
955
|
+
(pendingRows || (pendingRows = [])).push(msg);
|
|
956
|
+
break;
|
|
957
|
+
case Protocol.BackendMessageCode.EmptyQueryResponse:
|
|
958
|
+
case Protocol.BackendMessageCode.CommandComplete: {
|
|
959
|
+
const result = { command: msg?.command };
|
|
960
|
+
if (pendingFields) {
|
|
961
|
+
const parsers = getParsers(typeMap, pendingFields);
|
|
962
|
+
const resultFields = wrapRowDescription(typeMap, pendingFields, columnFormat);
|
|
963
|
+
result.fields = resultFields;
|
|
964
|
+
result.rowType = resolveRowType(options);
|
|
965
|
+
if (!result.command)
|
|
966
|
+
result.command = 'SELECT';
|
|
967
|
+
const rows = pendingRows || [];
|
|
968
|
+
const l = rows.length;
|
|
969
|
+
let i;
|
|
970
|
+
for (i = 0; i < l; i++) {
|
|
971
|
+
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
972
|
+
}
|
|
973
|
+
result.rows = rows;
|
|
974
|
+
}
|
|
975
|
+
if (result.command === 'DELETE' ||
|
|
976
|
+
result.command === 'INSERT' ||
|
|
977
|
+
result.command === 'UPDATE')
|
|
978
|
+
result.rowsAffected = msg.rowCount;
|
|
979
|
+
pendingFields = undefined;
|
|
980
|
+
pendingRows = undefined;
|
|
981
|
+
results.push(result);
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
case Protocol.BackendMessageCode.ErrorResponse:
|
|
985
|
+
error = msg;
|
|
986
|
+
break;
|
|
987
|
+
case Protocol.BackendMessageCode.ReadyForQuery:
|
|
988
|
+
this.transactionStatus = msg.status;
|
|
989
|
+
done(error);
|
|
990
|
+
break;
|
|
991
|
+
default:
|
|
992
|
+
done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));
|
|
993
|
+
}
|
|
994
|
+
})
|
|
995
|
+
.catch((e) => {
|
|
996
|
+
// The rejected statement is the one right after everything that
|
|
997
|
+
// completed, since results only grow on CommandComplete.
|
|
998
|
+
if (e instanceof DatabaseError && error === e)
|
|
999
|
+
e.failedIndex = results.length;
|
|
1000
|
+
throw e;
|
|
1001
|
+
})
|
|
1002
|
+
.finally(() => {
|
|
1003
|
+
this.runningQueryCount--;
|
|
1004
|
+
});
|
|
1005
|
+
if (timingEnabled) {
|
|
1006
|
+
const elapsed = performance.now() - startTime;
|
|
1007
|
+
const l = results.length;
|
|
1008
|
+
let i;
|
|
1009
|
+
for (i = 0; i < l; i++)
|
|
1010
|
+
results[i].executeTime = elapsed;
|
|
1011
|
+
}
|
|
1012
|
+
return results;
|
|
1013
|
+
}
|
|
1014
|
+
finally {
|
|
1015
|
+
this.unref();
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async executeBatchReused(statementName, cachedFields, paramTypes, paramSets, options) {
|
|
1019
|
+
this.assertConnected();
|
|
1020
|
+
this.ref();
|
|
1021
|
+
try {
|
|
1022
|
+
const typeMap = options.typeMap || GlobalTypeMap;
|
|
1023
|
+
const timingEnabled = options.timing ?? this.config.timing ?? false;
|
|
1024
|
+
const startTime = timingEnabled ? performance.now() : 0;
|
|
1025
|
+
const rowDecoder = resolveRowDecoder(options);
|
|
1026
|
+
let parsers;
|
|
1027
|
+
let resultFields;
|
|
1028
|
+
if (cachedFields) {
|
|
1029
|
+
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
|
|
1030
|
+
parsers = resolved.parsers;
|
|
1031
|
+
resultFields = resolved.resultFields;
|
|
1032
|
+
}
|
|
1033
|
+
const results = [];
|
|
1034
|
+
let pendingRows;
|
|
1035
|
+
let error;
|
|
1036
|
+
const binds = paramSets.map(params => ({
|
|
1037
|
+
typeMap,
|
|
1038
|
+
statement: statementName,
|
|
1039
|
+
paramTypes,
|
|
1040
|
+
params,
|
|
1041
|
+
queryOptions: options,
|
|
1042
|
+
}));
|
|
1043
|
+
this.runningQueryCount++;
|
|
1044
|
+
await this.socket
|
|
1045
|
+
.sendBatchBindExecuteMessages({
|
|
1046
|
+
binds,
|
|
1047
|
+
// Unlimited, never options.fetchCount: a suspended portal would
|
|
1048
|
+
// break the positional set-to-result mapping above.
|
|
1049
|
+
execute: { fetchCount: 0 },
|
|
1050
|
+
}, (code, msg, done) => {
|
|
1051
|
+
switch (code) {
|
|
1052
|
+
case Protocol.BackendMessageCode.BindComplete:
|
|
1053
|
+
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1054
|
+
break;
|
|
1055
|
+
case Protocol.BackendMessageCode.DataRow:
|
|
1056
|
+
(pendingRows || (pendingRows = [])).push(msg);
|
|
1057
|
+
break;
|
|
1058
|
+
case Protocol.BackendMessageCode.EmptyQueryResponse:
|
|
1059
|
+
case Protocol.BackendMessageCode.CommandComplete: {
|
|
1060
|
+
const item = {};
|
|
1061
|
+
if (msg?.command)
|
|
1062
|
+
item.command = msg.command;
|
|
1063
|
+
if (item.command === 'DELETE' ||
|
|
1064
|
+
item.command === 'INSERT' ||
|
|
1065
|
+
item.command === 'UPDATE')
|
|
1066
|
+
item.rowsAffected = msg.rowCount;
|
|
1067
|
+
if (pendingRows && parsers && resultFields) {
|
|
1068
|
+
const l = pendingRows.length;
|
|
1069
|
+
let i;
|
|
1070
|
+
for (i = 0; i < l; i++) {
|
|
1071
|
+
pendingRows[i] = rowDecoder.decode(parsers, pendingRows[i].data, pendingRows[i].columnCount, options, resultFields);
|
|
1072
|
+
}
|
|
1073
|
+
item.rows = pendingRows;
|
|
1074
|
+
if (!item.command)
|
|
1075
|
+
item.command = 'SELECT';
|
|
1076
|
+
}
|
|
1077
|
+
pendingRows = undefined;
|
|
1078
|
+
results.push(item);
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
case Protocol.BackendMessageCode.ErrorResponse:
|
|
1082
|
+
error = msg;
|
|
1083
|
+
break;
|
|
1084
|
+
case Protocol.BackendMessageCode.ReadyForQuery:
|
|
1085
|
+
this.transactionStatus = msg.status;
|
|
1086
|
+
done(error);
|
|
1087
|
+
break;
|
|
1088
|
+
default:
|
|
1089
|
+
done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));
|
|
1090
|
+
}
|
|
1091
|
+
})
|
|
1092
|
+
.catch((e) => {
|
|
1093
|
+
// The rejected set is the one right after everything that
|
|
1094
|
+
// completed, since results only grow on CommandComplete.
|
|
1095
|
+
if (e instanceof DatabaseError && error === e) {
|
|
1096
|
+
e.failedIndex = results.length;
|
|
1097
|
+
e.batchResults = results;
|
|
1098
|
+
}
|
|
1099
|
+
throw e;
|
|
1100
|
+
})
|
|
1101
|
+
.finally(() => {
|
|
1102
|
+
this.runningQueryCount--;
|
|
1103
|
+
});
|
|
1104
|
+
let totalRowsAffected = 0;
|
|
1105
|
+
const l = results.length;
|
|
1106
|
+
let i;
|
|
1107
|
+
for (i = 0; i < l; i++)
|
|
1108
|
+
totalRowsAffected += results[i].rowsAffected || 0;
|
|
1109
|
+
const out = { results, totalRowsAffected };
|
|
1110
|
+
if (resultFields)
|
|
1111
|
+
out.fields = resultFields;
|
|
1112
|
+
if (timingEnabled)
|
|
1113
|
+
out.executeTime = performance.now() - startTime;
|
|
1114
|
+
return out;
|
|
1115
|
+
}
|
|
1116
|
+
finally {
|
|
1117
|
+
this.unref();
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
async executeReused(statementName, cachedFields, paramTypes, params, options, savepoint) {
|
|
627
1121
|
this.assertConnected();
|
|
628
1122
|
this.ref();
|
|
629
1123
|
try {
|
|
@@ -636,36 +1130,19 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
636
1130
|
let resultFields;
|
|
637
1131
|
let commandTag;
|
|
638
1132
|
let error;
|
|
1133
|
+
const rowDecoder = resolveRowDecoder(options);
|
|
639
1134
|
if (cachedFields) {
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const cached = this._executeReusedParserCache.get(cachedFields);
|
|
644
|
-
if (cached &&
|
|
645
|
-
cached.typeMap === typeMap &&
|
|
646
|
-
columnFormatsEqual(cached.columnFormat, columnFormat)) {
|
|
647
|
-
parsers = cached.parsers;
|
|
648
|
-
resultFields = cached.resultFields;
|
|
649
|
-
}
|
|
650
|
-
else {
|
|
651
|
-
const fields = cachedFields.map((f, i) => ({
|
|
652
|
-
...f,
|
|
653
|
-
format: Array.isArray(columnFormat)
|
|
654
|
-
? columnFormat[i]
|
|
655
|
-
: columnFormat,
|
|
656
|
-
}));
|
|
657
|
-
parsers = getParsers(typeMap, fields);
|
|
658
|
-
resultFields = wrapRowDescription(typeMap, fields, columnFormat);
|
|
659
|
-
this._executeReusedParserCache.set(cachedFields, {
|
|
660
|
-
typeMap,
|
|
661
|
-
columnFormat,
|
|
662
|
-
parsers,
|
|
663
|
-
resultFields,
|
|
664
|
-
});
|
|
665
|
-
}
|
|
1135
|
+
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
|
|
1136
|
+
parsers = resolved.parsers;
|
|
1137
|
+
resultFields = resolved.resultFields;
|
|
666
1138
|
result.fields = resultFields;
|
|
667
|
-
result.rowType = options
|
|
1139
|
+
result.rowType = resolveRowType(options);
|
|
668
1140
|
}
|
|
1141
|
+
// The SAVEPOINT/RELEASE pair rollbackOnError needs travels with the
|
|
1142
|
+
// statement instead of costing a round trip each - see
|
|
1143
|
+
// PreparedStatement._withTransaction(), which hands the name down
|
|
1144
|
+
// rather than sending them itself.
|
|
1145
|
+
let leadingCommandTags = savepoint ? 1 : 0;
|
|
669
1146
|
this.runningQueryCount++;
|
|
670
1147
|
await this.socket
|
|
671
1148
|
.sendBindExecuteMessages({
|
|
@@ -677,8 +1154,11 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
677
1154
|
queryOptions: options,
|
|
678
1155
|
},
|
|
679
1156
|
execute: { fetchCount: options.fetchCount || 100 },
|
|
1157
|
+
before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
|
|
1158
|
+
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
680
1159
|
}, (code, msg, done) => {
|
|
681
1160
|
switch (code) {
|
|
1161
|
+
case Protocol.BackendMessageCode.ParseComplete:
|
|
682
1162
|
case Protocol.BackendMessageCode.BindComplete:
|
|
683
1163
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
684
1164
|
case Protocol.BackendMessageCode.PortalSuspended:
|
|
@@ -687,7 +1167,14 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
687
1167
|
rows.push(msg);
|
|
688
1168
|
break;
|
|
689
1169
|
case Protocol.BackendMessageCode.CommandComplete:
|
|
690
|
-
|
|
1170
|
+
// Three statements can answer here when a savepoint rides
|
|
1171
|
+
// along: SAVEPOINT's tag, then the caller's own, then
|
|
1172
|
+
// RELEASE's. Only the middle one describes what the caller
|
|
1173
|
+
// asked for.
|
|
1174
|
+
if (leadingCommandTags)
|
|
1175
|
+
leadingCommandTags--;
|
|
1176
|
+
else if (!commandTag)
|
|
1177
|
+
commandTag = msg;
|
|
691
1178
|
break;
|
|
692
1179
|
case Protocol.BackendMessageCode.ErrorResponse:
|
|
693
1180
|
error = msg;
|
|
@@ -712,9 +1199,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
712
1199
|
const l = rows.length;
|
|
713
1200
|
let i;
|
|
714
1201
|
for (i = 0; i < l; i++) {
|
|
715
|
-
rows[i] = options
|
|
716
|
-
? parseObjectRow(parsers, rows[i].data, rows[i].columnCount, options, resultFields)
|
|
717
|
-
: parseRow(parsers, rows[i].data, rows[i].columnCount, options);
|
|
1202
|
+
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
718
1203
|
}
|
|
719
1204
|
}
|
|
720
1205
|
if (result.command === 'DELETE' ||
|