postgrejs 2.23.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +162 -1
  2. package/LICENSE +26 -17
  3. package/README.md +195 -175
  4. package/connection/connection.d.ts +152 -13
  5. package/connection/connection.js +296 -49
  6. package/connection/copy-stream.d.ts +88 -0
  7. package/connection/copy-stream.js +244 -0
  8. package/connection/cursor.js +20 -13
  9. package/connection/intl-connection.d.ts +68 -3
  10. package/connection/intl-connection.js +414 -57
  11. package/connection/large-object.d.ts +65 -0
  12. package/connection/large-object.js +151 -0
  13. package/connection/logical-replication.d.ts +121 -0
  14. package/connection/logical-replication.js +279 -0
  15. package/connection/pool.d.ts +61 -4
  16. package/connection/pool.js +177 -29
  17. package/connection/portal.d.ts +18 -4
  18. package/connection/portal.js +53 -59
  19. package/connection/prepared-statement.d.ts +22 -1
  20. package/connection/prepared-statement.js +117 -97
  21. package/data-type-map.d.ts +2 -2
  22. package/data-type-map.js +4 -2
  23. package/data-types/bool-type.js +14 -10
  24. package/data-types/box-type.js +15 -8
  25. package/data-types/bytea-type.js +9 -5
  26. package/data-types/char-type.js +11 -5
  27. package/data-types/circle-type.js +15 -7
  28. package/data-types/date-type.js +25 -19
  29. package/data-types/float4-type.js +10 -3
  30. package/data-types/float8-type.js +12 -3
  31. package/data-types/int2-type.js +9 -4
  32. package/data-types/int2-vector-type.js +10 -4
  33. package/data-types/int4-type.js +11 -5
  34. package/data-types/int8-type.js +22 -7
  35. package/data-types/json-type.js +17 -8
  36. package/data-types/jsonb-type.js +20 -14
  37. package/data-types/lseg-type.js +16 -8
  38. package/data-types/numeric-type.d.ts +8 -0
  39. package/data-types/numeric-type.js +112 -8
  40. package/data-types/oid-type.d.ts +0 -1
  41. package/data-types/oid-type.js +9 -10
  42. package/data-types/oid-vector-type.d.ts +9 -0
  43. package/data-types/oid-vector-type.js +51 -0
  44. package/data-types/point-type.js +14 -6
  45. package/data-types/time-type.js +20 -13
  46. package/data-types/timestamp-type.js +32 -25
  47. package/data-types/timestamptz-type.d.ts +1 -1
  48. package/data-types/timestamptz-type.js +34 -21
  49. package/data-types/uuid-type.js +21 -11
  50. package/data-types/varchar-type.js +16 -4
  51. package/index.d.ts +5 -0
  52. package/index.js +5 -0
  53. package/interfaces/data-type.d.ts +5 -3
  54. package/interfaces/database-connection-params.d.ts +95 -0
  55. package/interfaces/query-options.d.ts +17 -0
  56. package/interfaces/script-execute-options.d.ts +17 -0
  57. package/package.json +3 -4
  58. package/protocol/backend.d.ts +6 -1
  59. package/protocol/backend.js +70 -45
  60. package/protocol/buffer-reader.d.ts +0 -2
  61. package/protocol/buffer-reader.js +3 -10
  62. package/protocol/cert-signature.d.ts +12 -0
  63. package/protocol/cert-signature.js +114 -0
  64. package/protocol/database-error.js +1 -0
  65. package/protocol/frontend.d.ts +16 -0
  66. package/protocol/frontend.js +58 -6
  67. package/protocol/pg-socket.d.ts +221 -10
  68. package/protocol/pg-socket.js +539 -102
  69. package/protocol/pgoutput.d.ts +76 -0
  70. package/protocol/pgoutput.js +155 -0
  71. package/protocol/protocol.d.ts +3 -4
  72. package/protocol/protocol.js +1 -1
  73. package/protocol/sasl.d.ts +11 -1
  74. package/protocol/sasl.js +35 -4
  75. package/protocol/smart-buffer.d.ts +0 -2
  76. package/protocol/smart-buffer.js +11 -16
  77. package/types.d.ts +5 -3
  78. package/util/abort-signal.d.ts +24 -0
  79. package/util/abort-signal.js +63 -0
  80. package/util/array-calculatedim.js +3 -1
  81. package/util/bigint-methods.js +1 -1
  82. package/util/connection-config.js +77 -3
  83. package/util/decode-binaryarray.d.ts +1 -1
  84. package/util/decode-binaryarray.js +15 -6
  85. package/util/encode-binaryarray.js +7 -3
  86. package/util/escape-identifier.d.ts +14 -0
  87. package/util/escape-identifier.js +32 -0
  88. package/util/escape-literal.js +3 -0
  89. package/util/fast-parseint.d.ts +1 -0
  90. package/util/fast-parseint.js +12 -17
  91. package/util/format-datetime.d.ts +10 -0
  92. package/util/format-datetime.js +71 -0
  93. package/util/get-parsers.js +31 -10
  94. package/util/parse-array.js +2 -2
  95. package/util/parse-bytea.d.ts +2 -0
  96. package/util/parse-bytea.js +83 -0
  97. package/util/parse-datetime.d.ts +3 -1
  98. package/util/parse-datetime.js +86 -17
  99. package/util/parse-row.d.ts +3 -1
  100. package/util/parse-row.js +60 -5
  101. package/util/parse-time.js +8 -1
  102. package/util/sql-tag.d.ts +60 -0
  103. package/util/sql-tag.js +202 -0
  104. package/util/starts-transaction.d.ts +38 -0
  105. package/util/starts-transaction.js +148 -0
  106. package/util/stringify-arrayliteral.js +13 -1
  107. package/util/wrap-row-description.js +13 -4
  108. package/util/convert-row-to-object.d.ts +0 -2
  109. package/util/convert-row-to-object.js +0 -9
