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/lib/wire/transaction.js
CHANGED
|
@@ -6,6 +6,8 @@ const callback_1 = require("../callback");
|
|
|
6
6
|
const named_params_1 = require("../named-params");
|
|
7
7
|
const utils_1 = require("../utils");
|
|
8
8
|
const const_1 = __importDefault(require("./const"));
|
|
9
|
+
const sql_template_1 = require("../sql-template");
|
|
10
|
+
const xsqlvar_1 = require("./xsqlvar");
|
|
9
11
|
const query_stream_1 = __importDefault(require("./query-stream"));
|
|
10
12
|
/***************************************
|
|
11
13
|
*
|
|
@@ -44,9 +46,73 @@ function hookAbortSignal(connection, signal, callback) {
|
|
|
44
46
|
}
|
|
45
47
|
class Transaction {
|
|
46
48
|
constructor(connection) {
|
|
49
|
+
/** Current savepoint nesting depth (names savepoints, see savepoint()). */
|
|
50
|
+
this._savepointDepth = 0;
|
|
47
51
|
this.connection = connection;
|
|
48
52
|
this.db = connection.db;
|
|
49
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Tagged-template query API: tx.sql`SELECT ... ${value}` (see README).
|
|
56
|
+
* Built lazily — transactions are created per-query internally, and
|
|
57
|
+
* those throwaway instances must not pay for the tag. The compiled text
|
|
58
|
+
* is positional-only, so the namedPlaceholders rewriter is disabled:
|
|
59
|
+
* any `:token` in the template is PSQL (EXECUTE BLOCK), not a
|
|
60
|
+
* placeholder.
|
|
61
|
+
*/
|
|
62
|
+
get sql() {
|
|
63
|
+
return this._sql || (this._sql = (0, sql_template_1.makeSqlTag)((text, params, options) => this.queryAsync(text, params, { ...options, namedPlaceholders: false })));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Run `work` inside a savepoint (Firebird 1.5+): on resolve the
|
|
67
|
+
* savepoint is released, on reject the transaction rolls back TO the
|
|
68
|
+
* savepoint — undoing only work's changes — and the error is rethrown,
|
|
69
|
+
* leaving the transaction itself usable. Nestable (each call generates
|
|
70
|
+
* a fresh NF_SP_n name), mirroring db.withTransaction's style and
|
|
71
|
+
* Postgres.js's sql.savepoint().
|
|
72
|
+
*
|
|
73
|
+
* Do NOT run sibling savepoints concurrently on one transaction
|
|
74
|
+
* (Promise.all): Firebird's RELEASE SAVEPOINT also releases every
|
|
75
|
+
* savepoint created after it, so interleaved siblings release each
|
|
76
|
+
* other. Nested (awaited) savepoints are fine.
|
|
77
|
+
*/
|
|
78
|
+
async savepoint(work) {
|
|
79
|
+
if (typeof work !== 'function') {
|
|
80
|
+
throw new Error('savepoint(work) expects a function');
|
|
81
|
+
}
|
|
82
|
+
// named by nesting depth, not a global counter: sequential
|
|
83
|
+
// savepoints at the same depth reuse the same three SQL strings, so
|
|
84
|
+
// the statement cache serves them instead of accumulating
|
|
85
|
+
// single-use entries (redefining a released savepoint name is legal)
|
|
86
|
+
const name = 'NF_SP_' + (++this._savepointDepth);
|
|
87
|
+
try {
|
|
88
|
+
await this.queryAsync('SAVEPOINT ' + name);
|
|
89
|
+
let result;
|
|
90
|
+
try {
|
|
91
|
+
result = await work(this);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// only a work() failure rolls back to the savepoint — a
|
|
95
|
+
// RELEASE failure below must NOT undo work's successful
|
|
96
|
+
// changes
|
|
97
|
+
try {
|
|
98
|
+
await this.queryAsync('ROLLBACK TO SAVEPOINT ' + name);
|
|
99
|
+
}
|
|
100
|
+
catch (rollbackErr) {
|
|
101
|
+
// the original failure matters more; keep the rollback
|
|
102
|
+
// failure attached for diagnosis
|
|
103
|
+
if (err && typeof err === 'object') {
|
|
104
|
+
err.savepointRollbackError = rollbackErr;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
await this.queryAsync('RELEASE SAVEPOINT ' + name);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
this._savepointDepth--;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
50
116
|
/** Per-call options.namedPlaceholders overrides the connection option. */
|
|
51
117
|
namedPlaceholdersEnabled(options) {
|
|
52
118
|
if (options && options.namedPlaceholders !== undefined)
|
|
@@ -116,6 +182,66 @@ class Transaction {
|
|
|
116
182
|
dropError(err);
|
|
117
183
|
return;
|
|
118
184
|
}
|
|
185
|
+
// withMeta applies to query/execute only: in streaming mode
|
|
186
|
+
// (sequentially/queryStream, which spread user options) rows
|
|
187
|
+
// bypass fetchAll's array, so a result object here would
|
|
188
|
+
// carry rows: [] and a meaningless affectedRows
|
|
189
|
+
var withMeta = Boolean(options && typeof options === 'object' &&
|
|
190
|
+
options.withMeta && !options.asStream);
|
|
191
|
+
// Deliver the historic result shape, or — when options.withMeta
|
|
192
|
+
// is set — request the per-verb DML row counts while the
|
|
193
|
+
// statement handle is still open and wrap everything in a
|
|
194
|
+
// { rows, fields, affectedRows, recordCounts, warnings } object.
|
|
195
|
+
function deliver(rows, isSelect, plainDml) {
|
|
196
|
+
if (!withMeta) {
|
|
197
|
+
statement.release();
|
|
198
|
+
if (callback) {
|
|
199
|
+
if (plainDml) {
|
|
200
|
+
// plain DML historically calls back with no args
|
|
201
|
+
callback();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
callback(undefined, rows, statement.output, isSelect);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
var execWarnings = (ret && ret.warnings) || [];
|
|
210
|
+
var finalize = function (counts) {
|
|
211
|
+
statement.release();
|
|
212
|
+
if (!callback) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// DML: what the server actually changed; SELECT: rows
|
|
216
|
+
// returned (pg's rowCount convention)
|
|
217
|
+
var affectedRows = counts
|
|
218
|
+
? counts.insertCount + counts.updateCount + counts.deleteCount
|
|
219
|
+
: (Array.isArray(rows) ? rows.length : (rows !== undefined ? 1 : 0));
|
|
220
|
+
callback(undefined, {
|
|
221
|
+
rows: rows,
|
|
222
|
+
fields: (0, xsqlvar_1.describeFields)(statement.output),
|
|
223
|
+
affectedRows: affectedRows,
|
|
224
|
+
recordCounts: counts,
|
|
225
|
+
warnings: execWarnings,
|
|
226
|
+
}, statement.output, isSelect);
|
|
227
|
+
};
|
|
228
|
+
var t = statement.type;
|
|
229
|
+
var isDml = t === const_1.default.isc_info_sql_stmt_insert ||
|
|
230
|
+
t === const_1.default.isc_info_sql_stmt_update ||
|
|
231
|
+
t === const_1.default.isc_info_sql_stmt_delete ||
|
|
232
|
+
t === const_1.default.isc_info_sql_stmt_exec_procedure;
|
|
233
|
+
if (!isDml) {
|
|
234
|
+
finalize();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
self.connection.statementInfo(statement, const_1.default.RECORDS_INFO, function (err, info) {
|
|
238
|
+
if (err) {
|
|
239
|
+
dropError(err);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
finalize((0, xsqlvar_1.parseRecordCounts)(info && info.buffer));
|
|
243
|
+
});
|
|
244
|
+
}
|
|
119
245
|
switch (statement.type) {
|
|
120
246
|
case const_1.default.isc_info_sql_stmt_select:
|
|
121
247
|
statement.fetchAll(self, function (err, r) {
|
|
@@ -123,35 +249,27 @@ class Transaction {
|
|
|
123
249
|
dropError(err);
|
|
124
250
|
return;
|
|
125
251
|
}
|
|
126
|
-
|
|
127
|
-
if (callback)
|
|
128
|
-
callback(undefined, r, statement.output, true);
|
|
252
|
+
deliver(r, true);
|
|
129
253
|
});
|
|
130
254
|
break;
|
|
131
255
|
case const_1.default.isc_info_sql_stmt_exec_procedure:
|
|
132
256
|
if (ret && ret.data && ret.data.length > 0) {
|
|
133
|
-
|
|
134
|
-
if (callback)
|
|
135
|
-
callback(undefined, ret.data[0], statement.output, true);
|
|
257
|
+
deliver(ret.data[0], true);
|
|
136
258
|
break;
|
|
137
259
|
}
|
|
138
260
|
else if (statement.output.length) {
|
|
139
|
-
statement.fetch(self, 1, function (err,
|
|
261
|
+
statement.fetch(self, 1, function (err, fret) {
|
|
140
262
|
if (err) {
|
|
141
263
|
dropError(err);
|
|
142
264
|
return;
|
|
143
265
|
}
|
|
144
|
-
|
|
145
|
-
if (callback)
|
|
146
|
-
callback(undefined, ret.data[0], statement.output, false);
|
|
266
|
+
deliver(fret.data[0], false);
|
|
147
267
|
});
|
|
148
268
|
break;
|
|
149
269
|
}
|
|
150
270
|
// Fall through is normal
|
|
151
271
|
default:
|
|
152
|
-
|
|
153
|
-
if (callback)
|
|
154
|
-
callback();
|
|
272
|
+
deliver(undefined, false, true);
|
|
155
273
|
break;
|
|
156
274
|
}
|
|
157
275
|
}, options);
|
package/lib/wire/xsqlvar.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { XdrReader, XdrWriter, BlrWriter } from './serialize';
|
|
2
|
+
import type { RecordCounts } from '../types';
|
|
2
3
|
/**
|
|
3
4
|
* Common shape of all SQLVar descriptor objects. The metadata properties
|
|
4
5
|
* are populated externally (in connection.ts) from the op_prepare_statement
|
|
@@ -41,7 +42,23 @@ export interface ColumnKey {
|
|
|
41
42
|
* decoder and by fetchBlobSyncRow, which must agree on where each column
|
|
42
43
|
* landed in the row.
|
|
43
44
|
*/
|
|
44
|
-
export declare function computeColumnKeys(output: SQLVarBase[], nestTables: boolean | string | undefined, lowercaseKeys: boolean | undefined): ColumnKey[];
|
|
45
|
+
export declare function computeColumnKeys(output: SQLVarBase[], nestTables: boolean | string | undefined, lowercaseKeys: boolean | undefined, transform?: (key: string) => string): ColumnKey[];
|
|
46
|
+
/** transformKeys option value: the built-in 'camel', or a custom mapper. */
|
|
47
|
+
export type KeyTransform = 'camel' | ((key: string) => string);
|
|
48
|
+
/** FIRST_NAME → firstName (the transformKeys: 'camel' built-in). */
|
|
49
|
+
export declare function camelizeKey(key: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the effective transformKeys value (per-query wins over the
|
|
52
|
+
* connection option) into a callable mapper, or undefined when off.
|
|
53
|
+
* A custom mapper is guarded like the typeCast hook: a throw inside the
|
|
54
|
+
* row-decode loop would be mistaken for an incomplete packet and desync
|
|
55
|
+
* the response queue, so failures fall back to the untransformed key.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveKeyTransform(queryOptions: {
|
|
58
|
+
transformKeys?: KeyTransform;
|
|
59
|
+
} | undefined, connectionOptions: {
|
|
60
|
+
transformKeys?: KeyTransform;
|
|
61
|
+
} | undefined): ((key: string) => string) | undefined;
|
|
45
62
|
/**
|
|
46
63
|
* Resolve the effective nestTables value: the per-query option wins over
|
|
47
64
|
* the connection option. The decoder and fetchBlobSyncRow both use this —
|
|
@@ -60,6 +77,50 @@ export declare function resolveNestTables(queryOptions: {
|
|
|
60
77
|
* cell by ColumnKey must resolve it through here.
|
|
61
78
|
*/
|
|
62
79
|
export declare function nestCell(row: any, table: string | undefined): any;
|
|
80
|
+
/** Human-readable names for the SQL_* wire type codes. */
|
|
81
|
+
export declare const SQL_TYPE_NAMES: Record<number, string>;
|
|
82
|
+
/**
|
|
83
|
+
* Public column-metadata shape for one output descriptor: the vocabulary
|
|
84
|
+
* both the typeCast hook and withMeta `fields` deliver. Keep the two in
|
|
85
|
+
* lockstep by building both through here.
|
|
86
|
+
*/
|
|
87
|
+
export declare function describeField(meta: Partial<SQLVarBase>): {
|
|
88
|
+
type: number;
|
|
89
|
+
typeName: string;
|
|
90
|
+
subType: number | undefined;
|
|
91
|
+
scale: number | undefined;
|
|
92
|
+
length: number | undefined;
|
|
93
|
+
nullable: boolean | undefined;
|
|
94
|
+
field: string | undefined;
|
|
95
|
+
relation: string | undefined;
|
|
96
|
+
relationAlias: string | undefined;
|
|
97
|
+
relationSchema: string | undefined;
|
|
98
|
+
alias: string | undefined;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Map a statement's output descriptors to the column-metadata array
|
|
102
|
+
* delivered in withMeta results ({ rows, fields, ... }).
|
|
103
|
+
*/
|
|
104
|
+
export declare function describeFields(output: SQLVarBase[]): {
|
|
105
|
+
type: number;
|
|
106
|
+
typeName: string;
|
|
107
|
+
subType: number | undefined;
|
|
108
|
+
scale: number | undefined;
|
|
109
|
+
length: number | undefined;
|
|
110
|
+
nullable: boolean | undefined;
|
|
111
|
+
field: string | undefined;
|
|
112
|
+
relation: string | undefined;
|
|
113
|
+
relationAlias: string | undefined;
|
|
114
|
+
relationSchema: string | undefined;
|
|
115
|
+
alias: string | undefined;
|
|
116
|
+
}[];
|
|
117
|
+
/**
|
|
118
|
+
* Parse the op_info_sql response buffer of a Const.RECORDS_INFO request
|
|
119
|
+
* into per-verb row counts. The buffer holds an isc_info_sql_records
|
|
120
|
+
* cluster (2-byte total length, then nested isc_info_req_*_count items,
|
|
121
|
+
* each 2-byte length + little-endian integer) terminated by isc_info_end.
|
|
122
|
+
*/
|
|
123
|
+
export declare function parseRecordCounts(buffer: Buffer | undefined): RecordCounts;
|
|
63
124
|
export declare class SQLVarText extends SQLVarBase {
|
|
64
125
|
decode(data: XdrReader, lowerV13: boolean, options?: any): string | Buffer<ArrayBufferLike> | null | undefined;
|
|
65
126
|
calcBlr(blr: BlrWriter): void;
|
package/lib/wire/xsqlvar.js
CHANGED
|
@@ -3,12 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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.SQLVarBase = void 0;
|
|
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
7
|
exports.computeColumnKeys = computeColumnKeys;
|
|
8
|
+
exports.camelizeKey = camelizeKey;
|
|
9
|
+
exports.resolveKeyTransform = resolveKeyTransform;
|
|
8
10
|
exports.resolveNestTables = resolveNestTables;
|
|
9
11
|
exports.nestCell = nestCell;
|
|
12
|
+
exports.describeField = describeField;
|
|
13
|
+
exports.describeFields = describeFields;
|
|
14
|
+
exports.parseRecordCounts = parseRecordCounts;
|
|
10
15
|
exports.encodeDateTimeParts = encodeDateTimeParts;
|
|
11
16
|
const const_1 = __importDefault(require("./const"));
|
|
17
|
+
const serialize_1 = require("./serialize");
|
|
12
18
|
/***************************************
|
|
13
19
|
*
|
|
14
20
|
* SQLVar
|
|
@@ -84,12 +90,15 @@ exports.SQLVarBase = SQLVarBase;
|
|
|
84
90
|
* decoder and by fetchBlobSyncRow, which must agree on where each column
|
|
85
91
|
* landed in the row.
|
|
86
92
|
*/
|
|
87
|
-
function computeColumnKeys(output, nestTables, lowercaseKeys) {
|
|
93
|
+
function computeColumnKeys(output, nestTables, lowercaseKeys, transform) {
|
|
88
94
|
return output.map((column) => {
|
|
89
95
|
let key = column.alias || '';
|
|
90
96
|
if (lowercaseKeys) {
|
|
91
97
|
key = key.toLowerCase();
|
|
92
98
|
}
|
|
99
|
+
if (transform) {
|
|
100
|
+
key = transform(key);
|
|
101
|
+
}
|
|
93
102
|
if (nestTables !== true && typeof nestTables !== 'string') {
|
|
94
103
|
return { key };
|
|
95
104
|
}
|
|
@@ -97,12 +106,63 @@ function computeColumnKeys(output, nestTables, lowercaseKeys) {
|
|
|
97
106
|
if (lowercaseKeys) {
|
|
98
107
|
table = table.toLowerCase();
|
|
99
108
|
}
|
|
109
|
+
if (transform) {
|
|
110
|
+
table = transform(table);
|
|
111
|
+
}
|
|
100
112
|
if (nestTables === true) {
|
|
101
113
|
return { table, key };
|
|
102
114
|
}
|
|
103
115
|
return { key: table + nestTables + key };
|
|
104
116
|
});
|
|
105
117
|
}
|
|
118
|
+
/** FIRST_NAME → firstName (the transformKeys: 'camel' built-in). */
|
|
119
|
+
function camelizeKey(key) {
|
|
120
|
+
const parts = String(key).toLowerCase().split('_');
|
|
121
|
+
let out = parts[0] || '';
|
|
122
|
+
for (let i = 1; i < parts.length; i++) {
|
|
123
|
+
const part = parts[i];
|
|
124
|
+
if (part) {
|
|
125
|
+
out += part.charAt(0).toUpperCase() + part.slice(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolve the effective transformKeys value (per-query wins over the
|
|
132
|
+
* connection option) into a callable mapper, or undefined when off.
|
|
133
|
+
* A custom mapper is guarded like the typeCast hook: a throw inside the
|
|
134
|
+
* row-decode loop would be mistaken for an incomplete packet and desync
|
|
135
|
+
* the response queue, so failures fall back to the untransformed key.
|
|
136
|
+
*/
|
|
137
|
+
function resolveKeyTransform(queryOptions, connectionOptions) {
|
|
138
|
+
const value = resolveQueryOption('transformKeys', queryOptions, connectionOptions);
|
|
139
|
+
if (value === 'camel') {
|
|
140
|
+
return camelizeKey;
|
|
141
|
+
}
|
|
142
|
+
if (typeof value !== 'function') {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
return (key) => {
|
|
146
|
+
try {
|
|
147
|
+
return String(value(key));
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.warn('[node-firebird] transformKeys mapper threw for key "%s": %s — using the untransformed key', key, err && err.message);
|
|
151
|
+
return key;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Shared precedence rule for per-query-overridable connection options:
|
|
157
|
+
* the per-query value wins whenever it is present (even if falsy), the
|
|
158
|
+
* connection option applies otherwise.
|
|
159
|
+
*/
|
|
160
|
+
function resolveQueryOption(name, queryOptions, connectionOptions) {
|
|
161
|
+
if (queryOptions && queryOptions[name] !== undefined) {
|
|
162
|
+
return queryOptions[name];
|
|
163
|
+
}
|
|
164
|
+
return connectionOptions ? connectionOptions[name] : undefined;
|
|
165
|
+
}
|
|
106
166
|
/**
|
|
107
167
|
* Resolve the effective nestTables value: the per-query option wins over
|
|
108
168
|
* the connection option. The decoder and fetchBlobSyncRow both use this —
|
|
@@ -110,10 +170,7 @@ function computeColumnKeys(output, nestTables, lowercaseKeys) {
|
|
|
110
170
|
* up in the wrong place.
|
|
111
171
|
*/
|
|
112
172
|
function resolveNestTables(queryOptions, connectionOptions) {
|
|
113
|
-
|
|
114
|
-
return queryOptions.nestTables;
|
|
115
|
-
}
|
|
116
|
-
return connectionOptions && connectionOptions.nestTables;
|
|
173
|
+
return resolveQueryOption('nestTables', queryOptions, connectionOptions);
|
|
117
174
|
}
|
|
118
175
|
/**
|
|
119
176
|
* The object a column's value lives in: the row itself, or — when the
|
|
@@ -128,6 +185,108 @@ function nestCell(row, table) {
|
|
|
128
185
|
return row[table] || (row[table] = {});
|
|
129
186
|
}
|
|
130
187
|
//------------------------------------------------------
|
|
188
|
+
/** Human-readable names for the SQL_* wire type codes. */
|
|
189
|
+
exports.SQL_TYPE_NAMES = {
|
|
190
|
+
[const_1.default.SQL_TEXT]: 'TEXT',
|
|
191
|
+
[const_1.default.SQL_VARYING]: 'VARYING',
|
|
192
|
+
[const_1.default.SQL_SHORT]: 'SHORT',
|
|
193
|
+
[const_1.default.SQL_LONG]: 'LONG',
|
|
194
|
+
[const_1.default.SQL_FLOAT]: 'FLOAT',
|
|
195
|
+
[const_1.default.SQL_DOUBLE]: 'DOUBLE',
|
|
196
|
+
[const_1.default.SQL_D_FLOAT]: 'D_FLOAT',
|
|
197
|
+
[const_1.default.SQL_TIMESTAMP]: 'TIMESTAMP',
|
|
198
|
+
[const_1.default.SQL_BLOB]: 'BLOB',
|
|
199
|
+
[const_1.default.SQL_ARRAY]: 'ARRAY',
|
|
200
|
+
[const_1.default.SQL_QUAD]: 'QUAD',
|
|
201
|
+
[const_1.default.SQL_TYPE_TIME]: 'TIME',
|
|
202
|
+
[const_1.default.SQL_TYPE_DATE]: 'DATE',
|
|
203
|
+
[const_1.default.SQL_INT64]: 'INT64',
|
|
204
|
+
[const_1.default.SQL_INT128]: 'INT128',
|
|
205
|
+
[const_1.default.SQL_TIMESTAMP_TZ]: 'TIMESTAMP_TZ',
|
|
206
|
+
[const_1.default.SQL_TIMESTAMP_TZ_EX]: 'TIMESTAMP_TZ_EX',
|
|
207
|
+
[const_1.default.SQL_TIME_TZ]: 'TIME_TZ',
|
|
208
|
+
[const_1.default.SQL_TIME_TZ_EX]: 'TIME_TZ_EX',
|
|
209
|
+
[const_1.default.SQL_DEC16]: 'DEC16',
|
|
210
|
+
[const_1.default.SQL_DEC34]: 'DEC34',
|
|
211
|
+
[const_1.default.SQL_BOOLEAN]: 'BOOLEAN',
|
|
212
|
+
[const_1.default.SQL_NULL]: 'NULL',
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Public column-metadata shape for one output descriptor: the vocabulary
|
|
216
|
+
* both the typeCast hook and withMeta `fields` deliver. Keep the two in
|
|
217
|
+
* lockstep by building both through here.
|
|
218
|
+
*/
|
|
219
|
+
function describeField(meta) {
|
|
220
|
+
return {
|
|
221
|
+
type: meta.type,
|
|
222
|
+
typeName: exports.SQL_TYPE_NAMES[meta.type] || 'UNKNOWN',
|
|
223
|
+
subType: meta.subType,
|
|
224
|
+
scale: meta.scale,
|
|
225
|
+
length: meta.length,
|
|
226
|
+
nullable: meta.nullable,
|
|
227
|
+
field: meta.field,
|
|
228
|
+
relation: meta.relation,
|
|
229
|
+
relationAlias: meta.relationAlias,
|
|
230
|
+
relationSchema: meta.relationSchema,
|
|
231
|
+
alias: meta.alias,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Map a statement's output descriptors to the column-metadata array
|
|
236
|
+
* delivered in withMeta results ({ rows, fields, ... }).
|
|
237
|
+
*/
|
|
238
|
+
function describeFields(output) {
|
|
239
|
+
return (output || []).map(describeField);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Parse the op_info_sql response buffer of a Const.RECORDS_INFO request
|
|
243
|
+
* into per-verb row counts. The buffer holds an isc_info_sql_records
|
|
244
|
+
* cluster (2-byte total length, then nested isc_info_req_*_count items,
|
|
245
|
+
* each 2-byte length + little-endian integer) terminated by isc_info_end.
|
|
246
|
+
*/
|
|
247
|
+
function parseRecordCounts(buffer) {
|
|
248
|
+
const counts = { selectCount: 0, insertCount: 0, updateCount: 0, deleteCount: 0 };
|
|
249
|
+
if (!buffer || !buffer.length) {
|
|
250
|
+
return counts;
|
|
251
|
+
}
|
|
252
|
+
// this runs inside a response callback — a malformed/truncated buffer
|
|
253
|
+
// must yield partial counts, never a throw
|
|
254
|
+
try {
|
|
255
|
+
const br = new serialize_1.BlrReader(buffer);
|
|
256
|
+
while (br.pos < br.buffer.length) {
|
|
257
|
+
const item = br.readByteCode();
|
|
258
|
+
if (item === const_1.default.isc_info_end || item === const_1.default.isc_info_truncated) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
if (item === const_1.default.isc_info_sql_records) {
|
|
262
|
+
br.pos += 2; // skip the cluster's total length; nested items follow
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
switch (item) {
|
|
266
|
+
case const_1.default.isc_info_req_select_count:
|
|
267
|
+
counts.selectCount = br.readInt() || 0;
|
|
268
|
+
break;
|
|
269
|
+
case const_1.default.isc_info_req_insert_count:
|
|
270
|
+
counts.insertCount = br.readInt() || 0;
|
|
271
|
+
break;
|
|
272
|
+
case const_1.default.isc_info_req_update_count:
|
|
273
|
+
counts.updateCount = br.readInt() || 0;
|
|
274
|
+
break;
|
|
275
|
+
case const_1.default.isc_info_req_delete_count:
|
|
276
|
+
counts.deleteCount = br.readInt() || 0;
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
// unknown item: its 2-byte length prefix tells us how far to skip
|
|
280
|
+
br.pos += 2 + br.buffer.readUInt16LE(br.pos);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
// fall through with whatever was parsed so far
|
|
286
|
+
}
|
|
287
|
+
return counts;
|
|
288
|
+
}
|
|
289
|
+
//------------------------------------------------------
|
|
131
290
|
class SQLVarText extends SQLVarBase {
|
|
132
291
|
decode(data, lowerV13, options) {
|
|
133
292
|
let ret;
|
package/package.json
CHANGED
package/src/pool.ts
CHANGED
|
@@ -26,7 +26,12 @@ type AttachFn = (options: any, callback: Callback) => void;
|
|
|
26
26
|
*
|
|
27
27
|
* Options: max (factory argument), options.min (floor the reaper never
|
|
28
28
|
* shrinks below), options.idleTimeoutMillis (close idle connections after
|
|
29
|
-
* this many ms; 0/absent = never), options.connectTimeout
|
|
29
|
+
* this many ms; 0/absent = never), options.connectTimeout,
|
|
30
|
+
* options.maxUses (retire a connection after this many checkouts — pg's
|
|
31
|
+
* maxUses), options.maxLifetimeMillis (retire a connection this many ms
|
|
32
|
+
* after it was created — Postgres.js's max_lifetime). Retirement happens
|
|
33
|
+
* when the connection is returned to the pool, and the sweep also closes
|
|
34
|
+
* over-lifetime idle connections; a replacement is created on demand.
|
|
30
35
|
*/
|
|
31
36
|
class Pool extends Events.EventEmitter {
|
|
32
37
|
attach: AttachFn;
|
|
@@ -37,6 +42,8 @@ class Pool extends Events.EventEmitter {
|
|
|
37
42
|
max: number;
|
|
38
43
|
min: number;
|
|
39
44
|
idleTimeoutMillis: number;
|
|
45
|
+
maxUses: number;
|
|
46
|
+
maxLifetimeMillis: number;
|
|
40
47
|
pending: Callback[];
|
|
41
48
|
options: any;
|
|
42
49
|
_destroyed: boolean;
|
|
@@ -52,22 +59,50 @@ class Pool extends Events.EventEmitter {
|
|
|
52
59
|
this.max = max || 4;
|
|
53
60
|
this.min = (options && options.min > 0) ? Math.min(options.min, this.max) : 0;
|
|
54
61
|
this.idleTimeoutMillis = (options && options.idleTimeoutMillis > 0) ? options.idleTimeoutMillis : 0;
|
|
62
|
+
this.maxUses = (options && options.maxUses > 0) ? options.maxUses : 0;
|
|
63
|
+
this.maxLifetimeMillis = (options && options.maxLifetimeMillis > 0) ? options.maxLifetimeMillis : 0;
|
|
55
64
|
this.pending = []; // callbacks waiting for a free slot
|
|
56
65
|
this.options = options;
|
|
57
66
|
this._destroyed = false; // true after destroy() — prevents further use
|
|
58
67
|
this._reaper = null;
|
|
59
68
|
|
|
60
|
-
|
|
69
|
+
// the sweep serves both idle eviction and lifetime retirement of
|
|
70
|
+
// idle connections; base its cadence on the tightest configured limit
|
|
71
|
+
var sweepBasis = Math.min(this.idleTimeoutMillis || Infinity, this.maxLifetimeMillis || Infinity);
|
|
72
|
+
if (sweepBasis !== Infinity) {
|
|
61
73
|
var self = this;
|
|
62
|
-
// Sweep at half the
|
|
63
|
-
// connection lives at most ~1.5x
|
|
74
|
+
// Sweep at half the basis (bounded to 100ms..30s) so a
|
|
75
|
+
// connection lives at most ~1.5x its limit. unref() keeps
|
|
64
76
|
// the timer from holding the process open.
|
|
65
|
-
var interval = Math.min(Math.max(
|
|
77
|
+
var interval = Math.min(Math.max(sweepBasis / 2, 100), 30000);
|
|
66
78
|
this._reaper = setInterval(function() { self._reap(); }, interval);
|
|
67
79
|
if (this._reaper.unref) this._reaper.unref();
|
|
68
80
|
}
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
/** True when the connection exceeded maxUses / maxLifetimeMillis.
|
|
84
|
+
* Both stamps are set unconditionally when the pool creates the
|
|
85
|
+
* connection, so they can be read bare here. */
|
|
86
|
+
_isExpired(db: any): boolean {
|
|
87
|
+
if (this.maxUses > 0 && db.__poolUseCount >= this.maxUses) return true;
|
|
88
|
+
if (this.maxLifetimeMillis > 0 && Date.now() - db.__poolCreatedAt >= this.maxLifetimeMillis) return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Close a healthy pooled connection for good (reaper/retirement path). */
|
|
93
|
+
_retire(db: any): void {
|
|
94
|
+
var self = this;
|
|
95
|
+
this._forget(db);
|
|
96
|
+
db.connection._pooled = false;
|
|
97
|
+
try {
|
|
98
|
+
db.detach(function(err?: any) {
|
|
99
|
+
if (err) self._emitError(err, db);
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
self._emitError(e, db);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
71
106
|
/** Physical connections owned by the pool (idle + in use). */
|
|
72
107
|
get totalCount(): number {
|
|
73
108
|
return this.internaldb.length;
|
|
@@ -124,19 +159,18 @@ class Pool extends Events.EventEmitter {
|
|
|
124
159
|
self._forget(db);
|
|
125
160
|
return;
|
|
126
161
|
}
|
|
162
|
+
// lifetime retirement applies even below min — recycling is the
|
|
163
|
+
// point; replacements are created on demand
|
|
164
|
+
if (self._isExpired(db)) {
|
|
165
|
+
self._retire(db);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!self.idleTimeoutMillis) return;
|
|
127
169
|
if (self.internaldb.length <= self.min) return;
|
|
128
170
|
var idleSince = typeof db.__poolIdleSince === 'number' ? db.__poolIdleSince : now;
|
|
129
171
|
if (now - idleSince < self.idleTimeoutMillis) return;
|
|
130
172
|
|
|
131
|
-
self.
|
|
132
|
-
db.connection._pooled = false;
|
|
133
|
-
try {
|
|
134
|
-
db.detach(function(err?: any) {
|
|
135
|
-
if (err) self._emitError(err, db);
|
|
136
|
-
});
|
|
137
|
-
} catch (e) {
|
|
138
|
-
self._emitError(e, db);
|
|
139
|
-
}
|
|
173
|
+
self._retire(db);
|
|
140
174
|
});
|
|
141
175
|
}
|
|
142
176
|
|
|
@@ -175,6 +209,7 @@ class Pool extends Events.EventEmitter {
|
|
|
175
209
|
}
|
|
176
210
|
// Idle connection available — hand it out immediately.
|
|
177
211
|
self.dbinuse++;
|
|
212
|
+
db.__poolUseCount = (db.__poolUseCount || 0) + 1;
|
|
178
213
|
self.emit('acquire', db);
|
|
179
214
|
cb(null, db);
|
|
180
215
|
} else {
|
|
@@ -235,6 +270,8 @@ class Pool extends Events.EventEmitter {
|
|
|
235
270
|
|
|
236
271
|
if (!err) {
|
|
237
272
|
self.dbinuse++;
|
|
273
|
+
db.__poolCreatedAt = Date.now();
|
|
274
|
+
db.__poolUseCount = 1;
|
|
238
275
|
self.internaldb.push(db);
|
|
239
276
|
db.on('detach', function () {
|
|
240
277
|
// also in pool (could be a twice call to detach)
|
|
@@ -244,6 +281,12 @@ class Pool extends Events.EventEmitter {
|
|
|
244
281
|
if (db.connection._isClosed || db.connection._isDetach || db.connection._pooled === false) {
|
|
245
282
|
self.internaldb.splice(self.internaldb.indexOf(db), 1);
|
|
246
283
|
self.emit('remove', db);
|
|
284
|
+
} else if (self._isExpired(db)) {
|
|
285
|
+
// worn out (maxUses / maxLifetimeMillis): close it
|
|
286
|
+
// for good instead of returning it to the idle
|
|
287
|
+
// pool. The re-fired detach event exits early via
|
|
288
|
+
// the internaldb guard above.
|
|
289
|
+
self._retire(db);
|
|
247
290
|
} else {
|
|
248
291
|
db.__poolIdleSince = Date.now();
|
|
249
292
|
self.pooldb.push(db);
|