@saptools/cf-hana 0.1.3 → 0.1.5

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.d.ts CHANGED
@@ -96,6 +96,32 @@ interface HanaClientInfo {
96
96
  readonly credentialSource: CredentialSource;
97
97
  }
98
98
 
99
+ type WriteBackupOperation = "update" | "delete";
100
+ interface WriteBackupPlan {
101
+ readonly operation: WriteBackupOperation;
102
+ readonly statementSql: string;
103
+ readonly selectSql: string;
104
+ readonly selectParams: readonly SqlParam[];
105
+ }
106
+ interface SqlBackupWriteInput {
107
+ readonly operation: WriteBackupOperation;
108
+ readonly statementSql: string;
109
+ readonly result: QueryResult;
110
+ }
111
+ interface SqlBackupWriteOptions {
112
+ readonly now?: Date;
113
+ readonly saptoolsRoot?: string;
114
+ }
115
+ interface SqlBackupRecord {
116
+ readonly directory: string;
117
+ readonly statementPath: string;
118
+ readonly backupPath: string;
119
+ readonly rowCount: number;
120
+ }
121
+ declare function cfHanaBackupRoot(saptoolsRoot?: string): string;
122
+ declare function buildWriteBackupPlan(sql: string, params?: readonly SqlParam[]): WriteBackupPlan | undefined;
123
+ declare function writeSqlBackup(input: SqlBackupWriteInput, options?: SqlBackupWriteOptions): Promise<SqlBackupRecord>;
124
+
99
125
  interface DriverExecResult {
100
126
  readonly rows: readonly Record<string, SqlParam>[];
101
127
  readonly columns: readonly QueryResultColumn[];
@@ -219,6 +245,8 @@ declare class HanaClient {
219
245
  query<TRow = QueryRow>(sql: string, params?: readonly SqlParam[], options?: QueryOptions): Promise<QueryResult<TRow>>;
220
246
  /** Run a DML/DDL statement and return its affected-row count. */
221
247
  execute(sql: string, params?: readonly SqlParam[], options?: QueryOptions): Promise<QueryResult>;
248
+ /** Back up rows matched by an UPDATE or DELETE before the caller runs it. */
249
+ backupWriteStatement(sql: string, params?: readonly SqlParam[], options?: QueryOptions): Promise<SqlBackupRecord | undefined>;
222
250
  /** Run a typed `SELECT` built from a spec. */
223
251
  selectFrom<TRow = QueryRow>(spec: SelectSpec): Promise<QueryResult<TRow>>;
224
252
  /** Count rows in a table, optionally filtered. */
@@ -316,4 +344,4 @@ declare class DestructiveStatementError extends CfHanaError {
316
344
  /** Narrow an unknown thrown value to a human-readable message. */
317
345
  declare function errorMessage(error: unknown): string;
318
346
 
319
- export { type BuiltStatement, CfHanaError, type CfHanaErrorCode, type ColumnInfo, type ConnectOptions, type CredentialSource, CredentialsNotFoundError, type DbUserRole, DestructiveStatementError, type DriverConnectParams, type DriverConnection, type DriverExecResult, HanaClient, type HanaClientInfo, type HanaDriver, type OutputFormat, type PoolOptions, QueryError, type QueryOptions, type QueryResult, type QueryResultColumn, type QueryRow, ReadOnlyViolationError, type SelectSpec, type SqlParam, type StatementKind, type TableInfo, Transaction, buildCount, buildDelete, buildInsert, buildSelect, buildUpdate, connect, createDriver, errorMessage, formatCsv, formatJson, formatResult, formatTable, query, withConnection };
347
+ export { type BuiltStatement, CfHanaError, type CfHanaErrorCode, type ColumnInfo, type ConnectOptions, type CredentialSource, CredentialsNotFoundError, type DbUserRole, DestructiveStatementError, type DriverConnectParams, type DriverConnection, type DriverExecResult, HanaClient, type HanaClientInfo, type HanaDriver, type OutputFormat, type PoolOptions, QueryError, type QueryOptions, type QueryResult, type QueryResultColumn, type QueryRow, ReadOnlyViolationError, type SelectSpec, type SqlBackupRecord, type SqlBackupWriteInput, type SqlBackupWriteOptions, type SqlParam, type StatementKind, type TableInfo, Transaction, type WriteBackupOperation, type WriteBackupPlan, buildCount, buildDelete, buildInsert, buildSelect, buildUpdate, buildWriteBackupPlan, cfHanaBackupRoot, connect, createDriver, errorMessage, formatCsv, formatJson, formatResult, formatTable, query, withConnection, writeSqlBackup };
package/dist/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/backup.ts
4
+ import { createHash, randomUUID } from "crypto";
5
+ import { mkdir, writeFile } from "fs/promises";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+
3
9
  // src/errors.ts
4
10
  var CfHanaError = class extends Error {
5
11
  code;
@@ -43,6 +49,91 @@ function errorMessage(error) {
43
49
  return String(error);
44
50
  }
45
51
 
52
+ // src/format.ts
53
+ function cellText(value, nullText) {
54
+ if (value === null) {
55
+ return nullText;
56
+ }
57
+ if (value instanceof Date) {
58
+ return value.toISOString();
59
+ }
60
+ if (Buffer.isBuffer(value)) {
61
+ return `0x${value.toString("hex")}`;
62
+ }
63
+ if (typeof value === "boolean") {
64
+ return value ? "true" : "false";
65
+ }
66
+ return typeof value === "number" ? value.toString() : value;
67
+ }
68
+ function serializeCell(value) {
69
+ if (value === null) {
70
+ return null;
71
+ }
72
+ if (value instanceof Date) {
73
+ return value.toISOString();
74
+ }
75
+ if (Buffer.isBuffer(value)) {
76
+ return `0x${value.toString("hex")}`;
77
+ }
78
+ return value;
79
+ }
80
+ function csvEscape(text) {
81
+ if (/[",\r\n]/.test(text)) {
82
+ return `"${text.replace(/"/g, '""')}"`;
83
+ }
84
+ return text;
85
+ }
86
+ function formatTable(result) {
87
+ if (result.columns.length === 0) {
88
+ return `(${String(result.rowCount)} row(s) affected)`;
89
+ }
90
+ const headers = result.columns.map((column) => column.name);
91
+ const rows = result.rows.map(
92
+ (row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL"))
93
+ );
94
+ const widths = headers.map((header, index) => {
95
+ const widest = rows.reduce(
96
+ (max, cells) => Math.max(max, (cells[index] ?? "").length),
97
+ header.length
98
+ );
99
+ return widest;
100
+ });
101
+ const renderRow = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" | ");
102
+ const separator = widths.map((width) => "-".repeat(width)).join("-+-");
103
+ const body = rows.map((cells) => renderRow(cells));
104
+ return [renderRow(headers), separator, ...body].join("\n");
105
+ }
106
+ function formatJson(result) {
107
+ const rows = result.rows.map((row) => {
108
+ const serialized = {};
109
+ for (const [key, value] of Object.entries(row)) {
110
+ serialized[key] = serializeCell(value);
111
+ }
112
+ return serialized;
113
+ });
114
+ return JSON.stringify(rows, null, 2);
115
+ }
116
+ function formatCsv(result) {
117
+ const headers = result.columns.map((column) => column.name);
118
+ const lines = [headers.map((header) => csvEscape(header)).join(",")];
119
+ for (const row of result.rows) {
120
+ lines.push(
121
+ result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, ""))).join(",")
122
+ );
123
+ }
124
+ return lines.join("\r\n");
125
+ }
126
+ function formatResult(result, format) {
127
+ switch (format) {
128
+ case "table":
129
+ return formatTable(result);
130
+ case "json":
131
+ return formatJson(result);
132
+ case "csv":
133
+ return formatCsv(result);
134
+ }
135
+ }
136
+
46
137
  // src/statements.ts
47
138
  var LEADING_NOISE = /^(?:\s|--[^\n]*\n?|\/\*[\s\S]*?\*\/)+/;
48
139
  var SELECT_KEYWORDS = /* @__PURE__ */ new Set(["SELECT", "WITH"]);
@@ -131,6 +222,182 @@ function assertParamArity(sql, params) {
131
222
  }
132
223
  }
133
224
 
225
+ // src/backup.ts
226
+ var SAPTOOLS_DIR_NAME = ".saptools";
227
+ var CF_HANA_DIR_NAME = "cf-hana";
228
+ var BACKUPS_DIR_NAME = "backups";
229
+ function defaultSaptoolsRoot() {
230
+ return join(homedir(), SAPTOOLS_DIR_NAME);
231
+ }
232
+ function trimStatementSql(sql) {
233
+ return sql.trim().replace(/;+\s*$/, "").trim();
234
+ }
235
+ function isIdentifierChar(char) {
236
+ return char !== void 0 && /[A-Za-z0-9_$#]/.test(char);
237
+ }
238
+ function skipQuotedText(sql, index) {
239
+ const quote = sql[index];
240
+ let cursor = index + 1;
241
+ while (cursor < sql.length) {
242
+ if (sql[cursor] === quote) {
243
+ if (sql[cursor + 1] === quote) {
244
+ cursor += 2;
245
+ continue;
246
+ }
247
+ return cursor + 1;
248
+ }
249
+ cursor += 1;
250
+ }
251
+ return cursor;
252
+ }
253
+ function skipLineComment(sql, index) {
254
+ let cursor = index + 2;
255
+ while (cursor < sql.length && sql[cursor] !== "\n") {
256
+ cursor += 1;
257
+ }
258
+ return cursor;
259
+ }
260
+ function skipBlockComment(sql, index) {
261
+ let cursor = index + 2;
262
+ while (cursor < sql.length && !(sql[cursor] === "*" && sql[cursor + 1] === "/")) {
263
+ cursor += 1;
264
+ }
265
+ return Math.min(cursor + 2, sql.length);
266
+ }
267
+ function skipNonCode(sql, index) {
268
+ const char = sql[index];
269
+ if (char === "'" || char === '"') {
270
+ return skipQuotedText(sql, index);
271
+ }
272
+ if (char === "-" && sql[index + 1] === "-") {
273
+ return skipLineComment(sql, index);
274
+ }
275
+ if (char === "/" && sql[index + 1] === "*") {
276
+ return skipBlockComment(sql, index);
277
+ }
278
+ return void 0;
279
+ }
280
+ function keywordMatches(sql, index, keyword) {
281
+ const end = index + keyword.length;
282
+ return sql.slice(index, end).toUpperCase() === keyword && !isIdentifierChar(sql[index - 1]) && !isIdentifierChar(sql[end]);
283
+ }
284
+ function findTopLevelKeyword(sql, keyword, startIndex = 0) {
285
+ let index = startIndex;
286
+ let depth = 0;
287
+ while (index < sql.length) {
288
+ const skipped = skipNonCode(sql, index);
289
+ if (skipped !== void 0) {
290
+ index = skipped;
291
+ continue;
292
+ }
293
+ const char = sql[index];
294
+ if (char === "(") {
295
+ depth += 1;
296
+ } else if (char === ")" && depth > 0) {
297
+ depth -= 1;
298
+ } else if (depth === 0 && keywordMatches(sql, index, keyword)) {
299
+ return index;
300
+ }
301
+ index += 1;
302
+ }
303
+ return void 0;
304
+ }
305
+ function selectParamsAfterWhere(statementSql, whereIndex, params) {
306
+ return params.slice(countPlaceholders(statementSql.slice(0, whereIndex)));
307
+ }
308
+ function buildSelectPlan(operation, statementSql, targetSql, whereIndex, params) {
309
+ const target = targetSql.trim();
310
+ if (target.length === 0) {
311
+ throw new QueryError(`${operation.toUpperCase()} backup requires a target table`);
312
+ }
313
+ if (whereIndex === void 0) {
314
+ return { operation, statementSql, selectSql: `SELECT * FROM ${target}`, selectParams: [] };
315
+ }
316
+ const whereSql = statementSql.slice(whereIndex + "WHERE".length).trim();
317
+ if (whereSql.length === 0) {
318
+ throw new QueryError(`${operation.toUpperCase()} backup requires a non-empty WHERE clause`);
319
+ }
320
+ return {
321
+ operation,
322
+ statementSql,
323
+ selectSql: `SELECT * FROM ${target} WHERE ${whereSql}`,
324
+ selectParams: selectParamsAfterWhere(statementSql, whereIndex, params)
325
+ };
326
+ }
327
+ function buildUpdateBackupPlan(statementSql, params) {
328
+ const updateIndex = findTopLevelKeyword(statementSql, "UPDATE");
329
+ const setIndex = updateIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "SET", updateIndex + "UPDATE".length);
330
+ if (updateIndex === void 0 || setIndex === void 0) {
331
+ throw new QueryError("UPDATE backup requires UPDATE <target> SET syntax");
332
+ }
333
+ const whereIndex = findTopLevelKeyword(statementSql, "WHERE", setIndex + "SET".length);
334
+ return buildSelectPlan(
335
+ "update",
336
+ statementSql,
337
+ statementSql.slice(updateIndex + "UPDATE".length, setIndex),
338
+ whereIndex,
339
+ params
340
+ );
341
+ }
342
+ function buildDeleteBackupPlan(statementSql, params) {
343
+ const deleteIndex = findTopLevelKeyword(statementSql, "DELETE");
344
+ const fromIndex = deleteIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "FROM", deleteIndex + "DELETE".length);
345
+ if (deleteIndex === void 0 || fromIndex === void 0) {
346
+ throw new QueryError("DELETE backup requires DELETE FROM <target> syntax");
347
+ }
348
+ const whereIndex = findTopLevelKeyword(statementSql, "WHERE", fromIndex + "FROM".length);
349
+ const targetEnd = whereIndex ?? statementSql.length;
350
+ return buildSelectPlan(
351
+ "delete",
352
+ statementSql,
353
+ statementSql.slice(fromIndex + "FROM".length, targetEnd),
354
+ whereIndex,
355
+ params
356
+ );
357
+ }
358
+ function backupTimestamp(now) {
359
+ return now.toISOString().replace(/:/g, "").replace(".", "");
360
+ }
361
+ function backupHash(statementSql) {
362
+ return createHash("sha256").update(statementSql).update("\0").update(randomUUID()).digest("hex").slice(0, 12);
363
+ }
364
+ function cfHanaBackupRoot(saptoolsRoot) {
365
+ return join(saptoolsRoot ?? defaultSaptoolsRoot(), CF_HANA_DIR_NAME, BACKUPS_DIR_NAME);
366
+ }
367
+ function buildWriteBackupPlan(sql, params = []) {
368
+ const statementSql = trimStatementSql(sql);
369
+ const keyword = firstKeyword(statementSql);
370
+ if (keyword !== "UPDATE" && keyword !== "DELETE") {
371
+ return void 0;
372
+ }
373
+ assertParamArity(statementSql, params);
374
+ if (keyword === "UPDATE") {
375
+ return buildUpdateBackupPlan(statementSql, params);
376
+ }
377
+ return buildDeleteBackupPlan(statementSql, params);
378
+ }
379
+ async function writeSqlBackup(input, options = {}) {
380
+ const now = options.now ?? /* @__PURE__ */ new Date();
381
+ const directory = join(
382
+ cfHanaBackupRoot(options.saptoolsRoot),
383
+ `${backupTimestamp(now)}-${input.operation}-${backupHash(input.statementSql)}`
384
+ );
385
+ const statementPath = join(directory, "statement.sql");
386
+ const backupPath = join(directory, "backup.csv");
387
+ await mkdir(directory, { recursive: true, mode: 448 });
388
+ await Promise.all([
389
+ writeFile(statementPath, `${input.statementSql}
390
+ `, { encoding: "utf8", mode: 384 }),
391
+ writeFile(backupPath, formatCsv(input.result), { encoding: "utf8", mode: 384 })
392
+ ]);
393
+ return {
394
+ directory,
395
+ statementPath,
396
+ backupPath,
397
+ rowCount: input.result.rowCount
398
+ };
399
+ }
400
+
134
401
  // src/builder.ts
