node-firebird 2.12.0 → 2.13.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 +96 -6
- package/lib/types.d.ts +38 -5
- package/lib/utils.d.ts +12 -0
- package/lib/utils.js +20 -2
- package/lib/wire/batch-stream.d.ts +26 -0
- package/lib/wire/batch-stream.js +109 -0
- package/lib/wire/codepages.d.ts +23 -0
- package/lib/wire/codepages.js +137 -0
- package/lib/wire/connection.d.ts +38 -4
- package/lib/wire/connection.js +294 -44
- package/lib/wire/database.d.ts +9 -0
- package/lib/wire/database.js +13 -6
- package/lib/wire/serialize.d.ts +2 -0
- package/lib/wire/serialize.js +10 -0
- package/lib/wire/socket.js +9 -3
- package/lib/wire/transaction.d.ts +6 -0
- package/lib/wire/transaction.js +27 -2
- package/lib/wire/xsqlvar.d.ts +56 -1
- package/lib/wire/xsqlvar.js +107 -29
- package/package.json +19 -1
- package/src/types.ts +40 -5
- package/src/utils.ts +19 -1
- package/src/wire/batch-stream.ts +121 -0
- package/src/wire/codepages.ts +147 -0
- package/src/wire/connection.ts +315 -49
- package/src/wire/database.ts +15 -7
- package/src/wire/serialize.ts +11 -0
- package/src/wire/socket.ts +9 -3
- package/src/wire/transaction.ts +28 -2
- package/src/wire/xsqlvar.ts +129 -30
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
- [Connection types](#connection-types) — connection options, `firebird://` URIs and traditional connection strings, classic connections, pooling
|
|
15
15
|
- [Database object (db)](#database-object-db) — database, transaction and statement methods/options
|
|
16
16
|
- [Examples](#examples) — parametrized queries, tagged-template queries (sql), named placeholders, nested result tables (nestTables), row-key transforms (transformKeys), result metadata / affected rows (withMeta), custom type parsers (typeCast), BLOBs, streaming big data, transactions, driver events, database events (POST_EVENT), service manager, charsets/encoding, Firebird 3.0–6.0 features
|
|
17
|
-
- [Extensive Examples](#extensive-examples) — DECFLOAT/INT128, query cancellation (AbortSignal), batch execution (bulk inserts), 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 incl. BLOBs), bulk-insert stream (batchStream), 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)
|
|
@@ -39,16 +39,31 @@ and 26 against Firebird 3, 4, 5 and 6).
|
|
|
39
39
|
|
|
40
40
|
## Usage
|
|
41
41
|
|
|
42
|
+
CommonJS and ESM are both first-class (conditional `exports`):
|
|
43
|
+
|
|
42
44
|
```js
|
|
43
|
-
|
|
45
|
+
// CommonJS
|
|
46
|
+
const Firebird = require('node-firebird');
|
|
47
|
+
|
|
48
|
+
// ESM — default and named imports both work
|
|
49
|
+
import Firebird from 'node-firebird';
|
|
50
|
+
import { attach, pool, GDSCode, SQL_TYPES } from 'node-firebird';
|
|
44
51
|
```
|
|
45
52
|
|
|
53
|
+
The documented subpaths keep working in both module systems
|
|
54
|
+
(`require('node-firebird/lib/gdscodes')`, …).
|
|
55
|
+
|
|
46
56
|
TypeScript is fully supported — the driver itself is written in TypeScript and
|
|
47
|
-
ships its own type declarations:
|
|
57
|
+
ships its own type declarations, with generics on the query APIs:
|
|
48
58
|
|
|
49
59
|
```ts
|
|
50
60
|
import * as Firebird from 'node-firebird';
|
|
51
61
|
import type { Options, Database } from 'node-firebird';
|
|
62
|
+
|
|
63
|
+
interface Emp { ID: number; NAME: string }
|
|
64
|
+
const rows = await db.queryAsync<Emp>('SELECT ID, NAME FROM EMP'); // Emp[]
|
|
65
|
+
db.query<Emp>('SELECT ID, NAME FROM EMP', [], (err, rows) => { /* rows: Emp[] */ });
|
|
66
|
+
const r = await db.queryAsync<Emp>('SELECT ...', [], { withMeta: true }); // QueryResult<Emp>
|
|
52
67
|
```
|
|
53
68
|
|
|
54
69
|
### Developing the driver
|
|
@@ -1209,7 +1224,11 @@ Firebird.attach(options, function (err, db) {
|
|
|
1209
1224
|
});
|
|
1210
1225
|
|
|
1211
1226
|
db.on('error', function (err) {
|
|
1212
|
-
// connection-level errors (socket errors, closed connection, etc.)
|
|
1227
|
+
// connection-level errors (socket errors, closed connection, etc.).
|
|
1228
|
+
// Delivered to listeners only: without one, background failures (e.g.
|
|
1229
|
+
// a failed automatic reconnect) are NOT re-thrown as uncaught
|
|
1230
|
+
// exceptions — the operations they affect still receive the error
|
|
1231
|
+
// through their own callbacks/promises.
|
|
1213
1232
|
});
|
|
1214
1233
|
|
|
1215
1234
|
db.on('transaction', function (options) {
|
|
@@ -1506,7 +1525,27 @@ Commonly used Firebird character sets are automatically mapped to their correspo
|
|
|
1506
1525
|
| `ASCII` | `ascii` | 7-bit ASCII. |
|
|
1507
1526
|
| `NONE` | `latin1` | Raw/unspecified character set. Treated as binary-safe 8-bit characters. |
|
|
1508
1527
|
|
|
1509
|
-
|
|
1528
|
+
Beyond Node's native encodings, the driver ships **codepage codecs** for the
|
|
1529
|
+
single-byte charsets (decode *and* encode — columns, parameters, SQL
|
|
1530
|
+
literals and `blobAsText` blobs all transcode):
|
|
1531
|
+
|
|
1532
|
+
> `WIN1250`–`WIN1258` (Central European, Cyrillic, Greek, Turkish, Hebrew,
|
|
1533
|
+
> Arabic, Baltic, Vietnamese), `ISO8859_2`–`ISO8859_9`, `ISO8859_13`,
|
|
1534
|
+
> `KOI8R`, `KOI8U`, `DOS866`
|
|
1535
|
+
|
|
1536
|
+
```js
|
|
1537
|
+
const options = { /* ... */ encoding: 'WIN1251' };
|
|
1538
|
+
await db.queryAsync('INSERT INTO T VALUES (?)', ['Привет']); // encoded as cp1251
|
|
1539
|
+
```
|
|
1540
|
+
|
|
1541
|
+
The codecs are built from Node's ICU tables at first use (present in every
|
|
1542
|
+
official Node build). `attachOrCreate`/`create` honour `options.encoding`
|
|
1543
|
+
for the new database's default charset too. Accented characters and
|
|
1544
|
+
fixed-length `CHAR(N)` whitespace/truncation are handled automatically per
|
|
1545
|
+
the charset width — and single-byte columns (including charset `NONE`) are
|
|
1546
|
+
readable in full under the default UTF8 connection (the declared fetch
|
|
1547
|
+
lengths are widened per the charset-width ratio, fixing the
|
|
1548
|
+
`string right truncation` errors of issue [#422](https://github.com/hgourvest/node-firebird/issues/422)).
|
|
1510
1549
|
|
|
1511
1550
|
#### Custom Charset Connection Example
|
|
1512
1551
|
```js
|
|
@@ -1770,11 +1809,43 @@ Notes:
|
|
|
1770
1809
|
- Values are encoded from the statement's own parameter metadata, so
|
|
1771
1810
|
NUMERIC/DECIMAL scale, `BIGINT`/`INT128` (pass `BigInt`), `BOOLEAN`,
|
|
1772
1811
|
`TIMESTAMP`/`DATE`/`TIME`, `FLOAT`/`DOUBLE` and `DECFLOAT` all round-trip
|
|
1773
|
-
exactly.
|
|
1812
|
+
exactly.
|
|
1813
|
+
- `BLOB` columns accept Buffers, strings, JSON-able objects, or
|
|
1814
|
+
pre-created blob quad ids: values are uploaded as transaction blobs
|
|
1815
|
+
first — all initiated back-to-back so the blob ops pipeline on the
|
|
1816
|
+
wire — and the batch messages reference their ids. `ARRAY` parameters
|
|
1817
|
+
are not supported.
|
|
1774
1818
|
- Oversized `CHAR`/`VARCHAR` values fail the batch client-side before
|
|
1775
1819
|
anything is sent; server-side record errors (constraint violations,
|
|
1776
1820
|
truncation…) are reported per record.
|
|
1777
1821
|
|
|
1822
|
+
### Bulk-insert stream (batchStream, Firebird 4.0+)
|
|
1823
|
+
|
|
1824
|
+
`db.batchStream(sql, options)` is the COPY FROM analogue: an object-mode
|
|
1825
|
+
`Writable` that flushes parameter-array rows in chunks through one
|
|
1826
|
+
prepared statement using the batch API. The Database form runs its own
|
|
1827
|
+
transaction — **committed on finish, rolled back on error or destroy**,
|
|
1828
|
+
all-or-nothing for the whole stream:
|
|
1829
|
+
|
|
1830
|
+
```js
|
|
1831
|
+
const { pipeline } = require('stream/promises');
|
|
1832
|
+
|
|
1833
|
+
const stream = db.batchStream('INSERT INTO EVENTS VALUES (?, ?, ?)', {
|
|
1834
|
+
flushRows: 1000, // rows buffered per batch flush (default 1000)
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
await pipeline(mySourceOfRowArrays, stream); // e.g. a CSV parser
|
|
1838
|
+
console.log(stream.recordCount, stream.affectedRows); // totals after 'finish'
|
|
1839
|
+
```
|
|
1840
|
+
|
|
1841
|
+
Backpressure is the `Writable` machinery itself: writes pause while a
|
|
1842
|
+
chunk is in flight, so an arbitrarily large source never accumulates in
|
|
1843
|
+
memory beyond `flushRows`. BLOB columns accept Buffers/strings per the
|
|
1844
|
+
batch rules above. `transaction.batchStream(sql, options)` runs inside an
|
|
1845
|
+
existing transaction and leaves commit/rollback to you. The remaining
|
|
1846
|
+
options (`chunkSize`, `bufferSize`, …) pass through to
|
|
1847
|
+
[executeBatch](#batch-execution-firebird-40).
|
|
1848
|
+
|
|
1778
1849
|
### Statement Timeouts (Firebird 4.0+)
|
|
1779
1850
|
Setting a statement timeout allows the client to automatically abort queries that take too long on the server.
|
|
1780
1851
|
```js
|
|
@@ -2110,6 +2181,25 @@ app.get('/users/:id/picture', withConnection(pool, function (db, req, res, done)
|
|
|
2110
2181
|
|
|
2111
2182
|
Answers to recurring questions from the [issue tracker](https://github.com/hgourvest/node-firebird/issues).
|
|
2112
2183
|
|
|
2184
|
+
#### Text comes back as `������` with a WIN1250/1251/1253/1257 database (issue [#319](https://github.com/hgourvest/node-firebird/issues/319))
|
|
2185
|
+
|
|
2186
|
+
Resolved — the driver now ships codepage codecs for the single-byte
|
|
2187
|
+
charsets (`WIN1250`–`WIN1258`, `ISO8859_2`–`9`/`13`, `KOI8R`/`KOI8U`,
|
|
2188
|
+
`DOS866`): just set the matching connection encoding and both reads and
|
|
2189
|
+
writes transcode correctly, including parameters, SQL literals and
|
|
2190
|
+
`blobAsText` blobs:
|
|
2191
|
+
|
|
2192
|
+
```js
|
|
2193
|
+
const options = { /* ... */ encoding: 'WIN1253' };
|
|
2194
|
+
```
|
|
2195
|
+
|
|
2196
|
+
See [§ Character Set & Encoding Support](#character-set--encoding-support).
|
|
2197
|
+
For a charset *outside* that list (e.g. `DOS437`/`DOS850`), the
|
|
2198
|
+
[iconv-lite](https://www.npmjs.com/package/iconv-lite) escape hatch still
|
|
2199
|
+
works: connect with `encoding: 'NONE'`, read raw bytes via `latin1` in a
|
|
2200
|
+
[`typeCast` hook](#custom-type-parsers-typecast) and decode with the real
|
|
2201
|
+
codepage; write already-encoded bytes as Buffer parameters.
|
|
2202
|
+
|
|
2113
2203
|
#### Can I use aggregate functions like `LIST()`? I get "no database to handle" when I call the result.
|
|
2114
2204
|
|
|
2115
2205
|
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".
|
package/lib/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Readable } from 'stream';
|
|
1
|
+
import type { Readable, Writable } from 'stream';
|
|
2
2
|
import type { SqlTag } from './sql-template';
|
|
3
3
|
export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
|
|
4
4
|
export type DatabaseCallback = (err: any, db: Database) => void;
|
|
@@ -193,6 +193,20 @@ export interface QueryResult<T = any> {
|
|
|
193
193
|
recordCounts?: RecordCounts;
|
|
194
194
|
warnings: ServerWarning[];
|
|
195
195
|
}
|
|
196
|
+
/** Options for batchStream: the executeBatch options plus stream tuning. */
|
|
197
|
+
export type BatchStreamOptions = BatchOptions & {
|
|
198
|
+
/** Rows buffered per executeBatch flush (default 1000). */
|
|
199
|
+
flushRows?: number;
|
|
200
|
+
/** Writable highWaterMark in rows (default: flushRows). */
|
|
201
|
+
highWaterMark?: number;
|
|
202
|
+
};
|
|
203
|
+
/** The Writable returned by batchStream, with totals valid after 'finish'. */
|
|
204
|
+
export interface BatchStream extends Writable {
|
|
205
|
+
/** Records the server processed so far. */
|
|
206
|
+
recordCount: number;
|
|
207
|
+
/** Sum of per-record update counts so far. */
|
|
208
|
+
affectedRows: number;
|
|
209
|
+
}
|
|
196
210
|
export type QueryStreamOptions = QueryOptions & {
|
|
197
211
|
/**
|
|
198
212
|
* Rows buffered internally before fetching pauses (object-mode
|
|
@@ -213,8 +227,8 @@ export interface Database {
|
|
|
213
227
|
detach(callback?: SimpleCallback): Database;
|
|
214
228
|
transaction(options: TransactionOptions | Isolation | TransactionCallback, callback?: TransactionCallback): Database;
|
|
215
229
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
|
|
216
|
-
query(query: string, params: QueryParams, callback:
|
|
217
|
-
execute(query: string, params: QueryParams, callback:
|
|
230
|
+
query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
|
|
231
|
+
execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
|
|
218
232
|
/** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
|
|
219
233
|
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
|
|
220
234
|
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
@@ -225,6 +239,13 @@ export interface Database {
|
|
|
225
239
|
* fetch and releases the statement.
|
|
226
240
|
*/
|
|
227
241
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
|
|
242
|
+
/**
|
|
243
|
+
* Bulk-insert Writable (COPY FROM analogue, Firebird 4.0+): write
|
|
244
|
+
* parameter-array rows; they are flushed in chunks through the batch
|
|
245
|
+
* API. Runs its own transaction — committed on finish, rolled back on
|
|
246
|
+
* error/destroy. BLOB columns accept Buffers/strings.
|
|
247
|
+
*/
|
|
248
|
+
batchStream(query: string, options?: BatchStreamOptions): BatchStream;
|
|
228
249
|
drop(callback: SimpleCallback): void;
|
|
229
250
|
escape(value: any): string;
|
|
230
251
|
attachEvent(callback: any): this;
|
|
@@ -270,8 +291,8 @@ export interface Transaction {
|
|
|
270
291
|
*/
|
|
271
292
|
savepoint<T>(work: (transaction: Transaction) => Promise<T> | T): Promise<T>;
|
|
272
293
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
|
|
273
|
-
query(query: string, params: QueryParams, callback:
|
|
274
|
-
execute(query: string, params: QueryParams, callback:
|
|
294
|
+
query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
|
|
295
|
+
execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
|
|
275
296
|
/** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
|
|
276
297
|
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
|
|
277
298
|
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
@@ -281,6 +302,11 @@ export interface Transaction {
|
|
|
281
302
|
* transaction is NOT committed when the stream ends.
|
|
282
303
|
*/
|
|
283
304
|
queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
|
|
305
|
+
/**
|
|
306
|
+
* Bulk-insert Writable inside this transaction (see
|
|
307
|
+
* Database.batchStream); commit/rollback stays with the caller.
|
|
308
|
+
*/
|
|
309
|
+
batchStream(query: string, options?: BatchStreamOptions): BatchStream;
|
|
284
310
|
commit(callback?: SimpleCallback): void;
|
|
285
311
|
commitRetaining(callback?: SimpleCallback): void;
|
|
286
312
|
rollback(callback?: SimpleCallback): void;
|
|
@@ -358,6 +384,13 @@ export interface Options {
|
|
|
358
384
|
* per-query `namedPlaceholders: false` override.
|
|
359
385
|
*/
|
|
360
386
|
namedPlaceholders?: boolean;
|
|
387
|
+
/**
|
|
388
|
+
* Default character set of a NEWLY CREATED database (create /
|
|
389
|
+
* attachOrCreate only). Falls back to the connection `encoding`, then
|
|
390
|
+
* UTF8 — pass e.g. `defaultCharset: 'UTF8'` to keep a modern database
|
|
391
|
+
* default while connecting with a legacy codepage `encoding`.
|
|
392
|
+
*/
|
|
393
|
+
defaultCharset?: string;
|
|
361
394
|
/**
|
|
362
395
|
* Qualify object-row keys by source table (same option as mysql2), so
|
|
363
396
|
* JOINed columns with the same name stop overwriting each other:
|
package/lib/utils.d.ts
CHANGED
|
@@ -6,6 +6,18 @@ export declare const parseDate: (str: string) => Date;
|
|
|
6
6
|
/**
|
|
7
7
|
* Get Error Message per gdscode
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Turn a failed executeBatch completion into the all-or-nothing error
|
|
11
|
+
* shape shared by database.executeBatch and batchStream: the first
|
|
12
|
+
* record's own error (or a synthesized summary), with the full
|
|
13
|
+
* completion attached as err.batchCompletion.
|
|
14
|
+
*/
|
|
15
|
+
export declare const batchResultToError: (result: {
|
|
16
|
+
errors: {
|
|
17
|
+
error: any;
|
|
18
|
+
}[];
|
|
19
|
+
errorRecordNumbers: number[];
|
|
20
|
+
}) => any;
|
|
9
21
|
export declare const lookupMessages: (status: FbStatusItem[]) => string;
|
|
10
22
|
/**
|
|
11
23
|
* Escape value
|
package/lib/utils.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.escape = exports.lookupMessages = exports.parseDate = void 0;
|
|
6
|
+
exports.escape = exports.lookupMessages = exports.batchResultToError = exports.parseDate = void 0;
|
|
7
7
|
exports.noop = noop;
|
|
8
8
|
const firebird_msg_json_1 = __importDefault(require("./firebird.msg.json"));
|
|
9
9
|
const const_1 = __importDefault(require("./wire/const"));
|
|
@@ -97,6 +97,21 @@ exports.parseDate = parseDate;
|
|
|
97
97
|
/**
|
|
98
98
|
* Get Error Message per gdscode
|
|
99
99
|
*/
|
|
100
|
+
/**
|
|
101
|
+
* Turn a failed executeBatch completion into the all-or-nothing error
|
|
102
|
+
* shape shared by database.executeBatch and batchStream: the first
|
|
103
|
+
* record's own error (or a synthesized summary), with the full
|
|
104
|
+
* completion attached as err.batchCompletion.
|
|
105
|
+
*/
|
|
106
|
+
const batchResultToError = (result) => {
|
|
107
|
+
const first = result.errors.length ? result.errors[0] : null;
|
|
108
|
+
const err = first
|
|
109
|
+
? first.error
|
|
110
|
+
: new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
|
|
111
|
+
err.batchCompletion = result;
|
|
112
|
+
return err;
|
|
113
|
+
};
|
|
114
|
+
exports.batchResultToError = batchResultToError;
|
|
100
115
|
const lookupMessages = (status) => {
|
|
101
116
|
const messages = status.map((item) => {
|
|
102
117
|
let text = MessagesError[item.gdscode];
|
|
@@ -130,7 +145,10 @@ const escape = function (value, protocolVersion) {
|
|
|
130
145
|
case 'number':
|
|
131
146
|
return value.toString();
|
|
132
147
|
case 'string':
|
|
133
|
-
|
|
148
|
+
// Firebird string literals have NO backslash escapes — only the
|
|
149
|
+
// quote is doubled. Doubling backslashes corrupted the data
|
|
150
|
+
// (issue #156: '\' arrived as '\\').
|
|
151
|
+
return "'" + value.replace(/'/g, "''") + "'";
|
|
134
152
|
}
|
|
135
153
|
if (value instanceof Date)
|
|
136
154
|
return "'" + value.getFullYear() + '-' + (value.getMonth() + 1).toString().padStart(2, '0') + '-' + value.getDate().toString().padStart(2, '0') + ' ' + value.getHours().toString().padStart(2, '0') + ':' + value.getMinutes().toString().padStart(2, '0') + ':' + value.getSeconds().toString().padStart(2, '0') + '.' + value.getMilliseconds().toString().padStart(3, '0') + "'";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* batchStream — object-mode Writable over the Firebird 4 batch API
|
|
4
|
+
*
|
|
5
|
+
* The COPY FROM analogue: write parameter rows, they are flushed in
|
|
6
|
+
* chunks through statement.executeBatch (single prepared statement,
|
|
7
|
+
* protocol-level batching, BLOB values included). Backpressure is the
|
|
8
|
+
* Writable machinery itself: a write callback is held while a chunk
|
|
9
|
+
* is in flight.
|
|
10
|
+
*
|
|
11
|
+
***************************************/
|
|
12
|
+
import { Writable } from 'stream';
|
|
13
|
+
/**
|
|
14
|
+
* Build the Writable for Database.batchStream / Transaction.batchStream.
|
|
15
|
+
* With `ownsTransaction` (the Database form) the stream runs its own
|
|
16
|
+
* transaction: committed on finish, rolled back on error/destroy —
|
|
17
|
+
* all-or-nothing for the whole stream. The Transaction form leaves
|
|
18
|
+
* commit/rollback to the caller.
|
|
19
|
+
*
|
|
20
|
+
* Rows accumulate up to options.flushRows (default 1000) per
|
|
21
|
+
* executeBatch flush; the remaining executeBatch options (chunkSize,
|
|
22
|
+
* bufferSize, …) pass through. After 'finish', stream.recordCount and
|
|
23
|
+
* stream.affectedRows carry the totals.
|
|
24
|
+
*/
|
|
25
|
+
declare function makeBatchStream(target: any, query: string, options: any, ownsTransaction: boolean): Writable;
|
|
26
|
+
export = makeBatchStream;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/***************************************
|
|
3
|
+
*
|
|
4
|
+
* batchStream — object-mode Writable over the Firebird 4 batch API
|
|
5
|
+
*
|
|
6
|
+
* The COPY FROM analogue: write parameter rows, they are flushed in
|
|
7
|
+
* chunks through statement.executeBatch (single prepared statement,
|
|
8
|
+
* protocol-level batching, BLOB values included). Backpressure is the
|
|
9
|
+
* Writable machinery itself: a write callback is held while a chunk
|
|
10
|
+
* is in flight.
|
|
11
|
+
*
|
|
12
|
+
***************************************/
|
|
13
|
+
const stream_1 = require("stream");
|
|
14
|
+
const callback_1 = require("../callback");
|
|
15
|
+
const utils_1 = require("../utils");
|
|
16
|
+
/**
|
|
17
|
+
* Build the Writable for Database.batchStream / Transaction.batchStream.
|
|
18
|
+
* With `ownsTransaction` (the Database form) the stream runs its own
|
|
19
|
+
* transaction: committed on finish, rolled back on error/destroy —
|
|
20
|
+
* all-or-nothing for the whole stream. The Transaction form leaves
|
|
21
|
+
* commit/rollback to the caller.
|
|
22
|
+
*
|
|
23
|
+
* Rows accumulate up to options.flushRows (default 1000) per
|
|
24
|
+
* executeBatch flush; the remaining executeBatch options (chunkSize,
|
|
25
|
+
* bufferSize, …) pass through. After 'finish', stream.recordCount and
|
|
26
|
+
* stream.affectedRows carry the totals.
|
|
27
|
+
*/
|
|
28
|
+
function makeBatchStream(target, query, options, ownsTransaction) {
|
|
29
|
+
options = options || {};
|
|
30
|
+
const flushRows = options.flushRows > 0 ? Math.floor(options.flushRows) : 1000;
|
|
31
|
+
const batchOptions = { ...options };
|
|
32
|
+
delete batchOptions.flushRows;
|
|
33
|
+
delete batchOptions.highWaterMark;
|
|
34
|
+
let transaction = null;
|
|
35
|
+
let statement = null;
|
|
36
|
+
let buffered = [];
|
|
37
|
+
const init = async () => {
|
|
38
|
+
if (statement) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
transaction = ownsTransaction ? await target.transactionAsync() : target;
|
|
42
|
+
statement = await (0, callback_1.fromCallback)((cb) => transaction.newStatement(query, cb));
|
|
43
|
+
};
|
|
44
|
+
const flush = async () => {
|
|
45
|
+
if (!buffered.length) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await init();
|
|
49
|
+
const chunk = buffered;
|
|
50
|
+
buffered = [];
|
|
51
|
+
const result = await (0, callback_1.fromCallback)((cb) => statement.executeBatch(transaction, chunk, cb, batchOptions));
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
// the same all-or-nothing error shape database.executeBatch uses
|
|
54
|
+
throw (0, utils_1.batchResultToError)(result);
|
|
55
|
+
}
|
|
56
|
+
stream.recordCount += result.recordCount;
|
|
57
|
+
for (const count of result.updateCounts) {
|
|
58
|
+
stream.affectedRows += count;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const cleanup = async (commit) => {
|
|
62
|
+
if (statement) {
|
|
63
|
+
const stmt = statement;
|
|
64
|
+
statement = null;
|
|
65
|
+
await new Promise((resolve) => stmt.release(() => resolve()));
|
|
66
|
+
}
|
|
67
|
+
if (ownsTransaction && transaction) {
|
|
68
|
+
const tx = transaction;
|
|
69
|
+
transaction = null;
|
|
70
|
+
await (commit ? tx.commitAsync() : tx.rollbackAsync());
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const stream = new stream_1.Writable({
|
|
74
|
+
objectMode: true,
|
|
75
|
+
highWaterMark: options.highWaterMark > 0 ? options.highWaterMark : flushRows,
|
|
76
|
+
write(row, _enc, cb) {
|
|
77
|
+
if (!Array.isArray(row)) {
|
|
78
|
+
cb(new Error('batchStream expects parameter-array rows'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
buffered.push(row);
|
|
82
|
+
if (buffered.length >= flushRows) {
|
|
83
|
+
flush().then(() => cb(), cb);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
cb();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
final(cb) {
|
|
90
|
+
// an empty stream finishes without touching the server at all
|
|
91
|
+
// (flush() early-returns and init never runs)
|
|
92
|
+
flush()
|
|
93
|
+
.then(() => cleanup(true))
|
|
94
|
+
.then(() => cb(), (err) => {
|
|
95
|
+
// the failed stream must not commit half a bulk load
|
|
96
|
+
cleanup(false).catch(() => { });
|
|
97
|
+
cb(err);
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
destroy(err, cb) {
|
|
101
|
+
cleanup(false)
|
|
102
|
+
.then(() => cb(err), () => cb(err));
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
stream.recordCount = 0;
|
|
106
|
+
stream.affectedRows = 0;
|
|
107
|
+
return stream;
|
|
108
|
+
}
|
|
109
|
+
module.exports = makeBatchStream;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* Single-byte codepage codecs (WIN125x, ISO8859_x, KOI8, DOS866)
|
|
4
|
+
*
|
|
5
|
+
* Node's Buffer only decodes utf8/latin1/ascii natively. These
|
|
6
|
+
* codepages are decoded through the WHATWG TextDecoder (backed by
|
|
7
|
+
* ICU — present in every official Node build) and encoded through
|
|
8
|
+
* reverse tables built from the same decoder at first use, so the
|
|
9
|
+
* two directions can never disagree. Issues #319/#301/#422.
|
|
10
|
+
*
|
|
11
|
+
***************************************/
|
|
12
|
+
export interface TextCodec {
|
|
13
|
+
/** Firebird charset name (upper case). */
|
|
14
|
+
name: string;
|
|
15
|
+
decode(buffer: Buffer): string;
|
|
16
|
+
encode(value: string): Buffer;
|
|
17
|
+
}
|
|
18
|
+
export declare function charsetWidthById(id: number | undefined): number;
|
|
19
|
+
/**
|
|
20
|
+
* Codec for a Firebird charset name, or null when the charset is unknown,
|
|
21
|
+
* natively handled by Buffer, or the ICU tables are unavailable. Cached.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getCodec(charsetName: string | undefined): TextCodec | null;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/***************************************
|
|
3
|
+
*
|
|
4
|
+
* Single-byte codepage codecs (WIN125x, ISO8859_x, KOI8, DOS866)
|
|
5
|
+
*
|
|
6
|
+
* Node's Buffer only decodes utf8/latin1/ascii natively. These
|
|
7
|
+
* codepages are decoded through the WHATWG TextDecoder (backed by
|
|
8
|
+
* ICU — present in every official Node build) and encoded through
|
|
9
|
+
* reverse tables built from the same decoder at first use, so the
|
|
10
|
+
* two directions can never disagree. Issues #319/#301/#422.
|
|
11
|
+
*
|
|
12
|
+
***************************************/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.charsetWidthById = charsetWidthById;
|
|
15
|
+
exports.getCodec = getCodec;
|
|
16
|
+
/** Firebird charset name → WHATWG encoding label (single-byte only). */
|
|
17
|
+
const ICU_LABELS = Object.freeze({
|
|
18
|
+
WIN1250: 'windows-1250',
|
|
19
|
+
WIN1251: 'windows-1251',
|
|
20
|
+
WIN1253: 'windows-1253',
|
|
21
|
+
WIN1254: 'windows-1254',
|
|
22
|
+
WIN1255: 'windows-1255',
|
|
23
|
+
WIN1256: 'windows-1256',
|
|
24
|
+
WIN1257: 'windows-1257',
|
|
25
|
+
WIN1258: 'windows-1258',
|
|
26
|
+
ISO8859_2: 'iso-8859-2',
|
|
27
|
+
ISO8859_3: 'iso-8859-3',
|
|
28
|
+
ISO8859_4: 'iso-8859-4',
|
|
29
|
+
ISO8859_5: 'iso-8859-5',
|
|
30
|
+
ISO8859_6: 'iso-8859-6',
|
|
31
|
+
ISO8859_7: 'iso-8859-7',
|
|
32
|
+
ISO8859_8: 'iso-8859-8',
|
|
33
|
+
ISO8859_9: 'iso-8859-9',
|
|
34
|
+
ISO8859_13: 'iso-8859-13',
|
|
35
|
+
KOI8R: 'koi8-r',
|
|
36
|
+
KOI8U: 'koi8-u',
|
|
37
|
+
DOS866: 'ibm866',
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Bytes-per-character by Firebird charset id (RDB$CHARACTER_SETS —
|
|
41
|
+
* verified against a live server). Everything not listed (NONE, ASCII,
|
|
42
|
+
* ISO8859_x, WIN125x, DOS*, KOI8*, CYRL, TIS620, …) is single-byte.
|
|
43
|
+
*/
|
|
44
|
+
const CHARSET_WIDTH_BY_ID = Object.freeze({
|
|
45
|
+
3: 3, // UNICODE_FSS
|
|
46
|
+
4: 4, // UTF8
|
|
47
|
+
5: 2, // SJIS_0208
|
|
48
|
+
6: 2, // EUCJ_0208
|
|
49
|
+
44: 2, // KSC_5601
|
|
50
|
+
56: 2, // BIG_5
|
|
51
|
+
57: 2, // GB_2312
|
|
52
|
+
67: 2, // GBK
|
|
53
|
+
68: 2, // CP943C
|
|
54
|
+
69: 4, // GB18030
|
|
55
|
+
});
|
|
56
|
+
function charsetWidthById(id) {
|
|
57
|
+
if (id === undefined) {
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
return CHARSET_WIDTH_BY_ID[id] || 1;
|
|
61
|
+
}
|
|
62
|
+
const cache = new Map();
|
|
63
|
+
function buildCodec(name) {
|
|
64
|
+
const label = ICU_LABELS[name];
|
|
65
|
+
if (!label) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
let decoder;
|
|
69
|
+
try {
|
|
70
|
+
decoder = new TextDecoder(label);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Node built with small-icu: legacy encodings unavailable
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
// Build both directions from the decoder, one byte at a time — every
|
|
77
|
+
// byte of a single-byte codepage maps to exactly one BMP character
|
|
78
|
+
// (undefined bytes decode to U+FFFD, which is kept for decoding but
|
|
79
|
+
// never used for the reverse map).
|
|
80
|
+
const toCode = new Uint16Array(256);
|
|
81
|
+
const toByte = new Map();
|
|
82
|
+
const one = Buffer.alloc(1);
|
|
83
|
+
for (let i = 0; i < 256; i++) {
|
|
84
|
+
one[0] = i;
|
|
85
|
+
const ch = decoder.decode(one);
|
|
86
|
+
toCode[i] = ch.charCodeAt(0);
|
|
87
|
+
if (ch !== '�' && !toByte.has(ch)) {
|
|
88
|
+
toByte.set(ch, i);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
name,
|
|
93
|
+
decode(buffer) {
|
|
94
|
+
// batch through fromCharCode instead of per-byte string concat —
|
|
95
|
+
// wide CHAR columns and text blobs decode in O(chunks) allocations
|
|
96
|
+
const codes = new Array(buffer.length);
|
|
97
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
98
|
+
codes[i] = toCode[buffer[i]];
|
|
99
|
+
}
|
|
100
|
+
const CHUNK = 4096;
|
|
101
|
+
if (codes.length <= CHUNK) {
|
|
102
|
+
return String.fromCharCode(...codes);
|
|
103
|
+
}
|
|
104
|
+
let out = '';
|
|
105
|
+
for (let i = 0; i < codes.length; i += CHUNK) {
|
|
106
|
+
out += String.fromCharCode(...codes.slice(i, i + CHUNK));
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
},
|
|
110
|
+
encode(value) {
|
|
111
|
+
const out = Buffer.alloc(value.length);
|
|
112
|
+
for (let i = 0; i < value.length; i++) {
|
|
113
|
+
const b = toByte.get(value[i]);
|
|
114
|
+
// unmappable characters become '?' — the convention every
|
|
115
|
+
// codepage transcoder (incl. iconv) uses by default
|
|
116
|
+
out[i] = b === undefined ? 0x3f : b;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Codec for a Firebird charset name, or null when the charset is unknown,
|
|
124
|
+
* natively handled by Buffer, or the ICU tables are unavailable. Cached.
|
|
125
|
+
*/
|
|
126
|
+
function getCodec(charsetName) {
|
|
127
|
+
if (!charsetName) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const name = String(charsetName).toUpperCase();
|
|
131
|
+
let codec = cache.get(name);
|
|
132
|
+
if (codec === undefined) {
|
|
133
|
+
codec = buildCodec(name);
|
|
134
|
+
cache.set(name, codec);
|
|
135
|
+
}
|
|
136
|
+
return codec;
|
|
137
|
+
}
|