@@ -0,0 +1,151 @@
1
+ import { Readable, Writable } from 'node:stream';
2
+ import { DataTypeOIDs } from '../constants.js';
3
+ import { BindParam } from './bind-param.js';
4
+ /** Open modes, as PostgreSQL's own INV_READ / INV_WRITE constants. */
5
+ export const LargeObjectMode = {
6
+ read: 0x00040000,
7
+ write: 0x00020000,
8
+ readWrite: 0x00060000,
9
+ };
10
+ /**
11
+ * A PostgreSQL large object: binary data stored outside the row it belongs
12
+ * to and reached through a file-like interface, so it can be seeked into
13
+ * and read a piece at a time rather than loaded whole the way a `bytea`
14
+ * column must be. Up to 4TB, against roughly 1GB for `bytea`.
15
+ *
16
+ * ```ts
17
+ * const lo = await connection.createLargeObject();
18
+ * await pipeline(fs.createReadStream('video.mp4'), lo.writable());
19
+ * await lo.close();
20
+ * // keep lo.oid somewhere - a large object belongs to no row
21
+ * ```
22
+ *
23
+ * The descriptor a large object is opened with only lives as long as the
24
+ * transaction that opened it, so one is started if the connection is not
25
+ * already in one, and close() commits that one - but never a transaction
26
+ * the caller started, which stays theirs to finish.
27
+ */
28
+ export class LargeObject {
29
+ oid;
30
+ _connection;
31
+ _fd;
32
+ /** True when this object opened the transaction and so should commit it. */
33
+ _ownsTransaction;
34
+ _closed = false;
35
+ constructor(connection, oid, fd, ownsTransaction) {
36
+ this._connection = connection;
37
+ this.oid = oid;
38
+ this._fd = fd;
39
+ this._ownsTransaction = ownsTransaction;
40
+ }
41
+ /** Reads at most `length` bytes from the current position. */
42
+ async read(length) {
43
+ this._assertOpen();
44
+ const r = await this._connection.query('select loread($1, $2) as data', {
45
+ params: [
46
+ new BindParam(DataTypeOIDs.int4, this._fd),
47
+ new BindParam(DataTypeOIDs.int4, length),
48
+ ],
49
+ });
50
+ // loread() always returns a (possibly zero-length) bytea, never SQL
51
+ // NULL, so this fallback is a defensive backstop rather than a path
52
+ // real server behavior takes.
53
+ return r.rows?.[0][0] ?? Buffer.alloc(0);
54
+ }
55
+ /** Writes at the current position and returns how many bytes went in. */
56
+ async write(data) {
57
+ this._assertOpen();
58
+ const r = await this._connection.query('select lowrite($1, $2) as n', {
59
+ params: [
60
+ new BindParam(DataTypeOIDs.int4, this._fd),
61
+ new BindParam(DataTypeOIDs.bytea, data),
62
+ ],
63
+ });
64
+ return Number(r.rows?.[0][0]);
65
+ }
66
+ /** Moves the read/write position. `whence`: 0 start, 1 current, 2 end. */
67
+ async seek(offset, whence = 0) {
68
+ this._assertOpen();
69
+ const r = await this._connection.query('select lo_lseek64($1, $2, $3) as pos', {
70
+ params: [
71
+ new BindParam(DataTypeOIDs.int4, this._fd),
72
+ new BindParam(DataTypeOIDs.int8, BigInt(offset)),
73
+ new BindParam(DataTypeOIDs.int4, whence),
74
+ ],
75
+ });
76
+ return BigInt(r.rows?.[0][0]);
77
+ }
78
+ /** The current read/write position. */
79
+ async tell() {
80
+ this._assertOpen();
81
+ const r = await this._connection.query('select lo_tell64($1) as pos', {
82
+ params: [new BindParam(DataTypeOIDs.int4, this._fd)],
83
+ });
84
+ return BigInt(r.rows?.[0][0]);
85
+ }
86
+ /** Total size in bytes; leaves the position where it found it. */
87
+ async size() {
88
+ const here = await this.tell();
89
+ const end = await this.seek(0, 2);
90
+ await this.seek(here, 0);
91
+ return end;
92
+ }
93
+ /** Cuts the object down to `length` bytes. */
94
+ async truncate(length) {
95
+ this._assertOpen();
96
+ await this._connection.query('select lo_truncate64($1, $2)', {
97
+ params: [
98
+ new BindParam(DataTypeOIDs.int4, this._fd),
99
+ new BindParam(DataTypeOIDs.int8, BigInt(length)),
100
+ ],
101
+ });
102
+ }
103
+ /** Reads from the current position to the end. */
104
+ readable(options) {
105
+ const chunkSize = options?.chunkSize || 65536;
106
+ const self = this;
107
+ return new Readable({
108
+ async read() {
109
+ try {
110
+ const chunk = await self.read(chunkSize);
111
+ // A short read is not the end; an empty one is.
112
+ this.push(chunk.length ? chunk : null);
113
+ }
114
+ catch (e) {
115
+ this.destroy(e);
116
+ }
117
+ },
118
+ });
119
+ }
120
+ /** Writes from the current position onwards. */
121
+ writable(options) {
122
+ const self = this;
123
+ return new Writable({
124
+ highWaterMark: options?.chunkSize || 65536,
125
+ write(chunk, _encoding, callback) {
126
+ self
127
+ .write(chunk)
128
+ .then(() => callback())
129
+ .catch(callback);
130
+ },
131
+ });
132
+ }
133
+ /**
134
+ * Closes the descriptor, and commits the transaction if this object
135
+ * started it. Safe to call twice.
136
+ */
137
+ async close() {
138
+ if (this._closed)
139
+ return;
140
+ this._closed = true;
141
+ await this._connection.query('select lo_close($1)', {
142
+ params: [new BindParam(DataTypeOIDs.int4, this._fd)],
143
+ });
144
+ if (this._ownsTransaction)
145
+ await this._connection.commit();
146
+ }
147
+ _assertOpen() {
148
+ if (this._closed)
149
+ throw new Error(`Large object ${this.oid} is already closed`);
150
+ }
151
+ }
@@ -0,0 +1,121 @@
1
+ import type { ConnectionConfiguration } from '../interfaces/database-connection-params.js';
2
+ import type { CaptureCallback } from '../protocol/pg-socket.js';
3
+ import { PgOutputDecoder, type PgOutputMessage, type PgOutputRelation, type TupleValues } from '../protocol/pgoutput.js';
4
+ import { SafeEventEmitter } from '../safe-event-emitter.js';
5
+ import { IntlConnection } from './intl-connection.js';
6
+ export type ChangeCommand = 'insert' | 'update' | 'delete' | 'truncate';
7
+ export interface Change {
8
+ command: ChangeCommand;
9
+ /** Schema-qualified, e.g. `public.users`. */
10
+ table: string;
11
+ schema: string;
12
+ relation: PgOutputRelation;
13
+ /** The row after the change; absent for delete and truncate. */
14
+ row?: TupleValues;
15
+ /**
16
+ * The row before the change, for update and delete. How much of it is
17
+ * present depends on the table's REPLICA IDENTITY - by default only the
18
+ * primary key.
19
+ */
20
+ oldRow?: TupleValues;
21
+ /** Where this change sits in the write-ahead log. */
22
+ lsn: bigint;
23
+ /** Commit time of the transaction it belongs to. */
24
+ timestamp?: Date;
25
+ }
26
+ export interface LogicalReplicationOptions extends ConnectionConfiguration {
27
+ /** Publication(s) to subscribe to; they must already exist on the server. */
28
+ publication: string | string[];
29
+ /**
30
+ * Replication slot name. A slot is what makes the server keep WAL until it
31
+ * is confirmed, so the default is a temporary one, dropped when the
32
+ * connection ends. A permanent slot outlives the process and keeps WAL
33
+ * piling up until it is either read or dropped - which is how a disk fills.
34
+ */
35
+ slot?: string;
36
+ /** Keep the slot after disconnecting. Off by default, deliberately. */
37
+ permanent?: boolean;
38
+ /** Only these commands are yielded. */
39
+ commands?: ChangeCommand[];
40
+ /** Only these tables, as `users` or `public.users`. */
41
+ tables?: string[];
42
+ /** Anything the two above cannot express. */
43
+ filter?: (change: Change) => boolean;
44
+ /** How often to tell the server where we are, in ms (default 10000). */
45
+ keepAliveIntervalMs?: number;
46
+ }
47
+ /**
48
+ * Streams row-level changes out of PostgreSQL as they are committed.
49
+ *
50
+ * It owns its own connection rather than borrowing one: a connection that
51
+ * has run START_REPLICATION is in a streaming mode it never leaves, so none
52
+ * of Connection's own API would work on it afterwards.
53
+ *
54
+ * ```ts
55
+ * const sub = new LogicalReplication({ ...config, publication: 'my_pub' });
56
+ * for await (const change of sub) {
57
+ * if (change.command === 'insert') await index.add(change.row);
58
+ * }
59
+ * ```
60
+ *
61
+ * The loop is the backpressure: nothing is read from the socket while the
62
+ * body is running, so a slow consumer slows the stream instead of filling
63
+ * memory. Leaving the loop - by `break`, by throwing, or by closing the
64
+ * subscription - ends it cleanly.
65
+ *
66
+ * Each change is confirmed to the server when the *next* one is pulled, not
67
+ * when it is handed over. A consumer that crashes half way through therefore
68
+ * sees that change again on the next run rather than losing it, and no
69
+ * caller has to remember to acknowledge anything.
70
+ */
71
+ export declare class LogicalReplication extends SafeEventEmitter {
72
+ readonly options: LogicalReplicationOptions;
73
+ protected _intlCon?: IntlConnection;
74
+ protected readonly _decoder: PgOutputDecoder;
75
+ protected readonly _queue: Change[];
76
+ protected _waiting?: (value: void) => void;
77
+ protected _finished: boolean;
78
+ protected _error?: Error;
79
+ protected _slotName: string;
80
+ protected _slotCreated: boolean;
81
+ protected _keepAliveTimer?: NodeJS.Timeout;
82
+ protected _receivedLsn: bigint;
83
+ protected _confirmedLsn: bigint;
84
+ protected _pendingLsn: bigint;
85
+ protected _commitTime?: Date;
86
+ protected _streamDone?: Promise<any>;
87
+ constructor(options: LogicalReplicationOptions);
88
+ /** The slot this subscription is reading from. */
89
+ get slot(): string;
90
+ /** The last position confirmed to the server. */
91
+ get confirmedLsn(): string;
92
+ /**
93
+ * Opens the connection, creates the slot if needed and starts streaming.
94
+ * Iterating calls this on its own, so it is only needed to start early.
95
+ */
96
+ start(): Promise<void>;
97
+ /** Ends the subscription and closes its connection. */
98
+ close(): Promise<void>;
99
+ /**
100
+ * Confirms everything up to the last change handed out. Called for you
101
+ * when the next change is pulled; use it directly only to confirm sooner.
102
+ */
103
+ ack(): Promise<void>;
104
+ [Symbol.asyncIterator](): AsyncIterableIterator<Change>;
105
+ protected readonly _capture: CaptureCallback;
106
+ protected _handleCopyData(data: Buffer): void;
107
+ /**
108
+ * Turns one decoded message into the changes it represents - none for
109
+ * bookkeeping messages, one for a row change, and one per table for a
110
+ * truncate, so that a `tables` filter behaves the same way there.
111
+ */
112
+ protected _toChanges(message: PgOutputMessage, lsn: bigint): Change[];
113
+ protected _build(command: ChangeCommand, relation: PgOutputRelation, lsn: bigint, row?: TupleValues, oldRow?: TupleValues): Change;
114
+ /** Client-side filtering, on top of whatever the publication already limits. */
115
+ protected _accepts(change: Change): boolean;
116
+ /** Standby status update: where we have written, flushed and applied. */
117
+ protected _sendStatus(replyRequested: boolean): void;
118
+ protected _resume(): void;
119
+ protected _wake(): void;
120
+ protected _fail(err: Error): void;
121
+ }
@@ -0,0 +1,279 @@
1
+ import { ConnectionState } from '../constants.js';
2
+ import { formatLsn, PgOutputDecoder, } from '../protocol/pgoutput.js';
3
+ import { Protocol } from '../protocol/protocol.js';
4
+ import { SafeEventEmitter } from '../safe-event-emitter.js';
5
+ import { escapeLiteral } from '../util/escape-literal.js';
6
+ import { IntlConnection } from './intl-connection.js';
7
+ const PG_EPOCH_MS = Date.UTC(2000, 0, 1);
8
+ const DEFAULT_KEEPALIVE_MS = 10000;
9
+ /**
10
+ * Streams row-level changes out of PostgreSQL as they are committed.
11
+ *
12
+ * It owns its own connection rather than borrowing one: a connection that
13
+ * has run START_REPLICATION is in a streaming mode it never leaves, so none
14
+ * of Connection's own API would work on it afterwards.
15
+ *
16
+ * ```ts
17
+ * const sub = new LogicalReplication({ ...config, publication: 'my_pub' });
18
+ * for await (const change of sub) {
19
+ * if (change.command === 'insert') await index.add(change.row);
20
+ * }
21
+ * ```
22
+ *
23
+ * The loop is the backpressure: nothing is read from the socket while the
24
+ * body is running, so a slow consumer slows the stream instead of filling
25
+ * memory. Leaving the loop - by `break`, by throwing, or by closing the
26
+ * subscription - ends it cleanly.
27
+ *
28
+ * Each change is confirmed to the server when the *next* one is pulled, not
29
+ * when it is handed over. A consumer that crashes half way through therefore
30
+ * sees that change again on the next run rather than losing it, and no
31
+ * caller has to remember to acknowledge anything.
32
+ */
33
+ export class LogicalReplication extends SafeEventEmitter {
34
+ options;
35
+ _intlCon;
36
+ _decoder = new PgOutputDecoder();
37
+ _queue = [];
38
+ _waiting;
39
+ _finished = false;
40
+ _error;
41
+ _slotName;
42
+ _slotCreated = false;
43
+ _keepAliveTimer;
44
+ _receivedLsn = 0n;
45
+ _confirmedLsn = 0n;
46
+ _pendingLsn = 0n;
47
+ _commitTime;
48
+ _streamDone;
49
+ constructor(options) {
50
+ super();
51
+ if (!options.publication)
52
+ throw new TypeError('LogicalReplication requires a publication');
53
+ this.options = options;
54
+ this._slotName =
55
+ options.slot ||
56
+ 'postgrejs_' + Math.random().toString(36).substring(2, 10);
57
+ }
58
+ /** The slot this subscription is reading from. */
59
+ get slot() {
60
+ return this._slotName;
61
+ }
62
+ /** The last position confirmed to the server. */
63
+ get confirmedLsn() {
64
+ return formatLsn(this._confirmedLsn);
65
+ }
66
+ /**
67
+ * Opens the connection, creates the slot if needed and starts streaming.
68
+ * Iterating calls this on its own, so it is only needed to start early.
69
+ */
70
+ async start() {
71
+ if (this._intlCon)
72
+ return;
73
+ const intlCon = (this._intlCon = new IntlConnection({
74
+ ...this.options,
75
+ replication: 'database',
76
+ }));
77
+ await intlCon.connect();
78
+ if (!this.options.slot || this.options.permanent === false) {
79
+ await intlCon.execute(`CREATE_REPLICATION_SLOT ${this._slotName}` +
80
+ (this.options.permanent ? '' : ' TEMPORARY') +
81
+ ' LOGICAL pgoutput');
82
+ this._slotCreated = true;
83
+ }
84
+ const publications = (Array.isArray(this.options.publication)
85
+ ? this.options.publication
86
+ : [this.options.publication]).join(',');
87
+ const sql = `START_REPLICATION SLOT ${this._slotName} LOGICAL 0/0 ` +
88
+ `(proto_version '1', publication_names ${escapeLiteral(publications)})`;
89
+ // Deliberately not awaited here: this resolves only when streaming
90
+ // ends. Stored so close() can wait for it after asking the server to
91
+ // stop, rather than sending a new query while COPY BOTH is still active.
92
+ this._streamDone = intlCon.socket
93
+ .sendQueryMessage(sql, this._capture)
94
+ .catch(err => this._fail(err));
95
+ this._keepAliveTimer = setInterval(() => this._sendStatus(false), this.options.keepAliveIntervalMs || DEFAULT_KEEPALIVE_MS);
96
+ this._keepAliveTimer.unref();
97
+ }
98
+ /** Ends the subscription and closes its connection. */
99
+ async close() {
100
+ const wasStreaming = !this._finished;
101
+ this._finished = true;
102
+ this._wake();
103
+ if (this._keepAliveTimer)
104
+ clearInterval(this._keepAliveTimer);
105
+ this._keepAliveTimer = undefined;
106
+ const intlCon = this._intlCon;
107
+ this._intlCon = undefined;
108
+ if (!intlCon)
109
+ return;
110
+ if (wasStreaming && intlCon.state === ConnectionState.READY) {
111
+ // Still mid-COPY-BOTH: a new Query message (like the DROP below) is
112
+ // invalid until the server has acknowledged the end of streaming and
113
+ // returned to normal command processing. A paused socket (from
114
+ // backpressure) would never see that acknowledgment at all.
115
+ intlCon.socket.resume();
116
+ intlCon.socket.sendCopyDone();
117
+ await this._streamDone?.catch(() => undefined);
118
+ }
119
+ if (this._slotCreated &&
120
+ this.options.permanent &&
121
+ intlCon.state === ConnectionState.READY) {
122
+ await intlCon
123
+ .execute(`DROP_REPLICATION_SLOT ${this._slotName}`)
124
+ .catch(() => undefined);
125
+ }
126
+ await intlCon.close().catch(() => undefined);
127
+ }
128
+ /**
129
+ * Confirms everything up to the last change handed out. Called for you
130
+ * when the next change is pulled; use it directly only to confirm sooner.
131
+ */
132
+ async ack() {
133
+ this._confirmedLsn = this._pendingLsn;
134
+ this._sendStatus(false);
135
+ }
136
+ async *[Symbol.asyncIterator]() {
137
+ await this.start();
138
+ try {
139
+ while (true) {
140
+ if (this._error)
141
+ throw this._error;
142
+ const change = this._queue.shift();
143
+ if (change) {
144
+ this._confirmedLsn = this._pendingLsn;
145
+ this._pendingLsn = change.lsn;
146
+ yield change;
147
+ continue;
148
+ }
149
+ if (this._finished)
150
+ return;
151
+ this._resume();
152
+ await new Promise(resolve => (this._waiting = resolve));
153
+ }
154
+ }
155
+ finally {
156
+ await this.close();
157
+ }
158
+ }
159
+ _capture = (code, msg, done) => {
160
+ switch (code) {
161
+ case Protocol.BackendMessageCode.CopyBothResponse:
162
+ break;
163
+ case Protocol.BackendMessageCode.CopyData:
164
+ this._handleCopyData(msg.data);
165
+ break;
166
+ case Protocol.BackendMessageCode.ErrorResponse:
167
+ this._error = msg;
168
+ break;
169
+ case Protocol.BackendMessageCode.ReadyForQuery:
170
+ this._finished = true;
171
+ done(undefined);
172
+ this._wake();
173
+ break;
174
+ default:
175
+ break;
176
+ }
177
+ };
178
+ _handleCopyData(data) {
179
+ const kind = String.fromCharCode(data[0]);
180
+ if (kind === 'k') {
181
+ this._receivedLsn = data.readBigUInt64BE(1);
182
+ if (data[17] === 1)
183
+ this._sendStatus(false);
184
+ return;
185
+ }
186
+ if (kind !== 'w')
187
+ return; // XLogData is the only other thing sent here
188
+ const walStart = data.readBigUInt64BE(1);
189
+ this._receivedLsn = data.readBigUInt64BE(9);
190
+ const message = this._decoder.decode(data.subarray(25));
191
+ const changes = this._toChanges(message, walStart);
192
+ if (!changes.length)
193
+ return;
194
+ this._queue.push(...changes);
195
+ if (this._queue.length > 1)
196
+ this._intlCon?.socket.pause();
197
+ this._wake();
198
+ }
199
+ /**
200
+ * Turns one decoded message into the changes it represents - none for
201
+ * bookkeeping messages, one for a row change, and one per table for a
202
+ * truncate, so that a `tables` filter behaves the same way there.
203
+ */
204
+ _toChanges(message, lsn) {
205
+ let change;
206
+ switch (message.kind) {
207
+ case 'commit':
208
+ this._commitTime = message.commitTime;
209
+ return [];
210
+ case 'insert':
211
+ change = this._build('insert', message.relation, lsn, message.row);
212
+ break;
213
+ case 'update':
214
+ change = this._build('update', message.relation, lsn, message.row, message.oldRow);
215
+ break;
216
+ case 'delete':
217
+ change = this._build('delete', message.relation, lsn, undefined, message.oldRow);
218
+ break;
219
+ case 'truncate':
220
+ return message.relations
221
+ .map(relation => this._build('truncate', relation, lsn))
222
+ .filter(c => this._accepts(c));
223
+ default:
224
+ return [];
225
+ }
226
+ return this._accepts(change) ? [change] : [];
227
+ }
228
+ _build(command, relation, lsn, row, oldRow) {
229
+ return {
230
+ command,
231
+ relation,
232
+ schema: relation.schema,
233
+ table: `${relation.schema}.${relation.name}`,
234
+ row,
235
+ oldRow,
236
+ lsn,
237
+ timestamp: this._commitTime,
238
+ };
239
+ }
240
+ /** Client-side filtering, on top of whatever the publication already limits. */
241
+ _accepts(change) {
242
+ const { commands, tables, filter } = this.options;
243
+ if (commands && !commands.includes(change.command))
244
+ return false;
245
+ if (tables &&
246
+ !tables.some(t => t === change.table || t === change.relation.name))
247
+ return false;
248
+ if (filter && !filter(change))
249
+ return false;
250
+ return true;
251
+ }
252
+ /** Standby status update: where we have written, flushed and applied. */
253
+ _sendStatus(replyRequested) {
254
+ const socket = this._intlCon?.socket;
255
+ if (!socket)
256
+ return;
257
+ const buf = Buffer.allocUnsafe(34);
258
+ buf[0] = 0x72; // 'r'
259
+ buf.writeBigUInt64BE(this._receivedLsn, 1);
260
+ buf.writeBigUInt64BE(this._confirmedLsn, 9);
261
+ buf.writeBigUInt64BE(this._confirmedLsn, 17);
262
+ buf.writeBigInt64BE(BigInt(Date.now() - PG_EPOCH_MS) * 1000n, 25);
263
+ buf[33] = replyRequested ? 1 : 0;
264
+ socket.sendCopyData(buf);
265
+ }
266
+ _resume() {
267
+ this._intlCon?.socket.resume();
268
+ }
269
+ _wake() {
270
+ const waiting = this._waiting;
271
+ this._waiting = undefined;
272
+ waiting?.();
273
+ }
274
+ _fail(err) {
275
+ this._error = this._error || err;
276
+ this._finished = true;
277
+ this._wake();
278
+ }
279
+ }
@@ -6,27 +6,54 @@ import type { ScriptExecuteOptions } from '../interfaces/script-execute-options.
6
6
  import type { ScriptResult } from '../interfaces/script-result.js';
