@saptools/cf-hana 0.1.1 → 0.1.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/CHANGELOG.md +8 -0
- package/README.md +8 -8
- package/dist/cli.js +197 -35
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +177 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -493,7 +493,7 @@ var HdbConnection = class {
|
|
|
493
493
|
const raw = await this.executeStatement(statement, params);
|
|
494
494
|
return toExecResult(statement, raw);
|
|
495
495
|
} finally {
|
|
496
|
-
statement
|
|
496
|
+
dropStatementQuietly(statement);
|
|
497
497
|
}
|
|
498
498
|
}
|
|
499
499
|
setAutoCommit(enabled) {
|
|
@@ -560,6 +560,12 @@ var HdbConnection = class {
|
|
|
560
560
|
});
|
|
561
561
|
}
|
|
562
562
|
};
|
|
563
|
+
function dropStatementQuietly(statement) {
|
|
564
|
+
try {
|
|
565
|
+
statement.drop();
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
}
|
|
563
569
|
function openClient(client, timeoutMs) {
|
|
564
570
|
return new Promise((resolve, reject) => {
|
|
565
571
|
let settled = false;
|
|
@@ -595,6 +601,20 @@ function openClient(client, timeoutMs) {
|
|
|
595
601
|
});
|
|
596
602
|
});
|
|
597
603
|
}
|
|
604
|
+
async function closeClientQuietly(client) {
|
|
605
|
+
try {
|
|
606
|
+
await new Promise((resolve) => {
|
|
607
|
+
client.disconnect(() => {
|
|
608
|
+
resolve();
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
client.close();
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
598
618
|
function setCurrentSchema(client, schema) {
|
|
599
619
|
return new Promise((resolve, reject) => {
|
|
600
620
|
client.exec(`SET SCHEMA ${quoteIdentifier(schema)}`, (error) => {
|
|
@@ -616,7 +636,12 @@ async function connectHdb(params) {
|
|
|
616
636
|
useTLS: true
|
|
617
637
|
});
|
|
618
638
|
await openClient(client, params.connectTimeoutMs);
|
|
619
|
-
|
|
639
|
+
try {
|
|
640
|
+
await setCurrentSchema(client, params.schema);
|
|
641
|
+
} catch (error) {
|
|
642
|
+
await closeClientQuietly(client);
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
620
645
|
return new HdbConnection(client);
|
|
621
646
|
}
|
|
622
647
|
function createHdbDriver() {
|
|
@@ -642,11 +667,100 @@ function createDriver(name) {
|
|
|
642
667
|
// src/safety.ts
|
|
643
668
|
var DESTRUCTIVE_DDL_KEYWORDS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "ALTER"]);
|
|
644
669
|
var UNSCOPED_WRITE_KEYWORDS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
|
|
645
|
-
function
|
|
646
|
-
|
|
670
|
+
function skipQuotedText(sql, start) {
|
|
671
|
+
const quote = sql[start];
|
|
672
|
+
let index = start + 1;
|
|
673
|
+
while (index < sql.length) {
|
|
674
|
+
if (sql[index] === quote) {
|
|
675
|
+
if (sql[index + 1] === quote) {
|
|
676
|
+
index += 2;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
index += 1;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
index += 1;
|
|
683
|
+
}
|
|
684
|
+
return index;
|
|
685
|
+
}
|
|
686
|
+
function skipLineComment(sql, start) {
|
|
687
|
+
let index = start + 2;
|
|
688
|
+
while (index < sql.length && sql[index] !== "\n") {
|
|
689
|
+
index += 1;
|
|
690
|
+
}
|
|
691
|
+
return index;
|
|
692
|
+
}
|
|
693
|
+
function skipBlockComment(sql, start) {
|
|
694
|
+
let index = start + 2;
|
|
695
|
+
while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) {
|
|
696
|
+
index += 1;
|
|
697
|
+
}
|
|
698
|
+
return Math.min(index + 2, sql.length);
|
|
699
|
+
}
|
|
700
|
+
function maskIgnoredSqlText(sql) {
|
|
701
|
+
let masked = "";
|
|
702
|
+
let index = 0;
|
|
703
|
+
while (index < sql.length) {
|
|
704
|
+
const char = sql[index];
|
|
705
|
+
if (char === "'" || char === '"') {
|
|
706
|
+
const end = skipQuotedText(sql, index);
|
|
707
|
+
masked += " ".repeat(end - index);
|
|
708
|
+
index = end;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (char === "-" && sql[index + 1] === "-") {
|
|
712
|
+
const end = skipLineComment(sql, index);
|
|
713
|
+
masked += " ".repeat(end - index);
|
|
714
|
+
index = end;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (char === "/" && sql[index + 1] === "*") {
|
|
718
|
+
const end = skipBlockComment(sql, index);
|
|
719
|
+
masked += " ".repeat(end - index);
|
|
720
|
+
index = end;
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
masked += char ?? "";
|
|
724
|
+
index += 1;
|
|
725
|
+
}
|
|
726
|
+
return masked;
|
|
647
727
|
}
|
|
648
728
|
function hasWhereClause(sql) {
|
|
649
|
-
return /\bwhere\b/i.test(
|
|
729
|
+
return /\bwhere\b/i.test(maskIgnoredSqlText(sql));
|
|
730
|
+
}
|
|
731
|
+
function trailingLineCommentIndex(sql) {
|
|
732
|
+
let index = 0;
|
|
733
|
+
while (index < sql.length) {
|
|
734
|
+
const char = sql[index];
|
|
735
|
+
if (char === "'" || char === '"') {
|
|
736
|
+
index = skipQuotedText(sql, index);
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (char === "/" && sql[index + 1] === "*") {
|
|
740
|
+
index = skipBlockComment(sql, index);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (char === "-" && sql[index + 1] === "-") {
|
|
744
|
+
const lineEnd = sql.indexOf("\n", index + 2);
|
|
745
|
+
if (lineEnd === -1 || sql.slice(lineEnd + 1).trim().length === 0) {
|
|
746
|
+
return index;
|
|
747
|
+
}
|
|
748
|
+
index = lineEnd + 1;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
index += 1;
|
|
752
|
+
}
|
|
753
|
+
return void 0;
|
|
754
|
+
}
|
|
755
|
+
function appendLimit(sql, limit) {
|
|
756
|
+
const trimmed = sql.replace(/[\s;]+$/, "");
|
|
757
|
+
const commentIndex = trailingLineCommentIndex(trimmed);
|
|
758
|
+
if (commentIndex === void 0) {
|
|
759
|
+
return `${trimmed} LIMIT ${String(limit)}`;
|
|
760
|
+
}
|
|
761
|
+
const beforeComment = trimmed.slice(0, commentIndex).replace(/[\s;]+$/, "");
|
|
762
|
+
const comment = trimmed.slice(commentIndex);
|
|
763
|
+
return `${beforeComment} LIMIT ${String(limit)} ${comment}`;
|
|
650
764
|
}
|
|
651
765
|
function inspectStatement(sql) {
|
|
652
766
|
const kind = classifyStatement(sql);
|
|
@@ -664,12 +778,12 @@ function inspectStatement(sql) {
|
|
|
664
778
|
}
|
|
665
779
|
function evaluateGuard(sql, config) {
|
|
666
780
|
const inspection = inspectStatement(sql);
|
|
667
|
-
if (config.readOnly &&
|
|
781
|
+
if (config.readOnly && inspection.kind !== "select") {
|
|
668
782
|
return {
|
|
669
783
|
allowed: false,
|
|
670
784
|
destructive: inspection.destructive,
|
|
671
785
|
violation: "read-only",
|
|
672
|
-
reason: `read-only mode blocks ${inspection.kind.toUpperCase()} statements`
|
|
786
|
+
reason: inspection.kind === "unknown" ? "read-only mode only permits SELECT/WITH statements" : `read-only mode blocks ${inspection.kind.toUpperCase()} statements`
|
|
673
787
|
};
|
|
674
788
|
}
|
|
675
789
|
if (inspection.destructive && !config.allowDestructive) {
|
|
@@ -691,12 +805,11 @@ function applyAutoLimit(sql, limit) {
|
|
|
691
805
|
if (limit === false || classifyStatement(sql) !== "select") {
|
|
692
806
|
return { sql, applied: false };
|
|
693
807
|
}
|
|
694
|
-
const stripped =
|
|
808
|
+
const stripped = maskIgnoredSqlText(sql);
|
|
695
809
|
if (/\blimit\b/i.test(stripped) || /\btop\s+\d/i.test(stripped)) {
|
|
696
810
|
return { sql, applied: false };
|
|
697
811
|
}
|
|
698
|
-
|
|
699
|
-
return { sql: `${trimmed} LIMIT ${String(limit)}`, applied: true };
|
|
812
|
+
return { sql: appendLimit(sql, limit), applied: true };
|
|
700
813
|
}
|
|
701
814
|
|
|
702
815
|
// src/connection.ts
|
|
@@ -745,6 +858,12 @@ var Connection = class _Connection {
|
|
|
745
858
|
async execute(sql, params, options) {
|
|
746
859
|
return await this.run(sql, params ?? [], options ?? {});
|
|
747
860
|
}
|
|
861
|
+
assertAllowed(sql, options) {
|
|
862
|
+
this.evaluateSafety(sql, options ?? {});
|
|
863
|
+
}
|
|
864
|
+
async executeInternal(sql, params, options) {
|
|
865
|
+
return await this.run(sql, params ?? [], options ?? {}, { bypassSafety: true });
|
|
866
|
+
}
|
|
748
867
|
async setAutoCommit(enabled) {
|
|
749
868
|
await this.driverConnection.setAutoCommit(enabled);
|
|
750
869
|
}
|
|
@@ -760,21 +879,10 @@ var Connection = class _Connection {
|
|
|
760
879
|
get isClosed() {
|
|
761
880
|
return this.driverConnection.isClosed();
|
|
762
881
|
}
|
|
763
|
-
async run(sql, params, options) {
|
|
882
|
+
async run(sql, params, options, runOptions = {}) {
|
|
764
883
|
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
|
-
);
|
|
884
|
+
if (runOptions.bypassSafety !== true) {
|
|
885
|
+
this.evaluateSafety(sql, options);
|
|
778
886
|
}
|
|
779
887
|
const kind = classifyStatement(sql);
|
|
780
888
|
const autoLimit = options.autoLimit ?? this.config.autoLimit;
|
|
@@ -804,6 +912,23 @@ var Connection = class _Connection {
|
|
|
804
912
|
} catch {
|
|
805
913
|
}
|
|
806
914
|
}
|
|
915
|
+
evaluateSafety(sql, options) {
|
|
916
|
+
const decision = evaluateGuard(sql, {
|
|
917
|
+
readOnly: this.config.readOnly,
|
|
918
|
+
allowDestructive: options.allowDestructive ?? this.config.allowDestructive
|
|
919
|
+
});
|
|
920
|
+
if (decision.allowed) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (decision.violation === "read-only") {
|
|
924
|
+
throw new ReadOnlyViolationError(
|
|
925
|
+
decision.reason ?? "read-only mode blocks this statement"
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
throw new DestructiveStatementError(
|
|
929
|
+
decision.reason ?? "destructive statement blocked"
|
|
930
|
+
);
|
|
931
|
+
}
|
|
807
932
|
};
|
|
808
933
|
|
|
809
934
|
// src/pool.ts
|
|
@@ -944,6 +1069,7 @@ var ConnectionPool = class {
|
|
|
944
1069
|
(error) => {
|
|
945
1070
|
this.created -= 1;
|
|
946
1071
|
waiter.reject(error);
|
|
1072
|
+
this.servePendingWaiters();
|
|
947
1073
|
}
|
|
948
1074
|
);
|
|
949
1075
|
}
|
|
@@ -990,6 +1116,11 @@ var Transaction = class {
|
|
|
990
1116
|
};
|
|
991
1117
|
|
|
992
1118
|
// src/client.ts
|
|
1119
|
+
var explainStatementCounter = 0;
|
|
1120
|
+
function nextExplainStatementName() {
|
|
1121
|
+
explainStatementCounter = (explainStatementCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
1122
|
+
return `cf_hana_${String(process.pid)}_${String(Date.now())}_${String(explainStatementCounter)}`;
|
|
1123
|
+
}
|
|
993
1124
|
var HanaClient = class _HanaClient {
|
|
994
1125
|
constructor(pool, info) {
|
|
995
1126
|
this.pool = pool;
|
|
@@ -1108,21 +1239,35 @@ var HanaClient = class _HanaClient {
|
|
|
1108
1239
|
/** Return the HANA execution plan for a statement. */
|
|
1109
1240
|
async explain(sql, params) {
|
|
1110
1241
|
return await this.pool.withConnection(async (connection) => {
|
|
1111
|
-
|
|
1112
|
-
|
|
1242
|
+
connection.assertAllowed(sql);
|
|
1243
|
+
const statementName = nextExplainStatementName();
|
|
1244
|
+
await connection.executeInternal(
|
|
1113
1245
|
`EXPLAIN PLAN SET STATEMENT_NAME = '${statementName}' FOR ${sql}`,
|
|
1114
1246
|
params
|
|
1115
1247
|
);
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
"
|
|
1248
|
+
const cleanup = async () => {
|
|
1249
|
+
await connection.executeInternal(
|
|
1250
|
+
"DELETE FROM EXPLAIN_PLAN_TABLE WHERE STATEMENT_NAME = ?",
|
|
1119
1251
|
[statementName]
|
|
1120
1252
|
);
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1253
|
+
};
|
|
1254
|
+
let queryCompleted = false;
|
|
1255
|
+
try {
|
|
1256
|
+
const result = await connection.query(
|
|
1257
|
+
"SELECT OPERATOR_NAME, TABLE_NAME, TABLE_TYPE, EXECUTION_ENGINE FROM EXPLAIN_PLAN_TABLE WHERE STATEMENT_NAME = ? ORDER BY OPERATOR_ID",
|
|
1124
1258
|
[statementName]
|
|
1125
1259
|
);
|
|
1260
|
+
queryCompleted = true;
|
|
1261
|
+
await cleanup();
|
|
1262
|
+
return result;
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (!queryCompleted) {
|
|
1265
|
+
try {
|
|
1266
|
+
await cleanup();
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
throw error;
|
|
1126
1271
|
}
|
|
1127
1272
|
});
|
|
1128
1273
|
}
|