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.
Files changed (80) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/README.md +79 -13
  3. package/connection/connection.d.ts +97 -0
  4. package/connection/connection.js +141 -2
  5. package/connection/cursor.d.ts +2 -1
  6. package/connection/cursor.js +6 -6
  7. package/connection/intl-connection.d.ts +113 -3
  8. package/connection/intl-connection.js +530 -45
  9. package/connection/prepared-statement.d.ts +64 -1
  10. package/connection/prepared-statement.js +97 -6
  11. package/data-types/bool-type.js +0 -1
  12. package/data-types/box-type.js +0 -1
  13. package/data-types/bytea-type.js +5 -3
  14. package/data-types/char-type.js +2 -2
  15. package/data-types/circle-type.js +0 -1
  16. package/data-types/date-type.js +1 -2
  17. package/data-types/float4-type.js +4 -5
  18. package/data-types/float8-type.js +4 -6
  19. package/data-types/int2-type.js +2 -2
  20. package/data-types/int2-vector-type.js +3 -2
  21. package/data-types/int4-type.js +2 -2
  22. package/data-types/int8-type.js +2 -2
  23. package/data-types/json-type.js +2 -2
  24. package/data-types/jsonb-type.js +6 -5
  25. package/data-types/lseg-type.js +0 -1
  26. package/data-types/numeric-type.js +9 -4
  27. package/data-types/oid-type.js +2 -2
  28. package/data-types/oid-vector-type.js +3 -2
  29. package/data-types/point-type.js +0 -1
  30. package/data-types/time-type.js +1 -2
  31. package/data-types/timestamp-type.js +13 -3
  32. package/data-types/timestamptz-type.js +27 -10
  33. package/data-types/uuid-type.js +1 -2
  34. package/data-types/varchar-type.js +2 -2
  35. package/index.d.ts +4 -0
  36. package/index.js +4 -0
  37. package/interfaces/batch-result.d.ts +42 -0
  38. package/interfaces/batch-result.js +1 -0
  39. package/interfaces/command-result.d.ts +3 -2
  40. package/interfaces/copy-from-rows-options.d.ts +58 -0
  41. package/interfaces/copy-from-rows-options.js +1 -0
  42. package/interfaces/data-type.d.ts +0 -1
  43. package/interfaces/database-connection-params.d.ts +25 -0
  44. package/interfaces/pipeline-request.d.ts +16 -0
  45. package/interfaces/pipeline-request.js +1 -0
  46. package/interfaces/query-options.d.ts +18 -0
  47. package/interfaces/script-execute-options.d.ts +12 -0
  48. package/package.json +18 -3
  49. package/protocol/backend.js +6 -6
  50. package/protocol/buffer-reader.d.ts +7 -12
  51. package/protocol/buffer-reader.js +11 -58
  52. package/protocol/database-error.d.ts +16 -0
  53. package/protocol/database-error.js +15 -0
  54. package/protocol/frontend.js +86 -96
  55. package/protocol/pg-socket.d.ts +82 -33
  56. package/protocol/pg-socket.js +143 -86
  57. package/protocol/pgoutput.js +2 -2
  58. package/protocol/smart-buffer.d.ts +11 -29
  59. package/protocol/smart-buffer.js +19 -162
  60. package/types.d.ts +13 -1
  61. package/util/assert-integer.d.ts +43 -0
  62. package/util/assert-integer.js +58 -0
  63. package/util/bigint-methods.d.ts +0 -1
  64. package/util/bigint-methods.js +0 -22
  65. package/util/copy-from-rows.d.ts +56 -0
  66. package/util/copy-from-rows.js +95 -0
  67. package/util/decode-binaryarray.d.ts +1 -1
  68. package/util/decode-binaryarray.js +7 -7
  69. package/util/encode-binaryarray.js +6 -6
  70. package/util/encode-copy-binary.d.ts +48 -0
  71. package/util/encode-copy-binary.js +112 -0
  72. package/util/fast-parsefloat.d.ts +15 -0
  73. package/util/fast-parsefloat.js +141 -0
  74. package/util/get-parsers.js +4 -8
  75. package/util/parse-datetime.js +7 -0
  76. package/util/parse-pg-timestamp.d.ts +31 -0
  77. package/util/parse-pg-timestamp.js +199 -0
  78. package/util/parse-row.js +120 -0
  79. package/util/row-decoder.d.ts +49 -0
  80. package/util/row-decoder.js +63 -0
@@ -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
- const data = [
244
- this._frontend.getParseMessage(args.parse),
245
- this._frontend.getBindMessage(args.bind),
246
- this._frontend.getDescribeMessage(args.describe),
247
- this._frontend.getExecuteMessage(args.execute),
248
- this._frontend.getSyncMessage(),
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
- const data = [
278
- this._frontend.getBindMessage(args.bind),
279
- this._frontend.getExecuteMessage(args.execute),
280
- this._frontend.getSyncMessage(),
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
- if (!this._socket || !this._socket.writable)
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
- for (let i = 0; i < l; i++)
771
- this._pendingWrites.push({
772
- data: data[i],
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
- else
777
- this._pendingWrites.push({ data, cb });
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
- * Writes COPY bytes straight through instead of queueing them for the
790
- * next tick like _send() does. Two reasons: during a copy the capture
791
- * queue holds exactly one entry (the Query that opened it), so _send()'s
792
- * "batch only when more than one request is in flight" test would never
793
- * fire and every chunk would be written on its own anyway; and the
794
- * caller needs write()'s own return value to know when to stop, which a
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
  }
@@ -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.readBuffer(length) };
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.readBuffer(length).toString('utf8');
137
+ out[column.name] = io.readBytes(length).toString('utf8');
138
138
  }
139
139
  }
140
140
  return out;
@@ -1,37 +1,19 @@
1
- import { BufferReader } from './buffer-reader.js';
1
+ import { FlexyBuffer } from 'flexy-buffer';
2
2
  export interface SmartBufferConfig {
3
3
  pageSize?: number;
4
4
  maxLength?: number;
5
- houseKeepInterval?: number;
5
+ houseKeepMs?: number;
6
6
  }
7
- export declare class SmartBuffer extends BufferReader {
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
- get capacity(): number;
18
- get length(): number;
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
  }
@@ -1,172 +1,29 @@
1
+ import { FlexyBuffer } from 'flexy-buffer';
1
2
  import * as os from 'os';
2
- import { writeBigUInt64BE } from '../util/bigint-methods.js';
3
- import { BufferReader } from './buffer-reader.js';
4
- export class SmartBuffer extends BufferReader {
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(Buffer.allocUnsafe(cfg?.pageSize || SmartBuffer.DEFAULT_PAGE_SIZE));
15
- this._houseKeepInterval = cfg?.houseKeepInterval || 5000;
16
- this.pageSize = this.buffer.length;
17
- this.maxSize = cfg?.maxLength || SmartBuffer.DEFAULT_MAX_SIZE;
18
- this._length = 0;
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 len = str ? Buffer.byteLength(str, encoding) : 0;
68
- this.ensureSize(len + 1);
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
- const len = str ? Buffer.byteLength(str, encoding) : 0;
78
- this.ensureSize(len + 4);
79
- this.writeInt32BE(str == null ? -1 : len);
80
- if (str) {
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
- export type DecodeBinaryFunction = (buf: Buffer, offset: number, options: DataMappingOptions & Record<string, any>) => any;
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
+ }