node-firebird 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +211 -6
  2. package/lib/index.d.ts +5 -0
  3. package/lib/index.js +32 -1
  4. package/lib/pool.js +1 -1
  5. package/lib/srp.d.ts +3 -3
  6. package/lib/types.d.ts +165 -5
  7. package/lib/uri.js +2 -2
  8. package/lib/wire/connection.d.ts +92 -59
  9. package/lib/wire/connection.js +279 -58
  10. package/lib/wire/const.d.ts +9 -1
  11. package/lib/wire/const.js +23 -9
  12. package/lib/wire/database.d.ts +51 -26
  13. package/lib/wire/database.js +53 -20
  14. package/lib/wire/eventConnection.js +5 -3
  15. package/lib/wire/query-stream.d.ts +18 -0
  16. package/lib/wire/query-stream.js +73 -0
  17. package/lib/wire/serialize.d.ts +20 -2
  18. package/lib/wire/serialize.js +7 -0
  19. package/lib/wire/service.d.ts +42 -0
  20. package/lib/wire/service.js +145 -0
  21. package/lib/wire/socket.d.ts +3 -1
  22. package/lib/wire/socket.js +5 -2
  23. package/lib/wire/statement.d.ts +31 -19
  24. package/lib/wire/statement.js +1 -5
  25. package/lib/wire/transaction.d.ts +30 -18
  26. package/lib/wire/transaction.js +23 -4
  27. package/lib/wire/wire-types.d.ts +116 -0
  28. package/lib/wire/wire-types.js +10 -0
  29. package/lib/wire/xsqlvar.d.ts +57 -18
  30. package/lib/wire/xsqlvar.js +59 -0
  31. package/package.json +1 -1
  32. package/src/index.ts +36 -4
  33. package/src/messages.ts +1 -1
  34. package/src/pool.ts +1 -1
  35. package/src/srp.ts +6 -6
  36. package/src/types.ts +162 -5
  37. package/src/unix-crypt.ts +9 -9
  38. package/src/uri.ts +2 -2
  39. package/src/wire/connection.ts +475 -234
  40. package/src/wire/const.ts +23 -9
  41. package/src/wire/database.ts +101 -54
  42. package/src/wire/eventConnection.ts +8 -5
  43. package/src/wire/query-stream.ts +80 -0
  44. package/src/wire/serialize.ts +31 -0
  45. package/src/wire/service.ts +188 -6
  46. package/src/wire/socket.ts +17 -8
  47. package/src/wire/statement.ts +37 -29
  48. package/src/wire/transaction.ts +57 -32
  49. package/src/wire/wire-types.ts +127 -0
  50. package/src/wire/xsqlvar.ts +85 -7
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, 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, 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
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)
@@ -136,6 +136,7 @@ Available wrappers:
136
136
  - **database** — `db.queryAsync(sql, params?, options?)`, `executeAsync`, `executeBatchAsync(sql, rows, options?)`, `sequentiallyAsync(sql, params?, onRow, options?)`, `transactionAsync(options?)`, `newStatementAsync(sql)`, `attachEventAsync()`, `detachAsync()`, `dropAsync()`, `db.withTransaction(work, options?)`
137
137
  - **transaction** — `queryAsync`, `executeAsync`, `executeBatchAsync`, `sequentiallyAsync`, `newStatementAsync`, `commitAsync`, `rollbackAsync`, `commitRetainingAsync`, `rollbackRetainingAsync`
138
138
  - **statement** — `executeAsync(transaction, params?, options?)`, `executeBatchAsync(transaction, rows, options?)`, `fetchAsync`, `fetchScrollAsync`, `fetchAllAsync`, `closeAsync`, `dropAsync`, `releaseAsync`
