@bdkinc/knex-ibmi 0.4.3 → 0.5.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.mjs CHANGED
@@ -86,11 +86,16 @@ var ibmi_compiler_default = IBMiSchemaCompiler;
86
86
  import TableCompiler from "knex/lib/schema/tablecompiler.js";
87
87
  var IBMiTableCompiler = class extends TableCompiler {
88
88
  createQuery(columns, ifNot, like) {
89
- let createStatement = ifNot ? `if object_id('${this.tableName()}', 'U') is null ` : "";
89
+ if (ifNot && this.client?.logger?.warn) {
90
+ this.client.logger.warn(
91
+ "IBM i DB2: IF NOT EXISTS is not natively supported. Use hasTable() check instead."
92
+ );
93
+ }
94
+ let createStatement = "";
90
95
  if (like) {
91
- createStatement += `select * into ${this.tableName()} from ${this.tableNameLike()} WHERE 0=1`;
96
+ createStatement = `create table ${this.tableName()} as (select * from ${this.tableNameLike()}) with no data`;
92
97
  } else {
93
- createStatement += "create table " + this.tableName() + (this._formatting ? " (\n " : " (") + columns.sql.join(this._formatting ? ",\n " : ", ") + this._addChecks() + ")";
98
+ createStatement = "create table " + this.tableName() + (this._formatting ? " (\n " : " (") + columns.sql.join(this._formatting ? ",\n " : ", ") + this._addChecks() + ")";
94
99
  }
95
100
  this.pushQuery(createStatement);
96
101
  if (this.single.comment) {
@@ -187,9 +192,12 @@ var IBMiColumnCompiler = class extends ColumnCompiler {
187
192
  return "decimal(10, 2)";
188
193
  }
189
194
  // IBM i DB2 timestamp
195
+ // Note: IBM i DB2 does not support TIMESTAMP WITH TIME ZONE
190
196
  timestamp(options) {
191
- if (options?.useTz) {
192
- return "timestamp with time zone";
197
+ if (options?.useTz && this.client?.logger?.warn) {
198
+ this.client.logger.warn(
199
+ "IBM i DB2 does not support TIMESTAMP WITH TIME ZONE. Using plain TIMESTAMP instead."
200
+ );
193
201
  }
194
202
  return "timestamp";
195
203
  }
@@ -204,11 +212,13 @@ var IBMiColumnCompiler = class extends ColumnCompiler {
204
212
  return "time";
205
213
  }
206
214
  // JSON support (IBM i 7.3+)
215
+ // Note: CHECK constraints with column references are not supported in this context
216
+ // Users should add validation constraints separately if needed
207
217
  json() {
208
- return "clob(16M) check (json_valid(json_column))";
218
+ return "clob(16M)";
209
219
  }
210
220
  jsonb() {
211
- return "clob(16M) check (json_valid(jsonb_column))";
221
+ return "clob(16M)";
212
222
  }
213
223
  // UUID support using CHAR(36)
214
224
  uuid() {
@@ -279,8 +289,12 @@ var IBMiQueryCompiler = class extends QueryCompiler {
279
289
  }
280
290
  // Override select method to add IBM i optimization hints
281
291
  select() {
282
- const originalResult = super.select.call(this);
283
- return originalResult;
292
+ let sql = super.select.call(this);
293
+ const readUncommitted = this.client?.config?.ibmi?.readUncommitted === true;
294
+ if (readUncommitted && typeof sql === "string") {
295
+ sql = sql + " WITH UR";
296
+ }
297
+ return sql;
284
298
  }
285
299
  formatTimestampLocal(date) {
286
300
  const pad = (n) => String(n).padStart(2, "0");
@@ -314,12 +328,15 @@ var IBMiQueryCompiler = class extends QueryCompiler {
314
328
  const standardInsert = super.insert();
315
329
  const insertSql = typeof standardInsert === "object" && standardInsert.sql ? standardInsert.sql : standardInsert;
316
330
  const multiRow = isArrayInsert && !forceSingleRow;
331
+ if (multiRow && !returning) {
332
+ return { sql: insertSql, returning: void 0 };
333
+ }
317
334
  if (multiRow && returning === "*") {
318
335
  if (this.client?.printWarn) {
319
336
  this.client.printWarn("multi-row insert with returning * may be large");
320
337
  }
321
338
  }
322
- const selectColumns = returning ? this.formatter.columnize(returning) : multiRow ? "*" : "IDENTITY_VAL_LOCAL()";
339
+ const selectColumns = returning ? this.formatter.columnize(returning) : "IDENTITY_VAL_LOCAL()";
323
340
  const sql = `select ${selectColumns} from FINAL TABLE(${insertSql})`;
324
341
  if (multiRowStrategy === "sequential" && isArrayInsert) {
325
342
  const first = originalValues[0];
@@ -574,7 +591,7 @@ var IBMiMigrationRunner = class {
574
591
  console.log(`\u{1F4DD} Creating migration table: ${tableName}`);
575
592
  await this.knex.schema.createTable(tableName, (table) => {
576
593
  table.increments("id").primary();
577
- table.string("name");
594
+ table.string("name").unique();
578
595
  table.integer("batch");
579
596
  table.timestamp("migration_time");
580
597
  });
@@ -742,9 +759,51 @@ function createIBMiMigrationRunner(knex2, config) {
742
759
  }
743
760
 
744
761
  // src/index.ts
762
+ var StatementCache = class {
763
+ constructor(maxSize = 100) {
764
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
765
+ __publicField(this, "maxSize");
766
+ this.maxSize = maxSize;
767
+ }
768
+ get(sql) {
769
+ const stmt = this.cache.get(sql);
770
+ if (stmt) {
771
+ this.cache.delete(sql);
772
+ this.cache.set(sql, stmt);
773
+ }
774
+ return stmt;
775
+ }
776
+ set(sql, stmt) {
777
+ if (this.cache.size >= this.maxSize) {
778
+ const firstKey = this.cache.keys().next().value;
779
+ const oldStmt = this.cache.get(firstKey);
780
+ this.cache.delete(firstKey);
781
+ if (oldStmt && typeof oldStmt.close === "function") {
782
+ oldStmt.close().catch(() => {
783
+ });
784
+ }
785
+ }
786
+ this.cache.set(sql, stmt);
787
+ }
788
+ async clear() {
789
+ const statements = Array.from(this.cache.values());
790
+ this.cache.clear();
791
+ await Promise.all(
792
+ statements.map(
793
+ (stmt) => stmt && typeof stmt.close === "function" ? stmt.close().catch(() => {
794
+ }) : Promise.resolve()
795
+ )
796
+ );
797
+ }
798
+ size() {
799
+ return this.cache.size;
800
+ }
801
+ };
745
802
  var DB2Client = class extends knex.Client {
746
803
  constructor(config) {
747
804
  super(config);
805
+ // Per-connection statement cache (WeakMap so it's GC'd with connections)
806
+ __publicField(this, "statementCaches", /* @__PURE__ */ new WeakMap());
748
807
  this.driverName = "odbc";
749
808
  if (this.dialect && !this.config.client) {
750
809
  this.printWarn(
@@ -817,44 +876,45 @@ var DB2Client = class extends knex.Client {
817
876
  this.printDebug("acquiring raw connection");
818
877
  const connectionConfig = this.config.connection;
819
878
  if (!connectionConfig) {
820
- return this.printError("There is no connection config defined");
879
+ throw new Error("There is no connection config defined");
821
880
  }
822
881
  this.printDebug(
823
882
  "connection config: " + this._getConnectionString(connectionConfig)
824
883
  );
825
- let connection;
826
- if (this.config?.pool) {
827
- const poolConfig = {
828
- connectionString: this._getConnectionString(connectionConfig),
829
- connectionTimeout: this.config?.acquireConnectionTimeout || 6e4,
830
- initialSize: this.config?.pool?.min || 2,
831
- maxSize: this.config?.pool?.max || 10,
832
- reuseConnection: true
833
- };
834
- const pool = await this.driver.pool(poolConfig);
835
- connection = await pool.connect();
836
- } else {
837
- connection = await this.driver.connect(
838
- this._getConnectionString(connectionConfig)
839
- );
840
- }
884
+ const connection = await this.driver.connect(
885
+ this._getConnectionString(connectionConfig)
886
+ );
841
887
  return connection;
842
888
  }
843
889
  // Used to explicitly close a connection, called internally by the pool manager
844
890
  // when a connection times out or the pool is shutdown.
845
891
  async destroyRawConnection(connection) {
846
892
  this.printDebug("destroy connection");
893
+ const cache = this.statementCaches.get(connection);
894
+ if (cache) {
895
+ await cache.clear();
896
+ this.statementCaches.delete(connection);
897
+ }
847
898
  return await connection.close();
848
899
  }
849
900
  _getConnectionString(connectionConfig) {
850
- const connectionStringParams = connectionConfig.connectionStringParams || {};
901
+ const defaults = {
902
+ BLOCKFETCH: 1,
903
+ // Enable block fetch for better performance
904
+ TRUEAUTOCOMMIT: 0
905
+ // Use proper transaction handling
906
+ };
907
+ const connectionStringParams = {
908
+ ...defaults,
909
+ ...connectionConfig.connectionStringParams || {}
910
+ };
851
911
  const connectionStringExtension = Object.keys(
852
912
  connectionStringParams
853
913
  ).reduce((result, key) => {
854
914
  const value = connectionStringParams[key];
855
915
  return `${result}${key}=${value};`;
856
916
  }, "");
857
- return `DRIVER=${connectionConfig.driver};SYSTEM=${connectionConfig.host};HOSTNAME=${connectionConfig.host};PORT=${connectionConfig.port};DATABASE=${connectionConfig.database};UID=${connectionConfig.user};PWD=${connectionConfig.password};` + connectionStringExtension;
917
+ return `DRIVER=${connectionConfig.driver};SYSTEM=${connectionConfig.host};PORT=${connectionConfig.port || 8471};DATABASE=${connectionConfig.database};UID=${connectionConfig.user};PWD=${connectionConfig.password};` + connectionStringExtension;
858
918
  }
859
919
  // Runs the query on the specified connection, providing the bindings
860
920
  async _query(connection, obj) {
@@ -875,7 +935,9 @@ var DB2Client = class extends knex.Client {
875
935
  `Executing ${method} query: ${queryObject.sql.substring(0, 200)}...`
876
936
  );
877
937
  if (queryObject.bindings?.length) {
878
- this.printDebug(`Bindings: ${JSON.stringify(queryObject.bindings)}`);
938
+ this.printDebug(
939
+ `Bindings: ${this.safeStringify(queryObject.bindings)}`
940
+ );
879
941
  }
880
942
  }
881
943
  try {
@@ -1052,9 +1114,30 @@ var DB2Client = class extends knex.Client {
1052
1114
  }
1053
1115
  async executeStatementQuery(connection, obj) {
1054
1116
  let statement;
1117
+ let usedCache = false;
1118
+ const cacheEnabled = this.config?.ibmi?.preparedStatementCache === true;
1055
1119
  try {
1056
- statement = await connection.createStatement();
1057
- await statement.prepare(obj.sql);
1120
+ if (cacheEnabled) {
1121
+ let cache = this.statementCaches.get(connection);
1122
+ if (!cache) {
1123
+ const cacheSize = this.config?.ibmi?.preparedStatementCacheSize || 100;
1124
+ cache = new StatementCache(cacheSize);
1125
+ this.statementCaches.set(connection, cache);
1126
+ }
1127
+ statement = cache.get(obj.sql);
1128
+ if (statement) {
1129
+ usedCache = true;
1130
+ this.printDebug(`Using cached statement for: ${obj.sql.substring(0, 50)}...`);
1131
+ } else {
1132
+ statement = await connection.createStatement();
1133
+ await statement.prepare(obj.sql);
1134
+ cache.set(obj.sql, statement);
1135
+ this.printDebug(`Cached new statement (cache size: ${cache.size()})`);
1136
+ }
1137
+ } else {
1138
+ statement = await connection.createStatement();
1139
+ await statement.prepare(obj.sql);
1140
+ }
1058
1141
  if (obj.bindings) {
1059
1142
  await statement.bind(obj.bindings);
1060
1143
  }
@@ -1079,7 +1162,7 @@ var DB2Client = class extends knex.Client {
1079
1162
  this.printError(this.safeStringify(err));
1080
1163
  throw err;
1081
1164
  } finally {
1082
- if (statement && typeof statement.close === "function") {
1165
+ if (!usedCache && statement && typeof statement.close === "function") {
1083
1166
  try {
1084
1167
  await statement.close();
1085
1168
  } catch (closeErr) {
@@ -1188,7 +1271,7 @@ var DB2Client = class extends knex.Client {
1188
1271
  isClosed = true;
1189
1272
  cursor.close((closeError) => {
1190
1273
  if (closeError) {
1191
- parentThis.printError(JSON.stringify(closeError, null, 2));
1274
+ parentThis.printError(parentThis.safeStringify(closeError, 2));
1192
1275
  }
1193
1276
  if (result) {
1194
1277
  this.push(result);
@@ -1261,11 +1344,11 @@ var DB2Client = class extends knex.Client {
1261
1344
  }
1262
1345
  validateResponse(obj) {
1263
1346
  if (!obj.response) {
1264
- this.printDebug("response undefined" + JSON.stringify(obj));
1347
+ this.printDebug("response undefined " + this.safeStringify(obj));
1265
1348
  return null;
1266
1349
  }
1267
1350
  if (!obj.response.rows) {
1268
- this.printError("rows undefined" + JSON.stringify(obj));
1351
+ this.printError("rows undefined " + this.safeStringify(obj));
1269
1352
  return null;
1270
1353
  }
1271
1354
  return null;
@@ -1276,23 +1359,24 @@ var DB2Client = class extends knex.Client {
1276
1359
  sql: queryObject.sql ? queryObject.sql.substring(0, 100) + "..." : "unknown",
1277
1360
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1278
1361
  };
1362
+ const contextStr = this.safeStringify(context);
1279
1363
  if (this.isConnectionError(error)) {
1280
1364
  return new Error(
1281
- `IBM i DB2 connection error during ${method}: ${error.message} | Context: ${JSON.stringify(context)}`
1365
+ `IBM i DB2 connection error during ${method}: ${error.message} | Context: ${contextStr}`
1282
1366
  );
1283
1367
  }
1284
1368
  if (this.isTimeoutError(error)) {
1285
1369
  return new Error(
1286
- `IBM i DB2 timeout during ${method}: ${error.message} | Context: ${JSON.stringify(context)}`
1370
+ `IBM i DB2 timeout during ${method}: ${error.message} | Context: ${contextStr}`
1287
1371
  );
1288
1372
  }
1289
1373
  if (this.isSQLError(error)) {
1290
1374
  return new Error(
1291
- `IBM i DB2 SQL error during ${method}: ${error.message} | Context: ${JSON.stringify(context)}`
1375
+ `IBM i DB2 SQL error during ${method}: ${error.message} | Context: ${contextStr}`
1292
1376
  );
1293
1377
  }
1294
1378
  return new Error(
1295
- `IBM i DB2 error during ${method}: ${error.message} | Context: ${JSON.stringify(context)}`
1379
+ `IBM i DB2 error during ${method}: ${error.message} | Context: ${contextStr}`
1296
1380
  );
1297
1381
  }
1298
1382
  shouldRetryQuery(queryObject, method) {
@@ -1314,15 +1398,44 @@ var DB2Client = class extends knex.Client {
1314
1398
  return queryObject;
1315
1399
  }
1316
1400
  }
1401
+ /**
1402
+ * Extract SQLSTATE from ODBC error if available
1403
+ */
1404
+ getSQLState(error) {
1405
+ if (error?.odbcErrors && Array.isArray(error.odbcErrors)) {
1406
+ for (const odbcErr of error.odbcErrors) {
1407
+ const state = odbcErr?.state || odbcErr?.SQLSTATE;
1408
+ if (state) return String(state).toUpperCase();
1409
+ }
1410
+ }
1411
+ return null;
1412
+ }
1317
1413
  isConnectionError(error) {
1414
+ const sqlState = this.getSQLState(error);
1415
+ if (sqlState) {
1416
+ return sqlState.startsWith("08") || // 08001, 08003, 08007, 08S01, etc.
1417
+ sqlState === "40003";
1418
+ }
1318
1419
  const errorMessage = (error.message || error.toString || error).toLowerCase();
1319
1420
  return errorMessage.includes("connection") && (errorMessage.includes("closed") || errorMessage.includes("invalid") || errorMessage.includes("terminated") || errorMessage.includes("not connected"));
1320
1421
  }
1321
1422
  isTimeoutError(error) {
1423
+ const sqlState = this.getSQLState(error);
1424
+ if (sqlState) {
1425
+ return sqlState === "HYT00" || // Timeout expired
1426
+ sqlState === "HYT01";
1427
+ }
1322
1428
  const errorMessage = (error.message || error.toString || error).toLowerCase();
1323
1429
  return errorMessage.includes("timeout") || errorMessage.includes("timed out");
1324
1430
  }
1325
1431
  isSQLError(error) {
1432
+ const sqlState = this.getSQLState(error);
1433
+ if (sqlState) {
1434
+ return sqlState.startsWith("42") || // Syntax error or access violation
1435
+ sqlState.startsWith("22") || // Data exception
1436
+ sqlState.startsWith("23") || // Integrity constraint violation
1437
+ sqlState.startsWith("21");
1438
+ }
1326
1439
  const errorMessage = (error.message || error.toString || error).toLowerCase();
1327
1440
  return errorMessage.includes("sql") || errorMessage.includes("syntax") || errorMessage.includes("table") || errorMessage.includes("column");
1328
1441
  }