node-firebird 2.11.0 → 2.13.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.
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /***************************************
3
+ *
4
+ * Tagged-template query API (Postgres.js-style)
5
+ *
6
+ * db.sql`SELECT * FROM EMP WHERE ID = ${id}` → lazy thenable query
7
+ * db.sql('COLUMN NAME') → quoted identifier
8
+ *
9
+ * Interpolated values become positional `?` parameters — never string
10
+ * concatenation — so the API is injection-safe by construction. A query
11
+ * embedded inside another tag is treated as a fragment: its text and
12
+ * parameters are spliced in place. Arrays expand to `?, ?, ?` lists for
13
+ * IN clauses. Execution is lazy (on await/then) and happens exactly once.
14
+ *
15
+ ***************************************/
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.SqlQuery = exports.SqlIdentifier = void 0;
18
+ exports.quoteIdentifier = quoteIdentifier;
19
+ exports.makeSqlTag = makeSqlTag;
20
+ /** A dynamically quoted identifier produced by sql('name'). */
21
+ class SqlIdentifier {
22
+ constructor(name) {
23
+ this.name = name;
24
+ }
25
+ }
26
+ exports.SqlIdentifier = SqlIdentifier;
27
+ /**
28
+ * Quote a (possibly dot-qualified) identifier for dialect 3: each part is
29
+ * wrapped in double quotes with embedded quotes doubled, so user input can
30
+ * never break out of the identifier position.
31
+ */
32
+ function quoteIdentifier(name) {
33
+ return String(name)
34
+ .split('.')
35
+ .map((part) => '"' + part.replace(/"/g, '""') + '"')
36
+ .join('.');
37
+ }
38
+ function compile(strings, values, active) {
39
+ let text = '';
40
+ const params = [];
41
+ for (let i = 0; i < strings.length; i++) {
42
+ text += strings[i];
43
+ if (i >= values.length) {
44
+ continue;
45
+ }
46
+ const value = values[i];
47
+ if (value instanceof SqlIdentifier) {
48
+ text += quoteIdentifier(value.name);
49
+ }
50
+ else if (value instanceof SqlQuery) {
51
+ // embedded fragment: splice its text and params in place. The
52
+ // same fragment may appear several times (a DAG), but a fragment
53
+ // containing itself would recurse forever — track the expansion
54
+ // stack and reject cycles with a diagnosable error.
55
+ active = active || new Set();
56
+ if (active.has(value)) {
57
+ throw new Error('circular sql fragment: a query is embedded (transitively) inside itself');
58
+ }
59
+ active.add(value);
60
+ const inner = compile(value.strings, value.values, active);
61
+ active.delete(value);
62
+ text += inner.text;
63
+ params.push(...inner.params);
64
+ }
65
+ else if (Array.isArray(value)) {
66
+ // IN (${[1, 2, 3]}) → IN (?, ?, ?)
67
+ if (!value.length) {
68
+ // '' would compile to `IN ()` — invalid SQL raising a server
69
+ // syntax error the caller never wrote; fail early instead
70
+ throw new Error('cannot interpolate an empty array (would compile to invalid SQL like "IN ()")');
71
+ }
72
+ text += value.map(() => '?').join(', ');
73
+ params.push(...value);
74
+ }
75
+ else {
76
+ text += '?';
77
+ params.push(value);
78
+ }
79
+ }
80
+ return { text, params };
81
+ }
82
+ /**
83
+ * A lazily executed tagged query. Awaiting it (or calling then/catch/
84
+ * finally) runs it through the owning Database/Transaction exactly once;
85
+ * embedding it in another tag uses it as a fragment instead and never
86
+ * executes it.
87
+ */
88
+ class SqlQuery {
89
+ constructor(executor, strings, values) {
90
+ this.executor = executor;
91
+ this.strings = strings;
92
+ this.values = values;
93
+ }
94
+ /** The compiled SQL text (`?` placeholders) and parameter array. */
95
+ toQuery() {
96
+ return compile(this.strings, this.values);
97
+ }
98
+ /**
99
+ * Attach per-query options (timeout, signal, nestTables, …). Must be
100
+ * called before the query executes — options attached afterwards would
101
+ * be silently ignored, so that throws instead.
102
+ */
103
+ options(queryOptions) {
104
+ if (this.executed) {
105
+ throw new Error('sql query already executed — call .options() before awaiting it');
106
+ }
107
+ this.queryOptions = { ...this.queryOptions, ...queryOptions };
108
+ return this;
109
+ }
110
+ /** Execute resolving the full { rows, fields, affectedRows, … } result. */
111
+ withMeta() {
112
+ return this.run(true);
113
+ }
114
+ /**
115
+ * A query executes exactly once, in the shape of its first consumer
116
+ * (plain rows via then/await, or the full result via withMeta).
117
+ * Consuming it again in the OTHER shape cannot be honoured from the
118
+ * cached promise, so it throws rather than silently returning the
119
+ * wrong shape.
120
+ */
121
+ run(withMeta) {
122
+ if (this.executed) {
123
+ if (withMeta !== this.executedMeta) {
124
+ throw new Error(this.executedMeta
125
+ ? 'sql query already executed via .withMeta() — await that result instead of the query'
126
+ : 'sql query already executed as plain rows — call .withMeta() first, or build a new query');
127
+ }
128
+ return this.executed;
129
+ }
130
+ this.executedMeta = withMeta;
131
+ const { text, params } = compile(this.strings, this.values);
132
+ const options = withMeta ? { ...this.queryOptions, withMeta: true } : this.queryOptions;
133
+ this.executed = this.executor(text, params, options);
134
+ return this.executed;
135
+ }
136
+ then(onfulfilled, onrejected) {
137
+ return this.run(false).then(onfulfilled, onrejected);
138
+ }
139
+ catch(onrejected) {
140
+ return this.then(undefined, onrejected);
141
+ }
142
+ finally(onfinally) {
143
+ return this.run(false).finally(onfinally);
144
+ }
145
+ }
146
+ exports.SqlQuery = SqlQuery;
147
+ /**
148
+ * Build the `sql` tag for a Database/Transaction. `executor` receives the
149
+ * compiled text, params and per-query options and must return a promise
150
+ * (Database/Transaction pass their queryAsync).
151
+ */
152
+ function makeSqlTag(executor) {
153
+ return function sql(first, ...values) {
154
+ if (Array.isArray(first) && Object.prototype.hasOwnProperty.call(first, 'raw')) {
155
+ return new SqlQuery(executor, first, values);
156
+ }
157
+ if (typeof first === 'string') {
158
+ return new SqlIdentifier(first);
159
+ }
160
+ throw new Error('sql must be used as a template tag (sql`...`) or called with an identifier string (sql(\'NAME\'))');
161
+ };
162
+ }
package/lib/types.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { Readable } from 'stream';
1
+ import type { Readable, Writable } from 'stream';
2
+ import type { SqlTag } from './sql-template';
3
+ export type { SqlTag, SqlQuery, SqlIdentifier, CompiledQuery } from './sql-template';
2
4
  export type DatabaseCallback = (err: any, db: Database) => void;
3
5
  export type TransactionCallback = (err: any, transaction: Transaction) => void;
4
6
  export type QueryCallback = (err: any, result: any[]) => void;
@@ -127,7 +129,84 @@ export type QueryOptions = {
127
129
  * unaffected.
128
130
  */
129
131
  nestTables?: boolean | string;
132
+ /**
133
+ * Per-query override of the `transformKeys` connection option: rewrite
134
+ * object-row keys — `'camel'` turns `FIRST_NAME` into `firstName`, or
135
+ * pass a custom `(key) => key` mapper. Applied after `lowercase_keys`
136
+ * and to both parts of `nestTables` keys. Column metadata (`fields`,
137
+ * typeCast) keeps the raw server aliases.
138
+ */
139
+ transformKeys?: 'camel' | ((key: string) => string);
140
+ /**
141
+ * Deliver a full result object `{ rows, fields, affectedRows,
142
+ * recordCounts, warnings }` instead of the bare rows (callback and
143
+ * promise APIs). For DML, `affectedRows` is what the server actually
144
+ * changed (`isc_info_sql_records`, one extra lightweight info request
145
+ * per statement — hence opt-in) and `recordCounts` breaks it down per
146
+ * verb; for SELECT it is the number of rows returned (pg's `rowCount`
147
+ * convention) with no extra round-trip. `warnings` carries any
148
+ * `isc_arg_warning` entries from the execute response. Honoured by
149
+ * query/execute and their *Async wrappers only — ignored by the
150
+ * streaming APIs (sequentially/queryStream, where rows bypass the
151
+ * result) and executeBatch (which has its own completion shape).
152
+ */
153
+ withMeta?: boolean;
154
+ };
155
+ /** Column metadata delivered in withMeta results (`fields`) — the same
156
+ * vocabulary the typeCast hook receives, plus nullable and the relation
157
+ * alias/schema. */
158
+ export interface FieldMetadata {
159
+ type: number;
160
+ typeName: string;
161
+ subType?: number;
162
+ scale?: number;
163
+ length?: number;
164
+ nullable?: boolean;
165
+ field?: string;
166
+ relation?: string;
167
+ relationAlias?: string;
168
+ relationSchema?: string;
169
+ alias?: string;
170
+ }
171
+ /** Per-verb server row counts of an executed DML statement. */
172
+ export interface RecordCounts {
173
+ selectCount: number;
174
+ insertCount: number;
175
+ updateCount: number;
176
+ deleteCount: number;
177
+ }
178
+ /** An isc_arg_warning entry from a server response ('warning' driver event
179
+ * and withMeta `warnings`). */
180
+ export interface ServerWarning {
181
+ gdscode: number;
182
+ params?: (string | number)[];
183
+ message: string;
184
+ }
185
+ /** Full result shape delivered when `withMeta: true` is set. */
186
+ export interface QueryResult<T = any> {
187
+ /** Rows array (SELECT), single row object (RETURNING / procedures), or undefined (plain DML). */
188
+ rows: T[] | T | undefined;
189
+ fields: FieldMetadata[];
190
+ /** DML: rows the server changed; SELECT: rows returned. */
191
+ affectedRows: number;
192
+ /** Set for DML statements only. */
193
+ recordCounts?: RecordCounts;
194
+ warnings: ServerWarning[];
195
+ }
196
+ /** Options for batchStream: the executeBatch options plus stream tuning. */
197
+ export type BatchStreamOptions = BatchOptions & {
198
+ /** Rows buffered per executeBatch flush (default 1000). */
199
+ flushRows?: number;
200
+ /** Writable highWaterMark in rows (default: flushRows). */
201
+ highWaterMark?: number;
130
202
  };
203
+ /** The Writable returned by batchStream, with totals valid after 'finish'. */
204
+ export interface BatchStream extends Writable {
205
+ /** Records the server processed so far. */
206
+ recordCount: number;
207
+ /** Sum of per-record update counts so far. */
208
+ affectedRows: number;
209
+ }
131
210
  export type QueryStreamOptions = QueryOptions & {
132
211
  /**
133
212
  * Rows buffered internally before fetching pauses (object-mode
@@ -138,11 +217,18 @@ export type QueryStreamOptions = QueryOptions & {
138
217
  asObject?: boolean;
139
218
  };
140
219
  export interface Database {
220
+ /**
221
+ * Tagged-template query API (Postgres.js-style): interpolated values
222
+ * become positional parameters, `sql('NAME')` quotes an identifier,
223
+ * embedded `sql` fragments compose, arrays expand to `?, ?, ?` lists.
224
+ * The returned query is a lazy thenable — it executes once, on await.
225
+ */
226
+ sql: SqlTag;
141
227
  detach(callback?: SimpleCallback): Database;
142
228
  transaction(options: TransactionOptions | Isolation | TransactionCallback, callback?: TransactionCallback): Database;
143
229
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
144
- query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): Database;
145
- execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): Database;
230
+ query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
231
+ execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): Database;
146
232
  /** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
147
233
  executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
148
234
  sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
@@ -153,6 +239,13 @@ export interface Database {
153
239
  * fetch and releases the statement.
154
240
  */
