@saptools/cf-hana 0.3.4 → 0.4.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/dist/index.js CHANGED
@@ -5,6 +5,44 @@ import { mkdir, writeFile } from "fs/promises";
5
5
  import { homedir } from "os";
6
6
  import { join } from "path";
7
7
 
8
+ // src/config.ts
9
+ var CLI_VERSION = "0.4.0";
10
+ var ENV_PREFIX = "CF_HANA";
11
+ var DEFAULT_QUERY_TIMEOUT_MS = 6e4;
12
+ var DEFAULT_CONNECT_TIMEOUT_MS = 6e4;
13
+ var DEFAULT_POOL_MAX = 4;
14
+ var DEFAULT_POOL_IDLE_MS = 6e4;
15
+ var DEFAULT_AUTO_LIMIT = 100;
16
+ var DEFAULT_MAX_RESULT_STORE_BYTES = 256 * 1024 * 1024;
17
+ var MAX_RESULT_STORE_BYTES = resolveMaxResultStoreBytes();
18
+ function resolveMaxResultStoreBytes() {
19
+ if (readEnv(envName("DRIVER")) !== "fake") {
20
+ return DEFAULT_MAX_RESULT_STORE_BYTES;
21
+ }
22
+ const raw = readEnv(envName("FAKE_MAX_STORE_BYTES"));
23
+ const parsed = raw === void 0 ? Number.NaN : Number(raw);
24
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_RESULT_STORE_BYTES;
25
+ }
26
+ function envName(suffix) {
27
+ return `${ENV_PREFIX}_${suffix}`;
28
+ }
29
+ function readEnv(name) {
30
+ const value = process.env[name];
31
+ if (value === void 0) {
32
+ return void 0;
33
+ }
34
+ const trimmed = value.trim();
35
+ return trimmed.length === 0 ? void 0 : trimmed;
36
+ }
37
+ function readSapCredentials(overrides) {
38
+ const email = overrides?.email ?? readEnv("SAP_EMAIL");
39
+ const password = overrides?.password ?? readEnv("SAP_PASSWORD");
40
+ if (email === void 0 || password === void 0) {
41
+ return void 0;
42
+ }
43
+ return { email, password };
44
+ }
45
+
8
46
  // src/errors.ts
9
47
  var CfHanaError = class extends Error {
10
48
  code;
@@ -31,6 +69,12 @@ var QueryError = class extends CfHanaError {
31
69
  this.databaseCode = options?.databaseCode;
32
70
  }
33
71
  };
72
+ var BackupRequiredError = class extends CfHanaError {
73
+ constructor(message, options) {
74
+ super("BACKUP_REQUIRED", message, options);
75
+ this.name = "BackupRequiredError";
76
+ }
77
+ };
34
78
  function isDatabaseErrorShape(error) {
35
79
  return typeof error === "object" && error !== null && "code" in error;
36
80
  }
@@ -146,6 +190,16 @@ function formatJson(result) {
146
190
  });
147
191
  return JSON.stringify(rows, null, 2);
148
192
  }
193
+ function formatJsonCompact(result, preferredColumn) {
194
+ const singleColumn = result.columns.length === 1 ? result.columns[0]?.name : void 0;
195
+ const columnName = preferredColumn ?? singleColumn;
196
+ if (columnName === void 0) {
197
+ return formatJson(result);
198
+ }
199
+ const column = result.columns.find((item) => item.name === columnName);
200
+ const values = result.rows.map((row) => serializeCell(row[columnName] ?? null, column));
201
+ return JSON.stringify(values, null, 2);
202
+ }
149
203
  function formatCsv(result) {
150
204
  const headers = result.columns.map((column) => column.name);
151
205
  const lines = [headers.map((header) => csvEscape(header)).join(",")];
@@ -156,115 +210,20 @@ function formatCsv(result) {
156
210
  }
157
211
  return lines.join("\r\n");
158
212
  }
