node-firebird 2.12.0 → 2.14.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.
@@ -9,6 +9,7 @@ const const_1 = __importDefault(require("./const"));
9
9
  const sql_template_1 = require("../sql-template");
10
10
  const xsqlvar_1 = require("./xsqlvar");
11
11
  const query_stream_1 = __importDefault(require("./query-stream"));
12
+ const batch_stream_1 = __importDefault(require("./batch-stream"));
12
13
  /***************************************
13
14
  *
14
15
  * Transaction
@@ -254,7 +255,17 @@ class Transaction {
254
255
  break;
255
256
  case const_1.default.isc_info_sql_stmt_exec_procedure:
256
257
  if (ret && ret.data && ret.data.length > 0) {
257
- deliver(ret.data[0], true);
258
+ // singleton op_execute2 rows never pass through
259
+ // fetchAll, so their blobAsText fetches must be
260
+ // resolved here (issue #305: EXECUTE PROCEDURE
261
+ // returned text blobs as unresolved functions)
262
+ self.connection.resolveTextBlobs(self, ret, function (blobErr) {
263
+ if (blobErr) {
264
+ dropError(blobErr);
265
+ return;
266
+ }
267
+ deliver(ret.data[0], true);
268
+ });
258
269
  break;
259
270
  }
260
271
  else if (statement.output.length) {
@@ -263,7 +274,13 @@ class Transaction {
263
274
  dropError(err);
264
275
  return;
265
276
  }
266
- deliver(fret.data[0], false);
277
+ self.connection.resolveTextBlobs(self, fret, function (blobErr) {
278
+ if (blobErr) {
279
+ dropError(blobErr);
280
+ return;
281
+ }
282
+ deliver(fret.data[0], false);
283
+ });
267
284
  });
268
285
  break;
269
286
  }
@@ -344,6 +361,14 @@ class Transaction {
344
361
  queryStream(query, params, options) {
345
362
  return (0, query_stream_1.default)(this, query, params, options);
346
363
  }
364
+ /**
365
+ * Bulk-insert Writable running inside this transaction (see
366
+ * Database.batchStream). The transaction is NOT committed or rolled
367
+ * back by the stream — settle it yourself after 'finish'/'error'.
368
+ */
369
+ batchStream(query, options) {
370
+ return (0, batch_stream_1.default)(this, query, options, false);
371
+ }
347
372
  query(query, params, callback, options = {}) {
348
373
  if (params instanceof Function) {
349
374
  callback = params;
@@ -1,5 +1,47 @@
1
+ import type { TextCodec } from './codepages';
1
2
  import type { XdrReader, XdrWriter, BlrWriter } from './serialize';
2
3
  import type { RecordCounts } from '../types';
4
+ export declare function getFirebirdCharsetWidth(charset?: string): number;
5
+ /**
6
+ * Resolve the Node.js Buffer encoding to use when decoding text from a
7
+ * Firebird response buffer.
8
+ *
9
+ * @param {object|null} options Connection options object (may be falsy).
10
+ * @returns {string} A Node.js-compatible encoding string.
11
+ */
12
+ export declare function resolveTextEncoding(options?: any): BufferEncoding;
13
+ /**
14
+ * Codec for the CONNECTION charset when it is a codepage Node cannot
15
+ * handle natively (WIN1251, ISO8859_7, KOI8R, …); null on the native
16
+ * path (UTF8/latin1/ascii) and for unknown charsets. With a codec
17
+ * connection charset the server transliterates all text to that
18
+ * codepage, so every text column, parameter, SQL string and text blob
19
+ * goes through the codec (issues #319/#301).
20
+ */
21
+ export declare function resolveTextCodec(options?: any): TextCodec | null;
22
+ interface TextState {
23
+ key: string | undefined;
24
+ codec: TextCodec | null;
25
+ enc: BufferEncoding;
26
+ width: number;
27
+ }
28
+ /**
29
+ * Per-connection text handling, resolved once and memoized on the
30
+ * long-lived options object: the decode loop calls this per CELL, and
31
+ * recomputing uppercased names + map lookups a million times per large
32
+ * fetch is pure waste. Invalidated if options.encoding ever changes.
33
+ */
34
+ export declare function resolveTextState(options?: any): TextState;
35
+ /**
36
+ * Encode text in the CONNECTION charset — the byte form the server
37
+ * expects for parameters, SQL statement text and text-blob content.
38
+ */
39
+ export declare function encodeConnectionText(options: any, value: string): Buffer;
40
+ /**
41
+ * Decode connection-charset bytes to text (the read counterpart of
42
+ * encodeConnectionText — used for text blobs).
43
+ */
44
+ export declare function decodeConnectionText(options: any, buffer: Buffer): string;
3
45
  /**
4
46
  * Common shape of all SQLVar descriptor objects. The metadata properties
5
47
  * are populated externally (in connection.ts) from the op_prepare_statement
@@ -19,6 +61,9 @@ export declare abstract class SQLVarBase {
19
61
  owner?: string;
20
62
  charSetId?: number;
21
63
  collationId?: number;
64
+ /** Original declared byte length when scaleOutputLengths widened
65
+ * `length` for the fetch capacity check (issue #422). */
66
+ nativeLength?: number;
22
67
  abstract decode(data: XdrReader, lowerV13: boolean, options?: any): any;
23
68
  abstract calcBlr(blr: BlrWriter): void;
24
69
  }
@@ -278,7 +323,17 @@ export declare class SQLParamDate {
278
323
  }
279
324
  export declare class SQLParamBool {
280
325
  value: any;
281
- constructor(value: any);
326
+ /**
327
+ * Encode as a real BOOLEAN (blr_bool + xdr opaque byte) instead of the
328
+ * legacy blr_short 0/1. Set when the DESCRIBED parameter type is
329
+ * SQL_BOOLEAN: Firebird refuses smallint→BOOLEAN conversion
330
+ * ("conversion error from string", issue #122), and conversely BOOLEAN
331
+ * does not convert to numbers — so smallint targets keep the legacy
332
+ * form for compatibility.
333
+ */
334
+ asBoolean: boolean;
335
+ constructor(value: any, asBoolean?: boolean);
282
336
  encode(data: XdrWriter): void;
283
337
  calcBlr(blr: BlrWriter): void;
284
338
  }
339
+ export {};
@@ -4,6 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.SQLParamBool = exports.SQLParamDate = exports.SQLParamQuad = exports.SQLParamBuffer = exports.SQLParamString = exports.SQLParamDouble = exports.SQLParamDecFloat34 = exports.SQLParamDecFloat16 = exports.SQLParamInt128 = exports.SQLParamInt64 = exports.SQLParamInt = exports.SQLVarBoolean = exports.SQLVarTimeStampTzEx = exports.SQLVarTimeStampTz = exports.SQLVarTimeTzEx = exports.SQLVarTimeTz = exports.SQLVarTimeStamp = exports.SQLVarTime = exports.SQLVarDate = exports.SQLVarDouble = exports.SQLVarFloat = exports.SQLVarDecFloat34 = exports.SQLVarDecFloat16 = exports.SQLVarInt128 = exports.SQLVarInt64 = exports.SQLVarShort = exports.SQLVarInt = exports.SQLVarArray = exports.SQLVarBlob = exports.SQLVarQuad = exports.SQLVarString = exports.SQLVarNull = exports.SQLVarText = exports.SQL_TYPE_NAMES = exports.SQLVarBase = void 0;
7
+ exports.getFirebirdCharsetWidth = getFirebirdCharsetWidth;
8
+ exports.resolveTextEncoding = resolveTextEncoding;
9
+ exports.resolveTextCodec = resolveTextCodec;
10
+ exports.resolveTextState = resolveTextState;
11
+ exports.encodeConnectionText = encodeConnectionText;
12
+ exports.decodeConnectionText = decodeConnectionText;
7
13
  exports.computeColumnKeys = computeColumnKeys;
8
14
  exports.camelizeKey = camelizeKey;
9
15
  exports.resolveKeyTransform = resolveKeyTransform;
@@ -15,6 +21,7 @@ exports.parseRecordCounts = parseRecordCounts;
15
21
  exports.encodeDateTimeParts = encodeDateTimeParts;
16
22
  const const_1 = __importDefault(require("./const"));
17
23
  const serialize_1 = require("./serialize");
24
+ const codepages_1 = require("./codepages");
18
25
  /***************************************
19
26
  *
20
27
  * SQLVar
@@ -22,6 +29,7 @@ const serialize_1 = require("./serialize");
22
29
  ***************************************/
23
30
  const ScaleDivisor = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000, 10000000000, 100000000000, 1000000000000, 10000000000000, 100000000000000, 1000000000000000];
