@bdkinc/knex-ibmi 0.3.22 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,7 +181,7 @@ 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" : "");
@@ -160,65 +190,124 @@ var IBMiColumnCompiler = class extends import_columncompiler.default {
160
190
  var ibmi_columncompiler_default = IBMiColumnCompiler;
161
191
 
162
192
  // src/execution/ibmi-transaction.ts
163
- var import_transaction = __toESM(require("knex/lib/execution/transaction"));
193
+ var import_transaction = __toESM(require("knex/lib/execution/transaction.js"));
164
194
  var IBMiTransaction = class extends import_transaction.default {
165
195
  begin(connection) {
166
- return connection.beginTransaction();
196
+ try {
197
+ return connection.beginTransaction();
198
+ } catch (error) {
199
+ if (this.isConnectionClosed(error)) {
200
+ console.warn(
201
+ "IBM i DB2: Connection closed during transaction begin, DDL operations may have caused implicit commit"
202
+ );
203
+ throw new Error(
204
+ "Connection closed during transaction begin - consider using migrations.disableTransactions: true"
205
+ );
206
+ }
207
+ throw error;
208
+ }
167
209
  }
168
210
  rollback(connection) {
169
- return connection.rollback();
211
+ try {
212
+ return connection.rollback();
213
+ } catch (error) {
214
+ console.warn(
215
+ "IBM i DB2: Rollback encountered an error (likely closed connection):",
216
+ error?.message || error
217
+ );
218
+ return Promise.resolve();
219
+ }
170
220
  }
171
221
  commit(connection) {
172
- return connection.commit();
222
+ try {
223
+ return connection.commit();
224
+ } catch (error) {
225
+ if (this.isConnectionClosed(error)) {
226
+ console.warn(
227
+ "IBM i DB2: Connection closed during commit - DDL operations cause implicit commits"
228
+ );
229
+ throw new Error(
230
+ "Connection closed during commit - this is expected with DDL operations on IBM i DB2"
231
+ );
232
+ }
233
+ throw error;
234
+ }
235
+ }
236
+ isConnectionClosed(error) {
237
+ const message = String(error?.message || error || "").toLowerCase();
238
+ return message.includes("connection") && (message.includes("closed") || message.includes("invalid") || message.includes("terminated") || message.includes("not connected"));
173
239
  }
174
240
  };
175
241
  var ibmi_transaction_default = IBMiTransaction;
176
242
 
177
243
  // 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");
244
+ var import_querycompiler = __toESM(require("knex/lib/query/querycompiler.js"));
245
+ var import_wrappingFormatter = require("knex/lib/formatter/wrappingFormatter.js");
181
246
  var IBMiQueryCompiler = class extends import_querycompiler.default {
247
+ formatTimestampLocal(date) {
248
+ const pad = (n) => String(n).padStart(2, "0");
249
+ const y = date.getFullYear();
250
+ const m = pad(date.getMonth() + 1);
251
+ const d = pad(date.getDate());
252
+ const hh = pad(date.getHours());
253
+ const mm = pad(date.getMinutes());
254
+ const ss = pad(date.getSeconds());
255
+ return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
256
+ }
182
257
  insert() {
183
258
  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
259
  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 "";
260
+ if (this.isEmptyInsertValues(insertValues)) {
261
+ if (this.isEmptyObject(insertValues)) {
262
+ return this.buildEmptyInsertResult(returning);
191
263
  }
192
- } else if (typeof insertValues === "object" && Object.keys(insertValues).length === 0) {
193
- return {
194
- sql: sql + returningSql + this._emptyInsertValue,
195
- returning
196
- };
264
+ return "";
197
265
  }
198
- sql += this._buildInsertData(insertValues, returningSql);
199
- sql += ")";
200
- return {
201
- sql,
202
- returning
203
- };
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(" ");
273
+ const sql = `select ${selectColumns} from FINAL TABLE(${insertSql})`;
274
+ return { sql, returning };
275
+ }
276
+ isEmptyInsertValues(insertValues) {
277
+ return Array.isArray(insertValues) && insertValues.length === 0 || this.isEmptyObject(insertValues);
278
+ }
279
+ isEmptyObject(insertValues) {
280
+ return insertValues !== null && typeof insertValues === "object" && !Array.isArray(insertValues) && Object.keys(insertValues).length === 0;
281
+ }
282
+ buildEmptyInsertResult(returning) {
283
+ const selectColumns = returning ? this.formatter.columnize(returning) : "IDENTITY_VAL_LOCAL()";
284
+ const returningSql = returning ? this._returning("insert", returning, void 0) + " " : "";
285
+ const insertSql = [
286
+ this.with(),
287
+ `insert into ${this.tableName}`,
288
+ returningSql + this._emptyInsertValue
289
+ ].filter(Boolean).join(" ");
290
+ const sql = `select ${selectColumns} from FINAL TABLE(${insertSql})`;
291
+ return { sql, returning };
204
292
  }
205
293
  _buildInsertData(insertValues, returningSql) {
206
- let sql = "";
207
294
  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 "";
295
+ 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}`;
299
+ }
300
+ if (Array.isArray(insertValues) && insertValues.length === 1 && insertValues[0]) {
301
+ return returningSql + this._emptyInsertValue;
215
302
  }
216
- return sql;
303
+ return "";
217
304
  }
218
305
  _prepInsert(data) {
219
- if (typeof data === "object" && data.migration_time) {
306
+ if (typeof data === "object" && data?.migration_time) {
220
307
  const parsed = new Date(data.migration_time);
221
- data.migration_time = (0, import_date_fns.format)(parsed, "yyyy-MM-dd HH:mm:ss");
308
+ if (!isNaN(parsed.getTime())) {
309
+ data.migration_time = this.formatTimestampLocal(parsed);
310
+ }
222
311
  }
223
312
  const isRaw = (0, import_wrappingFormatter.rawOrFn)(
224
313
  data,
@@ -230,36 +319,23 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
230
319
  if (isRaw) {
231
320
  return isRaw;
232
321
  }
233
- let columns = [];
234
- const values = [];
235
- if (!Array.isArray(data)) {
236
- data = data ? [data] : [];
322
+ const dataArray = Array.isArray(data) ? data : data ? [data] : [];
323
+ if (dataArray.length === 0) {
324
+ return { columns: [], values: [] };
237
325
  }
238
- let i = -1;
239
- while (++i < data.length) {
240
- if (data[i] == null) {
241
- break;
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));
242
330
  }
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];
331
+ }
332
+ const columns = Array.from(allColumns).sort();
333
+ const values = [];
334
+ for (const item of dataArray) {
335
+ if (item == null) {
336
+ break;
262
337
  }
338
+ const row = columns.map((column) => item[column] ?? void 0);
263
339
  values.push(row);
264
340
  }
265
341
  return {
@@ -274,17 +350,32 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
274
350
  const order = this.order();
275
351
  const limit = this.limit();
276
352
  const { returning } = this.single;
277
- let sql = "";
353
+ const baseUpdateSql = [
354
+ withSQL,
355
+ `update ${this.single.only ? "only " : ""}${this.tableName}`,
356
+ "set",
357
+ updates.join(", "),
358
+ where,
359
+ order,
360
+ limit
361
+ ].filter(Boolean).join(" ");
278
362
  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(`;
281
- }
282
- sql += withSQL + `update ${this.single.only ? "only " : ""}${this.tableName} set ` + updates.join(", ") + (where ? ` ${where}` : "") + (order ? ` ${order}` : "") + (limit ? ` ${limit}` : "");
283
- if (returning) {
284
- sql += `)`;
285
- }
286
- return { sql, returning };
287
- }
363
+ this.client.logger.warn?.(
364
+ "IBMi DB2 does not support returning in update statements, only inserts"
365
+ );
366
+ const selectColumns = this.formatter.columnize(this.single.returning);
367
+ const sql = `select ${selectColumns} from FINAL TABLE(${baseUpdateSql})`;
368
+ return { sql, returning };
369
+ }
370
+ return { sql: baseUpdateSql, returning };
371
+ }
372
+ /**
373
+ * Handle returning clause for IBMi DB2 queries
374
+ * Note: IBMi DB2 has limited support for RETURNING clauses
375
+ * @param method - The SQL method (insert, update, delete)
376
+ * @param value - The returning value
377
+ * @param withTrigger - Trigger support (currently unused)
378
+ */
288
379
  _returning(method, value, withTrigger) {
289
380
  switch (method) {
290
381
  case "update":
@@ -294,6 +385,8 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
294
385
  return value ? `${withTrigger ? " into #out" : ""}` : "";
295
386
  case "rowcount":
296
387
  return value ? "select @@rowcount" : "";
388
+ default:
389
+ return "";
297
390
  }
298
391
  }
