@bdkinc/knex-ibmi 0.4.4 → 0.5.2
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 +38 -0
- package/dist/cli.cjs +1 -1
- 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/README.md
CHANGED
|
@@ -396,11 +396,49 @@ Native `RETURNING` is not broadly supported over ODBC on IBM i. The dialect prov
|
|
|
396
396
|
interface IbmiDialectConfig {
|
|
397
397
|
multiRowInsert?: 'auto' | 'sequential' | 'disabled';
|
|
398
398
|
sequentialInsertTransactional?: boolean; // if true, wraps sequential loop in BEGIN/COMMIT
|
|
399
|
+
preparedStatementCache?: boolean; // Enable per-connection statement caching (default: false)
|
|
400
|
+
preparedStatementCacheSize?: number; // Max cached statements per connection (default: 100)
|
|
401
|
+
readUncommitted?: boolean; // Append WITH UR to SELECT queries (default: false)
|
|
399
402
|
}
|
|
400
403
|
```
|
|
401
404
|
|
|
402
405
|
Attach under the root knex config as `ibmi`.
|
|
403
406
|
|
|
407
|
+
### Performance Tuning
|
|
408
|
+
|
|
409
|
+
#### Prepared Statement Caching (v0.5.0+)
|
|
410
|
+
|
|
411
|
+
Enable optional prepared statement caching to reduce parse overhead for repeated queries:
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const db = knex({
|
|
415
|
+
client: DB2Dialect,
|
|
416
|
+
connection: { /* ... */ },
|
|
417
|
+
ibmi: {
|
|
418
|
+
preparedStatementCache: true, // Enable caching
|
|
419
|
+
preparedStatementCacheSize: 100, // Max statements per connection
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
When enabled, the dialect maintains a per-connection LRU cache of prepared statements. Statements are automatically closed when evicted or when the connection is destroyed.
|
|
425
|
+
|
|
426
|
+
#### Read Uncommitted Isolation (v0.5.0+)
|
|
427
|
+
|
|
428
|
+
For read-heavy workloads, enable uncommitted read isolation to improve concurrency:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
const db = knex({
|
|
432
|
+
client: DB2Dialect,
|
|
433
|
+
connection: { /* ... */ },
|
|
434
|
+
ibmi: {
|
|
435
|
+
readUncommitted: true // Appends WITH UR to all SELECT queries
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
This appends `WITH UR` to all SELECT queries, allowing reads without waiting for locks. Only use this if your application can tolerate reading uncommitted data.
|
|
441
|
+
|
|
404
442
|
### Transactional Sequential Inserts
|
|
405
443
|
|
|
406
444
|
When `ibmi.sequentialInsertTransactional` is `true`, the dialect will attempt `BEGIN` before the per-row loop and `COMMIT` after. On commit failure it will attempt a `ROLLBACK`. If `BEGIN` is not supported, it logs a warning and continues non-transactionally.
|
package/dist/cli.cjs
CHANGED
|
@@ -63,7 +63,7 @@ var IBMiMigrationRunner = class {
|
|
|
63
63
|
console.log(`\u{1F4DD} Creating migration table: ${tableName}`);
|
|
64
64
|
await this.knex.schema.createTable(tableName, (table) => {
|
|
65
65
|
table.increments("id").primary();
|
|
66
|
-
table.string("name");
|
|
66
|
+
table.string("name").unique();
|
|
67
67
|
table.integer("batch");
|
|
68
68
|
table.timestamp("migration_time");
|
|
69
69
|
});
|
package/dist/index.d.mts
CHANGED
|
@@ -43,6 +43,7 @@ declare enum SqlMethod {
|
|
|
43
43
|
COUNTER = "counter"
|
|
44
44
|
}
|
|
45
45
|
declare class DB2Client extends knex.Client {
|
|
46
|
+
private statementCaches;
|
|
46
47
|
constructor(config: Knex.Config<DB2Config>);
|
|
47
48
|
private safeStringify;
|
|
48
49
|
_driver(): typeof odbc;
|
|
@@ -50,7 +51,7 @@ declare class DB2Client extends knex.Client {
|
|
|
50
51
|
printDebug(message: string): void;
|
|
51
52
|
printError(message: string): void;
|
|
52
53
|
printWarn(message: string): void;
|
|
53
|
-
acquireRawConnection(): Promise<
|
|
54
|
+
acquireRawConnection(): Promise<any>;
|
|
54
55
|
destroyRawConnection(connection: any): Promise<any>;
|
|
55
56
|
_getConnectionString(connectionConfig: DB2ConnectionConfig): string;
|
|
56
57
|
_query(connection: Connection, obj: any): Promise<any>;
|
|
@@ -93,6 +94,10 @@ declare class DB2Client extends knex.Client {
|
|
|
93
94
|
private wrapError;
|
|
94
95
|
private shouldRetryQuery;
|
|
95
96
|
private retryQuery;
|
|
97
|
+
/**
|
|
98
|
+
* Extract SQLSTATE from ODBC error if available
|
|
99
|
+
*/
|
|
100
|
+
private getSQLState;
|
|
96
101
|
private isConnectionError;
|
|
97
102
|
private isTimeoutError;
|
|
98
103
|
private isSQLError;
|
|
@@ -149,6 +154,9 @@ interface DB2Config extends Knex.Config {
|
|
|
149
154
|
ibmi?: {
|
|
150
155
|
multiRowInsert?: "auto" | "sequential" | "disabled";
|
|
151
156
|
sequentialInsertTransactional?: boolean;
|
|
157
|
+
preparedStatementCache?: boolean;
|
|
158
|
+
preparedStatementCacheSize?: number;
|
|
159
|
+
readUncommitted?: boolean;
|
|
152
160
|
};
|
|
153
161
|
}
|
|
154
162
|
declare const DB2Dialect: typeof DB2Client;
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ declare enum SqlMethod {
|
|
|
43
43
|
COUNTER = "counter"
|
|
44
44
|
}
|
|
45
45
|
declare class DB2Client extends knex.Client {
|
|
46
|
+
private statementCaches;
|
|
46
47
|
constructor(config: Knex.Config<DB2Config>);
|
|
47
48
|
private safeStringify;
|
|
48
49
|
_driver(): typeof odbc;
|
|
@@ -50,7 +51,7 @@ declare class DB2Client extends knex.Client {
|
|
|
50
51
|
printDebug(message: string): void;
|
|
51
52
|
printError(message: string): void;
|
|
52
53
|
printWarn(message: string): void;
|
|
53
|
-
acquireRawConnection(): Promise<
|
|
54
|
+
acquireRawConnection(): Promise<any>;
|
|
54
55
|
destroyRawConnection(connection: any): Promise<any>;
|
|
55
56
|
_getConnectionString(connectionConfig: DB2ConnectionConfig): string;
|
|
56
57
|
_query(connection: Connection, obj: any): Promise<any>;
|
|
@@ -93,6 +94,10 @@ declare class DB2Client extends knex.Client {
|
|
|
93
94
|
private wrapError;
|
|
94
95
|
private shouldRetryQuery;
|
|
95
96
|
private retryQuery;
|
|
97
|
+
/**
|
|
98
|
+
* Extract SQLSTATE from ODBC error if available
|
|
99
|
+
*/
|
|
100
|
+
private getSQLState;
|
|
96
101
|
private isConnectionError;
|
|
97
102
|
private isTimeoutError;
|
|
98
103
|
private isSQLError;
|
|
@@ -149,6 +154,9 @@ interface DB2Config extends Knex.Config {
|
|
|
149
154
|
ibmi?: {
|
|
150
155
|
multiRowInsert?: "auto" | "sequential" | "disabled";
|
|
151
156
|
sequentialInsertTransactional?: boolean;
|
|
157
|
+
preparedStatementCache?: boolean;
|
|
158
|
+
preparedStatementCacheSize?: number;
|
|
159
|
+
readUncommitted?: boolean;
|
|
152
160
|
};
|
|
153
161
|
}
|
|
154
162
|
declare const DB2Dialect: typeof DB2Client;
|
package/dist/index.js
CHANGED
|
@@ -120,11 +120,16 @@ var ibmi_compiler_default = IBMiSchemaCompiler;
|
|
|
120
120
|
var import_tablecompiler = __toESM(require("knex/lib/schema/tablecompiler.js"));
|
|
121
121
|
var IBMiTableCompiler = class extends import_tablecompiler.default {
|
|
122
122
|
createQuery(columns, ifNot, like) {
|
|
123
|
-
|
|
123
|
+
if (ifNot && this.client?.logger?.warn) {
|
|
124
|
+
this.client.logger.warn(
|
|
125
|
+
"IBM i DB2: IF NOT EXISTS is not natively supported. Use hasTable() check instead."
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
let createStatement = "";
|
|
124
129
|
if (like) {
|
|
125
|
-
createStatement
|
|
130
|
+
createStatement = `create table ${this.tableName()} as (select * from ${this.tableNameLike()}) with no data`;
|
|
126
131
|
} else {
|
|
127
|
-
createStatement
|
|
132
|
+
createStatement = "create table " + this.tableName() + (this._formatting ? " (\n " : " (") + columns.sql.join(this._formatting ? ",\n " : ", ") + this._addChecks() + ")";
|
|
128
133
|
}
|
|
129
134
|
this.pushQuery(createStatement);
|
|
130
135
|
if (this.single.comment) {
|
|
@@ -221,9 +226,12 @@ var IBMiColumnCompiler = class extends import_columncompiler.default {
|
|
|
221
226
|
return "decimal(10, 2)";
|
|
222
227
|
}
|
|
223
228
|
// IBM i DB2 timestamp
|
|
229
|
+
// Note: IBM i DB2 does not support TIMESTAMP WITH TIME ZONE
|
|
224
230
|
timestamp(options) {
|
|
225
|
-
if (options?.useTz) {
|
|
226
|
-
|
|
231
|
+
if (options?.useTz && this.client?.logger?.warn) {
|
|
232
|
+
this.client.logger.warn(
|
|
233
|
+
"IBM i DB2 does not support TIMESTAMP WITH TIME ZONE. Using plain TIMESTAMP instead."
|
|
234
|
+
);
|
|
227
235
|
}
|
|
228
236
|
return "timestamp";
|
|
229
237
|
}
|
|
@@ -238,11 +246,13 @@ var IBMiColumnCompiler = class extends import_columncompiler.default {
|
|
|
238
246
|
return "time";
|
|
239
247
|
}
|
|
240
248
|
// JSON support (IBM i 7.3+)
|
|
249
|
+
// Note: CHECK constraints with column references are not supported in this context
|
|
250
|
+
// Users should add validation constraints separately if needed
|
|
241
251
|
json() {
|
|
242
|
-
return "clob(16M)
|
|
252
|
+
return "clob(16M)";
|
|
243
253
|
}
|
|
244
254
|
jsonb() {
|
|
245
|
-
return "clob(16M)
|
|
255
|
+
return "clob(16M)";
|
|
246
256
|
}
|
|
247
257
|
// UUID support using CHAR(36)
|
|
248
258
|
uuid() {
|
|
@@ -313,8 +323,12 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
|
|
|
313
323
|
}
|
|
314
324
|
// Override select method to add IBM i optimization hints
|
|
315
325
|
select() {
|
|
316
|
-
|
|
317
|
-
|
|
326
|
+
let sql = super.select.call(this);
|
|
327
|
+
const readUncommitted = this.client?.config?.ibmi?.readUncommitted === true;
|
|
328
|
+
if (readUncommitted && typeof sql === "string") {
|
|
329
|
+
sql = sql + " WITH UR";
|
|
330
|
+
}
|
|
331
|
+
return sql;
|
|
318
332
|
}
|
|
319
333
|
formatTimestampLocal(date) {
|
|
320
334
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -348,12 +362,15 @@ var IBMiQueryCompiler = class extends import_querycompiler.default {
|
|
|
348
362
|
const standardInsert = super.insert();
|
|
349
363
|
const insertSql = typeof standardInsert === "object" && standardInsert.sql ? standardInsert.sql : standardInsert;
|
|
350
364
|
const multiRow = isArrayInsert && !forceSingleRow;
|
|
365
|
+
if (multiRow && !returning) {
|
|
366
|
+
return { sql: insertSql, returning: void 0 };
|
|
367
|
+
}
|
|
351
368
|
if (multiRow && returning === "*") {
|
|
352
369
|
if (this.client?.printWarn) {
|
|
353
370
|
this.client.printWarn("multi-row insert with returning * may be large");
|
|
354
371
|
}
|
|
355
372
|
}
|
|
356
|
-
const selectColumns = returning ? this.formatter.columnize(returning) :
|
|
373
|
+
const selectColumns = returning ? this.formatter.columnize(returning) : "IDENTITY_VAL_LOCAL()";
|
|
357
374
|
const sql = `select ${selectColumns} from FINAL TABLE(${insertSql})`;
|
|
358
375
|
if (multiRowStrategy === "sequential" && isArrayInsert) {
|
|
359
376
|
const first = originalValues[0];
|
|
@@ -608,7 +625,7 @@ var IBMiMigrationRunner = class {
|
|
|
608
625
|
console.log(`\u{1F4DD} Creating migration table: ${tableName}`);
|
|
609
626
|
await this.knex.schema.createTable(tableName, (table) => {
|
|
610
627
|
table.increments("id").primary();
|
|
611
|
-
table.string("name");
|
|
628
|
+
table.string("name").unique();
|
|
612
629
|
table.integer("batch");
|
|
613
630
|
table.timestamp("migration_time");
|
|
614
631
|
});
|
|
@@ -776,9 +793,51 @@ function createIBMiMigrationRunner(knex2, config) {
|
|
|
776
793
|
}
|
|
777
794
|
|
|
778
795
|
// src/index.ts
|
|
796
|
+
var StatementCache = class {
|
|
797
|
+
constructor(maxSize = 100) {
|
|
798
|
+
__publicField(this, "cache", /* @__PURE__ */ new Map());
|
|
799
|
+
__publicField(this, "maxSize");
|
|
800
|
+
this.maxSize = maxSize;
|
|
801
|
+
}
|
|
802
|
+
get(sql) {
|
|
803
|
+
const stmt = this.cache.get(sql);
|
|
804
|
+
if (stmt) {
|
|
805
|
+
this.cache.delete(sql);
|
|
806
|
+
this.cache.set(sql, stmt);
|
|
807
|
+
}
|
|
808
|
+
return stmt;
|
|
809
|
+
}
|
|
810
|
+
set(sql, stmt) {
|
|
811
|
+
if (this.cache.size >= this.maxSize) {
|
|
812
|
+
const firstKey = this.cache.keys().next().value;
|
|
813
|
+
const oldStmt = this.cache.get(firstKey);
|
|
814
|
+
this.cache.delete(firstKey);
|
|
815
|
+
if (oldStmt && typeof oldStmt.close === "function") {
|
|
816
|
+
oldStmt.close().catch(() => {
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
this.cache.set(sql, stmt);
|
|
821
|
+
}
|
|
822
|
+
async clear() {
|
|
823
|
+
const statements = Array.from(this.cache.values());
|
|
824
|
+
this.cache.clear();
|
|
825
|
+
await Promise.all(
|
|
826
|
+
statements.map(
|
|
827
|
+
(stmt) => stmt && typeof stmt.close === "function" ? stmt.close().catch(() => {
|
|
828
|
+
}) : Promise.resolve()
|
|
829
|
+
)
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
size() {
|
|
833
|
+
return this.cache.size;
|
|
834
|
+
}
|
|
835
|
+
};
|
|
779
836
|
var DB2Client = class extends import_knex.default.Client {
|
|
780
837
|
constructor(config) {
|
|
781
838
|
super(config);
|
|
839
|
+
// Per-connection statement cache (WeakMap so it's GC'd with connections)
|
|
840
|
+
__publicField(this, "statementCaches", /* @__PURE__ */ new WeakMap());
|
|
782
841
|
this.driverName = "odbc";
|
|
783
842
|
if (this.dialect && !this.config.client) {
|
|
784
843
|
this.printWarn(
|
|
@@ -851,44 +910,45 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
851
910
|
this.printDebug("acquiring raw connection");
|
|
852
911
|
const connectionConfig = this.config.connection;
|
|
853
912
|
if (!connectionConfig) {
|
|
854
|
-
|
|
913
|
+
throw new Error("There is no connection config defined");
|
|
855
914
|
}
|
|
856
915
|
this.printDebug(
|
|
857
916
|
"connection config: " + this._getConnectionString(connectionConfig)
|
|
858
917
|
);
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
connectionString: this._getConnectionString(connectionConfig),
|
|
863
|
-
connectionTimeout: this.config?.acquireConnectionTimeout || 6e4,
|
|
864
|
-
initialSize: this.config?.pool?.min || 2,
|
|
865
|
-
maxSize: this.config?.pool?.max || 10,
|
|
866
|
-
reuseConnection: true
|
|
867
|
-
};
|
|
868
|
-
const pool = await this.driver.pool(poolConfig);
|
|
869
|
-
connection = await pool.connect();
|
|
870
|
-
} else {
|
|
871
|
-
connection = await this.driver.connect(
|
|
872
|
-
this._getConnectionString(connectionConfig)
|
|
873
|
-
);
|
|
874
|
-
}
|
|
918
|
+
const connection = await this.driver.connect(
|
|
919
|
+
this._getConnectionString(connectionConfig)
|
|
920
|
+
);
|
|
875
921
|
return connection;
|
|
876
922
|
}
|
|
877
923
|
// Used to explicitly close a connection, called internally by the pool manager
|
|
878
924
|
// when a connection times out or the pool is shutdown.
|
|
879
925
|
async destroyRawConnection(connection) {
|
|
880
926
|
this.printDebug("destroy connection");
|
|
927
|
+
const cache = this.statementCaches.get(connection);
|
|
928
|
+
if (cache) {
|
|
929
|
+
await cache.clear();
|
|
930
|
+
this.statementCaches.delete(connection);
|
|
931
|
+
}
|
|
881
932
|
return await connection.close();
|
|
882
933
|
}
|
|
883
934
|
_getConnectionString(connectionConfig) {
|
|
884
|
-
const
|
|
935
|
+
const defaults = {
|
|
936
|
+
BLOCKFETCH: 1,
|
|
937
|
+
// Enable block fetch for better performance
|
|
938
|
+
TRUEAUTOCOMMIT: 0
|
|
939
|
+
// Use proper transaction handling
|
|
940
|
+
};
|
|
941
|
+
const connectionStringParams = {
|
|
942
|
+
...defaults,
|
|
943
|
+
...connectionConfig.connectionStringParams || {}
|
|
944
|
+
};
|
|
885
945
|
const connectionStringExtension = Object.keys(
|
|
886
946
|
connectionStringParams
|
|
887
947
|
).reduce((result, key) => {
|
|
888
948
|
const value = connectionStringParams[key];
|
|
889
949
|
return `${result}${key}=${value};`;
|
|
890
950
|
}, "");
|
|
891
|
-
return `DRIVER=${connectionConfig.driver};SYSTEM=${connectionConfig.host};
|
|
951
|
+
return `DRIVER=${connectionConfig.driver};SYSTEM=${connectionConfig.host};PORT=${connectionConfig.port || 8471};DATABASE=${connectionConfig.database};UID=${connectionConfig.user};PWD=${connectionConfig.password};` + connectionStringExtension;
|
|
892
952
|
}
|
|
893
953
|
// Runs the query on the specified connection, providing the bindings
|
|
894
954
|
async _query(connection, obj) {
|
|
@@ -909,7 +969,9 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
909
969
|
`Executing ${method} query: ${queryObject.sql.substring(0, 200)}...`
|
|
910
970
|
);
|
|
911
971
|
if (queryObject.bindings?.length) {
|
|
912
|
-
this.printDebug(
|
|
972
|
+
this.printDebug(
|
|
973
|
+
`Bindings: ${this.safeStringify(queryObject.bindings)}`
|
|
974
|
+
);
|
|
913
975
|
}
|
|
914
976
|
}
|
|
915
977
|
try {
|
|
@@ -1086,9 +1148,30 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
1086
1148
|
}
|
|
1087
1149
|
async executeStatementQuery(connection, obj) {
|
|
1088
1150
|
let statement;
|
|
1151
|
+
let usedCache = false;
|
|
1152
|
+
const cacheEnabled = this.config?.ibmi?.preparedStatementCache === true;
|
|
1089
1153
|
try {
|
|
1090
|
-
|
|
1091
|
-
|
|
1154
|
+
if (cacheEnabled) {
|
|
1155
|
+
let cache = this.statementCaches.get(connection);
|
|
1156
|
+
if (!cache) {
|
|
1157
|
+
const cacheSize = this.config?.ibmi?.preparedStatementCacheSize || 100;
|
|
1158
|
+
cache = new StatementCache(cacheSize);
|
|
1159
|
+
this.statementCaches.set(connection, cache);
|
|
1160
|
+
}
|
|
1161
|
+
statement = cache.get(obj.sql);
|
|
1162
|
+
if (statement) {
|
|
1163
|
+
usedCache = true;
|
|
1164
|
+
this.printDebug(`Using cached statement for: ${obj.sql.substring(0, 50)}...`);
|
|
1165
|
+
} else {
|
|
1166
|
+
statement = await connection.createStatement();
|
|
1167
|
+
await statement.prepare(obj.sql);
|
|
1168
|
+
cache.set(obj.sql, statement);
|
|
1169
|
+
this.printDebug(`Cached new statement (cache size: ${cache.size()})`);
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
statement = await connection.createStatement();
|
|
1173
|
+
await statement.prepare(obj.sql);
|
|
1174
|
+
}
|
|
1092
1175
|
if (obj.bindings) {
|
|
1093
1176
|
await statement.bind(obj.bindings);
|
|
1094
1177
|
}
|
|
@@ -1113,7 +1196,7 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
1113
1196
|
this.printError(this.safeStringify(err));
|
|
1114
1197
|
throw err;
|
|
1115
1198
|
} finally {
|
|
1116
|
-
if (statement && typeof statement.close === "function") {
|
|
1199
|
+
if (!usedCache && statement && typeof statement.close === "function") {
|
|
1117
1200
|
try {
|
|
1118
1201
|
await statement.close();
|
|
1119
1202
|
} catch (closeErr) {
|
|
@@ -1222,7 +1305,7 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
1222
1305
|
isClosed = true;
|
|
1223
1306
|
cursor.close((closeError) => {
|
|
1224
1307
|
if (closeError) {
|
|
1225
|
-
parentThis.printError(
|
|
1308
|
+
parentThis.printError(parentThis.safeStringify(closeError, 2));
|
|
1226
1309
|
}
|
|
1227
1310
|
if (result) {
|
|
1228
1311
|
this.push(result);
|
|
@@ -1295,11 +1378,11 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
1295
1378
|
}
|
|
1296
1379
|
validateResponse(obj) {
|
|
1297
1380
|
if (!obj.response) {
|
|
1298
|
-
this.printDebug("response undefined" +
|
|
1381
|
+
this.printDebug("response undefined " + this.safeStringify(obj));
|
|
1299
1382
|
return null;
|
|
1300
1383
|
}
|
|
1301
1384
|
if (!obj.response.rows) {
|
|
1302
|
-
this.printError("rows undefined" +
|
|
1385
|
+
this.printError("rows undefined " + this.safeStringify(obj));
|
|
1303
1386
|
return null;
|
|
1304
1387
|
}
|
|
1305
1388
|
return null;
|
|
@@ -1310,23 +1393,24 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
1310
1393
|
sql: queryObject.sql ? queryObject.sql.substring(0, 100) + "..." : "unknown",
|
|
1311
1394
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1312
1395
|
};
|
|
1396
|
+
const contextStr = this.safeStringify(context);
|
|
1313
1397
|
if (this.isConnectionError(error)) {
|
|
1314
1398
|
return new Error(
|
|
1315
|
-
`IBM i DB2 connection error during ${method}: ${error.message} | Context: ${
|
|
1399
|
+
`IBM i DB2 connection error during ${method}: ${error.message} | Context: ${contextStr}`
|
|
1316
1400
|
);
|
|
1317
1401
|
}
|
|
1318
1402
|
if (this.isTimeoutError(error)) {
|
|
1319
1403
|
return new Error(
|
|
1320
|
-
`IBM i DB2 timeout during ${method}: ${error.message} | Context: ${
|
|
1404
|
+
`IBM i DB2 timeout during ${method}: ${error.message} | Context: ${contextStr}`
|
|
1321
1405
|
);
|
|
1322
1406
|
}
|
|
1323
1407
|
if (this.isSQLError(error)) {
|
|
1324
1408
|
return new Error(
|
|
1325
|
-
`IBM i DB2 SQL error during ${method}: ${error.message} | Context: ${
|
|
1409
|
+
`IBM i DB2 SQL error during ${method}: ${error.message} | Context: ${contextStr}`
|
|
1326
1410
|
);
|
|
1327
1411
|
}
|
|
1328
1412
|
return new Error(
|
|
1329
|
-
`IBM i DB2 error during ${method}: ${error.message} | Context: ${
|
|
1413
|
+
`IBM i DB2 error during ${method}: ${error.message} | Context: ${contextStr}`
|
|
1330
1414
|
);
|
|
1331
1415
|
}
|
|
1332
1416
|
shouldRetryQuery(queryObject, method) {
|
|
@@ -1348,15 +1432,44 @@ var DB2Client = class extends import_knex.default.Client {
|
|
|
1348
1432
|
return queryObject;
|
|
1349
1433
|
}
|
|
1350
1434
|
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Extract SQLSTATE from ODBC error if available
|
|
1437
|
+
*/
|
|
1438
|
+
getSQLState(error) {
|
|
1439
|
+
if (error?.odbcErrors && Array.isArray(error.odbcErrors)) {
|
|
1440
|
+
for (const odbcErr of error.odbcErrors) {
|
|
1441
|
+
const state = odbcErr?.state || odbcErr?.SQLSTATE;
|
|
1442
|
+
if (state) return String(state).toUpperCase();
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1351
1447
|
isConnectionError(error) {
|
|
1448
|
+
const sqlState = this.getSQLState(error);
|
|
1449
|
+
if (sqlState) {
|
|
1450
|
+
return sqlState.startsWith("08") || // 08001, 08003, 08007, 08S01, etc.
|
|
1451
|
+
sqlState === "40003";
|
|
1452
|
+
}
|
|
1352
1453
|
const errorMessage = (error.message || error.toString || error).toLowerCase();
|
|
1353
1454
|
return errorMessage.includes("connection") && (errorMessage.includes("closed") || errorMessage.includes("invalid") || errorMessage.includes("terminated") || errorMessage.includes("not connected"));
|
|
1354
1455
|
}
|
|
1355
1456
|
isTimeoutError(error) {
|
|
1457
|
+
const sqlState = this.getSQLState(error);
|
|
1458
|
+
if (sqlState) {
|
|
1459
|
+
return sqlState === "HYT00" || // Timeout expired
|
|
1460
|
+
sqlState === "HYT01";
|
|
1461
|
+
}
|
|
1356
1462
|
const errorMessage = (error.message || error.toString || error).toLowerCase();
|
|
1357
1463
|
return errorMessage.includes("timeout") || errorMessage.includes("timed out");
|
|
1358
1464
|
}
|
|
1359
1465
|
isSQLError(error) {
|
|
1466
|
+
const sqlState = this.getSQLState(error);
|
|
1467
|
+
if (sqlState) {
|
|
1468
|
+
return sqlState.startsWith("42") || // Syntax error or access violation
|
|
1469
|
+
sqlState.startsWith("22") || // Data exception
|
|
1470
|
+
sqlState.startsWith("23") || // Integrity constraint violation
|
|
1471
|
+
sqlState.startsWith("21");
|
|
1472
|
+
}
|
|
1360
1473
|
const errorMessage = (error.message || error.toString || error).toLowerCase();
|
|
1361
1474
|
return errorMessage.includes("sql") || errorMessage.includes("syntax") || errorMessage.includes("table") || errorMessage.includes("column");
|
|
1362
1475
|
}
|