24
31
  const DateOffset = 40587, TimeCoeff = 86400000, MsPerMinute = 60000;
32
+ const EMPTY_BUFFER = Buffer.alloc(0);
25
33
  /**
26
34
  * Maps Firebird character-set names (upper-case) to the Node.js Buffer
27
35
  * encoding string used by Buffer.toString() / Buffer.from().
@@ -46,8 +54,16 @@ const FirebirdToNodeEncoding = Object.freeze({
46
54
  const FirebirdCharsetWidths = {
47
55
  'UTF8': 4,
48
56
  'UNICODE_FSS': 3,
49
- 'SJIS': 2,
50
- 'EUCJ': 2
57
+ // real Firebird names — the bare 'SJIS'/'EUCJ' keys never matched a
58
+ // valid encoding option and silently resolved to width 1
59
+ 'SJIS_0208': 2,
60
+ 'EUCJ_0208': 2,
61
+ 'KSC_5601': 2,
62
+ 'BIG_5': 2,
63
+ 'GB_2312': 2,
64
+ 'GBK': 2,
65
+ 'CP943C': 2,
66
+ 'GB18030': 4,
51
67
  };
52
68
  function getFirebirdCharsetWidth(charset) {
53
69
  if (!charset)
@@ -68,6 +84,67 @@ function resolveTextEncoding(options) {
68
84
  : const_1.default.DEFAULT_ENCODING;
69
85
  return (FirebirdToNodeEncoding[encoding] || const_1.default.DEFAULT_ENCODING.toLowerCase());
70
86
  }
87
+ /**
88
+ * Codec for the CONNECTION charset when it is a codepage Node cannot
89
+ * handle natively (WIN1251, ISO8859_7, KOI8R, …); null on the native
90
+ * path (UTF8/latin1/ascii) and for unknown charsets. With a codec
91
+ * connection charset the server transliterates all text to that
92
+ * codepage, so every text column, parameter, SQL string and text blob
93
+ * goes through the codec (issues #319/#301).
94
+ */
95
+ function resolveTextCodec(options) {
96
+ return resolveTextState(options).codec;
97
+ }
98
+ /**
99
+ * Per-connection text handling, resolved once and memoized on the
100
+ * long-lived options object: the decode loop calls this per CELL, and
101
+ * recomputing uppercased names + map lookups a million times per large
102
+ * fetch is pure waste. Invalidated if options.encoding ever changes.
103
+ */
104
+ function resolveTextState(options) {
105
+ const key = options && options.encoding;
106
+ if (options && options.__textState && options.__textState.key === key) {
107
+ return options.__textState;
108
+ }
109
+ const encoding = (key || const_1.default.DEFAULT_ENCODING).toUpperCase();
110
+ const state = {
111
+ key,
112
+ codec: FirebirdToNodeEncoding[encoding] ? null : (0, codepages_1.getCodec)(encoding),
113
+ enc: (FirebirdToNodeEncoding[encoding] || const_1.default.DEFAULT_ENCODING.toLowerCase()),
114
+ width: getFirebirdCharsetWidth(encoding),
115
+ };
116
+ if (options) {
117
+ options.__textState = state;
118
+ }
119
+ return state;
120
+ }
121
+ /**
122
+ * Encode text in the CONNECTION charset — the byte form the server
123
+ * expects for parameters, SQL statement text and text-blob content.
124
+ */
125
+ function encodeConnectionText(options, value) {
126
+ const state = resolveTextState(options);
127
+ if (state.codec) {
128
+ return state.codec.encode(value);
129
+ }
130
+ if (state.enc === 'ascii') {
131
+ // Node's 'ascii' encoding masks high bits (0xE4 → 'd') — replace
132
+ // non-ASCII with '?' instead, matching the codec policy
133
+ value = value.replace(/[^\x00-\x7F]/g, '?');
134
+ }
135
+ return Buffer.from(value, state.enc);
136
+ }
137
+ /**
138
+ * Decode connection-charset bytes to text (the read counterpart of
139
+ * encodeConnectionText — used for text blobs).
140
+ */
141
+ function decodeConnectionText(options, buffer) {
142
+ const codec = resolveTextCodec(options);
143
+ if (codec) {
144
+ return codec.decode(buffer);
145
+ }
146
+ return buffer.toString(resolveTextEncoding(options));
147
+ }
71
148
  //------------------------------------------------------
