@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/README.md +40 -1
- package/dist/cli.cjs +6 -2
- package/dist/index.d.mts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +154 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +154 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
96
|
+
createStatement = `create table ${this.tableName()} as (select * from ${this.tableNameLike()}) with no data`;
|
|
92
97
|
} else {
|
|
93
|
-
createStatement
|
|
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
|
-
|
|
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)
|
|
218
|
+
return "clob(16M)";
|
|
209
219
|
}
|
|
210
220
|
jsonb() {
|
|
211
|
-
return "clob(16M)
|
|
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
|
-
|
|
283
|
-
|
|
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) :
|
|
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
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
|
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};
|
|
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(
|
|
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
|
-
|
|
1057
|
-
|
|
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(
|
|
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" +
|
|
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" +
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
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
|
}
|