135
402
  function buildWhere(where) {
136
403
  if (where === void 0) {
@@ -243,7 +510,7 @@ async function listColumns(connection, schema, table) {
243
510
  }
244
511
 
245
512
  // src/config.ts
246
- var CLI_VERSION = "0.1.3";
513
+ var CLI_VERSION = "0.1.5";
247
514
  var ENV_PREFIX = "CF_HANA";
248
515
  var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
249
516
  var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
@@ -666,16 +933,16 @@ function createDriver(name) {
666
933
  }
667
934
 
668
935
  // 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";
936
+ import { appendFile, mkdir as mkdir2, readdir, rm } from "fs/promises";
937
+ import { homedir as homedir2 } from "os";
938
+ import { join as join2 } from "path";
939
+ var SAPTOOLS_DIR_NAME2 = ".saptools";
940
+ var CF_HANA_DIR_NAME2 = "cf-hana";
674
941
  var HISTORIES_DIR_NAME = "histories";
675
942
  var HISTORY_RETENTION_DAYS = 5;
676
943
  var HISTORY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
677
- function defaultSaptoolsRoot() {
678
- return join(homedir(), SAPTOOLS_DIR_NAME);
944
+ function defaultSaptoolsRoot2() {
945
+ return join2(homedir2(), SAPTOOLS_DIR_NAME2);
679
946
  }
680
947
  function padDatePart(value) {
681
948
  return value.toString().padStart(2, "0");
@@ -694,13 +961,13 @@ function retentionCutoffKey(now) {
694
961
  return dateKey(cutoff);
695
962
  }
696
963
  function resolveSaptoolsRoot(root) {
697
- return root ?? defaultSaptoolsRoot();
964
+ return root ?? defaultSaptoolsRoot2();
698
965
  }
699
966
  function cfHanaHistoryDirectory(saptoolsRoot) {
700
- return join(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME, HISTORIES_DIR_NAME);
967
+ return join2(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME2, HISTORIES_DIR_NAME);
701
968
  }
702
969
  function sqlHistoryFilePath(now = /* @__PURE__ */ new Date(), saptoolsRoot) {
703
- return join(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
970
+ return join2(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
704
971
  }
705
972
  async function pruneSqlHistory(now, saptoolsRoot) {
706
973
  const historyDir = cfHanaHistoryDirectory(saptoolsRoot);
@@ -716,7 +983,7 @@ async function pruneSqlHistory(now, saptoolsRoot) {
716
983
  const cutoffKey = retentionCutoffKey(now);
717
984
  await Promise.all(
718
985
  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 });
986
+ await rm(join2(historyDir, file), { force: true });
720
987
  })
721
988
  );
722
989
  }
@@ -727,7 +994,7 @@ async function appendSqlHistory(input, options = {}) {
727
994
  at: now.toISOString(),
728
995
  ...input
729
996
  };
730
- await mkdir(historyDir, { recursive: true, mode: 448 });
997
+ await mkdir2(historyDir, { recursive: true, mode: 448 });
731
998
  await appendFile(sqlHistoryFilePath(now, options.saptoolsRoot), `${JSON.stringify(entry)}
732
999
  `, {
733
1000
  encoding: "utf8",
@@ -743,7 +1010,7 @@ async function appendSqlHistory(input, options = {}) {
743
1010
  // src/safety.ts
744
1011
  var DESTRUCTIVE_DDL_KEYWORDS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "ALTER"]);
745
1012
  var UNSCOPED_WRITE_KEYWORDS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
746
- function skipQuotedText(sql, start) {
1013
+ function skipQuotedText2(sql, start) {
747
1014
  const quote = sql[start];
748
1015
  let index = start + 1;
749
1016
  while (index < sql.length) {
@@ -759,14 +1026,14 @@ function skipQuotedText(sql, start) {
759
1026
  }
760
1027
  return index;
761
1028
  }
762
- function skipLineComment(sql, start) {
1029
+ function skipLineComment2(sql, start) {
763
1030
  let index = start + 2;
764
1031
  while (index < sql.length && sql[index] !== "\n") {
765
1032
  index += 1;
766
1033
  }
767
1034
  return index;
768
1035
  }
769
- function skipBlockComment(sql, start) {
1036
+ function skipBlockComment2(sql, start) {
770
1037
  let index = start + 2;
771
1038
  while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) {
772
1039
  index += 1;
@@ -779,19 +1046,19 @@ function maskIgnoredSqlText(sql) {
779
1046
  while (index < sql.length) {
780
1047
  const char = sql[index];
781
1048
  if (char === "'" || char === '"') {
782
- const end = skipQuotedText(sql, index);
1049
+ const end = skipQuotedText2(sql, index);
783
1050
  masked += " ".repeat(end - index);
784
1051
  index = end;
785
1052
  continue;
786
1053
  }
787
1054
  if (char === "-" && sql[index + 1] === "-") {
788
- const end = skipLineComment(sql, index);
1055
+ const end = skipLineComment2(sql, index);
789
1056
  masked += " ".repeat(end - index);
790
1057
  index = end;
791
1058
  continue;
792
1059
  }
793
1060
  if (char === "/" && sql[index + 1] === "*") {
794
- const end = skipBlockComment(sql, index);
1061
+ const end = skipBlockComment2(sql, index);
795
1062
  masked += " ".repeat(end - index);
796
1063
  index = end;
797
1064
  continue;
@@ -809,11 +1076,11 @@ function trailingLineCommentIndex(sql) {
809
1076
  while (index < sql.length) {
810
1077
  const char = sql[index];
811
1078
  if (char === "'" || char === '"') {
812
- index = skipQuotedText(sql, index);
1079
+ index = skipQuotedText2(sql, index);
813
1080
  continue;
814
1081
  }
815
1082
  if (char === "/" && sql[index + 1] === "*") {
816
- index = skipBlockComment(sql, index);
1083
+ index = skipBlockComment2(sql, index);
817
1084
  continue;
818
1085
  }
819
1086
  if (char === "-" && sql[index + 1] === "-") {
@@ -1250,6 +1517,27 @@ var HanaClient = class _HanaClient {
1250
1517
  await this.recordSqlHistory("execute", sql, resolvedParams, result);
1251
1518
  return result;
1252
1519
  }
1520
+ /** Back up rows matched by an UPDATE or DELETE before the caller runs it. */
1521
+ async backupWriteStatement(sql, params, options) {
1522
+ const resolvedParams = params ?? [];
1523
+ const plan = buildWriteBackupPlan(sql, resolvedParams);
1524
+ if (plan === void 0) {
1525
+ return void 0;
1526
+ }
1527
+ return await this.pool.withConnection(async (connection) => {
1528
+ connection.assertAllowed(plan.statementSql, options);
1529
+ const backupQueryOptions = {
1530
+ autoLimit: false,
1531
+ ...options?.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
1532
+ };
1533
+ const result = await connection.query(plan.selectSql, plan.selectParams, backupQueryOptions);
1534
+ return await writeSqlBackup({
1535
+ operation: plan.operation,
1536
+ statementSql: plan.statementSql,
1537
+ result
1538
+ });
1539
+ });
1540
+ }
1253
1541
  /** Run a typed `SELECT` built from a spec. */
1254
1542
  async selectFrom(spec) {
1255
1543
  const built = buildSelect(spec);
@@ -1404,91 +1692,6 @@ async function withConnection(selector, work, options) {
1404
1692
  await client.close();
1405
1693
  }
1406
1694
  }
1407
-
1408
- // src/format.ts
1409
- function cellText(value, nullText) {
1410
- if (value === null) {
1411
- return nullText;
1412
- }
1413
- if (value instanceof Date) {
1414
- return value.toISOString();
1415
- }
1416
- if (Buffer.isBuffer(value)) {
1417
- return `0x${value.toString("hex")}`;
1418
- }
1419
- if (typeof value === "boolean") {
1420
- return value ? "true" : "false";
1421
- }
1422
- return typeof value === "number" ? value.toString() : value;
1423
- }
1424
- function serializeCell(value) {
1425
- if (value === null) {
1426
- return null;
1427
- }
1428
- if (value instanceof Date) {
1429
- return value.toISOString();
1430
- }
1431
- if (Buffer.isBuffer(value)) {
1432
- return `0x${value.toString("hex")}`;
1433
- }
1434
- return value;
1435
- }
1436
- function csvEscape(text) {
1437
- if (/[",\r\n]/.test(text)) {
1438
- return `"${text.replace(/"/g, '""')}"`;
1439
- }
1440
- return text;
1441
- }
1442
- function formatTable(result) {
1443
- if (result.columns.length === 0) {
1444
- return `(${String(result.rowCount)} row(s) affected)`;
1445
- }
1446
- const headers = result.columns.map((column) => column.name);
1447
- const rows = result.rows.map(
1448
- (row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL"))
1449
- );
1450
- const widths = headers.map((header, index) => {
1451
- const widest = rows.reduce(
1452
- (max, cells) => Math.max(max, (cells[index] ?? "").length),
1453
- header.length
1454
- );
1455
- return widest;
1456
- });
1457
- const renderRow = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" | ");
1458
- const separator = widths.map((width) => "-".repeat(width)).join("-+-");
1459
- const body = rows.map((cells) => renderRow(cells));
1460
- return [renderRow(headers), separator, ...body].join("\n");
1461
- }
1462
- function formatJson(result) {
1463
- const rows = result.rows.map((row) => {
1464
- const serialized = {};
1465
- for (const [key, value] of Object.entries(row)) {
1466
- serialized[key] = serializeCell(value);
1467
- }
1468
- return serialized;
1469
- });
1470
- return JSON.stringify(rows, null, 2);
1471
- }
1472
- function formatCsv(result) {
1473
- const headers = result.columns.map((column) => column.name);
1474
- const lines = [headers.map((header) => csvEscape(header)).join(",")];
1475
- for (const row of result.rows) {
1476
- lines.push(
1477
- result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, ""))).join(",")
1478
- );
1479
- }
1480
- return lines.join("\r\n");
1481
- }
1482
- function formatResult(result, format) {
1483
- switch (format) {
1484
- case "table":
1485
- return formatTable(result);
1486
- case "json":
1487
- return formatJson(result);
1488
- case "csv":
1489
- return formatCsv(result);
1490
- }
1491
- }
1492
1695
  export {
1493
1696
  CfHanaError,
1494
1697
  CredentialsNotFoundError,
@@ -1502,6 +1705,8 @@ export {
1502
1705
  buildInsert,
1503
1706
  buildSelect,
1504
1707
  buildUpdate,
1708
+ buildWriteBackupPlan,
1709
+ cfHanaBackupRoot,
1505
1710
  connect,
1506
1711
  createDriver,
1507
1712
  errorMessage,
@@ -1510,6 +1715,7 @@ export {
1510
1715
  formatResult,
1511
1716
  formatTable,
1512
1717
  query,
1513
- withConnection
1718
+ withConnection,
1719
+ writeSqlBackup
1514
1720
  };
1515
1721
  //# sourceMappingURL=index.js.map