@bdkinc/knex-ibmi 0.3.22 → 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
@@ -1,12 +1,12 @@
1
+ import { createRequire } from 'module';
2
+ const require = createRequire(import.meta.url);
1
3
  var __create = Object.create;
2
4
  var __defProp = Object.defineProperty;
3
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
7
  var __getProtoOf = Object.getPrototypeOf;
6
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __commonJS = (cb, mod) => function __require() {
8
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
- };
9
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
10
10
  var __export = (target, all) => {
11
11
  for (var name in all)
12
12
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -28,49 +28,80 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
28
28
  mod
29
29
  ));
30
30
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
31
-
32
- // node_modules/lodash/isObject.js
33
- var require_isObject = __commonJS({
34
- "node_modules/lodash/isObject.js"(exports2, module2) {
35
- function isObject2(value) {
36
- var type = typeof value;
37
- return value != null && (type == "object" || type == "function");
38
- }
39
- module2.exports = isObject2;
40
- }
41
- });
31
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
42
32
 
43
33
  // src/index.ts
44
- var src_exports = {};
45
- __export(src_exports, {
34
+ var index_exports = {};
35
+ __export(index_exports, {
46
36
  DB2Dialect: () => DB2Dialect,
47
- default: () => src_default
37
+ IBMiMigrationRunner: () => IBMiMigrationRunner,
38
+ createIBMiMigrationRunner: () => createIBMiMigrationRunner,
39
+ default: () => index_default
48
40
  });
49
- module.exports = __toCommonJS(src_exports);
41
+ module.exports = __toCommonJS(index_exports);
50
42
  var import_node_process = __toESM(require("process"));
51
- var import_knex = require("knex");
43
+ var import_knex = __toESM(require("knex"));
52
44
  var import_odbc = __toESM(require("odbc"));
53
45
 
54
46
  // src/schema/ibmi-compiler.ts
55
- var import_compiler = __toESM(require("knex/lib/schema/compiler"));
47
+ var import_compiler = __toESM(require("knex/lib/schema/compiler.js"));
56
48
  var IBMiSchemaCompiler = class extends import_compiler.default {
57
49
  hasTable(tableName) {
58
- const formattedTable = this.client.parameter(
59
- prefixedTableName(this.schema, tableName),
60
- this.builder,
61
- this.bindingsHolder
62
- );
63
- const bindings = [tableName];
64
- let sql = `select TABLE_NAME from QSYS2.SYSTABLES where TYPE = 'T' and TABLE_NAME = ${formattedTable}`;
65
- if (this.schema) {
66
- sql += " and TABLE_SCHEMA = ?";
67
- bindings.push(this.schema);
50
+ const upperName = String(tableName).toUpperCase();
51
+ let schemaName = null;
52
+ let actualTableName = upperName;
53
+ if (upperName.includes(".")) {
54
+ const parts = upperName.split(".");
55
+ schemaName = parts[0];
56
+ actualTableName = parts[1];
57
+ }
58
+ const builderSchema = this.builder._schema;
59
+ if (builderSchema) {
60
+ schemaName = builderSchema.toUpperCase();
61
+ }
62
+ let sql;
63
+ let bindings;
64
+ if (schemaName) {
65
+ sql = `select count(*) as table_count from QSYS2.SYSTABLES where UPPER(TABLE_NAME) = ? AND UPPER(TABLE_SCHEMA) = ?`;
66
+ bindings = [actualTableName, schemaName];
67
+ } else {
68
+ sql = `select count(*) as table_count from QSYS2.SYSTABLES where UPPER(TABLE_NAME) = ?`;
69
+ bindings = [actualTableName];
68
70
  }
69
71
  this.pushQuery({
70
72
  sql,
71
73
  bindings,
72
- output: (resp) => {
73
- return resp.rowCount > 0;
74
+ output: (runner, resp) => {
75
+ if (!resp) {
76
+ return false;
77
+ }
78
+ if (Array.isArray(resp) && resp.length > 0) {
79
+ const firstRow = resp[0];
80
+ if (firstRow && typeof firstRow === "object") {
81
+ const count = firstRow.table_count || firstRow.TABLE_COUNT || firstRow.count || firstRow.COUNT || 0;
82
+ return count > 0;
83
+ }
84
+ }
85
+ if (typeof resp === "object" && resp !== null) {
86
+ const keys = Object.keys(resp);
87
+ for (const key of keys) {
88
+ if (!isNaN(parseInt(key))) {
89
+ const row = resp[key];
90
+ if (row && typeof row === "object") {
91
+ const count = row.table_count || row.TABLE_COUNT || row.count || row.COUNT || 0;
92
+ return count > 0;
93
+ }
94
+ }
95
+ }
96
+ if (resp.rows && Array.isArray(resp.rows) && resp.rows.length > 0) {
97
+ const firstRow = resp.rows[0];
98
+ if (firstRow && typeof firstRow === "object") {
99
+ const count = firstRow.table_count || firstRow.TABLE_COUNT || firstRow.count || firstRow.COUNT || 0;
100
+ return count > 0;
101
+ }
102
+ }
103
+ }
104
+ return false;
74
105
  }
75
106
  });
76
107
  }
@@ -83,14 +114,10 @@ var IBMiSchemaCompiler = class extends import_compiler.default {
83
114
  return this.sequence;
84
115
  }
85
116
  };
86
- function prefixedTableName(prefix, table) {
87
- return prefix ? `${prefix}.${table}` : table;
88
- }
89
117
  var ibmi_compiler_default = IBMiSchemaCompiler;
90
118
 
91
119
  // src/schema/ibmi-tablecompiler.ts
92
- var import_tablecompiler = __toESM(require("knex/lib/schema/tablecompiler"));
93
- var import_isObject = __toESM(require_isObject());
120
+ var import_tablecompiler = __toESM(require("knex/lib/schema/tablecompiler.js"));
94
121
  var IBMiTableCompiler = class extends import_tablecompiler.default {
95
122
  createQuery(columns, ifNot, like) {
96
123
  let createStatement = ifNot ? `if object_id('${this.tableName()}', 'U') is null ` : "";
@@ -114,21 +141,24 @@ var IBMiTableCompiler = class extends import_tablecompiler.default {
114
141
  unique(columns, indexName) {
115
142
  let deferrable = "";
116
143
  let predicate;
117
- if ((0, import_isObject.default)(indexName)) {
118
- deferrable = indexName.deferrable;
144
+ let finalIndexName;
145
+ if (typeof indexName === "object" && indexName !== null) {
146
+ deferrable = indexName.deferrable || "";
119
147
  predicate = indexName.predicate;
120
- indexName = indexName.indexName;
148
+ finalIndexName = indexName.indexName;
149
+ } else {
150
+ finalIndexName = indexName;
121
151
  }
122
152
  if (deferrable && deferrable !== "not deferrable") {
123
153
  this.client.logger.warn?.(
124
- `IBMi: unique index \`${indexName}\` will not be deferrable ${deferrable}.`
154
+ `IBMi: unique index \`${finalIndexName}\` will not be deferrable ${deferrable}.`
125
155
  );
126
156
  }
127
- indexName = indexName ? this.formatter.wrap(indexName) : this._indexCommand("unique", this.tableNameRaw, columns);
157
+ const wrappedIndexName = finalIndexName ? this.formatter.wrap(finalIndexName) : this._indexCommand("unique", this.tableNameRaw, columns);
128
158
  columns = this.formatter.columnize(columns);
129
159
  const predicateQuery = predicate ? " " + this.client.queryCompiler(predicate).where() : "";
130
160
  this.pushQuery(
131
- `create unique index ${indexName} on ${this.tableName()} (${columns})${predicateQuery}`
161
+ `create unique index ${wrappedIndexName} on ${this.tableName()} (${columns})${predicateQuery}`
132
162
  );
133
163
  }
134
164
  // All of the columns to "add" for the query
@@ -151,74 +181,264 @@ var IBMiTableCompiler = class extends import_tablecompiler.default {
151
181
  var ibmi_tablecompiler_default = IBMiTableCompiler;
152
182
 
153
183
  // src/schema/ibmi-columncompiler.ts
154
- var import_columncompiler = __toESM(require("knex/lib/schema/columncompiler"));
184
+ var import_columncompiler = __toESM(require("knex/lib/schema/columncompiler.js"));
155
185
  var IBMiColumnCompiler = class extends import_columncompiler.default {
156
186
  increments(options = { primaryKey: true }) {
157
187
  return "int not null generated always as identity (start with 1, increment by 1)" + (this.tableCompiler._canBeAddPrimaryKey(options) ? " primary key" : "");
158
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
+ }
159
251
  };
160
252
  var ibmi_columncompiler_default = IBMiColumnCompiler;
161
253
 
162
254
  // src/execution/ibmi-transaction.ts
163
- var import_transaction = __toESM(require("knex/lib/execution/transaction"));
255
+ var import_transaction = __toESM(require("knex/lib/execution/transaction.js"));
164
256
  var IBMiTransaction = class extends import_transaction.default {
165
257
  begin(connection) {
166
- return connection.beginTransaction();
258
+ try {
259
+ return connection.beginTransaction();
260
+ } catch (error) {
261
+ if (this.isConnectionClosed(error)) {
262
+ console.warn(
263
+ "IBM i DB2: Connection closed during transaction begin, DDL operations may have caused implicit commit"
264
+ );
265
+ throw new Error(
266
+ "Connection closed during transaction begin - consider using migrations.disableTransactions: true"
267
+ );
268
+ }
269
+ throw error;
270
+ }
167
271
  }
168
272
  rollback(connection) {
169
- return connection.rollback();
273
+ try {
274
+ return connection.rollback();
275
+ } catch (error) {
276
+ console.warn(
277
+ "IBM i DB2: Rollback encountered an error (likely closed connection):",
278
+ error?.message || error
279
+ );
280
+ return Promise.resolve();
281
+ }
170
282
  }
171
283
  commit(connection) {
172
- return connection.commit();
284
+ try {
285
+ return connection.commit();
286
+ } catch (error) {
287
+ if (this.isConnectionClosed(error)) {
288
+ console.warn(
289
+ "IBM i DB2: Connection closed during commit - DDL operations cause implicit commits"
290
+ );
291
+ throw new Error(
292
+ "Connection closed during commit - this is expected with DDL operations on IBM i DB2"
293
+ );
294
+ }
295
+ throw error;
296
+ }
297
+ }
298
+ isConnectionClosed(error) {
299
+ const message = String(error?.message || error || "").toLowerCase();
300
+ return message.includes("connection") && (message.includes("closed") || message.includes("invalid") || message.includes("terminated") || message.includes("not connected"));
173
301
  }
174
302
  };
175
303
  var ibmi_transaction_default = IBMiTransaction;
176
304
 
177
305
  // src/query/ibmi-querycompiler.ts
178
- var import_querycompiler = __toESM(require("knex/lib/query/querycompiler"));
179
- var import_wrappingFormatter = require("knex/lib/formatter/wrappingFormatter");
180
- var import_date_fns = require("date-fns");
306
+ var import_querycompiler = __toESM(require("knex/lib/query/querycompiler.js"));
307
+ var import_wrappingFormatter = require("knex/lib/formatter/wrappingFormatter.js");
181
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
+ }
319
+ formatTimestampLocal(date) {
320
+ const pad = (n) => String(n).padStart(2, "0");
321
+ const y = date.getFullYear();
322
+ const m = pad(date.getMonth() + 1);
323
+ const d = pad(date.getDate());
324
+ const hh = pad(date.getHours());
325
+ const mm = pad(date.getMinutes());
326
+ const ss = pad(date.getSeconds());
327
+ return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
328
+ }
182
329
  insert() {
183
330
  const insertValues = this.single.insert || [];
184
- let sql = `select ${this.single.returning ? this.formatter.columnize(this.single.returning) : "IDENTITY_VAL_LOCAL()"} from FINAL TABLE(`;
185
- sql += this.with() + `insert into ${this.tableName} `;
186
331
  const { returning } = this.single;
187
- const returningSql = returning ? this._returning("insert", returning, void 0) + " " : "";
188
- if (Array.isArray(insertValues)) {
189
- if (insertValues.length === 0) {
190
- return "";
332
+ if (this.isEmptyInsertValues(insertValues)) {
333
+ if (this.isEmptyObject(insertValues)) {
334
+ return this.buildEmptyInsertResult(returning);
191
335
  }
192
- } else if (typeof insertValues === "object" && Object.keys(insertValues).length === 0) {
336
+ return "";
337
+ }
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()";
357
+ const sql = `select ${selectColumns} from FINAL TABLE(${insertSql})`;
358
+ if (multiRowStrategy === "sequential" && isArrayInsert) {
359
+ const first = originalValues[0];
360
+ const columns = Object.keys(first).sort();
193
361
  return {
194
- sql: sql + returningSql + this._emptyInsertValue,
195
- returning
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
+ }
196
371
  };
197
372
  }
198
- sql += this._buildInsertData(insertValues, returningSql);
199
- sql += ")";
200
- return {
201
- sql,
202
- returning
203
- };
373
+ return { sql, returning: void 0 };
374
+ }
375
+ isEmptyInsertValues(insertValues) {
376
+ return Array.isArray(insertValues) && insertValues.length === 0 || this.isEmptyObject(insertValues);
377
+ }
378
+ isEmptyObject(insertValues) {
379
+ return insertValues !== null && typeof insertValues === "object" && !Array.isArray(insertValues) && Object.keys(insertValues).length === 0;
380
+ }
381
+ buildEmptyInsertResult(returning) {
382
+ const selectColumns = returning ? this.formatter.columnize(returning) : "IDENTITY_VAL_LOCAL()";
383
+ const returningSql = returning ? this._returning("insert", returning, void 0) + " " : "";
384
+ const insertSql = [
385
+ this.with(),
386
+ `insert into ${this.tableName}`,
387
+ returningSql + this._emptyInsertValue
388
+ ].filter(Boolean).join(" ");
389
+ const sql = `select ${selectColumns} from FINAL TABLE(${insertSql})`;
390
+ return { sql, returning };
204
391
  }
205
392
  _buildInsertData(insertValues, returningSql) {
206
- let sql = "";
207
393
  const insertData = this._prepInsert(insertValues);
208
- if (insertData.columns.length) {
209
- sql += `(${this.formatter.columnize(insertData.columns)}`;
210
- sql += `) ${returningSql}values (` + this._buildInsertValues(insertData) + ")";
211
- } else if (insertValues.length === 1 && insertValues[0]) {
212
- sql += returningSql + this._emptyInsertValue;
213
- } else {
214
- return "";
394
+ if (insertData.columns.length > 0) {
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("");
406
+ }
407
+ if (Array.isArray(insertValues) && insertValues.length === 1 && insertValues[0]) {
408
+ return (returningSql || "") + this._emptyInsertValue;
409
+ }
410
+ return "";
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);
215
430
  }
216
- return sql;
431
+ return {
432
+ columns: cachedColumns,
433
+ values
434
+ };
217
435
  }
218
436
  _prepInsert(data) {
219
- if (typeof data === "object" && data.migration_time) {
437
+ if (typeof data === "object" && data?.migration_time) {
220
438
  const parsed = new Date(data.migration_time);
221
- data.migration_time = (0, import_date_fns.format)(parsed, "yyyy-MM-dd HH:mm:ss");
439
+ if (!isNaN(parsed.getTime())) {
440
+ data.migration_time = this.formatTimestampLocal(parsed);
441
+ }
222
442
  }
223
443
  const isRaw = (0, import_wrappingFormatter.rawOrFn)(
224
444
  data,
@@ -230,42 +450,29 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
230
450
  if (isRaw) {
231
451
  return isRaw;
232
452
  }
233
- let columns = [];
234
- const values = [];
235
- if (!Array.isArray(data)) {
236
- data = data ? [data] : [];
453
+ const dataArray = Array.isArray(data) ? data : data ? [data] : [];
454
+ if (dataArray.length === 0) {
455
+ return { columns: [], values: [] };
237
456
  }
238
- let i = -1;
239
- while (++i < data.length) {
240
- if (data[i] == null) {
241
- break;
242
- }
243
- if (i === 0) {
244
- columns = Object.keys(data[i]).sort();
245
- }
246
- const row = new Array(columns.length);
247
- const keys = Object.keys(data[i]);
248
- let j = -1;
249
- while (++j < keys.length) {
250
- const key = keys[j];
251
- let idx = columns.indexOf(key);
252
- if (idx === -1) {
253
- columns = columns.concat(key).sort();
254
- idx = columns.indexOf(key);
255
- let k = -1;
256
- while (++k < values.length) {
257
- values[k].splice(idx, 0, void 0);
258
- }
259
- row.splice(idx, 0, void 0);
260
- }
261
- row[idx] = data[i][key];
262
- }
263
- values.push(row);
457
+ const firstItem = dataArray[0];
458
+ if (!firstItem || typeof firstItem !== "object") {
459
+ return { columns: [], values: [] };
264
460
  }
265
- return {
266
- columns,
267
- values
268
- };
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);
469
+ }
470
+ const values = [];
471
+ for (const item of dataArray) {
472
+ if (!item || typeof item !== "object") continue;
473
+ values.push(columns.map((c) => item[c] ?? void 0));
474
+ }
475
+ return { columns, values };
269
476
  }
270
477
  update() {
271
478
  const withSQL = this.with();
@@ -274,17 +481,61 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
274
481
  const order = this.order();
275
482
  const limit = this.limit();
276
483
  const { returning } = this.single;
277
- let sql = "";
484
+ const optimizationHints = "";
485
+ const baseUpdateSql = [
486
+ withSQL,
487
+ `update ${this.single.only ? "only " : ""}${this.tableName}`,
488
+ "set",
489
+ updates.join(", "),
490
+ where,
491
+ order,
492
+ limit,
493
+ optimizationHints
494
+ ].filter(Boolean).join(" ");
278
495
  if (returning) {
279
- console.error("IBMi DB2 does not support returning in update statements, only inserts");
280
- sql += `select ${this.formatter.columnize(this.single.returning)} from FINAL TABLE(`;
496
+ const selectColumns = this.formatter.columnize(this.single.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
+ };
281
508
  }
282
- sql += withSQL + `update ${this.single.only ? "only " : ""}${this.tableName} set ` + updates.join(", ") + (where ? ` ${where}` : "") + (order ? ` ${order}` : "") + (limit ? ` ${limit}` : "");
283
- if (returning) {
284
- sql += `)`;
509
+ return { sql: baseUpdateSql, returning };
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 };
285
517
  }
286
- return { sql, returning };
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
+ };
287
531
  }
532
+ /**
533
+ * Handle returning clause for IBMi DB2 queries
534
+ * Note: IBMi DB2 has limited support for RETURNING clauses
535
+ * @param method - The SQL method (insert, update, delete)
536
+ * @param value - The returning value
537
+ * @param withTrigger - Trigger support (currently unused)
538
+ */
288
539
  _returning(method, value, withTrigger) {
289
540
  switch (method) {
290
541
  case "update":
@@ -294,24 +545,238 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
294
545
  return value ? `${withTrigger ? " into #out" : ""}` : "";
295
546
  case "rowcount":
296
547
  return value ? "select @@rowcount" : "";
548
+ default:
549
+ return "";
550
+ }
551
+ }
552
+ getOptimizationHints(queryType, data) {
553
+ const hints = [];
554
+ if (queryType === "select") {
555
+ hints.push("WITH UR");
297
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(" ") : "";
298
563
  }
299
564
  columnizeWithPrefix(prefix, target) {
300
565
  const columns = typeof target === "string" ? [target] : target;
301
- let str = "";
302
- let i = -1;
303
- while (++i < columns.length) {
304
- if (i > 0) str += ", ";
305
- 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]));
306
570
  }
307
- return str;
571
+ return parts.join("");
308
572
  }
309
573
  };
310
574
  var ibmi_querycompiler_default = IBMiQueryCompiler;
311
575
 
312
576
  // src/index.ts
313
577
  var import_node_stream = require("stream");
314
- var DB2Client = class extends import_knex.knex.Client {
578
+
579
+ // src/migrations/ibmi-migration-runner.ts
580
+ var import_fs = __toESM(require("fs"));
581
+ var import_path = __toESM(require("path"));
582
+ var IBMiMigrationRunner = class {
583
+ constructor(knex2, config) {
584
+ __publicField(this, "knex");
585
+ __publicField(this, "config");
586
+ this.knex = knex2;
587
+ this.config = {
588
+ directory: "./migrations",
589
+ tableName: "KNEX_MIGRATIONS",
590
+ schemaName: void 0,
591
+ extension: "js",
592
+ ...config
593
+ };
594
+ }
595
+ getFullTableName() {
596
+ return this.config.schemaName ? `${this.config.schemaName}.${this.config.tableName}` : this.config.tableName;
597
+ }
598
+ async latest() {
599
+ try {
600
+ console.log(
601
+ "\u{1F680} IBM i DB2 Migration Runner - bypassing Knex locking system"
602
+ );
603
+ const tableName = this.getFullTableName();
604
+ const migrationTableExists = await this.knex.schema.hasTable(
605
+ tableName
606
+ );
607
+ if (!migrationTableExists) {
608
+ console.log(`\u{1F4DD} Creating migration table: ${tableName}`);
609
+ await this.knex.schema.createTable(tableName, (table) => {
610
+ table.increments("id").primary();
611
+ table.string("name");
612
+ table.integer("batch");
613
+ table.timestamp("migration_time");
614
+ });
615
+ console.log("\u2705 Migration table created");
616
+ }
617
+ const completed = await this.knex(tableName).select("NAME").orderBy("ID");
618
+ const completedNames = completed.map((c) => c.NAME);
619
+ console.log(`\u{1F4CB} Found ${completedNames.length} completed migrations`);
620
+ const migrationFiles = this.getMigrationFiles();
621
+ console.log(`\u{1F4C1} Found ${migrationFiles.length} migration files`);
622
+ const newMigrations = migrationFiles.filter(
623
+ (file) => !completedNames.includes(file)
624
+ );
625
+ if (newMigrations.length === 0) {
626
+ console.log("\u2705 No new migrations to run");
627
+ return;
628
+ }
629
+ console.log(`\u{1F3AF} Running ${newMigrations.length} new migrations:`);
630
+ newMigrations.forEach((file) => console.log(` - ${file}`));
631
+ const batchResult = await this.knex(tableName).max("BATCH as max_batch");
632
+ const nextBatch = (batchResult[0]?.max_batch || 0) + 1;
633
+ console.log(`\u{1F4CA} Using batch number: ${nextBatch}`);
634
+ for (const migrationFile of newMigrations) {
635
+ console.log(`
636
+ \u{1F504} Running migration: ${migrationFile}`);
637
+ try {
638
+ const migrationPath = this.getMigrationPath(migrationFile);
639
+ const migration = await import(migrationPath);
640
+ if (!migration.up || typeof migration.up !== "function") {
641
+ throw new Error(`Migration ${migrationFile} has no 'up' function`);
642
+ }
643
+ console.log(` \u26A1 Executing migration...`);
644
+ await migration.up(this.knex);
645
+ await this.knex(tableName).insert({
646
+ name: migrationFile,
647
+ batch: nextBatch,
648
+ migration_time: /* @__PURE__ */ new Date()
649
+ });
650
+ console.log(` \u2705 Migration ${migrationFile} completed successfully`);
651
+ } catch (error) {
652
+ console.error(
653
+ ` \u274C Migration ${migrationFile} failed:`,
654
+ error.message
655
+ );
656
+ throw error;
657
+ }
658
+ }
659
+ console.log(`
660
+ \u{1F389} All migrations completed successfully!`);
661
+ } catch (error) {
662
+ console.error("\u274C Migration runner failed:", error.message);
663
+ throw error;
664
+ }
665
+ }
666
+ async rollback(steps = 1) {
667
+ try {
668
+ console.log(`\u{1F504} Rolling back ${steps} migration batch(es)...`);
669
+ const tableName = this.getFullTableName();
670
+ const batchesToRollback = await this.knex(tableName).distinct("BATCH").orderBy("BATCH", "desc").limit(steps);
671
+ if (batchesToRollback.length === 0) {
672
+ console.log("\u2705 No migrations to rollback");
673
+ return;
674
+ }
675
+ const batchNumbers = batchesToRollback.map((b) => b.BATCH);
676
+ console.log(`\u{1F4CA} Rolling back batches: ${batchNumbers.join(", ")}`);
677
+ const migrationsToRollback = await this.knex(tableName).select("NAME").whereIn("BATCH", batchNumbers).orderBy("ID", "desc");
678
+ console.log(`\u{1F3AF} Rolling back ${migrationsToRollback.length} migrations:`);
679
+ migrationsToRollback.forEach((m) => console.log(` - ${m.NAME}`));
680
+ for (const migrationRecord of migrationsToRollback) {
681
+ const migrationFile = migrationRecord.NAME;
682
+ console.log(`
683
+ \u{1F504} Rolling back migration: ${migrationFile}`);
684
+ try {
685
+ const migrationPath = this.getMigrationPath(migrationFile);
686
+ const migration = await import(migrationPath);
687
+ if (migration.down && typeof migration.down === "function") {
688
+ console.log(` \u26A1 Executing rollback...`);
689
+ await migration.down(this.knex);
690
+ } else {
691
+ console.log(
692
+ ` \u26A0\uFE0F Migration ${migrationFile} has no 'down' function, skipping rollback`
693
+ );
694
+ }
695
+ await this.knex(tableName).where("NAME", migrationFile).del();
696
+ console.log(
697
+ ` \u2705 Migration ${migrationFile} rolled back successfully`
698
+ );
699
+ } catch (error) {
700
+ console.error(
701
+ ` \u274C Migration ${migrationFile} rollback failed:`,
702
+ error.message
703
+ );
704
+ throw error;
705
+ }
706
+ }
707
+ console.log(`
708
+ \u{1F389} Rollback completed successfully!`);
709
+ } catch (error) {
710
+ console.error("\u274C Rollback failed:", error.message);
711
+ throw error;
712
+ }
713
+ }
714
+ async currentVersion() {
715
+ try {
716
+ const tableName = this.getFullTableName();
717
+ const migrationTableExists = await this.knex.schema.hasTable(
718
+ tableName
719
+ );
720
+ if (!migrationTableExists) {
721
+ return null;
722
+ }
723
+ const result = await this.knex(tableName).select("NAME").orderBy("ID", "desc").first();
724
+ return result?.NAME || null;
725
+ } catch (error) {
726
+ console.error("\u274C Error getting current version:", error.message);
727
+ return null;
728
+ }
729
+ }
730
+ async listExecuted() {
731
+ try {
732
+ const tableName = this.getFullTableName();
733
+ const migrationTableExists = await this.knex.schema.hasTable(
734
+ tableName
735
+ );
736
+ if (!migrationTableExists) {
737
+ return [];
738
+ }
739
+ const completed = await this.knex(tableName).select("NAME").orderBy("ID");
740
+ return completed.map((c) => c.NAME);
741
+ } catch (error) {
742
+ console.error("\u274C Error listing executed migrations:", error.message);
743
+ return [];
744
+ }
745
+ }
746
+ async listPending() {
747
+ try {
748
+ const allFiles = this.getMigrationFiles();
749
+ const executed = await this.listExecuted();
750
+ return allFiles.filter((file) => !executed.includes(file));
751
+ } catch (error) {
752
+ console.error("\u274C Error listing pending migrations:", error.message);
753
+ return [];
754
+ }
755
+ }
756
+ getMigrationFiles() {
757
+ const { directory, extension } = this.config;
758
+ if (!import_fs.default.existsSync(directory)) {
759
+ throw new Error(`Migration directory does not exist: ${directory}`);
760
+ }
761
+ const validExtensions = ["js", "ts", "mjs", "cjs"];
762
+ const extensionToCheck = extension || "js";
763
+ return import_fs.default.readdirSync(directory).filter((file) => {
764
+ if (extension && extension !== "js") {
765
+ return file.endsWith(`.${extension}`);
766
+ }
767
+ return validExtensions.some((ext) => file.endsWith(`.${ext}`));
768
+ }).sort();
769
+ }
770
+ getMigrationPath(filename) {
771
+ return import_path.default.resolve(this.config.directory, filename);
772
+ }
773
+ };
774
+ function createIBMiMigrationRunner(knex2, config) {
775
+ return new IBMiMigrationRunner(knex2, config);
776
+ }
777
+
778
+ // src/index.ts
779
+ var DB2Client = class extends import_knex.default.Client {
315
780
  constructor(config) {
316
781
  super(config);
317
782
  this.driverName = "odbc";
@@ -340,10 +805,25 @@ var DB2Client = class extends import_knex.knex.Client {
340
805
  this.valueForUndefined = null;
341
806
  }
342
807
  }
808
+ // Helper method to safely stringify objects that might have circular references
809
+ safeStringify(obj, indent = 0) {
810
+ try {
811
+ return JSON.stringify(obj, null, indent);
812
+ } catch (error) {
813
+ if (error instanceof Error && error.message.includes("circular")) {
814
+ return `[Circular structure - ${typeof obj}]`;
815
+ }
816
+ return `[Stringify error - ${typeof obj}]`;
817
+ }
818
+ }
343
819
  _driver() {
344
820
  return import_odbc.default;
345
821
  }
346
822
  wrapIdentifierImpl(value) {
823
+ if (!value) return value;
824
+ if (value.includes("KNEX_MIGRATIONS") || value.includes("knex_migrations")) {
825
+ return value.toUpperCase();
826
+ }
347
827
  return value;
348
828
  }
349
829
  printDebug(message) {
@@ -376,6 +856,7 @@ var DB2Client = class extends import_knex.knex.Client {
376
856
  this.printDebug(
377
857
  "connection config: " + this._getConnectionString(connectionConfig)
378
858
  );
859
+ let connection;
379
860
  if (this.config?.pool) {
380
861
  const poolConfig = {
381
862
  connectionString: this._getConnectionString(connectionConfig),
@@ -385,11 +866,13 @@ var DB2Client = class extends import_knex.knex.Client {
385
866
  reuseConnection: true
386
867
  };
387
868
  const pool = await this.driver.pool(poolConfig);
388
- return await pool.connect();
869
+ connection = await pool.connect();
870
+ } else {
871
+ connection = await this.driver.connect(
872
+ this._getConnectionString(connectionConfig)
873
+ );
389
874
  }
390
- return await this.driver.connect(
391
- this._getConnectionString(connectionConfig)
392
- );
875
+ return connection;
393
876
  }
394
877
  // Used to explicitly close a connection, called internally by the pool manager
395
878
  // when a connection times out or the pool is shutdown.
@@ -405,20 +888,180 @@ var DB2Client = class extends import_knex.knex.Client {
405
888
  const value = connectionStringParams[key];
406
889
  return `${result}${key}=${value};`;
407
890
  }, "");
408
- return `${`DRIVER=${connectionConfig.driver};SYSTEM=${connectionConfig.host};HOSTNAME=${connectionConfig.host};PORT=${connectionConfig.port};DATABASE=${connectionConfig.database};UID=${connectionConfig.user};PWD=${connectionConfig.password};` + connectionStringExtension}`;
891
+ return `DRIVER=${connectionConfig.driver};SYSTEM=${connectionConfig.host};HOSTNAME=${connectionConfig.host};PORT=${connectionConfig.port};DATABASE=${connectionConfig.database};UID=${connectionConfig.user};PWD=${connectionConfig.password};` + connectionStringExtension;
409
892
  }
410
893
  // Runs the query on the specified connection, providing the bindings
411
894
  async _query(connection, obj) {
412
895
  const queryObject = this.normalizeQueryObject(obj);
413
896
  const method = this.determineQueryMethod(queryObject);
414
897
  queryObject.sqlMethod = method;
415
- if (this.isSelectMethod(method)) {
416
- await this.executeSelectQuery(connection, queryObject);
417
- } else {
418
- await this.executeStatementQuery(connection, queryObject);
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
+ }
907
+ if (import_node_process.default.env.DEBUG === "true" && queryObject.sql && (queryObject.sql.toLowerCase().includes("create table") || queryObject.sql.toLowerCase().includes("knex_migrations"))) {
908
+ this.printDebug(
909
+ `Executing ${method} query: ${queryObject.sql.substring(0, 200)}...`
910
+ );
911
+ if (queryObject.bindings?.length) {
912
+ this.printDebug(`Bindings: ${JSON.stringify(queryObject.bindings)}`);
913
+ }
914
+ }
915
+ try {
916
+ const startTime = Date.now();
917
+ if (this.isSelectMethod(method)) {
918
+ await this.executeSelectQuery(connection, queryObject);
919
+ } else {
920
+ await this.executeStatementQuery(connection, queryObject);
921
+ }
922
+ const endTime = Date.now();
923
+ if (import_node_process.default.env.DEBUG === "true" && queryObject.sql && (queryObject.sql.toLowerCase().includes("create table") || queryObject.sql.toLowerCase().includes("knex_migrations"))) {
924
+ this.printDebug(`${method} completed in ${endTime - startTime}ms`);
925
+ }
926
+ this.printDebug(`Query completed: ${method} (${endTime - startTime}ms)`);
927
+ return queryObject;
928
+ } catch (error) {
929
+ const wrappedError = this.wrapError(error, method, queryObject);
930
+ if (this.isConnectionError(error)) {
931
+ this.printError(
932
+ `Connection error during ${method} query: ${error.message}`
933
+ );
934
+ if (this.shouldRetryQuery(queryObject, method)) {
935
+ return await this.retryQuery(connection, queryObject, method);
936
+ }
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"
996
+ );
997
+ }
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);
419
1064
  }
420
- this.printDebug(queryObject);
421
- return queryObject;
422
1065
  }
423
1066
  normalizeQueryObject(obj) {
424
1067
  if (!obj || typeof obj === "string") {
@@ -433,14 +1076,18 @@ var DB2Client = class extends import_knex.knex.Client {
433
1076
  return method === "select" || method === "first" || method === "pluck";
434
1077
  }
435
1078
  async executeSelectQuery(connection, obj) {
436
- const rows = await connection.query(obj.sql, obj.bindings);
1079
+ const rows = await connection.query(
1080
+ obj.sql,
1081
+ obj.bindings
1082
+ );
437
1083
  if (rows) {
438
1084
  obj.response = { rows, rowCount: rows.length };
439
1085
  }
440
1086
  }
441
1087
  async executeStatementQuery(connection, obj) {
1088
+ let statement;
442
1089
  try {
443
- const statement = await connection.createStatement();
1090
+ statement = await connection.createStatement();
444
1091
  await statement.prepare(obj.sql);
445
1092
  if (obj.bindings) {
446
1093
  await statement.bind(obj.bindings);
@@ -449,60 +1096,130 @@ var DB2Client = class extends import_knex.knex.Client {
449
1096
  this.printDebug(String(result));
450
1097
  obj.response = this.formatStatementResponse(result);
451
1098
  } catch (err) {
452
- this.printError(JSON.stringify(err));
1099
+ const sql = (obj.sql || "").toLowerCase();
1100
+ const isDml = obj.sqlMethod === "update" /* UPDATE */ || sql.includes(" update ") || sql.startsWith("update") || obj.sqlMethod === "del" /* DELETE */ || sql.includes(" delete ") || sql.startsWith("delete");
1101
+ const odbcErrors = err?.odbcErrors;
1102
+ const isEmptyOdbcError = Array.isArray(odbcErrors) && odbcErrors.length === 0;
1103
+ const hasNoDataState = Array.isArray(odbcErrors) ? odbcErrors.some(
1104
+ (e) => String(e?.state || e?.SQLSTATE || "").toUpperCase() === "02000"
1105
+ ) : false;
1106
+ if (isDml && (isEmptyOdbcError || hasNoDataState || this.isNoDataError(err))) {
1107
+ this.printWarn(
1108
+ `ODBC signaled no-data for ${sql.includes("update") ? "UPDATE" : "DELETE"}; treating as 0 rows affected`
1109
+ );
1110
+ obj.response = { rows: [], rowCount: 0 };
1111
+ return;
1112
+ }
1113
+ this.printError(this.safeStringify(err));
1114
+ throw err;
1115
+ } finally {
1116
+ if (statement && typeof statement.close === "function") {
1117
+ try {
1118
+ await statement.close();
1119
+ } catch (closeErr) {
1120
+ this.printDebug(
1121
+ `Error closing statement: ${this.safeStringify(closeErr, 2)}`
1122
+ );
1123
+ }
1124
+ }
453
1125
  }
454
1126
  }
1127
+ isNoDataError(error) {
1128
+ if (!error) return false;
1129
+ const msg = String(error?.message || error || "").toLowerCase();
1130
+ return msg.includes("02000") || msg.includes("no data") || msg.includes("no rows") || msg.includes("0 rows");
1131
+ }
1132
+ /**
1133
+ * Format statement response from ODBC driver
1134
+ * Handles special case for IDENTITY_VAL_LOCAL() function
1135
+ */
455
1136
  formatStatementResponse(result) {
456
- if (result.statement.includes("IDENTITY_VAL_LOCAL()")) {
1137
+ const isIdentityQuery = result.statement?.includes("IDENTITY_VAL_LOCAL()");
1138
+ if (isIdentityQuery && result.columns?.length > 0) {
457
1139
  return {
458
1140
  rows: result.map(
459
- (row) => result.columns && result.columns?.length > 0 ? row[result.columns[0].name] : row
1141
+ (row) => row[result.columns[0].name]
460
1142
  ),
461
1143
  rowCount: result.count
462
1144
  };
463
- } else {
464
- return { rows: result, rowCount: result.count };
465
1145
  }
1146
+ const rowCount = typeof result?.count === "number" ? result.count : 0;
1147
+ return {
1148
+ rows: result,
1149
+ rowCount
1150
+ };
466
1151
  }
467
1152
  async _stream(connection, obj, stream, options) {
468
1153
  if (!obj.sql) throw new Error("A query is required to stream results");
1154
+ const optimizedFetchSize = options?.fetchSize || this.calculateOptimalFetchSize(obj.sql);
469
1155
  return new Promise((resolve, reject) => {
470
- stream.on("error", reject);
471
- 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
+ });
472
1170
  connection.query(
473
1171
  obj.sql,
474
1172
  obj.bindings,
475
1173
  {
476
1174
  cursor: true,
477
- fetchSize: options?.fetchSize || 1
1175
+ fetchSize: optimizedFetchSize
478
1176
  },
479
1177
  (error, cursor) => {
480
1178
  if (error) {
481
- this.printError(JSON.stringify(error, null, 2));
1179
+ this.printError(this.safeStringify(error, 2));
1180
+ cleanup();
1181
+ reject(error);
482
1182
  return;
483
1183
  }
484
1184
  const readableStream = this._createCursorStream(cursor);
485
1185
  readableStream.on("error", (err) => {
1186
+ cleanup();
486
1187
  reject(err);
487
- stream.emit("error", err);
488
1188
  });
489
1189
  readableStream.pipe(stream);
490
1190
  }
491
1191
  );
492
1192
  });
493
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
+ }
494
1205
  _createCursorStream(cursor) {
495
1206
  const parentThis = this;
1207
+ let isClosed = false;
496
1208
  return new import_node_stream.Readable({
497
1209
  objectMode: true,
498
1210
  read() {
1211
+ if (isClosed) return;
499
1212
  cursor.fetch((error, result) => {
500
1213
  if (error) {
501
- parentThis.printError(JSON.stringify(error, null, 2));
1214
+ parentThis.printError(parentThis.safeStringify(error, 2));
1215
+ isClosed = true;
1216
+ this.emit("error", error);
1217
+ return;
502
1218
  }
503
1219
  if (!cursor.noData) {
504
1220
  this.push(result);
505
1221
  } else {
1222
+ isClosed = true;
506
1223
  cursor.close((closeError) => {
507
1224
  if (closeError) {
508
1225
  parentThis.printError(JSON.stringify(closeError, null, 2));
@@ -514,6 +1231,21 @@ var DB2Client = class extends import_knex.knex.Client {
514
1231
  });
515
1232
  }
516
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
+ }
517
1249
  }
518
1250
  });
519
1251
  }
@@ -532,26 +1264,102 @@ var DB2Client = class extends import_knex.knex.Client {
532
1264
  queryCompiler(builder, bindings) {
533
1265
  return new ibmi_querycompiler_default(this, builder, bindings);
534
1266
  }
1267
+ // Create IBM i-specific migration runner that bypasses Knex's problematic locking system
1268
+ createMigrationRunner(config) {
1269
+ const knexInstance = this.context || this;
1270
+ return createIBMiMigrationRunner(knexInstance, config);
1271
+ }
535
1272
  processResponse(obj, runner) {
536
1273
  if (obj === null) return null;
537
- const validationResult = this.validateResponse(obj);
538
- if (validationResult !== null) return validationResult;
539
1274
  const { response } = obj;
540
1275
  if (obj.output) {
541
- return obj.output(runner, response);
1276
+ try {
1277
+ const result = obj.output(runner, response);
1278
+ return result;
1279
+ } catch (error) {
1280
+ const wrappedError = this.wrapError(error, "custom_output", obj);
1281
+ this.printError(
1282
+ `Custom output function failed: ${wrappedError.message}`
1283
+ );
1284
+ if (this.isConnectionError(error)) {
1285
+ throw new Error(
1286
+ "Connection closed during query processing - consider using migrations.disableTransactions: true for DDL operations"
1287
+ );
1288
+ }
1289
+ throw wrappedError;
1290
+ }
542
1291
  }
1292
+ const validationResult = this.validateResponse(obj);
1293
+ if (validationResult !== null) return validationResult;
543
1294
  return this.processSqlMethod(obj);
544
1295
  }
545
1296
  validateResponse(obj) {
546
1297
  if (!obj.response) {
547
- this.printDebug("response undefined" + obj);
548
- return void 0;
1298
+ this.printDebug("response undefined" + JSON.stringify(obj));
1299
+ return null;
549
1300
  }
550
1301
  if (!obj.response.rows) {
551
- return this.printError("rows undefined" + obj);
1302
+ this.printError("rows undefined" + JSON.stringify(obj));
1303
+ return null;
552
1304
  }
553
1305
  return null;
554
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
+ }
1351
+ isConnectionError(error) {
1352
+ const errorMessage = (error.message || error.toString || error).toLowerCase();
1353
+ return errorMessage.includes("connection") && (errorMessage.includes("closed") || errorMessage.includes("invalid") || errorMessage.includes("terminated") || errorMessage.includes("not connected"));
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
+ }
555
1363
  processSqlMethod(obj) {
556
1364
  const { rows, rowCount } = obj.response;
557
1365
  switch (obj.sqlMethod) {
@@ -566,7 +1374,7 @@ var DB2Client = class extends import_knex.knex.Client {
566
1374
  case "del" /* DELETE */:
567
1375
  case "delete" /* DELETE_ALT */:
568
1376
  case "update" /* UPDATE */:
569
- return obj.select ? rows : rowCount;
1377
+ return obj.select ? rows : rowCount ?? 0;
570
1378
  case "counter" /* COUNTER */:
571
1379
  return rowCount;
572
1380
  default:
@@ -575,8 +1383,11 @@ var DB2Client = class extends import_knex.knex.Client {
575
1383
  }
576
1384
  };
577
1385
  var DB2Dialect = DB2Client;
578
- var src_default = DB2Client;
1386
+ var index_default = DB2Client;
579
1387
  // Annotate the CommonJS export names for ESM import in node:
580
1388
  0 && (module.exports = {
581
- DB2Dialect
1389
+ DB2Dialect,
1390
+ IBMiMigrationRunner,
1391
+ createIBMiMigrationRunner
582
1392
  });
1393
+ //# sourceMappingURL=index.js.map