155
241
  queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
242
+ /**
243
+ * Bulk-insert Writable (COPY FROM analogue, Firebird 4.0+): write
244
+ * parameter-array rows; they are flushed in chunks through the batch
245
+ * API. Runs its own transaction — committed on finish, rolled back on
246
+ * error/destroy. BLOB columns accept Buffers/strings.
247
+ */
248
+ batchStream(query: string, options?: BatchStreamOptions): BatchStream;
156
249
  drop(callback: SimpleCallback): void;
157
250
  escape(value: any): string;
158
251
  attachEvent(callback: any): this;
@@ -160,7 +253,13 @@ export interface Database {
160
253
  alterTablespace(name: string, filePath: string, callback?: QueryCallback): Database;
161
254
  dropTablespace(name: string, callback?: QueryCallback): Database;
162
255
  createSchema(schemaName: string, tablespaceName?: string | QueryCallback, callback?: QueryCallback): Database;
256
+ queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
257
+ withMeta: true;
258
+ }): Promise<QueryResult<T>>;
163
259
  queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
260
+ executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
261
+ withMeta: true;
262
+ }): Promise<QueryResult<T>>;
164
263
  executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
165
264
  executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
166
265
  sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
@@ -183,9 +282,17 @@ export interface Database {
183
282
  cancelAsync(kind?: number): Promise<void>;
184
283
  }
