postgrejs 3.0.1 → 3.0.3

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 CHANGED
@@ -1,6 +1,27 @@
1
1
  ## Changelog
2
2
 
3
- ### [v3.0.1](https://github.com/panates/postgrejs/compare/v3.0.0...v3.0.1) -
3
+ ### [v3.0.3](https://github.com/panates/postgrejs/compare/v3.0.2...v3.0.3) -
4
+
5
+ ### [v3.0.2](https://github.com/panates/postgrejs/compare/v3.0.1...v3.0.2) - 7 September 2026
6
+
7
+ #### 🚀 New Features
8
+
9
+ - feat: add Docker setup for PostgreSQL with SSL support @Eray Hanoğlu
10
+ - feat: support nested startTransaction()/commit() and savepoint()/releaseSavepoint() @Eray Hanoğlu
11
+ - feat: make execute()/query() timing an opt-in, default-off option @Eray Hanoğlu
12
+
13
+ #### 🪲 Fixes
14
+
15
+ - fix: raise warmup for Simple Query benchmark, regenerate BENCHMARKS.md @Eray Hanoğlu
16
+ - fix: enlarge benchmark charts so GitHub's Mermaid overlay doesn't swallow them @Eray Hanoğlu
17
+ - fix: replace deprecated substr() with substring() @Eray Hanoğlu
18
+
19
+ #### ⚡️ Performance Improvments
20
+
21
+ - perf: remove needless async wrapping and coercion from execute()/query() hot path @Eray Hanoğlu
22
+ - perf: hoist loop-invariant reads in Frontend/numeric encoders @Eray Hanoğlu
23
+
24
+ ### [v3.0.1](https://github.com/panates/postgrejs/compare/v3.0.0...v3.0.1) - 7 September 2026
4
25
 
5
26
  - Dev
6
27
  - Dev
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <!--suppress HtmlDeprecatedAttribute -->
2
2
  <p align="center">
3
3
  <a href="https://postgrejs.panates.com/" target="_blank" rel="noopener noreferrer">
4
- <img src="https://postgrejs.panates.com/img/postgrejs-header-block.png" width="800" height="446" alt="PostgreJS Logo" />
4
+ <img src="https://postgrejs.panates.com/img/postgrejs-header-block.webp" width="800" height="446" alt="PostgreJS Logo" />
5
5
  </a>
6
6
  </p>
7
7
 
@@ -126,7 +126,7 @@ in · 🟡 partial or needs a separate package · ❌ not supported.
126
126
  | Per-query type mapping | ✅ | ❌ <sup>6</sup> | ❌ <sup>6</sup> |
127
127
  | Query cancellation | ✅ AbortSignal | ✅ | ✅ |
128
128
  | Per-query timeout | ✅ AbortSignal | ✅ | ❌ <sup>7</sup> |
129
- | Reference counters | Connection · Statement | ❌ | ❌ |
129
+ | Reference counters | Connection / Statement | ❌ | ❌ |
130
130
  | Caller kept in async error stacks | ✅ | 🟡 <sup>8</sup> | 🟡 <sup>9</sup> |
131
131
  | Error located in the SQL text | ✅ line and mark | 🟡 offset | 🟡 offset |
132
132
  | TC39 Explicit Resource Management | ✅ | ❌ | ❌ |
@@ -157,9 +157,11 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
157
157
  */
158
158
  startTransaction(): Promise<void>;
159
159
  /**
160
- * Commits current transaction
160
+ * Commits current transaction.
161
+ * @param immediate - Commits right away, ignoring how many nested
162
+ * startTransaction() calls are still unmatched by a commit().
161
163
  */
162
- commit(): Promise<void>;
164
+ commit(immediate?: boolean): Promise<void>;
163
165
  /**
164
166
  * Rolls back current transaction
165
167
  */
@@ -206,8 +208,11 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
206
208
  /**
207
209
  * Releases savepoint
208
210
  * @param name {string} - Name of the savepoint
211
+ * @param immediate - Releases right away, ignoring how many nested
212
+ * savepoint() calls under this name are still unmatched by a
213
+ * releaseSavepoint().
209
214
  */
210
- releaseSavepoint(name: string): Promise<void>;
215
+ releaseSavepoint(name: string, immediate?: boolean): Promise<void>;
211
216
  listen(channel: string, callback: NotificationCallback): Promise<void>;
212
217
  unListen(channel: string): Promise<void>;
213
218
  unListenAll(): Promise<void>;
@@ -242,6 +247,15 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
242
247
  * `asyncErrorHandling` is the per-call override (execute()/query()'s own
243
248
  * option of the same name); when omitted, the connection's own
244
249
  * `DatabaseConnectionParams.asyncErrorHandling` decides.
250
+ *
251
+ * Not `async` itself, deliberately: every branch below already returns a
252
+ * promise directly (`promise` itself, or `promise.catch(...)`'s own
253
+ * derived one) with no `await` in between, so there is nothing here that
254
+ * needs the function to be a coroutine. Marking it `async` anyway would
255
+ * still be correct, but the spec requires wrapping whatever an async
256
+ * function returns through a promise-resolve step, a genuine extra
257
+ * microtask tick on top of the one `promise`/`.catch()` already goes
258
+ * through - paid on every single query this wraps, disabled or not.
245
259
  */
