node-firebird 2.10.0 → 2.12.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 +236 -3
- package/lib/pool.d.ts +14 -1
- package/lib/pool.js +59 -16
- package/lib/sql-template.d.ts +81 -0
- package/lib/sql-template.js +162 -0
- package/lib/types.d.ts +136 -0
- package/lib/uri.js +53 -2
- package/lib/wire/connection.d.ts +7 -1
- package/lib/wire/connection.js +67 -46
- package/lib/wire/const.d.ts +5 -0
- package/lib/wire/const.js +14 -0
- package/lib/wire/database.d.ts +9 -0
- package/lib/wire/database.js +49 -14
- package/lib/wire/serialize.d.ts +3 -1
- package/lib/wire/serialize.js +4 -0
- package/lib/wire/transaction.d.ts +27 -0
- package/lib/wire/transaction.js +131 -13
- package/lib/wire/xsqlvar.d.ts +100 -0
- package/lib/wire/xsqlvar.js +219 -1
- package/package.json +1 -1
- package/src/pool.ts +57 -14
- package/src/sql-template.ts +196 -0
- package/src/types.ts +135 -1
- package/src/uri.ts +54 -2
- package/src/wire/connection.ts +77 -46
- package/src/wire/const.ts +15 -0
- package/src/wire/database.ts +52 -14
- package/src/wire/serialize.ts +8 -2
- package/src/wire/transaction.ts +140 -19
- package/src/wire/xsqlvar.ts +241 -0
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
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, 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
|
|
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
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
|
|
18
18
|
- [Using node-firebird with Express.js](#using-node-firebird-with-expressjs)
|
|
19
19
|
- [FAQ](#faq)
|
|
@@ -154,6 +154,14 @@ Notes:
|
|
|
154
154
|
|
|
155
155
|
### Connection options
|
|
156
156
|
|
|
157
|
+
Settings you leave out fall back to environment variables first — using
|
|
158
|
+
Firebird's own conventions (`ISC_USER` and `ISC_PASSWORD`, the same
|
|
159
|
+
variables isql honours) plus `FIREBIRD_HOST`, `FIREBIRD_PORT`,
|
|
160
|
+
`FIREBIRD_DATABASE` and `FIREBIRD_ROLE` — and to the built-in defaults
|
|
161
|
+
below (SYSDBA / masterkey / 127.0.0.1) only after that. Explicitly
|
|
162
|
+
provided options always win, so credentials can stay out of code the
|
|
163
|
+
same way `PGUSER`/`PGPASSWORD` work with pg.
|
|
164
|
+
|
|
157
165
|
```js
|
|
158
166
|
var options = {};
|
|
159
167
|
|
|
@@ -185,6 +193,8 @@ options.searchPath = undefined; // optional; ordered list/array of schemas to re
|
|
|
185
193
|
options.owner = undefined; // optional; owner of a newly created database — lets a superuser create a database for another user (create only, FB >= 6.0)
|
|
186
194
|
options.jsonAsObject = false; // optional; automatically stringify parameters and parse query results that contain JSON (FB >= 6.0)
|
|
187
195
|
options.namedPlaceholders = false; // set to true to allow :name placeholders in SQL with a { name: value } params object (see Named placeholders)
|
|
196
|
+
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
|
|
197
|
+
options.transformKeys = undefined; // 'camel' (FIRST_NAME → firstName) or a (key) => key mapper for object-row keys — see Transforming row keys (transformKeys). Overridable per query
|
|
188
198
|
options.typeCast = undefined; // optional; custom type parser called for every result column value (see Custom type parsers)
|
|
189
199
|
options.statementCacheSize = 0; // optional; per-connection LRU cache of prepared statements, 0 = disabled (see Prepared-statement cache)
|
|
190
200
|
```
|
|
@@ -292,9 +302,11 @@ The pool is an `EventEmitter` and exposes live counters, following the
|
|
|
292
302
|
```js
|
|
293
303
|
const pool = Firebird.pool(10, {
|
|
294
304
|
...options,
|
|
295
|
-
idleTimeoutMillis: 30000,
|
|
296
|
-
min: 2,
|
|
305
|
+
idleTimeoutMillis: 30000, // close connections idle for 30s…
|
|
306
|
+
min: 2, // …but always keep 2 alive
|
|
297
307
|
connectTimeout: 5000,
|
|
308
|
+
maxUses: 7500, // retire a connection after 7500 checkouts (pg's maxUses)
|
|
309
|
+
maxLifetimeMillis: 3600000, // …or 1h after creation (Postgres.js's max_lifetime)
|
|
298
310
|
});
|
|
299
311
|
|
|
300
312
|
pool.on('connect', (db) => console.log('new server connection'));
|
|
@@ -317,6 +329,12 @@ console.log({
|
|
|
317
329
|
connection they ever created (issue [#329](https://github.com/hgourvest/node-firebird/issues/329)).
|
|
318
330
|
The sweep also evicts idle connections whose socket has died, so callers
|
|
319
331
|
don't receive them after a server restart (issue [#343](https://github.com/hgourvest/node-firebird/issues/343)).
|
|
332
|
+
- `maxUses` and `maxLifetimeMillis` **recycle** physical connections: a
|
|
333
|
+
worn-out or over-age connection is closed for good when returned to the
|
|
334
|
+
pool (lifetime is also enforced on idle connections by the sweep, even
|
|
335
|
+
below `min`) and a replacement is created on demand. Recycling bounds
|
|
336
|
+
server-side resource drift on long-lived connections. Both default to 0
|
|
337
|
+
(off).
|
|
320
338
|
- `error` is a background-error channel (idle eviction failures and the
|
|
321
339
|
like); unlike a plain `EventEmitter`, it is only emitted when a listener
|
|
322
340
|
is attached, so existing applications keep working unchanged.
|
|
@@ -452,6 +470,63 @@ Firebird.attach(options, function (err, db) {
|
|
|
452
470
|
});
|
|
453
471
|
```
|
|
454
472
|
|
|
473
|
+
### Tagged-template queries (sql)
|
|
474
|
+
|
|
475
|
+
`db.sql` / `transaction.sql` offer a Postgres.js-style tagged-template API
|
|
476
|
+
on top of the regular parameter machinery. Interpolated values are bound
|
|
477
|
+
as positional parameters — never concatenated into the SQL — so the API is
|
|
478
|
+
injection-safe by construction:
|
|
479
|
+
|
|
480
|
+
```js
|
|
481
|
+
const id = 2;
|
|
482
|
+
const rows = await db.sql`SELECT NAME FROM EMP WHERE ID = ${id}`;
|
|
483
|
+
// → executes SELECT NAME FROM EMP WHERE ID = ? with params [2]
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The returned query is a **lazy thenable**: it runs when awaited (or via
|
|
487
|
+
`.then`/`.catch`/`.finally`), exactly once. Until then it can be embedded
|
|
488
|
+
in another tag as a **fragment**, splicing its text and parameters in
|
|
489
|
+
place:
|
|
490
|
+
|
|
491
|
+
```js
|
|
492
|
+
const filter = db.sql`DEPT_ID = ${1} AND ACTIVE = ${true}`;
|
|
493
|
+
const rows = await db.sql`SELECT * FROM EMP WHERE ${filter} ORDER BY ID`;
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Arrays expand to placeholder lists (for `IN`), and calling the tag with a
|
|
497
|
+
string produces a safely quoted, dot-qualified **identifier** for dynamic
|
|
498
|
+
table/column names:
|
|
499
|
+
|
|
500
|
+
```js
|
|
501
|
+
await db.sql`SELECT * FROM EMP WHERE ID IN (${[1, 2, 3]})`;
|
|
502
|
+
|
|
503
|
+
const col = 'NAME';
|
|
504
|
+
await db.sql`SELECT ${db.sql(col)} FROM ${db.sql('S1.EMP')}`;
|
|
505
|
+
// → SELECT "NAME" FROM "S1"."EMP"
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
`.options({...})` attaches per-query options (`timeout`, `signal`,
|
|
509
|
+
`nestTables`, …), `.withMeta()` executes resolving the
|
|
510
|
+
[full result object](#result-metadata-and-affected-rows-withmeta), and
|
|
511
|
+
`.toQuery()` returns the compiled `{ text, params }` without executing —
|
|
512
|
+
handy for logging and tests:
|
|
513
|
+
|
|
514
|
+
```js
|
|
515
|
+
const r = await db.sql`UPDATE EMP SET ACTIVE = false WHERE ID = ${9}`.withMeta();
|
|
516
|
+
// r.affectedRows === 1
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
Sharp edges, made loud instead of silent: a query executes once, in the
|
|
520
|
+
shape of its first consumer — consuming it again in the *other* shape
|
|
521
|
+
(plain `await` after `.withMeta()`, or vice versa) throws, as does
|
|
522
|
+
`.options()` after execution. Interpolating an **empty array** throws
|
|
523
|
+
(it would compile to invalid SQL like `IN ()`), and circular fragments
|
|
524
|
+
are rejected instead of overflowing the stack. The compiled text is
|
|
525
|
+
positional-only, so the [named placeholders](#named-placeholders)
|
|
526
|
+
rewriter is disabled for tagged queries — PSQL `:variable` references
|
|
527
|
+
in an `EXECUTE BLOCK` template are safe even with `namedPlaceholders:
|
|
528
|
+
true` on the connection.
|
|
529
|
+
|
|
455
530
|
### Named placeholders
|
|
456
531
|
|
|
457
532
|
With the `namedPlaceholders: true` connection option, SQL may use `:name`
|
|
@@ -492,6 +567,120 @@ the per-query option:
|
|
|
492
567
|
await db.queryAsync(execBlockSql, [], { namedPlaceholders: false });
|
|
493
568
|
```
|
|
494
569
|
|
|
570
|
+
### Nested result tables (nestTables)
|
|
571
|
+
|
|
572
|
+
In a `JOIN`, columns sharing a name overwrite each other in object rows —
|
|
573
|
+
`SELECT EMP.ID, DEPT.ID ...` leaves only one `ID` key. The `nestTables`
|
|
574
|
+
option (same as mysql2's) qualifies row keys by source table instead. It is
|
|
575
|
+
accepted at connection level and per query; the per-query value wins.
|
|
576
|
+
|
|
577
|
+
With `nestTables: true` each row nests one sub-object per table:
|
|
578
|
+
|
|
579
|
+
```js
|
|
580
|
+
const rows = await db.queryAsync(
|
|
581
|
+
'SELECT EMP.ID, EMP.NAME, DEPT.ID, DEPT.NAME FROM EMP JOIN DEPT ON DEPT.ID = EMP.DEPT_ID',
|
|
582
|
+
[], { nestTables: true });
|
|
583
|
+
// rows[0] = { EMP: { ID: 10, NAME: 'Ada' }, DEPT: { ID: 1, NAME: 'Engineering' } }
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
With a string separator the keys stay flat but qualified:
|
|
587
|
+
|
|
588
|
+
```js
|
|
589
|
+
const rows = await db.queryAsync(sql, [], { nestTables: '_' });
|
|
590
|
+
// rows[0] = { EMP_ID: 10, EMP_NAME: 'Ada', DEPT_ID: 1, DEPT_NAME: 'Engineering' }
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
The table qualifier is the query's relation alias when one is used, the
|
|
594
|
+
table name otherwise — so self-joins nest cleanly:
|
|
595
|
+
|
|
596
|
+
```js
|
|
597
|
+
const rows = await db.queryAsync(
|
|
598
|
+
'SELECT E.NAME, B.NAME FROM EMP E LEFT JOIN EMP B ON B.ID = E.BOSS_ID',
|
|
599
|
+
[], { nestTables: true });
|
|
600
|
+
// rows[0] = { E: { NAME: 'Grace' }, B: { NAME: 'Ada' } }
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Expression columns (no source table) qualify as `''`, exactly like mysql2:
|
|
604
|
+
they land under the `''` key when nesting (`row[''].ANSWER`) and get the
|
|
605
|
+
bare separator prefix in separator mode (`row._ANSWER`) — always prefixing
|
|
606
|
+
keeps qualified keys collision-free (a bare expression alias could
|
|
607
|
+
otherwise collide with a real `table<sep>column` key). Keys honour
|
|
608
|
+
`lowercase_keys`, and the option composes with
|
|
609
|
+
`typeCast`, `blobAsText` and `queryStream`. Object rows only: `db.execute`
|
|
610
|
+
array rows are positional and need no qualification. Works on every
|
|
611
|
+
supported Firebird version (the source-table metadata comes from the
|
|
612
|
+
statement describe, available since Firebird 2.0).
|
|
613
|
+
|
|
614
|
+
### Transforming row keys (transformKeys)
|
|
615
|
+
|
|
616
|
+
`transformKeys` rewrites object-row keys — the counterpart of
|
|
617
|
+
Postgres.js's `transform`. The built-in `'camel'` maps `FIRST_NAME` →
|
|
618
|
+
`firstName`; a custom `(key) => key` mapper gives full control. Accepted
|
|
619
|
+
at connection level and per query (per-query wins):
|
|
620
|
+
|
|
621
|
+
```js
|
|
622
|
+
const rows = await db.queryAsync('SELECT EMP_ID, FIRST_NAME FROM EMP_INFO', [],
|
|
623
|
+
{ transformKeys: 'camel' });
|
|
624
|
+
// rows[0] = { empId: 1, firstName: 'Ada' }
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
The transform runs after `lowercase_keys` and applies to both parts of
|
|
628
|
+
[`nestTables`](#nested-result-tables-nesttables) keys (`row.e.empId`).
|
|
629
|
+
Column *metadata* — `withMeta` `fields` and the `typeCast` hook — keeps
|
|
630
|
+
the raw server aliases. A custom mapper that throws falls back to the
|
|
631
|
+
untransformed key (with a console warning) rather than corrupting the
|
|
632
|
+
row decode.
|
|
633
|
+
|
|
634
|
+
### Result metadata and affected rows (withMeta)
|
|
635
|
+
|
|
636
|
+
By default, queries deliver bare rows and DML row counts are not reported.
|
|
637
|
+
The per-query `withMeta: true` option switches the result (callback and
|
|
638
|
+
promise APIs alike) to a full result object, the counterpart of pg's
|
|
639
|
+
`{ rows, rowCount, fields }` and mysql2's `affectedRows`:
|
|
640
|
+
|
|
641
|
+
```js
|
|
642
|
+
const r = await db.queryAsync('UPDATE EMP SET ACTIVE = false WHERE DEPT_ID = ?', [1],
|
|
643
|
+
{ withMeta: true });
|
|
644
|
+
// r = {
|
|
645
|
+
// rows: undefined, // rows array (SELECT), row object (RETURNING), or undefined
|
|
646
|
+
// fields: [...], // per-column metadata (see below)
|
|
647
|
+
// affectedRows: 2, // what the server actually changed
|
|
648
|
+
// recordCounts: { selectCount: 0, insertCount: 0, updateCount: 2, deleteCount: 0 },
|
|
649
|
+
// warnings: [], // isc_arg_warning entries from the execute response
|
|
650
|
+
// }
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
For DML (`INSERT`/`UPDATE`/`DELETE`, including `... RETURNING` and
|
|
654
|
+
`EXECUTE PROCEDURE`), `affectedRows` is the server-reported count
|
|
655
|
+
(`isc_info_sql_records`) and `recordCounts` breaks it down per verb — this
|
|
656
|
+
costs one extra lightweight info request per statement, which is why the
|
|
657
|
+
option is opt-in. For `SELECT`, `affectedRows` is simply the number of rows
|
|
658
|
+
returned (pg's `rowCount` convention) with no extra round-trip, and
|
|
659
|
+
`recordCounts` is absent.
|
|
660
|
+
|
|
661
|
+
Each entry in `fields` describes one output column — the same vocabulary
|
|
662
|
+
the [typeCast hook](#custom-type-parsers-typecast) receives, plus
|
|
663
|
+
nullability and the relation alias/schema:
|
|
664
|
+
|
|
665
|
+
```js
|
|
666
|
+
{ type: 448, typeName: 'VARYING', subType: 4, scale: 0, length: 80,
|
|
667
|
+
nullable: true, field: 'NAME', relation: 'EMP', relationAlias: '',
|
|
668
|
+
relationSchema: 'PUBLIC', alias: 'NAME' }
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
(`relationSchema` is filled on Firebird 6.0+; `subType` of a text column is
|
|
672
|
+
its character-set id.) In TypeScript, `queryAsync<T>(sql, params,
|
|
673
|
+
{ withMeta: true })` resolves to `QueryResult<T>` automatically. Server
|
|
674
|
+
warnings attached to any response — not just queries — are also emitted as
|
|
675
|
+
[`'warning'` driver events](#driver-events).
|
|
676
|
+
|
|
677
|
+
The option is honoured by `query`/`execute` (and their `*Async` wrappers),
|
|
678
|
+
on databases and transactions alike. It is ignored by the streaming APIs
|
|
679
|
+
(`sequentially`, `queryStream` — rows bypass the result there) and by
|
|
680
|
+
`executeBatch` (which has its own completion shape). For `EXECUTE
|
|
681
|
+
PROCEDURE`, `affectedRows` reflects DML the procedure performed — a
|
|
682
|
+
procedure that only returns values reports 0 alongside its row.
|
|
683
|
+
|
|
495
684
|
### Custom type parsers (typeCast)
|
|
496
685
|
|
|
497
686
|
The `typeCast` connection option lets you override how column values are
|
|
@@ -964,6 +1153,41 @@ Firebird.attach(options, function (err, db) {
|
|
|
964
1153
|
});
|
|
965
1154
|
```
|
|
966
1155
|
|
|
1156
|
+
#### Savepoints
|
|
1157
|
+
|
|
1158
|
+
`transaction.savepoint(work)` runs `work` inside a savepoint (Firebird
|
|
1159
|
+
1.5+): on resolve the savepoint is **released**, on reject the transaction
|
|
1160
|
+
**rolls back to the savepoint** — undoing only `work`'s changes — and the
|
|
1161
|
+
error is rethrown while the transaction itself stays usable. Calls nest;
|
|
1162
|
+
names are generated automatically. This is the counterpart of
|
|
1163
|
+
Postgres.js's `sql.savepoint()` and mirrors `db.withTransaction`'s style:
|
|
1164
|
+
|
|
1165
|
+
```js
|
|
1166
|
+
await db.withTransaction(async (tx) => {
|
|
1167
|
+
await tx.sql`INSERT INTO ORDERS VALUES (${1}, ${'paid'})`;
|
|
1168
|
+
|
|
1169
|
+
try {
|
|
1170
|
+
await tx.savepoint(async () => {
|
|
1171
|
+
await tx.sql`INSERT INTO AUDIT VALUES (${1}, ${'optional enrichment'})`;
|
|
1172
|
+
await maybeFailingStep(tx);
|
|
1173
|
+
});
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
// the AUDIT insert is undone; the ORDERS insert survives and the
|
|
1176
|
+
// transaction continues toward commit
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
If the rollback-to itself fails (e.g. the connection died), the original
|
|
1182
|
+
error is still thrown, with the rollback failure attached as
|
|
1183
|
+
`err.savepointRollbackError`. A release failure does **not** roll the
|
|
1184
|
+
work back — only a `work` failure does.
|
|
1185
|
+
|
|
1186
|
+
> **Note:** do not run sibling savepoints concurrently on one transaction
|
|
1187
|
+
> (`Promise.all`): Firebird's `RELEASE SAVEPOINT` also releases every
|
|
1188
|
+
> savepoint created after it, so interleaved siblings would release each
|
|
1189
|
+
> other. Nested (awaited) savepoints are fine.
|
|
1190
|
+
|
|
967
1191
|
### Driver Events
|
|
968
1192
|
|
|
969
1193
|
Driver events are synchronous notifications emitted on the `Database` object for connection-level operations. Subscribe with `db.on(eventName, handler)`.
|
|
@@ -1015,6 +1239,15 @@ Firebird.attach(options, function (err, db) {
|
|
|
1015
1239
|
// rows === Array
|
|
1016
1240
|
});
|
|
1017
1241
|
|
|
1242
|
+
db.on('warning', function (warning) {
|
|
1243
|
+
// fired (next tick) for every isc_arg_warning the server attaches to a
|
|
1244
|
+
// successful response — e.g. "parallel workers value capped" when
|
|
1245
|
+
// parallelWorkers exceeds the server maximum. The listener above is
|
|
1246
|
+
// registered inside the attach callback and still catches attach-time
|
|
1247
|
+
// warnings, because emission is deferred by one tick.
|
|
1248
|
+
// warning === { gdscode: Number, params?: Array, message: String }
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1018
1251
|
db.detach();
|
|
1019
1252
|
});
|
|
1020
1253
|
```
|
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
|
-
|
|
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
|
|
48
|
-
// connection lives at most ~1.5x
|
|
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(
|
|
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.
|
|
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;
|