node-firebird 2.3.4 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +574 -20
  2. package/lib/callback.d.ts +20 -0
  3. package/lib/callback.js +6 -16
  4. package/lib/firebird.msg.json +1370 -1370
  5. package/lib/gdscodes.d.ts +1519 -1520
  6. package/lib/gdscodes.js +5 -5
  7. package/lib/ieee754-decimal.d.ts +38 -0
  8. package/lib/ieee754-decimal.js +71 -118
  9. package/lib/index.d.ts +37 -338
  10. package/lib/index.js +97 -86
  11. package/lib/messages.d.ts +6 -0
  12. package/lib/messages.js +135 -162
  13. package/lib/pool.d.ts +23 -0
  14. package/lib/pool.js +34 -47
  15. package/lib/srp.d.ts +51 -0
  16. package/lib/srp.js +62 -72
  17. package/lib/types.d.ts +375 -0
  18. package/lib/types.js +22 -0
  19. package/lib/unix-crypt.d.ts +1 -0
  20. package/lib/unix-crypt.js +126 -152
  21. package/lib/utils.d.ts +16 -0
  22. package/lib/utils.js +24 -48
  23. package/lib/wire/connection.d.ts +100 -0
  24. package/lib/wire/connection.js +1029 -1009
  25. package/lib/wire/const.d.ts +567 -0
  26. package/lib/wire/const.js +620 -599
  27. package/lib/wire/database.d.ts +57 -0
  28. package/lib/wire/database.js +111 -108
  29. package/lib/wire/eventConnection.d.ts +16 -0
  30. package/lib/wire/eventConnection.js +29 -31
  31. package/lib/wire/fbEventManager.d.ts +45 -0
  32. package/lib/wire/fbEventManager.js +38 -57
  33. package/lib/wire/serialize.d.ts +97 -0
  34. package/lib/wire/serialize.js +42 -141
  35. package/lib/wire/service.d.ts +66 -0
  36. package/lib/wire/service.js +374 -401
  37. package/lib/wire/socket.d.ts +59 -0
  38. package/lib/wire/socket.js +79 -40
  39. package/lib/wire/statement.d.ts +25 -0
  40. package/lib/wire/statement.js +13 -9
  41. package/lib/wire/transaction.d.ts +21 -0
  42. package/lib/wire/transaction.js +33 -61
  43. package/lib/wire/xsqlvar.d.ts +175 -0
  44. package/lib/wire/xsqlvar.js +169 -269
  45. package/package.json +12 -4
  46. package/src/callback.ts +55 -0
  47. package/{poc/node_modules/node-firebird/lib/gdscodes.js → src/gdscodes.ts} +3 -3
  48. package/{poc/node_modules/node-firebird/lib/ieee754-decimal.js → src/ieee754-decimal.ts} +7 -14
  49. package/src/index.ts +157 -0
  50. package/{poc/node_modules/node-firebird/lib/messages.js → src/messages.ts} +163 -162
  51. package/src/pool.ts +209 -0
  52. package/{poc/node_modules/node-firebird/lib/srp.js → src/srp.ts} +74 -33
  53. package/src/types.ts +419 -0
  54. package/{poc/node_modules/node-firebird/lib/unix-crypt.js → src/unix-crypt.ts} +6 -12
  55. package/{poc/node_modules/node-firebird/lib/utils.js → src/utils.ts} +11 -20
  56. package/{poc/node_modules/node-firebird/lib/wire/connection.js → src/wire/connection.ts} +611 -129
  57. package/{poc/node_modules/node-firebird/lib/wire/const.js → src/wire/const.ts} +96 -9
  58. package/{poc/node_modules/node-firebird/lib/wire/database.js → src/wire/database.ts} +110 -44
  59. package/{poc/node_modules/node-firebird/lib/wire/eventConnection.js → src/wire/eventConnection.ts} +29 -14
  60. package/{poc/node_modules/node-firebird/lib/wire/fbEventManager.js → src/wire/fbEventManager.ts} +39 -30
  61. package/{poc/node_modules/node-firebird/lib/wire/serialize.js → src/wire/serialize.ts} +78 -59
  62. package/{poc/node_modules/node-firebird/lib/wire/service.js → src/wire/service.ts} +90 -88
  63. package/src/wire/socket.ts +262 -0
  64. package/src/wire/statement.ts +71 -0
  65. package/{poc/node_modules/node-firebird/lib/wire/transaction.js → src/wire/transaction.ts} +25 -19
  66. package/{poc/node_modules/node-firebird/lib/wire/xsqlvar.js → src/wire/xsqlvar.ts} +244 -143
  67. package/.eslintrc.json +0 -12
  68. package/.github/workflows/codeql.yml +0 -76
  69. package/.github/workflows/node.js.yml +0 -95
  70. package/BIGINT_MIGRATION.md +0 -374
  71. package/CI_DEBUGGING_GUIDE.md +0 -148
  72. package/ENCRYPTION_CALLBACK.md +0 -152
  73. package/FIREBIRD_LOG_FEATURE.md +0 -145
  74. package/MINIMAL_CHANGES_SUMMARY.md +0 -136
  75. package/PR_SUMMARY.md +0 -96
  76. package/ROADMAP.md +0 -223
  77. package/SRP_PROTOCOL.md +0 -482
  78. package/poc/README.md +0 -160
  79. package/poc/helpers.js +0 -59
  80. package/poc/node_modules/.package-lock.json +0 -14
  81. package/poc/node_modules/node-firebird/.eslintrc.json +0 -12
  82. package/poc/node_modules/node-firebird/.github/workflows/codeql.yml +0 -76
  83. package/poc/node_modules/node-firebird/.github/workflows/node.js.yml +0 -95
  84. package/poc/node_modules/node-firebird/BIGINT_MIGRATION.md +0 -374
  85. package/poc/node_modules/node-firebird/CI_DEBUGGING_GUIDE.md +0 -148
  86. package/poc/node_modules/node-firebird/ENCRYPTION_CALLBACK.md +0 -152
  87. package/poc/node_modules/node-firebird/FIREBIRD_LOG_FEATURE.md +0 -145
  88. package/poc/node_modules/node-firebird/LICENSE +0 -373
  89. package/poc/node_modules/node-firebird/MINIMAL_CHANGES_SUMMARY.md +0 -136
  90. package/poc/node_modules/node-firebird/PR_SUMMARY.md +0 -96
  91. package/poc/node_modules/node-firebird/README.md +0 -794
  92. package/poc/node_modules/node-firebird/ROADMAP.md +0 -223
  93. package/poc/node_modules/node-firebird/SRP_PROTOCOL.md +0 -482
  94. package/poc/node_modules/node-firebird/lib/callback.js +0 -38
  95. package/poc/node_modules/node-firebird/lib/gdscodes.d.ts +0 -1524
  96. package/poc/node_modules/node-firebird/lib/index.d.ts +0 -316
  97. package/poc/node_modules/node-firebird/lib/index.js +0 -128
  98. package/poc/node_modules/node-firebird/lib/pool.js +0 -108
  99. package/poc/node_modules/node-firebird/lib/wire/socket.js +0 -175
  100. package/poc/node_modules/node-firebird/lib/wire/statement.js +0 -48
  101. package/poc/node_modules/node-firebird/package.json +0 -38
  102. package/poc/node_modules/node-firebird/vitest.config.js +0 -24
  103. package/poc/package-lock.json +0 -21
  104. package/poc/package.json +0 -12
  105. package/poc/reproduce-fixed.js +0 -150
  106. package/poc/reproduce.js +0 -133
  107. package/vitest.config.js +0 -27
  108. /package/{poc/node_modules/node-firebird/lib → src}/firebird.msg +0 -0
  109. /package/{poc/node_modules/node-firebird/lib → src}/firebird.msg.json +0 -0
