node-firebird 2.11.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,8 +13,8 @@
13
13
  - [Promises and async/await](#promises-and-asyncawait) — the `*Async` API plus `withConnection` / `withTransaction` helpers
14
14
  - [Connection types](#connection-types) — connection options, `firebird://` URIs and traditional connection strings, classic connections, pooling
15
15
  - [Database object (db)](#database-object-db) — database, transaction and statement methods/options
16
- - [Examples](#examples) — parametrized queries, named placeholders, nested result tables (nestTables), custom type parsers (typeCast), BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
17
- - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
16
+ - [Examples](#examples) — parametrized queries, tagged-template queries (sql), named placeholders, nested result tables (nestTables), row-key transforms (transformKeys), result metadata / affected rows (withMeta), custom type parsers (typeCast), BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
17
+ - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts incl. BLOBs), bulk-insert stream (batchStream), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
18
18
  - [Using node-firebird with Express.js](#using-node-firebird-with-expressjs)
19
19
  - [FAQ](#faq)
20
20
  - [Contributing](#contributing) · [Contributors](#contributors)
@@ -39,16 +39,31 @@ and 26 against Firebird 3, 4, 5 and 6).
39
39
 
40
40
  ## Usage
41
41
 
42
+ CommonJS and ESM are both first-class (conditional `exports`):
43
+
42
44
  ```js
43
- var Firebird = require('node-firebird');
45
+ // CommonJS
46
+ const Firebird = require('node-firebird');
47
+
48
+ // ESM — default and named imports both work
49
+ import Firebird from 'node-firebird';
50
+ import { attach, pool, GDSCode, SQL_TYPES } from 'node-firebird';
44
51
  ```
45
52
 
53
+ The documented subpaths keep working in both module systems
54
+ (`require('node-firebird/lib/gdscodes')`, …).
55
+
46
56
  TypeScript is fully supported — the driver itself is written in TypeScript and
47
- ships its own type declarations:
57
+ ships its own type declarations, with generics on the query APIs:
48
58
 
49
59
  ```ts
50
60
  import * as Firebird from 'node-firebird';
51
61
  import type { Options, Database } from 'node-firebird';
62
+
63
+ interface Emp { ID: number; NAME: string }
64
+ const rows = await db.queryAsync<Emp>('SELECT ID, NAME FROM EMP'); // Emp[]
65
+ db.query<Emp>('SELECT ID, NAME FROM EMP', [], (err, rows) => { /* rows: Emp[] */ });
66
+ const r = await db.queryAsync<Emp>('SELECT ...', [], { withMeta: true }); // QueryResult<Emp>
52
67
  ```
53
68
 
54
69
  ### Developing the driver
@@ -154,6 +169,14 @@ Notes:
154
169
 
155
170
  ### Connection options
156
171
 
172
+ Settings you leave out fall back to environment variables first — using
173
+ Firebird's own conventions (`ISC_USER` and `ISC_PASSWORD`, the same
174
+ variables isql honours) plus `FIREBIRD_HOST`, `FIREBIRD_PORT`,
175
+ `FIREBIRD_DATABASE` and `FIREBIRD_ROLE` — and to the built-in defaults
176
+ below (SYSDBA / masterkey / 127.0.0.1) only after that. Explicitly
177
+ provided options always win, so credentials can stay out of code the
178
+ same way `PGUSER`/`PGPASSWORD` work with pg.
179
+
157
180
  ```js
158
181
  var options = {};
159
182
 
@@ -186,6 +209,7 @@ options.owner = undefined; // optional; owner of a newly created database — le
186
209
  options.jsonAsObject = false; // optional; automatically stringify parameters and parse query results that contain JSON (FB >= 6.0)
187
210
  options.namedPlaceholders = false; // set to true to allow :name placeholders in SQL with a { name: value } params object (see Named placeholders)
188
211
  options.nestTables = false; // true nests object rows by source table (row[table][column]); a string separator flattens to 'table<sep>column' keys — see Nested result tables (nestTables). Overridable per query
212
+ options.transformKeys = undefined; // 'camel' (FIRST_NAME → firstName) or a (key) => key mapper for object-row keys — see Transforming row keys (transformKeys). Overridable per query
189
213
  options.typeCast = undefined; // optional; custom type parser called for every result column value (see Custom type parsers)
190
214
  options.statementCacheSize = 0; // optional; per-connection LRU cache of prepared statements, 0 = disabled (see Prepared-statement cache)
191
215
  ```
@@ -293,9 +317,11 @@ The pool is an `EventEmitter` and exposes live counters, following the
293
317
  ```js
294
318
  const pool = Firebird.pool(10, {
295
319
  ...options,
296
- idleTimeoutMillis: 30000, // close connections idle for 30s…
297
- min: 2, // …but always keep 2 alive
320
+ idleTimeoutMillis: 30000, // close connections idle for 30s…
321
+ min: 2, // …but always keep 2 alive
298
322
  connectTimeout: 5000,
323
+ maxUses: 7500, // retire a connection after 7500 checkouts (pg's maxUses)
324
+ maxLifetimeMillis: 3600000, // …or 1h after creation (Postgres.js's max_lifetime)
299
325
  });
300
326
 
301
327
  pool.on('connect', (db) => console.log('new server connection'));
@@ -318,6 +344,12 @@ console.log({
318
344
  connection they ever created (issue [#329](https://github.com/hgourvest/node-firebird/issues/329)).
319
345
  The sweep also evicts idle connections whose socket has died, so callers
320
346
  don't receive them after a server restart (issue [#343](https://github.com/hgourvest/node-firebird/issues/343)).
347
+ - `maxUses` and `maxLifetimeMillis` **recycle** physical connections: a
348
+ worn-out or over-age connection is closed for good when returned to the
349
+ pool (lifetime is also enforced on idle connections by the sweep, even
350
+ below `min`) and a replacement is created on demand. Recycling bounds
351
+ server-side resource drift on long-lived connections. Both default to 0
352
+ (off).
321
353
  - `error` is a background-error channel (idle eviction failures and the
322
354
  like); unlike a plain `EventEmitter`, it is only emitted when a listener
323
355
  is attached, so existing applications keep working unchanged.
@@ -453,6 +485,63 @@ Firebird.attach(options, function (err, db) {
453
485
  });
454
486
  ```
455
487
 
488
+ ### Tagged-template queries (sql)
489
+
490
+ `db.sql` / `transaction.sql` offer a Postgres.js-style tagged-template API
491
+ on top of the regular parameter machinery. Interpolated values are bound
492
+ as positional parameters — never concatenated into the SQL — so the API is
493
+ injection-safe by construction:
494
+
495
+ ```js
496
+ const id = 2;
497
+ const rows = await db.sql`SELECT NAME FROM EMP WHERE ID = ${id}`;
498
+ // → executes SELECT NAME FROM EMP WHERE ID = ? with params [2]
499
+ ```
500
+
501
+ The returned query is a **lazy thenable**: it runs when awaited (or via
502
+ `.then`/`.catch`/`.finally`), exactly once. Until then it can be embedded
503
+ in another tag as a **fragment**, splicing its text and parameters in
504
+ place:
505
+
506
+ ```js
507
+ const filter = db.sql`DEPT_ID = ${1} AND ACTIVE = ${true}`;
508
+ const rows = await db.sql`SELECT * FROM EMP WHERE ${filter} ORDER BY ID`;
509
+ ```
510
+
511
+ Arrays expand to placeholder lists (for `IN`), and calling the tag with a
512
+ string produces a safely quoted, dot-qualified **identifier** for dynamic
513
+ table/column names:
514
+
515
+ ```js
516
+ await db.sql`SELECT * FROM EMP WHERE ID IN (${[1, 2, 3]})`;
517
+
518
+ const col = 'NAME';
519
+ await db.sql`SELECT ${db.sql(col)} FROM ${db.sql('S1.EMP')}`;
520
+ // → SELECT "NAME" FROM "S1"."EMP"
521
+ ```
522
+
523
+ `.options({...})` attaches per-query options (`timeout`, `signal`,
524
+ `nestTables`, …), `.withMeta()` executes resolving the
525
+ [full result object](#result-metadata-and-affected-rows-withmeta), and
526
+ `.toQuery()` returns the compiled `{ text, params }` without executing —
527
+ handy for logging and tests:
528
+
529
+ ```js
530
+ const r = await db.sql`UPDATE EMP SET ACTIVE = false WHERE ID = ${9}`.withMeta();
531
+ // r.affectedRows === 1
532
+ ```
533
+
534
+ Sharp edges, made loud instead of silent: a query executes once, in the
535
+ shape of its first consumer — consuming it again in the *other* shape
536
+ (plain `await` after `.withMeta()`, or vice versa) throws, as does
537
+ `.options()` after execution. Interpolating an **empty array** throws
538
+ (it would compile to invalid SQL like `IN ()`), and circular fragments
539
+ are rejected instead of overflowing the stack. The compiled text is
540
+ positional-only, so the [named placeholders](#named-placeholders)
541
+ rewriter is disabled for tagged queries — PSQL `:variable` references
542
+ in an `EXECUTE BLOCK` template are safe even with `namedPlaceholders:
543
+ true` on the connection.
544
+
456
545
  ### Named placeholders
457
546
 
458
547
  With the `namedPlaceholders: true` connection option, SQL may use `:name`
@@ -537,6 +626,76 @@ array rows are positional and need no qualification. Works on every
537
626
  supported Firebird version (the source-table metadata comes from the
538
627
  statement describe, available since Firebird 2.0).
539
628
 
629
+ ### Transforming row keys (transformKeys)
630
+
631
+ `transformKeys` rewrites object-row keys — the counterpart of
632
+ Postgres.js's `transform`. The built-in `'camel'` maps `FIRST_NAME` →
633
+ `firstName`; a custom `(key) => key` mapper gives full control. Accepted
634
+ at connection level and per query (per-query wins):
635
+
636
+ ```js
637
+ const rows = await db.queryAsync('SELECT EMP_ID, FIRST_NAME FROM EMP_INFO', [],
638
+ { transformKeys: 'camel' });
639
+ // rows[0] = { empId: 1, firstName: 'Ada' }
640
+ ```
641
+
642
+ The transform runs after `lowercase_keys` and applies to both parts of
643
+ [`nestTables`](#nested-result-tables-nesttables) keys (`row.e.empId`).
644
+ Column *metadata* — `withMeta` `fields` and the `typeCast` hook — keeps
645
+ the raw server aliases. A custom mapper that throws falls back to the
646
+ untransformed key (with a console warning) rather than corrupting the
647
+ row decode.
648
+
649
+ ### Result metadata and affected rows (withMeta)
650
+
651
+ By default, queries deliver bare rows and DML row counts are not reported.
652
+ The per-query `withMeta: true` option switches the result (callback and
653
+ promise APIs alike) to a full result object, the counterpart of pg's
654
+ `{ rows, rowCount, fields }` and mysql2's `affectedRows`:
655
+
656
+ ```js
657
+ const r = await db.queryAsync('UPDATE EMP SET ACTIVE = false WHERE DEPT_ID = ?', [1],
658
+ { withMeta: true });
659
+ // r = {
660
+ // rows: undefined, // rows array (SELECT), row object (RETURNING), or undefined
661
+ // fields: [...], // per-column metadata (see below)
662
+ // affectedRows: 2, // what the server actually changed
663
+ // recordCounts: { selectCount: 0, insertCount: 0, updateCount: 2, deleteCount: 0 },
664
+ // warnings: [], // isc_arg_warning entries from the execute response
665
+ // }
666
+ ```
667
+
668
+ For DML (`INSERT`/`UPDATE`/`DELETE`, including `... RETURNING` and
669
+ `EXECUTE PROCEDURE`), `affectedRows` is the server-reported count
670
+ (`isc_info_sql_records`) and `recordCounts` breaks it down per verb — this
671
+ costs one extra lightweight info request per statement, which is why the
672
+ option is opt-in. For `SELECT`, `affectedRows` is simply the number of rows
673
+ returned (pg's `rowCount` convention) with no extra round-trip, and
674
+ `recordCounts` is absent.
675
+
676
+ Each entry in `fields` describes one output column — the same vocabulary
677
+ the [typeCast hook](#custom-type-parsers-typecast) receives, plus
678
+ nullability and the relation alias/schema:
679
+
680
+ ```js
681
+ { type: 448, typeName: 'VARYING', subType: 4, scale: 0, length: 80,
682
+ nullable: true, field: 'NAME', relation: 'EMP', relationAlias: '',
683
+ relationSchema: 'PUBLIC', alias: 'NAME' }
684
+ ```
685
+
686
+ (`relationSchema` is filled on Firebird 6.0+; `subType` of a text column is
687
+ its character-set id.) In TypeScript, `queryAsync<T>(sql, params,
688
+ { withMeta: true })` resolves to `QueryResult<T>` automatically. Server
689
+ warnings attached to any response — not just queries — are also emitted as
690
+ [`'warning'` driver events](#driver-events).
691
+
692
+ The option is honoured by `query`/`execute` (and their `*Async` wrappers),
693
+ on databases and transactions alike. It is ignored by the streaming APIs
694
+ (`sequentially`, `queryStream` — rows bypass the result there) and by
695
+ `executeBatch` (which has its own completion shape). For `EXECUTE
696
+ PROCEDURE`, `affectedRows` reflects DML the procedure performed — a
697
+ procedure that only returns values reports 0 alongside its row.
698
+
540
699
  ### Custom type parsers (typeCast)
541
700
 
542
701
  The `typeCast` connection option lets you override how column values are
@@ -1009,6 +1168,41 @@ Firebird.attach(options, function (err, db) {
1009
1168
  });
1010
1169
  ```
1011
1170
 
1171
+ #### Savepoints
1172
+
1173
+ `transaction.savepoint(work)` runs `work` inside a savepoint (Firebird
1174
+ 1.5+): on resolve the savepoint is **released**, on reject the transaction
1175
+ **rolls back to the savepoint** — undoing only `work`'s changes — and the
1176
+ error is rethrown while the transaction itself stays usable. Calls nest;
1177
+ names are generated automatically. This is the counterpart of
1178
+ Postgres.js's `sql.savepoint()` and mirrors `db.withTransaction`'s style:
1179
+
1180
+ ```js
1181
+ await db.withTransaction(async (tx) => {
1182
+ await tx.sql`INSERT INTO ORDERS VALUES (${1}, ${'paid'})`;
1183
+
1184
+ try {
1185
+ await tx.savepoint(async () => {
1186
+ await tx.sql`INSERT INTO AUDIT VALUES (${1}, ${'optional enrichment'})`;
1187
+ await maybeFailingStep(tx);
1188
+ });
1189
+ } catch (err) {
1190
+ // the AUDIT insert is undone; the ORDERS insert survives and the
1191
+ // transaction continues toward commit
1192
+ }
1193
+ });
1194
+ ```
1195
+
1196
+ If the rollback-to itself fails (e.g. the connection died), the original
1197
+ error is still thrown, with the rollback failure attached as
1198
+ `err.savepointRollbackError`. A release failure does **not** roll the
1199
+ work back — only a `work` failure does.
1200
+
1201
+ > **Note:** do not run sibling savepoints concurrently on one transaction
1202
+ > (`Promise.all`): Firebird's `RELEASE SAVEPOINT` also releases every
1203
+ > savepoint created after it, so interleaved siblings would release each
1204
+ > other. Nested (awaited) savepoints are fine.
1205
+
1012
1206
  ### Driver Events
1013
1207
 
1014
1208
  Driver events are synchronous notifications emitted on the `Database` object for connection-level operations. Subscribe with `db.on(eventName, handler)`.
@@ -1030,7 +1224,11 @@ Firebird.attach(options, function (err, db) {
1030
1224
  });
1031
1225
 
1032
1226
  db.on('error', function (err) {
1033
- // connection-level errors (socket errors, closed connection, etc.)
1227
+ // connection-level errors (socket errors, closed connection, etc.).
1228
+ // Delivered to listeners only: without one, background failures (e.g.
1229
+ // a failed automatic reconnect) are NOT re-thrown as uncaught
1230
+ // exceptions — the operations they affect still receive the error
1231
+ // through their own callbacks/promises.
1034
1232
  });
1035
1233
 
1036
1234
  db.on('transaction', function (options) {
@@ -1060,6 +1258,15 @@ Firebird.attach(options, function (err, db) {
1060
1258
  // rows === Array
1061
1259
  });
1062
1260
 
1261
+ db.on('warning', function (warning) {
1262
+ // fired (next tick) for every isc_arg_warning the server attaches to a
1263
+ // successful response — e.g. "parallel workers value capped" when
1264
+ // parallelWorkers exceeds the server maximum. The listener above is
1265
+ // registered inside the attach callback and still catches attach-time
1266
+ // warnings, because emission is deferred by one tick.
1267
+ // warning === { gdscode: Number, params?: Array, message: String }
1268
+ });
1269
+
1063
1270
  db.detach();
1064
1271
  });
1065
1272
  ```
@@ -1318,7 +1525,27 @@ Commonly used Firebird character sets are automatically mapped to their correspo
1318
1525
  | `ASCII` | `ascii` | 7-bit ASCII. |
1319
1526
  | `NONE` | `latin1` | Raw/unspecified character set. Treated as binary-safe 8-bit characters. |
1320
1527
 
1321
- Accented characters and fixed-length `CHAR(N)` column whitespace/truncation are handled automatically matching the connection character set width definitions.
1528
+ Beyond Node's native encodings, the driver ships **codepage codecs** for the
1529
+ single-byte charsets (decode *and* encode — columns, parameters, SQL
1530
+ literals and `blobAsText` blobs all transcode):
1531
+
1532
+ > `WIN1250`–`WIN1258` (Central European, Cyrillic, Greek, Turkish, Hebrew,
1533
+ > Arabic, Baltic, Vietnamese), `ISO8859_2`–`ISO8859_9`, `ISO8859_13`,
1534
+ > `KOI8R`, `KOI8U`, `DOS866`
1535
+
1536
+ ```js
1537
+ const options = { /* ... */ encoding: 'WIN1251' };
1538
+ await db.queryAsync('INSERT INTO T VALUES (?)', ['Привет']); // encoded as cp1251
1539
+ ```
1540
+
1541
+ The codecs are built from Node's ICU tables at first use (present in every
1542
+ official Node build). `attachOrCreate`/`create` honour `options.encoding`
1543
+ for the new database's default charset too. Accented characters and
1544
+ fixed-length `CHAR(N)` whitespace/truncation are handled automatically per
1545
+ the charset width — and single-byte columns (including charset `NONE`) are
1546
+ readable in full under the default UTF8 connection (the declared fetch
1547
+ lengths are widened per the charset-width ratio, fixing the
1548
+ `string right truncation` errors of issue [#422](https://github.com/hgourvest/node-firebird/issues/422)).
1322
1549
 
1323
1550
  #### Custom Charset Connection Example
1324
1551
  ```js
@@ -1582,11 +1809,43 @@ Notes:
1582
1809
  - Values are encoded from the statement's own parameter metadata, so
1583
1810
  NUMERIC/DECIMAL scale, `BIGINT`/`INT128` (pass `BigInt`), `BOOLEAN`,
1584
1811
  `TIMESTAMP`/`DATE`/`TIME`, `FLOAT`/`DOUBLE` and `DECFLOAT` all round-trip
1585
- exactly. BLOB and ARRAY parameters are not supported in batches yet.
1812
+ exactly.
1813
+ - `BLOB` columns accept Buffers, strings, JSON-able objects, or
1814
+ pre-created blob quad ids: values are uploaded as transaction blobs
1815
+ first — all initiated back-to-back so the blob ops pipeline on the
1816
+ wire — and the batch messages reference their ids. `ARRAY` parameters
1817
+ are not supported.
1586
1818
  - Oversized `CHAR`/`VARCHAR` values fail the batch client-side before
1587
1819
  anything is sent; server-side record errors (constraint violations,
1588
1820
  truncation…) are reported per record.
1589
1821
 
1822
+ ### Bulk-insert stream (batchStream, Firebird 4.0+)
1823
+
1824
+ `db.batchStream(sql, options)` is the COPY FROM analogue: an object-mode
1825
+ `Writable` that flushes parameter-array rows in chunks through one
1826
+ prepared statement using the batch API. The Database form runs its own
1827
+ transaction — **committed on finish, rolled back on error or destroy**,
1828
+ all-or-nothing for the whole stream:
1829
+
1830
+ ```js
1831
+ const { pipeline } = require('stream/promises');
1832
+
1833
+ const stream = db.batchStream('INSERT INTO EVENTS VALUES (?, ?, ?)', {
1834
+ flushRows: 1000, // rows buffered per batch flush (default 1000)
1835
+ });
1836
+
1837
+ await pipeline(mySourceOfRowArrays, stream); // e.g. a CSV parser
1838
+ console.log(stream.recordCount, stream.affectedRows); // totals after 'finish'
1839
+ ```
1840
+
1841
+ Backpressure is the `Writable` machinery itself: writes pause while a
1842
+ chunk is in flight, so an arbitrarily large source never accumulates in
1843
+ memory beyond `flushRows`. BLOB columns accept Buffers/strings per the
1844
+ batch rules above. `transaction.batchStream(sql, options)` runs inside an
1845
+ existing transaction and leaves commit/rollback to you. The remaining
1846
+ options (`chunkSize`, `bufferSize`, …) pass through to
1847
+ [executeBatch](#batch-execution-firebird-40).
1848
+
1590
1849
  ### Statement Timeouts (Firebird 4.0+)
1591
1850
  Setting a statement timeout allows the client to automatically abort queries that take too long on the server.
1592
1851
  ```js
@@ -1922,6 +2181,25 @@ app.get('/users/:id/picture', withConnection(pool, function (db, req, res, done)
1922
2181
 
1923
2182
  Answers to recurring questions from the [issue tracker](https://github.com/hgourvest/node-firebird/issues).
1924
2183
 
2184
+ #### Text comes back as `������` with a WIN1250/1251/1253/1257 database (issue [#319](https://github.com/hgourvest/node-firebird/issues/319))
2185
+
2186
+ Resolved — the driver now ships codepage codecs for the single-byte
2187
+ charsets (`WIN1250`–`WIN1258`, `ISO8859_2`–`9`/`13`, `KOI8R`/`KOI8U`,
2188
+ `DOS866`): just set the matching connection encoding and both reads and
2189
+ writes transcode correctly, including parameters, SQL literals and
2190
+ `blobAsText` blobs:
2191
+
2192
+ ```js
2193
+ const options = { /* ... */ encoding: 'WIN1253' };
2194
+ ```
2195
+
2196
+ See [§ Character Set & Encoding Support](#character-set--encoding-support).
2197
+ For a charset *outside* that list (e.g. `DOS437`/`DOS850`), the
2198
+ [iconv-lite](https://www.npmjs.com/package/iconv-lite) escape hatch still
2199
+ works: connect with `encoding: 'NONE'`, read raw bytes via `latin1` in a
2200
+ [`typeCast` hook](#custom-type-parsers-typecast) and decode with the real
2201
+ codepage; write already-encoded bytes as Buffer parameters.
2202
+
1925
2203
  #### Can I use aggregate functions like `LIST()`? I get "no database to handle" when I call the result.
1926
2204
 
1927
2205
  Yes — `LIST()` is plain SQL and needs no special driver support. The error happens because `LIST()` returns a text blob (subtype 1), and blob columns come back from `db.query`/`transaction.query` as **async reader functions** bound to the transaction the query ran in (see [Reading Blobs](#reading-blobs-asynchronous)). Calling that function without a transaction — or with a different one — is what throws "no database to handle".
package/lib/pool.d.ts CHANGED
@@ -22,7 +22,12 @@ type AttachFn = (options: any, callback: Callback) => void;
22
22
  *
23
23
  * Options: max (factory argument), options.min (floor the reaper never
24
24
  * shrinks below), options.idleTimeoutMillis (close idle connections after
25
- * this many ms; 0/absent = never), options.connectTimeout.
25
+ * this many ms; 0/absent = never), options.connectTimeout,
26
+ * options.maxUses (retire a connection after this many checkouts — pg's
27
+ * maxUses), options.maxLifetimeMillis (retire a connection this many ms
28
+ * after it was created — Postgres.js's max_lifetime). Retirement happens
29
+ * when the connection is returned to the pool, and the sweep also closes
30
+ * over-lifetime idle connections; a replacement is created on demand.
26
31
  */
27
32
  declare class Pool extends Events.EventEmitter {
28
33
  attach: AttachFn;
@@ -33,11 +38,19 @@ declare class Pool extends Events.EventEmitter {
33
38
  max: number;
34
39
  min: number;
35
40
  idleTimeoutMillis: number;
41
+ maxUses: number;
42
+ maxLifetimeMillis: number;
36
43
  pending: Callback[];
37
44
  options: any;
38
45
  _destroyed: boolean;
39
46
  _reaper: NodeJS.Timeout | null;
40
47
  constructor(attach: AttachFn, max: number, options: any);
48
+ /** True when the connection exceeded maxUses / maxLifetimeMillis.
49
+ * Both stamps are set unconditionally when the pool creates the
50
+ * connection, so they can be read bare here. */
51
+ _isExpired(db: any): boolean;
52
+ /** Close a healthy pooled connection for good (reaper/retirement path). */
53
+ _retire(db: any): void;
41
54
  /** Physical connections owned by the pool (idle + in use). */
42
55
  get totalCount(): number;
43
56
  /** Connections sitting idle in the pool. */
package/lib/pool.js CHANGED
@@ -25,7 +25,12 @@ const callback_1 = require("./callback");
25
25
  *
26
26
  * Options: max (factory argument), options.min (floor the reaper never
27
27
  * shrinks below), options.idleTimeoutMillis (close idle connections after
28
- * this many ms; 0/absent = never), options.connectTimeout.
28
+ * this many ms; 0/absent = never), options.connectTimeout,
29
+ * options.maxUses (retire a connection after this many checkouts — pg's
30
+ * maxUses), options.maxLifetimeMillis (retire a connection this many ms
31
+ * after it was created — Postgres.js's max_lifetime). Retirement happens
32
+ * when the connection is returned to the pool, and the sweep also closes
33
+ * over-lifetime idle connections; a replacement is created on demand.
29
34
  */
30
35
  class Pool extends events_1.default.EventEmitter {
31
36
  constructor(attach, max, options) {
@@ -38,21 +43,51 @@ class Pool extends events_1.default.EventEmitter {
38
43
  this.max = max || 4;
39
44
  this.min = (options && options.min > 0) ? Math.min(options.min, this.max) : 0;
40
45
  this.idleTimeoutMillis = (options && options.idleTimeoutMillis > 0) ? options.idleTimeoutMillis : 0;
46
+ this.maxUses = (options && options.maxUses > 0) ? options.maxUses : 0;
47
+ this.maxLifetimeMillis = (options && options.maxLifetimeMillis > 0) ? options.maxLifetimeMillis : 0;
41
48
  this.pending = []; // callbacks waiting for a free slot
42
49
  this.options = options;
43
50
  this._destroyed = false; // true after destroy() — prevents further use
44
51
  this._reaper = null;
45
- if (this.idleTimeoutMillis) {
52
+ // the sweep serves both idle eviction and lifetime retirement of
53
+ // idle connections; base its cadence on the tightest configured limit
54
+ var sweepBasis = Math.min(this.idleTimeoutMillis || Infinity, this.maxLifetimeMillis || Infinity);
55
+ if (sweepBasis !== Infinity) {
46
56
  var self = this;
47
- // Sweep at half the idle timeout (bounded to 100ms..30s) so a
48
- // connection lives at most ~1.5x idleTimeoutMillis. unref() keeps
57
+ // Sweep at half the basis (bounded to 100ms..30s) so a
58
+ // connection lives at most ~1.5x its limit. unref() keeps
49
59
  // the timer from holding the process open.
50
- var interval = Math.min(Math.max(this.idleTimeoutMillis / 2, 100), 30000);
60
+ var interval = Math.min(Math.max(sweepBasis / 2, 100), 30000);
51
61
  this._reaper = setInterval(function () { self._reap(); }, interval);
52
62
  if (this._reaper.unref)
53
63
  this._reaper.unref();
54
64
  }
55
65
  }
66
+ /** True when the connection exceeded maxUses / maxLifetimeMillis.
67
+ * Both stamps are set unconditionally when the pool creates the
68
+ * connection, so they can be read bare here. */
69
+ _isExpired(db) {
70
+ if (this.maxUses > 0 && db.__poolUseCount >= this.maxUses)
71
+ return true;
72
+ if (this.maxLifetimeMillis > 0 && Date.now() - db.__poolCreatedAt >= this.maxLifetimeMillis)
73
+ return true;
74
+ return false;
75
+ }
76
+ /** Close a healthy pooled connection for good (reaper/retirement path). */
77
+ _retire(db) {
78
+ var self = this;
79
+ this._forget(db);
80
+ db.connection._pooled = false;
81
+ try {
82
+ db.detach(function (err) {
83
+ if (err)
84
+ self._emitError(err, db);
85
+ });
86
+ }
87
+ catch (e) {
88
+ self._emitError(e, db);
89
+ }
90
+ }
56
91
  /** Physical connections owned by the pool (idle + in use). */
57
92
  get totalCount() {
58
93
  return this.internaldb.length;
@@ -105,22 +140,20 @@ class Pool extends events_1.default.EventEmitter {
105
140
  self._forget(db);
106
141
  return;
107
142
  }
143
+ // lifetime retirement applies even below min — recycling is the
144
+ // point; replacements are created on demand
145
+ if (self._isExpired(db)) {
146
+ self._retire(db);
147
+ return;
148
+ }
149
+ if (!self.idleTimeoutMillis)
150
+ return;
108
151
  if (self.internaldb.length <= self.min)
109
152
  return;
110
153
  var idleSince = typeof db.__poolIdleSince === 'number' ? db.__poolIdleSince : now;
111
154
  if (now - idleSince < self.idleTimeoutMillis)
112
155
  return;
113
- self._forget(db);
114
- db.connection._pooled = false;
115
- try {
116
- db.detach(function (err) {
117
- if (err)
118
- self._emitError(err, db);
119
- });
120
- }
121
- catch (e) {
122
- self._emitError(e, db);
123
- }
156
+ self._retire(db);
124
157
  });
125
158
  }
126
159
  get(callback) {
@@ -155,6 +188,7 @@ class Pool extends events_1.default.EventEmitter {
155
188
  }
156
189
  // Idle connection available — hand it out immediately.
157
190
  self.dbinuse++;
191
+ db.__poolUseCount = (db.__poolUseCount || 0) + 1;
158
192
  self.emit('acquire', db);
159
193
  cb(null, db);
160
194
  }
@@ -212,6 +246,8 @@ class Pool extends events_1.default.EventEmitter {
212
246
  }
213
247
  if (!err) {
214
248
  self.dbinuse++;
249
+ db.__poolCreatedAt = Date.now();
250
+ db.__poolUseCount = 1;
215
251
  self.internaldb.push(db);
216
252
  db.on('detach', function () {
217
253
  // also in pool (could be a twice call to detach)
@@ -222,6 +258,13 @@ class Pool extends events_1.default.EventEmitter {
222
258
  self.internaldb.splice(self.internaldb.indexOf(db), 1);
223
259
  self.emit('remove', db);
224
260
  }
261
+ else if (self._isExpired(db)) {
262
+ // worn out (maxUses / maxLifetimeMillis): close it
263
+ // for good instead of returning it to the idle
264
+ // pool. The re-fired detach event exits early via
265
+ // the internaldb guard above.
266
+ self._retire(db);
267
+ }
225
268
  else {
226
269
  db.__poolIdleSince = Date.now();
227
270
  self.pooldb.push(db);
@@ -0,0 +1,81 @@
1
+ /***************************************
2
+ *
3
+ * Tagged-template query API (Postgres.js-style)
4
+ *
5
+ * db.sql`SELECT * FROM EMP WHERE ID = ${id}` → lazy thenable query
6
+ * db.sql('COLUMN NAME') → quoted identifier
7
+ *
8
+ * Interpolated values become positional `?` parameters — never string
9
+ * concatenation — so the API is injection-safe by construction. A query
10
+ * embedded inside another tag is treated as a fragment: its text and
11
+ * parameters are spliced in place. Arrays expand to `?, ?, ?` lists for
12
+ * IN clauses. Execution is lazy (on await/then) and happens exactly once.
13
+ *
14
+ ***************************************/
15
+ import type { QueryOptions, QueryResult } from './types';
16
+ /** Executor provided by Database/Transaction: runs text+params, resolves rows
17
+ * (or the full QueryResult when options.withMeta is set). */
18
+ export type SqlExecutor = (text: string, params: any[], options?: QueryOptions) => Promise<any>;
19
+ /** A dynamically quoted identifier produced by sql('name'). */
20
+ export declare class SqlIdentifier {
21
+ name: string;
22
+ constructor(name: string);
23
+ }
24
+ /**
25
+ * Quote a (possibly dot-qualified) identifier for dialect 3: each part is
26
+ * wrapped in double quotes with embedded quotes doubled, so user input can
27
+ * never break out of the identifier position.
28
+ */
29
+ export declare function quoteIdentifier(name: string): string;
30
+ /** Compiled form of a tagged query: SQL text with `?` placeholders + params. */
31
+ export interface CompiledQuery {
32
+ text: string;
33
+ params: any[];
34
+ }
35
+ /**
36
+ * A lazily executed tagged query. Awaiting it (or calling then/catch/
37
+ * finally) runs it through the owning Database/Transaction exactly once;
38
+ * embedding it in another tag uses it as a fragment instead and never
39
+ * executes it.
40
+ */
41
+ export declare class SqlQuery<T = any> implements PromiseLike<T[]> {
42
+ readonly strings: readonly string[];
43
+ readonly values: any[];
44
+ private executor;
45
+ private queryOptions?;
46
+ private executed?;
47
+ private executedMeta?;
48
+ constructor(executor: SqlExecutor, strings: readonly string[], values: any[]);
49
+ /** The compiled SQL text (`?` placeholders) and parameter array. */
50
+ toQuery(): CompiledQuery;
51
+ /**
52
+ * Attach per-query options (timeout, signal, nestTables, …). Must be
53
+ * called before the query executes — options attached afterwards would
54
+ * be silently ignored, so that throws instead.
55
+ */
56
+ options(queryOptions: QueryOptions): this;
57
+ /** Execute resolving the full { rows, fields, affectedRows, … } result. */
58
+ withMeta(): Promise<QueryResult<T>>;
59
+ /**
60
+ * A query executes exactly once, in the shape of its first consumer
61
+ * (plain rows via then/await, or the full result via withMeta).
62
+ * Consuming it again in the OTHER shape cannot be honoured from the
63
+ * cached promise, so it throws rather than silently returning the
64
+ * wrong shape.
65
+ */
66
+ private run;
67
+ then<R1 = T[], R2 = never>(onfulfilled?: ((value: T[]) => R1 | PromiseLike<R1>) | null, onrejected?: ((reason: any) => R2 | PromiseLike<R2>) | null): Promise<R1 | R2>;
68
+ catch<R = never>(onrejected?: ((reason: any) => R | PromiseLike<R>) | null): Promise<T[] | R>;
69
+ finally(onfinally?: (() => void) | null): Promise<T[]>;
70
+ }
71
+ /** The dual-use tag: template tag executes, string call quotes an identifier. */
72
+ export interface SqlTag {
73
+ <T = any>(strings: TemplateStringsArray, ...values: any[]): SqlQuery<T>;
74
+ (identifier: string): SqlIdentifier;
75
+ }
76
+ /**
77
+ * Build the `sql` tag for a Database/Transaction. `executor` receives the
78
+ * compiled text, params and per-query options and must return a promise
79
+ * (Database/Transaction pass their queryAsync).
80
+ */
81
+ export declare function makeSqlTag(executor: SqlExecutor): SqlTag;