@saptools/cf-hana 0.1.1 → 0.1.3
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/CHANGELOG.md +17 -0
- package/README.md +29 -10
- package/dist/cli.js +314 -46
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +295 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -243,6 +243,7 @@ async function listColumns(connection, schema, table) {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
// src/config.ts
|
|
246
|
+
var CLI_VERSION = "0.1.3";
|
|
246
247
|
var ENV_PREFIX = "CF_HANA";
|
|
247
248
|
var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
248
249
|
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
@@ -493,7 +494,7 @@ var HdbConnection = class {
|
|
|
493
494
|
const raw = await this.executeStatement(statement, params);
|
|
494
495
|
return toExecResult(statement, raw);
|
|
495
496
|
} finally {
|
|
496
|
-
statement
|
|
497
|
+
dropStatementQuietly(statement);
|
|
497
498
|
}
|
|
498
499
|
}
|
|
499
500
|
setAutoCommit(enabled) {
|
|
@@ -560,6 +561,12 @@ var HdbConnection = class {
|
|
|
560
561
|
});
|
|
561
562
|
}
|
|
562
563
|
};
|
|
564
|
+
function dropStatementQuietly(statement) {
|
|
565
|
+
try {
|
|
566
|
+
statement.drop();
|
|
567
|
+
} catch {
|
|
568
|
+
}
|
|
569
|
+
}
|
|
563
570
|
function openClient(client, timeoutMs) {
|
|
564
571
|
return new Promise((resolve, reject) => {
|
|
565
572
|
let settled = false;
|
|
@@ -595,6 +602,20 @@ function openClient(client, timeoutMs) {
|
|
|
595
602
|
});
|
|
596
603
|
});
|
|
597
604
|
}
|
|
605
|
+
async function closeClientQuietly(client) {
|
|
606
|
+
try {
|
|
607
|
+
await new Promise((resolve) => {
|
|
608
|
+
client.disconnect(() => {
|
|
609
|
+
resolve();
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
client.close();
|
|
616
|
+
} catch {
|
|
617
|
+
}
|
|
618
|
+
}
|
|
598
619
|
function setCurrentSchema(client, schema) {
|
|
599
620
|
return new Promise((resolve, reject) => {
|
|
600
621
|
client.exec(`SET SCHEMA ${quoteIdentifier(schema)}`, (error) => {
|
|
@@ -616,7 +637,12 @@ async function connectHdb(params) {
|
|
|
616
637
|
useTLS: true
|
|
617
638
|
});
|
|
618
639
|
await openClient(client, params.connectTimeoutMs);
|
|
619
|
-
|
|
640
|
+
try {
|
|
641
|
+
await setCurrentSchema(client, params.schema);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
await closeClientQuietly(client);
|
|
644
|
+
throw error;
|
|
645
|
+
}
|
|
620
646
|
return new HdbConnection(client);
|
|
621
647
|
}
|
|
622
648
|
function createHdbDriver() {
|
|
@@ -639,14 +665,178 @@ function createDriver(name) {
|
|
|
639
665
|
}
|
|
640
666
|
}
|
|
641
667
|
|
|
668
|
+
// src/history.ts
|
|
669
|
+
import { appendFile, mkdir, readdir, rm } from "fs/promises";
|
|
670
|
+
import { homedir } from "os";
|
|
671
|
+
import { join } from "path";
|
|
672
|
+
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
673
|
+
var CF_HANA_DIR_NAME = "cf-hana";
|
|
674
|
+
var HISTORIES_DIR_NAME = "histories";
|
|
675
|
+
var HISTORY_RETENTION_DAYS = 5;
|
|
676
|
+
var HISTORY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
677
|
+
function defaultSaptoolsRoot() {
|
|
678
|
+
return join(homedir(), SAPTOOLS_DIR_NAME);
|
|
679
|
+
}
|
|
680
|
+
function padDatePart(value) {
|
|
681
|
+
return value.toString().padStart(2, "0");
|
|
682
|
+
}
|
|
683
|
+
function dateKey(date) {
|
|
684
|
+
return [
|
|
685
|
+
date.getFullYear().toString(),
|
|
686
|
+
padDatePart(date.getMonth() + 1),
|
|
687
|
+
padDatePart(date.getDate())
|
|
688
|
+
].join("-");
|
|
689
|
+
}
|
|
690
|
+
function retentionCutoffKey(now) {
|
|
691
|
+
const cutoff = new Date(now);
|
|
692
|
+
cutoff.setHours(0, 0, 0, 0);
|
|
693
|
+
cutoff.setDate(cutoff.getDate() - HISTORY_RETENTION_DAYS);
|
|
694
|
+
return dateKey(cutoff);
|
|
695
|
+
}
|
|
696
|
+
function resolveSaptoolsRoot(root) {
|
|
697
|
+
return root ?? defaultSaptoolsRoot();
|
|
698
|
+
}
|
|
699
|
+
function cfHanaHistoryDirectory(saptoolsRoot) {
|
|
700
|
+
return join(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME, HISTORIES_DIR_NAME);
|
|
701
|
+
}
|
|
702
|
+
function sqlHistoryFilePath(now = /* @__PURE__ */ new Date(), saptoolsRoot) {
|
|
703
|
+
return join(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
|
|
704
|
+
}
|
|
705
|
+
async function pruneSqlHistory(now, saptoolsRoot) {
|
|
706
|
+
const historyDir = cfHanaHistoryDirectory(saptoolsRoot);
|
|
707
|
+
let files;
|
|
708
|
+
try {
|
|
709
|
+
files = await readdir(historyDir);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (error.code === "ENOENT") {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
716
|
+
const cutoffKey = retentionCutoffKey(now);
|
|
717
|
+
await Promise.all(
|
|
718
|
+
files.filter((file) => HISTORY_FILE_PATTERN.test(file)).filter((file) => file.slice(0, 10) < cutoffKey).map(async (file) => {
|
|
719
|
+
await rm(join(historyDir, file), { force: true });
|
|
720
|
+
})
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
async function appendSqlHistory(input, options = {}) {
|
|
724
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
725
|
+
const historyDir = cfHanaHistoryDirectory(options.saptoolsRoot);
|
|
726
|
+
const entry = {
|
|
727
|
+
at: now.toISOString(),
|
|
728
|
+
...input
|
|
729
|
+
};
|
|
730
|
+
await mkdir(historyDir, { recursive: true, mode: 448 });
|
|
731
|
+
await appendFile(sqlHistoryFilePath(now, options.saptoolsRoot), `${JSON.stringify(entry)}
|
|
732
|
+
`, {
|
|
733
|
+
encoding: "utf8",
|
|
734
|
+
mode: 384
|
|
735
|
+
});
|
|
736
|
+
try {
|
|
737
|
+
await pruneSqlHistory(now, options.saptoolsRoot);
|
|
738
|
+
} catch {
|
|
739
|
+
}
|
|
740
|
+
return entry;
|
|
741
|
+
}
|
|
742
|
+
|
|
642
743
|
// src/safety.ts
|
|
643
744
|
var DESTRUCTIVE_DDL_KEYWORDS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "ALTER"]);
|
|
644
745
|
var UNSCOPED_WRITE_KEYWORDS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
|
|
645
|
-
function
|
|
646
|
-
|
|
746
|
+
function skipQuotedText(sql, start) {
|
|
747
|
+
const quote = sql[start];
|
|
748
|
+
let index = start + 1;
|
|
749
|
+
while (index < sql.length) {
|
|
750
|
+
if (sql[index] === quote) {
|
|
751
|
+
if (sql[index + 1] === quote) {
|
|
752
|
+
index += 2;
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
index += 1;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
index += 1;
|
|
759
|
+
}
|
|
760
|
+
return index;
|
|
761
|
+
}
|
|
762
|
+
function skipLineComment(sql, start) {
|
|
763
|
+
let index = start + 2;
|
|
764
|
+
while (index < sql.length && sql[index] !== "\n") {
|
|
765
|
+
index += 1;
|
|
766
|
+
}
|
|
767
|
+
return index;
|
|
768
|
+
}
|
|
769
|
+
function skipBlockComment(sql, start) {
|
|
770
|
+
let index = start + 2;
|
|
771
|
+
while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) {
|
|
772
|
+
index += 1;
|
|
773
|
+
}
|
|
774
|
+
return Math.min(index + 2, sql.length);
|
|
775
|
+
}
|
|
776
|
+
function maskIgnoredSqlText(sql) {
|
|
777
|
+
let masked = "";
|
|
778
|
+
let index = 0;
|
|
779
|
+
while (index < sql.length) {
|
|
780
|
+
const char = sql[index];
|
|
781
|
+
if (char === "'" || char === '"') {
|
|
782
|
+
const end = skipQuotedText(sql, index);
|
|
783
|
+
masked += " ".repeat(end - index);
|
|
784
|
+
index = end;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (char === "-" && sql[index + 1] === "-") {
|
|
788
|
+
const end = skipLineComment(sql, index);
|
|
789
|
+
masked += " ".repeat(end - index);
|
|
790
|
+
index = end;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (char === "/" && sql[index + 1] === "*") {
|
|
794
|
+
const end = skipBlockComment(sql, index);
|
|
795
|
+
masked += " ".repeat(end - index);
|
|
796
|
+
index = end;
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
masked += char ?? "";
|
|
800
|
+
index += 1;
|
|
801
|
+
}
|
|
802
|
+
return masked;
|
|
647
803
|
}
|
|
648
804
|
function hasWhereClause(sql) {
|
|
649
|
-
return /\bwhere\b/i.test(
|
|
805
|
+
return /\bwhere\b/i.test(maskIgnoredSqlText(sql));
|
|
806
|
+
}
|
|
807
|
+
function trailingLineCommentIndex(sql) {
|
|
808
|
+
let index = 0;
|
|
809
|
+
while (index < sql.length) {
|
|
810
|
+
const char = sql[index];
|
|
811
|
+
if (char === "'" || char === '"') {
|
|
812
|
+
index = skipQuotedText(sql, index);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
if (char === "/" && sql[index + 1] === "*") {
|
|
816
|
+
index = skipBlockComment(sql, index);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (char === "-" && sql[index + 1] === "-") {
|
|
820
|
+
const lineEnd = sql.indexOf("\n", index + 2);
|
|
821
|
+
if (lineEnd === -1 || sql.slice(lineEnd + 1).trim().length === 0) {
|
|
822
|
+
return index;
|
|
823
|
+
}
|
|
824
|
+
index = lineEnd + 1;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
index += 1;
|
|
828
|
+
}
|
|
829
|
+
return void 0;
|
|
830
|
+
}
|
|
831
|
+
function appendLimit(sql, limit) {
|
|
832
|
+
const trimmed = sql.replace(/[\s;]+$/, "");
|
|
833
|
+
const commentIndex = trailingLineCommentIndex(trimmed);
|
|
834
|
+
if (commentIndex === void 0) {
|
|
835
|
+
return `${trimmed} LIMIT ${String(limit)}`;
|
|
836
|
+
}
|
|
837
|
+
const beforeComment = trimmed.slice(0, commentIndex).replace(/[\s;]+$/, "");
|
|
838
|
+
const comment = trimmed.slice(commentIndex);
|
|
839
|
+
return `${beforeComment} LIMIT ${String(limit)} ${comment}`;
|
|
650
840
|
}
|
|
651
841
|
function inspectStatement(sql) {
|
|
652
842
|
const kind = classifyStatement(sql);
|
|
@@ -664,12 +854,12 @@ function inspectStatement(sql) {
|
|
|
664
854
|
}
|
|
665
855
|
function evaluateGuard(sql, config) {
|
|
666
856
|
const inspection = inspectStatement(sql);
|
|
667
|
-
if (config.readOnly &&
|
|
857
|
+
if (config.readOnly && inspection.kind !== "select") {
|
|
668
858
|
return {
|
|
669
859
|
allowed: false,
|
|
670
860
|
destructive: inspection.destructive,
|
|
671
861
|
violation: "read-only",
|
|
672
|
-
reason: `read-only mode blocks ${inspection.kind.toUpperCase()} statements`
|
|
862
|
+
reason: inspection.kind === "unknown" ? "read-only mode only permits SELECT/WITH statements" : `read-only mode blocks ${inspection.kind.toUpperCase()} statements`
|
|
673
863
|
};
|
|
674
864
|
}
|
|
675
865
|
if (inspection.destructive && !config.allowDestructive) {
|
|
@@ -691,12 +881,11 @@ function applyAutoLimit(sql, limit) {
|
|
|
691
881
|
if (limit === false || classifyStatement(sql) !== "select") {
|
|
692
882
|
return { sql, applied: false };
|
|
693
883
|
}
|
|
694
|
-
const stripped =
|
|
884
|
+
const stripped = maskIgnoredSqlText(sql);
|
|
695
885
|
if (/\blimit\b/i.test(stripped) || /\btop\s+\d/i.test(stripped)) {
|
|
696
886
|
return { sql, applied: false };
|
|
697
887
|
}
|
|
698
|
-
|
|
699
|
-
return { sql: `${trimmed} LIMIT ${String(limit)}`, applied: true };
|
|
888
|
+
return { sql: appendLimit(sql, limit), applied: true };
|
|
700
889
|
}
|
|
701
890
|
|
|
702
891
|
// src/connection.ts
|
|
@@ -745,6 +934,12 @@ var Connection = class _Connection {
|
|
|
745
934
|
async execute(sql, params, options) {
|
|
746
935
|
return await this.run(sql, params ?? [], options ?? {});
|
|
747
936
|
}
|
|
937
|
+
assertAllowed(sql, options) {
|
|
938
|
+
this.evaluateSafety(sql, options ?? {});
|
|
939
|
+
}
|
|
940
|
+
async executeInternal(sql, params, options) {
|
|
941
|
+
return await this.run(sql, params ?? [], options ?? {}, { bypassSafety: true });
|
|
942
|
+
}
|
|
748
943
|
async setAutoCommit(enabled) {
|
|
749
944
|
await this.driverConnection.setAutoCommit(enabled);
|
|
750
945
|
}
|
|
@@ -760,21 +955,10 @@ var Connection = class _Connection {
|
|
|
760
955
|
get isClosed() {
|
|
761
956
|
return this.driverConnection.isClosed();
|
|
762
957
|
}
|
|
763
|
-
async run(sql, params, options) {
|
|
958
|
+
async run(sql, params, options, runOptions = {}) {
|
|
764
959
|
assertParamArity(sql, params);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
allowDestructive: options.allowDestructive ?? this.config.allowDestructive
|
|
768
|
-
});
|
|
769
|
-
if (!decision.allowed) {
|
|
770
|
-
if (decision.violation === "read-only") {
|
|
771
|
-
throw new ReadOnlyViolationError(
|
|
772
|
-
decision.reason ?? "read-only mode blocks this statement"
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
throw new DestructiveStatementError(
|
|
776
|
-
decision.reason ?? "destructive statement blocked"
|
|
777
|
-
);
|
|
960
|
+
if (runOptions.bypassSafety !== true) {
|
|
961
|
+
this.evaluateSafety(sql, options);
|
|
778
962
|
}
|
|
779
963
|
const kind = classifyStatement(sql);
|
|
780
964
|
const autoLimit = options.autoLimit ?? this.config.autoLimit;
|
|
@@ -804,6 +988,23 @@ var Connection = class _Connection {
|
|
|
804
988
|
} catch {
|
|
805
989
|
}
|
|
806
990
|
}
|
|
991
|
+
evaluateSafety(sql, options) {
|
|
992
|
+
const decision = evaluateGuard(sql, {
|
|
993
|
+
readOnly: this.config.readOnly,
|
|
994
|
+
allowDestructive: options.allowDestructive ?? this.config.allowDestructive
|
|
995
|
+
});
|
|
996
|
+
if (decision.allowed) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (decision.violation === "read-only") {
|
|
1000
|
+
throw new ReadOnlyViolationError(
|
|
1001
|
+
decision.reason ?? "read-only mode blocks this statement"
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
throw new DestructiveStatementError(
|
|
1005
|
+
decision.reason ?? "destructive statement blocked"
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
807
1008
|
};
|
|
808
1009
|
|
|
809
1010
|
// src/pool.ts
|
|
@@ -944,6 +1145,7 @@ var ConnectionPool = class {
|
|
|
944
1145
|
(error) => {
|
|
945
1146
|
this.created -= 1;
|
|
946
1147
|
waiter.reject(error);
|
|
1148
|
+
this.servePendingWaiters();
|
|
947
1149
|
}
|
|
948
1150
|
);
|
|
949
1151
|
}
|
|
@@ -990,6 +1192,11 @@ var Transaction = class {
|
|
|
990
1192
|
};
|
|
991
1193
|
|
|
992
1194
|
// src/client.ts
|
|
1195
|
+
var explainStatementCounter = 0;
|
|
1196
|
+
function nextExplainStatementName() {
|
|
1197
|
+
explainStatementCounter = (explainStatementCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
1198
|
+
return `cf_hana_${String(process.pid)}_${String(Date.now())}_${String(explainStatementCounter)}`;
|
|
1199
|
+
}
|
|
993
1200
|
var HanaClient = class _HanaClient {
|
|
994
1201
|
constructor(pool, info) {
|
|
995
1202
|
this.pool = pool;
|
|
@@ -1031,41 +1238,43 @@ var HanaClient = class _HanaClient {
|
|
|
1031
1238
|
}
|
|
1032
1239
|
/** Run a SELECT (or any read) statement and return typed rows. */
|
|
1033
1240
|
async query(sql, params, options) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
);
|
|
1241
|
+
const resolvedParams = params ?? [];
|
|
1242
|
+
const result = await this.runQuery(sql, resolvedParams, options);
|
|
1243
|
+
await this.recordSqlHistory("query", sql, resolvedParams, result);
|
|
1244
|
+
return result;
|
|
1037
1245
|
}
|
|
1038
1246
|
/** Run a DML/DDL statement and return its affected-row count. */
|
|
1039
1247
|
async execute(sql, params, options) {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
);
|
|
1248
|
+
const resolvedParams = params ?? [];
|
|
1249
|
+
const result = await this.runExecute(sql, resolvedParams, options);
|
|
1250
|
+
await this.recordSqlHistory("execute", sql, resolvedParams, result);
|
|
1251
|
+
return result;
|
|
1043
1252
|
}
|
|
1044
1253
|
/** Run a typed `SELECT` built from a spec. */
|
|
1045
1254
|
async selectFrom(spec) {
|
|
1046
1255
|
const built = buildSelect(spec);
|
|
1047
|
-
return await this.
|
|
1256
|
+
return await this.runQuery(built.sql, built.params);
|
|
1048
1257
|
}
|
|
1049
1258
|
/** Count rows in a table, optionally filtered. */
|
|
1050
1259
|
async count(spec) {
|
|
1051
1260
|
const built = buildCount(spec);
|
|
1052
|
-
const result = await this.
|
|
1261
|
+
const result = await this.runQuery(built.sql, built.params);
|
|
1053
1262
|
return result.rows[0]?.COUNT ?? 0;
|
|
1054
1263
|
}
|
|
1055
1264
|
/** Insert a single row. */
|
|
1056
1265
|
async insertInto(schema, table, values) {
|
|
1057
1266
|
const built = buildInsert(schema, table, values);
|
|
1058
|
-
return await this.
|
|
1267
|
+
return await this.runExecute(built.sql, built.params);
|
|
1059
1268
|
}
|
|
1060
1269
|
/** Update rows matching a non-empty `where` filter. */
|
|
1061
1270
|
async update(schema, table, values, where) {
|
|
1062
1271
|
const built = buildUpdate(schema, table, values, where);
|
|
1063
|
-
return await this.
|
|
1272
|
+
return await this.runExecute(built.sql, built.params);
|
|
1064
1273
|
}
|
|
1065
1274
|
/** Delete rows matching a non-empty `where` filter. */
|
|
1066
1275
|
async deleteFrom(schema, table, where) {
|
|
1067
1276
|
const built = buildDelete(schema, table, where);
|
|
1068
|
-
return await this.
|
|
1277
|
+
return await this.runExecute(built.sql, built.params);
|
|
1069
1278
|
}
|
|
1070
1279
|
/** Run `work` inside a transaction, auto-committing on success. */
|
|
1071
1280
|
async transaction(work) {
|
|
@@ -1108,21 +1317,35 @@ var HanaClient = class _HanaClient {
|
|
|
1108
1317
|
/** Return the HANA execution plan for a statement. */
|
|
1109
1318
|
async explain(sql, params) {
|
|
1110
1319
|
return await this.pool.withConnection(async (connection) => {
|
|
1111
|
-
|
|
1112
|
-
|
|
1320
|
+
connection.assertAllowed(sql);
|
|
1321
|
+
const statementName = nextExplainStatementName();
|
|
1322
|
+
await connection.executeInternal(
|
|
1113
1323
|
`EXPLAIN PLAN SET STATEMENT_NAME = '${statementName}' FOR ${sql}`,
|
|
1114
1324
|
params
|
|
1115
1325
|
);
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
"
|
|
1326
|
+
const cleanup = async () => {
|
|
1327
|
+
await connection.executeInternal(
|
|
1328
|
+
"DELETE FROM EXPLAIN_PLAN_TABLE WHERE STATEMENT_NAME = ?",
|
|
1119
1329
|
[statementName]
|
|
1120
1330
|
);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1331
|
+
};
|
|
1332
|
+
let queryCompleted = false;
|
|
1333
|
+
try {
|
|
1334
|
+
const result = await connection.query(
|
|
1335
|
+
"SELECT OPERATOR_NAME, TABLE_NAME, TABLE_TYPE, EXECUTION_ENGINE FROM EXPLAIN_PLAN_TABLE WHERE STATEMENT_NAME = ? ORDER BY OPERATOR_ID",
|
|
1124
1336
|
[statementName]
|
|
1125
1337
|
);
|
|
1338
|
+
queryCompleted = true;
|
|
1339
|
+
await cleanup();
|
|
1340
|
+
return result;
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
if (!queryCompleted) {
|
|
1343
|
+
try {
|
|
1344
|
+
await cleanup();
|
|
1345
|
+
} catch {
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
throw error;
|
|
1126
1349
|
}
|
|
1127
1350
|
});
|
|
1128
1351
|
}
|
|
@@ -1130,6 +1353,35 @@ var HanaClient = class _HanaClient {
|
|
|
1130
1353
|
async close() {
|
|
1131
1354
|
await this.pool.drain();
|
|
1132
1355
|
}
|
|
1356
|
+
async runQuery(sql, params, options) {
|
|
1357
|
+
return await this.pool.withConnection(
|
|
1358
|
+
(connection) => connection.query(sql, params, options)
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
async runExecute(sql, params, options) {
|
|
1362
|
+
return await this.pool.withConnection(
|
|
1363
|
+
(connection) => connection.execute(sql, params, options)
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
async recordSqlHistory(operation, sql, params, result) {
|
|
1367
|
+
try {
|
|
1368
|
+
await appendSqlHistory({
|
|
1369
|
+
version: CLI_VERSION,
|
|
1370
|
+
operation,
|
|
1371
|
+
selector: this.info.selector,
|
|
1372
|
+
appName: this.info.appName,
|
|
1373
|
+
schema: this.info.schema,
|
|
1374
|
+
role: this.info.role,
|
|
1375
|
+
statement: result.statement,
|
|
1376
|
+
sql,
|
|
1377
|
+
paramCount: params.length,
|
|
1378
|
+
rowCount: result.rowCount,
|
|
1379
|
+
truncated: result.truncated,
|
|
1380
|
+
elapsedMs: result.elapsedMs
|
|
1381
|
+
});
|
|
1382
|
+
} catch {
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1133
1385
|
};
|
|
1134
1386
|
|
|
1135
1387
|
// src/api.ts
|