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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-firebird",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "Pure JavaScript and Asynchronous Firebird client for Node.js.",
5
5
  "keywords": [
6
6
  "firebird",
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
- if (this.idleTimeoutMillis) {
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 idle timeout (bounded to 100ms..30s) so a
63
- // connection lives at most ~1.5x idleTimeoutMillis. unref() keeps
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(this.idleTimeoutMillis / 2, 100), 30000);
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._forget(db);
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
+ }