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/lib/wire/xsqlvar.js
CHANGED
|
@@ -3,9 +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
|
+
exports.computeColumnKeys = computeColumnKeys;
|
|
8
|
+
exports.camelizeKey = camelizeKey;
|
|
9
|
+
exports.resolveKeyTransform = resolveKeyTransform;
|
|
10
|
+
exports.resolveNestTables = resolveNestTables;
|
|
11
|
+
exports.nestCell = nestCell;
|
|
12
|
+
exports.describeField = describeField;
|
|
13
|
+
exports.describeFields = describeFields;
|
|
14
|
+
exports.parseRecordCounts = parseRecordCounts;
|
|
7
15
|
exports.encodeDateTimeParts = encodeDateTimeParts;
|
|
8
16
|
const const_1 = __importDefault(require("./const"));
|
|
17
|
+
const serialize_1 = require("./serialize");
|
|
9
18
|
/***************************************
|
|
10
19
|
*
|
|
11
20
|
* SQLVar
|
|
@@ -68,6 +77,215 @@ function resolveTextEncoding(options) {
|
|
|
68
77
|
class SQLVarBase {
|
|
69
78
|
}
|
|
70
79
|
exports.SQLVarBase = SQLVarBase;
|
|
80
|
+
/**
|
|
81
|
+
* Compute the object-row property keys for a statement's output columns,
|
|
82
|
+
* honouring the nestTables and lowercase_keys options. The table qualifier
|
|
83
|
+
* is the query's relation alias when one is used (relationAlias, requested
|
|
84
|
+
* via isc_info_sql_relation_alias), the relation name otherwise, so
|
|
85
|
+
* self-joins nest under their query aliases. Expression columns (no source
|
|
86
|
+
* relation) qualify as '' exactly like mysql2: they nest under the '' key,
|
|
87
|
+
* and in separator mode become '<sep>alias' — always prefixing keeps
|
|
88
|
+
* qualified keys collision-free (a bare expression alias could otherwise
|
|
89
|
+
* collide with a real column's 'table<sep>column' key). Used by the fetch
|
|
90
|
+
* decoder and by fetchBlobSyncRow, which must agree on where each column
|
|
91
|
+
* landed in the row.
|
|
92
|
+
*/
|
|
93
|
+
function computeColumnKeys(output, nestTables, lowercaseKeys, transform) {
|
|
94
|
+
return output.map((column) => {
|
|
95
|
+
let key = column.alias || '';
|
|
96
|
+
if (lowercaseKeys) {
|
|
97
|
+
key = key.toLowerCase();
|
|
98
|
+
}
|
|
99
|
+
if (transform) {
|
|
100
|
+
key = transform(key);
|
|
101
|
+
}
|
|
102
|
+
if (nestTables !== true && typeof nestTables !== 'string') {
|
|
103
|
+
return { key };
|
|
104
|
+
}
|
|
105
|
+
let table = column.relationAlias || column.relation || '';
|
|
106
|
+
if (lowercaseKeys) {
|
|
107
|
+
table = table.toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
if (transform) {
|
|
110
|
+
table = transform(table);
|
|
111
|
+
}
|
|
112
|
+
if (nestTables === true) {
|
|
113
|
+
return { table, key };
|
|
114
|
+
}
|
|
115
|
+
return { key: table + nestTables + key };
|
|
116
|
+
});
|
|
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
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Resolve the effective nestTables value: the per-query option wins over
|
|
168
|
+
* the connection option. The decoder and fetchBlobSyncRow both use this —
|
|
169
|
+
* they must agree on whether nesting is active or blob cells are looked
|
|
170
|
+
* up in the wrong place.
|
|
171
|
+
*/
|
|
172
|
+
function resolveNestTables(queryOptions, connectionOptions) {
|
|
173
|
+
return resolveQueryOption('nestTables', queryOptions, connectionOptions);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The object a column's value lives in: the row itself, or — when the
|
|
177
|
+
* column carries a nestTables table qualifier — the row's per-table
|
|
178
|
+
* sub-object, created on first use. Every site that reads or writes a
|
|
179
|
+
* cell by ColumnKey must resolve it through here.
|
|
180
|
+
*/
|
|
181
|
+
function nestCell(row, table) {
|
|
182
|
+
if (table === undefined) {
|
|
183
|
+
return row;
|
|
184
|
+
}
|
|
185
|
+
return row[table] || (row[table] = {});
|
|
186
|
+
}
|
|
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
|
+
}
|
|
71
289
|
//------------------------------------------------------
|
|
72
290
|
class SQLVarText extends SQLVarBase {
|
|
73
291
|
decode(data, lowerV13, options) {
|
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);
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* Tagged-template query API (Postgres.js-style)
|
|
4
|
+
*
|
|
5
|
+
* db.sql`SELECT * FROM EMP WHERE ID = ${id}` → lazy thenable query
|
|
6
|
+
* db.sql('COLUMN NAME') → quoted identifier
|
|
7
|
+
*
|
|
8
|
+
* Interpolated values become positional `?` parameters — never string
|
|
9
|
+
* concatenation — so the API is injection-safe by construction. A query
|
|
10
|
+
* embedded inside another tag is treated as a fragment: its text and
|
|
11
|
+
* parameters are spliced in place. Arrays expand to `?, ?, ?` lists for
|
|
12
|
+
* IN clauses. Execution is lazy (on await/then) and happens exactly once.
|
|
13
|
+
*
|
|
14
|
+
***************************************/
|
|
15
|
+
|
|
16
|
+
import type { QueryOptions, QueryResult } from './types';
|
|
17
|
+
|
|
18
|
+
/** Executor provided by Database/Transaction: runs text+params, resolves rows
|
|
19
|
+
* (or the full QueryResult when options.withMeta is set). */
|
|
20
|
+
export type SqlExecutor = (text: string, params: any[], options?: QueryOptions) => Promise<any>;
|
|
21
|
+
|
|
22
|
+
/** A dynamically quoted identifier produced by sql('name'). */
|
|
23
|
+
export class SqlIdentifier {
|
|
24
|
+
name: string;
|
|
25
|
+
constructor(name: string) {
|
|
26
|
+
this.name = name;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Quote a (possibly dot-qualified) identifier for dialect 3: each part is
|
|
32
|
+
* wrapped in double quotes with embedded quotes doubled, so user input can
|
|
33
|
+
* never break out of the identifier position.
|
|
34
|
+
*/
|
|
35
|
+
export function quoteIdentifier(name: string): string {
|
|
36
|
+
return String(name)
|
|
37
|
+
.split('.')
|
|
38
|
+
.map((part) => '"' + part.replace(/"/g, '""') + '"')
|
|
39
|
+
.join('.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Compiled form of a tagged query: SQL text with `?` placeholders + params. */
|
|
43
|
+
export interface CompiledQuery {
|
|
44
|
+
text: string;
|
|
45
|
+
params: any[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compile(strings: readonly string[], values: any[], active?: Set<any>): CompiledQuery {
|
|
49
|
+
let text = '';
|
|
50
|
+
const params: any[] = [];
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < strings.length; i++) {
|
|
53
|
+
text += strings[i];
|
|
54
|
+
if (i >= values.length) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const value = values[i];
|
|
58
|
+
|
|
59
|
+
if (value instanceof SqlIdentifier) {
|
|
60
|
+
text += quoteIdentifier(value.name);
|
|
61
|
+
} else if (value instanceof SqlQuery) {
|
|
62
|
+
// embedded fragment: splice its text and params in place. The
|
|
63
|
+
// same fragment may appear several times (a DAG), but a fragment
|
|
64
|
+
// containing itself would recurse forever — track the expansion
|
|
65
|
+
// stack and reject cycles with a diagnosable error.
|
|
66
|
+
active = active || new Set();
|
|
67
|
+
if (active.has(value)) {
|
|
68
|
+
throw new Error('circular sql fragment: a query is embedded (transitively) inside itself');
|
|
69
|
+
}
|
|
70
|
+
active.add(value);
|
|
71
|
+
const inner = compile(value.strings, value.values, active);
|
|
72
|
+
active.delete(value);
|
|
73
|
+
text += inner.text;
|
|
74
|
+
params.push(...inner.params);
|
|
75
|
+
} else if (Array.isArray(value)) {
|
|
76
|
+
// IN (${[1, 2, 3]}) → IN (?, ?, ?)
|
|
77
|
+
if (!value.length) {
|
|
78
|
+
// '' would compile to `IN ()` — invalid SQL raising a server
|
|
79
|
+
// syntax error the caller never wrote; fail early instead
|
|
80
|
+
throw new Error('cannot interpolate an empty array (would compile to invalid SQL like "IN ()")');
|
|
81
|
+
}
|
|
82
|
+
text += value.map(() => '?').join(', ');
|
|
83
|
+
params.push(...value);
|
|
84
|
+
} else {
|
|
85
|
+
text += '?';
|
|
86
|
+
params.push(value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { text, params };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A lazily executed tagged query. Awaiting it (or calling then/catch/
|
|
95
|
+
* finally) runs it through the owning Database/Transaction exactly once;
|
|
96
|
+
* embedding it in another tag uses it as a fragment instead and never
|
|
97
|
+
* executes it.
|
|
98
|
+
*/
|
|
99
|
+
export class SqlQuery<T = any> implements PromiseLike<T[]> {
|
|
100
|
+
readonly strings: readonly string[];
|
|
101
|
+
readonly values: any[];
|
|
102
|
+
private executor: SqlExecutor;
|
|
103
|
+
private queryOptions?: QueryOptions;
|
|
104
|
+
private executed?: Promise<any>;
|
|
105
|
+
private executedMeta?: boolean;
|
|
106
|
+
|
|
107
|
+
constructor(executor: SqlExecutor, strings: readonly string[], values: any[]) {
|
|
108
|
+
this.executor = executor;
|
|
109
|
+
this.strings = strings;
|
|
110
|
+
this.values = values;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The compiled SQL text (`?` placeholders) and parameter array. */
|
|
114
|
+
toQuery(): CompiledQuery {
|
|
115
|
+
return compile(this.strings, this.values);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Attach per-query options (timeout, signal, nestTables, …). Must be
|
|
120
|
+
* called before the query executes — options attached afterwards would
|
|
121
|
+
* be silently ignored, so that throws instead.
|
|
122
|
+
*/
|
|
123
|
+
options(queryOptions: QueryOptions): this {
|
|
124
|
+
if (this.executed) {
|
|
125
|
+
throw new Error('sql query already executed — call .options() before awaiting it');
|
|
126
|
+
}
|
|
127
|
+
this.queryOptions = { ...this.queryOptions, ...queryOptions };
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Execute resolving the full { rows, fields, affectedRows, … } result. */
|
|
132
|
+
withMeta(): Promise<QueryResult<T>> {
|
|
133
|
+
return this.run(true);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A query executes exactly once, in the shape of its first consumer
|
|
138
|
+
* (plain rows via then/await, or the full result via withMeta).
|
|
139
|
+
* Consuming it again in the OTHER shape cannot be honoured from the
|
|
140
|
+
* cached promise, so it throws rather than silently returning the
|
|
141
|
+
* wrong shape.
|
|
142
|
+
*/
|
|
143
|
+
private run(withMeta: boolean): Promise<any> {
|
|
144
|
+
if (this.executed) {
|
|
145
|
+
if (withMeta !== this.executedMeta) {
|
|
146
|
+
throw new Error(this.executedMeta
|
|
147
|
+
? 'sql query already executed via .withMeta() — await that result instead of the query'
|
|
148
|
+
: 'sql query already executed as plain rows — call .withMeta() first, or build a new query');
|
|
149
|
+
}
|
|
150
|
+
return this.executed;
|
|
151
|
+
}
|
|
152
|
+
this.executedMeta = withMeta;
|
|
153
|
+
const { text, params } = compile(this.strings, this.values);
|
|
154
|
+
const options = withMeta ? { ...this.queryOptions, withMeta: true } : this.queryOptions;
|
|
155
|
+
this.executed = this.executor(text, params, options);
|
|
156
|
+
return this.executed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
then<R1 = T[], R2 = never>(
|
|
160
|
+
onfulfilled?: ((value: T[]) => R1 | PromiseLike<R1>) | null,
|
|
161
|
+
onrejected?: ((reason: any) => R2 | PromiseLike<R2>) | null
|
|
162
|
+
): Promise<R1 | R2> {
|
|
163
|
+
return this.run(false).then(onfulfilled, onrejected);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
catch<R = never>(onrejected?: ((reason: any) => R | PromiseLike<R>) | null): Promise<T[] | R> {
|
|
167
|
+
return this.then(undefined, onrejected);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
finally(onfinally?: (() => void) | null): Promise<T[]> {
|
|
171
|
+
return this.run(false).finally(onfinally) as Promise<T[]>;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The dual-use tag: template tag executes, string call quotes an identifier. */
|
|
176
|
+
export interface SqlTag {
|
|
177
|
+
<T = any>(strings: TemplateStringsArray, ...values: any[]): SqlQuery<T>;
|
|
178
|
+
(identifier: string): SqlIdentifier;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Build the `sql` tag for a Database/Transaction. `executor` receives the
|
|
183
|
+
* compiled text, params and per-query options and must return a promise
|
|
184
|
+
* (Database/Transaction pass their queryAsync).
|
|
185
|
+
*/
|
|
186
|
+
export function makeSqlTag(executor: SqlExecutor): SqlTag {
|
|
187
|
+
return function sql(first: any, ...values: any[]): any {
|
|
188
|
+
if (Array.isArray(first) && Object.prototype.hasOwnProperty.call(first, 'raw')) {
|
|
189
|
+
return new SqlQuery(executor, first, values);
|
|
190
|
+
}
|
|
191
|
+
if (typeof first === 'string') {
|
|
192
|
+
return new SqlIdentifier(first);
|
|
193
|
+
}
|
|
194
|
+
throw new Error('sql must be used as a template tag (sql`...`) or called with an identifier string (sql(\'NAME\'))');
|
|
195
|
+
} as SqlTag;
|
|
196
|
+
}
|