@bdkinc/knex-ibmi 0.3.21 → 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,69 +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 (typeof insertData === "string") {
209
- sql += insertData;
210
- } else {
211
- if (insertData.columns.length) {
212
- sql += `(${this.formatter.columnize(insertData.columns)}`;
213
- sql += `) ${returningSql}values (` + this._buildInsertValues(insertData) + ")";
214
- } else if (insertValues.length === 1 && insertValues[0]) {
215
- sql += returningSql + this._emptyInsertValue;
216
- } else {
217
- return "";
218
- }
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;
219
302
  }
220
- return sql;
303
+ return "";
221
304
  }
222
305
  _prepInsert(data) {
223
- if (typeof data === "object" && data.migration_time) {
306
+ if (typeof data === "object" && data?.migration_time) {
224
307
  const parsed = new Date(data.migration_time);
225
- 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
+ }
226
311
  }
227
312
  const isRaw = (0, import_wrappingFormatter.rawOrFn)(
228
313
  data,
@@ -234,36 +319,23 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
234
319
  if (isRaw) {
235
320
  return isRaw;
236
321
  }
237
- let columns = [];
238
- const values = [];
239
- if (!Array.isArray(data)) {
240
- data = data ? [data] : [];
322
+ const dataArray = Array.isArray(data) ? data : data ? [data] : [];
323
+ if (dataArray.length === 0) {
324
+ return { columns: [], values: [] };
241
325
  }
242
- let i = -1;
243
- while (++i < data.length) {
244
- if (data[i] == null) {
245
- break;
246
- }
247
- if (i === 0) {
248
- columns = Object.keys(data[i]).sort();
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));
249
330
  }
250
- const row = new Array(columns.length);
251
- const keys = Object.keys(data[i]);
252
- let j = -1;
253
- while (++j < keys.length) {
254
- const key = keys[j];
255
- let idx = columns.indexOf(key);
256
- if (idx === -1) {
257
- columns = columns.concat(key).sort();
258
- idx = columns.indexOf(key);
259
- let k = -1;
260
- while (++k < values.length) {
261
- values[k].splice(idx, 0, void 0);
262
- }
263
- row.splice(idx, 0, void 0);
264
- }
265
- 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;
266
337
  }
338
+ const row = columns.map((column) => item[column] ?? void 0);
267
339
  values.push(row);
268
340
  }
269
341
  return {
@@ -278,16 +350,32 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
278
350
  const order = this.order();
279
351
  const limit = this.limit();
280
352
  const { returning } = this.single;
281
- let sql = "";
282
- if (returning) {
283
- sql += `select ${this.formatter.columnize(this.single.returning)} from FINAL TABLE(`;
284
- }
285
- sql += withSQL + `update ${this.single.only ? "only " : ""}${this.tableName} set ` + updates.join(", ") + (where ? ` ${where}` : "") + (order ? ` ${order}` : "") + (limit ? ` ${limit}` : "");
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(" ");
286
362
  if (returning) {
287
- sql += `)`;
288
- }
289
- return { sql, returning };
290
- }
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
+ */
291
379
  _returning(method, value, withTrigger) {
292
380
  switch (method) {
293
381
  case "update":
@@ -297,6 +385,8 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
297
385
  return value ? `${withTrigger ? " into #out" : ""}` : "";
298
386
  case "rowcount":
299
387
  return value ? "select @@rowcount" : "";
388
+ default:
389
+ return "";
300
390
  }
301
391
  }
302
392
  columnizeWithPrefix(prefix, target) {
@@ -314,8 +404,210 @@ var ibmi_querycompiler_default = IBMiQueryCompiler;
314
404
 
315
405
  // src/index.ts
316
406
  var import_node_stream = require("stream");
317
- 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 {
318
609
  constructor(config) {
610
+ console.log("\u{1F3AF} DB2Client constructor called!");
319
611
  super(config);
320
612
  this.driverName = "odbc";
321
613
  if (this.dialect && !this.config.client) {
@@ -343,10 +635,24 @@ var DB2Client = class extends import_knex.knex.Client {
343
635
  this.valueForUndefined = null;
344
636
  }
345
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
+ }
346
649
  _driver() {
347
650
  return import_odbc.default;
348
651
  }
349
652
  wrapIdentifierImpl(value) {
653
+ if (value.includes("KNEX_MIGRATIONS") || value.includes("knex_migrations")) {
654
+ return value.toUpperCase();
655
+ }
350
656
  return value;
351
657
  }
352
658
  printDebug(message) {
@@ -379,6 +685,7 @@ var DB2Client = class extends import_knex.knex.Client {
379
685
  this.printDebug(
380
686
  "connection config: " + this._getConnectionString(connectionConfig)
381
687
  );
688
+ let connection;
382
689
  if (this.config?.pool) {
383
690
  const poolConfig = {
384
691
  connectionString: this._getConnectionString(connectionConfig),
@@ -388,11 +695,13 @@ var DB2Client = class extends import_knex.knex.Client {
388
695
  reuseConnection: true
389
696
  };
390
697
  const pool = await this.driver.pool(poolConfig);
391
- return await pool.connect();
698
+ connection = await pool.connect();
699
+ } else {
700
+ connection = await this.driver.connect(
701
+ this._getConnectionString(connectionConfig)
702
+ );
392
703
  }
393
- return await this.driver.connect(
394
- this._getConnectionString(connectionConfig)
395
- );
704
+ return connection;
396
705
  }
397
706
  // Used to explicitly close a connection, called internally by the pool manager
398
707
  // when a connection times out or the pool is shutdown.
@@ -408,46 +717,148 @@ var DB2Client = class extends import_knex.knex.Client {
408
717
  const value = connectionStringParams[key];
409
718
  return `${result}${key}=${value};`;
410
719
  }, "");
411
- 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;
412
721
  }
413
722
  // Runs the query on the specified connection, providing the bindings
414
723
  async _query(connection, obj) {
415
- if (!obj || typeof obj == "string") {
416
- obj = { sql: obj };
417
- }
418
- const method = (obj.hasOwnProperty("method") && obj.method !== "raw" ? obj.method : obj.sql.split(" ")[0]).toLowerCase();
419
- obj.sqlMethod = method;
420
- if (method === "select" || method === "first" || method === "pluck") {
421
- const rows = await connection.query(obj.sql, obj.bindings);
422
- if (rows) {
423
- obj.response = { rows, rowCount: rows.length };
724
+ const queryObject = this.normalizeQueryObject(obj);
725
+ const method = this.determineQueryMethod(queryObject);
726
+ queryObject.sqlMethod = method;
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)}`);
424
733
  }
425
- } else {
426
- try {
427
- const statement = await connection.createStatement();
428
- await statement.prepare(obj.sql);
429
- if (obj.bindings) {
430
- await statement.bind(obj.bindings);
431
- }
432
- const result = await statement.execute();
433
- this.printDebug(result);
434
- if (result.statement.includes("IDENTITY_VAL_LOCAL()")) {
435
- obj.response = {
436
- rows: result.map(
437
- (row) => result.columns && result.columns?.length > 0 ? row[result.columns[0].name] : row
438
- ),
439
- rowCount: result.count
440
- };
441
- } else {
442
- obj.response = { rows: result, rowCount: result.count };
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
+ }
443
768
  }
444
- } catch (err) {
445
- this.printError(JSON.stringify(err));
769
+ throw new Error(
770
+ `Connection closed during ${method} operation - IBM i DB2 DDL operations cause implicit commits`
771
+ );
446
772
  }
773
+ throw error;
774
+ }
775
+ }
776
+ normalizeQueryObject(obj) {
777
+ if (!obj || typeof obj === "string") {
778
+ return { sql: obj };
447
779
  }
448
- this.printDebug(obj);
449
780
  return obj;
450
781
  }
782
+ determineQueryMethod(obj) {
783
+ return (obj.hasOwnProperty("method") && obj.method !== "raw" ? obj.method : obj.sql.split(" ")[0]).toLowerCase();
784
+ }
785
+ isSelectMethod(method) {
786
+ return method === "select" || method === "first" || method === "pluck";
787
+ }
788
+ async executeSelectQuery(connection, obj) {
789
+ const rows = await connection.query(
790
+ obj.sql,
791
+ obj.bindings
792
+ );
793
+ if (rows) {
794
+ obj.response = { rows, rowCount: rows.length };
795
+ }
796
+ }
797
+ async executeStatementQuery(connection, obj) {
798
+ let statement;
799
+ try {
800
+ statement = await connection.createStatement();
801
+ await statement.prepare(obj.sql);
802
+ if (obj.bindings) {
803
+ await statement.bind(obj.bindings);
804
+ }
805
+ const result = await statement.execute();
806
+ this.printDebug(String(result));
807
+ obj.response = this.formatStatementResponse(result);
808
+ } catch (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
+ }
835
+ }
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
+ */
846
+ formatStatementResponse(result) {
847
+ const isIdentityQuery = result.statement?.includes("IDENTITY_VAL_LOCAL()");
848
+ if (isIdentityQuery && result.columns?.length > 0) {
849
+ return {
850
+ rows: result.map(
851
+ (row) => row[result.columns[0].name]
852
+ ),
853
+ rowCount: result.count
854
+ };
855
+ }
856
+ const rowCount = typeof result?.count === "number" ? result.count : 0;
857
+ return {
858
+ rows: result,
859
+ rowCount
860
+ };
861
+ }
451
862
  async _stream(connection, obj, stream, options) {
452
863
  if (!obj.sql) throw new Error("A query is required to stream results");
453
864
  return new Promise((resolve, reject) => {
@@ -462,30 +873,12 @@ var DB2Client = class extends import_knex.knex.Client {
462
873
  },
463
874
  (error, cursor) => {
464
875
  if (error) {
465
- this.printError(JSON.stringify(error, null, 2));
876
+ this.printError(this.safeStringify(error, 2));
877
+ stream.emit("error", error);
878
+ reject(error);
879
+ return;
466
880
  }
467
- const readableStream = new import_node_stream.Readable({
468
- objectMode: true,
469
- read() {
470
- cursor.fetch((error2, result) => {
471
- if (error2) {
472
- console.log(JSON.stringify(error2, null, 2));
473
- }
474
- if (!cursor.noData) {
475
- this.push(result);
476
- } else {
477
- cursor.close((error3) => {
478
- if (error3) {
479
- console.log(JSON.stringify(error3, null, 2));
480
- }
481
- if (result) {
482
- this.push(result);
483
- }
484
- });
485
- }
486
- });
487
- }
488
- });
881
+ const readableStream = this._createCursorStream(cursor);
489
882
  readableStream.on("error", (err) => {
490
883
  reject(err);
491
884
  stream.emit("error", err);
@@ -495,6 +888,32 @@ var DB2Client = class extends import_knex.knex.Client {
495
888
  );
496
889
  });
497
890
  }
891
+ _createCursorStream(cursor) {
892
+ const parentThis = this;
893
+ return new import_node_stream.Readable({
894
+ objectMode: true,
895
+ read() {
896
+ cursor.fetch((error, result) => {
897
+ if (error) {
898
+ parentThis.printError(parentThis.safeStringify(error, 2));
899
+ }
900
+ if (!cursor.noData) {
901
+ this.push(result);
902
+ } else {
903
+ cursor.close((closeError) => {
904
+ if (closeError) {
905
+ parentThis.printError(JSON.stringify(closeError, null, 2));
906
+ }
907
+ if (result) {
908
+ this.push(result);
909
+ }
910
+ this.push(null);
911
+ });
912
+ }
913
+ });
914
+ }
915
+ });
916
+ }
498
917
  transaction(container, config, outerTx) {
499
918
  return new ibmi_transaction_default(this, container, config, outerTx);
500
919
  }
@@ -510,32 +929,65 @@ var DB2Client = class extends import_knex.knex.Client {
510
929
  queryCompiler(builder, bindings) {
511
930
  return new ibmi_querycompiler_default(this, builder, bindings);
512
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
+ }
513
937
  processResponse(obj, runner) {
514
938
  if (obj === null) return null;
515
- const resp = obj.response;
516
- const method = obj.sqlMethod;
517
- if (!resp) {
518
- this.printDebug("response undefined" + obj);
939
+ const { response } = obj;
940
+ if (obj.output) {
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
+ }
519
955
  }
520
- const { rows, rowCount } = resp;
521
- if (obj.output) return obj.output.call(runner, resp);
522
- switch (method) {
523
- case "select":
956
+ const validationResult = this.validateResponse(obj);
957
+ if (validationResult !== null) return validationResult;
958
+ return this.processSqlMethod(obj);
959
+ }
960
+ validateResponse(obj) {
961
+ if (!obj.response) {
962
+ this.printDebug("response undefined" + JSON.stringify(obj));
963
+ return null;
964
+ }
965
+ if (!obj.response.rows) {
966
+ this.printError("rows undefined" + JSON.stringify(obj));
967
+ return null;
968
+ }
969
+ return null;
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
+ }
975
+ processSqlMethod(obj) {
976
+ const { rows, rowCount } = obj.response;
977
+ switch (obj.sqlMethod) {
978
+ case "select" /* SELECT */:
524
979
  return rows;
525
- case "pluck":
980
+ case "pluck" /* PLUCK */:
526
981
  return rows.map(obj.pluck);
527
- case "first":
982
+ case "first" /* FIRST */:
528
983
  return rows[0];
529
- case "insert":
984
+ case "insert" /* INSERT */:
530
985
  return rows;
531
- case "del":
532
- case "delete":
533
- case "update":
534
- if (obj.select) {
535
- return rows;
536
- }
537
- return rowCount;
538
- case "counter":
986
+ case "del" /* DELETE */:
987
+ case "delete" /* DELETE_ALT */:
988
+ case "update" /* UPDATE */:
989
+ return obj.select ? rows : rowCount ?? 0;
990
+ case "counter" /* COUNTER */:
539
991
  return rowCount;
540
992
  default:
541
993
  return rows;
@@ -543,8 +995,11 @@ var DB2Client = class extends import_knex.knex.Client {
543
995
  }
544
996
  };
545
997
  var DB2Dialect = DB2Client;
546
- var src_default = DB2Client;
998
+ var index_default = DB2Client;
547
999
  // Annotate the CommonJS export names for ESM import in node:
548
1000
  0 && (module.exports = {
549
- DB2Dialect
1001
+ DB2Dialect,
1002
+ IBMiMigrationRunner,
1003
+ createIBMiMigrationRunner
550
1004
  });
1005
+ //# sourceMappingURL=index.js.map