185
284
  export interface Transaction {
285
+ /** Tagged-template query API running inside this transaction (see Database.sql). */
286
+ sql: SqlTag;
287
+ /**
288
+ * Run `work` inside a savepoint: released on resolve, rolled back TO
289
+ * (undoing only work's changes) on reject — the transaction stays
290
+ * usable either way. Nestable.
291
+ */
292
+ savepoint<T>(work: (transaction: Transaction) => Promise<T> | T): Promise<T>;
186
293
  newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
187
- query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
188
- execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
294
+ query<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
295
+ execute<T = any>(query: string, params: QueryParams, callback: (err: any, result: T[], meta?: any[], isSelect?: boolean) => void, options?: QueryOptions): void;
189
296
  /** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
190
297
  executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
191
298
  sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
@@ -195,11 +302,22 @@ export interface Transaction {
195
302
  * transaction is NOT committed when the stream ends.
196
303
  */
197
304
  queryStream(query: string, params?: QueryParams, options?: QueryStreamOptions): Readable;
305
+ /**
306
+ * Bulk-insert Writable inside this transaction (see
307
+ * Database.batchStream); commit/rollback stays with the caller.
308
+ */
309
+ batchStream(query: string, options?: BatchStreamOptions): BatchStream;
198
310
  commit(callback?: SimpleCallback): void;
199
311
  commitRetaining(callback?: SimpleCallback): void;
200
312
  rollback(callback?: SimpleCallback): void;
201
313
  rollbackRetaining(callback?: SimpleCallback): void;
314
+ queryAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
315
+ withMeta: true;
316
+ }): Promise<QueryResult<T>>;
202
317
  queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