7
7
  import type { StatementPrepareOptions } from '../interfaces/statement-prepare-options.js';
8
8
  import { SafeEventEmitter } from '../safe-event-emitter.js';
9
+ import { QueryRequest } from '../util/sql-tag.js';
9
10
  import { Connection, type NotificationCallback } from './connection.js';
10
11
  import { IntlConnection } from './intl-connection.js';
11
12
  import type { PreparedStatement } from './prepared-statement.js';
13
+ export interface PoolPipelineOptions {
14
+ pipeline?: boolean;
15
+ }
16
+ export type PoolQueryOptions = QueryOptions & PoolPipelineOptions;
17
+ export type PoolScriptExecuteOptions = ScriptExecuteOptions & PoolPipelineOptions;
18
+ /**
19
+ * One pooled connection the pipelined path is currently borrowing.
20
+ *
21
+ * A slot joins the list the moment its acquire starts rather than when it
22
+ * finishes, so callers arriving while a connection is still being opened
23
+ * queue onto `promise` instead of starting a second acquire of their own.
24
+ * `load` counts queries handed to this slot that have not settled yet - it
25
+ * is incremented synchronously, at selection time, which `runningQueryCount`
26
+ * cannot be: everything in a Promise.all() burst picks its connection before
27
+ * any of them reaches execute(), so at that point every connection still
28
+ * reports zero running queries.
29
+ */
30
+ interface PipelineSlot {
31
+ connection?: Connection;
32
+ promise: Promise<Connection>;
33
+ load: number;
34
+ }
12
35
  export declare class Pool extends SafeEventEmitter {
13
36
  protected readonly _pool: LightningPool<IntlConnection>;
14
37
  protected readonly _notificationListeners: SafeEventEmitter;
15
38
  protected _notificationConnection?: Connection;
39
+ protected readonly _pipelineSlots: PipelineSlot[];
40
+ protected _pipelineMaxQueries: number;
41
+ protected _pipelineMaxConnections: number;
16
42
  readonly config: PoolConfiguration;
17
43
  constructor(config?: PoolConfiguration | string);
18
44
  /**
19
- * Returns number of connections that are currently acquired
45
+ * Returns the number of connections that are currently acquired
20
46
  */
21
47
  get acquiredConnections(): number;
22
48
  /**
23
- * Returns number of unused connections in the pool
49
+ * Returns the number of unused connections in the pool
24
50
  */
25
51
  get idleConnections(): number;
26
52
  /**
27
53
  * Returns total number of connections in the pool regardless of whether they are idle or in use
28
54
  */
29
55
  get totalConnections(): number;
56
+ start(): void;
30
57
  /**
31
58
  * Obtains a connection from the connection pool
32
59
  */
@@ -38,15 +65,45 @@ export declare class Pool extends SafeEventEmitter {
38
65
  /**
39
66
  * Executes a script
40
67
  */
41
- execute(sql: string, options?: ScriptExecuteOptions): Promise<ScriptResult>;
68
+ execute(sql: string | QueryRequest, options?: PoolScriptExecuteOptions): Promise<ScriptResult>;
42
69
  /**
43
70
  * Executes a query
44
71
  */
45
- query(sql: string, options?: QueryOptions): Promise<QueryResult>;
72
+ query(sql: string | QueryRequest, options?: PoolQueryOptions): Promise<QueryResult>;
46
73
  prepare(sql: string, options?: StatementPrepareOptions): Promise<PreparedStatement>;
47
74
  release(connection: Connection): Promise<void>;
48
75
  listen(channel: string, callback: NotificationCallback): Promise<void>;
49
76
  unListen(channel: string): Promise<void>;
50
77
  unListenAll(): Promise<void>;
78
+ protected _canPipeline(pipeline: boolean | undefined, sql: string, autoCommit: boolean | undefined, signal: AbortSignal | undefined): boolean;
79
+ /**
80
+ * Picks the pooled connection a pipelined query should ride on, or
81
+ * undefined when the caller should take a connection of its own.
82
+ *
83
+ * Deliberately synchronous: it must never await, because everything in a
84
+ * Promise.all() burst has to pick its connection and reach execute()
85
+ * within a single event loop tick for PgSocket to coalesce the whole
86
+ * burst into one write per connection. Opening another connection is
87
+ * started here but never awaited by the caller that triggered it.
88
+ */
89
+ protected _acquireShared(): PipelineSlot | undefined;
90
+ /**
91
+ * Starts borrowing one more connection and returns its slot right away,
92
+ * already carrying the caller that triggered the growth.
93
+ */
94
+ protected _growPipeline(): PipelineSlot;
95
+ /**
96
+ * Stops new queries joining a slot. Queries already on it are unaffected.
97
+ */
98
+ protected _dropPipelineSlot(slot: PipelineSlot): void;
99
+ /**
100
+ * Backstop for a transaction startsTransaction() could not see coming -
101
+ * one opened inside a stored procedure, for instance. The connection is
102
+ * still sound, but nothing else may join it while it is in a
103
+ * transaction, so the slot goes away and the connection returns to the
104
+ * pool once its own queries finish.
105
+ */
106
+ protected _checkSharedTransaction(slot: PipelineSlot, connection: Connection): void;
51
107
  protected _initNotificationConnection(): Promise<void>;
52
108
  }
109
+ export {};