node-firebird 2.10.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 +236 -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 +136 -0
- package/lib/uri.js +53 -2
- package/lib/wire/connection.d.ts +7 -1
- package/lib/wire/connection.js +67 -46
- package/lib/wire/const.d.ts +5 -0
- package/lib/wire/const.js +14 -0
- package/lib/wire/database.d.ts +9 -0
- package/lib/wire/database.js +49 -14
- package/lib/wire/serialize.d.ts +3 -1
- 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 +100 -0
- package/lib/wire/xsqlvar.js +219 -1
- package/package.json +1 -1
- package/src/pool.ts +57 -14
- package/src/sql-template.ts +196 -0
- package/src/types.ts +135 -1
- package/src/uri.ts +54 -2
- package/src/wire/connection.ts +77 -46
- package/src/wire/const.ts +15 -0
- package/src/wire/database.ts +52 -14
- package/src/wire/serialize.ts +8 -2
- package/src/wire/transaction.ts +140 -19
- package/src/wire/xsqlvar.ts +241 -0
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
|
*
|
|
@@ -93,6 +95,245 @@ export abstract class SQLVarBase {
|
|
|
93
95
|
|
|
94
96
|
//------------------------------------------------------
|
|
95
97
|
|
|
98
|
+
/** Effective object-row key(s) of one output column (see computeColumnKeys). */
|
|
99
|
+
export interface ColumnKey {
|
|
100
|
+
/** Top-level table key when nestTables === true; undefined otherwise. */
|
|
101
|
+
table?: string;
|
|
102
|
+
/** Property key: the column alias, or 'table<sep>alias' in separator mode. */
|
|
103
|
+
key: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Compute the object-row property keys for a statement's output columns,
|
|
108
|
+
* honouring the nestTables and lowercase_keys options. The table qualifier
|
|
109
|
+
* is the query's relation alias when one is used (relationAlias, requested
|
|
110
|
+
* via isc_info_sql_relation_alias), the relation name otherwise, so
|
|
111
|
+
* self-joins nest under their query aliases. Expression columns (no source
|
|
112
|
+
* relation) qualify as '' exactly like mysql2: they nest under the '' key,
|
|
113
|
+
* and in separator mode become '<sep>alias' — always prefixing keeps
|
|
114
|
+
* qualified keys collision-free (a bare expression alias could otherwise
|
|
115
|
+
* collide with a real column's 'table<sep>column' key). Used by the fetch
|
|
116
|
+
* decoder and by fetchBlobSyncRow, which must agree on where each column
|
|
117
|
+
* landed in the row.
|
|
118
|
+
*/
|
|
119
|
+
export function computeColumnKeys(
|
|
120
|
+
output: SQLVarBase[],
|
|
121
|
+
nestTables: boolean | string | undefined,
|
|
122
|
+
lowercaseKeys: boolean | undefined,
|
|
123
|
+
transform?: (key: string) => string
|
|
124
|
+
): ColumnKey[] {
|
|
125
|
+
return output.map((column) => {
|
|
126
|
+
let key = column.alias || '';
|
|
127
|
+
if (lowercaseKeys) {
|
|
128
|
+
key = key.toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
if (transform) {
|
|
131
|
+
key = transform(key);
|
|
132
|
+
}
|
|
133
|
+
if (nestTables !== true && typeof nestTables !== 'string') {
|
|
134
|
+
return { key };
|
|
135
|
+
}
|
|
136
|
+
let table = column.relationAlias || column.relation || '';
|
|
137
|
+
if (lowercaseKeys) {
|
|
138
|
+
table = table.toLowerCase();
|
|
139
|
+
}
|
|
140
|
+
if (transform) {
|
|
141
|
+
table = transform(table);
|
|
142
|
+
}
|
|
143
|
+
if (nestTables === true) {
|
|
144
|
+
return { table, key };
|
|
145
|
+
}
|
|
146
|
+
return { key: table + nestTables + key };
|
|
147
|
+
});
|
|
148
|
+
}
|
|
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
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Resolve the effective nestTables value: the per-query option wins over
|
|
213
|
+
* the connection option. The decoder and fetchBlobSyncRow both use this —
|
|
214
|
+
* they must agree on whether nesting is active or blob cells are looked
|
|
215
|
+
* up in the wrong place.
|
|
216
|
+
*/
|
|
217
|
+
export function resolveNestTables(
|
|
218
|
+
queryOptions: { nestTables?: boolean | string } | undefined,
|
|
219
|
+
connectionOptions: { nestTables?: boolean | string } | undefined
|
|
220
|
+
): boolean | string | undefined {
|
|
221
|
+
return resolveQueryOption('nestTables', queryOptions, connectionOptions);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The object a column's value lives in: the row itself, or — when the
|
|
226
|
+
* column carries a nestTables table qualifier — the row's per-table
|
|
227
|
+
* sub-object, created on first use. Every site that reads or writes a
|
|
228
|
+
* cell by ColumnKey must resolve it through here.
|
|
229
|
+
*/
|
|
230
|
+
export function nestCell(row: any, table: string | undefined) {
|
|
231
|
+
if (table === undefined) {
|
|
232
|
+
return row;
|
|
233
|
+
}
|
|
234
|
+
return row[table] || (row[table] = {});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
//------------------------------------------------------
|
|
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
|
+
|
|
96
337
|
export class SQLVarText extends SQLVarBase {
|
|
97
338
|
decode(data: XdrReader, lowerV13: boolean, options?: any) {
|
|
98
339
|
let ret;
|