72
149
  /**
73
150
  * Common shape of all SQLVar descriptor objects. The metadata properties
@@ -222,7 +299,9 @@ function describeField(meta) {
222
299
  typeName: exports.SQL_TYPE_NAMES[meta.type] || 'UNKNOWN',
223
300
  subType: meta.subType,
224
301
  scale: meta.scale,
225
- length: meta.length,
302
+ // report the column's true declared length, not the widened fetch
303
+ // buffer (see scaleOutputLengths)
304
+ length: meta.nativeLength !== undefined ? meta.nativeLength : meta.length,
226
305
  nullable: meta.nullable,
227
306
  field: meta.field,
228
307
  relation: meta.relation,
@@ -290,23 +369,12 @@ function parseRecordCounts(buffer) {
290
369
  class SQLVarText extends SQLVarBase {
291
370
  decode(data, lowerV13, options) {
292
371
  let ret;
293
- const textEncoding = resolveTextEncoding(options);
294
- if (this.subType > 1) {
295
- // ToDo: with column charset
296
- ret = data.readText(this.length, textEncoding);
297
- const encoding = options && options.encoding ? options.encoding : 'UTF8';
298
- const width = getFirebirdCharsetWidth(encoding);
299
- const charLength = Math.floor(this.length / width);
300
- if (ret.length > charLength) {
301
- ret = ret.substring(0, charLength);
302
- }
303
- }
304
- else if (this.subType === 0) {
305
- // without charset definition
306
- ret = data.readText(this.length, textEncoding);
307
- const encoding = options && options.encoding ? options.encoding : 'UTF8';
308
- const width = getFirebirdCharsetWidth(encoding);
309
- const charLength = Math.floor(this.length / width);
372
+ if (this.subType > 1 || this.subType === 0) {
373
+ const state = resolveTextState(options);
374
+ ret = state.codec
375
+ ? state.codec.decode(data.readBuffer(this.length) || EMPTY_BUFFER)
376
+ : data.readText(this.length, state.enc);
377
+ const charLength = Math.floor(this.length / state.width);
310
378
  if (ret.length > charLength) {
311
379
  ret = ret.substring(0, charLength);
312
380
  }
@@ -333,14 +401,11 @@ exports.SQLVarNull = SQLVarNull;
333
401
  class SQLVarString extends SQLVarBase {
334
402
  decode(data, lowerV13, options) {
335
403
  let ret;
336
- const textEncoding = resolveTextEncoding(options);
337
- if (this.subType > 1) {
338
- // ToDo: with column charset
339
- ret = data.readString(textEncoding);
340
- }
341
- else if (this.subType === 0) {
342
- // without charset definition
343
- ret = data.readString(textEncoding);
404
+ if (this.subType > 1 || this.subType === 0) {
405
+ const state = resolveTextState(options);
406
+ ret = state.codec
407
+ ? state.codec.decode(data.readArray() || EMPTY_BUFFER)
408
+ : data.readString(state.enc);
344
409
  }
345
410
  else {
346
411
  ret = data.readBuffer();
@@ -875,10 +940,19 @@ class SQLParamDate {
875
940
  exports.SQLParamDate = SQLParamDate;
876
941
  //------------------------------------------------------
877
942
  class SQLParamBool {
878
- constructor(value) {
943
+ constructor(value, asBoolean = false) {
879
944
  this.value = value;
945
+ this.asBoolean = asBoolean;
880
946
  }
881
947
  encode(data) {
948
+ if (this.asBoolean) {
949
+ // xdr_datum sends booleans as 1 opaque value byte + 3 pad bytes
950
+ // (NOT a big-endian int: the value byte comes FIRST — addInt(1)
951
+ // would decode server-side as false). Matches the batch encoder.
952
+ data.addBuffer(Buffer.from([this.value ? 1 : 0]));
953
+ data.addAlignment(1);
954
+ return;
955
+ }
882
956
  if (this.value != null) {
883
957
  data.addInt(this.value ? 1 : 0);
884
958
  }
@@ -888,6 +962,10 @@ class SQLParamBool {
888
962
  }
889
963
  }
890
964
  calcBlr(blr) {
965
+ if (this.asBoolean) {
966
+ blr.addByte(const_1.default.blr_bool);
967
+ return;
968
+ }
891
969
  blr.addByte(const_1.default.blr_short);
892
970
  blr.addShort(0);
893
971
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.12.0",
3
+ "version": "2.14.0",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
@@ -23,6 +23,24 @@
23
23
  ],
24
24
  "main": "./lib/index.js",
25
25
  "types": "./lib/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./lib/index.d.ts",
29
+ "import": "./lib/index.js",
30
+ "require": "./lib/index.js"
31
+ },
32
+ "./lib/firebird.msg": "./lib/firebird.msg",
33
+ "./lib/firebird.msg.json": "./lib/firebird.msg.json",
34
+ "./lib/*.js": {
35
+ "types": "./lib/*.d.ts",
36
+ "default": "./lib/*.js"
37
+ },
38
+ "./lib/*": {
39
+ "types": "./lib/*.d.ts",
40
+ "default": "./lib/*.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
26
44
  "files": [
27
45
  "lib",
28
46
  "src"
package/src/callback.ts CHANGED
@@ -45,6 +45,21 @@ export function toError(err: any): Error {
45
45
  * Run a callback-style operation and return a Promise for its result.
46
46
  * Usage: fromCallback<Database>(cb => attach(options, cb))
47
47
  */
