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
package/protocol/pg-socket.js
CHANGED
|
@@ -26,8 +26,6 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
26
26
|
_secretKey;
|
|
27
27
|
_protocolNegotiation;
|
|
28
28
|
_captureQueue = new DoublyLinked();
|
|
29
|
-
_pendingWrites = [];
|
|
30
|
-
_flushScheduled = false;
|
|
31
29
|
_hosts = [];
|
|
32
30
|
_hostIndex = 0;
|
|
33
31
|
_sessionAttrsChecked = false;
|
|
@@ -240,15 +238,61 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
240
238
|
* statement/portal at the next Parse/Bind referencing the unnamed name.
|
|
241
239
|
*/
|
|
242
240
|
sendExtendedQueryMessages(args, cb) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
241
|
+
if (!args.before && !args.after) {
|
|
242
|
+
const data = [
|
|
243
|
+
this._frontend.getParseMessage(args.parse),
|
|
244
|
+
this._frontend.getBindMessage(args.bind),
|
|
245
|
+
this._frontend.getDescribeMessage(args.describe),
|
|
246
|
+
this._frontend.getExecuteMessage(args.execute),
|
|
247
|
+
this._frontend.getSyncMessage(),
|
|
248
|
+
];
|
|
249
|
+
return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
|
|
250
|
+
}
|
|
251
|
+
const data = [];
|
|
252
|
+
if (args.before)
|
|
253
|
+
this._pushUtilityStatement(data, args.before, args.bind);
|
|
254
|
+
data.push(this._frontend.getParseMessage(args.parse), this._frontend.getBindMessage(args.bind), this._frontend.getDescribeMessage(args.describe), this._frontend.getExecuteMessage(args.execute));
|
|
255
|
+
if (args.after)
|
|
256
|
+
this._pushUtilityStatement(data, args.after, args.bind);
|
|
257
|
+
data.push(this._frontend.getSyncMessage());
|
|
250
258
|
return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
|
|
251
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Parse + Bind + Describe(portal) + Execute per statement, then a SINGLE
|
|
262
|
+
* Sync for the lot - the wire shape behind Connection.pipeline().
|
|
263
|
+
*
|
|
264
|
+
* The same framing as sendBatchBindExecuteMessages(), and the same
|
|
265
|
+
* consequences, except that each statement carries its own Parse rather
|
|
266
|
+
* than reusing one prepared statement: the statements share one implicit
|
|
267
|
+
* transaction, and PostgreSQL discards everything between an error and
|
|
268
|
+
* the Sync, so a rejected statement stops the ones behind it.
|
|
269
|
+
*
|
|
270
|
+
* Describe is not optional here even though nothing caches its answer.
|
|
271
|
+
* Without it the server sends DataRows for a row-returning statement with
|
|
272
|
+
* no RowDescription in front of them, leaving the column names and types
|
|
273
|
+
* unknown - the reason the one-shot path in sendExtendedQueryMessages()
|
|
274
|
+
* includes it too. It costs about a quarter of a millisecond across
|
|
275
|
+
* twenty statements, measured.
|
|
276
|
+
*
|
|
277
|
+
* Every Execute is unlimited (fetchCount 0): a portal that suspended
|
|
278
|
+
* mid-pipeline would answer PortalSuspended instead of CommandComplete
|
|
279
|
+
* and desynchronise the positional statement-to-result mapping.
|
|
280
|
+
*/
|
|
281
|
+
sendPipelineMessages(args, cb) {
|
|
282
|
+
const l = args.statements.length;
|
|
283
|
+
const data = new Array(l * 4 + 1);
|
|
284
|
+
let i;
|
|
285
|
+
let st;
|
|
286
|
+
for (i = 0; i < l; i++) {
|
|
287
|
+
st = args.statements[i];
|
|
288
|
+
data[i * 4] = this._frontend.getParseMessage(st.parse);
|
|
289
|
+
data[i * 4 + 1] = this._frontend.getBindMessage(st.bind);
|
|
290
|
+
data[i * 4 + 2] = this._frontend.getDescribeMessage(st.describe);
|
|
291
|
+
data[i * 4 + 3] = this._frontend.getExecuteMessage(st.execute);
|
|
292
|
+
}
|
|
293
|
+
data[l * 4] = this._frontend.getSyncMessage();
|
|
294
|
+
return this._sendAndCapture(data, cb, 'sendPipelineMessages', args);
|
|
295
|
+
}
|
|
252
296
|
/**
|
|
253
297
|
* Parse + Describe(statement) + Sync as a single round trip - used by
|
|
254
298
|
* PreparedStatement.prepare() instead of a separate awaited Parse then a
|
|
@@ -274,13 +318,56 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
274
318
|
* auto-clears an unnamed portal at the next Bind naming it.
|
|
275
319
|
*/
|
|
276
320
|
sendBindExecuteMessages(args, cb) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
321
|
+
if (!args.before && !args.after) {
|
|
322
|
+
const data = [
|
|
323
|
+
this._frontend.getBindMessage(args.bind),
|
|
324
|
+
this._frontend.getExecuteMessage(args.execute),
|
|
325
|
+
this._frontend.getSyncMessage(),
|
|
326
|
+
];
|
|
327
|
+
return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
|
|
328
|
+
}
|
|
329
|
+
const data = [];
|
|
330
|
+
if (args.before)
|
|
331
|
+
this._pushUtilityStatement(data, args.before, args.bind);
|
|
332
|
+
data.push(this._frontend.getBindMessage(args.bind), this._frontend.getExecuteMessage(args.execute));
|
|
333
|
+
if (args.after)
|
|
334
|
+
this._pushUtilityStatement(data, args.after, args.bind);
|
|
335
|
+
data.push(this._frontend.getSyncMessage());
|
|
282
336
|
return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
|
|
283
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* One Bind+Execute pair per parameter set against the same prepared
|
|
340
|
+
* statement, then a SINGLE Sync for the lot - the wire shape behind
|
|
341
|
+
* PreparedStatement.executeBatch().
|
|
342
|
+
*
|
|
343
|
+
* The single Sync is the whole point, and it is also what gives the
|
|
344
|
+
* batch its semantics. Sending Bind/Execute/Sync per set (what N
|
|
345
|
+
* separate execute() calls do, even pipelined) makes the server finish
|
|
346
|
+
* an implicit transaction and answer ReadyForQuery every time; one Sync
|
|
347
|
+
* at the end collapses that to one, which is where the bulk of the
|
|
348
|
+
* speedup comes from. It also means the sets share one implicit
|
|
349
|
+
* transaction, and that PostgreSQL discards everything between an error
|
|
350
|
+
* and the Sync - so a set that fails stops the ones behind it from
|
|
351
|
+
* running at all. Both of those are inherent to this framing rather
|
|
352
|
+
* than choices made above it.
|
|
353
|
+
*
|
|
354
|
+
* Every Execute here must be unlimited (fetchCount 0): a portal that
|
|
355
|
+
* suspends mid-batch would answer PortalSuspended instead of
|
|
356
|
+
* CommandComplete and desynchronise the positional set-to-result
|
|
357
|
+
* mapping the caller relies on.
|
|
358
|
+
*/
|
|
359
|
+
sendBatchBindExecuteMessages(args, cb) {
|
|
360
|
+
const binds = args.binds;
|
|
361
|
+
const l = binds.length;
|
|
362
|
+
const data = new Array(l * 2 + 1);
|
|
363
|
+
let i;
|
|
364
|
+
for (i = 0; i < l; i++) {
|
|
365
|
+
data[i * 2] = this._frontend.getBindMessage(binds[i]);
|
|
366
|
+
data[i * 2 + 1] = this._frontend.getExecuteMessage(args.execute);
|
|
367
|
+
}
|
|
368
|
+
data[l * 2] = this._frontend.getSyncMessage();
|
|
369
|
+
return this._sendAndCapture(data, cb, 'sendBatchBindExecuteMessages', args);
|
|
370
|
+
}
|
|
284
371
|
/**
|
|
285
372
|
* Bind + Describe(portal) + Flush as a single round trip (NOT Sync - see
|
|
286
373
|
* Portal.bindAndRetrieveFields()'s doc comment for why a Sync here would
|
|
@@ -407,6 +494,29 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
407
494
|
}
|
|
408
495
|
});
|
|
409
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Parse + Bind + Execute for a parameterless utility statement, appended
|
|
499
|
+
* to a message list that is about to be closed by one Sync - how
|
|
500
|
+
* sendBindExecuteMessages() and sendExtendedQueryMessages() carry their
|
|
501
|
+
* `before`/`after` SQL (today, SAVEPOINT and RELEASE) in the same round
|
|
502
|
+
* trip as the statement itself rather than as a round trip each.
|
|
503
|
+
*
|
|
504
|
+
* Everything here goes through the unnamed statement and the unnamed
|
|
505
|
+
* portal. That is safe in this order: the caller's own Bind names a
|
|
506
|
+
* prepared statement rather than the unnamed one, and PostgreSQL
|
|
507
|
+
* processes the messages in sequence, so each Bind only ever discards an
|
|
508
|
+
* unnamed portal whose Execute has already run.
|
|
509
|
+
*
|
|
510
|
+
* No Describe: neither statement returns rows, so there is no
|
|
511
|
+
* RowDescription worth asking for. Result formats come from the caller's
|
|
512
|
+
* own Bind args, which likewise cost nothing against no columns.
|
|
513
|
+
*/
|
|
514
|
+
_pushUtilityStatement(data, sql, bind) {
|
|
515
|
+
data.push(this._frontend.getParseMessage({ sql }), this._frontend.getBindMessage({
|
|
516
|
+
typeMap: bind.typeMap,
|
|
517
|
+
queryOptions: bind.queryOptions,
|
|
518
|
+
}), this._frontend.getExecuteMessage({}));
|
|
519
|
+
}
|
|
410
520
|
_removeListeners() {
|
|
411
521
|
if (!this._socket)
|
|
412
522
|
return;
|
|
@@ -421,9 +531,6 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
421
531
|
this._processID = undefined;
|
|
422
532
|
this._secretKey = undefined;
|
|
423
533
|
this._saslSession = undefined;
|
|
424
|
-
// Anything still queued belonged to the connection being reset - it
|
|
425
|
-
// must never reach a freshly connected socket.
|
|
426
|
-
this._pendingWrites = [];
|
|
427
534
|
}
|
|
428
535
|
_handleConnect() {
|
|
429
536
|
const socket = this._socket;
|
|
@@ -740,65 +847,38 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
740
847
|
result.rowCount = parseInt(m[2], 10);
|
|
741
848
|
return result;
|
|
742
849
|
}
|
|
743
|
-
/**
|
|
744
|
-
* Queues `data` for the socket instead of writing it straight through:
|
|
745
|
-
* everything handed to _send() during the same synchronous burst (e.g.
|
|
746
|
-
* many concurrent query() calls fired via Promise.all over one
|
|
747
|
-
* connection) is flushed together on the next tick, corked, so Node
|
|
748
|
-
* turns the whole batch into a single underlying writev rather than one
|
|
749
|
-
* socket write per call. Measured against postgres.js, which coalesces
|
|
750
|
-
* the same way at the application level (its connection.js write()/
|
|
751
|
-
* nextWrite() pair, flushing on a 1KB threshold or setImmediate): 50
|
|
752
|
-
* concurrent prepared executes cost it 3 socket writes where this cost
|
|
753
|
-
* us 50, and that write path was the single largest non-idle item in a
|
|
754
|
-
* CPU profile of the scenario.
|
|
755
|
-
*
|
|
756
|
-
* nextTick rather than setImmediate, deliberately: it still batches a
|
|
757
|
-
* whole synchronous burst, but a lone query doesn't have to wait for the
|
|
758
|
-
* event loop's check phase before its bytes go out - which would trade
|
|
759
|
-
* away the sequential/single-query scenarios to win the concurrent ones.
|
|
760
|
-
*
|
|
761
|
-
* Returns whether the socket could accept the data at all; the write
|
|
762
|
-
* itself is reported through `cb` (and a socket that dies before the
|
|
763
|
-
* flush rejects every pending capture via _failPendingCaptures()).
|
|
764
|
-
*/
|
|
765
850
|
_send(data, cb) {
|
|
766
|
-
|
|
851
|
+
const socket = this._socket;
|
|
852
|
+
if (!socket || !socket.writable)
|
|
767
853
|
return false;
|
|
768
|
-
if (Array.isArray(data)) {
|
|
854
|
+
if (!Array.isArray(data)) {
|
|
855
|
+
socket.write(data, cb);
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
socket.cork();
|
|
859
|
+
try {
|
|
769
860
|
const l = data.length;
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
cb: i === l - 1 ? cb : undefined,
|
|
774
|
-
});
|
|
861
|
+
let i;
|
|
862
|
+
for (i = 0; i < l; i++)
|
|
863
|
+
socket.write(data[i], i === l - 1 ? cb : undefined);
|
|
775
864
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if (this._captureQueue.length > 1) {
|
|
779
|
-
if (!this._flushScheduled) {
|
|
780
|
-
this._flushScheduled = true;
|
|
781
|
-
process.nextTick(this._flushPendingWrites);
|
|
782
|
-
}
|
|
865
|
+
finally {
|
|
866
|
+
socket.uncork();
|
|
783
867
|
}
|
|
784
|
-
else
|
|
785
|
-
this._flushPendingWrites();
|
|
786
868
|
return true;
|
|
787
869
|
}
|
|
788
870
|
/**
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
* deferred write cannot give it.
|
|
871
|
+
* Same write as _send(), but reporting backpressure instead of
|
|
872
|
+
* writability: a COPY IN producer needs write()'s own return value to
|
|
873
|
+
* know when to stop pushing, whereas _send()'s callers only need to know
|
|
874
|
+
* whether the socket could take the bytes at all. Kept separate rather
|
|
875
|
+
* than folded into _send() precisely so those two meanings of `false`
|
|
876
|
+
* cannot be confused at a call site.
|
|
796
877
|
*/
|
|
797
878
|
_sendCopy(data, cb) {
|
|
798
879
|
const socket = this._socket;
|
|
799
880
|
if (!socket || !socket.writable)
|
|
800
881
|
return false;
|
|
801
|
-
this._flushPendingWrites();
|
|
802
882
|
if (!Array.isArray(data))
|
|
803
883
|
return socket.write(data, cb);
|
|
804
884
|
socket.cork();
|
|
@@ -814,27 +894,4 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
814
894
|
socket.uncork();
|
|
815
895
|
}
|
|
816
896
|
}
|
|
817
|
-
/** Arrow property, not a method: used as a bare process.nextTick callback. */
|
|
818
|
-
_flushPendingWrites = () => {
|
|
819
|
-
this._flushScheduled = false;
|
|
820
|
-
const pending = this._pendingWrites;
|
|
821
|
-
if (!pending.length)
|
|
822
|
-
return;
|
|
823
|
-
this._pendingWrites = [];
|
|
824
|
-
const socket = this._socket;
|
|
825
|
-
if (!socket || !socket.writable)
|
|
826
|
-
return;
|
|
827
|
-
socket.cork();
|
|
828
|
-
try {
|
|
829
|
-
const l = pending.length;
|
|
830
|
-
let i;
|
|
831
|
-
for (i = 0; i < l; i++) {
|
|
832
|
-
const { data, cb } = pending[i];
|
|
833
|
-
socket.write(data, cb);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
finally {
|
|
837
|
-
socket.uncork();
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
897
|
}
|
package/protocol/pgoutput.js
CHANGED
|
@@ -106,7 +106,7 @@ export class PgOutputDecoder {
|
|
|
106
106
|
const lsn = readLsn(io);
|
|
107
107
|
const prefix = io.readCString();
|
|
108
108
|
const length = io.readUInt32BE();
|
|
109
|
-
return { kind: 'message', prefix, lsn, content: io.
|
|
109
|
+
return { kind: 'message', prefix, lsn, content: io.readBytes(length) };
|
|
110
110
|
}
|
|
111
111
|
default:
|
|
112
112
|
return { kind: 'unknown', code };
|
|
@@ -134,7 +134,7 @@ export class PgOutputDecoder {
|
|
|
134
134
|
}
|
|
135
135
|
else {
|
|
136
136
|
const length = io.readUInt32BE();
|
|
137
|
-
out[column.name] = io.
|
|
137
|
+
out[column.name] = io.readBytes(length).toString('utf8');
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
return out;
|
|
@@ -1,37 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FlexyBuffer } from 'flexy-buffer';
|
|
2
2
|
export interface SmartBufferConfig {
|
|
3
3
|
pageSize?: number;
|
|
4
4
|
maxLength?: number;
|
|
5
|
-
|
|
5
|
+
houseKeepMs?: number;
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Adds the PostgreSQL wire format's two string encodings on top of
|
|
9
|
+
* flexy-buffer's `FlexyBuffer`: a NUL-terminated C string, and a
|
|
10
|
+
* length-prefixed string where a `null`/`undefined` value is written as a
|
|
11
|
+
* length of -1 with no bytes following.
|
|
12
|
+
*/
|
|
13
|
+
export declare class SmartBuffer extends FlexyBuffer {
|
|
8
14
|
static DEFAULT_PAGE_SIZE: number;
|
|
9
15
|
static DEFAULT_MAX_SIZE: number;
|
|
10
|
-
private readonly _houseKeepInterval;
|
|
11
|
-
private _houseKeepTimer?;
|
|
12
|
-
private _stMaxPages;
|
|
13
|
-
private _length;
|
|
14
|
-
readonly pageSize: number;
|
|
15
|
-
readonly maxSize: number;
|
|
16
16
|
constructor(cfg?: SmartBufferConfig);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
start(): this;
|
|
20
|
-
flush(): Buffer;
|
|
21
|
-
growSize(len: number): this;
|
|
22
|
-
writeCString(str: string, encoding?: BufferEncoding): this;
|
|
23
|
-
writeLString(str?: string, encoding?: BufferEncoding): this;
|
|
24
|
-
writeString(str: string, encoding?: BufferEncoding): this;
|
|
25
|
-
writeInt8(n: number): this;
|
|
26
|
-
writeUInt8(n: number): this;
|
|
27
|
-
writeUInt16BE(n: number): this;
|
|
28
|
-
writeUInt32BE(n: number): this;
|
|
29
|
-
writeInt16BE(n: number): this;
|
|
30
|
-
writeInt32BE(n: number): this;
|
|
31
|
-
writeBigInt64BE(n: bigint | number): this;
|
|
32
|
-
writeFloatBE(n: number): this;
|
|
33
|
-
writeDoubleBE(n: number): this;
|
|
34
|
-
writeBuffer(buffer: Buffer): this;
|
|
35
|
-
private ensureSize;
|
|
36
|
-
private _houseKeep;
|
|
17
|
+
writeCString(str: string, encoding?: BufferEncoding): number;
|
|
18
|
+
writeLString(str?: string, encoding?: BufferEncoding): number;
|
|
37
19
|
}
|
package/protocol/smart-buffer.js
CHANGED
|
@@ -1,172 +1,29 @@
|
|
|
1
|
+
import { FlexyBuffer } from 'flexy-buffer';
|
|
1
2
|
import * as os from 'os';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Adds the PostgreSQL wire format's two string encodings on top of
|
|
5
|
+
* flexy-buffer's `FlexyBuffer`: a NUL-terminated C string, and a
|
|
6
|
+
* length-prefixed string where a `null`/`undefined` value is written as a
|
|
7
|
+
* length of -1 with no bytes following.
|
|
8
|
+
*/
|
|
9
|
+
export class SmartBuffer extends FlexyBuffer {
|
|
5
10
|
static DEFAULT_PAGE_SIZE = 512;
|
|
6
11
|
static DEFAULT_MAX_SIZE = Math.min(Math.floor(os.totalmem() / 2), 1024 * 1024 * 1024 * 2);
|
|
7
|
-
_houseKeepInterval;
|
|
8
|
-
_houseKeepTimer;
|
|
9
|
-
_stMaxPages = 1;
|
|
10
|
-
_length = 0;
|
|
11
|
-
pageSize;
|
|
12
|
-
maxSize;
|
|
13
12
|
constructor(cfg) {
|
|
14
|
-
super(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
get capacity() {
|
|
21
|
-
return this.buffer.length;
|
|
22
|
-
}
|
|
23
|
-
get length() {
|
|
24
|
-
return this._length;
|
|
25
|
-
}
|
|
26
|
-
start() {
|
|
27
|
-
this.offset = 0;
|
|
28
|
-
this._length = 0;
|
|
29
|
-
if (this._houseKeepTimer) {
|
|
30
|
-
clearTimeout(this._houseKeepTimer);
|
|
31
|
-
this._houseKeepTimer = undefined;
|
|
32
|
-
}
|
|
33
|
-
return this;
|
|
34
|
-
}
|
|
35
|
-
flush() {
|
|
36
|
-
if (this._houseKeepTimer)
|
|
37
|
-
clearTimeout(this._houseKeepTimer);
|
|
38
|
-
const length = this.length;
|
|
39
|
-
this._length = 0;
|
|
40
|
-
const out = Buffer.from(this.buffer.subarray(0, length));
|
|
41
|
-
const pages = length ? Math.ceil(length / this.pageSize) : 1;
|
|
42
|
-
this._stMaxPages = Math.max(this._stMaxPages, pages);
|
|
43
|
-
this._houseKeep();
|
|
44
|
-
if (this.buffer.length > this.pageSize) {
|
|
45
|
-
this._houseKeepTimer = setTimeout(() => {
|
|
46
|
-
this._houseKeepTimer = undefined;
|
|
47
|
-
this._houseKeep();
|
|
48
|
-
}, this._houseKeepInterval).unref();
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
|
-
growSize(len) {
|
|
53
|
-
const endOffset = this.offset + len;
|
|
54
|
-
if (this.capacity < endOffset) {
|
|
55
|
-
if (endOffset > this.maxSize)
|
|
56
|
-
throw new Error('Buffer limit exceeded.');
|
|
57
|
-
const byPage = Math.ceil(endOffset / this.pageSize) * this.pageSize;
|
|
58
|
-
const newSize = Math.min(Math.max(byPage, this.capacity * 2), this.maxSize);
|
|
59
|
-
const newBuffer = Buffer.allocUnsafe(newSize);
|
|
60
|
-
this.buffer.copy(newBuffer);
|
|
61
|
-
this.buffer = newBuffer;
|
|
62
|
-
}
|
|
63
|
-
this._length = Math.max(this.length, endOffset);
|
|
64
|
-
return this;
|
|
13
|
+
super({
|
|
14
|
+
pageSize: cfg?.pageSize || SmartBuffer.DEFAULT_PAGE_SIZE,
|
|
15
|
+
maxLength: cfg?.maxLength || SmartBuffer.DEFAULT_MAX_SIZE,
|
|
16
|
+
houseKeepMs: cfg?.houseKeepMs,
|
|
17
|
+
});
|
|
65
18
|
}
|
|
66
19
|
writeCString(str, encoding) {
|
|
67
|
-
const
|
|
68
|
-
this.
|
|
69
|
-
if (str) {
|
|
70
|
-
this.buffer.write(str, this.offset, encoding);
|
|
71
|
-
this.offset += len;
|
|
72
|
-
}
|
|
73
|
-
this.writeUInt8(0);
|
|
74
|
-
return this;
|
|
20
|
+
const written = this.writeString(str, encoding);
|
|
21
|
+
return written + this.writeUInt8(0);
|
|
75
22
|
}
|
|
76
23
|
writeLString(str, encoding) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (encoding)
|
|
82
|
-
this.offset += this.buffer.write(str, this.offset, encoding);
|
|
83
|
-
else
|
|
84
|
-
this.offset += this.buffer.write(str, this.offset);
|
|
85
|
-
}
|
|
86
|
-
return this;
|
|
87
|
-
}
|
|
88
|
-
writeString(str, encoding) {
|
|
89
|
-
if (str) {
|
|
90
|
-
const len = Buffer.byteLength(str, encoding);
|
|
91
|
-
this.ensureSize(len);
|
|
92
|
-
this.offset += this.buffer.write(str, this.offset, encoding);
|
|
93
|
-
}
|
|
94
|
-
return this;
|
|
95
|
-
}
|
|
96
|
-
writeInt8(n) {
|
|
97
|
-
this.ensureSize(1);
|
|
98
|
-
this.buffer.writeInt8(n, this.offset);
|
|
99
|
-
this.offset++;
|
|
100
|
-
return this;
|
|
101
|
-
}
|
|
102
|
-
writeUInt8(n) {
|
|
103
|
-
this.ensureSize(1);
|
|
104
|
-
this.buffer.writeUInt8(n, this.offset);
|
|
105
|
-
this.offset++;
|
|
106
|
-
return this;
|
|
107
|
-
}
|
|
108
|
-
writeUInt16BE(n) {
|
|
109
|
-
this.ensureSize(2);
|
|
110
|
-
this.buffer.writeUInt16BE(n, this.offset);
|
|
111
|
-
this.offset += 2;
|
|
112
|
-
return this;
|
|
113
|
-
}
|
|
114
|
-
writeUInt32BE(n) {
|
|
115
|
-
this.ensureSize(4);
|
|
116
|
-
this.buffer.writeUInt32BE(n, this.offset);
|
|
117
|
-
this.offset += 4;
|
|
118
|
-
return this;
|
|
119
|
-
}
|
|
120
|
-
writeInt16BE(n) {
|
|
121
|
-
this.ensureSize(2);
|
|
122
|
-
this.buffer.writeInt16BE(n, this.offset);
|
|
123
|
-
this.offset += 2;
|
|
124
|
-
return this;
|
|
125
|
-
}
|
|
126
|
-
writeInt32BE(n) {
|
|
127
|
-
this.ensureSize(4);
|
|
128
|
-
this.buffer.writeInt32BE(n, this.offset);
|
|
129
|
-
this.offset += 4;
|
|
130
|
-
return this;
|
|
131
|
-
}
|
|
132
|
-
writeBigInt64BE(n) {
|
|
133
|
-
n = typeof n === 'bigint' ? n : BigInt(n);
|
|
134
|
-
this.ensureSize(8);
|
|
135
|
-
if (typeof this.buffer.writeBigInt64BE === 'function')
|
|
136
|
-
this.buffer.writeBigInt64BE(n, this.offset);
|
|
137
|
-
else
|
|
138
|
-
writeBigUInt64BE(this.buffer, n, this.offset);
|
|
139
|
-
this.offset += 8;
|
|
140
|
-
return this;
|
|
141
|
-
}
|
|
142
|
-
writeFloatBE(n) {
|
|
143
|
-
this.ensureSize(4);
|
|
144
|
-
this.buffer.writeFloatBE(n, this.offset);
|
|
145
|
-
this.offset += 4;
|
|
146
|
-
return this;
|
|
147
|
-
}
|
|
148
|
-
writeDoubleBE(n) {
|
|
149
|
-
this.ensureSize(8);
|
|
150
|
-
this.buffer.writeDoubleBE(n, this.offset);
|
|
151
|
-
this.offset += 8;
|
|
152
|
-
return this;
|
|
153
|
-
}
|
|
154
|
-
writeBuffer(buffer) {
|
|
155
|
-
this.ensureSize(buffer.length);
|
|
156
|
-
buffer.copy(this.buffer, this.offset, 0, buffer.length);
|
|
157
|
-
this.offset += buffer.length;
|
|
158
|
-
return this;
|
|
159
|
-
}
|
|
160
|
-
ensureSize(len) {
|
|
161
|
-
const n = this.offset + len - this.length;
|
|
162
|
-
if (n > 0)
|
|
163
|
-
this.growSize(n);
|
|
164
|
-
return this;
|
|
165
|
-
}
|
|
166
|
-
_houseKeep() {
|
|
167
|
-
const needSize = this._stMaxPages * this.pageSize;
|
|
168
|
-
if (this.buffer.length > needSize)
|
|
169
|
-
this.buffer = Buffer.allocUnsafe(needSize);
|
|
170
|
-
this._stMaxPages = this.length ? Math.ceil(this.length / this.pageSize) : 1;
|
|
24
|
+
if (str == null)
|
|
25
|
+
return this.writeInt32BE(-1);
|
|
26
|
+
const len = Buffer.byteLength(str, encoding);
|
|
27
|
+
return this.writeInt32BE(len) + this.writeString(str, encoding);
|
|
171
28
|
}
|
|
172
29
|
}
|
package/types.d.ts
CHANGED
|
@@ -5,7 +5,19 @@ export type Maybe<T> = T | undefined;
|
|
|
5
5
|
export type Nullable<T> = T | null;
|
|
6
6
|
export type Row = any;
|
|
7
7
|
export type Callback = (err?: Error | null) => void;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* What a DataType's own `decodeBinary` implements. `buf` is a shared
|
|
10
|
+
* buffer - the whole row, or the whole array - and this value occupies
|
|
11
|
+
* exactly `len` bytes starting at `offset`.
|
|
12
|
+
*
|
|
13
|
+
* Honouring `len` is the implementation's responsibility, and reading past
|
|
14
|
+
* it is not a bounds error: it silently returns the next column's or
|
|
15
|
+
* element's bytes. A type whose binary form says how long it is - a fixed
|
|
16
|
+
* width like int4's four bytes, or a count it carries itself like
|
|
17
|
+
* numeric's - can ignore `len`; anything that would otherwise read "to the
|
|
18
|
+
* end of the buffer" must not.
|
|
19
|
+
*/
|
|
20
|
+
export type DecodeBinaryFunction = (buf: Buffer, offset: number, len: number, options: DataMappingOptions & Record<string, any>) => any;
|
|
9
21
|
export type DecodeTextBufferFunction = (buf: Buffer, offset: number, len: number, options: DataMappingOptions & Record<string, any>) => any;
|
|
10
22
|
export type EncodeBinaryFunction = (buf: SmartBuffer, v: any, options: DataMappingOptions) => void;
|
|
11
23
|
export type EncodeCalculateDimFunction = (v: any[]) => number[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns `v` coerced to an integer, or throws when it cannot be one.
|
|
3
|
+
*
|
|
4
|
+
* `fastParseInt()` already answers NaN for a value with no integer reading
|
|
5
|
+
* - `'abc'`, `{}`, `[]`, `true` - but `Buffer.writeInt32BE(NaN)` then
|
|
6
|
+
* stores a silent zero, so the answer is lost exactly where it matters.
|
|
7
|
+
* Nothing downstream can tell that apart from a real zero afterwards: the
|
|
8
|
+
* row inserts, the copy succeeds, and the wrong number is in the table.
|
|
9
|
+
*
|
|
10
|
+
* Floats are deliberately not rejected: truncating 3.7 to 3 is the
|
|
11
|
+
* long-standing behaviour of these encoders and is usually what a caller
|
|
12
|
+
* writing into an integer column means. Only "this is not a number at all"
|
|
13
|
+
* is refused.
|
|
14
|
+
*
|
|
15
|
+
* Unlike the integer types, float4/float8/numeric leave NaN alone - it is a
|
|
16
|
+
* value PostgreSQL stores and returns as NaN, distinct from NULL, on every
|
|
17
|
+
* supported server version.
|
|
18
|
+
*/
|
|
19
|
+
export declare function assertInteger(v: number, typeName: string): number;
|
|
20
|
+
/**
|
|
21
|
+
* Returns `parsed` when a value that was NOT already a number coerced to
|
|
22
|
+
* one, and throws when it did not.
|
|
23
|
+
*
|
|
24
|
+
* Kept apart from assertInteger() because the two families disagree about
|
|
25
|
+
* NaN on purpose. float4/float8/numeric all store NaN as a value distinct
|
|
26
|
+
* from NULL, so a caller passing the number NaN means it and it never
|
|
27
|
+
* reaches here - the encoders take `typeof v === 'number'` straight
|
|
28
|
+
* through. What does reach here is `'abc'`, `{}` or `true`, whose coercion
|
|
29
|
+
* produced NaN because they have no numeric reading at all; writing those
|
|
30
|
+
* as NaN would be indistinguishable afterwards from a NaN that was meant.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertCoercedNumber(parsed: number, typeName: string): number;
|
|
33
|
+
/**
|
|
34
|
+
* Refuses a value that only has an integer reading through a coercion
|
|
35
|
+
* nobody asked for.
|
|
36
|
+
*
|
|
37
|
+
* int8 goes to `BigInt` rather than through `fastParseInt()`, and BigInt is
|
|
38
|
+
* happy to convert things that are not numbers at all: `BigInt([])` is 0n
|
|
39
|
+
* because an empty array stringifies to "", and `BigInt(true)` is 1n. A
|
|
40
|
+
* bad value in a bigint column would land as a plausible-looking 0 or 1,
|
|
41
|
+
* so only the types that genuinely carry an integer are let through.
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertBigIntSource(v: any, typeName: string): bigint | number;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns `v` coerced to an integer, or throws when it cannot be one.
|
|
3
|
+
*
|
|
4
|
+
* `fastParseInt()` already answers NaN for a value with no integer reading
|
|
5
|
+
* - `'abc'`, `{}`, `[]`, `true` - but `Buffer.writeInt32BE(NaN)` then
|
|
6
|
+
* stores a silent zero, so the answer is lost exactly where it matters.
|
|
7
|
+
* Nothing downstream can tell that apart from a real zero afterwards: the
|
|
8
|
+
* row inserts, the copy succeeds, and the wrong number is in the table.
|
|
9
|
+
*
|
|
10
|
+
* Floats are deliberately not rejected: truncating 3.7 to 3 is the
|
|
11
|
+
* long-standing behaviour of these encoders and is usually what a caller
|
|
12
|
+
* writing into an integer column means. Only "this is not a number at all"
|
|
13
|
+
* is refused.
|
|
14
|
+
*
|
|
15
|
+
* Unlike the integer types, float4/float8/numeric leave NaN alone - it is a
|
|
16
|
+
* value PostgreSQL stores and returns as NaN, distinct from NULL, on every
|
|
17
|
+
* supported server version.
|
|
18
|
+
*/
|
|
19
|
+
export function assertInteger(v, typeName) {
|
|
20
|
+
if (Number.isNaN(v)) {
|
|
21
|
+
throw new TypeError(`Cannot encode value as ${typeName}: it has no integer value`);
|
|
22
|
+
}
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns `parsed` when a value that was NOT already a number coerced to
|
|
27
|
+
* one, and throws when it did not.
|
|
28
|
+
*
|
|
29
|
+
* Kept apart from assertInteger() because the two families disagree about
|
|
30
|
+
* NaN on purpose. float4/float8/numeric all store NaN as a value distinct
|
|
31
|
+
* from NULL, so a caller passing the number NaN means it and it never
|
|
32
|
+
* reaches here - the encoders take `typeof v === 'number'` straight
|
|
33
|
+
* through. What does reach here is `'abc'`, `{}` or `true`, whose coercion
|
|
34
|
+
* produced NaN because they have no numeric reading at all; writing those
|
|
35
|
+
* as NaN would be indistinguishable afterwards from a NaN that was meant.
|
|
36
|
+
*/
|
|
37
|
+
export function assertCoercedNumber(parsed, typeName) {
|
|
38
|
+
if (Number.isNaN(parsed)) {
|
|
39
|
+
throw new TypeError(`Cannot encode value as ${typeName}: it has no numeric value`);
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Refuses a value that only has an integer reading through a coercion
|
|
45
|
+
* nobody asked for.
|
|
46
|
+
*
|
|
47
|
+
* int8 goes to `BigInt` rather than through `fastParseInt()`, and BigInt is
|
|
48
|
+
* happy to convert things that are not numbers at all: `BigInt([])` is 0n
|
|
49
|
+
* because an empty array stringifies to "", and `BigInt(true)` is 1n. A
|
|
50
|
+
* bad value in a bigint column would land as a plausible-looking 0 or 1,
|
|
51
|
+
* so only the types that genuinely carry an integer are let through.
|
|
52
|
+
*/
|
|
53
|
+
export function assertBigIntSource(v, typeName) {
|
|
54
|
+
const t = typeof v;
|
|
55
|
+
if (t === 'bigint' || t === 'number' || t === 'string')
|
|
56
|
+
return v;
|
|
57
|
+
throw new TypeError(`Cannot encode value as ${typeName}: it has no integer value`);
|
|
58
|
+
}
|