299
392
  columnizeWithPrefix(prefix, target) {
@@ -311,8 +404,210 @@ var ibmi_querycompiler_default = IBMiQueryCompiler;
311
404
 
312
405
  // src/index.ts
313
406
  var import_node_stream = require("stream");
314
- var DB2Client = class extends import_knex.knex.Client {
407
+
408
+ // src/migrations/ibmi-migration-runner.ts
409
+ var import_fs = __toESM(require("fs"));
410
+ var import_path = __toESM(require("path"));
411
+ var IBMiMigrationRunner = class {
412
+ constructor(knex2, config) {
413
+ __publicField(this, "knex");
414
+ __publicField(this, "config");
415
+ this.knex = knex2;
416
+ this.config = {
417
+ directory: "./migrations",
418
+ tableName: "KNEX_MIGRATIONS",
419
+ schemaName: void 0,
420
+ extension: "js",
421
+ ...config
422
+ };
423
+ }
424
+ getFullTableName() {
425
+ return this.config.schemaName ? `${this.config.schemaName}.${this.config.tableName}` : this.config.tableName;
426
+ }
427
+ async latest() {
428
+ try {
429
+ console.log(
430
+ "\u{1F680} IBM i DB2 Migration Runner - bypassing Knex locking system"
431
+ );
432
+ const tableName = this.getFullTableName();
433
+ const migrationTableExists = await this.knex.schema.hasTable(
434
+ tableName
435
+ );
436
+ if (!migrationTableExists) {
437
+ console.log(`\u{1F4DD} Creating migration table: ${tableName}`);
438
+ await this.knex.schema.createTable(tableName, (table) => {
439
+ table.increments("id").primary();
440
+ table.string("name");
441
+ table.integer("batch");
442
+ table.timestamp("migration_time");
443
+ });
444
+ console.log("\u2705 Migration table created");
445
+ }
446
+ const completed = await this.knex(tableName).select("NAME").orderBy("ID");
447
+ const completedNames = completed.map((c) => c.NAME);
448
+ console.log(`\u{1F4CB} Found ${completedNames.length} completed migrations`);
449
+ const migrationFiles = this.getMigrationFiles();
450
+ console.log(`\u{1F4C1} Found ${migrationFiles.length} migration files`);
451
+ const newMigrations = migrationFiles.filter(
452
+ (file) => !completedNames.includes(file)
453
+ );
454
+ if (newMigrations.length === 0) {
455
+ console.log("\u2705 No new migrations to run");
456
+ return;
457
+ }
458
+ console.log(`\u{1F3AF} Running ${newMigrations.length} new migrations:`);
459
+ newMigrations.forEach((file) => console.log(` - ${file}`));
460
+ const batchResult = await this.knex(tableName).max("BATCH as max_batch");
461
+ const nextBatch = (batchResult[0]?.max_batch || 0) + 1;
462
+ console.log(`\u{1F4CA} Using batch number: ${nextBatch}`);
463
+ for (const migrationFile of newMigrations) {
464
+ console.log(`
465
+ \u{1F504} Running migration: ${migrationFile}`);
466
+ try {
467
+ const migrationPath = this.getMigrationPath(migrationFile);
468
+ const migration = await import(migrationPath);
469
+ if (!migration.up || typeof migration.up !== "function") {
470
+ throw new Error(`Migration ${migrationFile} has no 'up' function`);
471
+ }
472
+ console.log(` \u26A1 Executing migration...`);
473
+ await migration.up(this.knex);
474
+ await this.knex(tableName).insert({
475
+ name: migrationFile,
476
+ batch: nextBatch,
477
+ migration_time: /* @__PURE__ */ new Date()
478
+ });
479
+ console.log(` \u2705 Migration ${migrationFile} completed successfully`);
480
+ } catch (error) {
481
+ console.error(
482
+ ` \u274C Migration ${migrationFile} failed:`,
483
+ error.message
484
+ );
485
+ throw error;
486
+ }
487
+ }
488
+ console.log(`
489
+ \u{1F389} All migrations completed successfully!`);
490
+ } catch (error) {
491
+ console.error("\u274C Migration runner failed:", error.message);
492
+ throw error;
493
+ }
494
+ }
495
+ async rollback(steps = 1) {
496
+ try {
497
+ console.log(`\u{1F504} Rolling back ${steps} migration batch(es)...`);
498
+ const tableName = this.getFullTableName();
499
+ const batchesToRollback = await this.knex(tableName).distinct("BATCH").orderBy("BATCH", "desc").limit(steps);
500
+ if (batchesToRollback.length === 0) {
501
+ console.log("\u2705 No migrations to rollback");
502
+ return;
503
+ }
504
+ const batchNumbers = batchesToRollback.map((b) => b.BATCH);
505
+ console.log(`\u{1F4CA} Rolling back batches: ${batchNumbers.join(", ")}`);
506
+ const migrationsToRollback = await this.knex(tableName).select("NAME").whereIn("BATCH", batchNumbers).orderBy("ID", "desc");
507
+ console.log(`\u{1F3AF} Rolling back ${migrationsToRollback.length} migrations:`);
508
+ migrationsToRollback.forEach((m) => console.log(` - ${m.NAME}`));
509
+ for (const migrationRecord of migrationsToRollback) {
510
+ const migrationFile = migrationRecord.NAME;
511
+ console.log(`
512
+ \u{1F504} Rolling back migration: ${migrationFile}`);
513
+ try {
514
+ const migrationPath = this.getMigrationPath(migrationFile);
515
+ const migration = await import(migrationPath);
516
+ if (migration.down && typeof migration.down === "function") {
517
+ console.log(` \u26A1 Executing rollback...`);
518
+ await migration.down(this.knex);
519
+ } else {
520
+ console.log(
521
+ ` \u26A0\uFE0F Migration ${migrationFile} has no 'down' function, skipping rollback`
522
+ );
523
+ }
524
+ await this.knex(tableName).where("NAME", migrationFile).del();
525
+ console.log(
526
+ ` \u2705 Migration ${migrationFile} rolled back successfully`
527
+ );
528
+ } catch (error) {
529
+ console.error(
530
+ ` \u274C Migration ${migrationFile} rollback failed:`,
531
+ error.message
532
+ );
533
+ throw error;
534
+ }
535
+ }
536
+ console.log(`
537
+ \u{1F389} Rollback completed successfully!`);
538
+ } catch (error) {
539
+ console.error("\u274C Rollback failed:", error.message);
540
+ throw error;
541
+ }
542
+ }
543
+ async currentVersion() {
544
+ try {
545
+ const tableName = this.getFullTableName();
546
+ const migrationTableExists = await this.knex.schema.hasTable(
547
+ tableName
548
+ );
549
+ if (!migrationTableExists) {
550
+ return null;
551
+ }
552
+ const result = await this.knex(tableName).select("NAME").orderBy("ID", "desc").first();
553
+ return result?.NAME || null;
554
+ } catch (error) {
555
+ console.error("\u274C Error getting current version:", error.message);
556
+ return null;
557
+ }
558
+ }
559
+ async listExecuted() {
560
+ try {
561
+ const tableName = this.getFullTableName();
562
+ const migrationTableExists = await this.knex.schema.hasTable(
563
+ tableName
564
+ );
565
+ if (!migrationTableExists) {
566
+ return [];
567
+ }
568
+ const completed = await this.knex(tableName).select("NAME").orderBy("ID");
569
+ return completed.map((c) => c.NAME);
570
+ } catch (error) {
571
+ console.error("\u274C Error listing executed migrations:", error.message);
572
+ return [];
573
+ }
574
+ }
575
+ async listPending() {
576
+ try {
577
+ const allFiles = this.getMigrationFiles();
578
+ const executed = await this.listExecuted();
579
+ return allFiles.filter((file) => !executed.includes(file));
580
+ } catch (error) {
581
+ console.error("\u274C Error listing pending migrations:", error.message);
582
+ return [];
583
+ }
584
+ }
585
+ getMigrationFiles() {
586
+ const { directory, extension } = this.config;
587
+ if (!import_fs.default.existsSync(directory)) {
588
+ throw new Error(`Migration directory does not exist: ${directory}`);
589
+ }
590
+ const validExtensions = ["js", "ts", "mjs", "cjs"];
591
+ const extensionToCheck = extension || "js";
592
+ return import_fs.default.readdirSync(directory).filter((file) => {
593
+ if (extension && extension !== "js") {
594
+ return file.endsWith(`.${extension}`);
595
+ }
596
+ return validExtensions.some((ext) => file.endsWith(`.${ext}`));
597
+ }).sort();
598
+ }
599
+ getMigrationPath(filename) {
600
+ return import_path.default.resolve(this.config.directory, filename);
601
+ }
602
+ };
603
+ function createIBMiMigrationRunner(knex2, config) {
604
+ return new IBMiMigrationRunner(knex2, config);
605
+ }
606
+
607
+ // src/index.ts
608
+ var DB2Client = class extends import_knex.default.Client {
315
609
  constructor(config) {
610
+ console.log("\u{1F3AF} DB2Client constructor called!");
316
611
  super(config);
317
612
  this.driverName = "odbc";
318
613
  if (this.dialect && !this.config.client) {
@@ -340,10 +635,24 @@ var DB2Client = class extends import_knex.knex.Client {
340
635
  this.valueForUndefined = null;
341
636
  }
342
637
  }
638
+ // Helper method to safely stringify objects that might have circular references
639
+ safeStringify(obj, indent = 0) {
640
+ try {
641
+ return JSON.stringify(obj, null, indent);
642
+ } catch (error) {
643
+ if (error instanceof Error && error.message.includes("circular")) {
644
+ return `[Circular structure - ${typeof obj}]`;
645
+ }
646
+ return `[Stringify error - ${typeof obj}]`;
647
+ }
648
+ }
343
649
  _driver() {
344
650
  return import_odbc.default;
345
651
  }
346
652
  wrapIdentifierImpl(value) {
653
+ if (value.includes("KNEX_MIGRATIONS") || value.includes("knex_migrations")) {
654
+ return value.toUpperCase();
655
+ }
347
656
  return value;
348
657
  }
349
658
  printDebug(message) {
@@ -376,6 +685,7 @@ var DB2Client = class extends import_knex.knex.Client {
376
685
  this.printDebug(
377
686
  "connection config: " + this._getConnectionString(connectionConfig)
378
687
  );
688
+ let connection;
379
689
  if (this.config?.pool) {
380
690
  const poolConfig = {
381
691
  connectionString: this._getConnectionString(connectionConfig),
@@ -385,11 +695,13 @@ var DB2Client = class extends import_knex.knex.Client {
385
695
  reuseConnection: true
386
696
  };
387
697
  const pool = await this.driver.pool(poolConfig);
388
- return await pool.connect();
698
+ connection = await pool.connect();
699
+ } else {
700
+ connection = await this.driver.connect(
701
+ this._getConnectionString(connectionConfig)
702
+ );
389
703
  }
390
- return await this.driver.connect(
391
- this._getConnectionString(connectionConfig)
392
- );
704
+ return connection;
393
705
  }
394
706
  // Used to explicitly close a connection, called internally by the pool manager
395
707
  // when a connection times out or the pool is shutdown.
@@ -405,20 +717,61 @@ var DB2Client = class extends import_knex.knex.Client {
405
717
  const value = connectionStringParams[key];
406
718
  return `${result}${key}=${value};`;
407
719
  }, "");
408
- return `${`DRIVER=${connectionConfig.driver};SYSTEM=${connectionConfig.host};HOSTNAME=${connectionConfig.host};PORT=${connectionConfig.port};DATABASE=${connectionConfig.database};UID=${connectionConfig.user};PWD=${connectionConfig.password};` + connectionStringExtension}`;
720
+ return `DRIVER=${connectionConfig.driver};SYSTEM=${connectionConfig.host};HOSTNAME=${connectionConfig.host};PORT=${connectionConfig.port};DATABASE=${connectionConfig.database};UID=${connectionConfig.user};PWD=${connectionConfig.password};` + connectionStringExtension;
409
721
  }
410
722
  // Runs the query on the specified connection, providing the bindings
411
723
  async _query(connection, obj) {
412
724
  const queryObject = this.normalizeQueryObject(obj);
413
725
  const method = this.determineQueryMethod(queryObject);
414
726
  queryObject.sqlMethod = method;
415
- if (this.isSelectMethod(method)) {
416
- await this.executeSelectQuery(connection, queryObject);
417
- } else {
418
- await this.executeStatementQuery(connection, queryObject);
727
+ if (import_node_process.default.env.DEBUG === "true" && queryObject.sql && (queryObject.sql.toLowerCase().includes("create table") || queryObject.sql.toLowerCase().includes("knex_migrations"))) {
728
+ this.printDebug(
729
+ `Executing ${method} query: ${queryObject.sql.substring(0, 200)}...`
730
+ );
731
+ if (queryObject.bindings?.length) {
732
+ this.printDebug(`Bindings: ${JSON.stringify(queryObject.bindings)}`);
733
+ }
734
+ }
735
+ try {
736
+ const startTime = Date.now();
737
+ if (this.isSelectMethod(method)) {
738
+ await this.executeSelectQuery(connection, queryObject);
739
+ } else {
740
+ await this.executeStatementQuery(connection, queryObject);
741
+ }
742
+ const endTime = Date.now();
743
+ if (import_node_process.default.env.DEBUG === "true" && queryObject.sql && (queryObject.sql.toLowerCase().includes("create table") || queryObject.sql.toLowerCase().includes("knex_migrations"))) {
744
+ this.printDebug(`${method} completed in ${endTime - startTime}ms`);
745
+ }
746
+ this.printDebug(`Query completed: ${method} (${endTime - startTime}ms)`);
747
+ return queryObject;
748
+ } catch (error) {
749
+ if (this.isConnectionError(error)) {
750
+ this.printError(
751
+ `Connection error during ${method} query: ${error.message}`
752
+ );
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
+ }
768
+ }
769
+ throw new Error(
770
+ `Connection closed during ${method} operation - IBM i DB2 DDL operations cause implicit commits`
771
+ );
772
+ }
773
+ throw error;
419
774
  }
420
- this.printDebug(queryObject);
421
- return queryObject;
422
775
  }
423
776
  normalizeQueryObject(obj) {
424
777
  if (!obj || typeof obj === "string") {
@@ -433,14 +786,18 @@ var DB2Client = class extends import_knex.knex.Client {
433
786
  return method === "select" || method === "first" || method === "pluck";
434
787
  }
435
788
  async executeSelectQuery(connection, obj) {
436
- const rows = await connection.query(obj.sql, obj.bindings);
789
+ const rows = await connection.query(
790
+ obj.sql,
791
+ obj.bindings
792
+ );
437
793
  if (rows) {
438
794
  obj.response = { rows, rowCount: rows.length };
439
795
  }
440
796
  }
441
797
  async executeStatementQuery(connection, obj) {
798
+ let statement;
442
799
  try {
443
- const statement = await connection.createStatement();
800
+ statement = await connection.createStatement();
444
801
  await statement.prepare(obj.sql);
445
802
  if (obj.bindings) {
446
803
  await statement.bind(obj.bindings);
@@ -449,20 +806,58 @@ var DB2Client = class extends import_knex.knex.Client {
449
806
  this.printDebug(String(result));
450
807
  obj.response = this.formatStatementResponse(result);
451
808
  } catch (err) {
452
- this.printError(JSON.stringify(err));
809
+ const sql = (obj.sql || "").toLowerCase();
810
+ const isDml = obj.sqlMethod === "update" /* UPDATE */ || sql.includes(" update ") || sql.startsWith("update") || obj.sqlMethod === "del" /* DELETE */ || sql.includes(" delete ") || sql.startsWith("delete");
811
+ const odbcErrors = err?.odbcErrors;
812
+ const isEmptyOdbcError = Array.isArray(odbcErrors) && odbcErrors.length === 0;
813
+ const hasNoDataState = Array.isArray(odbcErrors) ? odbcErrors.some(
814
+ (e) => String(e?.state || e?.SQLSTATE || "").toUpperCase() === "02000"
815
+ ) : false;
816
+ if (isDml && (isEmptyOdbcError || hasNoDataState || this.isNoDataError(err))) {
817
+ this.printWarn(
818
+ `ODBC signaled no-data for ${sql.includes("update") ? "UPDATE" : "DELETE"}; treating as 0 rows affected`
819
+ );
820
+ obj.response = { rows: [], rowCount: 0 };
821
+ return;
822
+ }
823
+ this.printError(this.safeStringify(err));
824
+ throw err;
825
+ } finally {
826
+ if (statement && typeof statement.close === "function") {
827
+ try {
828
+ await statement.close();
829
+ } catch (closeErr) {
830
+ this.printDebug(
831
+ `Error closing statement: ${this.safeStringify(closeErr, 2)}`
832
+ );
833
+ }
834
+ }
453
835
  }
454
836
  }
837
+ isNoDataError(error) {
838
+ if (!error) return false;
839
+ const msg = String(error?.message || error || "").toLowerCase();
840
+ return msg.includes("02000") || msg.includes("no data") || msg.includes("no rows") || msg.includes("0 rows");
841
+ }
842
+ /**
843
+ * Format statement response from ODBC driver
844
+ * Handles special case for IDENTITY_VAL_LOCAL() function
845
+ */
455
846
  formatStatementResponse(result) {
456
- if (result.statement.includes("IDENTITY_VAL_LOCAL()")) {
847
+ const isIdentityQuery = result.statement?.includes("IDENTITY_VAL_LOCAL()");
848
+ if (isIdentityQuery && result.columns?.length > 0) {
457
849
  return {
458
850
  rows: result.map(
459
- (row) => result.columns && result.columns?.length > 0 ? row[result.columns[0].name] : row
851
+ (row) => row[result.columns[0].name]
460
852
  ),
461
853
  rowCount: result.count
462
854
  };
463
- } else {
464
- return { rows: result, rowCount: result.count };
465
855
  }
856
+ const rowCount = typeof result?.count === "number" ? result.count : 0;
857
+ return {
858
+ rows: result,
859
+ rowCount
860
+ };
466
861
  }
467
862
  async _stream(connection, obj, stream, options) {
468
863
  if (!obj.sql) throw new Error("A query is required to stream results");
@@ -478,7 +873,9 @@ var DB2Client = class extends import_knex.knex.Client {
478
873
  },
479
874
  (error, cursor) => {
480
875
  if (error) {
481
- this.printError(JSON.stringify(error, null, 2));
876
+ this.printError(this.safeStringify(error, 2));
877
+ stream.emit("error", error);
878
+ reject(error);
482
879
  return;
483
880
  }
484
881
  const readableStream = this._createCursorStream(cursor);
@@ -498,7 +895,7 @@ var DB2Client = class extends import_knex.knex.Client {
498
895
  read() {
499
896
  cursor.fetch((error, result) => {
500
897
  if (error) {
501
- parentThis.printError(JSON.stringify(error, null, 2));
898
+ parentThis.printError(parentThis.safeStringify(error, 2));
502
899
  }
503
900
  if (!cursor.noData) {
504
901
  this.push(result);
@@ -532,26 +929,49 @@ var DB2Client = class extends import_knex.knex.Client {
532
929
  queryCompiler(builder, bindings) {
533
930
  return new ibmi_querycompiler_default(this, builder, bindings);
534
931
  }
932
+ // Create IBM i-specific migration runner that bypasses Knex's problematic locking system
933
+ createMigrationRunner(config) {
934
+ const knexInstance = this.context || this;
935
+ return createIBMiMigrationRunner(knexInstance, config);
936
+ }
535
937
  processResponse(obj, runner) {
536
938
  if (obj === null) return null;
537
- const validationResult = this.validateResponse(obj);
538
- if (validationResult !== null) return validationResult;
539
939
  const { response } = obj;
540
940
  if (obj.output) {
541
- return obj.output(runner, response);
941
+ try {
942
+ const result = obj.output(runner, response);
943
+ return result;
944
+ } catch (error) {
945
+ this.printError(
946
+ `Custom output function failed: ${error.message || error}`
947
+ );
948
+ if (this.isConnectionError(error)) {
949
+ throw new Error(
950
+ "Connection closed during query processing - consider using migrations.disableTransactions: true for DDL operations"
951
+ );
952
+ }
953
+ throw error;
954
+ }
542
955
  }
956
+ const validationResult = this.validateResponse(obj);
957
+ if (validationResult !== null) return validationResult;
543
958
  return this.processSqlMethod(obj);
544
959
  }
545
960
  validateResponse(obj) {
546
961
  if (!obj.response) {
547
- this.printDebug("response undefined" + obj);
548
- return void 0;
962
+ this.printDebug("response undefined" + JSON.stringify(obj));
963
+ return null;
549
964
  }
550
965
  if (!obj.response.rows) {
551
- return this.printError("rows undefined" + obj);
966
+ this.printError("rows undefined" + JSON.stringify(obj));
967
+ return null;
552
968
  }
553
969
  return null;
554
970
  }
971
+ isConnectionError(error) {
972
+ const errorMessage = (error.message || error.toString || error).toLowerCase();
973
+ return errorMessage.includes("connection") && (errorMessage.includes("closed") || errorMessage.includes("invalid") || errorMessage.includes("terminated") || errorMessage.includes("not connected"));
974
+ }
555
975
  processSqlMethod(obj) {
556
976
  const { rows, rowCount } = obj.response;
557
977
  switch (obj.sqlMethod) {
@@ -566,7 +986,7 @@ var DB2Client = class extends import_knex.knex.Client {
566
986
  case "del" /* DELETE */:
567
987
  case "delete" /* DELETE_ALT */:
568
988
  case "update" /* UPDATE */:
569
- return obj.select ? rows : rowCount;
989
+ return obj.select ? rows : rowCount ?? 0;
570
990
  case "counter" /* COUNTER */:
571
991
  return rowCount;
572
992
  default:
@@ -575,8 +995,11 @@ var DB2Client = class extends import_knex.knex.Client {
575
995
  }
576
996
  };
577
997
  var DB2Dialect = DB2Client;
578
- var src_default = DB2Client;
998
+ var index_default = DB2Client;
579
999
  // Annotate the CommonJS export names for ESM import in node:
580
1000
  0 && (module.exports = {
581
- DB2Dialect
1001
+ DB2Dialect,
1002
+ IBMiMigrationRunner,
1003
+ createIBMiMigrationRunner
582
1004
  });
1005
+ //# sourceMappingURL=index.js.map