318
+ executeAsync<T = any>(query: string, params: QueryParams | undefined, options: QueryOptions & {
319
+ withMeta: true;
320
+ }): Promise<QueryResult<T>>;
203
321
  executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
204
322
  executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
205
323
  sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
@@ -266,6 +384,13 @@ export interface Options {
266
384
  * per-query `namedPlaceholders: false` override.
267
385
  */
268
386
  namedPlaceholders?: boolean;
387
+ /**
388
+ * Default character set of a NEWLY CREATED database (create /
389
+ * attachOrCreate only). Falls back to the connection `encoding`, then
390
+ * UTF8 — pass e.g. `defaultCharset: 'UTF8'` to keep a modern database
391
+ * default while connecting with a legacy codepage `encoding`.
392
+ */
393
+ defaultCharset?: string;
269
394
  /**
270
395
  * Qualify object-row keys by source table (same option as mysql2), so
271
396
  * JOINed columns with the same name stop overwriting each other:
@@ -276,6 +401,14 @@ export interface Options {
276
401
  * array rows (execute) are unaffected. Overridable per query.
277
402
  */
278
403
  nestTables?: boolean | string;
404
+ /**
405
+ * Rewrite object-row keys (Postgres.js `transform` counterpart):
406
+ * `'camel'` turns `FIRST_NAME` into `firstName`, or pass a custom
407
+ * `(key) => key` mapper. Applied after `lowercase_keys` and to both
408
+ * parts of `nestTables` keys; column metadata keeps raw aliases.
409
+ * Overridable per query.
410
+ */
411
+ transformKeys?: 'camel' | ((key: string) => string);
279
412
  /**
280
413
  * TCP keepalive probing to detect dead/stale connections (same option
281
414
  * names as mysql2). On by default; set false to disable.
@@ -314,6 +447,20 @@ export interface Options {
314
447
  * Default 0 (idle connections are kept forever).
315
448
  */
316
449
  idleTimeoutMillis?: number;
450
+ /**
451
+ * Pool only: retire a physical connection after this many checkouts
452
+ * (pg's `maxUses`) — it is closed for good when returned to the pool
453
+ * and replaced on demand. Bounds server-side resource drift on
454
+ * long-lived connections. Default 0 (unlimited uses).
455
+ */
456
+ maxUses?: number;
457
+ /**
458
+ * Pool only: retire a physical connection this many milliseconds after
459
+ * it was created (Postgres.js's `max_lifetime`), on return to the pool
460
+ * or by the idle sweep — even below `min`; replacements are created on
461
+ * demand. Default 0 (unlimited lifetime).
462
+ */
463
+ maxLifetimeMillis?: number;
317
464
  /**
318
465
  * **Firebird 6.0+ only (Protocol 20+)**
319
466
  *
package/lib/uri.js CHANGED
@@ -187,7 +187,58 @@ function parseConnectionString(str) {
187
187
  */
188
188
  function normalizeOptions(options) {
189
189
  if (typeof options === 'string') {
190
- return parseConnectionString(options);
190
+ options = parseConnectionString(options);
191
191
  }
192
- return options;
192
+ return applyEnvDefaults(options);
193
+ }
194
+ /**
195
+ * Fall back to environment variables for connection settings the caller
196
+ * did not provide — the pg-style convention using Firebird's own names:
197
+ * ISC_USER / ISC_PASSWORD (honoured by isql and every official tool) plus
198
+ * FIREBIRD_HOST / FIREBIRD_PORT / FIREBIRD_DATABASE / FIREBIRD_ROLE.
199
+ * Explicit options always win; the driver's built-in defaults (SYSDBA /
200
+ * masterkey / 127.0.0.1) still apply when neither is set. A fresh object
201
+ * is returned so caller-owned options objects are never mutated.
202
+ */
203
+ const ENV_FALLBACKS = [
204
+ ['user', 'ISC_USER'],
205
+ ['password', 'ISC_PASSWORD'],
206
+ ['host', 'FIREBIRD_HOST'],
207
+ ['port', 'FIREBIRD_PORT'],
208
+ ['database', 'FIREBIRD_DATABASE'],
209
+ ['role', 'FIREBIRD_ROLE'],
210
+ ];
211
+ function applyEnvDefaults(options) {
212
+ let out = options;
213
+ for (const [key, envName] of ENV_FALLBACKS) {
214
+ const value = process.env[envName];
215
+ // empty-string env vars (common in CI: `export ISC_PASSWORD=`)
216
+ // count as unset
217
+ if (value === undefined || value === '') {
218
+ continue;
219
+ }
220
+ // a service-manager connection's `database` selects the TARGET of
221
+ // backup/restore — never let a leftover env var pick that silently
222
+ if (key === 'database' && options.manager) {
223
+ continue;
224
+ }
225
+ if (out[key] === undefined || out[key] === null || out[key] === '') {
226
+ if (out === options) {
227
+ out = { ...options };
228
+ }
229
+ if (key === 'port') {
230
+ const port = Number(value);
231
+ if (!Number.isFinite(port) || port <= 0) {
232
+ // NaN is falsy: it would silently fall back to 3050
233
+ // downstream instead of surfacing the typo
234
+ throw new Error('Invalid FIREBIRD_PORT environment variable: ' + value);
235
+ }
236
+ out[key] = port;
237
+ }
238
+ else {
239
+ out[key] = value;
240
+ }
241
+ }
242
+ }
243
+ return out;
193
244
  }
package/lib/utils.d.ts CHANGED
@@ -6,6 +6,18 @@ export declare const parseDate: (str: string) => Date;
6
6
  /**
7
7
  * Get Error Message per gdscode
8
8
  */
9
+ /**
10
+ * Turn a failed executeBatch completion into the all-or-nothing error
11
+ * shape shared by database.executeBatch and batchStream: the first
12
+ * record's own error (or a synthesized summary), with the full
13
+ * completion attached as err.batchCompletion.
14
+ */
15
+ export declare const batchResultToError: (result: {
16
+ errors: {
17
+ error: any;
18
+ }[];
19
+ errorRecordNumbers: number[];
20
+ }) => any;
9
21
  export declare const lookupMessages: (status: FbStatusItem[]) => string;
10
22
  /**
11
23
  * Escape value
package/lib/utils.js CHANGED
@@ -3,7 +3,7 @@ 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.escape = exports.lookupMessages = exports.parseDate = void 0;
6
+ exports.escape = exports.lookupMessages = exports.batchResultToError = exports.parseDate = void 0;
7
7
  exports.noop = noop;
8
8
  const firebird_msg_json_1 = __importDefault(require("./firebird.msg.json"));
9
9
  const const_1 = __importDefault(require("./wire/const"));
@@ -97,6 +97,21 @@ exports.parseDate = parseDate;
97
97
  /**
98
98
  * Get Error Message per gdscode
99
99
  */
100
+ /**
101
+ * Turn a failed executeBatch completion into the all-or-nothing error
102
+ * shape shared by database.executeBatch and batchStream: the first
103
+ * record's own error (or a synthesized summary), with the full
104
+ * completion attached as err.batchCompletion.
105
+ */
106
+ const batchResultToError = (result) => {
107
+ const first = result.errors.length ? result.errors[0] : null;
108
+ const err = first
109
+ ? first.error
110
+ : new Error('Batch failed for record(s) ' + result.errorRecordNumbers.join(', '));
111
+ err.batchCompletion = result;
112
+ return err;
113
+ };
114
+ exports.batchResultToError = batchResultToError;
100
115
  const lookupMessages = (status) => {
101
116
  const messages = status.map((item) => {
102
117
  let text = MessagesError[item.gdscode];
@@ -130,7 +145,10 @@ const escape = function (value, protocolVersion) {
130
145
  case 'number':
131
146
  return value.toString();
132
147
  case 'string':
133
- return "'" + value.replace(/'/g, "''").replace(/\\/g, '\\\\') + "'";
148
+ // Firebird string literals have NO backslash escapes — only the
149
+ // quote is doubled. Doubling backslashes corrupted the data
150
+ // (issue #156: '\' arrived as '\\').
151
+ return "'" + value.replace(/'/g, "''") + "'";
134
152
  }
135
153
  if (value instanceof Date)
136
154
  return "'" + value.getFullYear() + '-' + (value.getMonth() + 1).toString().padStart(2, '0') + '-' + value.getDate().toString().padStart(2, '0') + ' ' + value.getHours().toString().padStart(2, '0') + ':' + value.getMinutes().toString().padStart(2, '0') + ':' + value.getSeconds().toString().padStart(2, '0') + '.' + value.getMilliseconds().toString().padStart(3, '0') + "'";
@@ -0,0 +1,26 @@
1
+ /***************************************
2
+ *
3
+ * batchStream — object-mode Writable over the Firebird 4 batch API
4
+ *
5
+ * The COPY FROM analogue: write parameter rows, they are flushed in
6
+ * chunks through statement.executeBatch (single prepared statement,
7
+ * protocol-level batching, BLOB values included). Backpressure is the
8
+ * Writable machinery itself: a write callback is held while a chunk
9
+ * is in flight.
10
+ *
11
+ ***************************************/
12
+ import { Writable } from 'stream';
13
+ /**
14
+ * Build the Writable for Database.batchStream / Transaction.batchStream.
15
+ * With `ownsTransaction` (the Database form) the stream runs its own
16
+ * transaction: committed on finish, rolled back on error/destroy —
17
+ * all-or-nothing for the whole stream. The Transaction form leaves
18
+ * commit/rollback to the caller.
19
+ *
20
+ * Rows accumulate up to options.flushRows (default 1000) per
21
+ * executeBatch flush; the remaining executeBatch options (chunkSize,
22
+ * bufferSize, …) pass through. After 'finish', stream.recordCount and
23
+ * stream.affectedRows carry the totals.
24
+ */
25
+ declare function makeBatchStream(target: any, query: string, options: any, ownsTransaction: boolean): Writable;
26
+ export = makeBatchStream;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /***************************************
3
+ *
4
+ * batchStream — object-mode Writable over the Firebird 4 batch API
5
+ *
6
+ * The COPY FROM analogue: write parameter rows, they are flushed in
7
+ * chunks through statement.executeBatch (single prepared statement,
8
+ * protocol-level batching, BLOB values included). Backpressure is the
9
+ * Writable machinery itself: a write callback is held while a chunk
10
+ * is in flight.
11
+ *
12
+ ***************************************/
13
+ const stream_1 = require("stream");
14
+ const callback_1 = require("../callback");
15
+ const utils_1 = require("../utils");
16
+ /**
17
+ * Build the Writable for Database.batchStream / Transaction.batchStream.
18
+ * With `ownsTransaction` (the Database form) the stream runs its own
19
+ * transaction: committed on finish, rolled back on error/destroy —
20
+ * all-or-nothing for the whole stream. The Transaction form leaves
21
+ * commit/rollback to the caller.
22
+ *
23
+ * Rows accumulate up to options.flushRows (default 1000) per
24
+ * executeBatch flush; the remaining executeBatch options (chunkSize,
25
+ * bufferSize, …) pass through. After 'finish', stream.recordCount and
26
+ * stream.affectedRows carry the totals.
27
+ */
28
+ function makeBatchStream(target, query, options, ownsTransaction) {
29
+ options = options || {};
30
+ const flushRows = options.flushRows > 0 ? Math.floor(options.flushRows) : 1000;
31
+ const batchOptions = { ...options };
32
+ delete batchOptions.flushRows;
33
+ delete batchOptions.highWaterMark;
34
+ let transaction = null;
35
+ let statement = null;
36
+ let buffered = [];
37
+ const init = async () => {
38
+ if (statement) {
39
+ return;
40
+ }
41
+ transaction = ownsTransaction ? await target.transactionAsync() : target;
42
+ statement = await (0, callback_1.fromCallback)((cb) => transaction.newStatement(query, cb));
43
+ };
44
+ const flush = async () => {
45
+ if (!buffered.length) {
46
+ return;
47
+ }
48
+ await init();
49
+ const chunk = buffered;
50
+ buffered = [];
51
+ const result = await (0, callback_1.fromCallback)((cb) => statement.executeBatch(transaction, chunk, cb, batchOptions));
52
+ if (!result.success) {
53
+ // the same all-or-nothing error shape database.executeBatch uses
54
+ throw (0, utils_1.batchResultToError)(result);
55
+ }
56
+ stream.recordCount += result.recordCount;
57
+ for (const count of result.updateCounts) {
58
+ stream.affectedRows += count;
59
+ }
60
+ };
61
+ const cleanup = async (commit) => {
62
+ if (statement) {
63
+ const stmt = statement;
64
+ statement = null;
65
+ await new Promise((resolve) => stmt.release(() => resolve()));
66
+ }
67
+ if (ownsTransaction && transaction) {
68
+ const tx = transaction;
69
+ transaction = null;
70
+ await (commit ? tx.commitAsync() : tx.rollbackAsync());
71
+ }
72
+ };
73
+ const stream = new stream_1.Writable({
74
+ objectMode: true,
75
+ highWaterMark: options.highWaterMark > 0 ? options.highWaterMark : flushRows,
76
+ write(row, _enc, cb) {
77
+ if (!Array.isArray(row)) {
78
+ cb(new Error('batchStream expects parameter-array rows'));
79
+ return;
80
+ }
81
+ buffered.push(row);
82
+ if (buffered.length >= flushRows) {
83
+ flush().then(() => cb(), cb);
84
+ }
85
+ else {
86
+ cb();
87
+ }
88
+ },
89
+ final(cb) {
90
+ // an empty stream finishes without touching the server at all
91
+ // (flush() early-returns and init never runs)
92
+ flush()
93
+ .then(() => cleanup(true))
94
+ .then(() => cb(), (err) => {
95
+ // the failed stream must not commit half a bulk load
96
+ cleanup(false).catch(() => { });
97
+ cb(err);
98
+ });
99
+ },
100
+ destroy(err, cb) {
101
+ cleanup(false)
102
+ .then(() => cb(err), () => cb(err));
103
+ },
104
+ });
105
+ stream.recordCount = 0;
106
+ stream.affectedRows = 0;
107
+ return stream;
108
+ }
109
+ module.exports = makeBatchStream;
@@ -0,0 +1,23 @@
1
+ /***************************************
2
+ *
3
+ * Single-byte codepage codecs (WIN125x, ISO8859_x, KOI8, DOS866)
4
+ *
5
+ * Node's Buffer only decodes utf8/latin1/ascii natively. These
6
+ * codepages are decoded through the WHATWG TextDecoder (backed by
7
+ * ICU — present in every official Node build) and encoded through
8
+ * reverse tables built from the same decoder at first use, so the
9
+ * two directions can never disagree. Issues #319/#301/#422.
10
+ *
11
+ ***************************************/
12
+ export interface TextCodec {
13
+ /** Firebird charset name (upper case). */
14
+ name: string;
15
+ decode(buffer: Buffer): string;
16
+ encode(value: string): Buffer;
17
+ }
18
+ export declare function charsetWidthById(id: number | undefined): number;
19
+ /**
20
+ * Codec for a Firebird charset name, or null when the charset is unknown,
21
+ * natively handled by Buffer, or the ICU tables are unavailable. Cached.
22
+ */
23
+ export declare function getCodec(charsetName: string | undefined): TextCodec | null;