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 +99 -4
- package/lib/pool.d.ts +43 -1
- package/lib/pool.js +124 -7
- package/lib/types.d.ts +63 -0
- package/lib/wire/connection.d.ts +15 -0
- package/lib/wire/connection.js +461 -0
- package/lib/wire/const.d.ts +15 -0
- package/lib/wire/const.js +21 -0
- package/lib/wire/database.d.ts +11 -0
- package/lib/wire/database.js +46 -0
- package/lib/wire/serialize.d.ts +2 -1
- package/lib/wire/serialize.js +9 -4
- package/lib/wire/statement.d.ts +6 -0
- package/lib/wire/statement.js +11 -0
- package/lib/wire/transaction.d.ts +11 -0
- package/lib/wire/transaction.js +27 -0
- package/lib/wire/xsqlvar.d.ts +9 -0
- package/lib/wire/xsqlvar.js +22 -11
- package/package.json +1 -1
- package/src/pool.ts +126 -6
- package/src/types.ts +68 -0
- package/src/wire/connection.ts +498 -0
- package/src/wire/const.ts +22 -0
- package/src/wire/database.ts +53 -0
- package/src/wire/serialize.ts +11 -5
- package/src/wire/statement.ts +13 -0
- package/src/wire/transaction.ts +30 -0
- package/src/wire/xsqlvar.ts +25 -14
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
|
-
|
|
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
|
-
|
|
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 (
|
|
45
|
-
|
|
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
|
-
|
|
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. */
|
package/lib/wire/connection.d.ts
CHANGED
|
@@ -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;
|