node-firebird 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  - [Connection types](#connection-types) — connection options, classic connections, pooling
15
15
  - [Database object (db)](#database-object-db) — database, transaction and statement methods/options
16
16
  - [Examples](#examples) — parametrized queries, BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
17
- - [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), statement timeouts, scrollable cursors, RETURNING multiple rows, SKIP LOCKED, advanced pooling
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)
20
20
  - [Contributing](#contributing) · [Contributors](#contributors)
@@ -133,9 +133,9 @@ Available wrappers:
133
133
 
134
134
  - **module** — `Firebird.attachAsync(options)`, `createAsync`, `attachOrCreateAsync`, `dropAsync`; resolve with a `Database` (or a `ServiceManager` when `options.manager` is `true`)
135
135
  - **pool** — `pool.getAsync()`, `pool.destroyAsync()`, `pool.withConnection(work)`
136
- - **database** — `db.queryAsync(sql, params?, options?)`, `executeAsync`, `sequentiallyAsync(sql, params?, onRow, options?)`, `transactionAsync(options?)`, `newStatementAsync(sql)`, `attachEventAsync()`, `detachAsync()`, `dropAsync()`, `db.withTransaction(work, options?)`
137
- - **transaction** — `queryAsync`, `executeAsync`, `sequentiallyAsync`, `newStatementAsync`, `commitAsync`, `rollbackAsync`, `commitRetainingAsync`, `rollbackRetainingAsync`
138
- - **statement** — `executeAsync(transaction, params?, options?)`, `fetchAsync`, `fetchScrollAsync`, `fetchAllAsync`, `closeAsync`, `dropAsync`, `releaseAsync`
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
+ - **transaction** — `queryAsync`, `executeAsync`, `executeBatchAsync`, `sequentiallyAsync`, `newStatementAsync`, `commitAsync`, `rollbackAsync`, `commitRetainingAsync`, `rollbackRetainingAsync`
138
+ - **statement** — `executeAsync(transaction, params?, options?)`, `executeBatchAsync(transaction, rows, options?)`, `fetchAsync`, `fetchScrollAsync`, `fetchAllAsync`, `closeAsync`, `dropAsync`, `releaseAsync`
139
139
 
140
140
  Notes:
141
141
 
@@ -217,6 +217,44 @@ pool.get(function (err, db) {
217
217
  pool.destroy();
218
218
  ```
219
219
 
220
+ #### Pool events and metrics
221
+
222
+ The pool is an `EventEmitter` and exposes live counters, following the
223
+ `pg.Pool` conventions:
224
+
225
+ ```js
226
+ const pool = Firebird.pool(10, {
227
+ ...options,
228
+ idleTimeoutMillis: 30000, // close connections idle for 30s…
229
+ min: 2, // …but always keep 2 alive
230
+ connectTimeout: 5000,
231
+ });
232
+
233
+ pool.on('connect', (db) => console.log('new server connection'));
234
+ pool.on('acquire', (db) => console.log('connection handed to a caller'));
235
+ pool.on('release', (db) => console.log('connection returned to the pool'));
236
+ pool.on('remove', (db) => console.log('connection closed & removed'));
237
+ pool.on('error', (err, db) => console.error('background pool error', err));
238
+
239
+ // live metrics — e.g. for a /health endpoint or periodic monitoring
240
+ console.log({
241
+ total: pool.totalCount, // physical connections (idle + in use)
242
+ idle: pool.idleCount, // available in the pool
243
+ active: pool.activeCount, // handed out to callers
244
+ waiting: pool.waitingCount, // get() calls queued for a free slot
245
+ });
246
+ ```
247
+
248
+ - `idleTimeoutMillis` closes connections that sat idle in the pool for that
249
+ long, never shrinking below `min` — long-lived pools no longer hold every
250
+ connection they ever created (issue [#329](https://github.com/hgourvest/node-firebird/issues/329)).
251
+ The sweep also evicts idle connections whose socket has died, so callers
252
+ don't receive them after a server restart (issue [#343](https://github.com/hgourvest/node-firebird/issues/343)).
253
+ - `error` is a background-error channel (idle eviction failures and the
254
+ like); unlike a plain `EventEmitter`, it is only emitted when a listener
255
+ is attached, so existing applications keep working unchanged.
256
+ - Metrics are plain getters — reading them has no side effects.
257
+
220
258
  #### Advanced Pooling Features
221
259
 
222
260
  The pool implementation includes several safeguards for reliability:
@@ -224,6 +262,7 @@ The pool implementation includes several safeguards for reliability:
224
262
  1. **Connection Timeout**: Use `options.connectTimeout` to prevent the pool from hanging if a server accepts the TCP connection but fails to respond to the Firebird wire protocol (e.g., during high load or authentication stalls).
225
263
  2. **Pool Destruction**: Calling `pool.destroy()` now immediately drains the `pending` queue, notifying all waiting callers with an error. It also prevents any further `pool.get()` calls.
226
264
  3. **Slot Recovery**: If a connection attempt times out, the pool slot is correctly freed so subsequent requests can be served. Late-arriving connections are automatically discarded to prevent resource leaks.
265
+ 4. **Idle Reaping & Health**: `idleTimeoutMillis`/`min` shrink the pool when traffic drops and evict dead idle connections (see [Pool events and metrics](#pool-events-and-metrics)).
227
266
 
228
267
  #### Pool Lifecycle State Diagram
229
268
 
@@ -277,6 +316,7 @@ sequenceDiagram
277
316
 
278
317
  - `db.query(query, [params], function(err, result), options)` - classic query, returns Array of Object
279
318
  - `db.execute(query, [params], function(err, result), options)` - classic query, returns Array of Array
319
+ - `db.executeBatch(query, rows, function(err, result), options)` - bulk execution in one round-trip, all-or-nothing (FB >= 4.0, see [Batch Execution](#batch-execution-firebird-40))
280
320
  - `db.sequentially(query, [params], function(row, index), function(err), options)` - sequentially query
281
321
  - `db.detach(function(err))` detach a database
282
322
  - `db.transaction(options, function(err, transaction))` create transaction
@@ -303,6 +343,7 @@ const options = {
303
343
 
304
344
  - `transaction.query(query, [params], function(err, result), options)` - classic query, returns Array of Object
305
345
  - `transaction.execute(query, [params], function(err, result), options)` - classic query, returns Array of Array
346
+ - `transaction.executeBatch(query, rows, function(err, result), options)` - bulk execution with per-record errors (FB >= 4.0, see [Batch Execution](#batch-execution-firebird-40))
306
347
  - `transaction.sequentially(query, [params], function(row, index), function(err), options)` - sequentially query
307
348
  - `transaction.commit(function(err))` commit current transaction
308
349
  - `transaction.rollback(function(err))` rollback current transaction
@@ -1192,6 +1233,60 @@ Notes:
1192
1233
  - A signal that is already aborted rejects immediately with an `AbortError`
1193
1234
  without contacting the server; cancelling an idle connection is harmless.
1194
1235
 
1236
+ ### Batch Execution (Firebird 4.0+)
1237
+
1238
+ `executeBatch` sends many parameter rows for one statement in a single
1239
+ round-trip using the Firebird 4 wire batch API (`op_batch_create` /
1240
+ `op_batch_msg` / `op_batch_exec`) — typically 5–10× faster than executing
1241
+ row by row. Available on the database, transaction and statement objects, in
1242
+ callback and promise flavours.
1243
+
1244
+ ```js
1245
+ const rows = [
1246
+ [1, 'Alice', 10.50, new Date(), true, 100n],
1247
+ [2, 'Bob', null, new Date(), false, null], // NULLs per column
1248
+ [3, 'Carol', 7.25, new Date(), true, 300n],
1249
+ ];
1250
+
1251
+ // database level: own transaction, all-or-nothing (rollback if any record fails)
1252
+ const res = await db.executeBatchAsync(
1253
+ 'INSERT INTO t (id, name, amount, created, active, big) VALUES (?, ?, ?, ?, ?, ?)',
1254
+ rows);
1255
+ console.log(res.recordCount, res.updateCounts, res.success);
1256
+
1257
+ // transaction level: partial success — failed records are reported,
1258
+ // the rest can still be committed
1259
+ const tr = await db.transactionAsync();
1260
+ const r = await tr.executeBatchAsync('INSERT INTO t (...) VALUES (?, ?, ?, ?, ?, ?)', rows);
1261
+ if (!r.success) {
1262
+ console.log('failed record indexes:', r.errorRecordNumbers); // 0-based
1263
+ console.log('first error:', r.errors[0].error.message);
1264
+ }
1265
+ await tr.commitAsync();
1266
+ ```
1267
+
1268
+ The result object contains `recordCount`, `updateCounts` (one entry per
1269
+ record), `errors` (`{ recordNumber, error }` with full gdscode details),
1270
+ `errorRecordNumbers` and `success`. At the database level a failed batch
1271
+ rejects with the first record error, carrying the full completion state as
1272
+ `err.batchCompletion`.
1273
+
1274
+ Options (last argument): `chunkSize` (rows per `op_batch_msg` packet,
1275
+ default 500), `multiError` (default `true` — collect all record errors
1276
+ instead of stopping at the first), `bufferSize` (server-side batch buffer
1277
+ in bytes).
1278
+
1279
+ Notes:
1280
+
1281
+ - Requires wire protocol 16+ (Firebird 4.0 or newer server).
1282
+ - Values are encoded from the statement's own parameter metadata, so
1283
+ NUMERIC/DECIMAL scale, `BIGINT`/`INT128` (pass `BigInt`), `BOOLEAN`,
1284
+ `TIMESTAMP`/`DATE`/`TIME`, `FLOAT`/`DOUBLE` and `DECFLOAT` all round-trip
1285
+ exactly. BLOB and ARRAY parameters are not supported in batches yet.
1286
+ - Oversized `CHAR`/`VARCHAR` values fail the batch client-side before
1287
+ anything is sent; server-side record errors (constraint violations,
1288
+ truncation…) are reported per record.
1289
+
1195
1290
  ### Statement Timeouts (Firebird 4.0+)
1196
1291
  Setting a statement timeout allows the client to automatically abort queries that take too long on the server.
1197
1292
  ```js
package/lib/pool.d.ts CHANGED
@@ -3,19 +3,61 @@
3
3
  * Simple Pooling
4
4
  *
5
5
  ***************************************/
6
+ import Events from 'events';
6
7
  import type { Callback } from './callback';
7
8
  type AttachFn = (options: any, callback: Callback) => void;
8
- declare class Pool {
9
+ /**
10
+ * Connection pool with pg.Pool-style observability.
11
+ *
12
+ * Events (all optional to listen to):
13
+ * 'connect' (db) — a new physical connection was established
14
+ * 'acquire' (db) — a connection was handed to a caller
15
+ * 'release' (db) — a connection was returned to the idle pool
16
+ * 'remove' (db) — a physical connection left the pool for good
17
+ * 'error' (err, db) — a background error (idle eviction, reaper detach);
18
+ * only emitted when a listener is attached, so
19
+ * existing applications never crash on it
20
+ *
21
+ * Metrics: totalCount, idleCount, activeCount, waitingCount.
22
+ *
23
+ * Options: max (factory argument), options.min (floor the reaper never
24
+ * shrinks below), options.idleTimeoutMillis (close idle connections after
25
+ * this many ms; 0/absent = never), options.connectTimeout.
26
+ */
27
+ declare class Pool extends Events.EventEmitter {
9
28
  attach: AttachFn;
10
29
  internaldb: any[];
11
30
  pooldb: any[];
12
31
  dbinuse: number;
13
32
  _creating: number;
14
33
  max: number;
34
+ min: number;
35
+ idleTimeoutMillis: number;
15
36
  pending: Callback[];
16
37
  options: any;
17
38
  _destroyed: boolean;
39
+ _reaper: NodeJS.Timeout | null;
18
40
  constructor(attach: AttachFn, max: number, options: any);
41
+ /** Physical connections owned by the pool (idle + in use). */
42
+ get totalCount(): number;
43
+ /** Connections sitting idle in the pool. */
44
+ get idleCount(): number;
45
+ /** Connections currently handed out to callers. */
46
+ get activeCount(): number;
47
+ /** get() requests queued for a free slot. */
48
+ get waitingCount(): number;
49
+ /** True when the connection can no longer be used. */
50
+ _isDead(db: any): boolean;
51
+ /** Drop a physical connection from the pool's books and emit 'remove'. */
52
+ _forget(db: any): void;
53
+ /** Emit 'error' only when someone listens — never crash the app. */
54
+ _emitError(err: any, db?: any): void;
55
+ /**
56
+ * Idle sweep: evict dead idle connections immediately and close healthy
57
+ * ones that have been idle longer than idleTimeoutMillis, keeping at
58
+ * least `min` physical connections. (issue #329)
59
+ */
60
+ _reap(): void;
19
61
  get(callback: Callback): this;
20
62
  check(): this;
21
63
  destroy(callback?: (err?: any) => void): void;
package/lib/pool.js CHANGED
@@ -4,18 +4,124 @@
4
4
  * Simple Pooling
5
5
  *
6
6
  ***************************************/
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ const events_1 = __importDefault(require("events"));
7
11
  const callback_1 = require("./callback");
8
- class Pool {
12
+ /**
13
+ * Connection pool with pg.Pool-style observability.
14
+ *
15
+ * Events (all optional to listen to):
16
+ * 'connect' (db) — a new physical connection was established
17
+ * 'acquire' (db) — a connection was handed to a caller
18
+ * 'release' (db) — a connection was returned to the idle pool
19
+ * 'remove' (db) — a physical connection left the pool for good
20
+ * 'error' (err, db) — a background error (idle eviction, reaper detach);
21
+ * only emitted when a listener is attached, so
22
+ * existing applications never crash on it
23
+ *
24
+ * Metrics: totalCount, idleCount, activeCount, waitingCount.
25
+ *
26
+ * Options: max (factory argument), options.min (floor the reaper never
27
+ * shrinks below), options.idleTimeoutMillis (close idle connections after
28
+ * this many ms; 0/absent = never), options.connectTimeout.
29
+ */
30
+ class Pool extends events_1.default.EventEmitter {
9
31
  constructor(attach, max, options) {
32
+ super();
10
33
  this.attach = attach;
11
34
  this.internaldb = []; // connections created by the pool (for destroy)
12
35
  this.pooldb = []; // connections available in the pool (idle)
13
36
  this.dbinuse = 0; // connections currently handed out to callers
14
37
  this._creating = 0; // connections currently being created (attach in flight)
15
38
  this.max = max || 4;
39
+ this.min = (options && options.min > 0) ? Math.min(options.min, this.max) : 0;
40
+ this.idleTimeoutMillis = (options && options.idleTimeoutMillis > 0) ? options.idleTimeoutMillis : 0;
16
41
  this.pending = []; // callbacks waiting for a free slot
17
42
  this.options = options;
18
43
  this._destroyed = false; // true after destroy() — prevents further use
44
+ this._reaper = null;
45
+ if (this.idleTimeoutMillis) {
46
+ var self = this;
47
+ // Sweep at half the idle timeout (bounded to 100ms..30s) so a
48
+ // connection lives at most ~1.5x idleTimeoutMillis. unref() keeps
49
+ // the timer from holding the process open.
50
+ var interval = Math.min(Math.max(this.idleTimeoutMillis / 2, 100), 30000);
51
+ this._reaper = setInterval(function () { self._reap(); }, interval);
52
+ if (this._reaper.unref)
53
+ this._reaper.unref();
54
+ }
55
+ }
56
+ /** Physical connections owned by the pool (idle + in use). */
57
+ get totalCount() {
58
+ return this.internaldb.length;
59
+ }
60
+ /** Connections sitting idle in the pool. */
61
+ get idleCount() {
62
+ return this.pooldb.length;
63
+ }
64
+ /** Connections currently handed out to callers. */
65
+ get activeCount() {
66
+ return this.dbinuse;
67
+ }
68
+ /** get() requests queued for a free slot. */
69
+ get waitingCount() {
70
+ return this.pending.length;
71
+ }
72
+ /** True when the connection can no longer be used. */
73
+ _isDead(db) {
74
+ return !db.connection || db.connection._isClosed || db.connection._isDetach ||
75
+ !db.connection._socket || db.connection._socket.destroyed;
76
+ }
77
+ /** Drop a physical connection from the pool's books and emit 'remove'. */
78
+ _forget(db) {
79
+ var idx = this.internaldb.indexOf(db);
80
+ if (idx !== -1)
81
+ this.internaldb.splice(idx, 1);
82
+ idx = this.pooldb.indexOf(db);
83
+ if (idx !== -1)
84
+ this.pooldb.splice(idx, 1);
85
+ this.emit('remove', db);
86
+ }
87
+ /** Emit 'error' only when someone listens — never crash the app. */
88
+ _emitError(err, db) {
89
+ if (this.listenerCount('error') > 0)
90
+ this.emit('error', err, db);
91
+ }
92
+ /**
93
+ * Idle sweep: evict dead idle connections immediately and close healthy
94
+ * ones that have been idle longer than idleTimeoutMillis, keeping at
95
+ * least `min` physical connections. (issue #329)
96
+ */
97
+ _reap() {
98
+ if (this._destroyed)
99
+ return;
100
+ var self = this;
101
+ var now = Date.now();
102
+ // iterate over a copy — we splice from pooldb while walking
103
+ this.pooldb.slice().forEach(function (db) {
104
+ if (self._isDead(db)) {
105
+ self._forget(db);
106
+ return;
107
+ }
108
+ if (self.internaldb.length <= self.min)
109
+ return;
110
+ var idleSince = typeof db.__poolIdleSince === 'number' ? db.__poolIdleSince : now;
111
+ if (now - idleSince < self.idleTimeoutMillis)
112
+ return;
113
+ self._forget(db);
114
+ db.connection._pooled = false;
115
+ try {
116
+ db.detach(function (err) {
117
+ if (err)
118
+ self._emitError(err, db);
119
+ });
120
+ }
121
+ catch (e) {
122
+ self._emitError(e, db);
123
+ }
124
+ });
19
125
  }
20
126
  get(callback) {
21
127
  // [Fix 2] Reject immediately if the pool has already been destroyed.
@@ -41,16 +147,15 @@ class Pool {
41
147
  if (self.pooldb.length) {
42
148
  var db = self.pooldb.shift();
43
149
  // Discard connections that have been closed or destroyed while idle
44
- if (db.connection && (db.connection._isClosed || db.connection._isDetach || !db.connection._socket || db.connection._socket.destroyed)) {
45
- var idx = self.internaldb.indexOf(db);
46
- if (idx !== -1)
47
- self.internaldb.splice(idx, 1);
150
+ if (self._isDead(db)) {
151
+ self._forget(db);
48
152
  self.pending.unshift(cb);
49
153
  setImmediate(function () { self.check(); });
50
154
  return self;
51
155
  }
52
156
  // Idle connection available — hand it out immediately.
53
157
  self.dbinuse++;
158
+ self.emit('acquire', db);
54
159
  cb(null, db);
55
160
  }
56
161
  else {
@@ -113,13 +218,20 @@ class Pool {
113
218
  if (self.pooldb.indexOf(db) !== -1 || self.internaldb.indexOf(db) === -1)
114
219
  return;
115
220
  // if not usable don't put it back in the pool
116
- if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false)
221
+ if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false) {
117
222
  self.internaldb.splice(self.internaldb.indexOf(db), 1);
118
- else
223
+ self.emit('remove', db);
224
+ }
225
+ else {
226
+ db.__poolIdleSince = Date.now();
119
227
  self.pooldb.push(db);
228
+ self.emit('release', db);
229
+ }
120
230
  self.dbinuse--;
121
231
  self.check();
122
232
  });
233
+ self.emit('connect', db);
234
+ self.emit('acquire', db);
123
235
  }
124
236
  cb(err, db);
125
237
  });
@@ -132,6 +244,10 @@ class Pool {
132
244
  destroy(callback) {
133
245
  var self = this;
134
246
  self._destroyed = true;
247
+ if (self._reaper) {
248
+ clearInterval(self._reaper);
249
+ self._reaper = null;
250
+ }
135
251
  // [Fix 4] Drain pending callbacks so callers are not left hanging.
136
252
  // This is critical when destroy() is called as a recovery measure after
137
253
  // a timeout: without draining, every concurrent pool.get() that had not
@@ -170,6 +286,7 @@ class Pool {
170
286
  self.pooldb.splice(_db_in_pool, 1);
171
287
  db.connection._pooled = false;
172
288
  db.detach(detachCallback);
289
+ self.emit('remove', db);
173
290
  }
174
291
  else {
175
292
  // [Fix 5] Connection is currently in use (dbinuse > 0).
package/lib/types.d.ts CHANGED
@@ -64,6 +64,32 @@ export type TransactionOptions = {
64
64
  wait?: boolean;
65
65
  waitTimeout?: number;
66
66
  };
67
+ /** Result of an executeBatch call (Firebird 4 batch API, protocol 16+). */
68
+ export interface BatchResult {
69
+ /** Total number of records processed by the server. */
70
+ recordCount: number;
71
+ /** Per-record update counts (in record order). */
72
+ updateCounts: number[];
73
+ /** Per-record failures with full status vectors (capped by `detailedErrors`). */
74
+ errors: Array<{
75
+ recordNumber: number;
76
+ error: Error;
77
+ }>;
78
+ /** Record numbers of ALL failed records (detailed + status-less). */
79
+ errorRecordNumbers: number[];
80
+ /** True when every record executed without error. */
81
+ success: boolean;
82
+ }
83
+ export type BatchOptions = {
84
+ /** Continue past per-record errors and report them all (default true). */
85
+ multiError?: boolean;
86
+ /** Server-side batch buffer limit in bytes (BATCH_TAG_BUFFER_BYTES_SIZE). */
87
+ bufferSize?: number;
88
+ /** Max number of detailed per-record status vectors returned (server default 64). */
89
+ detailedErrors?: number;
90
+ /** Rows per op_batch_msg packet (default 500). */
91
+ chunkSize?: number;
92
+ };
67
93
  export type QueryOptions = {
68
94
  timeout?: number;
69
95
  scrollable?: boolean;
@@ -83,6 +109,8 @@ export interface Database {
83
109
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
84
110
  query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
85
111
  execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
112
+ /** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
113
+ executeBatch(query: string, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
86
114
  sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
87
115
  drop(callback: SimpleCallback): void;
88
116
  escape(value: any): string;
@@ -93,6 +121,7 @@ export interface Database {
93
121
  createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
94
122
  queryAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
95
123
  executeAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
124
+ executeBatchAsync(query: string, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
96
125
  sequentiallyAsync(query: string, params: any[] | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
97
126
  sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
98
127
  transactionAsync(options?: TransactionOptions | Isolation): Promise<Transaction>;
@@ -116,6 +145,8 @@ export interface Transaction {
116
145
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
117
146
  query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
118
147
  execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
148
+ /** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
149
+ executeBatch(query: string, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
119
150
  sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
120
151
  commit(callback?: SimpleCallback): void;
121
152
  commitRetaining(callback?: SimpleCallback): void;
@@ -123,6 +154,7 @@ export interface Transaction {
123
154
  rollbackRetaining(callback?: SimpleCallback): void;
124
155
  queryAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
125
156
  executeAsync<T = any>(query: string, params?: any[], options?: QueryOptions): Promise<T[]>;
157
+ executeBatchAsync(query: string, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
126
158
  sequentiallyAsync(query: string, params: any[] | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
127
159
  sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
128
160
  newStatementAsync(query: string): Promise<Statement>;
@@ -139,7 +171,10 @@ export interface Statement {
139
171
  fetch(transaction: Transaction, count: number, callback: QueryCallback): void;
140
172
  fetchScroll(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset: number, count: number, callback: QueryCallback): void;
141
173
  fetchAll(transaction: Transaction, callback: QueryCallback): void;
174
+ /** Execute this prepared statement once per row (Firebird 4.0+ batch API). */
175
+ executeBatch(transaction: Transaction, rows: any[][], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
142
176
  executeAsync(transaction: Transaction, params?: any[], options?: QueryOptions): Promise<any>;
177
+ executeBatchAsync(transaction: Transaction, rows: any[][], options?: BatchOptions): Promise<BatchResult>;
143
178
  fetchAsync(transaction: Transaction, count: number | 'all'): Promise<any>;
144
179
  fetchScrollAsync(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset?: number, count?: number): Promise<any>;
145
180
  fetchAllAsync(transaction: Transaction): Promise<any>;
@@ -189,6 +224,19 @@ export interface Options {
189
224
  * expected Firebird server response time under load.
190
225
  */
191
226
  connectTimeout?: number;
227
+ /**
228
+ * Pool only: minimum number of physical connections the idle reaper
229
+ * keeps alive. Only meaningful together with `idleTimeoutMillis`.
230
+ * Default 0 (the pool may shrink to no connections).
231
+ */
232
+ min?: number;
233
+ /**
234
+ * Pool only: close connections that have been idle in the pool for this
235
+ * many milliseconds, never shrinking below `min`. Dead idle connections
236
+ * (server restarts, dropped sockets) are evicted on the same sweep.
237
+ * Default 0 (idle connections are kept forever).
238
+ */
239
+ idleTimeoutMillis?: number;
192
240
  /**
193
241
  * **Firebird 6.0+ only (Protocol 20+)**
194
242
  *
@@ -230,9 +278,24 @@ export interface Options {
230
278
  export interface SvcMgrOptions extends Options {
231
279
  manager: true;
232
280
  }
281
+ export type PoolEvent = 'connect' | 'acquire' | 'release' | 'remove' | 'error';
233
282
  export interface ConnectionPool {
234
283
  get(callback: DatabaseCallback): void;
235
284
  destroy(callback?: SimpleCallback): void;
285
+ /** Physical connections owned by the pool (idle + in use). */
286
+ readonly totalCount: number;
287
+ /** Connections sitting idle in the pool. */
288
+ readonly idleCount: number;
289
+ /** Connections currently handed out to callers. */
290
+ readonly activeCount: number;
291
+ /** get() requests queued for a free slot. */
292
+ readonly waitingCount: number;
293
+ on(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
294
+ on(event: 'error', listener: (err: Error, db?: Database) => void): this;
295
+ once(event: 'connect' | 'acquire' | 'release' | 'remove', listener: (db: Database) => void): this;
296
+ once(event: 'error', listener: (err: Error, db?: Database) => void): this;
297
+ off(event: PoolEvent, listener: (...args: any[]) => void): this;
298
+ removeListener(event: PoolEvent, listener: (...args: any[]) => void): this;
236
299
  getAsync(): Promise<Database>;
237
300
  destroyAsync(): Promise<void>;
238
301
  /** Acquire a connection, run `work`, always return the connection to the pool. */
@@ -64,6 +64,8 @@ declare class Connection {
64
64
  * op_cancel packet itself has no response, so nothing is queued here.
65
65
  */
66
66
  cancelOperation(kind: any, callback: any): this;
67
+ /** Write a prebuilt packet and queue its response callback. */
68
+ _queueEventBuffer(buffer: any, callback: any): void;
67
69
  _queueEvent(callback: any, defer?: boolean): void;
68
70
  connect(options: any, callback: any): void;
69
71
  attach(options: any, callback?: any, db?: any): void;
@@ -82,6 +84,19 @@ declare class Connection {
82
84
  allocateAndPrepareStatement(transaction: any, query: any, plan: any, callback: any): void;
83
85
  prepare(transaction: any, query: any, plan: any, callback: any): void;
84
86
  prepareStatement(transaction: any, statement: any, query: any, plan: any, callback: any): this;
87
+ /**
88
+ * Execute a statement once per row using the Firebird 4 batch API
89
+ * (protocol 16+): op_batch_create + op_batch_msg(s) + op_batch_exec +
90
+ * op_batch_rls, all pipelined in a single network flush. Every packet
91
+ * gets an in-order response (op_batch_cs for exec), so the regular
92
+ * response queue keeps everything in sync.
93
+ *
94
+ * rows: array of parameter arrays, one per record. BLOB/ARRAY columns
95
+ * are not supported yet. The callback receives a completion object:
96
+ * { recordCount, updateCounts, errors: [{recordNumber, error}],
97
+ * errorRecordNumbers, success }.
98
+ */
99
+ executeBatch(transaction: any, statement: any, rows: any, callback: any, options: any): this;
85
100
  executeStatement(transaction: any, statement: any, params: any, callback: any, custom: any): this;
86
101
  sendExecute(op: number, statement: any, transaction: any, callback: any, parameters?: any[]): void;
87
102
  fetch(statement: any, transaction: any, count: any, callback: any): void;