node-firebird 2.3.3 → 2.4.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 (109) hide show
  1. package/README.md +544 -6
  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
@@ -42,6 +42,29 @@ npm install node-firebird
42
42
  var Firebird = require('node-firebird');
43
43
  ```
44
44
 
45
+ TypeScript is fully supported — the driver itself is written in TypeScript and
46
+ ships its own type declarations:
47
+
48
+ ```ts
49
+ import * as Firebird from 'node-firebird';
50
+ import type { Options, Database } from 'node-firebird';
51
+ ```
52
+
53
+ ### Development
54
+
55
+ The sources live in `src/` (TypeScript) and are compiled to `lib/` (CommonJS +
56
+ `.d.ts`) by the native TypeScript 7 compiler:
57
+
58
+ ```bash
59
+ npm install # installs deps and builds lib/ via the prepare script
60
+ npm run build # compile src/ -> lib/
61
+ npm run typecheck # type-check sources and unit tests without emitting
62
+ npm test # build + run the vitest suite (unit + integration)
63
+ ```
64
+
65
+ The integration tests expect a Firebird server on `127.0.0.1:3050`
66
+ (SYSDBA/masterkey); the unit tests under `test/unit/` run without a server.
67
+
45
68
  ### Methods
46
69
 
47
70
  - `Firebird.escape(value) -> return {String}` - prevent for SQL Injections
@@ -75,6 +98,12 @@ options.wireCrypt = Firebird.WIRE_CRYPT_ENABLE; // default; set to Firebird.WIRE
75
98
  options.pluginName = undefined; // optional, auto-negotiated; can be set to Firebird.AUTH_PLUGIN_SRP256, Firebird.AUTH_PLUGIN_SRP, or Firebird.AUTH_PLUGIN_LEGACY
76
99
  options.dbCryptConfig = undefined; // optional; database encryption key for encrypted databases. Use 'base64:<value>' for base64-encoded keys or plain text
77
100
  options.connectTimeout = 10000; // optional; timeout in ms for a single pool.get() attach operation (default: no timeout)
101
+ options.parallelWorkers = undefined; // optional; request multiple thread workers for maintenance/index tasks (FB >= 5)
102
+ options.maxInlineBlobSize = undefined; // optional; threshold size in bytes for inline blob transmission (default 65535, FB >= 5.0.3)
103
+ options.maxNegotiatedProtocols = 10; // optional; limit maximum protocol versions negotiated (default 10 for compatibility, set to 11 for FB >= 6.0)
104
+ options.defaultSchema = undefined; // optional; sets session CURRENT_SCHEMA at connect time (FB >= 6.0)
105
+ options.searchPath = undefined; // optional; ordered list/array of schemas to resolve unqualified object references (FB >= 6.0)
106
+ options.jsonAsObject = false; // optional; automatically stringify parameters and parse query results that contain JSON (FB >= 6.0)
78
107
  ```
79
108
 
80
109
  ### Classic
@@ -175,6 +204,10 @@ sequenceDiagram
175
204
  - `db.sequentially(query, [params], function(row, index), function(err), options)` - sequentially query
176
205
  - `db.detach(function(err))` detach a database
177
206
  - `db.transaction(options, function(err, transaction))` create transaction
207
+ - `db.createTablespace(name, filePath, function(err, result))` - Create a physical tablespace (FB >= 6.0)
208
+ - `db.alterTablespace(name, filePath, function(err, result))` - Alter an existing tablespace physical location (FB >= 6.0)
209
+ - `db.dropTablespace(name, function(err, result))` - Drop a tablespace (FB >= 6.0)
210
+ - `db.createSchema(schemaName, [tablespaceName], function(err, result))` - Create a schema/namespace, optionally binding it to a tablespace (FB >= 6.0)
178
211
 
179
212
  ### Transaction options
180
213
 
@@ -235,6 +268,93 @@ Firebird.attach(options, function (err, db) {
235
268
  });
