@saptools/cf-hana 0.1.0 → 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/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.drop();
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
- await setCurrentSchema(client, params.schema);
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 stripStringLiterals(sql) {
646
- return sql.replace(/'(?:[^']|'')*'/g, "''");
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(stripStringLiterals(sql));
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 && (inspection.kind === "dml" || inspection.kind === "ddl")) {
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 = stripStringLiterals(sql);
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
- const trimmed = sql.replace(/[\s;]+$/, "");
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
- const decision = evaluateGuard(sql, {
766
- readOnly: this.config.readOnly,
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
- const statementName = `cf_hana_${String(Date.now())}`;
1112
- await connection.execute(
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
- try {
1117
- return await connection.query(
1118
- "SELECT OPERATOR_NAME, TABLE_NAME, TABLE_TYPE, EXECUTION_ENGINE FROM EXPLAIN_PLAN_TABLE WHERE STATEMENT_NAME = ? ORDER BY OPERATOR_ID",
1248
+ const cleanup = async () => {
1249
+ await connection.executeInternal(
1250
+ "DELETE FROM EXPLAIN_PLAN_TABLE WHERE STATEMENT_NAME = ?",
1119
1251
  [statementName]
1120
1252
  );
1121
- } finally {
1122
- await connection.execute(
1123
- "DELETE FROM EXPLAIN_PLAN_TABLE WHERE STATEMENT_NAME = ?",
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
  }