@bdkinc/knex-ibmi 0.4.0 → 0.4.3
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 +73 -32
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +453 -65
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +453 -65
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -186,6 +186,68 @@ var IBMiColumnCompiler = class extends import_columncompiler.default {
|
|
|
186
186
|
increments(options = { primaryKey: true }) {
|
|
187
187
|
return "int not null generated always as identity (start with 1, increment by 1)" + (this.tableCompiler._canBeAddPrimaryKey(options) ? " primary key" : "");
|
|
188
188
|
}
|
|
189
|
+
// Add more IBM i DB2 specific column types for better support
|
|
190
|
+
bigIncrements(options = { primaryKey: true }) {
|
|
191
|
+
return "bigint not null generated always as identity (start with 1, increment by 1)" + (this.tableCompiler._canBeAddPrimaryKey(options) ? " primary key" : "");
|
|
192
|
+
}
|
|
193
|
+
varchar(length) {
|
|
194
|
+
return length ? `varchar(${length})` : "varchar(255)";
|
|
195
|
+
}
|
|
196
|
+
char(length) {
|
|
197
|
+
return length ? `char(${length})` : "char(1)";
|
|
198
|
+
}
|
|
199
|
+
text() {
|
|
200
|
+
return "clob(1M)";
|
|
201
|
+
}
|
|
202
|
+
mediumtext() {
|
|
203
|
+
return "clob(16M)";
|
|
204
|
+
}
|
|
205
|
+
longtext() {
|
|
206
|
+
return "clob(2G)";
|
|
207
|
+
}
|
|
208
|
+
binary(length) {
|
|
209
|
+
return length ? `binary(${length})` : "binary(1)";
|
|
210
|
+
}
|
|
211
|
+
varbinary(length) {
|
|
212
|
+
return length ? `varbinary(${length})` : "varbinary(255)";
|
|
213
|
+
}
|
|
214
|
+
// IBM i DB2 decimal with precision/scale
|
|
215
|
+
decimal(precision, scale) {
|
|
216
|
+
if (precision && scale) {
|
|
217
|
+
return `decimal(${precision}, ${scale})`;
|
|
218
|
+
} else if (precision) {
|
|
219
|
+
return `decimal(${precision})`;
|
|
220
|
+
}
|
|
221
|
+
return "decimal(10, 2)";
|
|
222
|
+
}
|
|
223
|
+
// IBM i DB2 timestamp
|
|
224
|
+
timestamp(options) {
|
|
225
|
+
if (options?.useTz) {
|
|
226
|
+
return "timestamp with time zone";
|
|
227
|
+
}
|
|
228
|
+
return "timestamp";
|
|
229
|
+
}
|
|
230
|
+
datetime(options) {
|
|
231
|
+
return this.timestamp(options);
|
|
232
|
+
}
|
|
233
|
+
// IBM i DB2 date and time types
|
|
234
|
+
date() {
|
|
235
|
+
return "date";
|
|
236
|
+
}
|
|
237
|
+
time() {
|
|
238
|
+
return "time";
|
|
239
|
+
}
|
|
240
|
+
// JSON support (IBM i 7.3+)
|
|
241
|
+
json() {
|
|
242
|
+
return "clob(16M) check (json_valid(json_column))";
|
|
243
|
+
}
|
|
244
|
+
jsonb() {
|
|
245
|
+
return "clob(16M) check (json_valid(jsonb_column))";
|
|
246
|
+
}
|
|
247
|
+
// UUID support using CHAR(36)
|
|
248
|
+
uuid() {
|
|
249
|
+
return "char(36)";
|
|
250
|
+
}
|
|
189
251
|
};
|
|
190
252
|
var ibmi_columncompiler_default = IBMiColumnCompiler;
|
|
191
253
|
|
|
@@ -244,6 +306,16 @@ var ibmi_transaction_default = IBMiTransaction;
|
|
|
244
306
|
var import_querycompiler = __toESM(require("knex/lib/query/querycompiler.js"));
|
|
245
307
|
var import_wrappingFormatter = require("knex/lib/formatter/wrappingFormatter.js");
|
|
246
308
|
var IBMiQueryCompiler = class extends import_querycompiler.default {
|
|
309
|
+
constructor() {
|
|
310
|
+
super(...arguments);
|
|
311
|
+
// Cache for column metadata to improve performance with repeated operations
|
|
312
|
+
__publicField(this, "columnCache", /* @__PURE__ */ new Map());
|
|
313
|
+
}
|
|
314
|
+
// Override select method to add IBM i optimization hints
|
|
315
|
+
select() {
|
|
316
|
+
const originalResult = super.select.call(this);
|
|
317
|
+
return originalResult;
|
|
318
|
+
}
|
|
247
319
|
formatTimestampLocal(date) {
|
|
248
320
|
const pad = (n) => String(n).padStart(2, "0");
|
|
249
321
|
const y = date.getFullYear();
|
|
@@ -263,15 +335,42 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
|
|
|
263
335
|
}
|
|
264
336
|
return "";
|
|
265
337
|
}
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
338
|
+
const ibmiConfig = this.client?.config?.ibmi || {};
|
|
339
|
+
const multiRowStrategy = ibmiConfig.multiRowInsert || "auto";
|
|
340
|
+
const isArrayInsert = Array.isArray(insertValues) && insertValues.length > 1;
|
|
341
|
+
const originalValues = isArrayInsert ? insertValues.slice() : insertValues;
|
|
342
|
+
const forceSingleRow = multiRowStrategy === "disabled" || multiRowStrategy === "sequential" && isArrayInsert;
|
|
343
|
+
let workingValues = insertValues;
|
|
344
|
+
if (forceSingleRow && isArrayInsert) {
|
|
345
|
+
workingValues = [insertValues[0]];
|
|
346
|
+
this.single.insert = workingValues;
|
|
347
|
+
}
|
|
348
|
+
const standardInsert = super.insert();
|
|
349
|
+
const insertSql = typeof standardInsert === "object" && standardInsert.sql ? standardInsert.sql : standardInsert;
|
|
350
|
+
const multiRow = isArrayInsert && !forceSingleRow;
|
|
351
|
+
if (multiRow && returning === "*") {
|
|
352
|
+
if (this.client?.printWarn) {
|
|
353
|
+
this.client.printWarn("multi-row insert with returning * may be large");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const selectColumns = returning ? this.formatter.columnize(returning) : multiRow ? "*" : "IDENTITY_VAL_LOCAL()";
|
|
273
357
|
const sql = `select ${selectColumns} from FINAL TABLE(${insertSql})`;
|
|
274
|
-
|
|
358
|
+
if (multiRowStrategy === "sequential" && isArrayInsert) {
|
|
359
|
+
const first = originalValues[0];
|
|
360
|
+
const columns = Object.keys(first).sort();
|
|
361
|
+
return {
|
|
362
|
+
sql,
|
|
363
|
+
returning: void 0,
|
|
364
|
+
_ibmiSequentialInsert: {
|
|
365
|
+
columns,
|
|
366
|
+
rows: originalValues,
|
|
367
|
+
tableName: this.tableName,
|
|
368
|
+
returning: returning || null,
|
|
369
|
+
identityOnly: !returning
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return { sql, returning: void 0 };
|
|
275
374
|
}
|
|
276
375
|
isEmptyInsertValues(insertValues) {
|
|
277
376
|
return Array.isArray(insertValues) && insertValues.length === 0 || this.isEmptyObject(insertValues);
|
|
@@ -293,15 +392,47 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
|
|
|
293
392
|
_buildInsertData(insertValues, returningSql) {
|
|
294
393
|
const insertData = this._prepInsert(insertValues);
|
|
295
394
|
if (insertData.columns.length > 0) {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
395
|
+
const parts = [];
|
|
396
|
+
parts.push("(" + this.formatter.columnize(insertData.columns) + ") ");
|
|
397
|
+
if (returningSql) parts.push(returningSql);
|
|
398
|
+
parts.push("values ");
|
|
399
|
+
const rowsSql = [];
|
|
400
|
+
for (const row of insertData.values) {
|
|
401
|
+
const placeholders = row.map(() => "?").join(", ");
|
|
402
|
+
rowsSql.push("(" + placeholders + ")");
|
|
403
|
+
}
|
|
404
|
+
parts.push(rowsSql.join(", "));
|
|
405
|
+
return parts.join("");
|
|
299
406
|
}
|
|
300
407
|
if (Array.isArray(insertValues) && insertValues.length === 1 && insertValues[0]) {
|
|
301
|
-
return returningSql + this._emptyInsertValue;
|
|
408
|
+
return (returningSql || "") + this._emptyInsertValue;
|
|
302
409
|
}
|
|
303
410
|
return "";
|
|
304
411
|
}
|
|
412
|
+
generateCacheKey(data) {
|
|
413
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
414
|
+
return Object.keys(data[0] || {}).sort().join("|");
|
|
415
|
+
}
|
|
416
|
+
if (data && typeof data === "object") {
|
|
417
|
+
return Object.keys(data).sort().join("|");
|
|
418
|
+
}
|
|
419
|
+
return "";
|
|
420
|
+
}
|
|
421
|
+
buildFromCache(data, cachedColumns) {
|
|
422
|
+
const dataArray = Array.isArray(data) ? data : data ? [data] : [];
|
|
423
|
+
const values = [];
|
|
424
|
+
for (const item of dataArray) {
|
|
425
|
+
if (item == null) {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
const row = cachedColumns.map((column) => item[column] ?? void 0);
|
|
429
|
+
values.push(row);
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
columns: cachedColumns,
|
|
433
|
+
values
|
|
434
|
+
};
|
|
435
|
+
}
|
|
305
436
|
_prepInsert(data) {
|
|
306
437
|
if (typeof data === "object" && data?.migration_time) {
|
|
307
438
|
const parsed = new Date(data.migration_time);
|
|
@@ -323,25 +454,25 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
|
|
|
323
454
|
if (dataArray.length === 0) {
|
|
324
455
|
return { columns: [], values: [] };
|
|
325
456
|
}
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
457
|
+
const firstItem = dataArray[0];
|
|
458
|
+
if (!firstItem || typeof firstItem !== "object") {
|
|
459
|
+
return { columns: [], values: [] };
|
|
460
|
+
}
|
|
461
|
+
const cacheKey = this.generateCacheKey(firstItem);
|
|
462
|
+
let columns;
|
|
463
|
+
if (cacheKey && this.columnCache.has(cacheKey)) {
|
|
464
|
+
columns = this.columnCache.get(cacheKey);
|
|
465
|
+
} else {
|
|
466
|
+
columns = Object.keys(firstItem).sort();
|
|
467
|
+
if (cacheKey && columns.length > 0)
|
|
468
|
+
this.columnCache.set(cacheKey, columns);
|
|
331
469
|
}
|
|
332
|
-
const columns = Array.from(allColumns).sort();
|
|
333
470
|
const values = [];
|
|
334
471
|
for (const item of dataArray) {
|
|
335
|
-
if (item
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
const row = columns.map((column) => item[column] ?? void 0);
|
|
339
|
-
values.push(row);
|
|
472
|
+
if (!item || typeof item !== "object") continue;
|
|
473
|
+
values.push(columns.map((c) => item[c] ?? void 0));
|
|
340
474
|
}
|
|
341
|
-
return {
|
|
342
|
-
columns,
|
|
343
|
-
values
|
|
344
|
-
};
|
|
475
|
+
return { columns, values };
|
|
345
476
|
}
|
|
346
477
|
update() {
|
|
347
478
|
const withSQL = this.with();
|
|
@@ -350,6 +481,7 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
|
|
|
350
481
|
const order = this.order();
|
|
351
482
|
const limit = this.limit();
|
|
352
483
|
const { returning } = this.single;
|
|
484
|
+
const optimizationHints = "";
|
|
353
485
|
const baseUpdateSql = [
|
|
354
486
|
withSQL,
|
|
355
487
|
`update ${this.single.only ? "only " : ""}${this.tableName}`,
|
|
@@ -357,18 +489,46 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
|
|
|
357
489
|
updates.join(", "),
|
|
358
490
|
where,
|
|
359
491
|
order,
|
|
360
|
-
limit
|
|
492
|
+
limit,
|
|
493
|
+
optimizationHints
|
|
361
494
|
].filter(Boolean).join(" ");
|
|
362
495
|
if (returning) {
|
|
363
|
-
this.client.logger.warn?.(
|
|
364
|
-
"IBMi DB2 does not support returning in update statements, only inserts"
|
|
365
|
-
);
|
|
366
496
|
const selectColumns = this.formatter.columnize(this.single.returning);
|
|
367
|
-
const
|
|
368
|
-
return {
|
|
497
|
+
const expectedSql = `select ${selectColumns} from FINAL TABLE(${baseUpdateSql})`;
|
|
498
|
+
return {
|
|
499
|
+
sql: expectedSql,
|
|
500
|
+
returning,
|
|
501
|
+
_ibmiUpdateReturning: {
|
|
502
|
+
updateSql: baseUpdateSql,
|
|
503
|
+
selectColumns,
|
|
504
|
+
whereClause: where,
|
|
505
|
+
tableName: this.tableName
|
|
506
|
+
}
|
|
507
|
+
};
|
|
369
508
|
}
|
|
370
509
|
return { sql: baseUpdateSql, returning };
|
|
371
510
|
}
|
|
511
|
+
// Emulate DELETE ... RETURNING by compiling a FINAL TABLE wrapper for display and attaching metadata
|
|
512
|
+
del() {
|
|
513
|
+
const baseDelete = super.del();
|
|
514
|
+
const { returning } = this.single;
|
|
515
|
+
if (!returning) {
|
|
516
|
+
return { sql: baseDelete, returning: void 0 };
|
|
517
|
+
}
|
|
518
|
+
const deleteSql = typeof baseDelete === "object" && baseDelete.sql ? baseDelete.sql : baseDelete;
|
|
519
|
+
const selectColumns = this.formatter.columnize(returning);
|
|
520
|
+
const expectedSql = `select ${selectColumns} from FINAL TABLE(${deleteSql})`;
|
|
521
|
+
return {
|
|
522
|
+
sql: expectedSql,
|
|
523
|
+
returning,
|
|
524
|
+
_ibmiDeleteReturning: {
|
|
525
|
+
deleteSql,
|
|
526
|
+
selectColumns,
|
|
527
|
+
whereClause: this.where(),
|
|
528
|
+
tableName: this.tableName
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
372
532
|
/**
|
|
373
533
|
* Handle returning clause for IBMi DB2 queries
|
|
374
534
|
* Note: IBMi DB2 has limited support for RETURNING clauses
|
|
@@ -389,15 +549,26 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
|
|
|
389
549
|
return "";
|
|
390
550
|
}
|
|
391
551
|
}
|
|
552
|
+
getOptimizationHints(queryType, data) {
|
|
553
|
+
const hints = [];
|
|
554
|
+
if (queryType === "select") {
|
|
555
|
+
hints.push("WITH UR");
|
|
556
|
+
}
|
|
557
|
+
return hints.length > 0 ? " " + hints.join(" ") : "";
|
|
558
|
+
}
|
|
559
|
+
getSelectOptimizationHints(sql) {
|
|
560
|
+
const hints = [];
|
|
561
|
+
hints.push("WITH UR");
|
|
562
|
+
return hints.length > 0 ? " " + hints.join(" ") : "";
|
|
563
|
+
}
|
|
392
564
|
columnizeWithPrefix(prefix, target) {
|
|
393
565
|
const columns = typeof target === "string" ? [target] : target;
|
|
394
|
-
|
|
395
|
-
let i =
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
str += prefix + this.wrap(columns[i]);
|
|
566
|
+
const parts = [];
|
|
567
|
+
for (let i = 0; i < columns.length; i++) {
|
|
568
|
+
if (i > 0) parts.push(", ");
|
|
569
|
+
parts.push(prefix + this.wrap(columns[i]));
|
|
399
570
|
}
|
|
400
|
-
return
|
|
571
|
+
return parts.join("");
|
|
401
572
|
}
|
|
402
573
|
};
|
|
403
574
|
var ibmi_querycompiler_default = IBMiQueryCompiler;
|
|
@@ -607,7 +778,6 @@ function createIBMiMigrationRunner(knex2, config) {
|
|
|
607
778
|
// src/index.ts
|
|
608
779
|
var DB2Client = class extends import_knex.default.Client {
|
|
609
780
|
constructor(config) {
|
|
610
|
-
console.log("\u{1F3AF} DB2Client constructor called!");
|
|
611
781
|
super(config);
|
|
612
782
|
this.driverName = "odbc";
|
|
613
783
|
if (this.dialect && !this.config.client) {
|
|
@@ -650,6 +820,7 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
650
820
|
return import_odbc.default;
|
|
651
821
|
}
|
|
652
822
|
wrapIdentifierImpl(value) {
|
|
823
|
+
if (!value) return value;
|
|
653
824
|
if (value.includes("KNEX_MIGRATIONS") || value.includes("knex_migrations")) {
|
|
654
825
|
return value.toUpperCase();
|
|
655
826
|
}
|
|
@@ -724,6 +895,15 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
724
895
|
const queryObject = this.normalizeQueryObject(obj);
|
|
725
896
|
const method = this.determineQueryMethod(queryObject);
|
|
726
897
|
queryObject.sqlMethod = method;
|
|
898
|
+
if (queryObject._ibmiUpdateReturning) {
|
|
899
|
+
return await this.executeUpdateReturning(connection, queryObject);
|
|
900
|
+
}
|
|
901
|
+
if (queryObject._ibmiSequentialInsert) {
|
|
902
|
+
return await this.executeSequentialInsert(connection, queryObject);
|
|
903
|
+
}
|
|
904
|
+
if (queryObject._ibmiDeleteReturning) {
|
|
905
|
+
return await this.executeDeleteReturning(connection, queryObject);
|
|
906
|
+
}
|
|
727
907
|
if (import_node_process.default.env.DEBUG === "true" && queryObject.sql && (queryObject.sql.toLowerCase().includes("create table") || queryObject.sql.toLowerCase().includes("knex_migrations"))) {
|
|
728
908
|
this.printDebug(
|
|
729
909
|
`Executing ${method} query: ${queryObject.sql.substring(0, 200)}...`
|
|
@@ -746,31 +926,141 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
746
926
|
this.printDebug(`Query completed: ${method} (${endTime - startTime}ms)`);
|
|
747
927
|
return queryObject;
|
|
748
928
|
} catch (error) {
|
|
929
|
+
const wrappedError = this.wrapError(error, method, queryObject);
|
|
749
930
|
if (this.isConnectionError(error)) {
|
|
750
931
|
this.printError(
|
|
751
932
|
`Connection error during ${method} query: ${error.message}`
|
|
752
933
|
);
|
|
753
|
-
if (
|
|
754
|
-
this.
|
|
755
|
-
try {
|
|
756
|
-
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
757
|
-
if (this.isSelectMethod(method)) {
|
|
758
|
-
await this.executeSelectQuery(connection, queryObject);
|
|
759
|
-
} else {
|
|
760
|
-
await this.executeStatementQuery(connection, queryObject);
|
|
761
|
-
}
|
|
762
|
-
return queryObject;
|
|
763
|
-
} catch (retryError) {
|
|
764
|
-
this.printError(`Retry failed: ${retryError.message}`);
|
|
765
|
-
queryObject.response = { rows: [], rowCount: 0 };
|
|
766
|
-
return queryObject;
|
|
767
|
-
}
|
|
934
|
+
if (this.shouldRetryQuery(queryObject, method)) {
|
|
935
|
+
return await this.retryQuery(connection, queryObject, method);
|
|
768
936
|
}
|
|
769
|
-
throw
|
|
770
|
-
|
|
937
|
+
throw wrappedError;
|
|
938
|
+
}
|
|
939
|
+
throw wrappedError;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Execute UPDATE with returning clause using transaction + SELECT approach
|
|
944
|
+
* Since IBM i DB2 doesn't support FINAL TABLE with UPDATE, we:
|
|
945
|
+
* 1. Execute the UPDATE statement
|
|
946
|
+
* 2. Execute a SELECT to get the updated values using the same WHERE clause
|
|
947
|
+
*/
|
|
948
|
+
async executeUpdateReturning(connection, obj) {
|
|
949
|
+
const { _ibmiUpdateReturning } = obj;
|
|
950
|
+
const { updateSql, selectColumns, whereClause, tableName } = _ibmiUpdateReturning;
|
|
951
|
+
this.printDebug(
|
|
952
|
+
"Executing UPDATE with returning using transaction approach"
|
|
953
|
+
);
|
|
954
|
+
try {
|
|
955
|
+
const updateObj = {
|
|
956
|
+
sql: updateSql,
|
|
957
|
+
bindings: obj.bindings,
|
|
958
|
+
sqlMethod: "update"
|
|
959
|
+
};
|
|
960
|
+
await this.executeStatementQuery(connection, updateObj);
|
|
961
|
+
const selectSql = whereClause ? `select ${selectColumns} from ${tableName} ${whereClause}` : `select ${selectColumns} from ${tableName}`;
|
|
962
|
+
const updateSqlParts = updateSql.split(" where ");
|
|
963
|
+
const setClausePart = updateSqlParts[0];
|
|
964
|
+
const setBindingCount = (setClausePart.match(/\?/g) || []).length;
|
|
965
|
+
const whereBindings = obj.bindings ? obj.bindings.slice(setBindingCount) : [];
|
|
966
|
+
const selectObj = {
|
|
967
|
+
sql: selectSql,
|
|
968
|
+
bindings: whereBindings,
|
|
969
|
+
sqlMethod: "select",
|
|
970
|
+
response: void 0
|
|
971
|
+
};
|
|
972
|
+
await this.executeSelectQuery(connection, selectObj);
|
|
973
|
+
obj.response = selectObj.response;
|
|
974
|
+
obj.sqlMethod = "update";
|
|
975
|
+
obj.select = true;
|
|
976
|
+
return obj;
|
|
977
|
+
} catch (error) {
|
|
978
|
+
this.printError(`UPDATE with returning failed: ${error.message}`);
|
|
979
|
+
throw this.wrapError(error, "update_returning", obj);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async executeSequentialInsert(connection, obj) {
|
|
983
|
+
const meta = obj._ibmiSequentialInsert;
|
|
984
|
+
const { rows, columns, tableName, returning, identityOnly } = meta;
|
|
985
|
+
this.printDebug("Executing sequential multi-row insert");
|
|
986
|
+
const insertedRows = [];
|
|
987
|
+
const transactional = this.config?.ibmi?.sequentialInsertTransactional === true;
|
|
988
|
+
let beganTx = false;
|
|
989
|
+
if (transactional) {
|
|
990
|
+
try {
|
|
991
|
+
await connection.query("BEGIN");
|
|
992
|
+
beganTx = true;
|
|
993
|
+
} catch (e) {
|
|
994
|
+
this.printWarn(
|
|
995
|
+
"Could not begin transaction for sequential insert; proceeding without"
|
|
771
996
|
);
|
|
772
997
|
}
|
|
773
|
-
|
|
998
|
+
}
|
|
999
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1000
|
+
const row = rows[i];
|
|
1001
|
+
const colList = columns.join(", ");
|
|
1002
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
1003
|
+
const singleValues = columns.map((c) => row[c]);
|
|
1004
|
+
const baseInsert = `insert into ${tableName} (${colList}) values (${placeholders})`;
|
|
1005
|
+
const selectCols = returning ? this.queryCompiler({}).formatter.columnize(returning) : "IDENTITY_VAL_LOCAL()";
|
|
1006
|
+
const wrapped = `select ${selectCols} from FINAL TABLE(${baseInsert})`;
|
|
1007
|
+
const singleObj = {
|
|
1008
|
+
sql: wrapped,
|
|
1009
|
+
bindings: singleValues,
|
|
1010
|
+
sqlMethod: "insert",
|
|
1011
|
+
response: void 0
|
|
1012
|
+
};
|
|
1013
|
+
await this.executeStatementQuery(connection, singleObj);
|
|
1014
|
+
const resp = singleObj.response?.rows;
|
|
1015
|
+
if (resp) insertedRows.push(...resp);
|
|
1016
|
+
}
|
|
1017
|
+
if (transactional && beganTx) {
|
|
1018
|
+
try {
|
|
1019
|
+
await connection.query("COMMIT");
|
|
1020
|
+
} catch (commitErr) {
|
|
1021
|
+
this.printError(
|
|
1022
|
+
"Commit failed for sequential insert, attempting rollback: " + commitErr?.message
|
|
1023
|
+
);
|
|
1024
|
+
try {
|
|
1025
|
+
await connection.query("ROLLBACK");
|
|
1026
|
+
} catch {
|
|
1027
|
+
}
|
|
1028
|
+
throw commitErr;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
obj.response = { rows: insertedRows, rowCount: insertedRows.length };
|
|
1032
|
+
obj.sqlMethod = "insert";
|
|
1033
|
+
obj.select = true;
|
|
1034
|
+
return obj;
|
|
1035
|
+
}
|
|
1036
|
+
async executeDeleteReturning(connection, obj) {
|
|
1037
|
+
const meta = obj._ibmiDeleteReturning;
|
|
1038
|
+
const { deleteSql, selectColumns, whereClause, tableName } = meta;
|
|
1039
|
+
this.printDebug("Executing DELETE with returning emulation");
|
|
1040
|
+
try {
|
|
1041
|
+
const selectSql = whereClause ? `select ${selectColumns} from ${tableName} ${whereClause}` : `select ${selectColumns} from ${tableName}`;
|
|
1042
|
+
const selectObj = {
|
|
1043
|
+
sql: selectSql,
|
|
1044
|
+
bindings: obj.bindings,
|
|
1045
|
+
sqlMethod: "select",
|
|
1046
|
+
response: void 0
|
|
1047
|
+
};
|
|
1048
|
+
await this.executeSelectQuery(connection, selectObj);
|
|
1049
|
+
const rowsToReturn = selectObj.response?.rows || [];
|
|
1050
|
+
const deleteObj = {
|
|
1051
|
+
sql: deleteSql,
|
|
1052
|
+
bindings: obj.bindings,
|
|
1053
|
+
sqlMethod: "del",
|
|
1054
|
+
response: void 0
|
|
1055
|
+
};
|
|
1056
|
+
await this.executeStatementQuery(connection, deleteObj);
|
|
1057
|
+
obj.response = { rows: rowsToReturn, rowCount: rowsToReturn.length };
|
|
1058
|
+
obj.sqlMethod = "del";
|
|
1059
|
+
obj.select = true;
|
|
1060
|
+
return obj;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
this.printError(`DELETE with returning failed: ${error.message}`);
|
|
1063
|
+
throw this.wrapError(error, "delete_returning", obj);
|
|
774
1064
|
}
|
|
775
1065
|
}
|
|
776
1066
|
normalizeQueryObject(obj) {
|
|
@@ -861,45 +1151,75 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
861
1151
|
}
|
|
862
1152
|
async _stream(connection, obj, stream, options) {
|
|
863
1153
|
if (!obj.sql) throw new Error("A query is required to stream results");
|
|
1154
|
+
const optimizedFetchSize = options?.fetchSize || this.calculateOptimalFetchSize(obj.sql);
|
|
864
1155
|
return new Promise((resolve, reject) => {
|
|
865
|
-
|
|
866
|
-
|
|
1156
|
+
let isResolved = false;
|
|
1157
|
+
const cleanup = () => {
|
|
1158
|
+
if (!isResolved) {
|
|
1159
|
+
isResolved = true;
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
stream.on("error", (err) => {
|
|
1163
|
+
cleanup();
|
|
1164
|
+
reject(err);
|
|
1165
|
+
});
|
|
1166
|
+
stream.on("end", () => {
|
|
1167
|
+
cleanup();
|
|
1168
|
+
resolve(void 0);
|
|
1169
|
+
});
|
|
867
1170
|
connection.query(
|
|
868
1171
|
obj.sql,
|
|
869
1172
|
obj.bindings,
|
|
870
1173
|
{
|
|
871
1174
|
cursor: true,
|
|
872
|
-
fetchSize:
|
|
1175
|
+
fetchSize: optimizedFetchSize
|
|
873
1176
|
},
|
|
874
1177
|
(error, cursor) => {
|
|
875
1178
|
if (error) {
|
|
876
1179
|
this.printError(this.safeStringify(error, 2));
|
|
877
|
-
|
|
1180
|
+
cleanup();
|
|
878
1181
|
reject(error);
|
|
879
1182
|
return;
|
|
880
1183
|
}
|
|
881
1184
|
const readableStream = this._createCursorStream(cursor);
|
|
882
1185
|
readableStream.on("error", (err) => {
|
|
1186
|
+
cleanup();
|
|
883
1187
|
reject(err);
|
|
884
|
-
stream.emit("error", err);
|
|
885
1188
|
});
|
|
886
1189
|
readableStream.pipe(stream);
|
|
887
1190
|
}
|
|
888
1191
|
);
|
|
889
1192
|
});
|
|
890
1193
|
}
|
|
1194
|
+
calculateOptimalFetchSize(sql) {
|
|
1195
|
+
const sqlLower = sql.toLowerCase();
|
|
1196
|
+
const hasJoins = /\s+join\s+/i.test(sql);
|
|
1197
|
+
const hasAggregates = /\s+(count|sum|avg|max|min)\s*\(/i.test(sql);
|
|
1198
|
+
const hasOrderBy = /\s+order\s+by\s+/i.test(sql);
|
|
1199
|
+
const hasGroupBy = /\s+group\s+by\s+/i.test(sql);
|
|
1200
|
+
if (hasJoins || hasAggregates || hasOrderBy || hasGroupBy) {
|
|
1201
|
+
return 500;
|
|
1202
|
+
}
|
|
1203
|
+
return 100;
|
|
1204
|
+
}
|
|
891
1205
|
_createCursorStream(cursor) {
|
|
892
1206
|
const parentThis = this;
|
|
1207
|
+
let isClosed = false;
|
|
893
1208
|
return new import_node_stream.Readable({
|
|
894
1209
|
objectMode: true,
|
|
895
1210
|
read() {
|
|
1211
|
+
if (isClosed) return;
|
|
896
1212
|
cursor.fetch((error, result) => {
|
|
897
1213
|
if (error) {
|
|
898
1214
|
parentThis.printError(parentThis.safeStringify(error, 2));
|
|
1215
|
+
isClosed = true;
|
|
1216
|
+
this.emit("error", error);
|
|
1217
|
+
return;
|
|
899
1218
|
}
|
|
900
1219
|
if (!cursor.noData) {
|
|
901
1220
|
this.push(result);
|
|
902
1221
|
} else {
|
|
1222
|
+
isClosed = true;
|
|
903
1223
|
cursor.close((closeError) => {
|
|
904
1224
|
if (closeError) {
|
|
905
1225
|
parentThis.printError(JSON.stringify(closeError, null, 2));
|
|
@@ -911,6 +1231,21 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
911
1231
|
});
|
|
912
1232
|
}
|
|
913
1233
|
});
|
|
1234
|
+
},
|
|
1235
|
+
destroy(err, callback) {
|
|
1236
|
+
if (!isClosed) {
|
|
1237
|
+
isClosed = true;
|
|
1238
|
+
cursor.close((closeError) => {
|
|
1239
|
+
if (closeError) {
|
|
1240
|
+
parentThis.printDebug(
|
|
1241
|
+
"Error closing cursor during destroy: " + parentThis.safeStringify(closeError)
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
callback(err);
|
|
1245
|
+
});
|
|
1246
|
+
} else {
|
|
1247
|
+
callback(err);
|
|
1248
|
+
}
|
|
914
1249
|
}
|
|
915
1250
|
});
|
|
916
1251
|
}
|
|
@@ -942,15 +1277,16 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
942
1277
|
const result = obj.output(runner, response);
|
|
943
1278
|
return result;
|
|
944
1279
|
} catch (error) {
|
|
1280
|
+
const wrappedError = this.wrapError(error, "custom_output", obj);
|
|
945
1281
|
this.printError(
|
|
946
|
-
`Custom output function failed: ${
|
|
1282
|
+
`Custom output function failed: ${wrappedError.message}`
|
|
947
1283
|
);
|
|
948
1284
|
if (this.isConnectionError(error)) {
|
|
949
1285
|
throw new Error(
|
|
950
1286
|
"Connection closed during query processing - consider using migrations.disableTransactions: true for DDL operations"
|
|
951
1287
|
);
|
|
952
1288
|
}
|
|
953
|
-
throw
|
|
1289
|
+
throw wrappedError;
|
|
954
1290
|
}
|
|
955
1291
|
}
|
|
956
1292
|
const validationResult = this.validateResponse(obj);
|
|
@@ -968,10 +1304,62 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
968
1304
|
}
|
|
969
1305
|
return null;
|
|
970
1306
|
}
|
|
1307
|
+
wrapError(error, method, queryObject) {
|
|
1308
|
+
const context = {
|
|
1309
|
+
method,
|
|
1310
|
+
sql: queryObject.sql ? queryObject.sql.substring(0, 100) + "..." : "unknown",
|
|
1311
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1312
|
+
};
|
|
1313
|
+
if (this.isConnectionError(error)) {
|
|
1314
|
+
return new Error(
|
|
1315
|
+
`IBM i DB2 connection error during ${method}: ${error.message} | Context: ${JSON.stringify(context)}`
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
if (this.isTimeoutError(error)) {
|
|
1319
|
+
return new Error(
|
|
1320
|
+
`IBM i DB2 timeout during ${method}: ${error.message} | Context: ${JSON.stringify(context)}`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
if (this.isSQLError(error)) {
|
|
1324
|
+
return new Error(
|
|
1325
|
+
`IBM i DB2 SQL error during ${method}: ${error.message} | Context: ${JSON.stringify(context)}`
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
return new Error(
|
|
1329
|
+
`IBM i DB2 error during ${method}: ${error.message} | Context: ${JSON.stringify(context)}`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
shouldRetryQuery(queryObject, method) {
|
|
1333
|
+
return queryObject.sql?.toLowerCase().includes("systables") || queryObject.sql?.toLowerCase().includes("knex_migrations");
|
|
1334
|
+
}
|
|
1335
|
+
async retryQuery(connection, queryObject, method) {
|
|
1336
|
+
this.printDebug(`Retrying ${method} query due to connection error...`);
|
|
1337
|
+
try {
|
|
1338
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1339
|
+
if (this.isSelectMethod(method)) {
|
|
1340
|
+
await this.executeSelectQuery(connection, queryObject);
|
|
1341
|
+
} else {
|
|
1342
|
+
await this.executeStatementQuery(connection, queryObject);
|
|
1343
|
+
}
|
|
1344
|
+
return queryObject;
|
|
1345
|
+
} catch (retryError) {
|
|
1346
|
+
this.printError(`Retry failed: ${retryError.message}`);
|
|
1347
|
+
queryObject.response = { rows: [], rowCount: 0 };
|
|
1348
|
+
return queryObject;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
971
1351
|
isConnectionError(error) {
|
|
972
1352
|
const errorMessage = (error.message || error.toString || error).toLowerCase();
|
|
973
1353
|
return errorMessage.includes("connection") && (errorMessage.includes("closed") || errorMessage.includes("invalid") || errorMessage.includes("terminated") || errorMessage.includes("not connected"));
|
|
974
1354
|
}
|
|
1355
|
+
isTimeoutError(error) {
|
|
1356
|
+
const errorMessage = (error.message || error.toString || error).toLowerCase();
|
|
1357
|
+
return errorMessage.includes("timeout") || errorMessage.includes("timed out");
|
|
1358
|
+
}
|
|
1359
|
+
isSQLError(error) {
|
|
1360
|
+
const errorMessage = (error.message || error.toString || error).toLowerCase();
|
|
1361
|
+
return errorMessage.includes("sql") || errorMessage.includes("syntax") || errorMessage.includes("table") || errorMessage.includes("column");
|
|
1362
|
+
}
|
|
975
1363
|
processSqlMethod(obj) {
|
|
976
1364
|
const { rows, rowCount } = obj.response;
|
|
977
1365
|
switch (obj.sqlMethod) {
|