236
269
  ```
237
270
 
271
+ ### Tablespaces and Schema Partitioning (Firebird 6.0+)
272
+
273
+ 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.
274
+
275
+ ```js
276
+ Firebird.attach(options, function (err, db) {
277
+ if (err) throw err;
278
+
279
+ // 1. Create a physical tablespace mapping to a physical storage location
280
+ db.createTablespace('FAST_TS', '/ssd/fast_data.ts', function (err, result) {
281
+ if (err) throw err;
282
+ console.log('Tablespace FAST_TS created successfully');
283
+
284
+ // 2. Create a schema namespace and partition it into the FAST_TS tablespace
285
+ db.createSchema('MYSCHEMA', 'FAST_TS', function (err, result) {
286
+ if (err) throw err;
287
+ console.log('Schema MYSCHEMA partitioned to FAST_TS');
288
+
289
+ // 3. Drop tablespace when no longer needed
290
+ // db.dropTablespace('FAST_TS', function (err, result) { ... });
291
+
292
+ db.detach();
293
+ });
294
+ });
295
+ });
296
+ ```
297
+
298
+ ### Native JSON Data Type Support (Firebird 6.0+)
299
+
300
+ 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.
301
+
302
+ ```js
303
+ const options = {
304
+ // ...other connection options
305
+ jsonAsObject: true,
306
+ blobAsText: true // recommended to read text BLOBs as strings
307
+ };
308
+
309
+ Firebird.attach(options, function (err, db) {
310
+ if (err) throw err;
311
+
312
+ const data = { name: 'Alice', age: 30, roles: ['admin', 'user'] };
313
+
314
+ db.query(
315
+ 'INSERT INTO USERS (ID, PROFILE_JSON) VALUES (?, ?)',
316
+ [1, data],
317
+ function (err, result) {
318
+ if (err) throw err;
319
+
320
+ db.query(
321
+ 'SELECT PROFILE_JSON FROM USERS WHERE ID = ?',
322
+ [1],
323
+ function (err, result) {
324
+ if (err) throw err;
325
+ // PROFILE_JSON is automatically parsed back to a JavaScript object
326
+ console.log(result[0].profile_json); // { name: 'Alice', age: 30, roles: ['admin', 'user'] }
327
+ db.detach();
328
+ }
329
+ );
330
+ }
331
+ );
332
+ });
333
+ ```
334
+
335
+ ### SQL-Standard ROW Type (Firebird 6.0+)
336
+
337
+ 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:
338
+
339
+ ```js
340
+ Firebird.attach(options, function (err, db) {
341
+ if (err) throw err;
342
+
343
+ // Use a row value expression / tuple comparison
344
+ db.query(
345
+ 'SELECT * FROM USERS WHERE (ID, NAME) = (ROW(?, ?))',
346
+ [1, 'Alice'],
347
+ function (err, rows) {
348
+ if (err) throw err;
349
+ console.log(rows);
350
+ db.detach();
351
+ }
352
+ );
353
+ });
354
+ ```
355
+
356
+ 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`.
357
+
238
358
  ### BLOB (stream)
239
359
 
240
360
  ```js
@@ -404,6 +524,8 @@ Firebird.attach(options, function (err, db) {
404
524
 
405
525
  ### Streaming a big data
406
526
 
527
+ `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 `[]`).
528
+
407
529
  ```js
408
530
  Firebird.attach(options, function (err, db) {
409
531
  if (err) throw err;
@@ -424,6 +546,34 @@ Firebird.attach(options, function (err, db) {
424
546
  });
425
547
  ```
426
548
 