48
+ /**
49
+ * Run `work` with a pooled connection and always return it to its pool
50
+ * (detach) when the promise settles — a detach hiccup never masks the
51
+ * outcome of `work`. Shared by Pool.withConnection and
52
+ * PoolCluster.withConnection so the release semantics cannot drift.
53
+ */
54
+ export async function withPooledConnection<T>(getAsync: () => Promise<any>, work: (db: any) => Promise<T> | T): Promise<T> {
55
+ const db = await getAsync();
56
+ try {
57
+ return await work(db);
58
+ } finally {
59
+ await new Promise<void>(function(resolve) { db.detach(function() { resolve(); }); });
60
+ }
61
+ }
62
+
48
63
  export function fromCallback<T = any>(executor: (cb: Callback<T>) => void): Promise<T> {
49
64
  return new Promise<T>(function(resolve, reject) {
50
65
  executor(function(err?: any, result?: T) {
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ import Const from './wire/const';
2
2
  import { doError, doCallback, fromCallback, type Callback } from './callback';
3
3
  import Connection from './wire/connection';
4
4
  import Pool from './pool';
5
+ import PoolCluster from './pool-cluster';
6
+ import type { PoolClusterOptions } from './pool-cluster';
5
7
  import { escape as escapeValue } from './utils';
6
8
  import { parseConnectionUri, parseConnectionString, normalizeOptions } from './uri';
7
9
  import type {
@@ -194,6 +196,19 @@ export function pool(max: number, options: Options | string): ConnectionPool {
194
196
  return new Pool(attach, max, Object.assign({}, normalizeOptions(options), { isPool: true }));
195
197
  }
196
198
 
199
+ /**
200
+ * Multi-host pooling (primaries/replicas, failover): named nodes, each
201
+ * backed by a regular pool, selected by glob pattern + 'rr'/'random'/
202
+ * 'order' selector, with connection-failure failover and error-based
203
+ * node offlining. See README § Multi-host pooling.
204
+ */
205
+ export function poolCluster(options?: PoolClusterOptions): PoolCluster {
206
+ const normalized: PoolClusterOptions = { ...(options || {}) };
207
+ normalized.defaults = normalizeOptions(normalized.defaults || {});
208
+ return new PoolCluster(attach, normalized);
209
+ }
210
+ export type { PoolClusterOptions, ClusterSelector } from './pool-cluster';
211
+
197
212
  export { parseConnectionUri, parseConnectionString };
198
213
  export { parseNamedPlaceholders } from './named-params';
199
214