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/src/wire/connection.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as srp from '../srp';
|
|
|
8
8
|
import * as crypt from '../unix-crypt';
|
|
9
9
|
import Const from './const';
|
|
10
10
|
import * as Xsql from './xsqlvar';
|
|
11
|
+
import { charsetWidthById } from './codepages';
|
|
11
12
|
import ServiceManager from './service';
|
|
12
13
|
import Database from './database';
|
|
13
14
|
import Statement from './statement';
|
|
@@ -78,6 +79,21 @@ function statementCacheLimit(options: InternalOptions): number {
|
|
|
78
79
|
// SQL type-code names live in xsqlvar.ts alongside the descriptors
|
|
79
80
|
const SQL_TYPE_NAMES = Xsql.SQL_TYPE_NAMES;
|
|
80
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Build the wire parameter for a text value in the CONNECTION charset:
|
|
84
|
+
* plain SQLParamString for UTF-8 connections, pre-encoded bytes for
|
|
85
|
+
* everything else — SQLParamString hardwires utf8, which corrupted
|
|
86
|
+
* writes on latin1/codepage connections (issues #319/#301).
|
|
87
|
+
*/
|
|
88
|
+
function textParam(options: InternalOptions, value: string) {
|
|
89
|
+
const codec = Xsql.resolveTextCodec(options);
|
|
90
|
+
const enc = Xsql.resolveTextEncoding(options);
|
|
91
|
+
if (!codec && enc === 'utf8') {
|
|
92
|
+
return new Xsql.SQLParamString(value);
|
|
93
|
+
}
|
|
94
|
+
return new Xsql.SQLParamBuffer(Xsql.encodeConnectionText(options, value));
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
/**
|
|
82
98
|
* Run the user's typeCast hook (options.typeCast) for one column value.
|
|
83
99
|
* The hook receives the column metadata and a next() returning the value
|
|
@@ -263,6 +279,25 @@ class Connection {
|
|
|
263
279
|
}
|
|
264
280
|
|
|
265
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Deliver a connection-level error to 'error' listeners — and ONLY to
|
|
284
|
+
* listeners. Emitting an unlistened 'error' makes Node throw the error
|
|
285
|
+
* object as an uncaught exception; for errors that originate in
|
|
286
|
+
* background contexts (the reconnect timer, socket-level failures whose
|
|
287
|
+
* operations are separately rejected via _rejectPending) that crashes
|
|
288
|
+
* the process — or, under a test runner, fails whatever unrelated test
|
|
289
|
+
* happens to be running. The failing operations themselves always
|
|
290
|
+
* still receive their error through their own callbacks.
|
|
291
|
+
*/
|
|
292
|
+
_emitError(err: any) {
|
|
293
|
+
if (this.db && typeof this.db.listenerCount === 'function' && this.db.listenerCount('error') > 0) {
|
|
294
|
+
this.db.emit('error', err);
|
|
295
|
+
} else if (process.env.FIREBIRD_DEBUG) {
|
|
296
|
+
console.warn('[fb-debug] connection error (no error listener):', err && err.message);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
266
301
|
_bind_events(host: string, port: number, callback: SimpleCallback | undefined) {
|
|
267
302
|
|
|
268
303
|
var self = this;
|
|
@@ -287,6 +322,15 @@ class Connection {
|
|
|
287
322
|
|
|
288
323
|
self._rejectPending(lostError);
|
|
289
324
|
|
|
325
|
+
// Pooled connections do not self-reconnect: the pool is the
|
|
326
|
+
// recovery authority (dead-connection check on checkout + the
|
|
327
|
+
// reaper), and a background reconnect here would produce a
|
|
328
|
+
// zombie attachment the pool no longer tracks — whose own
|
|
329
|
+
// failures then surface as uncatchable async errors.
|
|
330
|
+
if (self.options && self.options.isPool) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
290
334
|
self._retry_connection_id = setTimeout(function() {
|
|
291
335
|
self._socket.removeAllListeners();
|
|
292
336
|
// transiently null while the replacement Connection is built
|
|
@@ -297,14 +341,14 @@ class Connection {
|
|
|
297
341
|
ctx.connect(self.options, function(err: any) {
|
|
298
342
|
|
|
299
343
|
if (err) {
|
|
300
|
-
self.
|
|
344
|
+
self._emitError(err);
|
|
301
345
|
return;
|
|
302
346
|
}
|
|
303
347
|
|
|
304
348
|
ctx.attach(self.options, function(err: any) {
|
|
305
349
|
|
|
306
350
|
if (err) {
|
|
307
|
-
self.
|
|
351
|
+
self._emitError(err);
|
|
308
352
|
return;
|
|
309
353
|
}
|
|
310
354
|
|
|
@@ -321,11 +365,13 @@ class Connection {
|
|
|
321
365
|
});
|
|
322
366
|
|
|
323
367
|
self._socket.on('error', function(e: any) {
|
|
324
|
-
|
|
368
|
+
|
|
325
369
|
self.error = e;
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
370
|
+
|
|
371
|
+
// listeners only (_emitError): the affected operations get their
|
|
372
|
+
// errors via the close handler's _rejectPending — an unlistened
|
|
373
|
+
// socket error must not become an uncaught exception
|
|
374
|
+
self._emitError(e);
|
|
329
375
|
|
|
330
376
|
if (callback)
|
|
331
377
|
callback(e);
|
|
@@ -767,7 +813,9 @@ class Connection {
|
|
|
767
813
|
blr.pos = 0;
|
|
768
814
|
|
|
769
815
|
blr.addByte(Const.isc_dpb_version1);
|
|
770
|
-
|
|
816
|
+
// charset names are sent verbatim — normalize case so encoding:
|
|
817
|
+
// 'utf8'/'win1251' (e.g. from URI query params) is accepted
|
|
818
|
+
blr.addString(Const.isc_dpb_lc_ctype, String(options.encoding || 'UTF8').toUpperCase(), Const.DEFAULT_ENCODING);
|
|
771
819
|
|
|
772
820
|
// For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
|
|
773
821
|
if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION13) {
|
|
@@ -895,8 +943,15 @@ class Connection {
|
|
|
895
943
|
|
|
896
944
|
blr.pos = 0;
|
|
897
945
|
blr.addByte(Const.isc_dpb_version1);
|
|
898
|
-
|
|
899
|
-
|
|
946
|
+
// honour options.encoding on the CREATE path too — hardcoding UTF8
|
|
947
|
+
// for lc_ctype made attachOrCreate silently ignore the requested
|
|
948
|
+
// connection charset (issue #319: 'Malformed string' for codepage
|
|
949
|
+
// text). The new database's DEFAULT charset follows the connection
|
|
950
|
+
// encoding unless options.defaultCharset overrides it.
|
|
951
|
+
blr.addString(Const.isc_dpb_set_db_charset,
|
|
952
|
+
String(options.defaultCharset || options.encoding || 'UTF8').toUpperCase(), Const.DEFAULT_ENCODING);
|
|
953
|
+
blr.addString(Const.isc_dpb_lc_ctype,
|
|
954
|
+
String(options.encoding || 'UTF8').toUpperCase(), Const.DEFAULT_ENCODING);
|
|
900
955
|
|
|
901
956
|
// For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
|
|
902
957
|
if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION13) {
|
|
@@ -1004,7 +1059,9 @@ class Connection {
|
|
|
1004
1059
|
|
|
1005
1060
|
throwClosed(callback: ((err: Error, ...args: any[]) => void) | undefined) {
|
|
1006
1061
|
var err = new Error('Connection is closed.');
|
|
1007
|
-
|
|
1062
|
+
// listeners only: the caller receives the error through its own
|
|
1063
|
+
// callback below either way
|
|
1064
|
+
this._emitError(err);
|
|
1008
1065
|
if (callback)
|
|
1009
1066
|
callback(err);
|
|
1010
1067
|
return this;
|
|
@@ -1254,7 +1311,9 @@ class Connection {
|
|
|
1254
1311
|
msg.addInt(transaction.handle);
|
|
1255
1312
|
msg.addInt(0xFFFF);
|
|
1256
1313
|
msg.addInt(3); // dialect = 3
|
|
1257
|
-
|
|
1314
|
+
// SQL text travels in the CONNECTION charset (codepage
|
|
1315
|
+
// connections encode via codec — greek/cyrillic literals included)
|
|
1316
|
+
msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
|
|
1258
1317
|
msg.addBlr(blr);
|
|
1259
1318
|
msg.addInt(65535); // buffer_length
|
|
1260
1319
|
if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION20) {
|
|
@@ -1316,7 +1375,9 @@ class Connection {
|
|
|
1316
1375
|
msg.addInt(transaction.handle);
|
|
1317
1376
|
msg.addInt(statement.handle);
|
|
1318
1377
|
msg.addInt(3); // dialect = 3
|
|
1319
|
-
|
|
1378
|
+
// SQL text travels in the CONNECTION charset (codepage
|
|
1379
|
+
// connections encode via codec — greek/cyrillic literals included)
|
|
1380
|
+
msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
|
|
1320
1381
|
msg.addBlr(blr);
|
|
1321
1382
|
msg.addInt(65535); // buffer_length
|
|
1322
1383
|
if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION20) {
|
|
@@ -1348,10 +1409,13 @@ class Connection {
|
|
|
1348
1409
|
* gets an in-order response (op_batch_cs for exec), so the regular
|
|
1349
1410
|
* response queue keeps everything in sync.
|
|
1350
1411
|
*
|
|
1351
|
-
* rows: array of parameter arrays, one per record. BLOB
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1412
|
+
* rows: array of parameter arrays, one per record. BLOB columns accept
|
|
1413
|
+
* Buffers, strings, JSON-able objects or pre-created blob quad ids —
|
|
1414
|
+
* values are uploaded as transaction blobs first (all initiated
|
|
1415
|
+
* back-to-back so they pipeline) and the batch messages reference their
|
|
1416
|
+
* ids. ARRAY columns are not supported. The callback receives a
|
|
1417
|
+
* completion object: { recordCount, updateCounts, errors:
|
|
1418
|
+
* [{recordNumber, error}], errorRecordNumbers, success }.
|
|
1355
1419
|
*/
|
|
1356
1420
|
executeBatch(transaction: Transaction, statement: Statement, rows: QueryParams[], callback: BatchCb | undefined, options?: BatchOptions) {
|
|
1357
1421
|
options = options || {};
|
|
@@ -1389,6 +1453,90 @@ class Connection {
|
|
|
1389
1453
|
}
|
|
1390
1454
|
}
|
|
1391
1455
|
|
|
1456
|
+
var self = this;
|
|
1457
|
+
|
|
1458
|
+
// BLOB pre-pass: upload every Buffer/string blob value as a
|
|
1459
|
+
// transaction blob and replace it (in a cloned row) with the quad
|
|
1460
|
+
// id the batch message will carry. All uploads are initiated
|
|
1461
|
+
// back-to-back, so the create/segment/close ops pipeline on the
|
|
1462
|
+
// wire instead of paying a round trip per blob.
|
|
1463
|
+
var blobCols: number[] = [];
|
|
1464
|
+
for (var bj = 0; bj < input.length; bj++) {
|
|
1465
|
+
var bt = input[bj].type;
|
|
1466
|
+
if (bt === Const.SQL_BLOB || bt === Const.SQL_QUAD) {
|
|
1467
|
+
blobCols.push(bj);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// all-NULL blob columns are common — a large row set must not pay
|
|
1472
|
+
// for a full clone when there is nothing to upload or unwrap
|
|
1473
|
+
var needsBlobPass = false;
|
|
1474
|
+
if (blobCols.length) {
|
|
1475
|
+
outer:
|
|
1476
|
+
for (var ri = 0; ri < rows.length; ri++) {
|
|
1477
|
+
for (var ci = 0; ci < blobCols.length; ci++) {
|
|
1478
|
+
var bv = (rows[ri] as any[])[blobCols[ci]];
|
|
1479
|
+
if (bv !== null && bv !== undefined) {
|
|
1480
|
+
needsBlobPass = true;
|
|
1481
|
+
break outer;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (needsBlobPass) {
|
|
1488
|
+
var cloned: any[][] = rows.map(function(r) { return (r as any[]).slice(); });
|
|
1489
|
+
var pendingBlobs = 1; // sentinel so zero uploads still settle
|
|
1490
|
+
var blobFailure: any = null;
|
|
1491
|
+
var settleBlobs = function(err?: any) {
|
|
1492
|
+
if (err && !blobFailure) {
|
|
1493
|
+
blobFailure = err;
|
|
1494
|
+
}
|
|
1495
|
+
if (--pendingBlobs) {
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (blobFailure) {
|
|
1499
|
+
doError(blobFailure, callback);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
self._executeBatchEncoded(transaction, statement, cloned, callback, options);
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
cloned.forEach(function(row) {
|
|
1506
|
+
blobCols.forEach(function(j) {
|
|
1507
|
+
var v = row[j];
|
|
1508
|
+
if (v === null || v === undefined) {
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
// a pre-created blob id (SQLParamQuad wrapper) passes
|
|
1512
|
+
// through; plain {high, low} objects are deliberately NOT
|
|
1513
|
+
// treated as ids — they are legitimate JSON blob content
|
|
1514
|
+
// and would silently misroute to a bogus blob reference
|
|
1515
|
+
if (v instanceof Xsql.SQLParamQuad) {
|
|
1516
|
+
row[j] = v.value;
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
pendingBlobs++;
|
|
1520
|
+
self.uploadBlob(transaction, v, function(err: any, oid: any) {
|
|
1521
|
+
if (!err) {
|
|
1522
|
+
row[j] = oid;
|
|
1523
|
+
}
|
|
1524
|
+
settleBlobs(err);
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1528
|
+
settleBlobs();
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
this._executeBatchEncoded(transaction, statement, rows as any[][], callback, options);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
/** Encode and send the batch packets (rows are fully materialized:
|
|
1537
|
+
* blob values already replaced by quad ids by executeBatch). */
|
|
1538
|
+
_executeBatchEncoded(transaction: Transaction, statement: Statement, rows: any[][], callback: BatchCb | undefined, options: BatchOptions) {
|
|
1539
|
+
var input = statement.input;
|
|
1392
1540
|
var built;
|
|
1393
1541
|
try {
|
|
1394
1542
|
built = buildBatchEncoders(input, Object.assign({}, this.options, options));
|
|
@@ -1584,9 +1732,9 @@ class Connection {
|
|
|
1584
1732
|
if (Buffer.isBuffer(value))
|
|
1585
1733
|
b = value;
|
|
1586
1734
|
else if (typeof(value) === 'string')
|
|
1587
|
-
b =
|
|
1735
|
+
b = Xsql.encodeConnectionText(self.options, value);
|
|
1588
1736
|
else if (!isStream)
|
|
1589
|
-
b =
|
|
1737
|
+
b = Xsql.encodeConnectionText(self.options, JSON.stringify(value));
|
|
1590
1738
|
|
|
1591
1739
|
// Use configured transfer size or default to 1024
|
|
1592
1740
|
var chunkSize = self.options.blobChunkSize || 1024;
|
|
@@ -1737,14 +1885,18 @@ class Connection {
|
|
|
1737
1885
|
ret[i] = new Xsql.SQLParamDouble(value);
|
|
1738
1886
|
break;
|
|
1739
1887
|
case 'string':
|
|
1740
|
-
ret[i] =
|
|
1888
|
+
ret[i] = textParam(self.options, value);
|
|
1741
1889
|
break;
|
|
1742
1890
|
case 'boolean':
|
|
1743
|
-
|
|
1891
|
+
// metadata-directed: BOOLEAN targets get a
|
|
1892
|
+
// real blr_bool (issue #122); smallint
|
|
1893
|
+
// targets keep the legacy 0/1 (BOOLEAN
|
|
1894
|
+
// does not convert to numbers)
|
|
1895
|
+
ret[i] = new Xsql.SQLParamBool(value, meta.type === Const.SQL_BOOLEAN);
|
|
1744
1896
|
break;
|
|
1745
1897
|
default:
|
|
1746
1898
|
//throw new Error('Unexpected parametter: ' + JSON.stringify(params) + ' - ' + JSON.stringify(input));
|
|
1747
|
-
ret[i] =
|
|
1899
|
+
ret[i] = textParam(self.options, value.toString());
|
|
1748
1900
|
break;
|
|
1749
1901
|
}
|
|
1750
1902
|
}
|
|
@@ -2005,6 +2157,37 @@ class Connection {
|
|
|
2005
2157
|
}
|
|
2006
2158
|
|
|
2007
2159
|
|
|
2160
|
+
/**
|
|
2161
|
+
* Resolve the pending blobAsText fetches of a decoded row batch
|
|
2162
|
+
* (ret.arrBlob) and write the text back into ret.data. Reads run
|
|
2163
|
+
* sequentially to respect Firebird's per-connection open-blob-handle
|
|
2164
|
+
* limit (issue #387). Used by fetchAll for cursors and by
|
|
2165
|
+
* transaction.execute for op_execute2 singletons (EXECUTE PROCEDURE /
|
|
2166
|
+
* RETURNING — issue #305, whose blobs never resolved before).
|
|
2167
|
+
*/
|
|
2168
|
+
resolveTextBlobs(transaction: Transaction, ret: any, callback: (err?: any) => void) {
|
|
2169
|
+
const self = this;
|
|
2170
|
+
const fns = (ret && ret.arrBlob) || [];
|
|
2171
|
+
if (!fns.length) {
|
|
2172
|
+
return callback();
|
|
2173
|
+
}
|
|
2174
|
+
const read = (index: number) => {
|
|
2175
|
+
if (index >= fns.length) {
|
|
2176
|
+
return callback();
|
|
2177
|
+
}
|
|
2178
|
+
fns[index](transaction).then((blob: any) => {
|
|
2179
|
+
// nestTables === true rows: the value lives in the
|
|
2180
|
+
// per-table sub-object, not on the row itself
|
|
2181
|
+
Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(
|
|
2182
|
+
self.options, blob.meta || {},
|
|
2183
|
+
parseValueIfJson(blob.value, self.options));
|
|
2184
|
+
read(index + 1);
|
|
2185
|
+
}, callback);
|
|
2186
|
+
};
|
|
2187
|
+
read(0);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
|
|
2008
2191
|
fetchAll(statement: Statement, transaction: Transaction, callback: Callback<any[]>) {
|
|
2009
2192
|
const self = this;
|
|
2010
2193
|
const custom = statement.options || {};
|
|
@@ -2018,29 +2201,10 @@ class Connection {
|
|
|
2018
2201
|
}
|
|
2019
2202
|
|
|
2020
2203
|
if (ret && ret.data && ret.data.length) {
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
const arrBlobFns = ret.arrBlob || [];
|
|
2026
|
-
const readBlobsSequentially = (index: any, results: any) => {
|
|
2027
|
-
if (index >= arrBlobFns.length) {
|
|
2028
|
-
return Promise.resolve(results);
|
|
2029
|
-
}
|
|
2030
|
-
return arrBlobFns[index](transaction).then((v: any) => {
|
|
2031
|
-
results.push(v);
|
|
2032
|
-
return readBlobsSequentially(index + 1, results);
|
|
2033
|
-
});
|
|
2034
|
-
};
|
|
2035
|
-
|
|
2036
|
-
readBlobsSequentially(0, []).then((arrBlob: any) => {
|
|
2037
|
-
for (let i = 0; i < arrBlob.length; i++) {
|
|
2038
|
-
const blob = arrBlob[i];
|
|
2039
|
-
// nestTables === true rows: the value lives in the
|
|
2040
|
-
// per-table sub-object, not on the row itself
|
|
2041
|
-
Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(
|
|
2042
|
-
statement.connection.options, blob.meta || {},
|
|
2043
|
-
parseValueIfJson(blob.value, statement.connection.options));
|
|
2204
|
+
self.resolveTextBlobs(transaction, ret, (blobErr?: any) => {
|
|
2205
|
+
if (blobErr) {
|
|
2206
|
+
callback(blobErr);
|
|
2207
|
+
return;
|
|
2044
2208
|
}
|
|
2045
2209
|
|
|
2046
2210
|
doSynchronousLoop(ret.data, (row, _i, next) => {
|
|
@@ -2062,7 +2226,7 @@ class Connection {
|
|
|
2062
2226
|
self.fetch(statement, transaction, Const.DEFAULT_FETCHSIZE, loop);
|
|
2063
2227
|
}
|
|
2064
2228
|
});
|
|
2065
|
-
})
|
|
2229
|
+
});
|
|
2066
2230
|
return;
|
|
2067
2231
|
}
|
|
2068
2232
|
|
|
@@ -2134,6 +2298,48 @@ class Connection {
|
|
|
2134
2298
|
}
|
|
2135
2299
|
|
|
2136
2300
|
|
|
2301
|
+
/**
|
|
2302
|
+
* Create a transaction blob, upload `value` (Buffer, string, or a
|
|
2303
|
+
* JSON-able object) and deliver its quad id. executeBatch's blob
|
|
2304
|
+
* pre-pass uses this: batch messages reference pre-created transaction
|
|
2305
|
+
* blobs (the batch parameter buffer's default BLOB_NONE policy), just
|
|
2306
|
+
* like the classic execute path stores blob params.
|
|
2307
|
+
*/
|
|
2308
|
+
uploadBlob(transaction: Transaction, value: any, callback: (err: any, oid?: any) => void) {
|
|
2309
|
+
var self = this;
|
|
2310
|
+
var b: Buffer;
|
|
2311
|
+
if (Buffer.isBuffer(value)) {
|
|
2312
|
+
b = value;
|
|
2313
|
+
} else if (typeof value === 'string') {
|
|
2314
|
+
b = Xsql.encodeConnectionText(this.options, value);
|
|
2315
|
+
} else {
|
|
2316
|
+
b = Xsql.encodeConnectionText(this.options, JSON.stringify(value));
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
self.createBlob2(transaction, function(err: any, blob: any) {
|
|
2320
|
+
if (err) {
|
|
2321
|
+
return callback(err);
|
|
2322
|
+
}
|
|
2323
|
+
var chunkSize = self.options.blobChunkSize || 1024;
|
|
2324
|
+
// bufferReader's next() drops errors — capture the first segment
|
|
2325
|
+
// failure so a truncated upload is never reported as success
|
|
2326
|
+
var segmentError: any = null;
|
|
2327
|
+
bufferReader(b, chunkSize, function(part, next) {
|
|
2328
|
+
self.batchSegments(blob, part, function(segErr: any) {
|
|
2329
|
+
if (segErr && !segmentError) {
|
|
2330
|
+
segmentError = segErr;
|
|
2331
|
+
}
|
|
2332
|
+
next();
|
|
2333
|
+
} as any);
|
|
2334
|
+
}, function() {
|
|
2335
|
+
self.closeBlob(blob, function(closeErr: any) {
|
|
2336
|
+
callback(segmentError || closeErr, blob.oid);
|
|
2337
|
+
} as any, false);
|
|
2338
|
+
});
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
|
|
2137
2343
|
svcattach(options: InternalOptions, callback?: Callback<ServiceManager>, svc?: ServiceManager) {
|
|
2138
2344
|
this._lowercase_keys = options.lowercase_keys || Const.DEFAULT_LOWERCASE_KEYS;
|
|
2139
2345
|
var database = options.database || options.filename;
|
|
@@ -3288,6 +3494,43 @@ function describe(buff: Buffer, statement: Statement) {
|
|
|
3288
3494
|
}
|
|
3289
3495
|
unpackCharSetCollation(statement.input);
|
|
3290
3496
|
unpackCharSetCollation(statement.output);
|
|
3497
|
+
scaleOutputLengths(statement.output, statement.connection && statement.connection.options);
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
/**
|
|
3501
|
+
* Widen declared OUTPUT byte lengths for text columns whose charset the
|
|
3502
|
+
* server did NOT transliterate in the describe. Under a UTF8 connection
|
|
3503
|
+
* the server rewrites known charsets (a WIN1251 VARCHAR(15) is described
|
|
3504
|
+
* as UTF8 with length 60) — but NONE (and any untransliterated charset)
|
|
3505
|
+
* keeps its native length, and the engine's fetch-time capacity check
|
|
3506
|
+
* then treats those bytes as connection-charset characters: VARCHAR(15)
|
|
3507
|
+
* NONE declared as 15 bytes only fits floor(15/4) = 3 characters and
|
|
3508
|
+
* longer values fail with "string right truncation" (issue #422).
|
|
3509
|
+
* Declaring charCapacity × connectionWidth bytes makes the check pass;
|
|
3510
|
+
* the width-based trim in the text decoders restores the column's true
|
|
3511
|
+
* character capacity. The native length is preserved for metadata.
|
|
3512
|
+
*/
|
|
3513
|
+
function scaleOutputLengths(output: any[], options: any) {
|
|
3514
|
+
if (!output || !output.length) return;
|
|
3515
|
+
const connWidth = Xsql.getFirebirdCharsetWidth(options && options.encoding);
|
|
3516
|
+
if (connWidth <= 1) return;
|
|
3517
|
+
|
|
3518
|
+
for (let i = 0; i < output.length; i++) {
|
|
3519
|
+
const p = output[i];
|
|
3520
|
+
if (!p || (p.type !== Const.SQL_TEXT && p.type !== Const.SQL_VARYING)) continue;
|
|
3521
|
+
// OCTETS (id 1) is binary and never transliterated
|
|
3522
|
+
if (p.charSetId === undefined || p.charSetId === 1) continue;
|
|
3523
|
+
const colWidth = charsetWidthById(p.charSetId);
|
|
3524
|
+
if (colWidth >= connWidth) continue;
|
|
3525
|
+
|
|
3526
|
+
p.nativeLength = p.length;
|
|
3527
|
+
// BLR encodes the length as a 16-bit word — cap the widened value
|
|
3528
|
+
// (a >16KB single-byte column keeps its full byte capacity, it just
|
|
3529
|
+
// can't be over-declared four-fold)
|
|
3530
|
+
p.length = Math.min(
|
|
3531
|
+
Math.floor(p.length / colWidth) * connWidth,
|
|
3532
|
+
Math.floor(0xFFFF / connWidth) * connWidth);
|
|
3533
|
+
}
|
|
3291
3534
|
}
|
|
3292
3535
|
|
|
3293
3536
|
/**
|
|
@@ -3313,7 +3556,7 @@ function buildBatchEncoders(input: any[], options: any) {
|
|
|
3313
3556
|
return String(v);
|
|
3314
3557
|
};
|
|
3315
3558
|
var toBytes = function(v: any, meta: any, column: number): Buffer {
|
|
3316
|
-
var b = Buffer.isBuffer(v) ? v :
|
|
3559
|
+
var b = Buffer.isBuffer(v) ? v : Xsql.encodeConnectionText(options, toText(v));
|
|
3317
3560
|
if (b.length > meta.length) {
|
|
3318
3561
|
throw new Error('Batch value for column ' + column + ' is ' + b.length +
|
|
3319
3562
|
' bytes but the column accepts at most ' + meta.length + ' (' + (meta.field || '?') + ')');
|
|
@@ -3451,6 +3694,24 @@ function buildBatchEncoders(input: any[], options: any) {
|
|
|
3451
3694
|
align(4); offset += 4;
|
|
3452
3695
|
break;
|
|
3453
3696
|
|
|
3697
|
+
case Const.SQL_BLOB:
|
|
3698
|
+
case Const.SQL_QUAD:
|
|
3699
|
+
// the value is a transaction blob quad id — placed by
|
|
3700
|
+
// executeBatch's uploadBlob pre-pass, or passed by the
|
|
3701
|
+
// caller directly. ISC_QUAD: two longs, align 4.
|
|
3702
|
+
encoders.push((function(col) {
|
|
3703
|
+
return function(msg: any, v: any) {
|
|
3704
|
+
if (!v || typeof v.high !== 'number' || typeof v.low !== 'number') {
|
|
3705
|
+
throw new Error('Batch value for BLOB column ' + col +
|
|
3706
|
+
' must be a Buffer, string, object, or blob quad id');
|
|
3707
|
+
}
|
|
3708
|
+
msg.addInt(v.high);
|
|
3709
|
+
msg.addInt(v.low);
|
|
3710
|
+
};
|
|
3711
|
+
})(column));
|
|
3712
|
+
align(4); offset += 8;
|
|
3713
|
+
break;
|
|
3714
|
+
|
|
3454
3715
|
default:
|
|
3455
3716
|
throw new Error('executeBatch does not support the type of parameter ' + column +
|
|
3456
3717
|
' yet (' + (meta.field || '?') + ', SQL type ' + meta.type + ')');
|
|
@@ -3508,12 +3769,16 @@ function CalcBlr(blr: BlrWriter, xsqlda: any[]) {
|
|
|
3508
3769
|
|
|
3509
3770
|
function fetch_blob_async_transaction(statement: Statement, id: Quad, column: string | number, row: number, meta?: Xsql.SQLVarBase, table?: string) {
|
|
3510
3771
|
const infoValue = { row, column, value: '', meta, table };
|
|
3772
|
+
// decode ONCE from the concatenated bytes: per-segment toString() both
|
|
3773
|
+
// ignored the connection charset (#301 — cyrillic mojibake) and could
|
|
3774
|
+
// split a multi-byte UTF-8 character across segment boundaries
|
|
3775
|
+
const chunks: Buffer[] = [];
|
|
3511
3776
|
|
|
3512
3777
|
return (transactionArg: any) => {
|
|
3513
3778
|
const cacheKey = `${id.high}:${id.low}`;
|
|
3514
3779
|
if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
|
|
3515
3780
|
const data = statement.connection._inlineBlobs.get(cacheKey);
|
|
3516
|
-
infoValue.value = data ?
|
|
3781
|
+
infoValue.value = data ? Xsql.decodeConnectionText(statement.connection.options, data) : '';
|
|
3517
3782
|
return Promise.resolve(infoValue);
|
|
3518
3783
|
}
|
|
3519
3784
|
|
|
@@ -3557,8 +3822,7 @@ function fetch_blob_async_transaction(statement: Statement, id: Quad, column: st
|
|
|
3557
3822
|
|
|
3558
3823
|
if (ret.buffer) {
|
|
3559
3824
|
const blr = new BlrReader(ret.buffer);
|
|
3560
|
-
|
|
3561
|
-
infoValue.value += data.toString(Const.DEFAULT_ENCODING);
|
|
3825
|
+
chunks.push(blr.readSegment());
|
|
3562
3826
|
}
|
|
3563
3827
|
|
|
3564
3828
|
if (ret.handle !== 2) {
|
|
@@ -3566,6 +3830,8 @@ function fetch_blob_async_transaction(statement: Statement, id: Quad, column: st
|
|
|
3566
3830
|
return;
|
|
3567
3831
|
}
|
|
3568
3832
|
|
|
3833
|
+
infoValue.value = Xsql.decodeConnectionText(
|
|
3834
|
+
statement.connection.options, Buffer.concat(chunks));
|
|
3569
3835
|
statement.connection.closeBlob(blob);
|
|
3570
3836
|
if (singleTransaction) {
|
|
3571
3837
|
transaction.commit((err: any) => {
|
package/src/wire/database.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import Events from 'events';
|
|
2
2
|
import { doError, fromCallback, type Callback, type SimpleCallback } from '../callback';
|
|
3
|
-
import { escape } from '../utils';
|
|
3
|
+
import { batchResultToError, escape } from '../utils';
|
|
4
4
|
import Const from './const';
|
|
5
5
|
import { makeSqlTag, type SqlTag } from '../sql-template';
|
|
6
6
|
import { computeColumnKeys, nestCell, resolveKeyTransform, resolveNestTables } from './xsqlvar';
|
|
7
7
|
import EventConnection from './eventConnection';
|
|
8
8
|
import FbEventManager from './fbEventManager';
|
|
9
9
|
import makeQueryStream from './query-stream';
|
|
10
|
+
import makeBatchStream from './batch-stream';
|
|
10
11
|
import type Connection from './connection';
|
|
11
12
|
import type Transaction from './transaction';
|
|
12
13
|
import type Statement from './statement';
|
|
@@ -300,12 +301,7 @@ class Database extends Events.EventEmitter {
|
|
|
300
301
|
|
|
301
302
|
if (!result.success) {
|
|
302
303
|
transaction.rollback(function() {
|
|
303
|
-
|
|
304
|
-
var batchError: any = first
|
|
305
|
-
? first.error
|
|
306
|
-
: new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
|
|
307
|
-
batchError.batchCompletion = result;
|
|
308
|
-
doError(batchError, callback);
|
|
304
|
+
doError(batchResultToError(result), callback);
|
|
309
305
|
});
|
|
310
306
|
return;
|
|
311
307
|
}
|
|
@@ -415,6 +411,18 @@ class Database extends Events.EventEmitter {
|
|
|
415
411
|
return makeQueryStream(this, query, params, options);
|
|
416
412
|
}
|
|
417
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Bulk-insert Writable (the COPY FROM analogue, Firebird 4.0+): write
|
|
416
|
+
* parameter-array rows, they are flushed in chunks through the batch
|
|
417
|
+
* API on one prepared statement. Runs its own transaction — committed
|
|
418
|
+
* on finish, rolled back on error/destroy (all-or-nothing for the
|
|
419
|
+
* whole stream). BLOB columns accept Buffers/strings. After 'finish',
|
|
420
|
+
* stream.recordCount / stream.affectedRows carry the totals.
|
|
421
|
+
*/
|
|
422
|
+
batchStream(query: string, options?: any) {
|
|
423
|
+
return makeBatchStream(this, query, options, true);
|
|
424
|
+
}
|
|
425
|
+
|
|
418
426
|
query(query: string, params?: QueryParams | Callback, callback?: any, options: InternalQueryOptions = {}): this {
|
|
419
427
|
if (params instanceof Function) {
|
|
420
428
|
options = callback || {};
|
package/src/wire/serialize.ts
CHANGED
|
@@ -346,6 +346,17 @@ export class XdrWriter {
|
|
|
346
346
|
this.pos += alen;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
/** addString for pre-encoded bytes (codepage connection charsets). */
|
|
350
|
+
addStringBuffer(b: Buffer): void {
|
|
351
|
+
var alen = align(b.length);
|
|
352
|
+
this.ensure(alen + 4);
|
|
353
|
+
this.buffer.writeInt32BE(b.length, this.pos);
|
|
354
|
+
this.pos += 4;
|
|
355
|
+
b.copy(this.buffer, this.pos);
|
|
356
|
+
this.buffer.fill(0, this.pos + b.length, this.pos + alen);
|
|
357
|
+
this.pos += alen;
|
|
358
|
+
}
|
|
359
|
+
|
|
349
360
|
addText(s: string, encoding: BufferEncoding): void {
|
|
350
361
|
var len = Buffer.byteLength(s, encoding);
|
|
351
362
|
var alen = align(len);
|
package/src/wire/socket.ts
CHANGED
|
@@ -163,9 +163,15 @@ class Socket {
|
|
|
163
163
|
* Compress and/or encrypt data before sending to socket.
|
|
164
164
|
*/
|
|
165
165
|
write(data: Buffer | Uint8Array, defer = false): void {
|
|
166
|
+
// Callers pass views of the connection's shared _msg buffer, and both
|
|
167
|
+
// net.Socket (when it cannot flush immediately) and zlib keep a
|
|
168
|
+
// REFERENCE to the chunk — a later sender rebuilding _msg would then
|
|
169
|
+
// corrupt this queued packet (intermittent, load-dependent). Own the
|
|
170
|
+
// bytes at the send boundary, once.
|
|
171
|
+
data = Buffer.from(data);
|
|
166
172
|
if (process.env.FIREBIRD_DEBUG) {
|
|
167
173
|
console.log('[fb-debug] socket.write: length=%d bytes=%s encrypt=%s defer=%s',
|
|
168
|
-
data.length,
|
|
174
|
+
data.length, (data as Buffer).toString('hex'), this.encrypt, defer);
|
|
169
175
|
}
|
|
170
176
|
if (defer) {
|
|
171
177
|
// Accumulate deferred packets instead of overwriting. Multiple
|
|
@@ -174,8 +180,8 @@ class Socket {
|
|
|
174
180
|
// overwriting the buffer silently drops packets and desynchronises
|
|
175
181
|
// the request/response queue, causing the connection to hang.
|
|
176
182
|
this.buffer = this.buffer
|
|
177
|
-
? Buffer.concat([this.buffer,
|
|
178
|
-
:
|
|
183
|
+
? Buffer.concat([this.buffer, data])
|
|
184
|
+
: (data as Buffer);
|
|
179
185
|
return;
|
|
180
186
|
}
|
|
181
187
|
|