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.
@@ -5,6 +5,7 @@ import Const from './const';
5
5
  import { makeSqlTag, type SqlTag } from '../sql-template';
6
6
  import { describeFields, parseRecordCounts } from './xsqlvar';
7
7
  import makeQueryStream from './query-stream';
8
+ import makeBatchStream from './batch-stream';
8
9
  import type Connection from './connection';
9
10
  import type Database from './database';
10
11
  import type Statement from './statement';
@@ -287,7 +288,17 @@ class Transaction {
287
288
 
288
289
  case Const.isc_info_sql_stmt_exec_procedure:
289
290
  if (ret && ret.data && ret.data.length > 0) {
290
- deliver(ret.data[0], true);
291
+ // singleton op_execute2 rows never pass through
292
+ // fetchAll, so their blobAsText fetches must be
293
+ // resolved here (issue #305: EXECUTE PROCEDURE
294
+ // returned text blobs as unresolved functions)
295
+ self.connection.resolveTextBlobs(self, ret, function(blobErr?: any) {
296
+ if (blobErr) {
297
+ dropError(blobErr);
298
+ return;
299
+ }
300
+ deliver(ret.data[0], true);
301
+ });
291
302
  break;
292
303
  } else if (statement.output.length) {
293
304
  statement.fetch(self, 1, function(err: any, fret: any) {
@@ -296,7 +307,13 @@ class Transaction {
296
307
  return;
297
308
  }
298
309
 
299
- deliver(fret.data[0], false);
310
+ self.connection.resolveTextBlobs(self, fret, function(blobErr?: any) {
311
+ if (blobErr) {
312
+ dropError(blobErr);
313
+ return;
314
+ }
315
+ deliver(fret.data[0], false);
316
+ });
300
317
  });
301
318
 
302
319
  break;
@@ -386,6 +403,15 @@ class Transaction {
386
403
  return makeQueryStream(this, query, params, options);
387
404
  }
388
405
 
406
+ /**
407
+ * Bulk-insert Writable running inside this transaction (see
408
+ * Database.batchStream). The transaction is NOT committed or rolled
409
+ * back by the stream — settle it yourself after 'finish'/'error'.
410
+ */
411
+ batchStream(query: string, options?: any) {
412
+ return makeBatchStream(this, query, options, false);
413
+ }
414
+
389
415
  query(query: string, params?: QueryParams | Callback, callback?: any, options: InternalQueryOptions = {}): void {
390
416
  if (params instanceof Function) {
391
417
  callback = params;
@@ -1,5 +1,7 @@
1
1
  import Const from './const';
2
2
  import { BlrReader } from './serialize';
3
+ import { getCodec } from './codepages';
4
+ import type { TextCodec } from './codepages';
3
5
  import type { XdrReader, XdrWriter, BlrWriter } from './serialize';
4
6
  import type { RecordCounts } from '../types';
5
7
 
@@ -16,6 +18,8 @@ const
16
18
  TimeCoeff = 86400000,
17
19
  MsPerMinute = 60000;
18
20
 
21
+ const EMPTY_BUFFER = Buffer.alloc(0);
22
+
19
23
  /**
20
24
  * Maps Firebird character-set names (upper-case) to the Node.js Buffer
21
25
  * encoding string used by Buffer.toString() / Buffer.from().
@@ -41,11 +45,19 @@ const FirebirdToNodeEncoding: Readonly<Record<string, string>> = Object.freeze({
41
45
  const FirebirdCharsetWidths: Record<string, number> = {
42
46
  'UTF8': 4,
43
47
  'UNICODE_FSS': 3,
44
- 'SJIS': 2,
45
- 'EUCJ': 2
48
+ // real Firebird names — the bare 'SJIS'/'EUCJ' keys never matched a
49
+ // valid encoding option and silently resolved to width 1
50
+ 'SJIS_0208': 2,
51
+ 'EUCJ_0208': 2,
52
+ 'KSC_5601': 2,
53
+ 'BIG_5': 2,
54
+ 'GB_2312': 2,
55
+ 'GBK': 2,
56
+ 'CP943C': 2,
57
+ 'GB18030': 4,
46
58
  };
47
59
 
48
- function getFirebirdCharsetWidth(charset?: string): number {
60
+ export function getFirebirdCharsetWidth(charset?: string): number {
49
61
  if (!charset) return 4;
50
62
  const upper = charset.toUpperCase();
51
63
  return FirebirdCharsetWidths[upper] || 1;
@@ -58,13 +70,85 @@ function getFirebirdCharsetWidth(charset?: string): number {
58
70
  * @param {object|null} options Connection options object (may be falsy).
59
71
  * @returns {string} A Node.js-compatible encoding string.
60
72
  */
61
- function resolveTextEncoding(options?: any): BufferEncoding {
73
+ export function resolveTextEncoding(options?: any): BufferEncoding {
62
74
  const encoding = (options && options.encoding)
63
75
  ? options.encoding.toUpperCase()
64
76
  : Const.DEFAULT_ENCODING;
65
77
  return (FirebirdToNodeEncoding[encoding] || Const.DEFAULT_ENCODING.toLowerCase()) as BufferEncoding;
66
78
  }
67
79
 
80
+ /**
81
+ * Codec for the CONNECTION charset when it is a codepage Node cannot
82
+ * handle natively (WIN1251, ISO8859_7, KOI8R, …); null on the native
83
+ * path (UTF8/latin1/ascii) and for unknown charsets. With a codec
84
+ * connection charset the server transliterates all text to that
85
+ * codepage, so every text column, parameter, SQL string and text blob
86
+ * goes through the codec (issues #319/#301).
87
+ */
88
+ export function resolveTextCodec(options?: any): TextCodec | null {
89
+ return resolveTextState(options).codec;
90
+ }
91
+
92
+ interface TextState {
93
+ key: string | undefined;
94
+ codec: TextCodec | null;
95
+ enc: BufferEncoding;
96
+ width: number;
97
+ }
98
+
99
+ /**
100
+ * Per-connection text handling, resolved once and memoized on the
101
+ * long-lived options object: the decode loop calls this per CELL, and
102
+ * recomputing uppercased names + map lookups a million times per large
103
+ * fetch is pure waste. Invalidated if options.encoding ever changes.
104
+ */
105
+ export function resolveTextState(options?: any): TextState {
106
+ const key = options && options.encoding;
107
+ if (options && options.__textState && options.__textState.key === key) {
108
+ return options.__textState;
109
+ }
110
+ const encoding = (key || Const.DEFAULT_ENCODING).toUpperCase();
111
+ const state: TextState = {
112
+ key,
113
+ codec: FirebirdToNodeEncoding[encoding] ? null : getCodec(encoding),
114
+ enc: (FirebirdToNodeEncoding[encoding] || Const.DEFAULT_ENCODING.toLowerCase()) as BufferEncoding,
115
+ width: getFirebirdCharsetWidth(encoding),
116
+ };
117
+ if (options) {
118
+ options.__textState = state;
119
+ }
120
+ return state;
121
+ }
122
+
123
+ /**
124
+ * Encode text in the CONNECTION charset — the byte form the server
125
+ * expects for parameters, SQL statement text and text-blob content.
126
+ */
127
+ export function encodeConnectionText(options: any, value: string): Buffer {
128
+ const state = resolveTextState(options);
129
+ if (state.codec) {
130
+ return state.codec.encode(value);
131
+ }
132
+ if (state.enc === 'ascii') {
133
+ // Node's 'ascii' encoding masks high bits (0xE4 → 'd') — replace
134
+ // non-ASCII with '?' instead, matching the codec policy
135
+ value = value.replace(/[^\x00-\x7F]/g, '?');
136
+ }
137
+ return Buffer.from(value, state.enc);
138
+ }
139
+
140
+ /**
141
+ * Decode connection-charset bytes to text (the read counterpart of
142
+ * encodeConnectionText — used for text blobs).
143
+ */
144
+ export function decodeConnectionText(options: any, buffer: Buffer): string {
145
+ const codec = resolveTextCodec(options);
146
+ if (codec) {
147
+ return codec.decode(buffer);
148
+ }
149
+ return buffer.toString(resolveTextEncoding(options));
150
+ }
151
+
68
152
  //------------------------------------------------------
69
153
 
70
154
  /**
@@ -88,6 +172,9 @@ export abstract class SQLVarBase {
88
172
  owner?: string;
89
173
  charSetId?: number;
90
174
  collationId?: number;
175
+ /** Original declared byte length when scaleOutputLengths widened
176
+ * `length` for the fetch capacity check (issue #422). */
177
+ nativeLength?: number;
91
178
 
92
179
  abstract decode(data: XdrReader, lowerV13: boolean, options?: any): any;
93
180
  abstract calcBlr(blr: BlrWriter): void;
@@ -274,7 +361,9 @@ export function describeField(meta: Partial<SQLVarBase>) {
274
361
  typeName: SQL_TYPE_NAMES[meta.type!] || 'UNKNOWN',
275
362
  subType: meta.subType,
276
363
  scale: meta.scale,
277
- length: meta.length,
364
+ // report the column's true declared length, not the widened fetch
365
+ // buffer (see scaleOutputLengths)
366
+ length: meta.nativeLength !== undefined ? meta.nativeLength : meta.length,
278
367
  nullable: meta.nullable,
279
368
  field: meta.field,
280
369
  relation: meta.relation,
@@ -337,22 +426,12 @@ export function parseRecordCounts(buffer: Buffer | undefined): RecordCounts {
337
426
  export class SQLVarText extends SQLVarBase {
338
427
  decode(data: XdrReader, lowerV13: boolean, options?: any) {
339
428
  let ret;
340
- const textEncoding = resolveTextEncoding(options);
341
- if (this.subType > 1) {
342
- // ToDo: with column charset
343
- ret = data.readText(this.length, textEncoding);
344
- const encoding = options && options.encoding ? options.encoding : 'UTF8';
345
- const width = getFirebirdCharsetWidth(encoding);
346
- const charLength = Math.floor(this.length / width);
347
- if (ret.length > charLength) {
348
- ret = ret.substring(0, charLength);
349
- }
350
- } else if (this.subType === 0) {
351
- // without charset definition
352
- ret = data.readText(this.length, textEncoding);
353
- const encoding = options && options.encoding ? options.encoding : 'UTF8';
354
- const width = getFirebirdCharsetWidth(encoding);
355
- const charLength = Math.floor(this.length / width);
429
+ if (this.subType > 1 || this.subType === 0) {
430
+ const state = resolveTextState(options);
431
+ ret = state.codec
432
+ ? state.codec.decode(data.readBuffer(this.length) || EMPTY_BUFFER)
433
+ : data.readText(this.length, state.enc);
434
+ const charLength = Math.floor(this.length / state.width);
356
435
  if (ret.length > charLength) {
357
436
  ret = ret.substring(0, charLength);
358
437
  }
@@ -383,13 +462,11 @@ export class SQLVarNull extends SQLVarText {
383
462
  export class SQLVarString extends SQLVarBase {
384
463
  decode(data: XdrReader, lowerV13: boolean, options?: any) {
385
464
  let ret;
386
- const textEncoding = resolveTextEncoding(options);
387
- if (this.subType > 1) {
388
- // ToDo: with column charset
389
- ret = data.readString(textEncoding);
390
- } else if (this.subType === 0) {
391
- // without charset definition
392
- ret = data.readString(textEncoding);
465
+ if (this.subType > 1 || this.subType === 0) {
466
+ const state = resolveTextState(options);
467
+ ret = state.codec
468
+ ? state.codec.decode(data.readArray() || EMPTY_BUFFER)
469
+ : data.readString(state.enc);
393
470
  } else {
394
471
  ret = data.readBuffer();
395
472
  }
@@ -1037,12 +1114,30 @@ export class SQLParamDate {
1037
1114
 
1038
1115
  export class SQLParamBool {
1039
1116
  value: any;
1040
-
1041
- constructor(value: any) {
1117
+ /**
1118
+ * Encode as a real BOOLEAN (blr_bool + xdr opaque byte) instead of the
1119
+ * legacy blr_short 0/1. Set when the DESCRIBED parameter type is
1120
+ * SQL_BOOLEAN: Firebird refuses smallint→BOOLEAN conversion
1121
+ * ("conversion error from string", issue #122), and conversely BOOLEAN
1122
+ * does not convert to numbers — so smallint targets keep the legacy
1123
+ * form for compatibility.
1124
+ */
1125
+ asBoolean: boolean;
1126
+
1127
+ constructor(value: any, asBoolean = false) {
1042
1128
  this.value = value;
1129
+ this.asBoolean = asBoolean;
1043
1130
  }
1044
1131
 
1045
1132
  encode(data: XdrWriter): void {
1133
+ if (this.asBoolean) {
1134
+ // xdr_datum sends booleans as 1 opaque value byte + 3 pad bytes
1135
+ // (NOT a big-endian int: the value byte comes FIRST — addInt(1)
1136
+ // would decode server-side as false). Matches the batch encoder.
1137
+ data.addBuffer(Buffer.from([this.value ? 1 : 0]));
1138
+ data.addAlignment(1);
1139
+ return;
1140
+ }
1046
1141
  if (this.value != null) {
1047
1142
  data.addInt(this.value ? 1 : 0);
1048
1143
  } else {
@@ -1052,6 +1147,10 @@ export class SQLParamBool {
1052
1147
  }
1053
1148
 
1054
1149
  calcBlr(blr: BlrWriter): void {
1150
+ if (this.asBoolean) {
1151
+ blr.addByte(Const.blr_bool);
1152
+ return;
1153
+ }
1055
1154
  blr.addByte(Const.blr_short);
1056
1155
  blr.addShort(0);
1057
1156
  }