node-firebird 2.11.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 +287 -9
- package/lib/pool.d.ts +14 -1
- package/lib/pool.js +59 -16
- package/lib/sql-template.d.ts +81 -0
- package/lib/sql-template.js +162 -0
- package/lib/types.d.ts +152 -5
- package/lib/uri.js +53 -2
- 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 +44 -4
- package/lib/wire/connection.js +344 -80
- package/lib/wire/const.d.ts +5 -0
- package/lib/wire/const.js +12 -0
- package/lib/wire/database.d.ts +18 -0
- package/lib/wire/database.js +40 -13
- package/lib/wire/serialize.d.ts +2 -0
- package/lib/wire/serialize.js +14 -0
- package/lib/wire/socket.js +9 -3
- package/lib/wire/transaction.d.ts +33 -0
- package/lib/wire/transaction.js +156 -13
- package/lib/wire/xsqlvar.d.ts +118 -2
- package/lib/wire/xsqlvar.js +271 -34
- package/package.json +19 -1
- package/src/pool.ts +57 -14
- package/src/sql-template.ts +196 -0
- package/src/types.ts +153 -6
- package/src/uri.ts +54 -2
- 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 +374 -86
- package/src/wire/const.ts +13 -0
- package/src/wire/database.ts +46 -15
- package/src/wire/serialize.ts +16 -1
- package/src/wire/socket.ts +9 -3
- package/src/wire/transaction.ts +166 -19
- package/src/wire/xsqlvar.ts +298 -34
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';
|
|
@@ -75,31 +76,23 @@ function statementCacheLimit(options: InternalOptions): number {
|
|
|
75
76
|
return 0;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
[Const.SQL_TIMESTAMP_TZ_EX]: 'TIMESTAMP_TZ_EX',
|
|
96
|
-
[Const.SQL_TIME_TZ]: 'TIME_TZ',
|
|
97
|
-
[Const.SQL_TIME_TZ_EX]: 'TIME_TZ_EX',
|
|
98
|
-
[Const.SQL_DEC16]: 'DEC16',
|
|
99
|
-
[Const.SQL_DEC34]: 'DEC34',
|
|
100
|
-
[Const.SQL_BOOLEAN]: 'BOOLEAN',
|
|
101
|
-
[Const.SQL_NULL]: 'NULL',
|
|
102
|
-
};
|
|
79
|
+
// SQL type-code names live in xsqlvar.ts alongside the descriptors
|
|
80
|
+
const SQL_TYPE_NAMES = Xsql.SQL_TYPE_NAMES;
|
|
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
|
+
}
|
|
103
96
|
|
|
104
97
|
/**
|
|
105
98
|
* Run the user's typeCast hook (options.typeCast) for one column value.
|
|
@@ -114,16 +107,7 @@ function applyTypeCast(options: InternalOptions, meta: Partial<Xsql.SQLVarBase>,
|
|
|
114
107
|
if (typeof typeCast !== 'function') {
|
|
115
108
|
return defaultValue;
|
|
116
109
|
}
|
|
117
|
-
const column =
|
|
118
|
-
type: meta.type!,
|
|
119
|
-
typeName: SQL_TYPE_NAMES[meta.type!] || 'UNKNOWN',
|
|
120
|
-
subType: meta.subType,
|
|
121
|
-
scale: meta.scale,
|
|
122
|
-
length: meta.length,
|
|
123
|
-
field: meta.field,
|
|
124
|
-
relation: meta.relation,
|
|
125
|
-
alias: meta.alias,
|
|
126
|
-
};
|
|
110
|
+
const column = Xsql.describeField(meta);
|
|
127
111
|
// A hook exception must never escape into the row-decode loop: there it
|
|
128
112
|
// would be mistaken for an incomplete packet and desync the response
|
|
129
113
|
// queue (the same failure mode as issue #341). Fall back to the default
|
|
@@ -295,6 +279,25 @@ class Connection {
|
|
|
295
279
|
}
|
|
296
280
|
|
|
297
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
|
+
|
|
298
301
|
_bind_events(host: string, port: number, callback: SimpleCallback | undefined) {
|
|
299
302
|
|
|
300
303
|
var self = this;
|
|
@@ -319,6 +322,15 @@ class Connection {
|
|
|
319
322
|
|
|
320
323
|
self._rejectPending(lostError);
|
|
321
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
|
+
|
|
322
334
|
self._retry_connection_id = setTimeout(function() {
|
|
323
335
|
self._socket.removeAllListeners();
|
|
324
336
|
// transiently null while the replacement Connection is built
|
|
@@ -329,14 +341,14 @@ class Connection {
|
|
|
329
341
|
ctx.connect(self.options, function(err: any) {
|
|
330
342
|
|
|
331
343
|
if (err) {
|
|
332
|
-
self.
|
|
344
|
+
self._emitError(err);
|
|
333
345
|
return;
|
|
334
346
|
}
|
|
335
347
|
|
|
336
348
|
ctx.attach(self.options, function(err: any) {
|
|
337
349
|
|
|
338
350
|
if (err) {
|
|
339
|
-
self.
|
|
351
|
+
self._emitError(err);
|
|
340
352
|
return;
|
|
341
353
|
}
|
|
342
354
|
|
|
@@ -353,11 +365,13 @@ class Connection {
|
|
|
353
365
|
});
|
|
354
366
|
|
|
355
367
|
self._socket.on('error', function(e: any) {
|
|
356
|
-
|
|
368
|
+
|
|
357
369
|
self.error = e;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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);
|
|
361
375
|
|
|
362
376
|
if (callback)
|
|
363
377
|
callback(e);
|
|
@@ -456,13 +470,38 @@ class Connection {
|
|
|
456
470
|
self._queue.length, self._pending.length, xdr.pos);
|
|
457
471
|
}
|
|
458
472
|
|
|
473
|
+
// Surface isc_arg_warning entries (parsed since 2.10.0 but
|
|
474
|
+
// dropped here): resolve their message text and emit them on
|
|
475
|
+
// the Database on the next tick, so a listener registered
|
|
476
|
+
// inside this very response's callback (e.g. right after
|
|
477
|
+
// attach) still receives them.
|
|
478
|
+
if (obj && obj.warnings && obj.warnings.length && self.db && typeof self.db.emit === 'function') {
|
|
479
|
+
const warnings = obj.warnings;
|
|
480
|
+
for (const w of warnings) {
|
|
481
|
+
if (w.message === undefined) {
|
|
482
|
+
w.message = lookupMessages([w]);
|
|
483
|
+
if (!w.message || w.message === 'Unknow error') {
|
|
484
|
+
// codes newer than the bundled firebird.msg:
|
|
485
|
+
// still say something actionable
|
|
486
|
+
w.message = 'Firebird warning ' + w.gdscode +
|
|
487
|
+
(w.params && w.params.length ? ': ' + w.params.join(', ') : '');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
process.nextTick(function () {
|
|
492
|
+
for (const w of warnings) {
|
|
493
|
+
self.db.emit('warning', w);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
459
498
|
if (obj && obj.status) {
|
|
460
499
|
obj.message = lookupMessages(obj.status);
|
|
461
500
|
doCallback(obj, cb);
|
|
462
501
|
} else {
|
|
463
502
|
doCallback(obj, cb);
|
|
464
503
|
}
|
|
465
|
-
|
|
504
|
+
|
|
466
505
|
});
|
|
467
506
|
|
|
468
507
|
if (xdr.pos === 0) {
|
|
@@ -774,7 +813,9 @@ class Connection {
|
|
|
774
813
|
blr.pos = 0;
|
|
775
814
|
|
|
776
815
|
blr.addByte(Const.isc_dpb_version1);
|
|
777
|
-
|
|
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);
|
|
778
819
|
|
|
779
820
|
// For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
|
|
780
821
|
if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION13) {
|
|
@@ -902,8 +943,15 @@ class Connection {
|
|
|
902
943
|
|
|
903
944
|
blr.pos = 0;
|
|
904
945
|
blr.addByte(Const.isc_dpb_version1);
|
|
905
|
-
|
|
906
|
-
|
|
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);
|
|
907
955
|
|
|
908
956
|
// For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
|
|
909
957
|
if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION13) {
|
|
@@ -1011,7 +1059,9 @@ class Connection {
|
|
|
1011
1059
|
|
|
1012
1060
|
throwClosed(callback: ((err: Error, ...args: any[]) => void) | undefined) {
|
|
1013
1061
|
var err = new Error('Connection is closed.');
|
|
1014
|
-
|
|
1062
|
+
// listeners only: the caller receives the error through its own
|
|
1063
|
+
// callback below either way
|
|
1064
|
+
this._emitError(err);
|
|
1015
1065
|
if (callback)
|
|
1016
1066
|
callback(err);
|
|
1017
1067
|
return this;
|
|
@@ -1261,7 +1311,9 @@ class Connection {
|
|
|
1261
1311
|
msg.addInt(transaction.handle);
|
|
1262
1312
|
msg.addInt(0xFFFF);
|
|
1263
1313
|
msg.addInt(3); // dialect = 3
|
|
1264
|
-
|
|
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));
|
|
1265
1317
|
msg.addBlr(blr);
|
|
1266
1318
|
msg.addInt(65535); // buffer_length
|
|
1267
1319
|
if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION20) {
|
|
@@ -1323,7 +1375,9 @@ class Connection {
|
|
|
1323
1375
|
msg.addInt(transaction.handle);
|
|
1324
1376
|
msg.addInt(statement.handle);
|
|
1325
1377
|
msg.addInt(3); // dialect = 3
|
|
1326
|
-
|
|
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));
|
|
1327
1381
|
msg.addBlr(blr);
|
|
1328
1382
|
msg.addInt(65535); // buffer_length
|
|
1329
1383
|
if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION20) {
|
|
@@ -1355,10 +1409,13 @@ class Connection {
|
|
|
1355
1409
|
* gets an in-order response (op_batch_cs for exec), so the regular
|
|
1356
1410
|
* response queue keeps everything in sync.
|
|
1357
1411
|
*
|
|
1358
|
-
* rows: array of parameter arrays, one per record. BLOB
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
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 }.
|
|
1362
1419
|
*/
|
|
1363
1420
|
executeBatch(transaction: Transaction, statement: Statement, rows: QueryParams[], callback: BatchCb | undefined, options?: BatchOptions) {
|
|
1364
1421
|
options = options || {};
|
|
@@ -1396,6 +1453,90 @@ class Connection {
|
|
|
1396
1453
|
}
|
|
1397
1454
|
}
|
|
1398
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;
|
|
1399
1540
|
var built;
|
|
1400
1541
|
try {
|
|
1401
1542
|
built = buildBatchEncoders(input, Object.assign({}, this.options, options));
|
|
@@ -1591,9 +1732,9 @@ class Connection {
|
|
|
1591
1732
|
if (Buffer.isBuffer(value))
|
|
1592
1733
|
b = value;
|
|
1593
1734
|
else if (typeof(value) === 'string')
|
|
1594
|
-
b =
|
|
1735
|
+
b = Xsql.encodeConnectionText(self.options, value);
|
|
1595
1736
|
else if (!isStream)
|
|
1596
|
-
b =
|
|
1737
|
+
b = Xsql.encodeConnectionText(self.options, JSON.stringify(value));
|
|
1597
1738
|
|
|
1598
1739
|
// Use configured transfer size or default to 1024
|
|
1599
1740
|
var chunkSize = self.options.blobChunkSize || 1024;
|
|
@@ -1744,14 +1885,18 @@ class Connection {
|
|
|
1744
1885
|
ret[i] = new Xsql.SQLParamDouble(value);
|
|
1745
1886
|
break;
|
|
1746
1887
|
case 'string':
|
|
1747
|
-
ret[i] =
|
|
1888
|
+
ret[i] = textParam(self.options, value);
|
|
1748
1889
|
break;
|
|
1749
1890
|
case 'boolean':
|
|
1750
|
-
|
|
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);
|
|
1751
1896
|
break;
|
|
1752
1897
|
default:
|
|
1753
1898
|
//throw new Error('Unexpected parametter: ' + JSON.stringify(params) + ' - ' + JSON.stringify(input));
|
|
1754
|
-
ret[i] =
|
|
1899
|
+
ret[i] = textParam(self.options, value.toString());
|
|
1755
1900
|
break;
|
|
1756
1901
|
}
|
|
1757
1902
|
}
|
|
@@ -1984,6 +2129,65 @@ class Connection {
|
|
|
1984
2129
|
}
|
|
1985
2130
|
|
|
1986
2131
|
|
|
2132
|
+
/**
|
|
2133
|
+
* Query runtime information about a prepared statement via op_info_sql
|
|
2134
|
+
* (e.g. Const.RECORDS_INFO for the per-verb DML row counts). The
|
|
2135
|
+
* response is a plain op_response whose buffer holds the info clusters.
|
|
2136
|
+
*/
|
|
2137
|
+
statementInfo(statement: Statement, items: number[], callback?: QueueCallback) {
|
|
2138
|
+
if (this._isClosed)
|
|
2139
|
+
return this.throwClosed(callback);
|
|
2140
|
+
|
|
2141
|
+
this._pending.push('statementInfo');
|
|
2142
|
+
|
|
2143
|
+
var msg = this._msg;
|
|
2144
|
+
var blr = this._blr;
|
|
2145
|
+
msg.pos = 0;
|
|
2146
|
+
blr.pos = 0;
|
|
2147
|
+
|
|
2148
|
+
blr.addBytes(items);
|
|
2149
|
+
|
|
2150
|
+
msg.addInt(Const.op_info_sql);
|
|
2151
|
+
msg.addInt(statement.handle);
|
|
2152
|
+
msg.addInt(0); // incarnation
|
|
2153
|
+
msg.addBlr(blr);
|
|
2154
|
+
msg.addInt(65535); // buffer_length
|
|
2155
|
+
|
|
2156
|
+
this._queueEvent(callback);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
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
|
+
|
|
1987
2191
|
fetchAll(statement: Statement, transaction: Transaction, callback: Callback<any[]>) {
|
|
1988
2192
|
const self = this;
|
|
1989
2193
|
const custom = statement.options || {};
|
|
@@ -1997,29 +2201,10 @@ class Connection {
|
|
|
1997
2201
|
}
|
|
1998
2202
|
|
|
1999
2203
|
if (ret && ret.data && ret.data.length) {
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
const arrBlobFns = ret.arrBlob || [];
|
|
2005
|
-
const readBlobsSequentially = (index: any, results: any) => {
|
|
2006
|
-
if (index >= arrBlobFns.length) {
|
|
2007
|
-
return Promise.resolve(results);
|
|
2008
|
-
}
|
|
2009
|
-
return arrBlobFns[index](transaction).then((v: any) => {
|
|
2010
|
-
results.push(v);
|
|
2011
|
-
return readBlobsSequentially(index + 1, results);
|
|
2012
|
-
});
|
|
2013
|
-
};
|
|
2014
|
-
|
|
2015
|
-
readBlobsSequentially(0, []).then((arrBlob: any) => {
|
|
2016
|
-
for (let i = 0; i < arrBlob.length; i++) {
|
|
2017
|
-
const blob = arrBlob[i];
|
|
2018
|
-
// nestTables === true rows: the value lives in the
|
|
2019
|
-
// per-table sub-object, not on the row itself
|
|
2020
|
-
Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(
|
|
2021
|
-
statement.connection.options, blob.meta || {},
|
|
2022
|
-
parseValueIfJson(blob.value, statement.connection.options));
|
|
2204
|
+
self.resolveTextBlobs(transaction, ret, (blobErr?: any) => {
|
|
2205
|
+
if (blobErr) {
|
|
2206
|
+
callback(blobErr);
|
|
2207
|
+
return;
|
|
2023
2208
|
}
|
|
2024
2209
|
|
|
2025
2210
|
doSynchronousLoop(ret.data, (row, _i, next) => {
|
|
@@ -2041,7 +2226,7 @@ class Connection {
|
|
|
2041
2226
|
self.fetch(statement, transaction, Const.DEFAULT_FETCHSIZE, loop);
|
|
2042
2227
|
}
|
|
2043
2228
|
});
|
|
2044
|
-
})
|
|
2229
|
+
});
|
|
2045
2230
|
return;
|
|
2046
2231
|
}
|
|
2047
2232
|
|
|
@@ -2113,6 +2298,48 @@ class Connection {
|
|
|
2113
2298
|
}
|
|
2114
2299
|
|
|
2115
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
|
+
|
|
2116
2343
|
svcattach(options: InternalOptions, callback?: Callback<ServiceManager>, svc?: ServiceManager) {
|
|
2117
2344
|
this._lowercase_keys = options.lowercase_keys || Const.DEFAULT_LOWERCASE_KEYS;
|
|
2118
2345
|
var database = options.database || options.filename;
|
|
@@ -2494,7 +2721,8 @@ function decodeResponse(data: XdrReader, callback: QueueCallback | undefined, cn
|
|
|
2494
2721
|
|
|
2495
2722
|
if (custom.asObject && !data.fcols) {
|
|
2496
2723
|
const nest = Xsql.resolveNestTables(custom, cnx.options);
|
|
2497
|
-
const
|
|
2724
|
+
const transform = Xsql.resolveKeyTransform(custom, cnx.options);
|
|
2725
|
+
const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys, transform);
|
|
2498
2726
|
data.fcols = columnKeys.map((k) => k.key);
|
|
2499
2727
|
if (nest === true) {
|
|
2500
2728
|
// computeColumnKeys always sets table when nesting
|
|
@@ -3266,6 +3494,43 @@ function describe(buff: Buffer, statement: Statement) {
|
|
|
3266
3494
|
}
|
|
3267
3495
|
unpackCharSetCollation(statement.input);
|
|
3268
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
|
+
}
|
|
3269
3534
|
}
|
|
3270
3535
|
|
|
3271
3536
|
/**
|
|
@@ -3291,7 +3556,7 @@ function buildBatchEncoders(input: any[], options: any) {
|
|
|
3291
3556
|
return String(v);
|
|
3292
3557
|
};
|
|
3293
3558
|
var toBytes = function(v: any, meta: any, column: number): Buffer {
|
|
3294
|
-
var b = Buffer.isBuffer(v) ? v :
|
|
3559
|
+
var b = Buffer.isBuffer(v) ? v : Xsql.encodeConnectionText(options, toText(v));
|
|
3295
3560
|
if (b.length > meta.length) {
|
|
3296
3561
|
throw new Error('Batch value for column ' + column + ' is ' + b.length +
|
|
3297
3562
|
' bytes but the column accepts at most ' + meta.length + ' (' + (meta.field || '?') + ')');
|
|
@@ -3429,6 +3694,24 @@ function buildBatchEncoders(input: any[], options: any) {
|
|
|
3429
3694
|
align(4); offset += 4;
|
|
3430
3695
|
break;
|
|
3431
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
|
+
|
|
3432
3715
|
default:
|
|
3433
3716
|
throw new Error('executeBatch does not support the type of parameter ' + column +
|
|
3434
3717
|
' yet (' + (meta.field || '?') + ', SQL type ' + meta.type + ')');
|
|
@@ -3486,12 +3769,16 @@ function CalcBlr(blr: BlrWriter, xsqlda: any[]) {
|
|
|
3486
3769
|
|
|
3487
3770
|
function fetch_blob_async_transaction(statement: Statement, id: Quad, column: string | number, row: number, meta?: Xsql.SQLVarBase, table?: string) {
|
|
3488
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[] = [];
|
|
3489
3776
|
|
|
3490
3777
|
return (transactionArg: any) => {
|
|
3491
3778
|
const cacheKey = `${id.high}:${id.low}`;
|
|
3492
3779
|
if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
|
|
3493
3780
|
const data = statement.connection._inlineBlobs.get(cacheKey);
|
|
3494
|
-
infoValue.value = data ?
|
|
3781
|
+
infoValue.value = data ? Xsql.decodeConnectionText(statement.connection.options, data) : '';
|
|
3495
3782
|
return Promise.resolve(infoValue);
|
|
3496
3783
|
}
|
|
3497
3784
|
|
|
@@ -3535,8 +3822,7 @@ function fetch_blob_async_transaction(statement: Statement, id: Quad, column: st
|
|
|
3535
3822
|
|
|
3536
3823
|
if (ret.buffer) {
|
|
3537
3824
|
const blr = new BlrReader(ret.buffer);
|
|
3538
|
-
|
|
3539
|
-
infoValue.value += data.toString(Const.DEFAULT_ENCODING);
|
|
3825
|
+
chunks.push(blr.readSegment());
|
|
3540
3826
|
}
|
|
3541
3827
|
|
|
3542
3828
|
if (ret.handle !== 2) {
|
|
@@ -3544,6 +3830,8 @@ function fetch_blob_async_transaction(statement: Statement, id: Quad, column: st
|
|
|
3544
3830
|
return;
|
|
3545
3831
|
}
|
|
3546
3832
|
|
|
3833
|
+
infoValue.value = Xsql.decodeConnectionText(
|
|
3834
|
+
statement.connection.options, Buffer.concat(chunks));
|
|
3547
3835
|
statement.connection.closeBlob(blob);
|
|
3548
3836
|
if (singleTransaction) {
|
|
3549
3837
|
transaction.commit((err: any) => {
|