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/lib/wire/connection.js
CHANGED
|
@@ -44,6 +44,7 @@ const srp = __importStar(require("../srp"));
|
|
|
44
44
|
const crypt = __importStar(require("../unix-crypt"));
|
|
45
45
|
const const_1 = __importDefault(require("./const"));
|
|
46
46
|
const Xsql = __importStar(require("./xsqlvar"));
|
|
47
|
+
const codepages_1 = require("./codepages");
|
|
47
48
|
const service_1 = __importDefault(require("./service"));
|
|
48
49
|
const database_1 = __importDefault(require("./database"));
|
|
49
50
|
const statement_1 = __importDefault(require("./statement"));
|
|
@@ -107,31 +108,22 @@ function statementCacheLimit(options) {
|
|
|
107
108
|
}
|
|
108
109
|
return 0;
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
[const_1.default.SQL_TIMESTAMP_TZ]: 'TIMESTAMP_TZ',
|
|
127
|
-
[const_1.default.SQL_TIMESTAMP_TZ_EX]: 'TIMESTAMP_TZ_EX',
|
|
128
|
-
[const_1.default.SQL_TIME_TZ]: 'TIME_TZ',
|
|
129
|
-
[const_1.default.SQL_TIME_TZ_EX]: 'TIME_TZ_EX',
|
|
130
|
-
[const_1.default.SQL_DEC16]: 'DEC16',
|
|
131
|
-
[const_1.default.SQL_DEC34]: 'DEC34',
|
|
132
|
-
[const_1.default.SQL_BOOLEAN]: 'BOOLEAN',
|
|
133
|
-
[const_1.default.SQL_NULL]: 'NULL',
|
|
134
|
-
};
|
|
111
|
+
// SQL type-code names live in xsqlvar.ts alongside the descriptors
|
|
112
|
+
const SQL_TYPE_NAMES = Xsql.SQL_TYPE_NAMES;
|
|
113
|
+
/**
|
|
114
|
+
* Build the wire parameter for a text value in the CONNECTION charset:
|
|
115
|
+
* plain SQLParamString for UTF-8 connections, pre-encoded bytes for
|
|
116
|
+
* everything else — SQLParamString hardwires utf8, which corrupted
|
|
117
|
+
* writes on latin1/codepage connections (issues #319/#301).
|
|
118
|
+
*/
|
|
119
|
+
function textParam(options, value) {
|
|
120
|
+
const codec = Xsql.resolveTextCodec(options);
|
|
121
|
+
const enc = Xsql.resolveTextEncoding(options);
|
|
122
|
+
if (!codec && enc === 'utf8') {
|
|
123
|
+
return new Xsql.SQLParamString(value);
|
|
124
|
+
}
|
|
125
|
+
return new Xsql.SQLParamBuffer(Xsql.encodeConnectionText(options, value));
|
|
126
|
+
}
|
|
135
127
|
/**
|
|
136
128
|
* Run the user's typeCast hook (options.typeCast) for one column value.
|
|
137
129
|
* The hook receives the column metadata and a next() returning the value
|
|
@@ -145,16 +137,7 @@ function applyTypeCast(options, meta, defaultValue) {
|
|
|
145
137
|
if (typeof typeCast !== 'function') {
|
|
146
138
|
return defaultValue;
|
|
147
139
|
}
|
|
148
|
-
const column =
|
|
149
|
-
type: meta.type,
|
|
150
|
-
typeName: SQL_TYPE_NAMES[meta.type] || 'UNKNOWN',
|
|
151
|
-
subType: meta.subType,
|
|
152
|
-
scale: meta.scale,
|
|
153
|
-
length: meta.length,
|
|
154
|
-
field: meta.field,
|
|
155
|
-
relation: meta.relation,
|
|
156
|
-
alias: meta.alias,
|
|
157
|
-
};
|
|
140
|
+
const column = Xsql.describeField(meta);
|
|
158
141
|
// A hook exception must never escape into the row-decode loop: there it
|
|
159
142
|
// would be mistaken for an incomplete packet and desync the response
|
|
160
143
|
// queue (the same failure mode as issue #341). Fall back to the default
|
|
@@ -272,6 +255,24 @@ class Connection {
|
|
|
272
255
|
(0, callback_1.doError)(err, queue[i]);
|
|
273
256
|
}
|
|
274
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Deliver a connection-level error to 'error' listeners — and ONLY to
|
|
260
|
+
* listeners. Emitting an unlistened 'error' makes Node throw the error
|
|
261
|
+
* object as an uncaught exception; for errors that originate in
|
|
262
|
+
* background contexts (the reconnect timer, socket-level failures whose
|
|
263
|
+
* operations are separately rejected via _rejectPending) that crashes
|
|
264
|
+
* the process — or, under a test runner, fails whatever unrelated test
|
|
265
|
+
* happens to be running. The failing operations themselves always
|
|
266
|
+
* still receive their error through their own callbacks.
|
|
267
|
+
*/
|
|
268
|
+
_emitError(err) {
|
|
269
|
+
if (this.db && typeof this.db.listenerCount === 'function' && this.db.listenerCount('error') > 0) {
|
|
270
|
+
this.db.emit('error', err);
|
|
271
|
+
}
|
|
272
|
+
else if (process.env.FIREBIRD_DEBUG) {
|
|
273
|
+
console.warn('[fb-debug] connection error (no error listener):', err && err.message);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
275
276
|
_bind_events(host, port, callback) {
|
|
276
277
|
var self = this;
|
|
277
278
|
self._socket.on('close', function () {
|
|
@@ -288,6 +289,14 @@ class Connection {
|
|
|
288
289
|
return;
|
|
289
290
|
}
|
|
290
291
|
self._rejectPending(lostError);
|
|
292
|
+
// Pooled connections do not self-reconnect: the pool is the
|
|
293
|
+
// recovery authority (dead-connection check on checkout + the
|
|
294
|
+
// reaper), and a background reconnect here would produce a
|
|
295
|
+
// zombie attachment the pool no longer tracks — whose own
|
|
296
|
+
// failures then surface as uncatchable async errors.
|
|
297
|
+
if (self.options && self.options.isPool) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
291
300
|
self._retry_connection_id = setTimeout(function () {
|
|
292
301
|
self._socket.removeAllListeners();
|
|
293
302
|
// transiently null while the replacement Connection is built
|
|
@@ -296,12 +305,12 @@ class Connection {
|
|
|
296
305
|
var ctx = new Connection(host, port, function (err) {
|
|
297
306
|
ctx.connect(self.options, function (err) {
|
|
298
307
|
if (err) {
|
|
299
|
-
self.
|
|
308
|
+
self._emitError(err);
|
|
300
309
|
return;
|
|
301
310
|
}
|
|
302
311
|
ctx.attach(self.options, function (err) {
|
|
303
312
|
if (err) {
|
|
304
|
-
self.
|
|
313
|
+
self._emitError(err);
|
|
305
314
|
return;
|
|
306
315
|
}
|
|
307
316
|
self.db.emit('reconnect');
|
|
@@ -313,8 +322,10 @@ class Connection {
|
|
|
313
322
|
});
|
|
314
323
|
self._socket.on('error', function (e) {
|
|
315
324
|
self.error = e;
|
|
316
|
-
|
|
317
|
-
|
|
325
|
+
// listeners only (_emitError): the affected operations get their
|
|
326
|
+
// errors via the close handler's _rejectPending — an unlistened
|
|
327
|
+
// socket error must not become an uncaught exception
|
|
328
|
+
self._emitError(e);
|
|
318
329
|
if (callback)
|
|
319
330
|
callback(e);
|
|
320
331
|
});
|
|
@@ -396,6 +407,30 @@ class Connection {
|
|
|
396
407
|
if (process.env.FIREBIRD_DEBUG) {
|
|
397
408
|
console.log('[fb-debug] response dispatched: queue remaining=%d pending remaining=%d xdr.pos=%d', self._queue.length, self._pending.length, xdr.pos);
|
|
398
409
|
}
|
|
410
|
+
// Surface isc_arg_warning entries (parsed since 2.10.0 but
|
|
411
|
+
// dropped here): resolve their message text and emit them on
|
|
412
|
+
// the Database on the next tick, so a listener registered
|
|
413
|
+
// inside this very response's callback (e.g. right after
|
|
414
|
+
// attach) still receives them.
|
|
415
|
+
if (obj && obj.warnings && obj.warnings.length && self.db && typeof self.db.emit === 'function') {
|
|
416
|
+
const warnings = obj.warnings;
|
|
417
|
+
for (const w of warnings) {
|
|
418
|
+
if (w.message === undefined) {
|
|
419
|
+
w.message = (0, utils_1.lookupMessages)([w]);
|
|
420
|
+
if (!w.message || w.message === 'Unknow error') {
|
|
421
|
+
// codes newer than the bundled firebird.msg:
|
|
422
|
+
// still say something actionable
|
|
423
|
+
w.message = 'Firebird warning ' + w.gdscode +
|
|
424
|
+
(w.params && w.params.length ? ': ' + w.params.join(', ') : '');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
process.nextTick(function () {
|
|
429
|
+
for (const w of warnings) {
|
|
430
|
+
self.db.emit('warning', w);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
399
434
|
if (obj && obj.status) {
|
|
400
435
|
obj.message = (0, utils_1.lookupMessages)(obj.status);
|
|
401
436
|
(0, callback_1.doCallback)(obj, cb);
|
|
@@ -656,7 +691,9 @@ class Connection {
|
|
|
656
691
|
msg.pos = 0;
|
|
657
692
|
blr.pos = 0;
|
|
658
693
|
blr.addByte(const_1.default.isc_dpb_version1);
|
|
659
|
-
|
|
694
|
+
// charset names are sent verbatim — normalize case so encoding:
|
|
695
|
+
// 'utf8'/'win1251' (e.g. from URI query params) is accepted
|
|
696
|
+
blr.addString(const_1.default.isc_dpb_lc_ctype, String(options.encoding || 'UTF8').toUpperCase(), const_1.default.DEFAULT_ENCODING);
|
|
660
697
|
// For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
|
|
661
698
|
if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION13) {
|
|
662
699
|
blr.addByte(const_1.default.isc_dpb_utf8_filename);
|
|
@@ -758,8 +795,13 @@ class Connection {
|
|
|
758
795
|
var blr = this._blr;
|
|
759
796
|
blr.pos = 0;
|
|
760
797
|
blr.addByte(const_1.default.isc_dpb_version1);
|
|
761
|
-
|
|
762
|
-
|
|
798
|
+
// honour options.encoding on the CREATE path too — hardcoding UTF8
|
|
799
|
+
// for lc_ctype made attachOrCreate silently ignore the requested
|
|
800
|
+
// connection charset (issue #319: 'Malformed string' for codepage
|
|
801
|
+
// text). The new database's DEFAULT charset follows the connection
|
|
802
|
+
// encoding unless options.defaultCharset overrides it.
|
|
803
|
+
blr.addString(const_1.default.isc_dpb_set_db_charset, String(options.defaultCharset || options.encoding || 'UTF8').toUpperCase(), const_1.default.DEFAULT_ENCODING);
|
|
804
|
+
blr.addString(const_1.default.isc_dpb_lc_ctype, String(options.encoding || 'UTF8').toUpperCase(), const_1.default.DEFAULT_ENCODING);
|
|
763
805
|
// For Firebird 3+ (protocol 13+), add UTF-8 filename flag to ensure all DPB strings are handled with UTF-8
|
|
764
806
|
if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION13) {
|
|
765
807
|
blr.addByte(const_1.default.isc_dpb_utf8_filename);
|
|
@@ -843,7 +885,9 @@ class Connection {
|
|
|
843
885
|
}
|
|
844
886
|
throwClosed(callback) {
|
|
845
887
|
var err = new Error('Connection is closed.');
|
|
846
|
-
|
|
888
|
+
// listeners only: the caller receives the error through its own
|
|
889
|
+
// callback below either way
|
|
890
|
+
this._emitError(err);
|
|
847
891
|
if (callback)
|
|
848
892
|
callback(err);
|
|
849
893
|
return this;
|
|
@@ -1035,7 +1079,9 @@ class Connection {
|
|
|
1035
1079
|
msg.addInt(transaction.handle);
|
|
1036
1080
|
msg.addInt(0xFFFF);
|
|
1037
1081
|
msg.addInt(3); // dialect = 3
|
|
1038
|
-
|
|
1082
|
+
// SQL text travels in the CONNECTION charset (codepage
|
|
1083
|
+
// connections encode via codec — greek/cyrillic literals included)
|
|
1084
|
+
msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
|
|
1039
1085
|
msg.addBlr(blr);
|
|
1040
1086
|
msg.addInt(65535); // buffer_length
|
|
1041
1087
|
if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
|
|
@@ -1083,7 +1129,9 @@ class Connection {
|
|
|
1083
1129
|
msg.addInt(transaction.handle);
|
|
1084
1130
|
msg.addInt(statement.handle);
|
|
1085
1131
|
msg.addInt(3); // dialect = 3
|
|
1086
|
-
|
|
1132
|
+
// SQL text travels in the CONNECTION charset (codepage
|
|
1133
|
+
// connections encode via codec — greek/cyrillic literals included)
|
|
1134
|
+
msg.addStringBuffer(Xsql.encodeConnectionText(this.options, query));
|
|
1087
1135
|
msg.addBlr(blr);
|
|
1088
1136
|
msg.addInt(65535); // buffer_length
|
|
1089
1137
|
if (this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION20) {
|
|
@@ -1108,10 +1156,13 @@ class Connection {
|
|
|
1108
1156
|
* gets an in-order response (op_batch_cs for exec), so the regular
|
|
1109
1157
|
* response queue keeps everything in sync.
|
|
1110
1158
|
*
|
|
1111
|
-
* rows: array of parameter arrays, one per record. BLOB
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1159
|
+
* rows: array of parameter arrays, one per record. BLOB columns accept
|
|
1160
|
+
* Buffers, strings, JSON-able objects or pre-created blob quad ids —
|
|
1161
|
+
* values are uploaded as transaction blobs first (all initiated
|
|
1162
|
+
* back-to-back so they pipeline) and the batch messages reference their
|
|
1163
|
+
* ids. ARRAY columns are not supported. The callback receives a
|
|
1164
|
+
* completion object: { recordCount, updateCounts, errors:
|
|
1165
|
+
* [{recordNumber, error}], errorRecordNumbers, success }.
|
|
1115
1166
|
*/
|
|
1116
1167
|
executeBatch(transaction, statement, rows, callback, options) {
|
|
1117
1168
|
options = options || {};
|
|
@@ -1143,6 +1194,82 @@ class Connection {
|
|
|
1143
1194
|
return;
|
|
1144
1195
|
}
|
|
1145
1196
|
}
|
|
1197
|
+
var self = this;
|
|
1198
|
+
// BLOB pre-pass: upload every Buffer/string blob value as a
|
|
1199
|
+
// transaction blob and replace it (in a cloned row) with the quad
|
|
1200
|
+
// id the batch message will carry. All uploads are initiated
|
|
1201
|
+
// back-to-back, so the create/segment/close ops pipeline on the
|
|
1202
|
+
// wire instead of paying a round trip per blob.
|
|
1203
|
+
var blobCols = [];
|
|
1204
|
+
for (var bj = 0; bj < input.length; bj++) {
|
|
1205
|
+
var bt = input[bj].type;
|
|
1206
|
+
if (bt === const_1.default.SQL_BLOB || bt === const_1.default.SQL_QUAD) {
|
|
1207
|
+
blobCols.push(bj);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// all-NULL blob columns are common — a large row set must not pay
|
|
1211
|
+
// for a full clone when there is nothing to upload or unwrap
|
|
1212
|
+
var needsBlobPass = false;
|
|
1213
|
+
if (blobCols.length) {
|
|
1214
|
+
outer: for (var ri = 0; ri < rows.length; ri++) {
|
|
1215
|
+
for (var ci = 0; ci < blobCols.length; ci++) {
|
|
1216
|
+
var bv = rows[ri][blobCols[ci]];
|
|
1217
|
+
if (bv !== null && bv !== undefined) {
|
|
1218
|
+
needsBlobPass = true;
|
|
1219
|
+
break outer;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (needsBlobPass) {
|
|
1225
|
+
var cloned = rows.map(function (r) { return r.slice(); });
|
|
1226
|
+
var pendingBlobs = 1; // sentinel so zero uploads still settle
|
|
1227
|
+
var blobFailure = null;
|
|
1228
|
+
var settleBlobs = function (err) {
|
|
1229
|
+
if (err && !blobFailure) {
|
|
1230
|
+
blobFailure = err;
|
|
1231
|
+
}
|
|
1232
|
+
if (--pendingBlobs) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
if (blobFailure) {
|
|
1236
|
+
(0, callback_1.doError)(blobFailure, callback);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
self._executeBatchEncoded(transaction, statement, cloned, callback, options);
|
|
1240
|
+
};
|
|
1241
|
+
cloned.forEach(function (row) {
|
|
1242
|
+
blobCols.forEach(function (j) {
|
|
1243
|
+
var v = row[j];
|
|
1244
|
+
if (v === null || v === undefined) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
// a pre-created blob id (SQLParamQuad wrapper) passes
|
|
1248
|
+
// through; plain {high, low} objects are deliberately NOT
|
|
1249
|
+
// treated as ids — they are legitimate JSON blob content
|
|
1250
|
+
// and would silently misroute to a bogus blob reference
|
|
1251
|
+
if (v instanceof Xsql.SQLParamQuad) {
|
|
1252
|
+
row[j] = v.value;
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
pendingBlobs++;
|
|
1256
|
+
self.uploadBlob(transaction, v, function (err, oid) {
|
|
1257
|
+
if (!err) {
|
|
1258
|
+
row[j] = oid;
|
|
1259
|
+
}
|
|
1260
|
+
settleBlobs(err);
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
settleBlobs();
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
this._executeBatchEncoded(transaction, statement, rows, callback, options);
|
|
1268
|
+
}
|
|
1269
|
+
/** Encode and send the batch packets (rows are fully materialized:
|
|
1270
|
+
* blob values already replaced by quad ids by executeBatch). */
|
|
1271
|
+
_executeBatchEncoded(transaction, statement, rows, callback, options) {
|
|
1272
|
+
var input = statement.input;
|
|
1146
1273
|
var built;
|
|
1147
1274
|
try {
|
|
1148
1275
|
built = buildBatchEncoders(input, Object.assign({}, this.options, options));
|
|
@@ -1310,9 +1437,9 @@ class Connection {
|
|
|
1310
1437
|
if (Buffer.isBuffer(value))
|
|
1311
1438
|
b = value;
|
|
1312
1439
|
else if (typeof (value) === 'string')
|
|
1313
|
-
b =
|
|
1440
|
+
b = Xsql.encodeConnectionText(self.options, value);
|
|
1314
1441
|
else if (!isStream)
|
|
1315
|
-
b =
|
|
1442
|
+
b = Xsql.encodeConnectionText(self.options, JSON.stringify(value));
|
|
1316
1443
|
// Use configured transfer size or default to 1024
|
|
1317
1444
|
var chunkSize = self.options.blobChunkSize || 1024;
|
|
1318
1445
|
if (Buffer.isBuffer(b)) {
|
|
@@ -1453,14 +1580,18 @@ class Connection {
|
|
|
1453
1580
|
ret[i] = new Xsql.SQLParamDouble(value);
|
|
1454
1581
|
break;
|
|
1455
1582
|
case 'string':
|
|
1456
|
-
ret[i] =
|
|
1583
|
+
ret[i] = textParam(self.options, value);
|
|
1457
1584
|
break;
|
|
1458
1585
|
case 'boolean':
|
|
1459
|
-
|
|
1586
|
+
// metadata-directed: BOOLEAN targets get a
|
|
1587
|
+
// real blr_bool (issue #122); smallint
|
|
1588
|
+
// targets keep the legacy 0/1 (BOOLEAN
|
|
1589
|
+
// does not convert to numbers)
|
|
1590
|
+
ret[i] = new Xsql.SQLParamBool(value, meta.type === const_1.default.SQL_BOOLEAN);
|
|
1460
1591
|
break;
|
|
1461
1592
|
default:
|
|
1462
1593
|
//throw new Error('Unexpected parametter: ' + JSON.stringify(params) + ' - ' + JSON.stringify(input));
|
|
1463
|
-
ret[i] =
|
|
1594
|
+
ret[i] = textParam(self.options, value.toString());
|
|
1464
1595
|
break;
|
|
1465
1596
|
}
|
|
1466
1597
|
}
|
|
@@ -1670,6 +1801,54 @@ class Connection {
|
|
|
1670
1801
|
callback.statement = statement;
|
|
1671
1802
|
this._queueEvent(callback);
|
|
1672
1803
|
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Query runtime information about a prepared statement via op_info_sql
|
|
1806
|
+
* (e.g. Const.RECORDS_INFO for the per-verb DML row counts). The
|
|
1807
|
+
* response is a plain op_response whose buffer holds the info clusters.
|
|
1808
|
+
*/
|
|
1809
|
+
statementInfo(statement, items, callback) {
|
|
1810
|
+
if (this._isClosed)
|
|
1811
|
+
return this.throwClosed(callback);
|
|
1812
|
+
this._pending.push('statementInfo');
|
|
1813
|
+
var msg = this._msg;
|
|
1814
|
+
var blr = this._blr;
|
|
1815
|
+
msg.pos = 0;
|
|
1816
|
+
blr.pos = 0;
|
|
1817
|
+
blr.addBytes(items);
|
|
1818
|
+
msg.addInt(const_1.default.op_info_sql);
|
|
1819
|
+
msg.addInt(statement.handle);
|
|
1820
|
+
msg.addInt(0); // incarnation
|
|
1821
|
+
msg.addBlr(blr);
|
|
1822
|
+
msg.addInt(65535); // buffer_length
|
|
1823
|
+
this._queueEvent(callback);
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Resolve the pending blobAsText fetches of a decoded row batch
|
|
1827
|
+
* (ret.arrBlob) and write the text back into ret.data. Reads run
|
|
1828
|
+
* sequentially to respect Firebird's per-connection open-blob-handle
|
|
1829
|
+
* limit (issue #387). Used by fetchAll for cursors and by
|
|
1830
|
+
* transaction.execute for op_execute2 singletons (EXECUTE PROCEDURE /
|
|
1831
|
+
* RETURNING — issue #305, whose blobs never resolved before).
|
|
1832
|
+
*/
|
|
1833
|
+
resolveTextBlobs(transaction, ret, callback) {
|
|
1834
|
+
const self = this;
|
|
1835
|
+
const fns = (ret && ret.arrBlob) || [];
|
|
1836
|
+
if (!fns.length) {
|
|
1837
|
+
return callback();
|
|
1838
|
+
}
|
|
1839
|
+
const read = (index) => {
|
|
1840
|
+
if (index >= fns.length) {
|
|
1841
|
+
return callback();
|
|
1842
|
+
}
|
|
1843
|
+
fns[index](transaction).then((blob) => {
|
|
1844
|
+
// nestTables === true rows: the value lives in the
|
|
1845
|
+
// per-table sub-object, not on the row itself
|
|
1846
|
+
Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(self.options, blob.meta || {}, parseValueIfJson(blob.value, self.options));
|
|
1847
|
+
read(index + 1);
|
|
1848
|
+
}, callback);
|
|
1849
|
+
};
|
|
1850
|
+
read(0);
|
|
1851
|
+
}
|
|
1673
1852
|
fetchAll(statement, transaction, callback) {
|
|
1674
1853
|
const self = this;
|
|
1675
1854
|
const custom = statement.options || {};
|
|
@@ -1682,26 +1861,10 @@ class Connection {
|
|
|
1682
1861
|
return;
|
|
1683
1862
|
}
|
|
1684
1863
|
if (ret && ret.data && ret.data.length) {
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const arrBlobFns = ret.arrBlob || [];
|
|
1690
|
-
const readBlobsSequentially = (index, results) => {
|
|
1691
|
-
if (index >= arrBlobFns.length) {
|
|
1692
|
-
return Promise.resolve(results);
|
|
1693
|
-
}
|
|
1694
|
-
return arrBlobFns[index](transaction).then((v) => {
|
|
1695
|
-
results.push(v);
|
|
1696
|
-
return readBlobsSequentially(index + 1, results);
|
|
1697
|
-
});
|
|
1698
|
-
};
|
|
1699
|
-
readBlobsSequentially(0, []).then((arrBlob) => {
|
|
1700
|
-
for (let i = 0; i < arrBlob.length; i++) {
|
|
1701
|
-
const blob = arrBlob[i];
|
|
1702
|
-
// nestTables === true rows: the value lives in the
|
|
1703
|
-
// per-table sub-object, not on the row itself
|
|
1704
|
-
Xsql.nestCell(ret.data[blob.row], blob.table)[blob.column] = applyTypeCast(statement.connection.options, blob.meta || {}, parseValueIfJson(blob.value, statement.connection.options));
|
|
1864
|
+
self.resolveTextBlobs(transaction, ret, (blobErr) => {
|
|
1865
|
+
if (blobErr) {
|
|
1866
|
+
callback(blobErr);
|
|
1867
|
+
return;
|
|
1705
1868
|
}
|
|
1706
1869
|
doSynchronousLoop(ret.data, (row, _i, next) => {
|
|
1707
1870
|
const pos = asStream ? streamIndex++ : (data.push(row) - 1);
|
|
@@ -1723,7 +1886,7 @@ class Connection {
|
|
|
1723
1886
|
self.fetch(statement, transaction, const_1.default.DEFAULT_FETCHSIZE, loop);
|
|
1724
1887
|
}
|
|
1725
1888
|
});
|
|
1726
|
-
})
|
|
1889
|
+
});
|
|
1727
1890
|
return;
|
|
1728
1891
|
}
|
|
1729
1892
|
if (ret && ret.fetched) {
|
|
@@ -1781,6 +1944,47 @@ class Connection {
|
|
|
1781
1944
|
msg.addBlr(blr);
|
|
1782
1945
|
this._queueEvent(callback);
|
|
1783
1946
|
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Create a transaction blob, upload `value` (Buffer, string, or a
|
|
1949
|
+
* JSON-able object) and deliver its quad id. executeBatch's blob
|
|
1950
|
+
* pre-pass uses this: batch messages reference pre-created transaction
|
|
1951
|
+
* blobs (the batch parameter buffer's default BLOB_NONE policy), just
|
|
1952
|
+
* like the classic execute path stores blob params.
|
|
1953
|
+
*/
|
|
1954
|
+
uploadBlob(transaction, value, callback) {
|
|
1955
|
+
var self = this;
|
|
1956
|
+
var b;
|
|
1957
|
+
if (Buffer.isBuffer(value)) {
|
|
1958
|
+
b = value;
|
|
1959
|
+
}
|
|
1960
|
+
else if (typeof value === 'string') {
|
|
1961
|
+
b = Xsql.encodeConnectionText(this.options, value);
|
|
1962
|
+
}
|
|
1963
|
+
else {
|
|
1964
|
+
b = Xsql.encodeConnectionText(this.options, JSON.stringify(value));
|
|
1965
|
+
}
|
|
1966
|
+
self.createBlob2(transaction, function (err, blob) {
|
|
1967
|
+
if (err) {
|
|
1968
|
+
return callback(err);
|
|
1969
|
+
}
|
|
1970
|
+
var chunkSize = self.options.blobChunkSize || 1024;
|
|
1971
|
+
// bufferReader's next() drops errors — capture the first segment
|
|
1972
|
+
// failure so a truncated upload is never reported as success
|
|
1973
|
+
var segmentError = null;
|
|
1974
|
+
bufferReader(b, chunkSize, function (part, next) {
|
|
1975
|
+
self.batchSegments(blob, part, function (segErr) {
|
|
1976
|
+
if (segErr && !segmentError) {
|
|
1977
|
+
segmentError = segErr;
|
|
1978
|
+
}
|
|
1979
|
+
next();
|
|
1980
|
+
});
|
|
1981
|
+
}, function () {
|
|
1982
|
+
self.closeBlob(blob, function (closeErr) {
|
|
1983
|
+
callback(segmentError || closeErr, blob.oid);
|
|
1984
|
+
}, false);
|
|
1985
|
+
});
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1784
1988
|
svcattach(options, callback, svc) {
|
|
1785
1989
|
this._lowercase_keys = options.lowercase_keys || const_1.default.DEFAULT_LOWERCASE_KEYS;
|
|
1786
1990
|
var database = options.database || options.filename;
|
|
@@ -2106,7 +2310,8 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
|
|
|
2106
2310
|
data.frows = data.frows || [];
|
|
2107
2311
|
if (custom.asObject && !data.fcols) {
|
|
2108
2312
|
const nest = Xsql.resolveNestTables(custom, cnx.options);
|
|
2109
|
-
const
|
|
2313
|
+
const transform = Xsql.resolveKeyTransform(custom, cnx.options);
|
|
2314
|
+
const columnKeys = Xsql.computeColumnKeys(output, nest, lowercase_keys, transform);
|
|
2110
2315
|
data.fcols = columnKeys.map((k) => k.key);
|
|
2111
2316
|
if (nest === true) {
|
|
2112
2317
|
// computeColumnKeys always sets table when nesting
|
|
@@ -2818,6 +3023,43 @@ function describe(buff, statement) {
|
|
|
2818
3023
|
}
|
|
2819
3024
|
unpackCharSetCollation(statement.input);
|
|
2820
3025
|
unpackCharSetCollation(statement.output);
|
|
3026
|
+
scaleOutputLengths(statement.output, statement.connection && statement.connection.options);
|
|
3027
|
+
}
|
|
3028
|
+
/**
|
|
3029
|
+
* Widen declared OUTPUT byte lengths for text columns whose charset the
|
|
3030
|
+
* server did NOT transliterate in the describe. Under a UTF8 connection
|
|
3031
|
+
* the server rewrites known charsets (a WIN1251 VARCHAR(15) is described
|
|
3032
|
+
* as UTF8 with length 60) — but NONE (and any untransliterated charset)
|
|
3033
|
+
* keeps its native length, and the engine's fetch-time capacity check
|
|
3034
|
+
* then treats those bytes as connection-charset characters: VARCHAR(15)
|
|
3035
|
+
* NONE declared as 15 bytes only fits floor(15/4) = 3 characters and
|
|
3036
|
+
* longer values fail with "string right truncation" (issue #422).
|
|
3037
|
+
* Declaring charCapacity × connectionWidth bytes makes the check pass;
|
|
3038
|
+
* the width-based trim in the text decoders restores the column's true
|
|
3039
|
+
* character capacity. The native length is preserved for metadata.
|
|
3040
|
+
*/
|
|
3041
|
+
function scaleOutputLengths(output, options) {
|
|
3042
|
+
if (!output || !output.length)
|
|
3043
|
+
return;
|
|
3044
|
+
const connWidth = Xsql.getFirebirdCharsetWidth(options && options.encoding);
|
|
3045
|
+
if (connWidth <= 1)
|
|
3046
|
+
return;
|
|
3047
|
+
for (let i = 0; i < output.length; i++) {
|
|
3048
|
+
const p = output[i];
|
|
3049
|
+
if (!p || (p.type !== const_1.default.SQL_TEXT && p.type !== const_1.default.SQL_VARYING))
|
|
3050
|
+
continue;
|
|
3051
|
+
// OCTETS (id 1) is binary and never transliterated
|
|
3052
|
+
if (p.charSetId === undefined || p.charSetId === 1)
|
|
3053
|
+
continue;
|
|
3054
|
+
const colWidth = (0, codepages_1.charsetWidthById)(p.charSetId);
|
|
3055
|
+
if (colWidth >= connWidth)
|
|
3056
|
+
continue;
|
|
3057
|
+
p.nativeLength = p.length;
|
|
3058
|
+
// BLR encodes the length as a 16-bit word — cap the widened value
|
|
3059
|
+
// (a >16KB single-byte column keeps its full byte capacity, it just
|
|
3060
|
+
// can't be over-declared four-fold)
|
|
3061
|
+
p.length = Math.min(Math.floor(p.length / colWidth) * connWidth, Math.floor(0xFFFF / connWidth) * connWidth);
|
|
3062
|
+
}
|
|
2821
3063
|
}
|
|
2822
3064
|
/**
|
|
2823
3065
|
* Batch support: the engine requires every batch message to use EXACTLY the
|
|
@@ -2844,7 +3086,7 @@ function buildBatchEncoders(input, options) {
|
|
|
2844
3086
|
return String(v);
|
|
2845
3087
|
};
|
|
2846
3088
|
var toBytes = function (v, meta, column) {
|
|
2847
|
-
var b = Buffer.isBuffer(v) ? v :
|
|
3089
|
+
var b = Buffer.isBuffer(v) ? v : Xsql.encodeConnectionText(options, toText(v));
|
|
2848
3090
|
if (b.length > meta.length) {
|
|
2849
3091
|
throw new Error('Batch value for column ' + column + ' is ' + b.length +
|
|
2850
3092
|
' bytes but the column accepts at most ' + meta.length + ' (' + (meta.field || '?') + ')');
|
|
@@ -2980,6 +3222,24 @@ function buildBatchEncoders(input, options) {
|
|
|
2980
3222
|
align(4);
|
|
2981
3223
|
offset += 4;
|
|
2982
3224
|
break;
|
|
3225
|
+
case const_1.default.SQL_BLOB:
|
|
3226
|
+
case const_1.default.SQL_QUAD:
|
|
3227
|
+
// the value is a transaction blob quad id — placed by
|
|
3228
|
+
// executeBatch's uploadBlob pre-pass, or passed by the
|
|
3229
|
+
// caller directly. ISC_QUAD: two longs, align 4.
|
|
3230
|
+
encoders.push((function (col) {
|
|
3231
|
+
return function (msg, v) {
|
|
3232
|
+
if (!v || typeof v.high !== 'number' || typeof v.low !== 'number') {
|
|
3233
|
+
throw new Error('Batch value for BLOB column ' + col +
|
|
3234
|
+
' must be a Buffer, string, object, or blob quad id');
|
|
3235
|
+
}
|
|
3236
|
+
msg.addInt(v.high);
|
|
3237
|
+
msg.addInt(v.low);
|
|
3238
|
+
};
|
|
3239
|
+
})(column));
|
|
3240
|
+
align(4);
|
|
3241
|
+
offset += 8;
|
|
3242
|
+
break;
|
|
2983
3243
|
default:
|
|
2984
3244
|
throw new Error('executeBatch does not support the type of parameter ' + column +
|
|
2985
3245
|
' yet (' + (meta.field || '?') + ', SQL type ' + meta.type + ')');
|
|
@@ -3028,11 +3288,15 @@ function CalcBlr(blr, xsqlda) {
|
|
|
3028
3288
|
}
|
|
3029
3289
|
function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
|
|
3030
3290
|
const infoValue = { row, column, value: '', meta, table };
|
|
3291
|
+
// decode ONCE from the concatenated bytes: per-segment toString() both
|
|
3292
|
+
// ignored the connection charset (#301 — cyrillic mojibake) and could
|
|
3293
|
+
// split a multi-byte UTF-8 character across segment boundaries
|
|
3294
|
+
const chunks = [];
|
|
3031
3295
|
return (transactionArg) => {
|
|
3032
3296
|
const cacheKey = `${id.high}:${id.low}`;
|
|
3033
3297
|
if (statement.connection._inlineBlobs && statement.connection._inlineBlobs.has(cacheKey)) {
|
|
3034
3298
|
const data = statement.connection._inlineBlobs.get(cacheKey);
|
|
3035
|
-
infoValue.value = data ?
|
|
3299
|
+
infoValue.value = data ? Xsql.decodeConnectionText(statement.connection.options, data) : '';
|
|
3036
3300
|
return Promise.resolve(infoValue);
|
|
3037
3301
|
}
|
|
3038
3302
|
const singleTransaction = transactionArg === undefined;
|
|
@@ -3071,13 +3335,13 @@ function fetch_blob_async_transaction(statement, id, column, row, meta, table) {
|
|
|
3071
3335
|
}
|
|
3072
3336
|
if (ret.buffer) {
|
|
3073
3337
|
const blr = new serialize_1.BlrReader(ret.buffer);
|
|
3074
|
-
|
|
3075
|
-
infoValue.value += data.toString(const_1.default.DEFAULT_ENCODING);
|
|
3338
|
+
chunks.push(blr.readSegment());
|
|
3076
3339
|
}
|
|
3077
3340
|
if (ret.handle !== 2) {
|
|
3078
3341
|
read();
|
|
3079
3342
|
return;
|
|
3080
3343
|
}
|
|
3344
|
+
infoValue.value = Xsql.decodeConnectionText(statement.connection.options, Buffer.concat(chunks));
|
|
3081
3345
|
statement.connection.closeBlob(blob);
|
|
3082
3346
|
if (singleTransaction) {
|
|
3083
3347
|
transaction.commit((err) => {
|
package/lib/wire/const.d.ts
CHANGED
|
@@ -404,6 +404,10 @@ declare const Const: Readonly<{
|
|
|
404
404
|
isc_info_sql_stmt_type: number;
|
|
405
405
|
isc_info_sql_get_plan: number;
|
|
406
406
|
isc_info_sql_records: number;
|
|
407
|
+
isc_info_req_select_count: number;
|
|
408
|
+
isc_info_req_insert_count: number;
|
|
409
|
+
isc_info_req_update_count: number;
|
|
410
|
+
isc_info_req_delete_count: number;
|
|
407
411
|
isc_info_sql_batch_fetch: number;
|
|
408
412
|
isc_info_sql_relation_alias: number;
|
|
409
413
|
isc_info_sql_explain_plan: number;
|
|
@@ -589,6 +593,7 @@ declare const Const: Readonly<{
|
|
|
589
593
|
isc_spb_trc_cfg: number;
|
|
590
594
|
DESCRIBE: number[];
|
|
591
595
|
DESCRIBE_WITH_SCHEMA: number[];
|
|
596
|
+
RECORDS_INFO: number[];
|
|
592
597
|
SUPPORTED_PROTOCOL: number[][];
|
|
593
598
|
}>;
|
|
594
599
|
export = Const;
|