549
+ #### Backpressure
550
+
551
+ 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:
552
+
553
+ ```js
554
+ db.sequentially(
555
+ 'SELECT * FROM BIGTABLE',
556
+ function (row, index, next) {
557
+ // Only ask for the next row once the downstream write has drained.
558
+ if (outputStream.write(JSON.stringify(row) + '\n')) {
559
+ next();
560
+ } else {
561
+ outputStream.once('drain', next);
562
+ }
563
+ },
564
+ function (err) {
565
+ db.detach();
566
+ }
567
+ );
568
+ ```
569
+
570
+ #### Do / Don't
571
+
572
+ - **Do** use `sequentially` for large tables, full-table exports, or any query whose row count you can't bound in advance.
573
+ - **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.
574
+ - **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.
575
+ - **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.
576
+
427
577
  ### Transactions
428
578
 
429
579
  **Transaction types:**
@@ -519,9 +669,9 @@ Firebird.attach(options, function (err, db) {
519
669
 
520
670
  ### Firebird Database Events (POST_EVENT)
521
671
 
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.
672
+ Firebird database events are **asynchronous** notifications triggered by `POST_EVENT` inside PSQL
673
+ triggers or stored procedures. They travel over a separate "aux" connection (opened via
674
+ `db.attachEvent()`) and are managed through a `FbEventManager` instance.
525
675
 
526
676
  ```js
527
677
  Firebird.attach(options, function (err, db) {
@@ -531,7 +681,8 @@ Firebird.attach(options, function (err, db) {
531
681
  db.attachEvent(function (err, evtmgr) {
532
682
  if (err) throw err;
533
683
 
534
- // 2. Subscribe to one or more named events
684
+ // 2. Subscribe to one or more named events (names must match POST_EVENT('name') in your
685
+ // PSQL triggers/procedures). Resolves once op_que_events is acknowledged by the server.
535
686
  evtmgr.registerEvent(['MY_EVENT'], function (err) {
536
687
  if (err) throw err;
537
688
 
@@ -542,10 +693,20 @@ Firebird.attach(options, function (err, db) {
542
693
  });
543
694
  });
544
695
 
545
- // 4. Unsubscribe from events when no longer needed
696
+ // 4. Unsubscribe from one or more events. Passing all currently registered names cancels
697
+ // the subscription (sends op_cancel_events); the manager re-subscribes automatically if
698
+ // other event names remain registered.
546
699
  // evtmgr.unregisterEvent(['MY_EVENT'], function (err) { ... });
547
700
 
548
- // 5. Release the aux connection when done
701
+ // 5. Inspect the current subscription state for debugging: returns
702
+ // { state, hasActiveSubscription, registeredEvents, eventId,
703
+ // isEventConnectionOpen, isDatabaseConnectionClosed }.
704
+ // state is one of 'IDLE' (aux connection open, no active subscription),
705
+ // 'SUBSCRIBED' (op_que_events acknowledged) or 'CLOSED'.
706
+ // const state = evtmgr.getState();
707
+
708
+ // 6. Release the aux connection when done. Cancels any active subscription first, then
709
+ // gracefully closes the aux socket.
549
710
  // evtmgr.close(function (err) { ... });
550
711
  });
551
712
  });
@@ -939,6 +1100,77 @@ Firebird.attach(options, function(err, db) {
939
1100
  });
940
1101
  ```
941
1102
 
1103
+ ### Bidirectional Scrollable Cursors (Firebird 5.0+)
1104
+ 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()`.
1105
+
1106
+ ```js
1107
+ db.transaction(function(err, tx) {
1108
+ tx.newStatement('SELECT ID, VAL FROM MY_TABLE ORDER BY ID', function(err, statement) {
1109
+ if (err) throw err;
1110
+
1111
+ // Execute the statement and request a scrollable cursor on the server
1112
+ statement.execute(tx, [], function(err) {
1113
+ if (err) throw err;
1114
+
1115
+ // 1. Fetch the first row
1116
+ statement.fetchScroll(tx, 'FIRST', 0, 1, function(err, res) {
1117
+ console.log('First:', res.data); // Row 1
1118
+
1119
+ // 2. Fetch the next row
1120
+ statement.fetchScroll(tx, 'NEXT', 0, 1, function(err, res) {
1121
+ console.log('Next:', res.data); // Row 2
1122
+
1123
+ // 3. Fetch the prior row
1124
+ statement.fetchScroll(tx, 'PRIOR', 0, 1, function(err, res) {
1125
+ console.log('Prior:', res.data); // Row 1 again
1126
+
1127
+ // 4. Fetch the absolute 3rd row
1128
+ statement.fetchScroll(tx, 'ABSOLUTE', 3, 1, function(err, res) {
1129
+ console.log('Absolute 3rd:', res.data); // Row 3
1130
+
1131
+ statement.release();
1132
+ tx.commit();
1133
+ db.detach();
1134
+ });
1135
+ });
1136
+ });
1137
+ });
1138
+ }, { scrollable: true });
1139
+ });
1140
+ });
1141
+ ```
1142
+
1143
+ Supported directions are: `'NEXT'` (0), `'PRIOR'` (1), `'FIRST'` (2), `'LAST'` (3), `'ABSOLUTE'` (4), and `'RELATIVE'` (5).
1144
+
1145
+ ### DML RETURNING Multiple Rows (Firebird 5.0+)
1146
+ 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:
1147
+
1148
+ ```js
1149
+ db.query(
1150
+ 'UPDATE MY_TABLE SET VAL = VAL || \'!\' WHERE ID > 1 RETURNING ID, VAL',
1151
+ [],
1152
+ function(err, rows) {
1153
+ if (err) throw err;
1154
+ console.log(rows); // Array of updated rows: [{ id: 2, val: 'two!' }, { id: 3, val: 'three!' }]
1155
+ }
1156
+ );
1157
+ ```
1158
+
1159
+ ### SKIP LOCKED (Firebird 5.0+)
1160
+ 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:
1161
+
1162
+ ```js
1163
+ // Selects unlocked rows, skipping any locked by concurrent processes
1164
+ db.query(
1165
+ 'SELECT * FROM QUEUE_TASK WHERE STATUS = \'PENDING\' WITH LOCK SKIP LOCKED',
1166
+ [],
1167
+ function(err, result) {
1168
+ if (err) throw err;
1169
+ console.log(result);
1170
+ }
1171
+ );
1172
+ ```
1173
+
942
1174
  ### Advanced Connection Pooling & Life-cycle
943
1175
  ```js
944
1176
  var pool = Firebird.pool(10, {
@@ -968,6 +1200,312 @@ process.on('SIGTERM', function() {
968
1200
  });
969
1201
  ```
970
1202
 
1203
+ ## Using node-firebird with Express.js
1204
+
1205
+ 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).
1206
+
1207
+ ### Recommended architecture: one pool per app, created at startup
1208
+
1209
+ 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.
1210
+
1211
+ ```js
1212
+ // db.js
1213
+ const Firebird = require('node-firebird');
1214
+
1215
+ const options = {
1216
+ host: '127.0.0.1',
1217
+ database: '/var/lib/firebird/data/app.fdb',
1218
+ user: 'SYSDBA',
1219
+ password: 'masterkey',
1220
+ connectTimeout: 5000,
1221
+ };
1222
+
1223
+ // Size the pool to your expected concurrency, not to the number of requests.
1224
+ const pool = Firebird.pool(10, options);
1225
+
1226
+ module.exports = pool;
1227
+ ```
1228
+
1229
+ ```js
1230
+ // app.js
1231
+ const express = require('express');
1232
+ const pool = require('./db');
1233
+
1234
+ const app = express();
1235
+
1236
+ // ...routes...
1237
+
1238
+ const server = app.listen(3000);
1239
+
1240
+ // Close the pool (and reject any in-flight pool.get() calls) on shutdown.
1241
+ process.on('SIGTERM', function () {
1242
+ server.close(function () {
1243
+ pool.destroy();
1244
+ });
1245
+ });
1246
+ ```
1247
+
1248
+ ### Request lifecycle: acquire → query → always release
1249
+
1250
+ 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:
1251
+
1252
+ ```js
1253
+ // withConnection.js
1254
+ function withConnection(pool, handler) {
1255
+ return function (req, res, next) {
1256
+ pool.get(function (err, db) {
1257
+ if (err) return next(err);
1258
+
1259
+ var finished = false;
1260
+
1261
+ // Safe to call more than once; only the first call has any effect.
1262
+ function done(err) {
1263
+ if (finished) return;
1264
+ finished = true;
1265
+ db.detach();
1266
+ if (err) next(err);
1267
+ }
1268
+
1269
+ try {
1270
+ handler(db, req, res, done);
1271
+ } catch (err) {
1272
+ done(err);
1273
+ }
1274
+ });
1275
+ };
1276
+ }
1277
+
1278
+ module.exports = withConnection;
1279
+ ```
1280
+
1281
+ ```js
1282
+ app.get('/users', withConnection(pool, function (db, req, res, done) {
1283
+ db.query('SELECT ID, ALIAS FROM USERS', function (err, rows) {
1284
+ if (err) return done(err);
1285
+ res.json(rows);
1286
+ done();
1287
+ });
1288
+ }));
1289
+ ```
1290
+
1291
+ ### Transaction middleware: commit on success, rollback on error
1292
+
1293
+ 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:
1294
+
1295
+ ```js
1296
+ function transactional(pool) {
1297
+ return function (req, res, next) {
1298
+ pool.get(function (err, db) {
1299
+ if (err) return next(err);
1300
+
1301
+ db.transaction(function (err, tx) {
1302
+ if (err) {
1303
+ db.detach();
1304
+ return next(err);
1305
+ }
1306
+
1307
+ req.tx = tx;
1308
+
1309
+ // Call on success — commits, releases the connection, then responds.
1310
+ req.commit = function (payload) {
1311
+ tx.commit(function (err) {
1312
+ db.detach();
1313
+ if (err) return next(err);
1314
+ res.json(payload);
1315
+ });
1316
+ };
1317
+
1318
+ // Call on failure — rolls back, releases the connection, then
1319
+ // forwards the error to Express's error-handling middleware.
1320
+ req.rollbackOnError = function (err) {
1321
+ tx.rollback(function () {
1322
+ db.detach();
1323
+ next(err);
1324
+ });
1325
+ };
1326
+
1327
+ next();
1328
+ });
1329
+ });
1330
+ };
1331
+ }
1332
+
1333
+ app.post('/orders', transactional(pool), function (req, res, next) {
1334
+ req.tx.query(
1335
+ 'INSERT INTO ORDERS (CUSTOMER, TOTAL) VALUES (?, ?) RETURNING ID',
1336
+ [req.body.customer, req.body.total],
1337
+ function (err, result) {
1338
+ if (err) return req.rollbackOnError(err);
1339
+ req.commit(result);
1340
+ }
1341
+ );
1342
+ });
1343
+ ```
1344
+
1345
+ ### Error handling: map Firebird errors to HTTP status codes
1346
+
1347
+ 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.
1348
+
1349
+ ```js
1350
+ const { GDSCode } = require('node-firebird/lib/gdscodes');
1351
+
1352
+ function mapFirebirdErrorToStatus(err) {
1353
+ switch (err.gdscode) {
1354
+ case GDSCode.UNIQUE_KEY_VIOLATION:
1355
+ return 409; // Conflict
1356
+ case GDSCode.LOCK_CONFLICT:
1357
+ return 423; // Locked
1358
+ default:
1359
+ return 500;
1360
+ }
1361
+ }
1362
+
1363
+ // Express error-handling middleware (register last, with 4 arguments).
1364
+ app.use(function (err, req, res, next) {
1365
+ console.error(err); // full detail goes to server logs only
1366
+
1367
+ res.status(mapFirebirdErrorToStatus(err)).json({
1368
+ error: err.gdscode === GDSCode.UNIQUE_KEY_VIOLATION ? 'Duplicate record' : 'Request failed',
1369
+ });
1370
+ });
1371
+ ```
1372
+
1373
+ ### BLOB streaming: pipe a BLOB column straight to the response
1374
+
1375
+ 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.
1376
+
1377
+ ```js
1378
+ app.get('/users/:id/picture', withConnection(pool, function (db, req, res, done) {
1379
+ db.query(
1380
+ 'SELECT USERPICTURE FROM USERS WHERE ID = ?',
1381
+ [req.params.id],
1382
+ function (err, rows) {
1383
+ if (err) return done(err);
1384
+ if (!rows.length) {
1385
+ res.sendStatus(404);
1386
+ return done();
1387
+ }
1388
+
1389
+ rows[0].userpicture(function (err, name, blobStream) {
1390
+ if (err) return done(err);
1391
+
1392
+ res.type('application/octet-stream');
1393
+
1394
+ // done() is idempotent — whichever of these fires first releases
1395
+ // the connection; the rest are no-ops.
1396
+ blobStream.once('end', function () { done(); });
1397
+ blobStream.once('error', done);
1398
+ res.once('close', function () { done(); });
1399
+
1400
+ blobStream.pipe(res);
1401
+ });
1402
+ }
1403
+ );
1404
+ }));
1405
+ ```
1406
+
1407
+ ## FAQ
1408
+
1409
+ Answers to recurring questions from the [issue tracker](https://github.com/hgourvest/node-firebird/issues).
1410
+
1411
+ #### Can I use aggregate functions like `LIST()`? I get "no database to handle" when I call the result.
1412
+
1413
+ 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".
1414
+
1415
+ The simplest fix is `blobAsText: true`, which returns subtype-1 blobs as plain strings automatically, no callback needed:
1416
+
1417
+ ```js
1418
+ const options = { /* ...other options... */ blobAsText: true };
1419
+
1420
+ Firebird.attach(options, function (err, db) {
1421
+ if (err) throw err;
1422
+ db.query('SELECT LIST(RDB$FIELD_NAME) AS COLUMNS FROM RDB$RELATION_FIELDS', function (err, rows) {
1423
+ if (err) throw err;
1424
+ console.log(rows[0].columns); // plain string, not a function
1425
+ db.detach();
1426
+ });
1427
+ });
1428
+ ```
1429
+
1430
+ If you need to read it manually instead, pass the **same** transaction the query used:
1431
+
1432
+ ```js
1433
+ db.transaction(function (err, tx) {
1434
+ tx.query('SELECT LIST(RDB$FIELD_NAME) AS COLUMNS FROM RDB$RELATION_FIELDS', function (err, rows) {
1435
+ rows[0].columns(tx, function (err, name, e) {
1436
+ let collected = '';
1437
+ e.on('data', (chunk) => (collected += chunk));
1438
+ e.on('end', () => console.log(collected));
1439
+ });
1440
+ });
1441
+ });
1442
+ ```
1443
+
1444
+ #### Is the wire protocol version hard-coded?
1445
+
1446
+ 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:
1447
+
1448
+ ```js
1449
+ options.maxNegotiatedProtocols = 11; // offers Protocol 20 as well as 19
1450
+ ```
1451
+
1452
+ 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).
1453
+
1454
+ #### BLOB reads/writes are very slow, especially for large files over a remote connection
1455
+
1456
+ 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):
1457
+
1458
+ ```js
1459
+ options.blobChunkSize = 65535;
1460
+ options.blobReadChunkSize = 65535;
1461
+ ```
1462
+
1463
+ If your server and client are on the same host, this won't matter much — the slowdown is latency-bound, not throughput-bound.
1464
+
1465
+ #### How do I use an encoding other than UTF-8 (e.g. WIN1252/Latin1)?
1466
+
1467
+ Set `options.encoding` — no source changes required (see [Character Set & Encoding Support](#character-set--encoding-support) for the full mapping table):
1468
+
1469
+ ```js
1470
+ options.encoding = 'WIN1252'; // or 'ISO8859_1', 'LATIN1', 'ASCII', 'NONE'
1471
+ ```
1472
+
1473
+ `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.
1474
+
1475
+ 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:
1476
+
1477
+ ```js
1478
+ const iconv = require('iconv-lite');
1479
+ options.encoding = 'NONE';
1480
+
1481
+ db.query(
1482
+ 'INSERT INTO NOTES (ID, BODY) VALUES (?, ?)',
1483
+ [1, iconv.encode('Café', 'win1252')], // pre-encoded bytes, written as-is
1484
+ function (err) { /* ... */ }
1485
+ );
1486
+ ```
1487
+
1488
+ #### I get "Dynamic SQL Error" (string truncation) using `LIKE` with a pattern longer than the column
1489
+
1490
+ ```
1491
+ SELECT * FROM ACTORS WHERE NAME LIKE 'James Wick%' -- NAME is VARCHAR(10), literal is 11 chars
1492
+ ```
1493
+
1494
+ 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.
1495
+
1496
+ 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:
1497
+
1498
+ ```js
1499
+ // Instead of embedding the literal in the SQL text:
1500
+ db.query("SELECT * FROM ACTORS WHERE NAME LIKE 'James Wick%'", ...);
1501
+
1502
+ // Bind it as a parameter:
1503
+ db.query('SELECT * FROM ACTORS WHERE NAME LIKE ?', ['James Wick%'], function (err, rows) {
1504
+ if (err) throw err;
1505
+ console.log(rows);
1506
+ });
1507
+ ```
1508
+
971
1509
  ## Contributors
972
1510
 
973
1511
  - Henri Gourvest, <https://github.com/hgourvest>
@@ -0,0 +1,20 @@
1
+ /** A single Firebird status-vector entry (gds code plus optional parameters). */
2
+ export interface FbStatusItem {
3
+ gdscode: number;
4
+ params?: any[];
5
+ }
6
+ /** Error-like object produced by the wire protocol layer. */
7
+ export interface FbStatusObject {
8
+ status?: FbStatusItem[];
9
+ message?: string;
10
+ sqlcode?: number;
11
+ }
12
+ /** An Error enriched with the Firebird gds code and its parameters. */
13
+ export interface FbError extends Error {
14
+ gdscode?: number;
15
+ gdsparams?: any[];
16
+ }
17
+ export type SimpleCallback = (err?: any) => void;
18
+ export type Callback<T = any> = (err?: any, result?: T) => void;
19
+ export declare function doError(obj: any, callback?: (...args: any[]) => void): void;
20
+ export declare function doCallback<T>(obj: T, callback?: Callback<T>): void;
package/lib/callback.js CHANGED
@@ -1,24 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.doError = doError;
4
+ exports.doCallback = doCallback;
1
5
  function doError(obj, callback) {
2
6
  if (callback)
3
- callback(obj)
7
+ callback(obj);
4
8
  }
5
-
6
9
  function isError(obj) {
7
- return Boolean(
8
- obj != null && typeof obj === "object" && !Array.isArray(obj) && obj.status
9
- );
10
+ return Boolean(obj != null && typeof obj === "object" && !Array.isArray(obj) && obj.status);
10
11
  }
11
-
12
12
  function doCallback(obj, callback) {
13
-
14
13
  if (!callback)
15
14
  return;
16
-
17
15
  if (obj instanceof Error) {
18
16
  callback(obj);
19
17
  return;
20
18
  }
21
-
22
19
  if (isError(obj)) {
23
20
  var error = new Error(obj.message);
24
21
  var status = obj.status && obj.status.length && obj.status[0] || {};
@@ -27,12 +24,5 @@ function doCallback(obj, callback) {
27
24
  callback(error);
28
25
  return;
29
26
  }
30
-
31
27
  callback(undefined, obj);
32
-
33
- }
34
-
35
- module.exports = {
36
- doError,
37
- doCallback
38
28
  }