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/xsqlvar.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import Const from './const';
|
|
2
|
+
import { BlrReader } from './serialize';
|
|
3
|
+
import { getCodec } from './codepages';
|
|
4
|
+
import type { TextCodec } from './codepages';
|
|
2
5
|
import type { XdrReader, XdrWriter, BlrWriter } from './serialize';
|
|
6
|
+
import type { RecordCounts } from '../types';
|
|
3
7
|
|
|
4
8
|
/***************************************
|
|
5
9
|
*
|
|
@@ -14,6 +18,8 @@ const
|
|
|
14
18
|
TimeCoeff = 86400000,
|
|
15
19
|
MsPerMinute = 60000;
|
|
16
20
|
|
|
21
|
+
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
22
|
+
|
|
17
23
|
/**
|
|
18
24
|
* Maps Firebird character-set names (upper-case) to the Node.js Buffer
|
|
19
25
|
* encoding string used by Buffer.toString() / Buffer.from().
|
|
@@ -39,11 +45,19 @@ const FirebirdToNodeEncoding: Readonly<Record<string, string>> = Object.freeze({
|
|
|
39
45
|
const FirebirdCharsetWidths: Record<string, number> = {
|
|
40
46
|
'UTF8': 4,
|
|
41
47
|
'UNICODE_FSS': 3,
|
|
42
|
-
'SJIS'
|
|
43
|
-
|
|
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,
|
|
44
58
|
};
|
|
45
59
|
|
|
46
|
-
function getFirebirdCharsetWidth(charset?: string): number {
|
|
60
|
+
export function getFirebirdCharsetWidth(charset?: string): number {
|
|
47
61
|
if (!charset) return 4;
|
|
48
62
|
const upper = charset.toUpperCase();
|
|
49
63
|
return FirebirdCharsetWidths[upper] || 1;
|
|
@@ -56,13 +70,85 @@ function getFirebirdCharsetWidth(charset?: string): number {
|
|
|
56
70
|
* @param {object|null} options Connection options object (may be falsy).
|
|
57
71
|
* @returns {string} A Node.js-compatible encoding string.
|
|
58
72
|
*/
|
|
59
|
-
function resolveTextEncoding(options?: any): BufferEncoding {
|
|
73
|
+
export function resolveTextEncoding(options?: any): BufferEncoding {
|
|
60
74
|
const encoding = (options && options.encoding)
|
|
61
75
|
? options.encoding.toUpperCase()
|
|
62
76
|
: Const.DEFAULT_ENCODING;
|
|
63
77
|
return (FirebirdToNodeEncoding[encoding] || Const.DEFAULT_ENCODING.toLowerCase()) as BufferEncoding;
|
|
64
78
|
}
|
|
65
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
|
+
|
|
66
152
|
//------------------------------------------------------
|
|
67
153
|
|
|
68
154
|
/**
|
|
@@ -86,6 +172,9 @@ export abstract class SQLVarBase {
|
|
|
86
172
|
owner?: string;
|
|
87
173
|
charSetId?: number;
|
|
88
174
|
collationId?: number;
|
|
175
|
+
/** Original declared byte length when scaleOutputLengths widened
|
|
176
|
+
* `length` for the fetch capacity check (issue #422). */
|
|
177
|
+
nativeLength?: number;
|
|
89
178
|
|
|
90
179
|
abstract decode(data: XdrReader, lowerV13: boolean, options?: any): any;
|
|
91
180
|
abstract calcBlr(blr: BlrWriter): void;
|
|
@@ -117,13 +206,17 @@ export interface ColumnKey {
|
|
|
117
206
|
export function computeColumnKeys(
|
|
118
207
|
output: SQLVarBase[],
|
|
119
208
|
nestTables: boolean | string | undefined,
|
|
120
|
-
lowercaseKeys: boolean | undefined
|
|
209
|
+
lowercaseKeys: boolean | undefined,
|
|
210
|
+
transform?: (key: string) => string
|
|
121
211
|
): ColumnKey[] {
|
|
122
212
|
return output.map((column) => {
|
|
123
213
|
let key = column.alias || '';
|
|
124
214
|
if (lowercaseKeys) {
|
|
125
215
|
key = key.toLowerCase();
|
|
126
216
|
}
|
|
217
|
+
if (transform) {
|
|
218
|
+
key = transform(key);
|
|
219
|
+
}
|
|
127
220
|
if (nestTables !== true && typeof nestTables !== 'string') {
|
|
128
221
|
return { key };
|
|
129
222
|
}
|
|
@@ -131,6 +224,9 @@ export function computeColumnKeys(
|
|
|
131
224
|
if (lowercaseKeys) {
|
|
132
225
|
table = table.toLowerCase();
|
|
133
226
|
}
|
|
227
|
+
if (transform) {
|
|
228
|
+
table = transform(table);
|
|
229
|
+
}
|
|
134
230
|
if (nestTables === true) {
|
|
135
231
|
return { table, key };
|
|
136
232
|
}
|
|
@@ -138,6 +234,67 @@ export function computeColumnKeys(
|
|
|
138
234
|
});
|
|
139
235
|
}
|
|
140
236
|
|
|
237
|
+
/** transformKeys option value: the built-in 'camel', or a custom mapper. */
|
|
238
|
+
export type KeyTransform = 'camel' | ((key: string) => string);
|
|
239
|
+
|
|
240
|
+
/** FIRST_NAME → firstName (the transformKeys: 'camel' built-in). */
|
|
241
|
+
export function camelizeKey(key: string): string {
|
|
242
|
+
const parts = String(key).toLowerCase().split('_');
|
|
243
|
+
let out = parts[0] || '';
|
|
244
|
+
for (let i = 1; i < parts.length; i++) {
|
|
245
|
+
const part = parts[i];
|
|
246
|
+
if (part) {
|
|
247
|
+
out += part.charAt(0).toUpperCase() + part.slice(1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolve the effective transformKeys value (per-query wins over the
|
|
255
|
+
* connection option) into a callable mapper, or undefined when off.
|
|
256
|
+
* A custom mapper is guarded like the typeCast hook: a throw inside the
|
|
257
|
+
* row-decode loop would be mistaken for an incomplete packet and desync
|
|
258
|
+
* the response queue, so failures fall back to the untransformed key.
|
|
259
|
+
*/
|
|
260
|
+
export function resolveKeyTransform(
|
|
261
|
+
queryOptions: { transformKeys?: KeyTransform } | undefined,
|
|
262
|
+
connectionOptions: { transformKeys?: KeyTransform } | undefined
|
|
263
|
+
): ((key: string) => string) | undefined {
|
|
264
|
+
const value = resolveQueryOption<KeyTransform>('transformKeys', queryOptions, connectionOptions);
|
|
265
|
+
if (value === 'camel') {
|
|
266
|
+
return camelizeKey;
|
|
267
|
+
}
|
|
268
|
+
if (typeof value !== 'function') {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
return (key: string) => {
|
|
272
|
+
try {
|
|
273
|
+
return String(value(key));
|
|
274
|
+
} catch (err: any) {
|
|
275
|
+
console.warn('[node-firebird] transformKeys mapper threw for key "%s": %s — using the untransformed key',
|
|
276
|
+
key, err && err.message);
|
|
277
|
+
return key;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Shared precedence rule for per-query-overridable connection options:
|
|
284
|
+
* the per-query value wins whenever it is present (even if falsy), the
|
|
285
|
+
* connection option applies otherwise.
|
|
286
|
+
*/
|
|
287
|
+
function resolveQueryOption<T>(
|
|
288
|
+
name: string,
|
|
289
|
+
queryOptions: Record<string, any> | undefined,
|
|
290
|
+
connectionOptions: Record<string, any> | undefined
|
|
291
|
+
): T | undefined {
|
|
292
|
+
if (queryOptions && queryOptions[name] !== undefined) {
|
|
293
|
+
return queryOptions[name];
|
|
294
|
+
}
|
|
295
|
+
return connectionOptions ? connectionOptions[name] : undefined;
|
|
296
|
+
}
|
|
297
|
+
|
|
141
298
|
/**
|
|
142
299
|
* Resolve the effective nestTables value: the per-query option wins over
|
|
143
300
|
* the connection option. The decoder and fetchBlobSyncRow both use this —
|
|
@@ -148,10 +305,7 @@ export function resolveNestTables(
|
|
|
148
305
|
queryOptions: { nestTables?: boolean | string } | undefined,
|
|
149
306
|
connectionOptions: { nestTables?: boolean | string } | undefined
|
|
150
307
|
): boolean | string | undefined {
|
|
151
|
-
|
|
152
|
-
return queryOptions.nestTables;
|
|
153
|
-
}
|
|
154
|
-
return connectionOptions && connectionOptions.nestTables;
|
|
308
|
+
return resolveQueryOption('nestTables', queryOptions, connectionOptions);
|
|
155
309
|
}
|
|
156
310
|
|
|
157
311
|
/**
|
|
@@ -169,25 +323,115 @@ export function nestCell(row: any, table: string | undefined) {
|
|
|
169
323
|
|
|
170
324
|
//------------------------------------------------------
|
|
171
325
|
|
|
326
|
+
/** Human-readable names for the SQL_* wire type codes. */
|
|
327
|
+
export const SQL_TYPE_NAMES: Record<number, string> = {
|
|
328
|
+
[Const.SQL_TEXT]: 'TEXT',
|
|
329
|
+
[Const.SQL_VARYING]: 'VARYING',
|
|
330
|
+
[Const.SQL_SHORT]: 'SHORT',
|
|
331
|
+
[Const.SQL_LONG]: 'LONG',
|
|
332
|
+
[Const.SQL_FLOAT]: 'FLOAT',
|
|
333
|
+
[Const.SQL_DOUBLE]: 'DOUBLE',
|
|
334
|
+
[Const.SQL_D_FLOAT]: 'D_FLOAT',
|
|
335
|
+
[Const.SQL_TIMESTAMP]: 'TIMESTAMP',
|
|
336
|
+
[Const.SQL_BLOB]: 'BLOB',
|
|
337
|
+
[Const.SQL_ARRAY]: 'ARRAY',
|
|
338
|
+
[Const.SQL_QUAD]: 'QUAD',
|
|
339
|
+
[Const.SQL_TYPE_TIME]: 'TIME',
|
|
340
|
+
[Const.SQL_TYPE_DATE]: 'DATE',
|
|
341
|
+
[Const.SQL_INT64]: 'INT64',
|
|
342
|
+
[Const.SQL_INT128]: 'INT128',
|
|
343
|
+
[Const.SQL_TIMESTAMP_TZ]: 'TIMESTAMP_TZ',
|
|
344
|
+
[Const.SQL_TIMESTAMP_TZ_EX]: 'TIMESTAMP_TZ_EX',
|
|
345
|
+
[Const.SQL_TIME_TZ]: 'TIME_TZ',
|
|
346
|
+
[Const.SQL_TIME_TZ_EX]: 'TIME_TZ_EX',
|
|
347
|
+
[Const.SQL_DEC16]: 'DEC16',
|
|
348
|
+
[Const.SQL_DEC34]: 'DEC34',
|
|
349
|
+
[Const.SQL_BOOLEAN]: 'BOOLEAN',
|
|
350
|
+
[Const.SQL_NULL]: 'NULL',
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Public column-metadata shape for one output descriptor: the vocabulary
|
|
355
|
+
* both the typeCast hook and withMeta `fields` deliver. Keep the two in
|
|
356
|
+
* lockstep by building both through here.
|
|
357
|
+
*/
|
|
358
|
+
export function describeField(meta: Partial<SQLVarBase>) {
|
|
359
|
+
return {
|
|
360
|
+
type: meta.type!,
|
|
361
|
+
typeName: SQL_TYPE_NAMES[meta.type!] || 'UNKNOWN',
|
|
362
|
+
subType: meta.subType,
|
|
363
|
+
scale: meta.scale,
|
|
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,
|
|
367
|
+
nullable: meta.nullable,
|
|
368
|
+
field: meta.field,
|
|
369
|
+
relation: meta.relation,
|
|
370
|
+
relationAlias: meta.relationAlias,
|
|
371
|
+
relationSchema: meta.relationSchema,
|
|
372
|
+
alias: meta.alias,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Map a statement's output descriptors to the column-metadata array
|
|
378
|
+
* delivered in withMeta results ({ rows, fields, ... }).
|
|
379
|
+
*/
|
|
380
|
+
export function describeFields(output: SQLVarBase[]) {
|
|
381
|
+
return (output || []).map(describeField);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Parse the op_info_sql response buffer of a Const.RECORDS_INFO request
|
|
386
|
+
* into per-verb row counts. The buffer holds an isc_info_sql_records
|
|
387
|
+
* cluster (2-byte total length, then nested isc_info_req_*_count items,
|
|
388
|
+
* each 2-byte length + little-endian integer) terminated by isc_info_end.
|
|
389
|
+
*/
|
|
390
|
+
export function parseRecordCounts(buffer: Buffer | undefined): RecordCounts {
|
|
391
|
+
const counts = { selectCount: 0, insertCount: 0, updateCount: 0, deleteCount: 0 };
|
|
392
|
+
if (!buffer || !buffer.length) {
|
|
393
|
+
return counts;
|
|
394
|
+
}
|
|
395
|
+
// this runs inside a response callback — a malformed/truncated buffer
|
|
396
|
+
// must yield partial counts, never a throw
|
|
397
|
+
try {
|
|
398
|
+
const br = new BlrReader(buffer);
|
|
399
|
+
while (br.pos < br.buffer.length) {
|
|
400
|
+
const item = br.readByteCode();
|
|
401
|
+
if (item === Const.isc_info_end || item === Const.isc_info_truncated) {
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
if (item === Const.isc_info_sql_records) {
|
|
405
|
+
br.pos += 2; // skip the cluster's total length; nested items follow
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
switch (item) {
|
|
409
|
+
case Const.isc_info_req_select_count: counts.selectCount = br.readInt() || 0; break;
|
|
410
|
+
case Const.isc_info_req_insert_count: counts.insertCount = br.readInt() || 0; break;
|
|
411
|
+
case Const.isc_info_req_update_count: counts.updateCount = br.readInt() || 0; break;
|
|
412
|
+
case Const.isc_info_req_delete_count: counts.deleteCount = br.readInt() || 0; break;
|
|
413
|
+
default:
|
|
414
|
+
// unknown item: its 2-byte length prefix tells us how far to skip
|
|
415
|
+
br.pos += 2 + br.buffer.readUInt16LE(br.pos);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} catch (e) {
|
|
419
|
+
// fall through with whatever was parsed so far
|
|
420
|
+
}
|
|
421
|
+
return counts;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
//------------------------------------------------------
|
|
425
|
+
|
|
172
426
|
export class SQLVarText extends SQLVarBase {
|
|
173
427
|
decode(data: XdrReader, lowerV13: boolean, options?: any) {
|
|
174
428
|
let ret;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
const charLength = Math.floor(this.length / width);
|
|
182
|
-
if (ret.length > charLength) {
|
|
183
|
-
ret = ret.substring(0, charLength);
|
|
184
|
-
}
|
|
185
|
-
} else if (this.subType === 0) {
|
|
186
|
-
// without charset definition
|
|
187
|
-
ret = data.readText(this.length, textEncoding);
|
|
188
|
-
const encoding = options && options.encoding ? options.encoding : 'UTF8';
|
|
189
|
-
const width = getFirebirdCharsetWidth(encoding);
|
|
190
|
-
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);
|
|
191
435
|
if (ret.length > charLength) {
|
|
192
436
|
ret = ret.substring(0, charLength);
|
|
193
437
|
}
|
|
@@ -218,13 +462,11 @@ export class SQLVarNull extends SQLVarText {
|
|
|
218
462
|
export class SQLVarString extends SQLVarBase {
|
|
219
463
|
decode(data: XdrReader, lowerV13: boolean, options?: any) {
|
|
220
464
|
let ret;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// without charset definition
|
|
227
|
-
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);
|
|
228
470
|
} else {
|
|
229
471
|
ret = data.readBuffer();
|
|
230
472
|
}
|
|
@@ -872,12 +1114,30 @@ export class SQLParamDate {
|
|
|
872
1114
|
|
|
873
1115
|
export class SQLParamBool {
|
|
874
1116
|
value: any;
|
|
875
|
-
|
|
876
|
-
|
|
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) {
|
|
877
1128
|
this.value = value;
|
|
1129
|
+
this.asBoolean = asBoolean;
|
|
878
1130
|
}
|
|
879
1131
|
|
|
880
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
|
+
}
|
|
881
1141
|
if (this.value != null) {
|
|
882
1142
|
data.addInt(this.value ? 1 : 0);
|
|
883
1143
|
} else {
|
|
@@ -887,6 +1147,10 @@ export class SQLParamBool {
|
|
|
887
1147
|
}
|
|
888
1148
|
|
|
889
1149
|
calcBlr(blr: BlrWriter): void {
|
|
1150
|
+
if (this.asBoolean) {
|
|
1151
|
+
blr.addByte(Const.blr_bool);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
890
1154
|
blr.addByte(Const.blr_short);
|
|
891
1155
|
blr.addShort(0);
|
|
892
1156
|
}
|