159
- function formatResult(result, format) {
213
+ function formatResult(result, format, compactColumn) {
160
214
  switch (format) {
161
215
  case "table":
162
216
  return formatTable(result);
163
217
  case "json":
164
218
  return formatJson(result);
219
+ case "json-compact":
220
+ return formatJsonCompact(result, compactColumn);
165
221
  case "csv":
166
222
  return formatCsv(result);
167
223
  }
168
224
  }
169
225
 
170
- // src/statements.ts
171
- var LEADING_NOISE = /^(?:\s|--[^\n]*\n?|\/\*[\s\S]*?\*\/)+/;
172
- var SELECT_KEYWORDS = /* @__PURE__ */ new Set(["SELECT", "WITH"]);
173
- var DML_KEYWORDS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE", "MERGE", "UPSERT", "REPLACE"]);
174
- var DDL_KEYWORDS = /* @__PURE__ */ new Set(["CREATE", "DROP", "ALTER", "TRUNCATE", "RENAME", "COMMENT"]);
175
- function firstKeyword(sql) {
176
- const stripped = sql.replace(LEADING_NOISE, "");
177
- const match = /^[A-Za-z]+/.exec(stripped);
178
- return (match?.[0] ?? "").toUpperCase();
179
- }
180
- function classifyStatement(sql) {
181
- const keyword = firstKeyword(sql);
182
- if (SELECT_KEYWORDS.has(keyword)) {
183
- return "select";
184
- }
185
- if (DML_KEYWORDS.has(keyword)) {
186
- return "dml";
187
- }
188
- if (DDL_KEYWORDS.has(keyword)) {
189
- return "ddl";
190
- }
191
- return "unknown";
192
- }
193
- function quoteIdentifier(identifier) {
194
- if (identifier.length === 0) {
195
- throw new QueryError("A SQL identifier must not be empty");
196
- }
197
- if (identifier.includes("\0")) {
198
- throw new QueryError("A SQL identifier must not contain a NUL character");
199
- }
200
- return `"${identifier.replace(/"/g, '""')}"`;
201
- }
202
- function qualifiedName(schema, table) {
203
- return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
204
- }
205
- function countPlaceholders(sql) {
206
- let count = 0;
207
- let index = 0;
208
- const length = sql.length;
209
- while (index < length) {
210
- const char = sql[index];
211
- if (char === "'" || char === '"') {
212
- const quote = char;
213
- index += 1;
214
- while (index < length) {
215
- if (sql[index] === quote) {
216
- if (sql[index + 1] === quote) {
217
- index += 2;
218
- continue;
219
- }
220
- index += 1;
221
- break;
222
- }
223
- index += 1;
224
- }
225
- continue;
226
- }
227
- if (char === "-" && sql[index + 1] === "-") {
228
- index += 2;
229
- while (index < length && sql[index] !== "\n") {
230
- index += 1;
231
- }
232
- continue;
233
- }
234
- if (char === "/" && sql[index + 1] === "*") {
235
- index += 2;
236
- while (index < length && !(sql[index] === "*" && sql[index + 1] === "/")) {
237
- index += 1;
238
- }
239
- index += 2;
240
- continue;
241
- }
242
- if (char === "?") {
243
- count += 1;
244
- }
245
- index += 1;
246
- }
247
- return count;
248
- }
249
- function assertParamArity(sql, params) {
250
- const expected = countPlaceholders(sql);
251
- if (expected !== params.length) {
252
- throw new QueryError(
253
- `SQL expects ${String(expected)} bound parameter(s) but received ${String(params.length)} value(s)`
254
- );
255
- }
256
- }
257
-
258
- // src/backup.ts
259
- var SAPTOOLS_DIR_NAME = ".saptools";
260
- var CF_HANA_DIR_NAME = "cf-hana";
261
- var BACKUPS_DIR_NAME = "backups";
262
- function defaultSaptoolsRoot() {
263
- return join(homedir(), SAPTOOLS_DIR_NAME);
264
- }
265
- function trimStatementSql(sql) {
266
- return sql.trim().replace(/;+\s*$/, "").trim();
267
- }
226
+ // src/003-backup-sql-parser.ts
268
227
  function isIdentifierChar(char) {
269
228
  return char !== void 0 && /[A-Za-z0-9_$#]/.test(char);
270
229
  }
@@ -310,6 +269,105 @@ function skipNonCode(sql, index) {
310
269
  }
311
270
  return void 0;
312
271
  }
272
+ function skipTrivia(sql, start, end) {
273
+ let index = start;
274
+ while (index < end) {
275
+ if (/\s/.test(sql.charAt(index))) {
276
+ index += 1;
277
+ continue;
278
+ }
279
+ if (sql[index] === "-" && sql[index + 1] === "-") {
280
+ index = skipLineComment(sql, index);
281
+ continue;
282
+ }
283
+ if (sql[index] === "/" && sql[index + 1] === "*") {
284
+ index = skipBlockComment(sql, index);
285
+ continue;
286
+ }
287
+ break;
288
+ }
289
+ return index;
290
+ }
291
+ function readIdentifierPart(sql, start, end) {
292
+ const index = skipTrivia(sql, start, end);
293
+ if (sql[index] === '"') {
294
+ const next2 = skipQuotedText(sql, index);
295
+ return next2 <= end && sql[next2 - 1] === '"' ? { sql: sql.slice(index, next2), end: next2 } : void 0;
296
+ }
297
+ if (!/[A-Za-z_#$]/.test(sql.charAt(index))) {
298
+ return void 0;
299
+ }
300
+ let next = index + 1;
301
+ while (next < end && /[A-Za-z0-9_#$]/.test(sql.charAt(next))) {
302
+ next += 1;
303
+ }
304
+ return { sql: sql.slice(index, next), end: next };
305
+ }
306
+ function readQualifiedTarget(sql, start, end) {
307
+ const first = readIdentifierPart(sql, start, end);
308
+ if (first === void 0) {
309
+ return void 0;
310
+ }
311
+ const parts = [first.sql];
312
+ let cursor = first.end;
313
+ while (parts.length < 3) {
314
+ const dot = skipTrivia(sql, cursor, end);
315
+ if (sql[dot] !== ".") {
316
+ break;
317
+ }
318
+ const part = readIdentifierPart(sql, dot + 1, end);
319
+ if (part === void 0) {
320
+ return void 0;
321
+ }
322
+ parts.push(part.sql);
323
+ cursor = part.end;
324
+ }
325
+ return { sql: parts.join("."), reference: parts.at(-1) ?? first.sql, end: cursor };
326
+ }
327
+ function topLevelTokens(sql) {
328
+ const tokens = [];
329
+ let index = 0;
330
+ let depth = 0;
331
+ while (index < sql.length) {
332
+ const skipped = skipNonCode(sql, index);
333
+ if (skipped !== void 0) {
334
+ index = skipped;
335
+ continue;
336
+ }
337
+ const char = sql.charAt(index);
338
+ if (char === "(") {
339
+ depth += 1;
340
+ index += 1;
341
+ continue;
342
+ }
343
+ if (char === ")") {
344
+ depth = Math.max(0, depth - 1);
345
+ index += 1;
346
+ continue;
347
+ }
348
+ if (!/[A-Za-z_#$]/.test(char)) {
349
+ index += 1;
350
+ continue;
351
+ }
352
+ let end = index + 1;
353
+ while (end < sql.length && /[A-Za-z0-9_#$]/.test(sql.charAt(end))) {
354
+ end += 1;
355
+ }
356
+ if (depth === 0) {
357
+ tokens.push({ keyword: sql.slice(index, end).toUpperCase(), start: index, end });
358
+ }
359
+ index = end;
360
+ }
361
+ return tokens;
362
+ }
363
+ function findToken(tokens, keyword, start = 0) {
364
+ for (let index = start; index < tokens.length; index += 1) {
365
+ if (tokens[index]?.keyword === keyword) {
366
+ return index;
367
+ }
368
+ }
369
+ return void 0;
370
+ }
313
371
  function keywordMatches(sql, index, keyword) {
314
372
  const end = index + keyword.length;
315
373
  return sql.slice(index, end).toUpperCase() === keyword && !isIdentifierChar(sql[index - 1]) && !isIdentifierChar(sql[end]);
@@ -357,6 +415,158 @@ function findTopLevelChar(sql, target, startIndex, endIndex) {
357
415
  }
358
416
  return void 0;
359
417
  }
418
+
419
+ // src/statements.ts
420
+ var SELECT_KEYWORDS = /* @__PURE__ */ new Set(["SELECT", "WITH"]);
421
+ var DML_KEYWORDS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE", "MERGE", "UPSERT", "REPLACE"]);
422
+ var DDL_KEYWORDS = /* @__PURE__ */ new Set(["CREATE", "DROP", "ALTER", "TRUNCATE", "RENAME", "COMMENT"]);
423
+ function firstKeyword(sql) {
424
+ let index = 0;
425
+ while (index < sql.length) {
426
+ const char = sql.charAt(index);
427
+ if (char.trim().length === 0) {
428
+ index += 1;
429
+ continue;
430
+ }
431
+ if (char === "-" && sql[index + 1] === "-") {
432
+ index += 2;
433
+ while (index < sql.length && sql[index] !== "\n") {
434
+ index += 1;
435
+ }
436
+ continue;
437
+ }
438
+ if (char === "/" && sql[index + 1] === "*") {
439
+ const commentEnd = sql.indexOf("*/", index + 2);
440
+ if (commentEnd === -1) {
441
+ return "";
442
+ }
443
+ index = commentEnd + 2;
444
+ continue;
445
+ }
446
+ break;
447
+ }
448
+ let keywordEnd = index;
449
+ while (keywordEnd < sql.length) {
450
+ const code = sql.charCodeAt(keywordEnd);
451
+ const isUpperCase = code >= 65 && code <= 90;
452
+ const isLowerCase = code >= 97 && code <= 122;
453
+ if (!isUpperCase && !isLowerCase) {
454
+ break;
455
+ }
456
+ keywordEnd += 1;
457
+ }
458
+ return sql.slice(index, keywordEnd).toUpperCase();
459
+ }
460
+ function classifyStatement(sql) {
461
+ const keyword = firstKeyword(sql);
462
+ if (SELECT_KEYWORDS.has(keyword)) {
463
+ return "select";
464
+ }
465
+ if (DML_KEYWORDS.has(keyword)) {
466
+ return "dml";
467
+ }
468
+ if (DDL_KEYWORDS.has(keyword)) {
469
+ return "ddl";
470
+ }
471
+ return "unknown";
472
+ }
473
+ function quoteIdentifier(identifier) {
474
+ if (identifier.length === 0) {
475
+ throw new QueryError("A SQL identifier must not be empty");
476
+ }
477
+ if (identifier.includes("\0")) {
478
+ throw new QueryError("A SQL identifier must not contain a NUL character");
479
+ }
480
+ return `"${identifier.replace(/"/g, '""')}"`;
481
+ }
482
+ function qualifiedName(schema, table) {
483
+ return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
484
+ }
485
+ function countPlaceholders(sql) {
486
+ let count = 0;
487
+ let index = 0;
488
+ const length = sql.length;
489
+ while (index < length) {
490
+ const char = sql[index];
491
+ if (char === "'" || char === '"') {
492
+ const quote = char;
493
+ index += 1;
494
+ while (index < length) {
495
+ if (sql[index] === quote) {
496
+ if (sql[index + 1] === quote) {
497
+ index += 2;
498
+ continue;
499
+ }
500
+ index += 1;
501
+ break;
502
+ }
503
+ index += 1;
504
+ }
505
+ continue;
506
+ }
507
+ if (char === "-" && sql[index + 1] === "-") {
508
+ index += 2;
509
+ while (index < length && sql[index] !== "\n") {
510
+ index += 1;
511
+ }
512
+ continue;
513
+ }
514
+ if (char === "/" && sql[index + 1] === "*") {
515
+ index += 2;
516
+ while (index < length && !(sql[index] === "*" && sql[index + 1] === "/")) {
517
+ index += 1;
518
+ }
519
+ index += 2;
520
+ continue;
521
+ }
522
+ if (char === "?") {
523
+ count += 1;
524
+ }
525
+ index += 1;
526
+ }
527
+ return count;
528
+ }
529
+ function assertParamArity(sql, params) {
530
+ const expected = countPlaceholders(sql);
531
+ if (expected !== params.length) {
532
+ throw new QueryError(
533
+ `SQL expects ${String(expected)} bound parameter(s) but received ${String(params.length)} value(s)`
534
+ );
535
+ }
536
+ }
537
+
538
+ // src/001-backup-planner.ts
539
+ var WRITE_KEYWORDS = /* @__PURE__ */ new Set([
540
+ "UPDATE",
541
+ "UPSERT",
542
+ "REPLACE",
543
+ "MERGE",
544
+ "DELETE"
545
+ ]);
546
+ var MERGE_MODIFY_ACTIONS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
547
+ function trimStatementSql(sql) {
548
+ const trimmed = sql.trim();
549
+ let end = trimmed.length;
550
+ while (end > 0 && trimmed.charAt(end - 1) === ";") {
551
+ end -= 1;
552
+ }
553
+ return trimmed.slice(0, end).trim();
554
+ }
555
+ function isMergeClauseStart(tokens, index) {
556
+ if (tokens[index]?.keyword !== "WHEN") {
557
+ return false;
558
+ }
559
+ return tokens[index + 1]?.keyword === "MATCHED" || tokens[index + 1]?.keyword === "NOT" && tokens[index + 2]?.keyword === "MATCHED";
560
+ }
561
+ function mergeClauseIndexes(tokens, start) {
562
+ const indexes = [];
563
+ for (let index = start; index < tokens.length; index += 1) {
564
+ if (isMergeClauseStart(tokens, index)) {
565
+ indexes.push(index);
566
+ }
567
+ }
568
+ return indexes;
569
+ }
360
570
  function selectParamsAfterWhere(statementSql, whereIndex, params) {
361
571
  return params.slice(countPlaceholders(statementSql.slice(0, whereIndex)));
362
572
  }
@@ -394,23 +604,178 @@ function buildUpdateBackupPlan(statementSql, params) {
394
604
  params
395
605
  );
396
606
  }
397
- function buildUpsertBackupPlan(statementSql, params) {
398
- const upsertIndex = findTopLevelKeyword(statementSql, "UPSERT");
399
- const valuesIndex = upsertIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "VALUES", upsertIndex + "UPSERT".length);
400
- if (upsertIndex === void 0 || valuesIndex === void 0) {
401
- throw new QueryError("UPSERT backup requires UPSERT <target> VALUES syntax");
607
+ function buildUpsertBackupPlan(statementSql, params, keyword) {
608
+ const operation = keyword === "UPSERT" ? "upsert" : "replace";
609
+ const keywordIndex = findTopLevelKeyword(statementSql, keyword);
610
+ const target = keywordIndex === void 0 ? void 0 : readQualifiedTarget(statementSql, keywordIndex + keyword.length, statementSql.length);
611
+ if (keywordIndex === void 0 || target === void 0) {
612
+ throw new BackupRequiredError(
613
+ `${keyword} write refused: cannot derive a trustworthy backup target`
614
+ );
615
+ }
616
+ const valuesIndex = findTopLevelKeyword(statementSql, "VALUES", target.end);
617
+ if (valuesIndex === void 0) {
618
+ const selectIndex = findTopLevelKeyword(statementSql, "SELECT", target.end);
619
+ const withIndex = findTopLevelKeyword(statementSql, "WITH", target.end);
620
+ if (selectIndex === void 0 && withIndex === void 0) {
621
+ throw new BackupRequiredError(
622
+ `${keyword} write refused: expected VALUES, WITH PRIMARY KEY, or a subquery`
623
+ );
624
+ }
625
+ return buildSelectPlan(operation, statementSql, target.sql, void 0, params);
402
626
  }
403
- const targetStart = upsertIndex + "UPSERT".length;
404
- const columnListIndex = findTopLevelChar(statementSql, "(", targetStart, valuesIndex);
405
- const targetEnd = columnListIndex ?? valuesIndex;
406
627
  const whereIndex = findTopLevelKeyword(statementSql, "WHERE", valuesIndex + "VALUES".length);
407
- return buildSelectPlan(
408
- "upsert",
628
+ return buildSelectPlan(operation, statementSql, target.sql, whereIndex, params);
629
+ }
630
+ function mergeTargetReference(statementSql, target, usingStart) {
631
+ let suffix = statementSql.slice(target.end, usingStart).trim();
632
+ if (suffix.length === 0) {
633
+ return target.reference;
634
+ }
635
+ if (/^PARTITION\b/i.test(suffix)) {
636
+ const open = suffix.indexOf("(");
637
+ if (open === -1) {
638
+ return void 0;
639
+ }
640
+ const close = findTopLevelChar(suffix, ")", open + 1, suffix.length);
641
+ if (close === void 0) {
642
+ return void 0;
643
+ }
644
+ suffix = suffix.slice(close + 1).trim();
645
+ if (suffix.length === 0) {
646
+ return target.reference;
647
+ }
648
+ }
649
+ suffix = suffix.replace(/^AS\b/i, "").trim();
650
+ const alias = readIdentifierPart(suffix, 0, suffix.length);
651
+ if (alias === void 0 || skipTrivia(suffix, alias.end, suffix.length) !== suffix.length) {
652
+ return void 0;
653
+ }
654
+ return alias.sql;
655
+ }
656
+ function paramsInRange(sql, params, start, end) {
657
+ const offset = countPlaceholders(sql.slice(0, start));
658
+ const length = countPlaceholders(sql.slice(start, end));
659
+ return params.slice(offset, offset + length);
660
+ }
661
+ function wholeTargetPlan(operation, statementSql, target) {
662
+ return buildSelectPlan(operation, statementSql, target.sql, void 0, []);
663
+ }
664
+ function readRequiredMergeToken(tokens, keyword, start) {
665
+ const index = findToken(tokens, keyword, start);
666
+ const token = index === void 0 ? void 0 : tokens[index];
667
+ if (index === void 0 || token === void 0) {
668
+ throw new BackupRequiredError(
669
+ "MERGE write refused: cannot derive a trustworthy backup target"
670
+ );
671
+ }
672
+ return { index, token };
673
+ }
674
+ function parseMerge(statementSql) {
675
+ const tokens = topLevelTokens(statementSql);
676
+ const into = readRequiredMergeToken(tokens, "INTO", 1);
677
+ const using = readRequiredMergeToken(tokens, "USING", into.index + 1);
678
+ const on = readRequiredMergeToken(tokens, "ON", using.index + 1);
679
+ const target = readQualifiedTarget(statementSql, into.token.end, using.token.start);
680
+ if (target === void 0) {
681
+ throw new BackupRequiredError(
682
+ "MERGE write refused: cannot derive a trustworthy backup target"
683
+ );
684
+ }
685
+ const clauseIndexes = mergeClauseIndexes(tokens, on.index + 1);
686
+ if (clauseIndexes.length === 0) {
687
+ throw new BackupRequiredError("MERGE write refused: no supported WHEN clause was found");
688
+ }
689
+ const firstClause = tokens[clauseIndexes[0] ?? 0];
690
+ return {
691
+ tokens,
692
+ into: into.token,
693
+ using: using.token,
694
+ on: on.token,
695
+ target,
696
+ clauseIndexes,
697
+ matchedIndexes: clauseIndexes.filter((index) => tokens[index + 1]?.keyword === "MATCHED"),
698
+ targetClause: statementSql.slice(into.token.end, using.token.start).trim(),
699
+ sourceClause: statementSql.slice(using.token.end, on.token.start).trim(),
700
+ onCondition: statementSql.slice(on.token.end, firstClause?.start).trim()
701
+ };
702
+ }
703
+ function readMatchedActionTokens(tokens, matchedIndex) {
704
+ const thenIndex = findToken(tokens, "THEN", matchedIndex + 2);
705
+ if (thenIndex === void 0) {
706
+ return void 0;
707
+ }
708
+ const thenToken = tokens[thenIndex];
709
+ const actionToken = tokens[thenIndex + 1];
710
+ const matchedToken = tokens[matchedIndex + 1];
711
+ if (thenToken === void 0 || actionToken === void 0 || matchedToken === void 0) {
712
+ return void 0;
713
+ }
714
+ if (!MERGE_MODIFY_ACTIONS.has(actionToken.keyword)) {
715
+ return void 0;
716
+ }
717
+ const clauseToken = tokens[matchedIndex];
718
+ return {
719
+ thenToken,
720
+ conditionToken: tokens[matchedIndex + 2],
721
+ clauseStart: clauseToken === void 0 ? 0 : clauseToken.start
722
+ };
723
+ }
724
+ function parseMatchedClause(statementSql, params, tokens, matchedIndex) {
725
+ const action = readMatchedActionTokens(tokens, matchedIndex);
726
+ if (action?.conditionToken === void 0) {
727
+ return void 0;
728
+ }
729
+ if (action.conditionToken.keyword === "THEN") {
730
+ return { clauseStart: action.clauseStart, matchedCondition: "", conditionParams: [] };
731
+ }
732
+ if (action.conditionToken.keyword !== "AND") {
733
+ return void 0;
734
+ }
735
+ return {
736
+ clauseStart: action.clauseStart,
737
+ matchedCondition: statementSql.slice(action.conditionToken.end, action.thenToken.start).trim(),
738
+ conditionParams: paramsInRange(
739
+ statementSql,
740
+ params,
741
+ action.conditionToken.end,
742
+ action.thenToken.start
743
+ )
744
+ };
745
+ }
746
+ function buildExactMergePlan(statementSql, params, merge, matchedIndex) {
747
+ const targetReference = mergeTargetReference(statementSql, merge.target, merge.using.start);
748
+ const matchedClause = parseMatchedClause(statementSql, params, merge.tokens, matchedIndex);
749
+ if (targetReference === void 0 || matchedClause === void 0) {
750
+ return void 0;
751
+ }
752
+ const sourceParams = paramsInRange(
409
753
  statementSql,
410
- statementSql.slice(targetStart, targetEnd),
411
- whereIndex,
412
- params
754
+ params,
755
+ merge.using.end,
756
+ matchedClause.clauseStart
413
757
  );
758
+ const conditionSql = matchedClause.matchedCondition.length === 0 ? "" : ` AND (${matchedClause.matchedCondition})`;
759
+ return {
760
+ operation: "merge",
761
+ statementSql,
762
+ selectSql: `SELECT ${targetReference}.* FROM ${merge.targetClause} WHERE EXISTS (SELECT 1 FROM ${merge.sourceClause} WHERE (${merge.onCondition})${conditionSql})`,
763
+ selectParams: [...sourceParams, ...matchedClause.conditionParams]
764
+ };
765
+ }
766
+ function buildMergeBackupPlan(statementSql, params) {
767
+ const merge = parseMerge(statementSql);
768
+ if (merge.matchedIndexes.length === 0) {
769
+ return void 0;
770
+ }
771
+ if (merge.matchedIndexes.length > 1) {
772
+ return wholeTargetPlan("merge", statementSql, merge.target);
773
+ }
774
+ if (merge.sourceClause.length === 0 || merge.onCondition.length === 0) {
775
+ return wholeTargetPlan("merge", statementSql, merge.target);
776
+ }
777
+ const matchedIndex = merge.matchedIndexes[0] ?? 0;
778
+ return buildExactMergePlan(statementSql, params, merge, matchedIndex) ?? wholeTargetPlan("merge", statementSql, merge.target);
414
779
  }
415
780
  function buildDeleteBackupPlan(statementSql, params) {
416
781
  const deleteIndex = findTopLevelKeyword(statementSql, "DELETE");
@@ -428,6 +793,49 @@ function buildDeleteBackupPlan(statementSql, params) {
428
793
  params
429
794
  );
430
795
  }
796
+ function isWriteKeyword(keyword) {
797
+ return WRITE_KEYWORDS.has(keyword);
798
+ }
799
+ function buildMergeWriteBackupPlan(statementSql, params) {
800
+ const secondKeyword = topLevelTokens(statementSql)[1]?.keyword;
801
+ if (secondKeyword === "DELTA") {
802
+ return void 0;
803
+ }
804
+ if (secondKeyword !== "INTO") {
805
+ throw new BackupRequiredError("MERGE write refused: expected MERGE INTO syntax");
806
+ }
807
+ return buildMergeBackupPlan(statementSql, params);
808
+ }
809
+ function dispatchWriteBackupPlan(keyword, statementSql, params) {
810
+ switch (keyword) {
811
+ case "UPDATE":
812
+ return buildUpdateBackupPlan(statementSql, params);
813
+ case "UPSERT":
814
+ case "REPLACE":
815
+ return buildUpsertBackupPlan(statementSql, params, keyword);
816
+ case "MERGE":
817
+ return buildMergeWriteBackupPlan(statementSql, params);
818
+ case "DELETE":
819
+ return buildDeleteBackupPlan(statementSql, params);
820
+ }
821
+ }
822
+ function buildWriteBackupPlan(sql, params = []) {
823
+ const statementSql = trimStatementSql(sql);
824
+ const keyword = firstKeyword(statementSql);
825
+ if (!isWriteKeyword(keyword)) {
826
+ return void 0;
827
+ }
828
+ assertParamArity(statementSql, params);
829
+ return dispatchWriteBackupPlan(keyword, statementSql, params);
830
+ }
831
+
832
+ // src/backup.ts
833
+ var SAPTOOLS_DIR_NAME = ".saptools";
834
+ var CF_HANA_DIR_NAME = "cf-hana";
835
+ var BACKUPS_DIR_NAME = "backups";
836
+ function defaultSaptoolsRoot() {
837
+ return join(homedir(), SAPTOOLS_DIR_NAME);
838
+ }
431
839
  function backupTimestamp(now) {
432
840
  return now.toISOString().replace(/:/g, "").replace(".", "");
433
841
  }
@@ -447,21 +855,6 @@ function backupBaseName(input, now) {
447
855
  function cfHanaBackupRoot(saptoolsRoot) {
448
856
  return join(saptoolsRoot ?? defaultSaptoolsRoot(), CF_HANA_DIR_NAME, BACKUPS_DIR_NAME);
449
857
  }
450
- function buildWriteBackupPlan(sql, params = []) {
451
- const statementSql = trimStatementSql(sql);
452
- const keyword = firstKeyword(statementSql);
453
- if (keyword !== "UPDATE" && keyword !== "UPSERT" && keyword !== "DELETE") {
454
- return void 0;
455
- }
456
- assertParamArity(statementSql, params);
457
- if (keyword === "UPDATE") {
458
- return buildUpdateBackupPlan(statementSql, params);
459
- }
460
- if (keyword === "UPSERT") {
461
- return buildUpsertBackupPlan(statementSql, params);
462
- }
463
- return buildDeleteBackupPlan(statementSql, params);
464
- }
465
858
  async function writeSqlBackup(input, options = {}) {
466
859
  const now = options.now ?? /* @__PURE__ */ new Date();
467
860
  const directory = join(cfHanaBackupRoot(options.saptoolsRoot), backupMonth(now));
@@ -477,11 +870,17 @@ async function writeSqlBackup(input, options = {}) {
477
870
  rowCount: input.result.rowCount,
478
871
  createdAt: now.toISOString()
479
872
  };
873
+ const csv = formatCsv(input.result);
874
+ if (Buffer.byteLength(csv) > (options.maxBytes ?? MAX_RESULT_STORE_BYTES)) {
875
+ throw new BackupRequiredError(
876
+ "Write backup exceeds the storage limit; the write was refused"
877
+ );
878
+ }
480
879
  await mkdir(directory, { recursive: true, mode: 448 });
481
880
  await Promise.all([
482
881
  writeFile(statementPath, `${input.statementSql}
483
882
  `, { encoding: "utf8", mode: 384 }),
484
- writeFile(backupPath, formatCsv(input.result), { encoding: "utf8", mode: 384 }),
883
+ writeFile(backupPath, csv, { encoding: "utf8", mode: 384 }),
485
884
  writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}
486
885
  `, {
487
886
  encoding: "utf8",
@@ -624,35 +1023,6 @@ async function listColumns(connection, schema, table) {
624
1023
  }));
625
1024
  }
626
1025
 
627
- // src/config.ts
628
- var CLI_VERSION = "0.3.4";
629
- var ENV_PREFIX = "CF_HANA";
630
- var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
631
- var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
632
- var DEFAULT_POOL_MAX = 4;
633
- var DEFAULT_POOL_IDLE_MS = 6e4;
634
- var DEFAULT_AUTO_LIMIT = 100;
635
- var MAX_RESULT_STORE_BYTES = 256 * 1024 * 1024;
636
- function envName(suffix) {
637
- return `${ENV_PREFIX}_${suffix}`;
638
- }
639
- function readEnv(name) {
640
- const value = process.env[name];
641
- if (value === void 0) {
642
- return void 0;
643
- }
644
- const trimmed = value.trim();
645
- return trimmed.length === 0 ? void 0 : trimmed;
646
- }
647
- function readSapCredentials(overrides) {
648
- const email = overrides?.email ?? readEnv("SAP_EMAIL");
649
- const password = overrides?.password ?? readEnv("SAP_PASSWORD");
650
- if (email === void 0 || password === void 0) {
651
- return void 0;
652
- }
653
- return { email, password };
654
- }
655
-
656
1026
  // src/cf.ts
657
1027
  import { execFile } from "child_process";
658
1028
  import { mkdtemp, rm } from "fs/promises";
@@ -661,9 +1031,9 @@ import { join as join2 } from "path";
661
1031
  import { promisify } from "util";
662
1032
  var execFileAsync = promisify(execFile);
663
1033
  var MAX_BUFFER = 16 * 1024 * 1024;
664
- var DEFAULT_TIMEOUT_MS = 3e4;
1034
+ var DEFAULT_TIMEOUT_MS = 6e4;
665
1035
  var CF_RETRY_ATTEMPTS = 3;
666
- var CF_RETRY_BASE_DELAY_MS = 120;
1036
+ var CF_RETRY_BASE_DELAY_MS = 500;
667
1037
  var REGION_API_MAP = {
668
1038
  ae01: "https://api.cf.ae01.hana.ondemand.com",
669
1039
  ap01: "https://api.cf.ap01.hana.ondemand.com",
@@ -785,29 +1155,45 @@ function buildEnv(ctx, overrides = {}) {
785
1155
  env["CF_HOME"] = ctx.cfHome;
786
1156
  return env;
787
1157
  }
788
- async function runCf(args, ctx, overrides = {}) {
789
- const { bin, argsPrefix } = resolveCfBin();
790
- const env = buildEnv(ctx, overrides);
1158
+ async function execWithRetries(bin, args, env, timeoutMs = DEFAULT_TIMEOUT_MS) {
791
1159
  let lastErr;
792
1160
  for (let attempt = 0; attempt < CF_RETRY_ATTEMPTS; attempt++) {
793
1161
  try {
794
- const { stdout } = await execFileAsync(bin, [...argsPrefix, ...args], {
1162
+ const { stdout } = await execFileAsync(bin, args, {
795
1163
  env,
796
1164
  maxBuffer: MAX_BUFFER,
797
- timeout: DEFAULT_TIMEOUT_MS
1165
+ timeout: timeoutMs,
1166
+ killSignal: "SIGKILL"
798
1167
  });
799
1168
  return stdout;
800
1169
  } catch (err) {
801
1170
  lastErr = err;
1171
+ const e = err;
1172
+ const output = `${e.message ?? ""} ${e.stderr ? String(e.stderr) : ""}`.toLowerCase();
1173
+ const isTimeout = e.killed === true;
1174
+ const isNetworkFlake = output.includes("error performing request") || output.includes("timeout") || output.includes("connection reset") || output.includes("connection refused") || output.includes("eof") || output.includes("502 bad gateway") || output.includes("503 service unavailable") || output.includes("504 gateway timeout") || output.includes("dial tcp") || output.includes("no such host");
1175
+ const isEnoent = e.code === "ENOENT";
1176
+ if (isEnoent || !isTimeout && !isNetworkFlake) {
1177
+ throw err;
1178
+ }
802
1179
  if (attempt < CF_RETRY_ATTEMPTS - 1) {
803
1180
  await new Promise((r) => setTimeout(r, CF_RETRY_BASE_DELAY_MS * (attempt + 1)));
804
1181
  }
805
1182
  }
806
1183
  }
807
- const e = lastErr;
808
- const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
809
- const cmd = args[0] === "auth" ? "cf auth" : `cf ${args.join(" ")}`;
810
- throw new Error(`${cmd} failed: ${detail}`.trim(), { cause: lastErr });
1184
+ throw lastErr;
1185
+ }
1186
+ async function runCf(args, ctx, overrides = {}) {
1187
+ const { bin, argsPrefix } = resolveCfBin();
1188
+ const env = buildEnv(ctx, overrides);
1189
+ try {
1190
+ return await execWithRetries(bin, [...argsPrefix, ...args], env);
1191
+ } catch (lastErr) {
1192
+ const e = lastErr;
1193
+ const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
1194
+ const cmd = args[0] === "auth" ? "cf auth" : `cf ${args.join(" ")}`;
1195
+ throw new Error(`${cmd} failed: ${detail}`.trim(), { cause: lastErr });
1196
+ }
811
1197
  }
812
1198
  async function cfApi(apiEndpoint, ctx) {
813
1199
  await runCf(["api", apiEndpoint], ctx);
@@ -829,12 +1215,7 @@ async function cfEnvDirect(appName) {
829
1215
  const env = { ...process.env };
830
1216
  delete env["SAP_EMAIL"];
831
1217
  delete env["SAP_PASSWORD"];
832
- const { stdout } = await execFileAsync(bin, [...argsPrefix, "env", appName], {
833
- env,
834
- maxBuffer: MAX_BUFFER,
835
- timeout: DEFAULT_TIMEOUT_MS
836
- });
837
- return stdout;
1218
+ return await execWithRetries(bin, [...argsPrefix, "env", appName], env);
838
1219
  }
839
1220
  async function readCurrentCfTarget() {
840
1221
  const { bin, argsPrefix } = resolveCfBin();
@@ -842,11 +1223,7 @@ async function readCurrentCfTarget() {
842
1223
  delete env["SAP_EMAIL"];
843
1224
  delete env["SAP_PASSWORD"];
844
1225
  try {
845
- const { stdout } = await execFileAsync(bin, [...argsPrefix, "target"], {
846
- env,
847
- maxBuffer: MAX_BUFFER,
848
- timeout: 1e4
849
- });
1226
+ const stdout = await execWithRetries(bin, [...argsPrefix, "target"], env);
850
1227
  return parseCfTargetOutput(stdout);
851
1228
  } catch {
852
1229
  return void 0;
@@ -897,6 +1274,81 @@ function formatCurrentCfAppSelector(target, appName) {
897
1274
  const region = target.regionKey ?? "current";
898
1275
  return `${region}/${target.orgName}/${target.spaceName}/${name}`;
899
1276
  }
1277
+ function findJsonObjectEnd(source, startIndex) {
1278
+ let depth = 0;
1279
+ let inString = false;
1280
+ let escaped = false;
1281
+ for (let index = startIndex; index < source.length; index++) {
1282
+ const character = source[index];
1283
+ if (escaped) {
1284
+ escaped = false;
1285
+ continue;
1286
+ }
1287
+ if (inString && character === "\\") {
1288
+ escaped = true;
1289
+ continue;
1290
+ }
1291
+ if (character === '"') {
1292
+ inString = !inString;
1293
+ continue;
1294
+ }
1295
+ if (inString) {
1296
+ continue;
1297
+ }
1298
+ if (character === "{") {
1299
+ depth++;
1300
+ continue;
1301
+ }
1302
+ if (character === "}" && --depth === 0) {
1303
+ return index;
1304
+ }
1305
+ }
1306
+ return -1;
1307
+ }
1308
+ function extractVcapApplicationJson(stdout) {
1309
+ const marker = "VCAP_APPLICATION:";
1310
+ const markerIndex = stdout.indexOf(marker);
1311
+ if (markerIndex === -1) {
1312
+ throw new Error("VCAP_APPLICATION section not found in cf env output");
1313
+ }
1314
+ const afterMarker = stdout.slice(markerIndex + marker.length);
1315
+ const openIndex = afterMarker.indexOf("{");
1316
+ if (openIndex === -1) {
1317
+ throw new Error("VCAP_APPLICATION JSON payload not found in cf env output");
1318
+ }
1319
+ const closeIndex = findJsonObjectEnd(afterMarker, openIndex);
1320
+ if (closeIndex === -1) {
1321
+ throw new Error("Malformed VCAP_APPLICATION JSON in cf env output");
1322
+ }
1323
+ return afterMarker.slice(openIndex, closeIndex + 1);
1324
+ }
1325
+ function requireApplicationField(application, field) {
1326
+ const value = application[field];
1327
+ if (typeof value !== "string" || value.trim().length === 0) {
1328
+ throw new Error(`VCAP_APPLICATION.${field} must be a non-empty string`);
1329
+ }
1330
+ return value.trim();
1331
+ }
1332
+ function extractCfEnvApplicationIdentity(stdout) {
1333
+ let application;
1334
+ try {
1335
+ application = JSON.parse(extractVcapApplicationJson(stdout));
1336
+ } catch (error) {
1337
+ if (error instanceof SyntaxError) {
1338
+ throw new Error("VCAP_APPLICATION is not valid JSON", { cause: error });
1339
+ }
1340
+ throw error;
1341
+ }
1342
+ if (!isRecord(application)) {
1343
+ throw new Error("VCAP_APPLICATION must be an object");
1344
+ }
1345
+ return {
1346
+ appName: requireApplicationField(application, "application_name"),
1347
+ apiEndpoint: normalizeSapCfApiEndpoint(requireApplicationField(application, "cf_api")),
1348
+ orgName: requireApplicationField(application, "organization_name"),
1349
+ spaceName: requireApplicationField(application, "space_name")
1350
+ };
1351
+ }
900
1352
  function extractVcapSection(stdout) {
901
1353
  const start = stdout.indexOf("VCAP_SERVICES:");
902
1354
  if (start === -1) {
@@ -978,107 +1430,143 @@ function classifyCfError(stderr = "", stdout = "") {
978
1430
  function errorMessageFromUnknown(error) {
979
1431
  return error instanceof Error ? error.message : "Invalid or untrusted CF API endpoint.";
980
1432
  }
981
- async function resolveAppBindings(rawSelector, options) {
982
- const selector = rawSelector.trim();
983
- if (!selector) {
984
- throw new CfHanaError("CONFIG", "App selector is required");
1433
+ function parseExplicitTarget(selector) {
1434
+ const [regionKey, orgName, spaceName, appName, extra] = selector.split("/").map((part) => part.trim());
1435
+ if (extra !== void 0 || !regionKey || !orgName || !spaceName || !appName) {
1436
+ throw new CfHanaError(
1437
+ "CONFIG",
1438
+ `Invalid selector "${selector}". Use region/org/space/app or a bare app name.`
1439
+ );
985
1440
  }
986
- let target;
987
- const isBare = !selector.includes("/");
988
- if (isBare) {
989
- const current = await readCurrentCfTarget();
990
- if (!current) {
991
- throw new CfHanaError(
992
- "CONFIG",
993
- "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
994
- );
995
- }
996
- const displaySelector = current.regionKey ? formatCurrentCfAppSelector(current, selector) : `current/${current.orgName}/${current.spaceName}/${selector}`;
997
- target = {
998
- selector: displaySelector,
999
- apiEndpoint: current.apiEndpoint,
1000
- orgName: current.orgName,
1001
- spaceName: current.spaceName,
1002
- appName: selector
1441
+ const apiEndpoint = getApiEndpointForRegion(regionKey);
1442
+ if (apiEndpoint === void 0) {
1443
+ throw new CfHanaError(
1444
+ "CONFIG",
1445
+ `Unknown SAP CF region "${regionKey}". Verify the current SAP region list or use the current CF target.`
1446
+ );
1447
+ }
1448
+ try {
1449
+ return {
1450
+ selector,
1451
+ apiEndpoint: normalizeSapCfApiEndpoint(apiEndpoint),
1452
+ orgName,
1453
+ spaceName,
1454
+ appName,
1455
+ selectorSource: "explicit",
1456
+ regionConfirmed: true,
1457
+ selectorCanBePinned: true
1003
1458
  };
1004
- } else {
1005
- const parts = selector.split("/").map((p) => p.trim());
1006
- if (parts.length !== 4 || !parts[0] || !parts[1] || !parts[2] || !parts[3]) {
1007
- throw new CfHanaError(
1008
- "CONFIG",
1009
- `Invalid selector "${selector}". Use region/org/space/app or a bare app name.`
1010
- );
1011
- }
1012
- const [regionKey, orgName, spaceName, appName] = parts;
1013
- const apiEndpoint = getApiEndpointForRegion(regionKey);
1014
- if (!apiEndpoint) {
1459
+ } catch (error) {
1460
+ throw new CfHanaError("CONFIG", errorMessageFromUnknown(error), { cause: error });
1461
+ }
1462
+ }
1463
+ async function resolveTarget(selector) {
1464
+ if (selector.includes("/")) {
1465
+ return parseExplicitTarget(selector);
1466
+ }
1467
+ const current = await readCurrentCfTarget();
1468
+ if (current === void 0) {
1469
+ throw new CfHanaError(
1470
+ "CONFIG",
1471
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
1472
+ );
1473
+ }
1474
+ return {
1475
+ selector: formatCurrentCfAppSelector(current, selector),
1476
+ apiEndpoint: current.apiEndpoint,
1477
+ orgName: current.orgName,
1478
+ spaceName: current.spaceName,
1479
+ appName: selector,
1480
+ selectorSource: "ambient",
1481
+ regionConfirmed: current.regionKey !== void 0,
1482
+ selectorCanBePinned: current.regionKey !== void 0 && getApiEndpointForRegion(current.regionKey) !== void 0
1483
+ };
1484
+ }
1485
+ function commandErrorText(error) {
1486
+ if (typeof error !== "object" || error === null) {
1487
+ return "";
1488
+ }
1489
+ if ("stderr" in error && typeof error.stderr === "string" && error.stderr.length > 0) {
1490
+ return error.stderr;
1491
+ }
1492
+ return "message" in error && typeof error.message === "string" ? error.message : "";
1493
+ }
1494
+ function isSameTarget(target, current) {
1495
+ return current?.apiEndpoint === target.apiEndpoint && current.orgName === target.orgName && current.spaceName === target.spaceName;
1496
+ }
1497
+ async function assertAmbientTargetUnchanged(target) {
1498
+ const current = await readCurrentCfTarget();
1499
+ if (!isSameTarget(target, current)) {
1500
+ throw new CfHanaError(
1501
+ "CONFIG",
1502
+ "CF target changed during binding discovery; no database connection was opened. Retry or use an explicit region/org/space/app selector."
1503
+ );
1504
+ }
1505
+ }
1506
+ function readCfEnvApplicationIdentity(stdout) {
1507
+ try {
1508
+ return extractCfEnvApplicationIdentity(stdout);
1509
+ } catch (error) {
1510
+ throw new CfHanaError(
1511
+ "CONFIG",
1512
+ "Could not verify cf env application identity for the ambient target; no database connection was opened. Retry or use an explicit region/org/space/app selector.",
1513
+ { cause: error }
1514
+ );
1515
+ }
1516
+ }
1517
+ function assertCfEnvApplicationMatchesTarget(target, application) {
1518
+ const matches = application.apiEndpoint === target.apiEndpoint && application.orgName === target.orgName && application.spaceName === target.spaceName && application.appName === target.appName;
1519
+ if (matches) {
1520
+ return;
1521
+ }
1522
+ throw new CfHanaError(
1523
+ "CONFIG",
1524
+ "CF env application identity did not match the resolved ambient target; no database connection was opened. Retry or use an explicit region/org/space/app selector."
1525
+ );
1526
+ }
1527
+ async function readIsolatedBindings(target, sap) {
1528
+ return await withCfSession(async (ctx) => {
1529
+ await cfApi(target.apiEndpoint, ctx);
1530
+ await cfAuth(sap.email, sap.password, ctx);
1531
+ await cfTargetSpace(target.orgName, target.spaceName, ctx);
1532
+ return extractHanaBindingsFromCfEnv(await cfEnv(target.appName, ctx));
1533
+ });
1534
+ }
1535
+ async function readBareBindings(target, options) {
1536
+ let stdout;
1537
+ try {
1538
+ stdout = await cfEnvDirect(target.appName);
1539
+ } catch (directError) {
1540
+ const classified = classifyCfError(commandErrorText(directError));
1541
+ if (!classified.isAuthError) {
1015
1542
  throw new CfHanaError(
1016
1543
  "CONFIG",
1017
- `Unknown SAP CF region "${regionKey}". Verify the current SAP region list or use the current CF target.`
1544
+ `Failed to get HANA bindings for bare app "${target.appName}" using current target (org=${target.orgName}, space=${target.spaceName}). Verify with "cf target" and "cf env ${target.appName}".`,
1545
+ { cause: directError }
1018
1546
  );
1019
1547
  }
1020
- let normalizedApiEndpoint;
1021
- try {
1022
- normalizedApiEndpoint = normalizeSapCfApiEndpoint(apiEndpoint);
1023
- } catch (error) {
1024
- throw new CfHanaError("CONFIG", errorMessageFromUnknown(error), { cause: error });
1025
- }
1026
- target = { selector, apiEndpoint: normalizedApiEndpoint, orgName, spaceName, appName };
1027
- }
1028
- let bindings;
1029
- const source = "live";
1030
- if (isBare) {
1031
- try {
1032
- const stdout = await cfEnvDirect(target.appName);
1033
- bindings = extractHanaBindingsFromCfEnv(stdout);
1034
- } catch (directError) {
1035
- let stderr = "";
1036
- if (directError && typeof directError === "object") {
1037
- const e = directError;
1038
- stderr = (typeof e.stderr === "string" ? e.stderr : "") || (typeof e.message === "string" ? e.message : "");
1039
- }
1040
- const classified = classifyCfError(stderr);
1041
- if (classified.isAuthError) {
1042
- const sap = readSapCredentials({ email: options.email, password: options.password });
1043
- if (!sap) {
1044
- throw new CredentialsNotFoundError(
1045
- `Current CF session problem for bare app "${target.appName}" (${classified.reason}).
1046
- Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
1047
- { cause: directError }
1048
- );
1049
- }
1050
- const api = target.apiEndpoint;
1051
- bindings = await withCfSession(async (ctx) => {
1052
- await cfApi(api, ctx);
1053
- await cfAuth(sap.email, sap.password, ctx);
1054
- await cfTargetSpace(target.orgName, target.spaceName, ctx);
1055
- const stdout = await cfEnv(target.appName, ctx);
1056
- return extractHanaBindingsFromCfEnv(stdout);
1057
- });
1058
- } else {
1059
- throw new CfHanaError(
1060
- "CONFIG",
1061
- `Failed to get HANA bindings for bare app "${target.appName}" using current target (org=${target.orgName}, space=${target.spaceName}). Verify with "cf target" and "cf env ${target.appName}". ${stderr ? `Details: ${stderr}` : ""}`,
1062
- { cause: directError }
1063
- );
1064
- }
1065
- }
1066
- } else {
1067
1548
  const sap = readSapCredentials({ email: options.email, password: options.password });
1068
- if (!sap) {
1549
+ if (sap === void 0) {
1069
1550
  throw new CredentialsNotFoundError(
1070
- `SAP_EMAIL and SAP_PASSWORD are required to fetch HANA bindings for explicit selector "${selector}".`
1551
+ `Current CF session problem for bare app "${target.appName}" (${classified.reason}).
1552
+ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
1553
+ { cause: directError }
1071
1554
  );
1072
1555
  }
1073
- const api = target.apiEndpoint;
1074
- bindings = await withCfSession(async (ctx) => {
1075
- await cfApi(api, ctx);
1076
- await cfAuth(sap.email, sap.password, ctx);
1077
- await cfTargetSpace(target.orgName, target.spaceName, ctx);
1078
- const stdout = await cfEnv(target.appName, ctx);
1079
- return extractHanaBindingsFromCfEnv(stdout);
1080
- });
1556
+ return await readIsolatedBindings(target, sap);
1557
+ }
1558
+ const application = readCfEnvApplicationIdentity(stdout);
1559
+ await assertAmbientTargetUnchanged(target);
1560
+ assertCfEnvApplicationMatchesTarget(target, application);
1561
+ return extractHanaBindingsFromCfEnv(stdout);
1562
+ }
1563
+ async function resolveAppBindings(rawSelector, options) {
1564
+ const selector = rawSelector.trim();
1565
+ if (selector.length === 0) {
1566
+ throw new CfHanaError("CONFIG", "App selector is required");
1081
1567
  }
1568
+ const target = await resolveTarget(selector);
1569
+ const bindings = target.selectorSource === "ambient" ? await readBareBindings(target, options) : await readExplicitBindings(target, options);
1082
1570
  if (bindings.length === 0) {
1083
1571
  throw new CredentialsNotFoundError(`App "${target.selector}" has no HANA service binding.`);
1084
1572
  }
@@ -1086,9 +1574,21 @@ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
1086
1574
  selector: target.selector,
1087
1575
  appName: target.appName,
1088
1576
  bindings,
1089
- source
1577
+ source: "live",
1578
+ selectorSource: target.selectorSource,
1579
+ regionConfirmed: target.regionConfirmed,
1580
+ selectorCanBePinned: target.selectorCanBePinned
1090
1581
  };
1091
1582
  }
1583
+ async function readExplicitBindings(target, options) {
1584
+ const sap = readSapCredentials({ email: options.email, password: options.password });
1585
+ if (sap === void 0) {
1586
+ throw new CredentialsNotFoundError(
1587
+ `SAP_EMAIL and SAP_PASSWORD are required to fetch HANA bindings for explicit selector "${target.selector}".`
1588
+ );
1589
+ }
1590
+ return await readIsolatedBindings(target, sap);
1591
+ }
1092
1592
  function selectBinding(bindings, selector) {
1093
1593
  if (selector.bindingName !== void 0) {
1094
1594
  const match = bindings.find((binding) => binding.name === selector.bindingName);
@@ -1159,42 +1659,82 @@ function tableColumns() {
1159
1659
  { COLUMN_NAME: "SCOPE_NAME", DATA_TYPE_NAME: "NVARCHAR", LENGTH: 255, SCALE: null, IS_NULLABLE: "TRUE", POSITION: 3 }
1160
1660
  ];
1161
1661
  }
1162
- function fakeExec(sql) {
1163
- const kind = classifyStatement(sql);
1662
+ function throwForcedFailure(kind) {
1164
1663
  const forcedFailure = readEnv(envName("FAKE_FAIL_STATEMENT"))?.toLowerCase();
1165
1664
  if (forcedFailure === kind) {
1166
1665
  throw new Error(`fake driver forced ${kind.toUpperCase()} failure`);
1167
1666
  }
1168
- const upperSql = sql.toUpperCase();
1667
+ }
1668
+ function throwEarlyFixtureError(upperSql) {
1669
+ if (upperSql.includes("PRIVILEGE_ERROR_CODE")) {
1670
+ throw new QueryError("insufficient privilege: not authorized", { databaseCode: 258 });
1671
+ }
1672
+ if (upperSql.includes("PRIVILEGE_ERROR_MESSAGE")) {
1673
+ throw new QueryError("insufficient privilege: grant is missing");
1674
+ }
1675
+ if (upperSql.includes("NON_PRIVILEGE_ERROR")) {
1676
+ throw new QueryError("fake unrelated query failure", { databaseCode: 999 });
1677
+ }
1678
+ if (readEnv(envName("FAKE_PRIVILEGE_CATALOG")) === "1" && (upperSql.includes("SYS.TABLES") || upperSql.includes("SYS.TABLE_COLUMNS"))) {
1679
+ throw new QueryError("insufficient privilege: catalog access denied", { databaseCode: 258 });
1680
+ }
1681
+ }
1682
+ function catalogObjectsResult() {
1683
+ return {
1684
+ rows: catalogObjects(),
1685
+ columns: [
1686
+ { name: "SCHEMA_NAME", typeName: "NVARCHAR" },
1687
+ { name: "OBJECT_NAME", typeName: "NVARCHAR" },
1688
+ { name: "OBJECT_TYPE", typeName: "NVARCHAR" }
1689
+ ],
1690
+ affectedRows: 0
1691
+ };
1692
+ }
1693
+ function tablesResult() {
1694
+ return {
1695
+ rows: [
1696
+ { SCHEMA_NAME: "APP_SCHEMA", TABLE_NAME: "EXISTING_TABLE", TABLE_TYPE: "COLUMN TABLE" },
1697
+ { SCHEMA_NAME: "APP_SCHEMA", TABLE_NAME: "STATUS_ITEMS", TABLE_TYPE: "ROW TABLE" }
1698
+ ],
1699
+ columns: [
1700
+ { name: "SCHEMA_NAME", typeName: "NVARCHAR" },
1701
+ { name: "TABLE_NAME", typeName: "NVARCHAR" },
1702
+ { name: "TABLE_TYPE", typeName: "NVARCHAR" }
1703
+ ],
1704
+ affectedRows: 0
1705
+ };
1706
+ }
1707
+ function tableColumnsResult() {
1708
+ return {
1709
+ rows: tableColumns(),
1710
+ columns: [
1711
+ { name: "COLUMN_NAME", typeName: "NVARCHAR" },
1712
+ { name: "DATA_TYPE_NAME", typeName: "NVARCHAR" },
1713
+ { name: "LENGTH", typeName: "INTEGER" },
1714
+ { name: "SCALE", typeName: "INTEGER" },
1715
+ { name: "IS_NULLABLE", typeName: "NVARCHAR" },
1716
+ { name: "POSITION", typeName: "INTEGER" }
1717
+ ],
1718
+ affectedRows: 0
1719
+ };
1720
+ }
1721
+ function matchCatalogFixture(upperSql) {
1169
1722
  if (upperSql.includes("SYS.TABLES") && upperSql.includes("SYS.VIEWS")) {
1170
1723
  if (readEnv(envName("FAKE_FAIL_CATALOG_ONCE")) === "1" && !catalogFailureInjected) {
1171
1724
  catalogFailureInjected = true;
1172
1725
  throw new QueryError("fake transient catalog metadata failure");
1173
1726
  }
1174
- return {
1175
- rows: catalogObjects(),
1176
- columns: [
1177
- { name: "SCHEMA_NAME", typeName: "NVARCHAR" },
1178
- { name: "OBJECT_NAME", typeName: "NVARCHAR" },
1179
- { name: "OBJECT_TYPE", typeName: "NVARCHAR" }
1180
- ],
1181
- affectedRows: 0
1182
- };
1727
+ return catalogObjectsResult();
1728
+ }
1729
+ if (upperSql.includes("SYS.TABLES")) {
1730
+ return tablesResult();
1183
1731
  }
1184
1732
  if (upperSql.includes("SYS.TABLE_COLUMNS")) {
1185
- return {
1186
- rows: tableColumns(),
1187
- columns: [
1188
- { name: "COLUMN_NAME", typeName: "NVARCHAR" },
1189
- { name: "DATA_TYPE_NAME", typeName: "NVARCHAR" },
1190
- { name: "LENGTH", typeName: "INTEGER" },
1191
- { name: "SCALE", typeName: "INTEGER" },
1192
- { name: "IS_NULLABLE", typeName: "NVARCHAR" },
1193
- { name: "POSITION", typeName: "INTEGER" }
1194
- ],
1195
- affectedRows: 0
1196
- };
1733
+ return tableColumnsResult();
1197
1734
  }
1735
+ return void 0;
1736
+ }
1737
+ function throwLateFixtureError(upperSql) {
1198
1738
  if (upperSql.includes("MISSING_TABLE") || upperSql.includes("MISSING_TABLES")) {
1199
1739
  throw new QueryError("invalid table name: MISSING_TABLE", { sqlState: "42S02" });
1200
1740
  }
@@ -1207,30 +1747,45 @@ function fakeExec(sql) {
1207
1747
  if (upperSql.includes("LOB_GROUP_ERROR")) {
1208
1748
  throw new QueryError("inconsistent datatype: LOB type is not allowed in GROUP BY clause", { databaseCode: 274 });
1209
1749
  }
1210
- if (sql.toUpperCase().includes("LOB_FIXTURE")) {
1750
+ }
1751
+ function lobFixtureResult() {
1752
+ return {
1753
+ rows: [
1754
+ {
1755
+ LOG_CONTENT: Buffer.from("Example log entry", "utf8"),
1756
+ CLOB_CONTENT: Buffer.from("Clob log entry", "utf8"),
1757
+ PAYLOAD: Buffer.from([0, 1, 2, 255])
1758
+ }
1759
+ ],
1760
+ columns: [
1761
+ { name: "LOG_CONTENT", typeName: "NCLOB" },
1762
+ { name: "CLOB_CONTENT", typeName: "CLOB" },
1763
+ { name: "PAYLOAD", typeName: "BLOB" }
1764
+ ],
1765
+ affectedRows: 0
1766
+ };
1767
+ }
1768
+ function matchDataFixture(upperSql) {
1769
+ if (upperSql.includes("LOB_FIXTURE")) {
1770
+ return lobFixtureResult();
1771
+ }
1772
+ if (upperSql.includes("SINGLE_COLUMN_FIXTURE")) {
1211
1773
  return {
1212
- rows: [
1213
- {
1214
- LOG_CONTENT: Buffer.from("Example log entry", "utf8"),
1215
- CLOB_CONTENT: Buffer.from("Clob log entry", "utf8"),
1216
- PAYLOAD: Buffer.from([0, 1, 2, 255])
1217
- }
1218
- ],
1219
- columns: [
1220
- { name: "LOG_CONTENT", typeName: "NCLOB" },
1221
- { name: "CLOB_CONTENT", typeName: "CLOB" },
1222
- { name: "PAYLOAD", typeName: "BLOB" }
1223
- ],
1774
+ rows: [{ VALUE: "alpha" }, { VALUE: "beta" }],
1775
+ columns: [{ name: "VALUE", typeName: "NVARCHAR" }],
1224
1776
  affectedRows: 0
1225
1777
  };
1226
1778
  }
1227
- if (sql.toUpperCase().includes("DUMMY")) {
1779
+ if (upperSql.includes("DUMMY")) {
1228
1780
  return {
1229
1781
  rows: [{ "1": 1 }],
1230
1782
  columns: [{ name: "1", typeName: "INTEGER" }],
1231
1783
  affectedRows: 0
1232
1784
  };
1233
1785
  }
1786
+ return void 0;
1787
+ }
1788
+ function defaultResult(kind) {
1234
1789
  if (kind === "select") {
1235
1790
  return {
1236
1791
  rows: [
@@ -1249,6 +1804,18 @@ function fakeExec(sql) {
1249
1804
  }
1250
1805
  return { rows: [], columns: [], affectedRows: 0 };
1251
1806
  }
1807
+ function fakeExec(sql) {
1808
+ const kind = classifyStatement(sql);
1809
+ throwForcedFailure(kind);
1810
+ const upperSql = sql.toUpperCase();
1811
+ throwEarlyFixtureError(upperSql);
1812
+ const catalogFixture = matchCatalogFixture(upperSql);
1813
+ if (catalogFixture !== void 0) {
1814
+ return catalogFixture;
1815
+ }
1816
+ throwLateFixtureError(upperSql);
1817
+ return matchDataFixture(upperSql) ?? defaultResult(kind);
1818
+ }
1252
1819
  async function traceFakeExec(sql, params) {
1253
1820
  const tracePath = readEnv(envName("FAKE_TRACE_FILE"));
1254
1821
  if (tracePath === void 0) {
@@ -1679,8 +2246,30 @@ function maskIgnoredSqlText(sql) {
1679
2246
  }
1680
2247
  return masked;
1681
2248
  }
1682
- function hasWhereClause(sql) {
1683
- return /\bwhere\b/i.test(maskIgnoredSqlText(sql));
2249
+ function hasTopLevelKeyword(sql, keyword) {
2250
+ const masked = maskIgnoredSqlText(sql);
2251
+ let depth = 0;
2252
+ for (let index = 0; index < masked.length; index += 1) {
2253
+ const char = masked[index];
2254
+ if (char === "(") {
2255
+ depth += 1;
2256
+ continue;
2257
+ }
2258
+ if (char === ")") {
2259
+ depth = Math.max(0, depth - 1);
2260
+ continue;
2261
+ }
2262
+ if (depth === 0 && masked.slice(index, index + keyword.length).toUpperCase() === keyword && !/[A-Za-z0-9_$#]/.test(masked.charAt(index - 1)) && !/[A-Za-z0-9_$#]/.test(masked.charAt(index + keyword.length))) {
2263
+ return true;
2264
+ }
2265
+ }
2266
+ return false;
2267
+ }
2268
+ function isUnconditionalMergeDelete(sql) {
2269
+ return /\bWHEN\s+MATCHED\s+THEN\s+DELETE\b/i.test(maskIgnoredSqlText(sql));
2270
+ }
2271
+ function isMalformedReplace(sql) {
2272
+ return !hasTopLevelKeyword(sql, "VALUES") && !hasTopLevelKeyword(sql, "SELECT") && !hasTopLevelKeyword(sql, "WITH");
1684
2273
  }
1685
2274
  function trailingLineCommentIndex(sql) {
1686
2275
  let index = 0;
@@ -1723,9 +2312,10 @@ function inspectStatement(sql) {
1723
2312
  return { kind, destructive: DESTRUCTIVE_DDL_KEYWORDS.has(keyword) };
1724
2313
  }
1725
2314
  if (kind === "dml") {
2315
+ const destructive = UNSCOPED_WRITE_KEYWORDS.has(keyword) && !hasTopLevelKeyword(sql, "WHERE") || keyword === "MERGE" && isUnconditionalMergeDelete(sql) || keyword === "REPLACE" && isMalformedReplace(sql);
1726
2316
  return {
1727
2317
  kind,
1728
- destructive: UNSCOPED_WRITE_KEYWORDS.has(keyword) && !hasWhereClause(sql)
2318
+ destructive
1729
2319
  };
1730
2320
  }
1731
2321
  return { kind, destructive: false };
@@ -1745,7 +2335,7 @@ function evaluateGuard(sql, config) {
1745
2335
  allowed: false,
1746
2336
  destructive: true,
1747
2337
  violation: "destructive",
1748
- reason: "destructive statement blocked (DROP/TRUNCATE/ALTER or unscoped UPDATE/DELETE); allow it explicitly to proceed"
2338
+ reason: "destructive statement blocked (DROP/TRUNCATE/ALTER, unscoped UPDATE/DELETE, or unconditional matched MERGE DELETE); allow it explicitly to proceed"
1749
2339
  };
1750
2340
  }
1751
2341
  return {
@@ -1785,6 +2375,57 @@ function boundedRows(rows, requestedLimit) {
1785
2375
  truncated: rows.length > requestedLimit
1786
2376
  };
1787
2377
  }
2378
+ function isBooleanColumn(column) {
2379
+ return column.typeName.trim().toUpperCase() === "BOOLEAN";
2380
+ }
2381
+ function normalizeBooleanCell(value) {
2382
+ if (value === 0) {
2383
+ return false;
2384
+ }
2385
+ if (value === 1) {
2386
+ return true;
2387
+ }
2388
+ if (typeof value !== "string") {
2389
+ return value;
2390
+ }
2391
+ const normalized = value.trim().toLowerCase();
2392
+ if (normalized === "0") {
2393
+ return false;
2394
+ }
2395
+ if (normalized === "1") {
2396
+ return true;
2397
+ }
2398
+ if (normalized === "false") {
2399
+ return false;
2400
+ }
2401
+ if (normalized === "true") {
2402
+ return true;
2403
+ }
2404
+ return value;
2405
+ }
2406
+ function normalizeBooleanRow(row, columns) {
2407
+ let normalizedRow;
2408
+ for (const column of columns) {
2409
+ const value = row[column.name];
2410
+ if (value === void 0) {
2411
+ continue;
2412
+ }
2413
+ const normalizedValue = normalizeBooleanCell(value);
2414
+ if (normalizedValue === value) {
2415
+ continue;
2416
+ }
2417
+ normalizedRow ??= { ...row };
2418
+ normalizedRow[column.name] = normalizedValue;
2419
+ }
2420
+ return normalizedRow ?? row;
2421
+ }
2422
+ function normalizeBooleanRows(rows, columns) {
2423
+ const booleanColumns = columns.filter(isBooleanColumn);
2424
+ if (booleanColumns.length === 0 || rows.length === 0) {
2425
+ return rows;
2426
+ }
2427
+ return rows.map((row) => normalizeBooleanRow(row, booleanColumns));
2428
+ }
1788
2429
  async function withTimeout(work, timeoutMs, onTimeout) {
1789
2430
  let timer;
1790
2431
  const timeout = new Promise((_resolve, reject) => {
@@ -1870,7 +2511,8 @@ var Connection = class _Connection {
1870
2511
  }
1871
2512
  );
1872
2513
  const elapsedMs = Date.now() - started;
1873
- const selected = boundedRows(execResult.rows, limited.requestedLimit);
2514
+ const normalizedRows = normalizeBooleanRows(execResult.rows, execResult.columns);
2515
+ const selected = boundedRows(normalizedRows, limited.requestedLimit);
1874
2516
  return {
1875
2517
  rows: selected.rows,
1876
2518
  columns: execResult.columns,
@@ -2095,18 +2737,37 @@ function nextExplainStatementName() {
2095
2737
  explainStatementCounter = (explainStatementCounter + 1) % Number.MAX_SAFE_INTEGER;
2096
2738
  return `cf_hana_${String(process.pid)}_${String(Date.now())}_${String(explainStatementCounter)}`;
2097
2739
  }
2740
+ function toPoolOptions(options) {
2741
+ if (options.pool === false) {
2742
+ return { max: 1 };
2743
+ }
2744
+ return options.pool ?? {};
2745
+ }
2746
+ function toSelectedBindingInfo(bindings, selected) {
2747
+ const availableBindingNames = bindings.flatMap(
2748
+ (binding) => binding.name === void 0 ? [] : [binding.name]
2749
+ );
2750
+ return {
2751
+ ...selected.name === void 0 ? {} : { bindingName: selected.name },
2752
+ bindingIndex: bindings.indexOf(selected),
2753
+ availableBindingNames
2754
+ };
2755
+ }
2098
2756
  var HanaClient = class _HanaClient {
2099
- constructor(pool, info) {
2757
+ constructor(pool, info, databaseUser = "") {
2100
2758
  this.pool = pool;
2101
2759
  this.info = info;
2760
+ this.databaseUser = databaseUser;
2102
2761
  }
2103
2762
  pool;
2104
2763
  info;
2764
+ databaseUser;
2105
2765
  /** Open a client for a `region/org/space/app` selector (or a bare app name). */
2106
2766
  static async connect(selector, options = {}) {
2107
2767
  const resolved = await resolveAppBindings(selector, options);
2108
2768
  const role = options.role ?? "runtime";
2109
2769
  const binding = selectBinding(resolved.bindings, options);
2770
+ const bindingInfo = toSelectedBindingInfo(resolved.bindings, binding);
2110
2771
  const target = toConnectionTarget(binding, role);
2111
2772
  const driver = createDriver();
2112
2773
  const config = {
@@ -2122,7 +2783,7 @@ var HanaClient = class _HanaClient {
2122
2783
  allowDestructive: options.allowDestructive ?? false,
2123
2784
  autoLimit: options.autoLimit ?? DEFAULT_AUTO_LIMIT
2124
2785
  };
2125
- const poolOptions = options.pool === false ? { max: 1 } : options.pool ?? {};
2786
+ const poolOptions = toPoolOptions(options);
2126
2787
  const info = {
2127
2788
  selector: resolved.selector,
2128
2789
  appName: resolved.appName,
@@ -2130,9 +2791,13 @@ var HanaClient = class _HanaClient {
2130
2791
  schema: target.schema,
2131
2792
  role,
2132
2793
  driver: driver.name,
2133
- credentialSource: resolved.source
2794
+ credentialSource: resolved.source,
2795
+ selectorSource: resolved.selectorSource,
2796
+ regionConfirmed: resolved.regionConfirmed,
2797
+ selectorCanBePinned: resolved.selectorCanBePinned,
2798
+ ...bindingInfo
2134
2799
  };
2135
- return new _HanaClient(new ConnectionPool(driver, config, poolOptions), info);
2800
+ return new _HanaClient(new ConnectionPool(driver, config, poolOptions), info, target.user);
2136
2801
  }
2137
2802
  /** Run a SELECT (or any read) statement and return typed rows. */
2138
2803
  async query(sql, params, options) {
@@ -2148,7 +2813,7 @@ var HanaClient = class _HanaClient {
2148
2813
  await this.recordSqlHistory("execute", sql, resolvedParams, result);
2149
2814
  return result;
2150
2815
  }
2151
- /** Back up rows matched by an UPDATE or DELETE before the caller runs it. */
2816
+ /** Back up pre-image rows required by a supported write before the caller runs it. */
2152
2817
  async backupWriteStatement(sql, params, options) {
2153
2818
  const resolvedParams = params ?? [];
2154
2819
  const plan = buildWriteBackupPlan(sql, resolvedParams);
@@ -2329,6 +2994,7 @@ async function withConnection(selector, work, options) {
2329
2994
  }
2330
2995
  }
2331
2996
  export {
2997
+ BackupRequiredError,
2332
2998
  CfHanaError,
2333
2999
  CredentialsNotFoundError,
2334
3000
  DestructiveStatementError,