@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/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 selectColumns = returning ? this.formatter.columnize(returning) : "IDENTITY_VAL_LOCAL()";
267
- const returningSql = returning ? this._returning("insert", returning, void 0) + " " : "";
268
- const insertSql = [
269
- this.with(),
270
- `insert into ${this.tableName}`,
271
- this._buildInsertData(insertValues, returningSql)
272
- ].filter(Boolean).join(" ");
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
- return { sql, returning };
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 columnsSql = `(${this.formatter.columnize(insertData.columns)})`;
297
- const valuesSql = `(${this._buildInsertValues(insertData)})`;
298
- return `${columnsSql} ${returningSql}values ${valuesSql}`;
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 allColumns = /* @__PURE__ */ new Set();
327
- for (const item of dataArray) {
328
- if (item != null) {
329
- Object.keys(item).forEach((key) => allColumns.add(key));
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 == null) {
336
- break;
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 sql = `select ${selectColumns} from FINAL TABLE(${baseUpdateSql})`;
368
- return { sql, returning };
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
- let str = "";
395
- let i = -1;
396
- while (++i < columns.length) {
397
- if (i > 0) str += ", ";
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 str;
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 (queryObject.sql?.toLowerCase().includes("systables")) {
754
- this.printDebug("Retrying hasTable query due to connection error...");
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 new Error(
770
- `Connection closed during ${method} operation - IBM i DB2 DDL operations cause implicit commits`
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
- throw error;
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
- stream.on("error", reject);
866
- stream.on("end", resolve);
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: options?.fetchSize || 1
1175
+ fetchSize: optimizedFetchSize
873
1176
  },
874
1177
  (error, cursor) => {
875
1178
  if (error) {
876
1179
  this.printError(this.safeStringify(error, 2));
877
- stream.emit("error", error);
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: ${error.message || error}`
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 error;
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) {