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.
@@ -277,6 +277,16 @@ class XdrWriter {
277
277
  this.buffer.fill(0, this.pos + len, this.pos + alen);
278
278
  this.pos += alen;
279
279
  }
280
+ /** addString for pre-encoded bytes (codepage connection charsets). */
281
+ addStringBuffer(b) {
282
+ var alen = align(b.length);
283
+ this.ensure(alen + 4);
284
+ this.buffer.writeInt32BE(b.length, this.pos);
285
+ this.pos += 4;
286
+ b.copy(this.buffer, this.pos);
287
+ this.buffer.fill(0, this.pos + b.length, this.pos + alen);
288
+ this.pos += alen;
289
+ }
280
290
  addText(s, encoding) {
281
291
  var len = Buffer.byteLength(s, encoding);
282
292
  var alen = align(len);
@@ -132,8 +132,14 @@ class Socket {
132
132
  * Compress and/or encrypt data before sending to socket.
133
133
  */
134
134
  write(data, defer = false) {
135
+ // Callers pass views of the connection's shared _msg buffer, and both
136
+ // net.Socket (when it cannot flush immediately) and zlib keep a
137
+ // REFERENCE to the chunk — a later sender rebuilding _msg would then
138
+ // corrupt this queued packet (intermittent, load-dependent). Own the
139
+ // bytes at the send boundary, once.
140
+ data = Buffer.from(data);
135
141
  if (process.env.FIREBIRD_DEBUG) {
136
- console.log('[fb-debug] socket.write: length=%d bytes=%s encrypt=%s defer=%s', data.length, Buffer.from(data).toString('hex'), this.encrypt, defer);
142
+ console.log('[fb-debug] socket.write: length=%d bytes=%s encrypt=%s defer=%s', data.length, data.toString('hex'), this.encrypt, defer);
137
143
  }
138
144
  if (defer) {
139
145
  // Accumulate deferred packets instead of overwriting. Multiple
@@ -142,8 +148,8 @@ class Socket {
142
148
  // overwriting the buffer silently drops packets and desynchronises
143
149
  // the request/response queue, causing the connection to hang.
144
150
  this.buffer = this.buffer
145
- ? Buffer.concat([this.buffer, Buffer.from(data)])
146
- : Buffer.from(data);
151
+ ? Buffer.concat([this.buffer, data])
152
+ : data;
147
153
  return;
148
154
  }
149
155
  if (!defer && this.buffer) {
@@ -48,6 +48,12 @@ declare class Transaction {
48
48
  * stream ends — commit or roll back yourself.
49
49
  */
50
50
  queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): import("node:stream").Readable;
51
+ /**
52
+ * Bulk-insert Writable running inside this transaction (see
53
+ * Database.batchStream). The transaction is NOT committed or rolled
54
+ * back by the stream — settle it yourself after 'finish'/'error'.
55
+ */
56
+ batchStream(query: string, options?: any): import("node:stream").Writable;
51
57
  query(query: string, params?: QueryParams | Callback, callback?: any, options?: InternalQueryOptions): void;
52
58
  /**
53
59
  * Execute `query` once per row in `rows` using the Firebird 4 batch API
@@ -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.13.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/types.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // They now live in the TypeScript source tree and are compiled into the
6
6
  // published declaration files.
7
7
 
8
- import type { Readable } from 'stream';
8
+ import type { Readable, Writable } from 'stream';
9
9
  import type { SqlTag } from './sql-template';
10
10
 
11
11
  export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
@@ -211,6 +211,22 @@ export interface QueryResult<T = any> {
211
211
  warnings: ServerWarning[];
212
212
  }
213
213
 
214
+ /** Options for batchStream: the executeBatch options plus stream tuning. */
215
+ export type BatchStreamOptions = BatchOptions & {
216
+ /** Rows buffered per executeBatch flush (default 1000). */
217
+ flushRows?: number;
218
+ /** Writable highWaterMark in rows (default: flushRows). */
219
+ highWaterMark?: number;
220
+ };
221
+
222
+ /** The Writable returned by batchStream, with totals valid after 'finish'. */
223
+ export interface BatchStream extends Writable {
224
+ /** Records the server processed so far. */
225
+ recordCount: number;
226
+ /** Sum of per-record update counts so far. */
227
+ affectedRows: number;
228
+ }
229
+
214
230
  export type QueryStreamOptions = QueryOptions & {
215
231
  /**
216
232
  * Rows buffered internally before fetching pauses (object-mode
@@ -232,8 +248,8 @@ export interface Database {
232
248
  detach(callback?: SimpleCallback): Database;
233
249
  transaction(options: TransactionOptions|Isolation|TransactionCallback, callback?: TransactionCallback): Database;
234
250
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
235
- query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): Database;
236
- execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): Database;
251
+ query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
252
+ execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
237
253
  /** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
238
254
  executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
239
255
  sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
@@ -244,6 +260,13 @@ export interface Database {
244
260
  * fetch and releases the statement.
245
261
  */
246
262
  queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
263
+ /**
264
+ * Bulk-insert Writable (COPY FROM analogue, Firebird 4.0+): write
265
+ * parameter-array rows; they are flushed in chunks through the batch
266
+ * API. Runs its own transaction — committed on finish, rolled back on
267
+ * error/destroy. BLOB columns accept Buffers/strings.
268
+ */
269
+ batchStream(query: string, options?: BatchStreamOptions): BatchStream;
247
270
  drop(callback: SimpleCallback): void;
248
271
  escape(value: any): string;
249
272
  attachEvent(callback: any): this;
@@ -290,8 +313,8 @@ export interface Transaction {
290
313
  */
291
314
  savepoint<T>(work: (transaction: Transaction) => Promise<T> | T): Promise<T>;
292
315
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
293
- query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
294
- execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
316
+ query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
317
+ execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
295
318
  /** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
296
319
  executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
297
320
  sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
@@ -301,6 +324,11 @@ export interface Transaction {
301
324
  * transaction is NOT committed when the stream ends.
302
325
  */
303
326
  queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
327
+ /**
328
+ * Bulk-insert Writable inside this transaction (see
329
+ * Database.batchStream); commit/rollback stays with the caller.
330
+ */
331
+ batchStream(query: string, options?: BatchStreamOptions): BatchStream;
304
332
  commit(callback?: SimpleCallback): void;
305
333
  commitRetaining(callback?: SimpleCallback): void;
306
334
  rollback(callback?: SimpleCallback): void;
@@ -416,6 +444,13 @@ export interface Options {
416
444
  * per-query `namedPlaceholders: false` override.
417
445
  */
418
446
  namedPlaceholders?: boolean;
447
+ /**
448
+ * Default character set of a NEWLY CREATED database (create /
449
+ * attachOrCreate only). Falls back to the connection `encoding`, then
450
+ * UTF8 — pass e.g. `defaultCharset: 'UTF8'` to keep a modern database
451
+ * default while connecting with a legacy codepage `encoding`.
452
+ */
453
+ defaultCharset?: string;
419
454
  /**
420
455
  * Qualify object-row keys by source table (same option as mysql2), so
421
456
  * JOINed columns with the same name stop overwriting each other:
package/src/utils.ts CHANGED
@@ -108,6 +108,21 @@ export const parseDate = (str: string): Date => {
108
108
  /**
109
109
  * Get Error Message per gdscode
110
110
  */
111
+ /**
112
+ * Turn a failed executeBatch completion into the all-or-nothing error
113
+ * shape shared by database.executeBatch and batchStream: the first
114
+ * record's own error (or a synthesized summary), with the full
115
+ * completion attached as err.batchCompletion.
116
+ */
117
+ export const batchResultToError = (result: { errors: { error: any }[]; errorRecordNumbers: number[] }): any => {
118
+ const first = result.errors.length ? result.errors[0] : null;
119
+ const err: any = first
120
+ ? first.error
121
+ : new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
122
+ err.batchCompletion = result;
123
+ return err;
124
+ };
125
+
111
126
  export const lookupMessages = (status: FbStatusItem[]): string => {
112
127
  const messages = status.map((item) => {
113
128
  let text = MessagesError[item.gdscode];
@@ -143,7 +158,10 @@ export const escape = function(value: any, protocolVersion?: number): string {
143
158
  case 'number':
144
159
  return value.toString();
145
160
  case 'string':
146
- return "'" + value.replace(/'/g, "''").replace(/\\/g, '\\\\') + "'";
161
+ // Firebird string literals have NO backslash escapes — only the
162
+ // quote is doubled. Doubling backslashes corrupted the data
163
+ // (issue #156: '\' arrived as '\\').
164
+ return "'" + value.replace(/'/g, "''") + "'";
147
165
  }
148
166
 
149
167
  if (value instanceof Date)