139
+ - **service manager** — every [Service Manager function](#service-manager-functions) has an `*Async` counterpart (`backupAsync`, `restoreAsync`, `getUsersAsync`, `addUserAsync`, `getFbserverInfosAsync`, `startTraceAsync`, …); stream-producing functions resolve with the `Readable`, info functions with the info object
139
140
 
140
141
  Notes:
141
142
 
@@ -174,13 +175,19 @@ options.wireCrypt = Firebird.WIRE_CRYPT_ENABLE; // default; set to Firebird.WIRE
174
175
  options.pluginName = undefined; // optional, auto-negotiated; can be set to Firebird.AUTH_PLUGIN_SRP256, Firebird.AUTH_PLUGIN_SRP, or Firebird.AUTH_PLUGIN_LEGACY
175
176
  options.dbCryptConfig = undefined; // optional; database encryption key for encrypted databases. Use 'base64:<value>' for base64-encoded keys or plain text
176
177
  options.connectTimeout = 10000; // optional; timeout in ms for a single pool.get() attach operation (default: no timeout)
178
+ options.enableKeepAlive = true; // TCP keepalive probing to detect dead/stale connections (same option names as mysql2); set to false to disable
179
+ options.keepAliveInitialDelay = 60000; // ms a socket must be idle before the first keepalive probe (ignored when enableKeepAlive is false)
177
180
  options.parallelWorkers = undefined; // optional; request multiple thread workers for maintenance/index tasks (FB >= 5)
178
181
  options.maxInlineBlobSize = undefined; // optional; threshold size in bytes for inline blob transmission (default 65535, FB >= 5.0.3)
179
- options.maxNegotiatedProtocols = 10; // optional; limit maximum protocol versions negotiated (default 10 for compatibility, set to 11 for FB >= 6.0)
180
- options.defaultSchema = undefined; // optional; sets session CURRENT_SCHEMA at connect time (FB >= 6.0)
182
+ options.maxNegotiatedProtocols = undefined; // optional; cap how many protocol versions are offered, oldest first (default: all, up to Protocol 20; set to 10 to stop at Protocol 19)
183
+ options.defaultSchema = undefined; // optional; sets session CURRENT_SCHEMA at connect time by putting the schema first in the search path (FB >= 6.0)
181
184
  options.searchPath = undefined; // optional; ordered list/array of schemas to resolve unqualified object references (FB >= 6.0)
185
+ options.owner = undefined; // optional; owner of a newly created database — lets a superuser create a database for another user (create only, FB >= 6.0)
182
186
  options.jsonAsObject = false; // optional; automatically stringify parameters and parse query results that contain JSON (FB >= 6.0)
183
187
  options.namedPlaceholders = false; // set to true to allow :name placeholders in SQL with a { name: value } params object (see Named placeholders)
188
+ 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
189
+ options.typeCast = undefined; // optional; custom type parser called for every result column value (see Custom type parsers)
190
+ options.statementCacheSize = 0; // optional; per-connection LRU cache of prepared statements, 0 = disabled (see Prepared-statement cache)
184
191
  ```
185
192
 
186
193
  ### Connection URI strings
@@ -486,6 +493,178 @@ the per-query option:
486
493
  await db.queryAsync(execBlockSql, [], { namedPlaceholders: false });
487
494
  ```
488
495
 
496
+ ### Nested result tables (nestTables)
497
+
498
+ In a `JOIN`, columns sharing a name overwrite each other in object rows —
499
+ `SELECT EMP.ID, DEPT.ID ...` leaves only one `ID` key. The `nestTables`
500
+ option (same as mysql2's) qualifies row keys by source table instead. It is
501
+ accepted at connection level and per query; the per-query value wins.
502
+
503
+ With `nestTables: true` each row nests one sub-object per table:
504
+
505
+ ```js
506
+ const rows = await db.queryAsync(
507
+ 'SELECT EMP.ID, EMP.NAME, DEPT.ID, DEPT.NAME FROM EMP JOIN DEPT ON DEPT.ID = EMP.DEPT_ID',
508
+ [], { nestTables: true });
509
+ // rows[0] = { EMP: { ID: 10, NAME: 'Ada' }, DEPT: { ID: 1, NAME: 'Engineering' } }
510
+ ```
511
+
512
+ With a string separator the keys stay flat but qualified:
513
+
514
+ ```js
515
+ const rows = await db.queryAsync(sql, [], { nestTables: '_' });
516
+ // rows[0] = { EMP_ID: 10, EMP_NAME: 'Ada', DEPT_ID: 1, DEPT_NAME: 'Engineering' }
517
+ ```
518
+
519
+ The table qualifier is the query's relation alias when one is used, the
520
+ table name otherwise — so self-joins nest cleanly:
521
+
522
+ ```js
523
+ const rows = await db.queryAsync(
524
+ 'SELECT E.NAME, B.NAME FROM EMP E LEFT JOIN EMP B ON B.ID = E.BOSS_ID',
525
+ [], { nestTables: true });
526
+ // rows[0] = { E: { NAME: 'Grace' }, B: { NAME: 'Ada' } }
527
+ ```
528
+
529
+ Expression columns (no source table) qualify as `''`, exactly like mysql2:
530
+ they land under the `''` key when nesting (`row[''].ANSWER`) and get the
531
+ bare separator prefix in separator mode (`row._ANSWER`) — always prefixing
532
+ keeps qualified keys collision-free (a bare expression alias could
533
+ otherwise collide with a real `table<sep>column` key). Keys honour
534
+ `lowercase_keys`, and the option composes with
535
+ `typeCast`, `blobAsText` and `queryStream`. Object rows only: `db.execute`
536
+ array rows are positional and need no qualification. Works on every
537
+ supported Firebird version (the source-table metadata comes from the
538
+ statement describe, available since Firebird 2.0).
539
+
540
+ ### Custom type parsers (typeCast)
541
+
542
+ The `typeCast` connection option lets you override how column values are
543
+ decoded, per SQL type or per column — the same idea as mysql2's `typeCast`
544
+ and pg's `setTypeParser`. The hook is called for **every column value of
545
+ every result row** (including `NULL`s); whatever it returns becomes the
546
+ value in the row. Call `next()` to get the value the driver would produce
547
+ by default (after `blobAsText` / `jsonAsObject` are applied).
548
+
549
+ ```js
550
+ const Firebird = require('node-firebird');
551
+
552
+ Firebird.attach({
553
+ ...options,
554
+ typeCast: (column, next) => {
555
+ // dates as ISO strings instead of Date objects
556
+ if (column.typeName === 'DATE') {
557
+ const v = next();
558
+ return v === null ? null : v.toISOString().slice(0, 10);
559
+ }
560
+ // BIGINT columns as strings
561
+ if (column.type === Firebird.SQL_TYPES.SQL_INT64 && !column.scale) {
562
+ return String(next());
563
+ }
564
+ return next(); // everything else: default decoding
565
+ },
566
+ }, (err, db) => { /* ... */ });
567
+ ```
568
+
569
+ `column` describes the result column:
570
+
571
+ | Property | Meaning |
572
+ | :--------- | :------------------------------------------------------------------ |
573
+ | `type` | Firebird SQL type code — compare against `Firebird.SQL_TYPES.*` |
574
+ | `typeName` | Friendly name: `'VARYING'`, `'INT64'`, `'DATE'`, `'BLOB'`, ... |
575
+ | `subType` | Column subtype (`1` = text for BLOBs) |
576
+ | `scale` | Negative decimal scale for `NUMERIC`/`DECIMAL` (e.g. `-2`) |
577
+ | `length` | Declared length in bytes |
578
+ | `field` | Column name in the table |
579
+ | `relation` | Table name |
580
+ | `alias` | SELECT-list alias (the row key for object rows) |
581
+
582
+ Notes:
583
+
584
+ - Non-text BLOB columns reach the hook as the usual asynchronous fetch
585
+ function; text BLOBs with `blobAsText: true` reach it as the resolved
586
+ string.
587
+ - The hook must be a **pure function** of its inputs: when a response
588
+ spans multiple TCP packets the affected rows can be decoded more than
589
+ once, calling the hook again for the same value.
590
+ - The hook runs for every value on the hot row-decoding path — keep it
591
+ cheap, and prefer dispatching on `column.type`/`column.typeName` early.
592
+ - Exceptions thrown by the hook are caught: the default value is used and
593
+ a warning is printed. A throw cannot be allowed to escape into the wire
594
+ decoder, so validate inside the hook and encode failures in the value.
595
+
596
+ ### Prepared-statement cache
597
+
598
+ Setting `statementCacheSize` keeps a per-connection LRU cache of prepared
599
+ statements (like mysql2's statement cache): running the same SQL string
600
+ again transparently reuses the already-prepared server-side statement,
601
+ skipping the prepare round-trip. No API changes are needed — `db.query`,
602
+ `tx.query`, `sequentially`, `executeBatch` and the `*Async` wrappers all
603
+ benefit automatically.
604
+
605
+ ```js
606
+ Firebird.attach({ ...options, statementCacheSize: 100 }, (err, db) => {
607
+ // the second identical query reuses the prepared statement
608
+ db.query('SELECT * FROM t WHERE id = ?', [1], () => {
609
+ db.query('SELECT * FROM t WHERE id = ?', [2], () => { /* ... */ });
610
+ });
611
+ });
612
+ ```
613
+
614
+ How it works:
615
+
616
+ - The number is the maximum of **idle** statements kept per connection;
617
+ the least-recently-used statement is dropped when the limit is exceeded.
618
+ - A cached statement leaves the cache while in use, so concurrent runs of
619
+ the same SQL never share a server-side cursor — extra preparations run
620
+ in parallel and only one goes back into the cache.
621
+ - Statements that failed and DDL statements are never cached.
622
+ - Cache keys are exact SQL strings (after the `namedPlaceholders`
623
+ rewrite), so use parametrized queries to get hits.
624
+ - The legacy `cacheQuery: true` / `maxCachedQuery` options remain
625
+ supported and now map onto the same LRU cache (with a default limit of
626
+ 100 instead of the old unbounded map).
627
+
628
+ > **Note (DDL):** a statement prepared before a metadata change (e.g.
629
+ > `ALTER TABLE`) may fail when reused. If you mix DDL with hot queries on
630
+ > the same connection, keep the cache small or disabled.
631
+
632
+ ### Streaming rows with queryStream
633
+
634
+ `db.queryStream(sql, params, options)` returns an **object-mode
635
+ `Readable`** emitting one row per chunk — the counterpart of
636
+ `pg-query-stream` and mysql2's `.stream()`. It is built on
637
+ `sequentially()`'s backpressure: fetching from the server pauses while
638
+ the stream's buffer is full and resumes as the consumer drains it, so
639
+ constant memory is used regardless of the result size.
640
+
641
+ ```js
642
+ const { pipeline } = require('stream/promises');
643
+
644
+ // async iteration
645
+ for await (const row of db.queryStream('SELECT * FROM big_table')) {
646
+ console.log(row.ID);
647
+ }
648
+
649
+ // or piping into any Writable/Transform (HTTP response, CSV encoder, ...)
650
+ await pipeline(
651
+ db.queryStream('SELECT * FROM big_table WHERE grp = ?', [42]),
652
+ myCsvTransform,
653
+ res);
654
+ ```
655
+
656
+ - `db.queryStream` runs in its own transaction (like `db.query`);
657
+ `transaction.queryStream` runs inside your transaction, which is *not*
658
+ committed when the stream ends.
659
+ - Destroying the stream early — including an error mid-`pipeline()` —
660
+ aborts the fetch and releases the statement; the connection stays
661
+ usable.
662
+ - Options: everything `query` accepts (e.g. `signal`), plus
663
+ `highWaterMark` (rows buffered before fetching pauses, default 16) and
664
+ `asObject: false` for array rows.
665
+ - Rows go through the regular decode path, so `typeCast`, `blobAsText`
666
+ and `jsonAsObject` all apply.
667
+
489
668
  ### Tablespaces and Schema Partitioning (Firebird 6.0+)
490
669
 
491
670
  For Firebird 6.0+ (Protocol 20+), you can create and manage physical tablespace locations and logical schema namespaces, optionally partitioning schemas into specific physical tablespaces.
@@ -1020,6 +1199,26 @@ var fbsvc = {
1020
1199
 
1021
1200
  ```
1022
1201
 
1202
+ Every function also has a promise-returning `*Async` counterpart (no
1203
+ callback argument): stream-producing functions resolve with the
1204
+ `Readable`, info functions with the info object.
1205
+
1206
+ ```js
1207
+ const svc = await Firebird.attachAsync({ ...options, manager: true });
1208
+ try {
1209
+ const info = await svc.getFbserverInfosAsync();
1210
+ console.log(info.fbversion);
1211
+
1212
+ const backup = await svc.backupAsync({
1213
+ database: '/DB/MYDB.FDB',
1214
+ files: [{ filename: '/DB/MYDB.FBK' }]
1215
+ });
1216
+ for await (const line of backup) console.log(line);
1217
+ } finally {
1218
+ await svc.detachAsync();
1219
+ }
1220
+ ```
1221
+
1023
1222
  ### Backup Service example
1024
1223
 
1025
1224
  ```js
@@ -1758,13 +1957,13 @@ db.transaction(function (err, tx) {
1758
1957
 
1759
1958
  #### Is the wire protocol version hard-coded?
1760
1959
 
1761
- No. node-firebird negotiates the highest protocol version both the client and server support, up to `options.maxNegotiatedProtocols` (default `10`, i.e. Protocol 19 see [Protocol Implementation Status](ROADMAP.md#4-protocol-implementation-status) in the roadmap for the full version table). Raise it if you're on Firebird 6.0 and want to attempt Protocol 20:
1960
+ No. node-firebird negotiates the highest protocol version both the client and server support up to Protocol 20 (Firebird 6.0) by default; see [Protocol Implementation Status](ROADMAP.md#4-protocol-implementation-status) in the roadmap for the full version table. Servers ignore protocol versions they do not know, so offering the full list is safe on old servers too. To cap negotiation at an older protocol, limit how many versions are offered (the list is ordered oldest first):
1762
1961
 
1763
1962
  ```js
1764
- options.maxNegotiatedProtocols = 11; // offers Protocol 20 as well as 19
1963
+ options.maxNegotiatedProtocols = 10; // stop at Protocol 19 (pre-Firebird 6 behavior)
1765
1964
  ```
1766
1965
 
1767
- The default is capped at 10 (Protocol 19) rather than the full list because Protocol 20 has a known query-preparation hang on some Firebird 6.0 builds see the "Firebird 6 and Beyond" note in [ROADMAP.md](ROADMAP.md#4-protocol-implementation-status).
1966
+ Historical note: Protocol 20 used to be excluded by default because of a query-preparation hang the client did not send the `p_sqlst_flags` field that protocol 20 added to `op_prepare_statement`, leaving the server blocked mid-packet. The field is now sent and Protocol 20 is fully negotiated.
1768
1967
 
1769
1968
  #### BLOB reads/writes are very slow, especially for large files over a remote connection
1770
1969
 
@@ -1821,6 +2020,12 @@ db.query('SELECT * FROM ACTORS WHERE NAME LIKE ?', ['James Wick%'], function (er
1821
2020
  });
1822
2021
  ```
1823
2022
 
2023
+ #### attach() *sometimes* fails with gdscode 335544472 ("Your user name and password are not defined") even though the credentials are correct
2024
+
2025
+ If the failure is intermittent — the same code with the same credentials succeeds on most attempts and fails on others — update the driver: versions before 2.8.1 had two serialization mismatches in the SRP (Srp/Srp256/384/512) proof-of-password computation that made roughly 1 in 80 attaches fail with exactly this error (whenever the ephemeral SRP values happened to have a leading zero byte). Fixed in 2.8.1; see [issue #421](https://github.com/hgourvest/node-firebird/issues/421) and [issue #347](https://github.com/hgourvest/node-firebird/issues/347). Switching the user to `Legacy_UserManager`/`Legacy_Auth` "fixed" it on older versions only because that avoids the SRP code path entirely — with 2.8.1+ this workaround is no longer needed.
2026
+
2027
+ If the failure is consistent, the credentials really don't match an account for the authentication plugin in use: check `AuthServer`/`UserManager` in `firebird.conf` and remember that SRP and Legacy user managers keep separate password stores — a user created under one plugin does not automatically exist for the other.
2028
+
1824
2029
  ## Contributing
1825
2030
 
1826
2031
  Contributions are welcome — code, documentation, and bug reports alike.
package/lib/index.d.ts CHANGED
@@ -24,6 +24,11 @@ export declare const ISOLATION_REPEATABLE_READ: number[];
24
24
  export declare const ISOLATION_SERIALIZABLE: number[];
25
25
  export declare const ISOLATION_READ_COMMITTED_READ_ONLY: number[];
26
26
  export declare const escape: typeof escapeValue;
27
+ /**
28
+ * Firebird SQL type codes, as seen in `column.type` inside a `typeCast`
29
+ * hook (each code also has a friendly `column.typeName`).
30
+ */
31
+ export declare const SQL_TYPES: Readonly<Record<string, number>>;
27
32
  /**
28
33
  * The most recent Connection created by attach()/create()/attachOrCreate().
29
34
  * Kept for backwards compatibility with the previous CommonJS module where
package/lib/index.js CHANGED
@@ -17,7 +17,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
17
17
  return (mod && mod.__esModule) ? mod : { "default": mod };
18
18
  };
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
- exports.parseNamedPlaceholders = exports.parseConnectionString = exports.parseConnectionUri = exports.connection = exports.escape = exports.ISOLATION_READ_COMMITTED_READ_ONLY = exports.ISOLATION_SERIALIZABLE = exports.ISOLATION_REPEATABLE_READ = exports.ISOLATION_READ_COMMITTED = exports.ISOLATION_READ_UNCOMMITTED = exports.WIRE_CRYPT_ENABLE = exports.WIRE_CRYPT_DISABLE = exports.AUTH_PLUGIN_SRP512 = exports.AUTH_PLUGIN_SRP384 = exports.AUTH_PLUGIN_SRP256 = exports.AUTH_PLUGIN_SRP = exports.AUTH_PLUGIN_LEGACY = exports.GDSCode = void 0;
20
+ exports.parseNamedPlaceholders = exports.parseConnectionString = exports.parseConnectionUri = exports.connection = exports.SQL_TYPES = exports.escape = exports.ISOLATION_READ_COMMITTED_READ_ONLY = exports.ISOLATION_SERIALIZABLE = exports.ISOLATION_REPEATABLE_READ = exports.ISOLATION_READ_COMMITTED = exports.ISOLATION_READ_UNCOMMITTED = exports.WIRE_CRYPT_ENABLE = exports.WIRE_CRYPT_DISABLE = exports.AUTH_PLUGIN_SRP512 = exports.AUTH_PLUGIN_SRP384 = exports.AUTH_PLUGIN_SRP256 = exports.AUTH_PLUGIN_SRP = exports.AUTH_PLUGIN_LEGACY = exports.GDSCode = void 0;
21
21
  exports.attach = attach;
22
22
  exports.drop = drop;
23
23
  exports.create = create;
@@ -63,6 +63,35 @@ exports.ISOLATION_REPEATABLE_READ = const_1.default.ISOLATION_REPEATABLE_READ;
63
63
  exports.ISOLATION_SERIALIZABLE = const_1.default.ISOLATION_SERIALIZABLE;
64
64
  exports.ISOLATION_READ_COMMITTED_READ_ONLY = const_1.default.ISOLATION_READ_COMMITTED_READ_ONLY;
65
65
  exports.escape = utils_1.escape;
66
+ /**
67
+ * Firebird SQL type codes, as seen in `column.type` inside a `typeCast`
68
+ * hook (each code also has a friendly `column.typeName`).
69
+ */
70
+ exports.SQL_TYPES = Object.freeze({
71
+ SQL_TEXT: const_1.default.SQL_TEXT,
72
+ SQL_VARYING: const_1.default.SQL_VARYING,
73
+ SQL_SHORT: const_1.default.SQL_SHORT,
74
+ SQL_LONG: const_1.default.SQL_LONG,
75
+ SQL_FLOAT: const_1.default.SQL_FLOAT,
76
+ SQL_DOUBLE: const_1.default.SQL_DOUBLE,
77
+ SQL_D_FLOAT: const_1.default.SQL_D_FLOAT,
78
+ SQL_TIMESTAMP: const_1.default.SQL_TIMESTAMP,
79
+ SQL_BLOB: const_1.default.SQL_BLOB,
80
+ SQL_ARRAY: const_1.default.SQL_ARRAY,
81
+ SQL_QUAD: const_1.default.SQL_QUAD,
82
+ SQL_TYPE_TIME: const_1.default.SQL_TYPE_TIME,
83
+ SQL_TYPE_DATE: const_1.default.SQL_TYPE_DATE,
84
+ SQL_INT64: const_1.default.SQL_INT64,
85
+ SQL_INT128: const_1.default.SQL_INT128,
86
+ SQL_TIMESTAMP_TZ: const_1.default.SQL_TIMESTAMP_TZ,
87
+ SQL_TIMESTAMP_TZ_EX: const_1.default.SQL_TIMESTAMP_TZ_EX,
88
+ SQL_TIME_TZ: const_1.default.SQL_TIME_TZ,
89
+ SQL_TIME_TZ_EX: const_1.default.SQL_TIME_TZ_EX,
90
+ SQL_DEC16: const_1.default.SQL_DEC16,
91
+ SQL_DEC34: const_1.default.SQL_DEC34,
92
+ SQL_BOOLEAN: const_1.default.SQL_BOOLEAN,
93
+ SQL_NULL: const_1.default.SQL_NULL,
94
+ });
66
95
  function attach(options, callback) {
67
96
  options = (0, uri_1.normalizeOptions)(options);
68
97
  var host = options.host || const_1.default.DEFAULT_HOST;
@@ -135,6 +164,8 @@ function attachOrCreate(options, callback) {
135
164
  if (!err) {
136
165
  if (self.db)
137
166
  self.db.emit('connect', ret);
167
+ // DatabaseCallback stays permissive (db non-optional) for
168
+ // API users; internally the error path passes no db
138
169
  (0, callback_1.doCallback)(ret, callback);
139
170
  return;
140
171
  }
package/lib/pool.js CHANGED
@@ -141,7 +141,7 @@ class Pool extends events_1.default.EventEmitter {
141
141
  return self;
142
142
  if ((self.dbinuse + self._creating) >= self.max)
143
143
  return self;
144
- var cb = self.pending.shift();
144
+ const cb = self.pending.shift();
145
145
  if (!cb)
146
146
  return self;
147
147
  if (self.pooldb.length) {
package/lib/srp.d.ts CHANGED
@@ -22,7 +22,7 @@ export declare function clientSeed(a?: bigint): KeyPair;
22
22
  * @param b BigInt Server private key.
23
23
  * @returns {{private: BigInt, public: BigInt}}
24
24
  */
25
- export declare function serverSeed(user: string, password: string, salt: Buffer, b?: bigint | string, hashAlgo?: string): KeyPair;
25
+ export declare function serverSeed(user: string, password: string, salt: Buffer | string, b?: bigint | string, hashAlgo?: string): KeyPair;
26
26
  /**
27
27
  * Server session secret.
28
28
  *
@@ -34,11 +34,11 @@ export declare function serverSeed(user: string, password: string, salt: Buffer,
34
34
  * @param b BigInt Server private key.
35
35
  * @returns {BigInt}
36
36
  */
37
- export declare function serverSession(user: string, password: string, salt: Buffer, A: bigint, B: bigint, b: bigint, hashAlgo?: string): bigint;
37
+ export declare function serverSession(user: string, password: string, salt: Buffer | string, A: bigint, B: bigint, b: bigint, hashAlgo?: string): bigint;
38
38
  /**
39
39
  * M = H(H(N) xor H(g), H(I), s, A, B, K)
40
40
  */
41
- export declare function clientProof(user: string, password: string, salt: Buffer, A: bigint, B: bigint, a: bigint, hashAlgo?: string): ClientProof;
41
+ export declare function clientProof(user: string, password: string, salt: Buffer | string, A: bigint, B: bigint, a: bigint, hashAlgo?: string): ClientProof;
42
42
  /**
43
43
  * Pad hex string.
44
44
  */
package/lib/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Readable } from 'stream';
1
2
  export type DatabaseCallback = (err: any, db: Database) => void;
2
3
  export type TransactionCallback = (err: any, transaction: Transaction) => void;
3
4
  export type QueryCallback = (err: any, result: any[]) => void;
@@ -114,6 +115,27 @@ export type QueryOptions = {
114
115
  * it cancels whatever is currently executing on the connection.
115
116
  */
116
117
  signal?: AbortSignal;
118
+ /**
119
+ * Per-query override of the `nestTables` connection option (mysql2
120
+ * semantics). `true` nests each object row by source table:
121
+ * `row[table][column]` — the table key is the query's relation alias
122
+ * when one is used (`FROM emp e` → `row.E`), the table name otherwise,
123
+ * and `''` for expression columns. A string separator flattens keys
124
+ * instead: `nestTables: '_'` → `row.EMP_NAME`; expression columns get
125
+ * the bare separator prefix (`row._ANSWER`, as in mysql2). Keys honour
126
+ * `lowercase_keys`. Object rows only — `db.execute` array rows are
127
+ * unaffected.
128
+ */
129
+ nestTables?: boolean | string;
130
+ };
131
+ export type QueryStreamOptions = QueryOptions & {
132
+ /**
133
+ * Rows buffered internally before fetching pauses (object-mode
134
+ * Readable highWaterMark, default 16).
135
+ */
136
+ highWaterMark?: number;
137
+ /** Emit array rows instead of objects (like db.execute). */
138
+ asObject?: boolean;
117
139
  };
118
140
  export interface Database {
119
141
  detach(callback?: SimpleCallback): Database;
@@ -124,6 +146,13 @@ export interface Database {
124
146
  /** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
125
147
  executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
126
148
  sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
149
+ /**
150
+ * Run `query` and return an object-mode Readable emitting one row per
151
+ * chunk, with backpressure (fetching pauses while the buffer is full).
152
+ * Runs in its own transaction. Destroying the stream early aborts the
153
+ * fetch and releases the statement.
154
+ */
155
+ queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
127
156
  drop(callback: SimpleCallback): void;
128
157
  escape(value: any): string;
129
158
  attachEvent(callback: any): this;
@@ -160,6 +189,12 @@ export interface Transaction {
160
189
  /** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
161
190
  executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
162
191
  sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
192
+ /**
193
+ * Run `query` inside this transaction and return an object-mode
194
+ * Readable emitting one row per chunk, with backpressure. The
195
+ * transaction is NOT committed when the stream ends.
196
+ */
197
+ queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
163
198
  commit(callback?: SimpleCallback): void;
164
199
  commitRetaining(callback?: SimpleCallback): void;
165
200
  rollback(callback?: SimpleCallback): void;
@@ -231,6 +266,26 @@ export interface Options {
231
266
  * per-query `namedPlaceholders: false` override.
232
267
  */
233
268
  namedPlaceholders?: boolean;
269
+ /**
270
+ * Qualify object-row keys by source table (same option as mysql2), so
271
+ * JOINed columns with the same name stop overwriting each other:
272
+ * `true` nests each row as `row[table][column]`; a string separator
273
+ * flattens to `row['table' + sep + 'column']`. See
274
+ * `QueryOptions.nestTables` for the exact key rules. Applies wherever
275
+ * object rows are produced (query / sequentially / queryStream);
276
+ * array rows (execute) are unaffected. Overridable per query.
277
+ */
278
+ nestTables?: boolean | string;
279
+ /**
280
+ * TCP keepalive probing to detect dead/stale connections (same option
281
+ * names as mysql2). On by default; set false to disable.
282
+ */
283
+ enableKeepAlive?: boolean;
284
+ /**
285
+ * Milliseconds a socket must be idle before the first TCP keepalive
286
+ * probe is sent (default 60000). Ignored when enableKeepAlive is false.
287
+ */
288
+ keepAliveInitialDelay?: number;
234
289
  pluginName?: string;
235
290
  parallelWorkers?: number;
236
291
  maxInlineBlobSize?: number;
@@ -262,11 +317,11 @@ export interface Options {
262
317
  /**
263
318
  * **Firebird 6.0+ only (Protocol 20+)**
264
319
  *
265
- * Sets the session's current schema at connection time. Equivalent to
266
- * executing `SET SCHEMA <name>` immediately after connecting.
267
- *
268
- * Unqualified object references (tables, procedures, etc.) that do not
269
- * match any schema in the `searchPath` fall back to `PUBLIC`.
320
+ * Sets the session's current schema at connection time. `CURRENT_SCHEMA`
321
+ * in Firebird is the first existing schema of the search path, so this
322
+ * option is implemented by putting the schema at the front of the
323
+ * `searchPath` sent to the server (with `PUBLIC` kept as a fallback when
324
+ * no explicit `searchPath` is given).
270
325
  *
271
326
  * Example: `defaultSchema: 'myapp'`
272
327
  */
@@ -288,6 +343,18 @@ export interface Options {
288
343
  * (typically `PUBLIC` then `SYSTEM`).
289
344
  */
290
345
  searchPath?: string | string[];
346
+ /**
347
+ * **Firebird 6.0+ only**
348
+ *
349
+ * Owner of a newly created database (`isc_dpb_owner`), allowing a
350
+ * superuser to create a database owned by another user
351
+ * ([firebird#7718](https://github.com/FirebirdSQL/firebird/issues/7718)).
352
+ * Only honored by `create`/`attachOrCreate` when the database is
353
+ * created; ignored on plain attach and by older servers.
354
+ *
355
+ * Example: `owner: 'APP_OWNER'`
356
+ */
357
+ owner?: string;
291
358
  /**
292
359
  * **Firebird 6.0+ only (Protocol 20+)**
293
360
  *
@@ -296,7 +363,55 @@ export interface Options {
296
363
  * text/BLOB columns back into JavaScript objects/arrays.
297
364
  */
298
365
  jsonAsObject?: boolean;
366
+ /**
367
+ * Custom type parser (mysql2-style). Called for every column value of
368
+ * every result row (including NULLs); whatever it returns becomes the
369
+ * value in the row. Call `next()` to get the value the driver would
370
+ * produce by default (after `blobAsText`/`jsonAsObject` are applied).
371
+ *
372
+ * ```js
373
+ * typeCast: (column, next) =>
374
+ * column.typeName === 'INT64' ? Number(next()) : next()
375
+ * ```
376
+ *
377
+ * Non-text BLOB columns reach the hook as the usual fetch function;
378
+ * text BLOBs with `blobAsText` reach it as the resolved string. The
379
+ * hook must be a pure function: a row can be decoded more than once
380
+ * when a response spans TCP packets.
381
+ */
382
+ typeCast?: TypeCastFunction;
383
+ /**
384
+ * Per-connection LRU cache of prepared statements (like mysql2's
385
+ * statement cache). `db.query`/`tx.query` and friends transparently
386
+ * reuse the prepared handle for a repeated SQL string, skipping the
387
+ * prepare round-trip on hot paths. The number is the maximum of idle
388
+ * cached statements; least-recently-used ones are dropped over the
389
+ * limit. 0 / unset = disabled. Statements that failed and DDL are
390
+ * never cached; concurrent runs of the same SQL never share a
391
+ * statement (extra preparations are simply not cached).
392
+ */
393
+ statementCacheSize?: number;
299
394
  }
395
+ /** Column metadata passed to the {@link Options.typeCast} hook. */
396
+ export interface TypeCastColumn {
397
+ /** Firebird SQL type code (see the exported `SQL_TYPES` map). */
398
+ type: number;
399
+ /** Friendly name of the type code: 'VARYING', 'INT64', 'BLOB', ... */
400
+ typeName: string;
401
+ /** Column subtype (e.g. 1 = text for BLOBs; charset id for strings). */
402
+ subType?: number;
403
+ /** Negative decimal scale for NUMERIC/DECIMAL columns (e.g. -2). */
404
+ scale?: number;
405
+ /** Declared length in bytes. */
406
+ length?: number;
407
+ /** Column name in the table. */
408
+ field?: string;
409
+ /** Table (relation) name. */
410
+ relation?: string;
411
+ /** Alias used in the SELECT list (the row key for object rows). */
412
+ alias?: string;
413
+ }
414
+ export type TypeCastFunction = (column: TypeCastColumn, next: () => any) => any;
300
415
  export interface SvcMgrOptions extends Options {
301
416
  manager: true;
302
417
  }
@@ -506,4 +621,49 @@ export interface ServiceManager {
506
621
  hasRunningAction(options: ReadableOptions, callback: ReadableCallback): void;
507
622
  readusers(options: ReadableOptions, callback: ReadableCallback): void;
508
623
  readlimbo(options: ReadableOptions, callback: ReadableCallback): void;
624
+ detachAsync(force?: boolean): Promise<void>;
625
+ backupAsync(options: BackupOptions): Promise<NodeJS.ReadableStream>;
626
+ nbackupAsync(options: BackupOptions): Promise<NodeJS.ReadableStream>;
627
+ restoreAsync(options: RestoreOptions): Promise<NodeJS.ReadableStream>;
628
+ nrestoreAsync(options: NRestoreOptions): Promise<NodeJS.ReadableStream>;
629
+ setDialectAsync(db: string, dialect: 1 | 3): Promise<NodeJS.ReadableStream>;
630
+ setSweepintervalAsync(db: string, interval: number): Promise<any>;
631
+ setCachebufferAsync(db: string, nbpages: any): Promise<NodeJS.ReadableStream>;
632
+ BringOnlineAsync(db: string): Promise<NodeJS.ReadableStream>;
633
+ ShutdownAsync(db: string, kind: ShutdownKind, delay: number, mode?: ShutdownMode): Promise<NodeJS.ReadableStream>;
634
+ setShadowAsync(db: string, val: boolean): Promise<NodeJS.ReadableStream>;
635
+ setForcewriteAsync(db: string, val: boolean): Promise<NodeJS.ReadableStream>;
636
+ setReservespaceAsync(db: string, val: boolean): Promise<NodeJS.ReadableStream>;
637
+ setReadonlyModeAsync(db: string): Promise<NodeJS.ReadableStream>;
638
+ setReadwriteModeAsync(db: string): Promise<NodeJS.ReadableStream>;
639
+ validateAsync(options: ValidateOptions): Promise<NodeJS.ReadableStream>;
640
+ commitAsync(db: string, transactid: number): Promise<NodeJS.ReadableStream>;
641
+ rollbackAsync(db: string, transactid: number): Promise<NodeJS.ReadableStream>;
642
+ recoverAsync(db: string, transactid: number): Promise<NodeJS.ReadableStream>;
643
+ getStatsAsync(options: StatsOptions): Promise<NodeJS.ReadableStream>;
644
+ getLogAsync(options: ReadableOptions): Promise<NodeJS.ReadableStream>;
645
+ getUsersAsync(username?: string | null): Promise<ServerInfo>;
646
+ addUserAsync(username: string, password: string, info?: UserInfo): Promise<NodeJS.ReadableStream>;
647
+ editUserAsync(username: string, info: UserInfo): Promise<NodeJS.ReadableStream>;
648
+ removeUserAsync(username: string, rolename?: string | null): Promise<NodeJS.ReadableStream>;
649
+ getFbserverInfosAsync(infos?: ServerInfoReq, options?: {
650
+ buffersize?: number;
651
+ timeout?: number;
652
+ }): Promise<ServerInfo>;
653
+ startTraceAsync(options: TraceOptions): Promise<NodeJS.ReadableStream>;
654
+ suspendTraceAsync(options: TraceOptions): Promise<NodeJS.ReadableStream>;
655
+ resumeTraceAsync(options: TraceOptions): Promise<NodeJS.ReadableStream>;
656
+ stopTraceAsync(options: TraceOptions): Promise<NodeJS.ReadableStream>;
657
+ getTraceListAsync(options?: ReadableOptions): Promise<NodeJS.ReadableStream>;
658
+ readlineAsync(options?: ReadableOptions): Promise<{
659
+ result: number;
660
+ line: string;
661
+ }>;
662
+ readeofAsync(options?: ReadableOptions): Promise<{
663
+ result: number;
664
+ line: string;
665
+ }>;
666
+ hasRunningActionAsync(options?: ReadableOptions): Promise<any>;
667
+ readusersAsync(options?: ReadableOptions): Promise<any>;
668
+ readlimboAsync(options?: ReadableOptions): Promise<any>;
509
669
  }
package/lib/uri.js CHANGED
@@ -15,14 +15,14 @@ exports.normalizeOptions = normalizeOptions;
15
15
  */
16
16
  const BOOLEAN_KEYS = new Set([
17
17
  'lowercase_keys', 'blobAsText', 'wireCompression', 'manager',
18
- 'namedPlaceholders',
18
+ 'namedPlaceholders', 'enableKeepAlive',
19
19
  ]);
20
20
  /** Option keys coerced to number when they arrive as URI query parameters. */
21
21
  const NUMBER_KEYS = new Set([
22
22
  'port', 'pageSize', 'timeout', 'retryConnectionInterval',
23
23
  'blobChunkSize', 'blobReadChunkSize', 'wireCrypt', 'parallelWorkers',
24
24
  'maxInlineBlobSize', 'maxNegotiatedProtocols', 'connectTimeout',
25
- 'min', 'idleTimeoutMillis',
25
+ 'min', 'idleTimeoutMillis', 'keepAliveInitialDelay',
26
26
  ]);
27
27
  function coerce(key, value) {
28
28
  if (BOOLEAN_KEYS.has(key)) {