246
260
  protected _captureErrorStack<T>(promise: Promise<T>, entry?: (...args: any[]) => any, asyncErrorHandling?: boolean): Promise<T>;
247
261
  [Symbol.asyncDispose](): Promise<void>;
@@ -1,4 +1,3 @@
1
- import { coerceToBoolean } from 'putil-varhelpers';
2
1
  import { ConnectionState, DataTypeOIDs } from '../constants.js';
3
2
  import { GlobalTypeMap } from '../data-type-map.js';
4
3
  import { SafeEventEmitter } from '../safe-event-emitter.js';
@@ -141,7 +140,8 @@ export class Connection extends SafeEventEmitter {
141
140
  async execute(sql, options) {
142
141
  if (typeof sql === 'object' && sql instanceof QueryRequest)
143
142
  sql = sql.stringify({ ...options, typeMap: options?.typeMap });
144
- this.emit('execute', sql, options);
143
+ if (this.listenerCount('execute'))
144
+ this.emit('execute', sql, options);
145
145
  return withAbortSignal(options?.signal, () => this._intlCon.cancel(), () => this._captureErrorStack(this._intlCon.execute(sql, options), this.execute, options?.asyncErrorHandling).catch((e) => {
146
146
  throw this._handleError(e, sql);
147
147
  }));
@@ -164,7 +164,8 @@ export class Connection extends SafeEventEmitter {
164
164
  });
165
165
  }
166
166
  /* c8 ignore stop */
167
- this.emit('query', sql, options);
167
+ if (this.listenerCount('query'))
168
+ this.emit('query', sql, options);
168
169
  return withAbortSignal(options?.signal, () => this._intlCon.cancel(), () => this._query(sql, options));
169
170
  }
170
171
  /**
@@ -198,7 +199,8 @@ export class Connection extends SafeEventEmitter {
198
199
  });
199
200
  }
200
201
  /* c8 ignore stop */
201
- this.emit('execute', sql);
202
+ if (this.listenerCount('execute'))
203
+ this.emit('execute', sql);
202
204
  return await this._captureErrorStack(this._intlCon.copyTo(sql)).catch((e) => {
203
205
  throw this._handleError(e, sql);
204
206
  });
@@ -231,7 +233,8 @@ export class Connection extends SafeEventEmitter {
231
233
  });
232
234
  }
233
235
  /* c8 ignore stop */
234
- this.emit('execute', sql);
236
+ if (this.listenerCount('execute'))
237
+ this.emit('execute', sql);
235
238
  return await this._captureErrorStack(this._intlCon.copyFrom(sql)).catch((e) => {
236
239
  throw this._handleError(e, sql);
237
240
  });
@@ -313,10 +316,12 @@ export class Connection extends SafeEventEmitter {
313
316
  return this._captureErrorStack(this._intlCon.startTransaction());
314
317
  }
315
318
  /**
316
- * Commits current transaction
319
+ * Commits current transaction.
320
+ * @param immediate - Commits right away, ignoring how many nested
321
+ * startTransaction() calls are still unmatched by a commit().
317
322
  */
