node-firebird 2.11.0 → 2.12.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 +191 -3
- 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 +114 -0
- package/lib/uri.js +53 -2
- package/lib/wire/connection.d.ts +6 -0
- package/lib/wire/connection.js +50 -36
- package/lib/wire/const.d.ts +5 -0
- package/lib/wire/const.js +12 -0
- package/lib/wire/database.d.ts +9 -0
- package/lib/wire/database.js +27 -7
- package/lib/wire/serialize.js +4 -0
- package/lib/wire/transaction.d.ts +27 -0
- package/lib/wire/transaction.js +131 -13
- package/lib/wire/xsqlvar.d.ts +62 -1
- package/lib/wire/xsqlvar.js +165 -6
- package/package.json +1 -1
- package/src/pool.ts +57 -14
- package/src/sql-template.ts +196 -0
- package/src/types.ts +113 -1
- package/src/uri.ts +54 -2
- package/src/wire/connection.ts +59 -37
- package/src/wire/const.ts +13 -0
- package/src/wire/database.ts +31 -8
- package/src/wire/serialize.ts +5 -1
- package/src/wire/transaction.ts +140 -19
- package/src/wire/xsqlvar.ts +170 -5
package/src/wire/database.ts
CHANGED
|
@@ -2,7 +2,8 @@ import Events from 'events';
|
|
|
2
2
|
import { doError, fromCallback, type Callback, type SimpleCallback } from '../callback';
|
|
3
3
|
import { escape } from '../utils';
|
|
4
4
|
import Const from './const';
|
|
5
|
-
import {
|
|
5
|
+
import { makeSqlTag, type SqlTag } from '../sql-template';
|
|
6
|
+
import { computeColumnKeys, nestCell, resolveKeyTransform, resolveNestTables } from './xsqlvar';
|
|
6
7
|
import EventConnection from './eventConnection';
|
|
7
8
|
import FbEventManager from './fbEventManager';
|
|
8
9
|
import makeQueryStream from './query-stream';
|
|
@@ -95,7 +96,7 @@ function readblob(blob: any, callback: Callback): void {
|
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
function fetchBlobSyncRow(row: any, meta: any[], nestTables: boolean | string | undefined, lowercaseKeys: boolean | undefined, callback: Callback): void {
|
|
99
|
+
function fetchBlobSyncRow(row: any, meta: any[], nestTables: boolean | string | undefined, lowercaseKeys: boolean | undefined, transform: ((key: string) => string) | undefined, callback: Callback): void {
|
|
99
100
|
if (!row || !meta || !meta.length || !meta.some((m) => m && m.type === Const.SQL_BLOB)) {
|
|
100
101
|
callback(null, row);
|
|
101
102
|
return;
|
|
@@ -106,7 +107,7 @@ function fetchBlobSyncRow(row: any, meta: any[], nestTables: boolean | string |
|
|
|
106
107
|
// duplicate JOIN column names (and nested rows) break that alignment.
|
|
107
108
|
// Array rows (sequentially's legacy boolean form) are keyed by index.
|
|
108
109
|
const isArrayRow = Array.isArray(row);
|
|
109
|
-
const keys = isArrayRow ? null : computeColumnKeys(meta, nestTables, lowercaseKeys);
|
|
110
|
+
const keys = isArrayRow ? null : computeColumnKeys(meta, nestTables, lowercaseKeys, transform);
|
|
110
111
|
const blobCells: { target: any; key: string | number }[] = [];
|
|
111
112
|
|
|
112
113
|
for (let i = 0; i < meta.length; i++) {
|
|
@@ -147,6 +148,7 @@ function fetchBlobSyncRow(row: any, meta: any[], nestTables: boolean | string |
|
|
|
147
148
|
class Database extends Events.EventEmitter {
|
|
148
149
|
connection: Connection;
|
|
149
150
|
eventid: number;
|
|
151
|
+
private _sql?: SqlTag;
|
|
150
152
|
|
|
151
153
|
constructor(connection: Connection) {
|
|
152
154
|
super();
|
|
@@ -155,6 +157,17 @@ class Database extends Events.EventEmitter {
|
|
|
155
157
|
this.eventid = 1;
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Tagged-template query API: db.sql`SELECT ... ${value}` (see README).
|
|
162
|
+
* Built lazily on first access; the compiled text is positional-only,
|
|
163
|
+
* so the namedPlaceholders rewriter is disabled — any `:token` in the
|
|
164
|
+
* template is PSQL (EXECUTE BLOCK), not a placeholder.
|
|
165
|
+
*/
|
|
166
|
+
get sql(): SqlTag {
|
|
167
|
+
return this._sql || (this._sql = makeSqlTag((text, params, options) =>
|
|
168
|
+
this.queryAsync(text, params, { ...options, namedPlaceholders: false })));
|
|
169
|
+
}
|
|
170
|
+
|
|
158
171
|
escape(value: any): string {
|
|
159
172
|
return escape(value, this.connection.accept.protocolVersion);
|
|
160
173
|
}
|
|
@@ -325,6 +338,9 @@ class Database extends Events.EventEmitter {
|
|
|
325
338
|
}
|
|
326
339
|
|
|
327
340
|
var self = this;
|
|
341
|
+
var keyResolutionDone = false;
|
|
342
|
+
var resolvedNest: boolean | string | undefined;
|
|
343
|
+
var resolvedTransform: ((key: string) => string) | undefined;
|
|
328
344
|
var _on = function(row: any, i: number, meta: any[], next: (err?: any) => void) {
|
|
329
345
|
var done = false;
|
|
330
346
|
var finish = function(err?: any) {
|
|
@@ -335,9 +351,15 @@ class Database extends Events.EventEmitter {
|
|
|
335
351
|
next(err);
|
|
336
352
|
};
|
|
337
353
|
|
|
338
|
-
// options is read at call time, after the normalization below
|
|
339
|
-
|
|
340
|
-
|
|
354
|
+
// options is read at call time, after the normalization below;
|
|
355
|
+
// both values are query-invariant, so resolve them once on the
|
|
356
|
+
// first row instead of allocating per row
|
|
357
|
+
if (!keyResolutionDone) {
|
|
358
|
+
resolvedNest = resolveNestTables(options as any, self.connection.options);
|
|
359
|
+
resolvedTransform = resolveKeyTransform(options as any, self.connection.options);
|
|
360
|
+
keyResolutionDone = true;
|
|
361
|
+
}
|
|
362
|
+
fetchBlobSyncRow(row, meta, resolvedNest, self.connection._lowercase_keys, resolvedTransform, function(blobErr: any) {
|
|
341
363
|
if (blobErr) {
|
|
342
364
|
finish(blobErr);
|
|
343
365
|
return;
|
|
@@ -552,8 +574,9 @@ class Database extends Events.EventEmitter {
|
|
|
552
574
|
/*
|
|
553
575
|
* Promise / async-await API.
|
|
554
576
|
* Each *Async method wraps its callback counterpart; the callback API
|
|
555
|
-
* stays untouched.
|
|
556
|
-
*
|
|
577
|
+
* stays untouched. The promises resolve with the rows alone unless
|
|
578
|
+
* { withMeta: true } is passed, which resolves the full
|
|
579
|
+
* { rows, fields, affectedRows, recordCounts, warnings } result.
|
|
557
580
|
*/
|
|
558
581
|
|
|
559
582
|
queryAsync(query: string, params?: QueryParams, options?: InternalQueryOptions): Promise<any[]> {
|
package/src/wire/serialize.ts
CHANGED
|
@@ -197,7 +197,11 @@ export class BlrReader {
|
|
|
197
197
|
value = this.buffer.readInt16LE(this.pos);
|
|
198
198
|
break;
|
|
199
199
|
case 4:
|
|
200
|
-
value = this.buffer.readInt32LE(this.pos)
|
|
200
|
+
value = this.buffer.readInt32LE(this.pos);
|
|
201
|
+
break;
|
|
202
|
+
case 8:
|
|
203
|
+
// e.g. record counts above 2^31 (isc_info_sql_records)
|
|
204
|
+
value = Number(this.buffer.readBigInt64LE(this.pos));
|
|
201
205
|
}
|
|
202
206
|
this.pos += len;
|
|
203
207
|
return value;
|
package/src/wire/transaction.ts
CHANGED
|
@@ -2,12 +2,14 @@ import { doCallback, doError, fromCallback, type Callback, type SimpleCallback }
|
|
|
2
2
|
import { parseNamedPlaceholders } from '../named-params';
|
|
3
3
|
import { noop } from '../utils';
|
|
4
4
|
import Const from './const';
|
|
5
|
+
import { makeSqlTag, type SqlTag } from '../sql-template';
|
|
6
|
+
import { describeFields, parseRecordCounts } from './xsqlvar';
|
|
5
7
|
import makeQueryStream from './query-stream';
|
|
6
8
|
import type Connection from './connection';
|
|
7
9
|
import type Database from './database';
|
|
8
10
|
import type Statement from './statement';
|
|
9
11
|
import type { BatchCb, StatementCb, InternalQueryOptions } from './wire-types';
|
|
10
|
-
import type { BatchOptions, BatchResult, QueryOptions, QueryParams, QueryStreamOptions, SequentialCallback } from '../types';
|
|
12
|
+
import type { BatchOptions, BatchResult, QueryOptions, QueryParams, QueryStreamOptions, RecordCounts, SequentialCallback } from '../types';
|
|
11
13
|
|
|
12
14
|
/***************************************
|
|
13
15
|
*
|
|
@@ -53,11 +55,80 @@ class Transaction {
|
|
|
53
55
|
// populated externally from the op_transaction response
|
|
54
56
|
handle!: number;
|
|
55
57
|
|
|
58
|
+
private _sql?: SqlTag;
|
|
59
|
+
|
|
56
60
|
constructor(connection: Connection) {
|
|
57
61
|
this.connection = connection;
|
|
58
62
|
this.db = connection.db;
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Tagged-template query API: tx.sql`SELECT ... ${value}` (see README).
|
|
67
|
+
* Built lazily — transactions are created per-query internally, and
|
|
68
|
+
* those throwaway instances must not pay for the tag. The compiled text
|
|
69
|
+
* is positional-only, so the namedPlaceholders rewriter is disabled:
|
|
70
|
+
* any `:token` in the template is PSQL (EXECUTE BLOCK), not a
|
|
71
|
+
* placeholder.
|
|
72
|
+
*/
|
|
73
|
+
get sql(): SqlTag {
|
|
74
|
+
return this._sql || (this._sql = makeSqlTag((text, params, options) =>
|
|
75
|
+
this.queryAsync(text, params, { ...options, namedPlaceholders: false })));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Current savepoint nesting depth (names savepoints, see savepoint()). */
|
|
79
|
+
private _savepointDepth = 0;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run `work` inside a savepoint (Firebird 1.5+): on resolve the
|
|
83
|
+
* savepoint is released, on reject the transaction rolls back TO the
|
|
84
|
+
* savepoint — undoing only work's changes — and the error is rethrown,
|
|
85
|
+
* leaving the transaction itself usable. Nestable (each call generates
|
|
86
|
+
* a fresh NF_SP_n name), mirroring db.withTransaction's style and
|
|
87
|
+
* Postgres.js's sql.savepoint().
|
|
88
|
+
*
|
|
89
|
+
* Do NOT run sibling savepoints concurrently on one transaction
|
|
90
|
+
* (Promise.all): Firebird's RELEASE SAVEPOINT also releases every
|
|
91
|
+
* savepoint created after it, so interleaved siblings release each
|
|
92
|
+
* other. Nested (awaited) savepoints are fine.
|
|
93
|
+
*/
|
|
94
|
+
async savepoint<T>(work: (transaction: this) => Promise<T> | T): Promise<T> {
|
|
95
|
+
if (typeof work !== 'function') {
|
|
96
|
+
throw new Error('savepoint(work) expects a function');
|
|
97
|
+
}
|
|
98
|
+
// named by nesting depth, not a global counter: sequential
|
|
99
|
+
// savepoints at the same depth reuse the same three SQL strings, so
|
|
100
|
+
// the statement cache serves them instead of accumulating
|
|
101
|
+
// single-use entries (redefining a released savepoint name is legal)
|
|
102
|
+
const name = 'NF_SP_' + (++this._savepointDepth);
|
|
103
|
+
try {
|
|
104
|
+
await this.queryAsync('SAVEPOINT ' + name);
|
|
105
|
+
|
|
106
|
+
let result: T;
|
|
107
|
+
try {
|
|
108
|
+
result = await work(this);
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
// only a work() failure rolls back to the savepoint — a
|
|
111
|
+
// RELEASE failure below must NOT undo work's successful
|
|
112
|
+
// changes
|
|
113
|
+
try {
|
|
114
|
+
await this.queryAsync('ROLLBACK TO SAVEPOINT ' + name);
|
|
115
|
+
} catch (rollbackErr: any) {
|
|
116
|
+
// the original failure matters more; keep the rollback
|
|
117
|
+
// failure attached for diagnosis
|
|
118
|
+
if (err && typeof err === 'object') {
|
|
119
|
+
err.savepointRollbackError = rollbackErr;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await this.queryAsync('RELEASE SAVEPOINT ' + name);
|
|
126
|
+
return result;
|
|
127
|
+
} finally {
|
|
128
|
+
this._savepointDepth--;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
61
132
|
/** Per-call options.namedPlaceholders overrides the connection option. */
|
|
62
133
|
private namedPlaceholdersEnabled(options?: InternalQueryOptions): boolean {
|
|
63
134
|
if (options && options.namedPlaceholders !== undefined)
|
|
@@ -138,6 +209,69 @@ class Transaction {
|
|
|
138
209
|
return;
|
|
139
210
|
}
|
|
140
211
|
|
|
212
|
+
// withMeta applies to query/execute only: in streaming mode
|
|
213
|
+
// (sequentially/queryStream, which spread user options) rows
|
|
214
|
+
// bypass fetchAll's array, so a result object here would
|
|
215
|
+
// carry rows: [] and a meaningless affectedRows
|
|
216
|
+
var withMeta = Boolean(options && typeof options === 'object' &&
|
|
217
|
+
(options as any).withMeta && !(options as any).asStream);
|
|
218
|
+
|
|
219
|
+
// Deliver the historic result shape, or — when options.withMeta
|
|
220
|
+
// is set — request the per-verb DML row counts while the
|
|
221
|
+
// statement handle is still open and wrap everything in a
|
|
222
|
+
// { rows, fields, affectedRows, recordCounts, warnings } object.
|
|
223
|
+
function deliver(rows: any, isSelect: boolean, plainDml?: boolean) {
|
|
224
|
+
if (!withMeta) {
|
|
225
|
+
statement!.release();
|
|
226
|
+
if (callback) {
|
|
227
|
+
if (plainDml) {
|
|
228
|
+
// plain DML historically calls back with no args
|
|
229
|
+
callback();
|
|
230
|
+
} else {
|
|
231
|
+
callback(undefined, rows, statement!.output, isSelect);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
var execWarnings = (ret && ret.warnings) || [];
|
|
238
|
+
var finalize = function(counts?: RecordCounts) {
|
|
239
|
+
statement!.release();
|
|
240
|
+
if (!callback) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// DML: what the server actually changed; SELECT: rows
|
|
244
|
+
// returned (pg's rowCount convention)
|
|
245
|
+
var affectedRows = counts
|
|
246
|
+
? counts.insertCount + counts.updateCount + counts.deleteCount
|
|
247
|
+
: (Array.isArray(rows) ? rows.length : (rows !== undefined ? 1 : 0));
|
|
248
|
+
callback(undefined, {
|
|
249
|
+
rows: rows,
|
|
250
|
+
fields: describeFields(statement!.output),
|
|
251
|
+
affectedRows: affectedRows,
|
|
252
|
+
recordCounts: counts,
|
|
253
|
+
warnings: execWarnings,
|
|
254
|
+
}, statement!.output, isSelect);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
var t = statement!.type;
|
|
258
|
+
var isDml = t === Const.isc_info_sql_stmt_insert ||
|
|
259
|
+
t === Const.isc_info_sql_stmt_update ||
|
|
260
|
+
t === Const.isc_info_sql_stmt_delete ||
|
|
261
|
+
t === Const.isc_info_sql_stmt_exec_procedure;
|
|
262
|
+
if (!isDml) {
|
|
263
|
+
finalize();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
self.connection.statementInfo(statement!, Const.RECORDS_INFO, function(err: any, info: any) {
|
|
267
|
+
if (err) {
|
|
268
|
+
dropError(err);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
finalize(parseRecordCounts(info && info.buffer));
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
141
275
|
switch (statement.type) {
|
|
142
276
|
case Const.isc_info_sql_stmt_select:
|
|
143
277
|
statement.fetchAll(self, function(err: any, r: any) {
|
|
@@ -146,34 +280,23 @@ class Transaction {
|
|
|
146
280
|
return;
|
|
147
281
|
}
|
|
148
282
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (callback)
|
|
152
|
-
callback(undefined, r, statement.output, true);
|
|
153
|
-
|
|
283
|
+
deliver(r, true);
|
|
154
284
|
});
|
|
155
285
|
|
|
156
286
|
break;
|
|
157
287
|
|
|
158
288
|
case Const.isc_info_sql_stmt_exec_procedure:
|
|
159
289
|
if (ret && ret.data && ret.data.length > 0) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (callback)
|
|
163
|
-
callback(undefined, ret.data[0], statement.output, true);
|
|
164
|
-
|
|
290
|
+
deliver(ret.data[0], true);
|
|
165
291
|
break;
|
|
166
292
|
} else if (statement.output.length) {
|
|
167
|
-
statement.fetch(self, 1, function(err: any,
|
|
293
|
+
statement.fetch(self, 1, function(err: any, fret: any) {
|
|
168
294
|
if (err) {
|
|
169
295
|
dropError(err);
|
|
170
296
|
return;
|
|
171
297
|
}
|
|
172
298
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (callback)
|
|
176
|
-
callback(undefined, ret.data[0], statement.output, false);
|
|
299
|
+
deliver(fret.data[0], false);
|
|
177
300
|
});
|
|
178
301
|
|
|
179
302
|
break;
|
|
@@ -181,9 +304,7 @@ class Transaction {
|
|
|
181
304
|
|
|
182
305
|
// Fall through is normal
|
|
183
306
|
default:
|
|
184
|
-
|
|
185
|
-
if (callback)
|
|
186
|
-
callback()
|
|
307
|
+
deliver(undefined, false, true);
|
|
187
308
|
break;
|
|
188
309
|
}
|
|
189
310
|
|
package/src/wire/xsqlvar.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import Const from './const';
|
|
2
|
+
import { BlrReader } from './serialize';
|
|
2
3
|
import type { XdrReader, XdrWriter, BlrWriter } from './serialize';
|
|
4
|
+
import type { RecordCounts } from '../types';
|
|
3
5
|
|
|
4
6
|
/***************************************
|
|
5
7
|
*
|
|
@@ -117,13 +119,17 @@ export interface ColumnKey {
|
|
|
117
119
|
export function computeColumnKeys(
|
|
118
120
|
output: SQLVarBase[],
|
|
119
121
|
nestTables: boolean | string | undefined,
|
|
120
|
-
lowercaseKeys: boolean | undefined
|
|
122
|
+
lowercaseKeys: boolean | undefined,
|
|
123
|
+
transform?: (key: string) => string
|
|
121
124
|
): ColumnKey[] {
|
|
122
125
|
return output.map((column) => {
|
|
123
126
|
let key = column.alias || '';
|
|
124
127
|
if (lowercaseKeys) {
|
|
125
128
|
key = key.toLowerCase();
|
|
126
129
|
}
|
|
130
|
+
if (transform) {
|
|
131
|
+
key = transform(key);
|
|
132
|
+
}
|
|
127
133
|
if (nestTables !== true && typeof nestTables !== 'string') {
|
|
128
134
|
return { key };
|
|
129
135
|
}
|
|
@@ -131,6 +137,9 @@ export function computeColumnKeys(
|
|
|
131
137
|
if (lowercaseKeys) {
|
|
132
138
|
table = table.toLowerCase();
|
|
133
139
|
}
|
|
140
|
+
if (transform) {
|
|
141
|
+
table = transform(table);
|
|
142
|
+
}
|
|
134
143
|
if (nestTables === true) {
|
|
135
144
|
return { table, key };
|
|
136
145
|
}
|
|
@@ -138,6 +147,67 @@ export function computeColumnKeys(
|
|
|
138
147
|
});
|
|
139
148
|
}
|
|
140
149
|
|
|
150
|
+
/** transformKeys option value: the built-in 'camel', or a custom mapper. */
|
|
151
|
+
export type KeyTransform = 'camel' | ((key: string) => string);
|
|
152
|
+
|
|
153
|
+
/** FIRST_NAME → firstName (the transformKeys: 'camel' built-in). */
|
|
154
|
+
export function camelizeKey(key: string): string {
|
|
155
|
+
const parts = String(key).toLowerCase().split('_');
|
|
156
|
+
let out = parts[0] || '';
|
|
157
|
+
for (let i = 1; i < parts.length; i++) {
|
|
158
|
+
const part = parts[i];
|
|
159
|
+
if (part) {
|
|
160
|
+
out += part.charAt(0).toUpperCase() + part.slice(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolve the effective transformKeys value (per-query wins over the
|
|
168
|
+
* connection option) into a callable mapper, or undefined when off.
|
|
169
|
+
* A custom mapper is guarded like the typeCast hook: a throw inside the
|
|
170
|
+
* row-decode loop would be mistaken for an incomplete packet and desync
|
|
171
|
+
* the response queue, so failures fall back to the untransformed key.
|
|
172
|
+
*/
|
|
173
|
+
export function resolveKeyTransform(
|
|
174
|
+
queryOptions: { transformKeys?: KeyTransform } | undefined,
|
|
175
|
+
connectionOptions: { transformKeys?: KeyTransform } | undefined
|
|
176
|
+
): ((key: string) => string) | undefined {
|
|
177
|
+
const value = resolveQueryOption<KeyTransform>('transformKeys', queryOptions, connectionOptions);
|
|
178
|
+
if (value === 'camel') {
|
|
179
|
+
return camelizeKey;
|
|
180
|
+
}
|
|
181
|
+
if (typeof value !== 'function') {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
return (key: string) => {
|
|
185
|
+
try {
|
|
186
|
+
return String(value(key));
|
|
187
|
+
} catch (err: any) {
|
|
188
|
+
console.warn('[node-firebird] transformKeys mapper threw for key "%s": %s — using the untransformed key',
|
|
189
|
+
key, err && err.message);
|
|
190
|
+
return key;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Shared precedence rule for per-query-overridable connection options:
|
|
197
|
+
* the per-query value wins whenever it is present (even if falsy), the
|
|
198
|
+
* connection option applies otherwise.
|
|
199
|
+
*/
|
|
200
|
+
function resolveQueryOption<T>(
|
|
201
|
+
name: string,
|
|
202
|
+
queryOptions: Record<string, any> | undefined,
|
|
203
|
+
connectionOptions: Record<string, any> | undefined
|
|
204
|
+
): T | undefined {
|
|
205
|
+
if (queryOptions && queryOptions[name] !== undefined) {
|
|
206
|
+
return queryOptions[name];
|
|
207
|
+
}
|
|
208
|
+
return connectionOptions ? connectionOptions[name] : undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
141
211
|
/**
|
|
142
212
|
* Resolve the effective nestTables value: the per-query option wins over
|
|
143
213
|
* the connection option. The decoder and fetchBlobSyncRow both use this —
|
|
@@ -148,10 +218,7 @@ export function resolveNestTables(
|
|
|
148
218
|
queryOptions: { nestTables?: boolean | string } | undefined,
|
|
149
219
|
connectionOptions: { nestTables?: boolean | string } | undefined
|
|
150
220
|
): boolean | string | undefined {
|
|
151
|
-
|
|
152
|
-
return queryOptions.nestTables;
|
|
153
|
-
}
|
|
154
|
-
return connectionOptions && connectionOptions.nestTables;
|
|
221
|
+
return resolveQueryOption('nestTables', queryOptions, connectionOptions);
|
|
155
222
|
}
|
|
156
223
|
|
|
157
224
|
/**
|
|
@@ -169,6 +236,104 @@ export function nestCell(row: any, table: string | undefined) {
|
|
|
169
236
|
|
|
170
237
|
//------------------------------------------------------
|
|
171
238
|
|
|
239
|
+
/** Human-readable names for the SQL_* wire type codes. */
|
|
240
|
+
export const SQL_TYPE_NAMES: Record<number, string> = {
|
|
241
|
+
[Const.SQL_TEXT]: 'TEXT',
|
|
242
|
+
[Const.SQL_VARYING]: 'VARYING',
|
|
243
|
+
[Const.SQL_SHORT]: 'SHORT',
|
|
244
|
+
[Const.SQL_LONG]: 'LONG',
|
|
245
|
+
[Const.SQL_FLOAT]: 'FLOAT',
|
|
246
|
+
[Const.SQL_DOUBLE]: 'DOUBLE',
|
|
247
|
+
[Const.SQL_D_FLOAT]: 'D_FLOAT',
|
|
248
|
+
[Const.SQL_TIMESTAMP]: 'TIMESTAMP',
|
|
249
|
+
[Const.SQL_BLOB]: 'BLOB',
|
|
250
|
+
[Const.SQL_ARRAY]: 'ARRAY',
|
|
251
|
+
[Const.SQL_QUAD]: 'QUAD',
|
|
252
|
+
[Const.SQL_TYPE_TIME]: 'TIME',
|
|
253
|
+
[Const.SQL_TYPE_DATE]: 'DATE',
|
|
254
|
+
[Const.SQL_INT64]: 'INT64',
|
|
255
|
+
[Const.SQL_INT128]: 'INT128',
|
|
256
|
+
[Const.SQL_TIMESTAMP_TZ]: 'TIMESTAMP_TZ',
|
|
257
|
+
[Const.SQL_TIMESTAMP_TZ_EX]: 'TIMESTAMP_TZ_EX',
|
|
258
|
+
[Const.SQL_TIME_TZ]: 'TIME_TZ',
|
|
259
|
+
[Const.SQL_TIME_TZ_EX]: 'TIME_TZ_EX',
|
|
260
|
+
[Const.SQL_DEC16]: 'DEC16',
|
|
261
|
+
[Const.SQL_DEC34]: 'DEC34',
|
|
262
|
+
[Const.SQL_BOOLEAN]: 'BOOLEAN',
|
|
263
|
+
[Const.SQL_NULL]: 'NULL',
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Public column-metadata shape for one output descriptor: the vocabulary
|
|
268
|
+
* both the typeCast hook and withMeta `fields` deliver. Keep the two in
|
|
269
|
+
* lockstep by building both through here.
|
|
270
|
+
*/
|
|
271
|
+
export function describeField(meta: Partial<SQLVarBase>) {
|
|
272
|
+
return {
|
|
273
|
+
type: meta.type!,
|
|
274
|
+
typeName: SQL_TYPE_NAMES[meta.type!] || 'UNKNOWN',
|
|
275
|
+
subType: meta.subType,
|
|
276
|
+
scale: meta.scale,
|
|
277
|
+
length: meta.length,
|
|
278
|
+
nullable: meta.nullable,
|
|
279
|
+
field: meta.field,
|
|
280
|
+
relation: meta.relation,
|
|
281
|
+
relationAlias: meta.relationAlias,
|
|
282
|
+
relationSchema: meta.relationSchema,
|
|
283
|
+
alias: meta.alias,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Map a statement's output descriptors to the column-metadata array
|
|
289
|
+
* delivered in withMeta results ({ rows, fields, ... }).
|
|
290
|
+
*/
|
|
291
|
+
export function describeFields(output: SQLVarBase[]) {
|
|
292
|
+
return (output || []).map(describeField);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parse the op_info_sql response buffer of a Const.RECORDS_INFO request
|
|
297
|
+
* into per-verb row counts. The buffer holds an isc_info_sql_records
|
|
298
|
+
* cluster (2-byte total length, then nested isc_info_req_*_count items,
|
|
299
|
+
* each 2-byte length + little-endian integer) terminated by isc_info_end.
|
|
300
|
+
*/
|
|
301
|
+
export function parseRecordCounts(buffer: Buffer | undefined): RecordCounts {
|
|
302
|
+
const counts = { selectCount: 0, insertCount: 0, updateCount: 0, deleteCount: 0 };
|
|
303
|
+
if (!buffer || !buffer.length) {
|
|
304
|
+
return counts;
|
|
305
|
+
}
|
|
306
|
+
// this runs inside a response callback — a malformed/truncated buffer
|
|
307
|
+
// must yield partial counts, never a throw
|
|
308
|
+
try {
|
|
309
|
+
const br = new BlrReader(buffer);
|
|
310
|
+
while (br.pos < br.buffer.length) {
|
|
311
|
+
const item = br.readByteCode();
|
|
312
|
+
if (item === Const.isc_info_end || item === Const.isc_info_truncated) {
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (item === Const.isc_info_sql_records) {
|
|
316
|
+
br.pos += 2; // skip the cluster's total length; nested items follow
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
switch (item) {
|
|
320
|
+
case Const.isc_info_req_select_count: counts.selectCount = br.readInt() || 0; break;
|
|
321
|
+
case Const.isc_info_req_insert_count: counts.insertCount = br.readInt() || 0; break;
|
|
322
|
+
case Const.isc_info_req_update_count: counts.updateCount = br.readInt() || 0; break;
|
|
323
|
+
case Const.isc_info_req_delete_count: counts.deleteCount = br.readInt() || 0; break;
|
|
324
|
+
default:
|
|
325
|
+
// unknown item: its 2-byte length prefix tells us how far to skip
|
|
326
|
+
br.pos += 2 + br.buffer.readUInt16LE(br.pos);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (e) {
|
|
330
|
+
// fall through with whatever was parsed so far
|
|
331
|
+
}
|
|
332
|
+
return counts;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
//------------------------------------------------------
|
|
336
|
+
|
|
172
337
|
export class SQLVarText extends SQLVarBase {
|
|
173
338
|
decode(data: XdrReader, lowerV13: boolean, options?: any) {
|
|
174
339
|
let ret;
|