package/README.md CHANGED
@@ -13,20 +13,6 @@
13
13
  - [Firebird on Twitter](https://twitter.com/firebirdsql/)
14
14
  - [Firebird on Facebook](https://www.facebook.com/FirebirdSQL)
15
15
 
16
- ## Changelog for version v0.2.x
17
-
18
- - added auto-reconnect
19
- - added [sequentially selects](https://github.com/hgourvest/node-firebird/wiki/What-is-sequentially-selects)
20
- - events for connection (attach, detach, row, result, transaction, commit, rollback, error, etc.)
21
- - performance improvements
22
- - supports inserting/updating buffers and streams
23
- - reading blobs (sequentially)
24
- - pooling
25
- - `database.detach()` waits for last command
26
- - better unit-test
27
-
28
- ---
29
-
30
16
  - [Firebird documentation](https://firebirdsql.org/en/documentation/)
31
17
  - [Firebird limits and data types](https://firebirdsql.org/en/firebird-technical-specifications/)
32
18
 
@@ -36,12 +22,65 @@
36
22
  npm install node-firebird
37
23
  ```
38
24
 
25
+ The driver is pure JavaScript at runtime — no native addons and no runtime
26
+ dependencies. Node.js 20 or newer is supported (CI runs on Node 20, 22, 24
27
+ and 26 against Firebird 3, 4, 5 and 6).
28
+
39
29
  ## Usage
40
30
 
41
31
  ```js
42
32
  var Firebird = require('node-firebird');
43
33
  ```
44
34
 
35
+ TypeScript is fully supported — the driver itself is written in TypeScript and
36
+ ships its own type declarations:
37
+
38
+ ```ts
39
+ import * as Firebird from 'node-firebird';
40
+ import type { Options, Database } from 'node-firebird';
41
+ ```
42
+
43
+ ### Developing the driver
44
+
45
+ Since v2.4.0 the driver is written in TypeScript and compiled with the native
46
+ TypeScript 7 compiler (`tsc`). The published package ships both the compiled
47
+ output and the sources.
48
+
49
+ **Requirements**
50
+
51
+ - Node.js >= 20 (CI matrix: 20, 22, 24, 26)
52
+ - npm (TypeScript 7 and all tooling are installed as devDependencies — no
53
+ global installs needed)
54
+ - a Firebird server on `127.0.0.1:3050` with `SYSDBA`/`masterkey` for the
55
+ integration tests (CI tests against Firebird 3, 4, 5 and 6-snapshot); the
56
+ unit tests under `test/unit/` run without a server
57
+
58
+ The quickest way to get a test server is Docker:
59
+
60
+ ```bash
61
+ docker run -d --name firebird -p 3050:3050 \
62
+ -e FIREBIRD_ROOT_PASSWORD=masterkey \
63
+ firebirdsql/firebird:5
64
+ ```
65
+
66
+ **Layout**
67
+
68
+ - `src/` — the TypeScript sources; this is what you edit
69
+ - `lib/` — compiled CommonJS + generated `.d.ts` declarations; build output,
70
+ gitignored — never edit it by hand
71
+ - `test/` — vitest suite (integration tests at the top level, server-free
72
+ tests in `test/unit/`)
73
+
74
+ **Workflow**
75
+
76
+ ```bash
77
+ npm install # installs deps and builds lib/ via the prepare script
78
+ npm run build # compile src/ -> lib/
79
+ npm run typecheck # type-check sources and tests without emitting
80
+ npm run lint # oxlint
81
+ npm test # build + run the vitest suite (unit + integration)
82
+ ```
83
+
45
84
  ### Methods
46
85
 
47
86
  - `Firebird.escape(value) -> return {String}` - prevent for SQL Injections
@@ -75,6 +114,12 @@ options.wireCrypt = Firebird.WIRE_CRYPT_ENABLE; // default; set to Firebird.WIRE
75
114
  options.pluginName = undefined; // optional, auto-negotiated; can be set to Firebird.AUTH_PLUGIN_SRP256, Firebird.AUTH_PLUGIN_SRP, or Firebird.AUTH_PLUGIN_LEGACY
76
115
  options.dbCryptConfig = undefined; // optional; database encryption key for encrypted databases. Use 'base64:<value>' for base64-encoded keys or plain text
77
116
  options.connectTimeout = 10000; // optional; timeout in ms for a single pool.get() attach operation (default: no timeout)
117
+ options.parallelWorkers = undefined; // optional; request multiple thread workers for maintenance/index tasks (FB >= 5)
118
+ options.maxInlineBlobSize = undefined; // optional; threshold size in bytes for inline blob transmission (default 65535, FB >= 5.0.3)
119
+ options.maxNegotiatedProtocols = 10; // optional; limit maximum protocol versions negotiated (default 10 for compatibility, set to 11 for FB >= 6.0)
120
+ options.defaultSchema = undefined; // optional; sets session CURRENT_SCHEMA at connect time (FB >= 6.0)
121
+ options.searchPath = undefined; // optional; ordered list/array of schemas to resolve unqualified object references (FB >= 6.0)
122
+ options.jsonAsObject = false; // optional; automatically stringify parameters and parse query results that contain JSON (FB >= 6.0)
78
123
  ```
79
124
 
80
125
  ### Classic
@@ -175,6 +220,10 @@ sequenceDiagram
175
220
  - `db.sequentially(query, [params], function(row, index), function(err), options)` - sequentially query
176
221
  - `db.detach(function(err))` detach a database
177
222
  - `db.transaction(options, function(err, transaction))` create transaction
223
+ - `db.createTablespace(name, filePath, function(err, result))` - Create a physical tablespace (FB >= 6.0)
224
+ - `db.alterTablespace(name, filePath, function(err, result))` - Alter an existing tablespace physical location (FB >= 6.0)
225
+ - `db.dropTablespace(name, function(err, result))` - Drop a tablespace (FB >= 6.0)
226
+ - `db.createSchema(schemaName, [tablespaceName], function(err, result))` - Create a schema/namespace, optionally binding it to a tablespace (FB >= 6.0)
178
227
 
179
228
  ### Transaction options
180
229
 
@@ -235,6 +284,93 @@ Firebird.attach(options, function (err, db) {
235
284
  });
236
285
  ```
237
286
 
287
+ ### Tablespaces and Schema Partitioning (Firebird 6.0+)
288
+
289
+ 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.
290
+
291
+ ```js
292
+ Firebird.attach(options, function (err, db) {
293
+ if (err) throw err;
294
+
295
+ // 1. Create a physical tablespace mapping to a physical storage location
296
+ db.createTablespace('FAST_TS', '/ssd/fast_data.ts', function (err, result) {
297
+ if (err) throw err;
298
+ console.log('Tablespace FAST_TS created successfully');
299
+
300
+ // 2. Create a schema namespace and partition it into the FAST_TS tablespace
301
+ db.createSchema('MYSCHEMA', 'FAST_TS', function (err, result) {
302
+ if (err) throw err;
303
+ console.log('Schema MYSCHEMA partitioned to FAST_TS');
304
+
305
+ // 3. Drop tablespace when no longer needed
306
+ // db.dropTablespace('FAST_TS', function (err, result) { ... });
307
+
308
+ db.detach();
309
+ });
310
+ });
311
+ });
312
+ ```
313
+
314
+ ### Native JSON Data Type Support (Firebird 6.0+)
315
+
316
+ By enabling the `jsonAsObject` connection parameter, the driver will automatically serialize JavaScript objects/arrays passed as query parameters to JSON strings, and automatically parse returned JSON text/BLOB columns back into JavaScript objects/arrays.
317
+
318
+ ```js
319
+ const options = {
320
+ // ...other connection options
321
+ jsonAsObject: true,
322
+ blobAsText: true // recommended to read text BLOBs as strings
323
+ };
324
+
325
+ Firebird.attach(options, function (err, db) {
326
+ if (err) throw err;
327
+
328
+ const data = { name: 'Alice', age: 30, roles: ['admin', 'user'] };
329
+
330
+ db.query(
331
+ 'INSERT INTO USERS (ID, PROFILE_JSON) VALUES (?, ?)',
332
+ [1, data],
333
+ function (err, result) {
334
+ if (err) throw err;
335
+
336
+ db.query(
337
+ 'SELECT PROFILE_JSON FROM USERS WHERE ID = ?',
338
+ [1],
339
+ function (err, result) {
340
+ if (err) throw err;
341
+ // PROFILE_JSON is automatically parsed back to a JavaScript object
342
+ console.log(result[0].profile_json); // { name: 'Alice', age: 30, roles: ['admin', 'user'] }
343
+ db.detach();
344
+ }
345
+ );
346
+ }
347
+ );
348
+ });
349
+ ```
350
+
351
+ ### SQL-Standard ROW Type (Firebird 6.0+)
352
+
353
+ Firebird 6.0+ supports the SQL-standard `ROW` type representing composite records / tuples (e.g. `ROW(id INT, name VARCHAR(20))`). Since the database server compiles row value expressions into individual scalar columns/parameters at the wire interface, you can pass individual parameters or tuple arrays natively:
354
+
355
+ ```js
356
+ Firebird.attach(options, function (err, db) {
357
+ if (err) throw err;
358
+
359
+ // Use a row value expression / tuple comparison
360
+ db.query(
361
+ 'SELECT * FROM USERS WHERE (ID, NAME) = (ROW(?, ?))',
362
+ [1, 'Alice'],
363
+ function (err, rows) {
364
+ if (err) throw err;
365
+ console.log(rows);
366
+ db.detach();
367
+ }
368
+ );
369
+ });
370
+ ```
371
+
372
+ For PSQL block declarations (triggers, procedures), you can declare and use `ROW`/`RECORD` variables (such as `DECLARE VARIABLE myrow ROW(id INT, name VARCHAR(20))`) directly within the compiled SQL strings executed via `db.query` or `db.execute`.
373
+
238
374
  ### BLOB (stream)
239
375
 
240
376
  ```js
@@ -404,6 +540,8 @@ Firebird.attach(options, function (err, db) {
404
540
 
405
541
  ### Streaming a big data
406
542
 
543
+ `db.query` / `db.execute` buffer the **entire** result set into memory as an array before your callback runs — fine for small/medium results, but a poor fit for big tables or unbounded exports (this is what causes "Sequential heap limit / allocation failed"-style errors on very large result sets). `db.sequentially` / `transaction.sequentially` stream rows one at a time to an `on(row, index)` callback instead: node-firebird never accumulates the rows itself, so memory use stays flat regardless of table size (the `rows` argument passed to the completion callback is always `[]`).
544
+
407
545
  ```js
408
546
  Firebird.attach(options, function (err, db) {
409
547
  if (err) throw err;
@@ -424,6 +562,34 @@ Firebird.attach(options, function (err, db) {
424
562
  });
425
563
  ```
426
564
 
565
+ #### Backpressure
566
+
567
+ Rows are still fetched from the server in batches (200 rows per round-trip) as fast as your `on` callback returns. If you're forwarding each row to something that can fall behind (an HTTP response, a file write stream, a rate-limited API), declare `on` with a third `next` parameter — or return a `Promise` — and node-firebird will wait for you to call it (or for the promise to resolve) before fetching or processing the next row:
568
+
569
+ ```js
570
+ db.sequentially(
571
+ 'SELECT * FROM BIGTABLE',
572
+ function (row, index, next) {
573
+ // Only ask for the next row once the downstream write has drained.
574
+ if (outputStream.write(JSON.stringify(row) + '\n')) {
575
+ next();
576
+ } else {
577
+ outputStream.once('drain', next);
578
+ }
579
+ },
580
+ function (err) {
581
+ db.detach();
582
+ }
583
+ );
584
+ ```
585
+
586
+ #### Do / Don't
587
+
588
+ - **Do** use `sequentially` for large tables, full-table exports, or any query whose row count you can't bound in advance.
589
+ - **Do** use the 3-arg `on(row, index, next)` form (or return a `Promise` from `on`) when writing rows to something that applies its own backpressure, so unprocessed rows can't pile up faster than they're consumed.
590
+ - **Don't** use `db.query` / `db.execute` for big or unbounded result sets — both build the full array in memory before your callback ever runs.
591
+ - **Don't** assume the 2-arg `on(row, index)` form throttles you — it only guarantees node-firebird itself won't buffer rows; if your handler does async work without waiting on it (e.g. fire-and-forget writes), buffering can still build up on the *consumer* side.
592
+
427
593
  ### Transactions
428
594
 
429
595
  **Transaction types:**
@@ -519,9 +685,9 @@ Firebird.attach(options, function (err, db) {
519
685
 
520
686
  ### Firebird Database Events (POST_EVENT)
521
687
 
522
- Firebird database events are **asynchronous** notifications triggered by `POST_EVENT` inside PSQL triggers or stored procedures. They travel over a separate aux connection and are handled through `FbEventManager`.
523
-
524
- > **Note:** Full POST_EVENT reception is not yet implemented. `attachEvent` and `registerEvent` are available, but actual event delivery requires completing the `op_que_events`/`op_event` wire-protocol implementation.
688
+ Firebird database events are **asynchronous** notifications triggered by `POST_EVENT` inside PSQL
689
+ triggers or stored procedures. They travel over a separate "aux" connection (opened via
690
+ `db.attachEvent()`) and are managed through a `FbEventManager` instance.
525
691
 
526
692
  ```js
527
693
  Firebird.attach(options, function (err, db) {
@@ -531,7 +697,8 @@ Firebird.attach(options, function (err, db) {
531
697
  db.attachEvent(function (err, evtmgr) {
532
698
  if (err) throw err;
533
699
 
534
- // 2. Subscribe to one or more named events
700
+ // 2. Subscribe to one or more named events (names must match POST_EVENT('name') in your
701
+ // PSQL triggers/procedures). Resolves once op_que_events is acknowledged by the server.
535
702
  evtmgr.registerEvent(['MY_EVENT'], function (err) {
536
703
  if (err) throw err;
537
704
 
@@ -542,10 +709,20 @@ Firebird.attach(options, function (err, db) {
542
709
  });
543
710
  });
544
711
 
545
- // 4. Unsubscribe from events when no longer needed
712
+ // 4. Unsubscribe from one or more events. Passing all currently registered names cancels
713
+ // the subscription (sends op_cancel_events); the manager re-subscribes automatically if
714
+ // other event names remain registered.
546
715
  // evtmgr.unregisterEvent(['MY_EVENT'], function (err) { ... });
547
716
 
548
- // 5. Release the aux connection when done
717
+ // 5. Inspect the current subscription state for debugging: returns
718
+ // { state, hasActiveSubscription, registeredEvents, eventId,
719
+ // isEventConnectionOpen, isDatabaseConnectionClosed }.
720
+ // state is one of 'IDLE' (aux connection open, no active subscription),
721
+ // 'SUBSCRIBED' (op_que_events acknowledged) or 'CLOSED'.
722
+ // const state = evtmgr.getState();
723
+
724
+ // 6. Release the aux connection when done. Cancels any active subscription first, then
725
+ // gracefully closes the aux socket.
549
726
  // evtmgr.close(function (err) { ... });
550
727
  });
551
728
  });
@@ -939,6 +1116,77 @@ Firebird.attach(options, function(err, db) {
939
1116
  });
940
1117
  ```
941
1118
 
1119
+ ### Bidirectional Scrollable Cursors (Firebird 5.0+)
1120
+ Firebird 5.0 introduced native support for scrollable cursors, enabling bi-directional result set traversal on the server side. You can execute a statement with `{ scrollable: true }` and navigate with `statement.fetchScroll()`.
1121
+
1122
+ ```js
1123
+ db.transaction(function(err, tx) {
1124
+ tx.newStatement('SELECT ID, VAL FROM MY_TABLE ORDER BY ID', function(err, statement) {
1125
+ if (err) throw err;
1126
+
1127
+ // Execute the statement and request a scrollable cursor on the server
1128
+ statement.execute(tx, [], function(err) {
1129
+ if (err) throw err;
1130
+
1131
+ // 1. Fetch the first row
1132
+ statement.fetchScroll(tx, 'FIRST', 0, 1, function(err, res) {
1133
+ console.log('First:', res.data); // Row 1
1134
+
1135
+ // 2. Fetch the next row
1136
+ statement.fetchScroll(tx, 'NEXT', 0, 1, function(err, res) {
1137
+ console.log('Next:', res.data); // Row 2
1138
+
1139
+ // 3. Fetch the prior row
1140
+ statement.fetchScroll(tx, 'PRIOR', 0, 1, function(err, res) {
1141
+ console.log('Prior:', res.data); // Row 1 again
1142
+
1143
+ // 4. Fetch the absolute 3rd row
1144
+ statement.fetchScroll(tx, 'ABSOLUTE', 3, 1, function(err, res) {
1145
+ console.log('Absolute 3rd:', res.data); // Row 3
1146
+
1147
+ statement.release();
1148
+ tx.commit();
1149
+ db.detach();
1150
+ });
1151
+ });
1152
+ });
1153
+ });
1154
+ }, { scrollable: true });
1155
+ });
1156
+ });
1157
+ ```
1158
+
1159
+ Supported directions are: `'NEXT'` (0), `'PRIOR'` (1), `'FIRST'` (2), `'LAST'` (3), `'ABSOLUTE'` (4), and `'RELATIVE'` (5).
1160
+
1161
+ ### DML RETURNING Multiple Rows (Firebird 5.0+)
1162
+ In Firebird 5.0, DML statements like `UPDATE`, `DELETE`, and `INSERT ... SELECT` with a `RETURNING` clause can return multiple rows. When executing these statements, the driver receives an array of objects representing all the affected rows:
1163
+
1164
+ ```js
1165
+ db.query(
1166
+ 'UPDATE MY_TABLE SET VAL = VAL || \'!\' WHERE ID > 1 RETURNING ID, VAL',
1167
+ [],
1168
+ function(err, rows) {
1169
+ if (err) throw err;
1170
+ console.log(rows); // Array of updated rows: [{ id: 2, val: 'two!' }, { id: 3, val: 'three!' }]
1171
+ }
1172
+ );
1173
+ ```
1174
+
1175
+ ### SKIP LOCKED (Firebird 5.0+)
1176
+ Firebird 5.0 supports the `SKIP LOCKED` clause with `SELECT ... WITH LOCK`, `UPDATE`, and `DELETE` statements. This allows transactions to skip rows currently locked by other transactions instead of waiting or raising lock conflict errors, making it ideal for concurrency queues:
1177
+
1178
+ ```js
1179
+ // Selects unlocked rows, skipping any locked by concurrent processes
1180
+ db.query(
1181
+ 'SELECT * FROM QUEUE_TASK WHERE STATUS = \'PENDING\' WITH LOCK SKIP LOCKED',
1182
+ [],
1183
+ function(err, result) {
1184
+ if (err) throw err;
1185
+ console.log(result);
1186
+ }
1187
+ );
1188
+ ```
1189
+
942
1190
  ### Advanced Connection Pooling & Life-cycle
943
1191
  ```js
944
1192
  var pool = Firebird.pool(10, {
@@ -968,6 +1216,312 @@ process.on('SIGTERM', function() {
968
1216
  });
969
1217
  ```
970
1218
 
1219
+ ## Using node-firebird with Express.js
1220
+
1221
+ node-firebird works well with Express, but because the driver is connection/pool based rather than an ORM with automatic connection management, a few request-lifecycle patterns keep connections from leaking under load. This section documents the recommended architecture, referenced from the [roadmap](ROADMAP.md#2-expressjs-support-first-class-integration).
1222
+
1223
+ ### Recommended architecture: one pool per app, created at startup
1224
+
1225
+ Create a single `Firebird.pool(...)` when the app boots and reuse it for the lifetime of the process. Do **not** call `Firebird.pool()` or `Firebird.attach()` inside a request handler — that opens a new socket (or an entire new pool) on every request and will exhaust server and database resources under load.
1226
+
1227
+ ```js
1228
+ // db.js
1229
+ const Firebird = require('node-firebird');
1230
+
1231
+ const options = {
1232
+ host: '127.0.0.1',
1233
+ database: '/var/lib/firebird/data/app.fdb',
1234
+ user: 'SYSDBA',
1235
+ password: 'masterkey',
1236
+ connectTimeout: 5000,
1237
+ };
1238
+
1239
+ // Size the pool to your expected concurrency, not to the number of requests.
1240
+ const pool = Firebird.pool(10, options);
1241
+
1242
+ module.exports = pool;
1243
+ ```
1244
+
1245
+ ```js
1246
+ // app.js
1247
+ const express = require('express');
1248
+ const pool = require('./db');
1249
+
1250
+ const app = express();
1251
+
1252
+ // ...routes...
1253
+
1254
+ const server = app.listen(3000);
1255
+
1256
+ // Close the pool (and reject any in-flight pool.get() calls) on shutdown.
1257
+ process.on('SIGTERM', function () {
1258
+ server.close(function () {
1259
+ pool.destroy();
1260
+ });
1261
+ });
1262
+ ```
1263
+
1264
+ ### Request lifecycle: acquire → query → always release
1265
+
1266
+ Every request that touches the database must release its connection back to the pool exactly once, on every code path: success, query error, or a synchronous throw. There is no native `finally` across async callbacks, so a small helper plays that role:
1267
+
1268
+ ```js
1269
+ // withConnection.js
1270
+ function withConnection(pool, handler) {
1271
+ return function (req, res, next) {
1272
+ pool.get(function (err, db) {
1273
+ if (err) return next(err);
1274
+
1275
+ var finished = false;
1276
+
1277
+ // Safe to call more than once; only the first call has any effect.
1278
+ function done(err) {
1279
+ if (finished) return;
1280
+ finished = true;
1281
+ db.detach();
1282
+ if (err) next(err);
1283
+ }
1284
+
1285
+ try {
1286
+ handler(db, req, res, done);
1287
+ } catch (err) {
1288
+ done(err);
1289
+ }
1290
+ });
1291
+ };
1292
+ }
1293
+
1294
+ module.exports = withConnection;
1295
+ ```
1296
+
1297
+ ```js
1298
+ app.get('/users', withConnection(pool, function (db, req, res, done) {
1299
+ db.query('SELECT ID, ALIAS FROM USERS', function (err, rows) {
1300
+ if (err) return done(err);
1301
+ res.json(rows);
1302
+ done();
1303
+ });
1304
+ }));
1305
+ ```
1306
+
1307
+ ### Transaction middleware: commit on success, rollback on error
1308
+
1309
+ For write endpoints, acquire a connection and start a transaction in middleware, expose it as `req.tx`, and let the route handler decide whether to commit or roll back:
1310
+
1311
+ ```js
1312
+ function transactional(pool) {
1313
+ return function (req, res, next) {
1314
+ pool.get(function (err, db) {
1315
+ if (err) return next(err);
1316
+
1317
+ db.transaction(function (err, tx) {
1318
+ if (err) {
1319
+ db.detach();
1320
+ return next(err);
1321
+ }
1322
+
1323
+ req.tx = tx;
1324
+
1325
+ // Call on success — commits, releases the connection, then responds.
1326
+ req.commit = function (payload) {
1327
+ tx.commit(function (err) {
1328
+ db.detach();
1329
+ if (err) return next(err);
1330
+ res.json(payload);
1331
+ });
1332
+ };
1333
+
1334
+ // Call on failure — rolls back, releases the connection, then
1335
+ // forwards the error to Express's error-handling middleware.
1336
+ req.rollbackOnError = function (err) {
1337
+ tx.rollback(function () {
1338
+ db.detach();
1339
+ next(err);
1340
+ });
1341
+ };
1342
+
1343
+ next();
1344
+ });
1345
+ });
1346
+ };
1347
+ }
1348
+
1349
+ app.post('/orders', transactional(pool), function (req, res, next) {
1350
+ req.tx.query(
1351
+ 'INSERT INTO ORDERS (CUSTOMER, TOTAL) VALUES (?, ?) RETURNING ID',
1352
+ [req.body.customer, req.body.total],
1353
+ function (err, result) {
1354
+ if (err) return req.rollbackOnError(err);
1355
+ req.commit(result);
1356
+ }
1357
+ );
1358
+ });
1359
+ ```
1360
+
1361
+ ### Error handling: map Firebird errors to HTTP status codes
1362
+
1363
+ Firebird errors surface as a plain `Error` with `.message`, `.gdscode`, and `.gdsparams` (see `lib/callback.js`). Map the codes you care about — exported as named constants from `node-firebird/lib/gdscodes` — to HTTP status codes in your error-handling middleware, and log the original error server-side only. Don't forward `err.message` as-is to clients: it can contain SQL text, table/column names, or file paths.
1364
+
1365
+ ```js
1366
+ const { GDSCode } = require('node-firebird/lib/gdscodes');
1367
+
1368
+ function mapFirebirdErrorToStatus(err) {
1369
+ switch (err.gdscode) {
1370
+ case GDSCode.UNIQUE_KEY_VIOLATION:
1371
+ return 409; // Conflict
1372
+ case GDSCode.LOCK_CONFLICT:
1373
+ return 423; // Locked
1374
+ default:
1375
+ return 500;
1376
+ }
1377
+ }
1378
+
1379
+ // Express error-handling middleware (register last, with 4 arguments).
1380
+ app.use(function (err, req, res, next) {
1381
+ console.error(err); // full detail goes to server logs only
1382
+
1383
+ res.status(mapFirebirdErrorToStatus(err)).json({
1384
+ error: err.gdscode === GDSCode.UNIQUE_KEY_VIOLATION ? 'Duplicate record' : 'Request failed',
1385
+ });
1386
+ });
1387
+ ```
1388
+
1389
+ ### BLOB streaming: pipe a BLOB column straight to the response
1390
+
1391
+ Blob columns come back from `db.query`/`transaction.query` as functions (see [Reading Blobs](#reading-blobs-asynchronous)). Call the column function to get an `EventEmitter` you can `.pipe()` straight into `res`, and release the connection once streaming ends, errors, *or* the client disconnects early.
1392
+
1393
+ ```js
1394
+ app.get('/users/:id/picture', withConnection(pool, function (db, req, res, done) {
1395
+ db.query(
1396
+ 'SELECT USERPICTURE FROM USERS WHERE ID = ?',
1397
+ [req.params.id],
1398
+ function (err, rows) {
1399
+ if (err) return done(err);
1400
+ if (!rows.length) {
1401
+ res.sendStatus(404);
1402
+ return done();
1403
+ }
1404
+
1405
+ rows[0].userpicture(function (err, name, blobStream) {
1406
+ if (err) return done(err);
1407
+
1408
+ res.type('application/octet-stream');
1409
+
1410
+ // done() is idempotent — whichever of these fires first releases
1411
+ // the connection; the rest are no-ops.
1412
+ blobStream.once('end', function () { done(); });
1413
+ blobStream.once('error', done);
1414
+ res.once('close', function () { done(); });
1415
+
1416
+ blobStream.pipe(res);
1417
+ });
1418
+ }
1419
+ );
1420
+ }));
1421
+ ```
1422
+
1423
+ ## FAQ
1424
+
1425
+ Answers to recurring questions from the [issue tracker](https://github.com/hgourvest/node-firebird/issues).
1426
+
1427
+ #### Can I use aggregate functions like `LIST()`? I get "no database to handle" when I call the result.
1428
+
1429
+ 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".
1430
+
1431
+ The simplest fix is `blobAsText: true`, which returns subtype-1 blobs as plain strings automatically, no callback needed:
1432
+
1433
+ ```js
1434
+ const options = { /* ...other options... */ blobAsText: true };
1435
+
1436
+ Firebird.attach(options, function (err, db) {
1437
+ if (err) throw err;
1438
+ db.query('SELECT LIST(RDB$FIELD_NAME) AS COLUMNS FROM RDB$RELATION_FIELDS', function (err, rows) {
1439
+ if (err) throw err;
1440
+ console.log(rows[0].columns); // plain string, not a function
1441
+ db.detach();
1442
+ });
1443
+ });
1444
+ ```
1445
+
1446
+ If you need to read it manually instead, pass the **same** transaction the query used:
1447
+
1448
+ ```js
1449
+ db.transaction(function (err, tx) {
1450
+ tx.query('SELECT LIST(RDB$FIELD_NAME) AS COLUMNS FROM RDB$RELATION_FIELDS', function (err, rows) {
1451
+ rows[0].columns(tx, function (err, name, e) {
1452
+ let collected = '';
1453
+ e.on('data', (chunk) => (collected += chunk));
1454
+ e.on('end', () => console.log(collected));
1455
+ });
1456
+ });
1457
+ });
1458
+ ```
1459
+
1460
+ #### Is the wire protocol version hard-coded?
1461
+
1462
+ 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:
1463
+
1464
+ ```js
1465
+ options.maxNegotiatedProtocols = 11; // offers Protocol 20 as well as 19
1466
+ ```
1467
+
1468
+ 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).
1469
+
1470
+ #### BLOB reads/writes are very slow, especially for large files over a remote connection
1471
+
1472
+ This is almost always network round-trips, not the database. By default, blobs are streamed in small (1024-byte) segments; for a multi-megabyte file over a non-local connection that's thousands of round-trips. Raise `blobChunkSize` / `blobReadChunkSize` (up to the protocol max of `65535`) to fetch/send far larger segments per round-trip — see [Optimizing BLOB Read/Write Chunk Sizes](#optimizing-blob-readwrite-chunk-sizes):
1473
+
1474
+ ```js
1475
+ options.blobChunkSize = 65535;
1476
+ options.blobReadChunkSize = 65535;
1477
+ ```
1478
+
1479
+ If your server and client are on the same host, this won't matter much — the slowdown is latency-bound, not throughput-bound.
1480
+
1481
+ #### How do I use an encoding other than UTF-8 (e.g. WIN1252/Latin1)?
1482
+
1483
+ Set `options.encoding` — no source changes required (see [Character Set & Encoding Support](#character-set--encoding-support) for the full mapping table):
1484
+
1485
+ ```js
1486
+ options.encoding = 'WIN1252'; // or 'ISO8859_1', 'LATIN1', 'ASCII', 'NONE'
1487
+ ```
1488
+
1489
+ `options.encoding` must match the character set the *database itself* was created with. If you set an encoding that doesn't match the database's charset, Firebird will reject transliteration of characters that don't exist in both charsets with an error like `Cannot transliterate character between character sets` — that's the server protecting you from silent data corruption, not a driver bug. When in doubt, check the database's `RDB$CHARACTER_SETS` / connection charset rather than guessing.
1490
+
1491
+ If your database uses `charset NONE` (no transliteration at all — the server stores whatever bytes it's given) and you need full control over the byte encoding yourself, pass a `Buffer` instead of a string for text-column parameters. node-firebird writes `Buffer` values through unchanged, bypassing Node's UTF-8 string handling entirely — combine with a transliteration library like [`iconv-lite`](https://www.npmjs.com/package/iconv-lite) to produce the bytes:
1492
+
1493
+ ```js
1494
+ const iconv = require('iconv-lite');
1495
+ options.encoding = 'NONE';
1496
+
1497
+ db.query(
1498
+ 'INSERT INTO NOTES (ID, BODY) VALUES (?, ?)',
1499
+ [1, iconv.encode('Café', 'win1252')], // pre-encoded bytes, written as-is
1500
+ function (err) { /* ... */ }
1501
+ );
1502
+ ```
1503
+
1504
+ #### I get "Dynamic SQL Error" (string truncation) using `LIKE` with a pattern longer than the column
1505
+
1506
+ ```
1507
+ SELECT * FROM ACTORS WHERE NAME LIKE 'James Wick%' -- NAME is VARCHAR(10), literal is 11 chars
1508
+ ```
1509
+
1510
+ On some Firebird server versions, the optimizer infers a literal's type from the column it's compared against and can raise a truncation error at compile time if an inline string literal is longer than the column's declared length — even though the same query works fine as a parameterized query. This is server-side SQL compilation behavior, not something node-firebird controls; it did not reproduce against the Firebird 6.0 server used by this project's own test suite, so it appears to be limited to certain older engine versions/optimizer paths.
1511
+
1512
+ The reliable fix, which also happens to be the general best practice (see [Parametrized Queries](#parametrized-queries)), is to pass the pattern as a parameter instead of inlining it:
1513
+
1514
+ ```js
1515
+ // Instead of embedding the literal in the SQL text:
1516
+ db.query("SELECT * FROM ACTORS WHERE NAME LIKE 'James Wick%'", ...);
1517
+
1518
+ // Bind it as a parameter:
1519
+ db.query('SELECT * FROM ACTORS WHERE NAME LIKE ?', ['James Wick%'], function (err, rows) {
1520
+ if (err) throw err;
1521
+ console.log(rows);
1522
+ });
1523
+ ```
1524
+
971
1525
  ## Contributors
972
1526
 
973
1527
  - Henri Gourvest, <https://github.com/hgourvest>