318
- commit() {
319
- return this._captureErrorStack(this._intlCon.commit());
323
+ commit(immediate) {
324
+ return this._captureErrorStack(this._intlCon.commit(immediate));
320
325
  }
321
326
  /**
322
327
  * Rolls back current transaction
@@ -378,9 +383,12 @@ export class Connection extends SafeEventEmitter {
378
383
  /**
379
384
  * Releases savepoint
380
385
  * @param name {string} - Name of the savepoint
386
+ * @param immediate - Releases right away, ignoring how many nested
387
+ * savepoint() calls under this name are still unmatched by a
388
+ * releaseSavepoint().
381
389
  */
382
- releaseSavepoint(name) {
383
- return this._captureErrorStack(this._intlCon.releaseSavepoint(name));
390
+ releaseSavepoint(name, immediate) {
391
+ return this._captureErrorStack(this._intlCon.releaseSavepoint(name, immediate));
384
392
  }
385
393
  async listen(channel, callback) {
386
394
  if (!/^[A-Z]\w+$/i.test(channel))
@@ -420,8 +428,8 @@ export class Connection extends SafeEventEmitter {
420
428
  ? options.autoCommit
421
429
  : this._intlCon.config.autoCommit;
422
430
  if (!options?.cursor &&
423
- !this._intlCon.inTransaction &&
424
- effectiveAutoCommit !== false) {
431
+ effectiveAutoCommit !== false &&
432
+ !this._intlCon.inTransaction) {
425
433
  const params = options?.params?.map(prm => prm instanceof BindParam ? prm.value : prm);
426
434
  return await this._captureErrorStack(this._intlCon.queryOnce(sql, paramTypes, params, options || {}), this.query, options?.asyncErrorHandling).catch((e) => {
427
435
  throw this._handleError(e, sql);
@@ -509,11 +517,18 @@ export class Connection extends SafeEventEmitter {
509
517
  * `asyncErrorHandling` is the per-call override (execute()/query()'s own
510
518
  * option of the same name); when omitted, the connection's own
511
519
  * `DatabaseConnectionParams.asyncErrorHandling` decides.
520
+ *
521
+ * Not `async` itself, deliberately: every branch below already returns a
522
+ * promise directly (`promise` itself, or `promise.catch(...)`'s own
523
+ * derived one) with no `await` in between, so there is nothing here that
524
+ * needs the function to be a coroutine. Marking it `async` anyway would
525
+ * still be correct, but the spec requires wrapping whatever an async
526
+ * function returns through a promise-resolve step, a genuine extra
527
+ * microtask tick on top of the one `promise`/`.catch()` already goes
528
+ * through - paid on every single query this wraps, disabled or not.
512
529
  */
513
- async _captureErrorStack(promise, entry, asyncErrorHandling) {
514
- const enabled = asyncErrorHandling != null
515
- ? asyncErrorHandling
516
- : coerceToBoolean(this._intlCon.config.asyncErrorHandling, true);
530
+ _captureErrorStack(promise, entry, asyncErrorHandling) {
531
+ const enabled = asyncErrorHandling ?? this._intlCon.config.asyncErrorHandling ?? true;
517
532
  if (!enabled)
518
533
  return promise;
519
534
  const stackHolder = {};
@@ -13,6 +13,8 @@ import { CopyFromStream, CopyToStream } from './copy-stream.js';
13
13
  export declare class IntlConnection extends SafeEventEmitter {
14
14
  private _executeReusedParserCache;
15
15
  protected _refCount: number;
16
+ protected _transactionDepth: number;
17
+ protected _savepointDepths: Map<string, number>;
16
18
  protected _config: ConnectionConfiguration;
17
19
  protected _onErrorSavePoint: string;
18
20
  transactionStatus: string;
@@ -30,9 +32,45 @@ export declare class IntlConnection extends SafeEventEmitter {
30
32
  connect(): Promise<void>;
31
33
  close(): Promise<void>;
32
34
  execute(sql: string, options?: ScriptExecuteOptions, cb?: (event: string, ...args: any[]) => void): Promise<ScriptResult>;
35
+ /**
36
+ * Starts a transaction, or - if one is already running - marks a nested
37
+ * level of it. Each call increments `_transactionDepth`; only the
38
+ * outermost one actually sends BEGIN (`inTransaction` is already false at
39
+ * that point). A matching number of commit() calls is then needed to
40
+ * actually commit - see commit().
41
+ */
33
42
  startTransaction(): Promise<void>;
43
+ /**
44
+ * Starts a savepoint, or - if one under the same `name` is already open -
45
+ * marks a nested level of it. Mirrors startTransaction(): only the
46
+ * outermost call for a given name actually sends SAVEPOINT: a savepoint
47
+ * already exists under that name, re-declaring it would just stack a
48
+ * second, independent one with the same name on the server rather than
49
+ * nesting the existing one.
50
+ */
34
51
  savepoint(name: string): Promise<void>;
35
- commit(): Promise<void>;
52
+ /**
53
+ * Commits the transaction started by startTransaction() - or, when nested,
54
+ * just unwinds one level of `_transactionDepth`. The actual COMMIT is only
55
+ * sent once the depth reaches zero, i.e. once every startTransaction()
56
+ * call has a matching commit().
57
+ * @param immediate - Ignores `_transactionDepth` and commits right away,
58
+ * regardless of how many nested levels are still open.
59
+ */
60
+ commit(immediate?: boolean): Promise<void>;
61
+ /**
62
+ * Releases the savepoint created by savepoint() - or, when nested, just
63
+ * unwinds one level of that name's depth. The actual RELEASE SAVEPOINT is
64
+ * only sent once that depth reaches zero.
65
+ * @param name - Name of the savepoint.
66
+ * @param immediate - Ignores the tracked depth and releases right away.
67
+ */
68
+ releaseSavepoint(name: string, immediate?: boolean): Promise<void>;
69
+ /**
70
+ * Rolls back the whole transaction, regardless of `_transactionDepth` -
71
+ * a rollback ends the transaction outright, so there is nothing left for
72
+ * any pending, still-nested commit() call to unwind.
73
+ */
36
74
  rollback(): Promise<void>;
37
75
  /**
38
76
  * Ends the current transaction as a prepared one: it stops being tied to
@@ -53,8 +91,13 @@ export declare class IntlConnection extends SafeEventEmitter {
53
91
  commitPrepared(name: string): Promise<void>;
54
92
  /** Discards a transaction left waiting by prepareTransaction(). */
55
93
  rollbackPrepared(name: string): Promise<void>;
94
+ /**
95
+ * Rolls back to the given savepoint, regardless of its tracked depth - a
96
+ * rollback discards it (and everything after it) outright, so there is
97
+ * nothing left for any pending, still-nested releaseSavepoint() call to
98
+ * unwind.
99
+ */
56
100
  rollbackToSavepoint(name: string): Promise<void>;
57
- releaseSavepoint(name: string): Promise<void>;
58
101
  /** Asks the server to cancel whatever this session is running. */
59
102
  cancel(): Promise<void>;
60
103
  ref(): void;
@@ -1,5 +1,4 @@
1
1
  import { performance } from 'node:perf_hooks';
2
- import { coerceToBoolean } from 'putil-varhelpers';
3
2
  import { ConnectionState, DEFAULT_COLUMN_FORMAT } from '../constants.js';
4
3
  import { GlobalTypeMap } from '../data-type-map.js';
5
4
  import { PgSocket } from '../protocol/pg-socket.js';
@@ -29,6 +28,8 @@ function columnFormatsEqual(a, b) {
29
28
  export class IntlConnection extends SafeEventEmitter {
30
29
  _executeReusedParserCache = new WeakMap();
31
30
  _refCount = 0;
31
+ _transactionDepth = 0;
32
+ _savepointDepths = new Map();
32
33
  _config;
33
34
  _onErrorSavePoint;
34
35
  transactionStatus = 'I';
@@ -109,58 +110,122 @@ export class IntlConnection extends SafeEventEmitter {
109
110
  });
110
111
  });
111
112
  }
112
- async execute(sql, options, cb) {
113
+ async execute(sql, options = {}, cb) {
113
114
  this.assertConnected();
114
115
  const transactionCommand = sql.match(/^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i);
115
116
  let beginFirst = false;
116
117
  let commitLast = false;
118
+ const { autoCommit } = options;
117
119
  if (!transactionCommand) {
118
- if (!this.inTransaction &&
119
- (options?.autoCommit != null
120
- ? options?.autoCommit
121
- : this.config.autoCommit) === false) {
120
+ if ((autoCommit != null ? autoCommit : this.config.autoCommit) === false &&
121
+ !this.inTransaction) {
122
122
  beginFirst = true;
123
123
  }
124
- if (this.inTransaction && options?.autoCommit)
124
+ if (autoCommit && this.inTransaction)
125
125
  commitLast = true;
126
126
  }
127
127
  if (beginFirst)
128
128
  await this._execute('BEGIN');
129
+ // this.inTransaction goes first here, before the config fallback: the
130
+ // result is only ever consulted below when it's true, so checking it
131
+ // first short-circuits that read on every call made outside of a
132
+ // transaction, the common case. Below, `rollbackOnError &&
133
+ // this.inTransaction` deliberately puts it last instead - re-checking
134
+ // live state a plain local boolean can't capture (the awaited call in
135
+ // between may have committed, rolled back, or errored the transaction
136
+ // out from under it) - but ordered so the cheap local read still
137
+ // short-circuits the getter whenever rollbackOnError is already
138
+ // false, which it is outside a transaction.
129
139
  const rollbackOnError = !transactionCommand &&
130
- (options?.rollbackOnError != null
131
- ? options.rollbackOnError
132
- : coerceToBoolean(this.config.rollbackOnError, true));
133
- if (this.inTransaction && rollbackOnError)
140
+ this.inTransaction &&
141
+ (options.rollbackOnError ?? this.config.rollbackOnError ?? true);
142
+ if (rollbackOnError && this.inTransaction)
134
143
  await this._execute('SAVEPOINT ' + this._onErrorSavePoint);
135
144
  try {
136
145
  const result = await this._execute(sql, options, cb);
137
146
  if (commitLast)
138
147
  await this._execute('COMMIT');
139
- else if (this.inTransaction && rollbackOnError) {
148
+ else if (rollbackOnError && this.inTransaction) {
140
149
  await this._execute('RELEASE ' + this._onErrorSavePoint + ';');
141
150
  }
142
151
  return result;
143
152
  }
144
153
  catch (e) {
145
- if (this.inTransaction && rollbackOnError)
154
+ if (rollbackOnError && this.inTransaction)
146
155
  await this._execute('ROLLBACK TO ' + this._onErrorSavePoint + ';');
147
156
  throw e;
148
157
  }
149
158
  }
159
+ /**
160
+ * Starts a transaction, or - if one is already running - marks a nested
161
+ * level of it. Each call increments `_transactionDepth`; only the
162
+ * outermost one actually sends BEGIN (`inTransaction` is already false at
163
+ * that point). A matching number of commit() calls is then needed to
164
+ * actually commit - see commit().
165
+ */
150
166
  async startTransaction() {
167
+ this._transactionDepth++;
151
168
  if (!this.inTransaction)
152
169
  await this.execute('BEGIN');
153
170
  }
171
+ /**
172
+ * Starts a savepoint, or - if one under the same `name` is already open -
173
+ * marks a nested level of it. Mirrors startTransaction(): only the
174
+ * outermost call for a given name actually sends SAVEPOINT: a savepoint
175
+ * already exists under that name, re-declaring it would just stack a
176
+ * second, independent one with the same name on the server rather than
177
+ * nesting the existing one.
178
+ */
154
179
  async savepoint(name) {
155
180
  if (!(name && name.match(/^[a-zA-Z]\w+$/)))
156
181
  throw new Error(`Invalid savepoint "${name}"`);
157
- await this.execute('BEGIN; SAVEPOINT ' + name);
182
+ const depth = (this._savepointDepths.get(name) || 0) + 1;
183
+ this._savepointDepths.set(name, depth);
184
+ if (depth === 1)
185
+ await this.execute('BEGIN; SAVEPOINT ' + name);
158
186
  }
159
- async commit() {
187
+ /**
188
+ * Commits the transaction started by startTransaction() - or, when nested,
189
+ * just unwinds one level of `_transactionDepth`. The actual COMMIT is only
190
+ * sent once the depth reaches zero, i.e. once every startTransaction()
191
+ * call has a matching commit().
192
+ * @param immediate - Ignores `_transactionDepth` and commits right away,
193
+ * regardless of how many nested levels are still open.
194
+ */
195
+ async commit(immediate) {
196
+ if (!immediate && this._transactionDepth > 1) {
197
+ this._transactionDepth--;
198
+ return;
199
+ }
200
+ this._transactionDepth = 0;
160
201
  if (this.inTransaction)
161
202
  await this.execute('COMMIT');
162
203
  }
204
+ /**
205
+ * Releases the savepoint created by savepoint() - or, when nested, just
206
+ * unwinds one level of that name's depth. The actual RELEASE SAVEPOINT is
207
+ * only sent once that depth reaches zero.
208
+ * @param name - Name of the savepoint.
209
+ * @param immediate - Ignores the tracked depth and releases right away.
210
+ */
211
+ async releaseSavepoint(name, immediate) {
212
+ if (!(name && name.match(/^[a-zA-Z]\w+$/)))
213
+ throw new Error(`Invalid savepoint "${name}"`);
214
+ const depth = this._savepointDepths.get(name) || 0;
215
+ if (!immediate && depth > 1) {
216
+ this._savepointDepths.set(name, depth - 1);
217
+ return;
218
+ }
219
+ this._savepointDepths.delete(name);
220
+ await this.execute('RELEASE SAVEPOINT ' + name, { autoCommit: false });
221
+ }
222
+ /**
223
+ * Rolls back the whole transaction, regardless of `_transactionDepth` -
224
+ * a rollback ends the transaction outright, so there is nothing left for
225
+ * any pending, still-nested commit() call to unwind.
226
+ */
163
227
  async rollback() {
228
+ this._transactionDepth = 0;
164
229
  if (this.inTransaction)
165
230
  await this.execute('ROLLBACK');
166
231
  }
@@ -189,16 +254,18 @@ export class IntlConnection extends SafeEventEmitter {
189
254
  async rollbackPrepared(name) {
190
255
  await this._execute('ROLLBACK PREPARED ' + escapeLiteral(name));
191
256
  }
257
+ /**
258
+ * Rolls back to the given savepoint, regardless of its tracked depth - a
259
+ * rollback discards it (and everything after it) outright, so there is
260
+ * nothing left for any pending, still-nested releaseSavepoint() call to
261
+ * unwind.
262
+ */
192
263
  async rollbackToSavepoint(name) {
193
264
  if (!(name && name.match(/^[a-zA-Z]\w+$/)))
194
265
  throw new Error(`Invalid savepoint "${name}"`);
266
+ this._savepointDepths.delete(name);
195
267
  await this.execute('ROLLBACK TO SAVEPOINT ' + name, { autoCommit: false });
196
268
  }
197
- async releaseSavepoint(name) {
198
- if (!(name && name.match(/^[a-zA-Z]\w+$/)))
199
- throw new Error(`Invalid savepoint "${name}"`);
200
- await this.execute('RELEASE SAVEPOINT ' + name, { autoCommit: false });
201
- }
202
269
  /** Asks the server to cancel whatever this session is running. */
203
270
  cancel() {
204
271
  return this.socket.cancel();
@@ -257,22 +324,21 @@ export class IntlConnection extends SafeEventEmitter {
257
324
  await stream.waitStarted();
258
325
  return stream;
259
326
  }
260
- async _execute(sql, options, cb) {
327
+ async _execute(sql, options = {}, cb) {
261
328
  this.ref();
262
329
  try {
263
- const startTime = performance.now();
330
+ const timingEnabled = options.timing ?? this.config.timing ?? false;
331
+ const startTime = timingEnabled ? performance.now() : 0;
264
332
  const result = {
265
333
  totalCommands: 0,
266
- totalTime: 0,
267
334
  results: [],
268
335
  };
269
- const opts = options || {};
270
336
  let currentStart = startTime;
271
337
  let parsers;
272
338
  let current = { command: undefined };
273
339
  let fields;
274
340
  let error;
275
- const typeMap = opts.typeMap || GlobalTypeMap;
341
+ const typeMap = options.typeMap || GlobalTypeMap;
276
342
  this.runningQueryCount++;
277
343
  return await this.socket.sendQueryMessage(sql, (code, msg, done) => {
278
344
  switch (code) {
@@ -301,9 +367,9 @@ export class IntlConnection extends SafeEventEmitter {
301
367
  break;
302
368
  case Protocol.BackendMessageCode.DataRow:
303
369
  {
304
- const row = opts.objectRows && current.fields
305
- ? parseObjectRow(parsers, msg.data, msg.columnCount, opts, current.fields)
306
- : parseRow(parsers, msg.data, msg.columnCount, opts);
370
+ const row = options.objectRows && current.fields
371
+ ? parseObjectRow(parsers, msg.data, msg.columnCount, options, current.fields)
372
+ : parseRow(parsers, msg.data, msg.columnCount, options);
307
373
  if (cb)
308
374
  cb('row', row);
309
375
  current.rows = current.rows || [];
@@ -318,15 +384,17 @@ export class IntlConnection extends SafeEventEmitter {
318
384
  current.command === 'UPDATE') {
319
385
  current.rowsAffected = msg.rowCount;
320
386
  }
321
- current.executeTime = performance.now() - currentStart;
387
+ if (timingEnabled)
388
+ current.executeTime = performance.now() - currentStart;
322
389
  if (current.rows)
323
390
  current.rowType =
324
- opts.objectRows && current.fields ? 'object' : 'array';
391
+ options.objectRows && current.fields ? 'object' : 'array';
325
392
  result.results.push(current);
326
393
  if (cb)
327
394
  cb('command-complete', current);
328
395
  current = { command: undefined };
329
- currentStart = performance.now();
396
+ if (timingEnabled)
397
+ currentStart = performance.now();
330
398
  break;
331
399
  case Protocol.BackendMessageCode.ReadyForQuery:
332
400
  this.transactionStatus = msg.status;
@@ -334,7 +402,8 @@ export class IntlConnection extends SafeEventEmitter {
334
402
  done(error);
335
403
  break;
336
404
  }
337
- result.totalTime = performance.now() - startTime;
405
+ if (timingEnabled)
406
+ result.totalTime = performance.now() - startTime;
338
407
  // Ignore COMMIT command that we added to sql
339
408
  result.totalCommands = result.results.length;
340
409
  done(undefined, result);
@@ -361,7 +430,8 @@ export class IntlConnection extends SafeEventEmitter {
361
430
  this.ref();
362
431
  try {
363
432
  const typeMap = options.typeMap || GlobalTypeMap;
364
- const startTime = performance.now();
433
+ const timingEnabled = options.timing ?? this.config.timing ?? false;
434
+ const startTime = timingEnabled ? performance.now() : 0;
365
435
  const result = { command: undefined };
366
436
  const rows = [];
367
437
  let parsers;
@@ -428,7 +498,8 @@ export class IntlConnection extends SafeEventEmitter {
428
498
  result.command === 'UPDATE') {
429
499
  result.rowsAffected = commandTag?.rowCount;
430
500
  }
431
- result.executeTime = performance.now() - startTime;
501
+ if (timingEnabled)
502
+ result.executeTime = performance.now() - startTime;
432
503
  return result;
433
504
  }
434
505
  finally {
@@ -498,7 +569,8 @@ export class IntlConnection extends SafeEventEmitter {
498
569
  this.ref();
499
570
  try {
500
571
  const typeMap = options.typeMap || GlobalTypeMap;
501
- const startTime = performance.now();
572
+ const timingEnabled = options.timing ?? this.config.timing ?? false;
573
+ const startTime = timingEnabled ? performance.now() : 0;
502
574
  const result = { command: undefined };
503
575
  const rows = [];
504
576
  let parsers;
@@ -591,7 +663,8 @@ export class IntlConnection extends SafeEventEmitter {
591
663
  result.command === 'UPDATE') {
592
664
  result.rowsAffected = commandTag?.rowCount;
593
665
  }
594
- result.executeTime = performance.now() - startTime;
666
+ if (timingEnabled)
667
+ result.executeTime = performance.now() - startTime;
595
668
  return result;
596
669
  }
597
670
  finally {
@@ -1,4 +1,3 @@
1
- import { coerceToBoolean } from 'putil-varhelpers';
2
1
  import { DEFAULT_COLUMN_FORMAT } from '../constants.js';
3
2
  import { GlobalTypeMap } from '../data-type-map.js';
4
3
  import { Protocol } from '../protocol/protocol.js';
@@ -60,35 +59,36 @@ export class PreparedStatement extends SafeEventEmitter {
60
59
  const transactionCommand = this.sql.match(/^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i);
61
60
  let beginFirst = false;
62
61
  let commitLast = false;
62
+ const autoCommit = options?.autoCommit;
63
63
  if (!transactionCommand) {
64
- if (!intlCon.inTransaction &&
65
- (options?.autoCommit != null
66
- ? options?.autoCommit
67
- : intlCon.config.autoCommit) === false) {
64
+ if ((autoCommit != null ? autoCommit : intlCon.config.autoCommit) ===
65
+ false &&
66
+ !intlCon.inTransaction) {
68
67
  beginFirst = true;
69
68
  }
70
- if (intlCon.inTransaction && options?.autoCommit)
69
+ if (autoCommit && intlCon.inTransaction)
71
70
  commitLast = true;
72
71
  }
73
72
  if (beginFirst)
74
73
  await intlCon.execute('BEGIN');
74
+ // See IntlConnection.execute()'s own rollbackOnError for why
75
+ // intlCon.inTransaction goes first here but last in the checks below.
75
76
  const rollbackOnError = !transactionCommand &&
76
- (options?.rollbackOnError != null
77
- ? options.rollbackOnError
78
- : coerceToBoolean(intlCon.config.rollbackOnError, true));
79
- if (intlCon.inTransaction && rollbackOnError)
77
+ intlCon.inTransaction &&
78
+ (options?.rollbackOnError ?? intlCon.config.rollbackOnError ?? true);
79
+ if (rollbackOnError && intlCon.inTransaction)
80
80
  await intlCon.execute('SAVEPOINT ' + this._onErrorSavePoint);
81
81
  try {
82
82
  const result = await this._execute(options);
83
83
  if (commitLast)
84
84
  await intlCon.execute('COMMIT');
85
- else if (intlCon.inTransaction && rollbackOnError) {
85
+ else if (rollbackOnError && intlCon.inTransaction) {
86
86
  await intlCon.execute('RELEASE ' + this._onErrorSavePoint + ';');
87
87
  }
88
88
  return result;
89
89
  }
90
90
  catch (e) {
91
- if (intlCon.inTransaction && rollbackOnError) {
91
+ if (rollbackOnError && intlCon.inTransaction) {
92
92
  await intlCon.execute('ROLLBACK TO ' + this._onErrorSavePoint + ';');
93
93
  }
94
94
  throw e;
@@ -59,9 +59,12 @@ export const NumericType = {
59
59
  const fracPad = (DEC_DIGITS - (fracPart.length % DEC_DIGITS)) % DEC_DIGITS;
60
60
  fracPart = fracPart + '0'.repeat(fracPad);
61
61
  const digits = [];
62
- for (let i = 0; i < intPart.length; i += DEC_DIGITS)
62
+ let i;
63
+ let l = intPart.length;
64
+ for (i = 0; i < l; i += DEC_DIGITS)
63
65
  digits.push(+intPart.substring(i, i + DEC_DIGITS));
64
- for (let i = 0; i < fracPart.length; i += DEC_DIGITS)
66
+ l = fracPart.length;
67
+ for (i = 0; i < l; i += DEC_DIGITS)
65
68
  digits.push(+fracPart.substring(i, i + DEC_DIGITS));
66
69
  // weight counts groups before the point, less one; leading zero groups
67
70
  // are dropped and lower it, trailing ones are simply dropped.
@@ -75,8 +78,10 @@ export const NumericType = {
75
78
  if (!digits.length)
76
79
  weight = 0;
77
80
  writeHeader(digits.length, weight, sign, dscale);
78
- for (const d of digits)
79
- buf.writeInt16BE(d);
81
+ l = digits.length;
82
+ for (i = 0; i < l; i++) {
83
+ buf.writeInt16BE(digits[i]);
84
+ }
80
85
  },
81
86
  decodeBinary(v, offset = 0) {
82
87
  const len = v.readInt16BE(offset);
@@ -190,7 +195,7 @@ export function numberBytesToString(digits, scale, weight, sign) {
190
195
  }
191
196
  }
192
197
  const extra = (i - scale) % DEC_DIGITS;
193
- return out.substr(0, out.length - extra);
198
+ return out.substring(0, out.length - extra);
194
199
  }
195
200
  /* https://github.com/pgjdbc/pgjdbc/blob/3eca3a76aa4a04cb28cb960ed674cb67db30b5e3/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java */
196
201
  /**
@@ -18,7 +18,8 @@ export interface CommandResult {
18
18
  */
19
19
  rowType?: 'array' | 'object';
20
20
  /**
21
- * Time elapsed to execute command
21
+ * Time elapsed to execute command - only measured when `timing` is
22
+ * enabled (off by default; see `DatabaseConnectionParams.timing`).
22
23
  */
23
24
  executeTime?: number;
24
25
  /**
@@ -91,6 +91,19 @@ export interface DatabaseConnectionParams {
91
91
  * @default true
92
92
  */
93
93
  asyncErrorHandling?: boolean;
94
+ /**
95
+ * Whether `query()`/`execute()` (etc.) measure how long the call took -
96
+ * `CommandResult.executeTime` and `ScriptResult.totalTime`.
97
+ *
98
+ * Off by default: each measurement is a `performance.now()` call, and
99
+ * unlike `asyncErrorHandling` there is no way to compute it lazily only
100
+ * once something actually reads it - a real, if small, cost paid on
101
+ * every call whether or not the result is ever inspected, and one a
102
+ * client that does not offer this at all does not pay. Set to `true` to
103
+ * turn it on, connection-wide or for one call.
104
+ * @default false
105
+ */
106
+ timing?: boolean;
94
107
  debugLogger?: DebugLogger;
95
108
  }
96
109
  export type TargetSessionAttrs = 'read-write' | 'read-only' | 'primary' | 'standby' | 'prefer-standby';
@@ -60,4 +60,10 @@ export interface QueryOptions extends DataMappingOptions {
60
60
  * @default the connection's own setting
61
61
  */
62
62
  asyncErrorHandling?: boolean;
63
+ /**
64
+ * Overrides the connection's own `timing` for this call only - see
65
+ * `DatabaseConnectionParams.timing`.
66
+ * @default the connection's own setting
67
+ */
68
+ timing?: boolean;
63
69
  }
@@ -40,4 +40,10 @@ export interface ScriptExecuteOptions extends DataMappingOptions {
40
40
  * @default the connection's own setting
41
41
  */
42
42
  asyncErrorHandling?: boolean;
43
+ /**
44
+ * Overrides the connection's own `timing` for this call only - see
45
+ * `DatabaseConnectionParams.timing`.
46
+ * @default the connection's own setting
47
+ */
48
+ timing?: boolean;
43
49
  }
@@ -9,7 +9,8 @@ export interface ScriptResult {
9
9
  */
10
10
  totalCommands: number;
11
11
  /**
12
- * Total execution time
12
+ * Total execution time - only measured when `timing` is enabled (off by
13
+ * default; see `DatabaseConnectionParams.timing`).
13
14
  */
14
- totalTime: number;
15
+ totalTime?: number;
15
16
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "postgrejs",
3
3
  "description": "Professional PostgreSQL client NodeJS",
4
- "version": "3.0.1",
4
+ "version": "3.0.3",
5
5
  "author": "Panates",
6
6
  "license": "BSD-3-Clause",
7
7
  "dependencies": {
@@ -52,7 +52,12 @@ export class Frontend {
52
52
  .writeInt32BE(0) // Preserve length
53
53
  .writeInt16BE(Protocol.VERSION_MAJOR)
54
54
  .writeInt16BE(Protocol.VERSION_MINOR);
55
- for (const [k, v] of Object.entries(args)) {
55
+ const entries = Object.entries(args);
56
+ const l = entries.length;
57
+ let k;
58
+ let v;
59
+ for (let i = 0; i < l; i++) {
60
+ [k, v] = entries[i];
56
61
  if (k !== 'client_encoding')
57
62
  io.writeCString(k, 'utf8').writeCString(v, 'utf8');
58
63
  }
@@ -105,16 +110,19 @@ export class Frontend {
105
110
  getParseMessage(args) {
106
111
  if (args.statement && args.statement.length > 63)
107
112
  throw new Error('Query name length must be lower than 63');
113
+ const { paramTypes } = args;
108
114
  const io = this._io
109
115
  .start()
110
116
  .writeInt8(Protocol.FrontendMessageCode.Parse)
111
117
  .writeInt32BE(0) // Preserve header
112
118
  .writeCString(args.statement || '', 'utf8')
113
119
  .writeCString(args.sql, 'utf8')
114
- .writeUInt16BE(args.paramTypes ? args.paramTypes.length : 0);
115
- if (args.paramTypes) {
116
- for (const t of args.paramTypes) {
117
- io.writeUInt32BE(t || 0);
120
+ .writeUInt16BE(paramTypes?.length || 0);
121
+ if (paramTypes) {
122
+ let i;
123
+ const l = paramTypes.length;
124
+ for (i = 0; i < l; i++) {
125
+ io.writeUInt32BE(paramTypes[i] || 0);
118
126
  }
119
127
  }
120
128
  return setLengthAndFlush(io, 1);
package/protocol/sasl.js CHANGED
@@ -96,7 +96,7 @@ export var SASL;
96
96
  const arr = data.split(',');
97
97
  for (const s of arr) {
98
98
  if (s[0] === 'v')
99
- serverSignature = s.substr(2);
99
+ serverSignature = s.substring(2);
100
100
  }
101
101
  if (serverSignature !== session.serverSignature)
102
102
  throw new Error('SASL: Server signature does not match');
@@ -20,5 +20,15 @@ export declare function abortError(signal: AbortSignal, cause?: unknown): Error;
20
20
  *
21
21
  * The listener is always removed again: one long-lived signal shared by many
22
22
  * queries would otherwise accumulate a listener per query.
23
+ *
24
+ * Not `async` itself: the overwhelmingly common case (no signal passed) just
25
+ * returns `run()`'s own promise straight through. Marking this function
26
+ * `async` would still be correct, but every `async function` return goes
27
+ * through the spec's promise-resolve machinery - a real extra microtask
28
+ * tick, even for a value that is already the right kind of promise - and
29
+ * this sits on every query's hot path, signal or not. The slow path (a
30
+ * signal actually given) is factored into its own async helper below, where
31
+ * the cost is negligible next to the abort-listener bookkeeping it already
32
+ * does.
23
33
  */
24
34
  export declare function withAbortSignal<T>(signal: AbortSignal | undefined, cancel: () => Promise<void>, run: () => Promise<T>): Promise<T>;
@@ -36,13 +36,26 @@ export function abortError(signal, cause) {
36
36
  *
37
37
  * The listener is always removed again: one long-lived signal shared by many
38
38
  * queries would otherwise accumulate a listener per query.
39
+ *
40
+ * Not `async` itself: the overwhelmingly common case (no signal passed) just
41
+ * returns `run()`'s own promise straight through. Marking this function
42
+ * `async` would still be correct, but every `async function` return goes
43
+ * through the spec's promise-resolve machinery - a real extra microtask
44
+ * tick, even for a value that is already the right kind of promise - and
45
+ * this sits on every query's hot path, signal or not. The slow path (a
46
+ * signal actually given) is factored into its own async helper below, where
47
+ * the cost is negligible next to the abort-listener bookkeeping it already
48
+ * does.
39
49
  */
40
- export async function withAbortSignal(signal, cancel, run) {
50
+ export function withAbortSignal(signal, cancel, run) {
41
51
  if (!signal)
42
52
  return run();
43
53
  // Already aborted: nothing has been sent, so nothing needs cancelling.
44
54
  if (signal.aborted)
45
- throw abortError(signal);
55
+ return Promise.reject(abortError(signal));
56
+ return runWithAbortSignal(signal, cancel, run);
57
+ }
58
+ async function runWithAbortSignal(signal, cancel, run) {
46
59
  const onAbort = () => {
47
60
  // A failed cancel (the session already gone, say) must not replace the
48
61
  // error the query itself is about to report.