@saptools/cf-hana 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +66 -12
- package/dist/cli.js +422 -107
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +418 -105
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,6 +510,7 @@ async function listColumns(connection, schema, table) {
|
|
|
243
510
|
}
|
|
244
511
|
|
|
245
512
|
// src/config.ts
|
|
513
|
+
var CLI_VERSION = "0.1.4";
|
|
246
514
|
var ENV_PREFIX = "CF_HANA";
|
|
247
515
|
var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
248
516
|
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
@@ -664,10 +932,85 @@ function createDriver(name) {
|
|
|
664
932
|
}
|
|
665
933
|
}
|
|
666
934
|
|
|
935
|
+
// src/history.ts
|
|
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";
|
|
941
|
+
var HISTORIES_DIR_NAME = "histories";
|
|
942
|
+
var HISTORY_RETENTION_DAYS = 5;
|
|
943
|
+
var HISTORY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
944
|
+
function defaultSaptoolsRoot2() {
|
|
945
|
+
return join2(homedir2(), SAPTOOLS_DIR_NAME2);
|
|
946
|
+
}
|
|
947
|
+
function padDatePart(value) {
|
|
948
|
+
return value.toString().padStart(2, "0");
|
|
949
|
+
}
|
|
950
|
+
function dateKey(date) {
|
|
951
|
+
return [
|
|
952
|
+
date.getFullYear().toString(),
|
|
953
|
+
padDatePart(date.getMonth() + 1),
|
|
954
|
+
padDatePart(date.getDate())
|
|
955
|
+
].join("-");
|
|
956
|
+
}
|
|
957
|
+
function retentionCutoffKey(now) {
|
|
958
|
+
const cutoff = new Date(now);
|
|
959
|
+
cutoff.setHours(0, 0, 0, 0);
|
|
960
|
+
cutoff.setDate(cutoff.getDate() - HISTORY_RETENTION_DAYS);
|
|
961
|
+
return dateKey(cutoff);
|
|
962
|
+
}
|
|
963
|
+
function resolveSaptoolsRoot(root) {
|
|
964
|
+
return root ?? defaultSaptoolsRoot2();
|
|
965
|
+
}
|
|
966
|
+
function cfHanaHistoryDirectory(saptoolsRoot) {
|
|
967
|
+
return join2(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME2, HISTORIES_DIR_NAME);
|
|
968
|
+
}
|
|
969
|
+
function sqlHistoryFilePath(now = /* @__PURE__ */ new Date(), saptoolsRoot) {
|
|
970
|
+
return join2(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
|
|
971
|
+
}
|
|
972
|
+
async function pruneSqlHistory(now, saptoolsRoot) {
|
|
973
|
+
const historyDir = cfHanaHistoryDirectory(saptoolsRoot);
|
|
974
|
+
let files;
|
|
975
|
+
try {
|
|
976
|
+
files = await readdir(historyDir);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
if (error.code === "ENOENT") {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
throw error;
|
|
982
|
+
}
|
|
983
|
+
const cutoffKey = retentionCutoffKey(now);
|
|
984
|
+
await Promise.all(
|
|
985
|
+
files.filter((file) => HISTORY_FILE_PATTERN.test(file)).filter((file) => file.slice(0, 10) < cutoffKey).map(async (file) => {
|
|
986
|
+
await rm(join2(historyDir, file), { force: true });
|
|
987
|
+
})
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
async function appendSqlHistory(input, options = {}) {
|
|
991
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
992
|
+
const historyDir = cfHanaHistoryDirectory(options.saptoolsRoot);
|
|
993
|
+
const entry = {
|
|
994
|
+
at: now.toISOString(),
|
|
995
|
+
...input
|
|
996
|
+
};
|
|
997
|
+
await mkdir2(historyDir, { recursive: true, mode: 448 });
|
|
998
|
+
await appendFile(sqlHistoryFilePath(now, options.saptoolsRoot), `${JSON.stringify(entry)}
|
|
999
|
+
`, {
|
|
1000
|
+
encoding: "utf8",
|
|
1001
|
+
mode: 384
|
|
1002
|
+
});
|
|
1003
|
+
try {
|
|
1004
|
+
await pruneSqlHistory(now, options.saptoolsRoot);
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
return entry;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
667
1010
|
// src/safety.ts
|
|
668
1011
|
var DESTRUCTIVE_DDL_KEYWORDS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "ALTER"]);
|
|
669
1012
|
var UNSCOPED_WRITE_KEYWORDS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
|
|
670
|
-
function
|
|
1013
|
+
function skipQuotedText2(sql, start) {
|
|
671
1014
|
const quote = sql[start];
|
|
672
1015
|
let index = start + 1;
|
|
673
1016
|
while (index < sql.length) {
|
|
@@ -683,14 +1026,14 @@ function skipQuotedText(sql, start) {
|
|
|
683
1026
|
}
|
|
684
1027
|
return index;
|
|
685
1028
|
}
|
|
686
|
-
function
|
|
1029
|
+
function skipLineComment2(sql, start) {
|
|
687
1030
|
let index = start + 2;
|
|
688
1031
|
while (index < sql.length && sql[index] !== "\n") {
|
|
689
1032
|
index += 1;
|
|
690
1033
|
}
|
|
691
1034
|
return index;
|
|
692
1035
|
}
|
|
693
|
-
function
|
|
1036
|
+
function skipBlockComment2(sql, start) {
|
|
694
1037
|
let index = start + 2;
|
|
695
1038
|
while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) {
|
|
696
1039
|
index += 1;
|
|
@@ -703,19 +1046,19 @@ function maskIgnoredSqlText(sql) {
|
|
|
703
1046
|
while (index < sql.length) {
|
|
704
1047
|
const char = sql[index];
|
|
705
1048
|
if (char === "'" || char === '"') {
|
|
706
|
-
const end =
|
|
1049
|
+
const end = skipQuotedText2(sql, index);
|
|
707
1050
|
masked += " ".repeat(end - index);
|
|
708
1051
|
index = end;
|
|
709
1052
|
continue;
|
|
710
1053
|
}
|
|
711
1054
|
if (char === "-" && sql[index + 1] === "-") {
|
|
712
|
-
const end =
|
|
1055
|
+
const end = skipLineComment2(sql, index);
|
|
713
1056
|
masked += " ".repeat(end - index);
|
|
714
1057
|
index = end;
|
|
715
1058
|
continue;
|
|
716
1059
|
}
|
|
717
1060
|
if (char === "/" && sql[index + 1] === "*") {
|
|
718
|
-
const end =
|
|
1061
|
+
const end = skipBlockComment2(sql, index);
|
|
719
1062
|
masked += " ".repeat(end - index);
|
|
720
1063
|
index = end;
|
|
721
1064
|
continue;
|
|
@@ -733,11 +1076,11 @@ function trailingLineCommentIndex(sql) {
|
|
|
733
1076
|
while (index < sql.length) {
|
|
734
1077
|
const char = sql[index];
|
|
735
1078
|
if (char === "'" || char === '"') {
|
|
736
|
-
index =
|
|
1079
|
+
index = skipQuotedText2(sql, index);
|
|
737
1080
|
continue;
|
|
738
1081
|
}
|
|
739
1082
|
if (char === "/" && sql[index + 1] === "*") {
|
|
740
|
-
index =
|
|
1083
|
+
index = skipBlockComment2(sql, index);
|
|
741
1084
|
continue;
|
|
742
1085
|
}
|
|
743
1086
|
if (char === "-" && sql[index + 1] === "-") {
|
|
@@ -1162,41 +1505,64 @@ var HanaClient = class _HanaClient {
|
|
|
1162
1505
|
}
|
|
1163
1506
|
/** Run a SELECT (or any read) statement and return typed rows. */
|
|
1164
1507
|
async query(sql, params, options) {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
);
|
|
1508
|
+
const resolvedParams = params ?? [];
|
|
1509
|
+
const result = await this.runQuery(sql, resolvedParams, options);
|
|
1510
|
+
await this.recordSqlHistory("query", sql, resolvedParams, result);
|
|
1511
|
+
return result;
|
|
1168
1512
|
}
|
|
1169
1513
|
/** Run a DML/DDL statement and return its affected-row count. */
|
|
1170
1514
|
async execute(sql, params, options) {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
);
|
|
1515
|
+
const resolvedParams = params ?? [];
|
|
1516
|
+
const result = await this.runExecute(sql, resolvedParams, options);
|
|
1517
|
+
await this.recordSqlHistory("execute", sql, resolvedParams, result);
|
|
1518
|
+
return result;
|
|
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
|
+
});
|
|
1174
1540
|
}
|
|
1175
1541
|
/** Run a typed `SELECT` built from a spec. */
|
|
1176
1542
|
async selectFrom(spec) {
|
|
1177
1543
|
const built = buildSelect(spec);
|
|
1178
|
-
return await this.
|
|
1544
|
+
return await this.runQuery(built.sql, built.params);
|
|
1179
1545
|
}
|
|
1180
1546
|
/** Count rows in a table, optionally filtered. */
|
|
1181
1547
|
async count(spec) {
|
|
1182
1548
|
const built = buildCount(spec);
|
|
1183
|
-
const result = await this.
|
|
1549
|
+
const result = await this.runQuery(built.sql, built.params);
|
|
1184
1550
|
return result.rows[0]?.COUNT ?? 0;
|
|
1185
1551
|
}
|
|
1186
1552
|
/** Insert a single row. */
|
|
1187
1553
|
async insertInto(schema, table, values) {
|
|
1188
1554
|
const built = buildInsert(schema, table, values);
|
|
1189
|
-
return await this.
|
|
1555
|
+
return await this.runExecute(built.sql, built.params);
|
|
1190
1556
|
}
|
|
1191
1557
|
/** Update rows matching a non-empty `where` filter. */
|
|
1192
1558
|
async update(schema, table, values, where) {
|
|
1193
1559
|
const built = buildUpdate(schema, table, values, where);
|
|
1194
|
-
return await this.
|
|
1560
|
+
return await this.runExecute(built.sql, built.params);
|
|
1195
1561
|
}
|
|
1196
1562
|
/** Delete rows matching a non-empty `where` filter. */
|
|
1197
1563
|
async deleteFrom(schema, table, where) {
|
|
1198
1564
|
const built = buildDelete(schema, table, where);
|
|
1199
|
-
return await this.
|
|
1565
|
+
return await this.runExecute(built.sql, built.params);
|
|
1200
1566
|
}
|
|
1201
1567
|
/** Run `work` inside a transaction, auto-committing on success. */
|
|
1202
1568
|
async transaction(work) {
|
|
@@ -1275,6 +1641,35 @@ var HanaClient = class _HanaClient {
|
|
|
1275
1641
|
async close() {
|
|
1276
1642
|
await this.pool.drain();
|
|
1277
1643
|
}
|
|
1644
|
+
async runQuery(sql, params, options) {
|
|
1645
|
+
return await this.pool.withConnection(
|
|
1646
|
+
(connection) => connection.query(sql, params, options)
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
async runExecute(sql, params, options) {
|
|
1650
|
+
return await this.pool.withConnection(
|
|
1651
|
+
(connection) => connection.execute(sql, params, options)
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
async recordSqlHistory(operation, sql, params, result) {
|
|
1655
|
+
try {
|
|
1656
|
+
await appendSqlHistory({
|
|
1657
|
+
version: CLI_VERSION,
|
|
1658
|
+
operation,
|
|
1659
|
+
selector: this.info.selector,
|
|
1660
|
+
appName: this.info.appName,
|
|
1661
|
+
schema: this.info.schema,
|
|
1662
|
+
role: this.info.role,
|
|
1663
|
+
statement: result.statement,
|
|
1664
|
+
sql,
|
|
1665
|
+
paramCount: params.length,
|
|
1666
|
+
rowCount: result.rowCount,
|
|
1667
|
+
truncated: result.truncated,
|
|
1668
|
+
elapsedMs: result.elapsedMs
|
|
1669
|
+
});
|
|
1670
|
+
} catch {
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1278
1673
|
};
|
|
1279
1674
|
|
|
1280
1675
|
// src/api.ts
|
|
@@ -1297,91 +1692,6 @@ async function withConnection(selector, work, options) {
|
|
|
1297
1692
|
await client.close();
|
|
1298
1693
|
}
|
|
1299
1694
|
}
|
|
1300
|
-
|
|
1301
|
-
// src/format.ts
|
|
1302
|
-
function cellText(value, nullText) {
|
|
1303
|
-
if (value === null) {
|
|
1304
|
-
return nullText;
|
|
1305
|
-
}
|
|
1306
|
-
if (value instanceof Date) {
|
|
1307
|
-
return value.toISOString();
|
|
1308
|
-
}
|
|
1309
|
-
if (Buffer.isBuffer(value)) {
|
|
1310
|
-
return `0x${value.toString("hex")}`;
|
|
1311
|
-
}
|
|
1312
|
-
if (typeof value === "boolean") {
|
|
1313
|
-
return value ? "true" : "false";
|
|
1314
|
-
}
|
|
1315
|
-
return typeof value === "number" ? value.toString() : value;
|
|
1316
|
-
}
|
|
1317
|
-
function serializeCell(value) {
|
|
1318
|
-
if (value === null) {
|
|
1319
|
-
return null;
|
|
1320
|
-
}
|
|
1321
|
-
if (value instanceof Date) {
|
|
1322
|
-
return value.toISOString();
|
|
1323
|
-
}
|
|
1324
|
-
if (Buffer.isBuffer(value)) {
|
|
1325
|
-
return `0x${value.toString("hex")}`;
|
|
1326
|
-
}
|
|
1327
|
-
return value;
|
|
1328
|
-
}
|
|
1329
|
-
function csvEscape(text) {
|
|
1330
|
-
if (/[",\r\n]/.test(text)) {
|
|
1331
|
-
return `"${text.replace(/"/g, '""')}"`;
|
|
1332
|
-
}
|
|
1333
|
-
return text;
|
|
1334
|
-
}
|
|
1335
|
-
function formatTable(result) {
|
|
1336
|
-
if (result.columns.length === 0) {
|
|
1337
|
-
return `(${String(result.rowCount)} row(s) affected)`;
|
|
1338
|
-
}
|
|
1339
|
-
const headers = result.columns.map((column) => column.name);
|
|
1340
|
-
const rows = result.rows.map(
|
|
1341
|
-
(row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL"))
|
|
1342
|
-
);
|
|
1343
|
-
const widths = headers.map((header, index) => {
|
|
1344
|
-
const widest = rows.reduce(
|
|
1345
|
-
(max, cells) => Math.max(max, (cells[index] ?? "").length),
|
|
1346
|
-
header.length
|
|
1347
|
-
);
|
|
1348
|
-
return widest;
|
|
1349
|
-
});
|
|
1350
|
-
const renderRow = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" | ");
|
|
1351
|
-
const separator = widths.map((width) => "-".repeat(width)).join("-+-");
|
|
1352
|
-
const body = rows.map((cells) => renderRow(cells));
|
|
1353
|
-
return [renderRow(headers), separator, ...body].join("\n");
|
|
1354
|
-
}
|
|
1355
|
-
function formatJson(result) {
|
|
1356
|
-
const rows = result.rows.map((row) => {
|
|
1357
|
-
const serialized = {};
|
|
1358
|
-
for (const [key, value] of Object.entries(row)) {
|
|
1359
|
-
serialized[key] = serializeCell(value);
|
|
1360
|
-
}
|
|
1361
|
-
return serialized;
|
|
1362
|
-
});
|
|
1363
|
-
return JSON.stringify(rows, null, 2);
|
|
1364
|
-
}
|
|
1365
|
-
function formatCsv(result) {
|
|
1366
|
-
const headers = result.columns.map((column) => column.name);
|
|
1367
|
-
const lines = [headers.map((header) => csvEscape(header)).join(",")];
|
|
1368
|
-
for (const row of result.rows) {
|
|
1369
|
-
lines.push(
|
|
1370
|
-
result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, ""))).join(",")
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
return lines.join("\r\n");
|
|
1374
|
-
}
|
|
1375
|
-
function formatResult(result, format) {
|
|
1376
|
-
switch (format) {
|
|
1377
|
-
case "table":
|
|
1378
|
-
return formatTable(result);
|
|
1379
|
-
case "json":
|
|
1380
|
-
return formatJson(result);
|
|
1381
|
-
case "csv":
|
|
1382
|
-
return formatCsv(result);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
1695
|
export {
|
|
1386
1696
|
CfHanaError,
|
|
1387
1697
|
CredentialsNotFoundError,
|
|
@@ -1395,6 +1705,8 @@ export {
|
|
|
1395
1705
|
buildInsert,
|
|
1396
1706
|
buildSelect,
|
|
1397
1707
|
buildUpdate,
|
|
1708
|
+
buildWriteBackupPlan,
|
|
1709
|
+
cfHanaBackupRoot,
|
|
1398
1710
|
connect,
|
|
1399
1711
|
createDriver,
|
|
1400
1712
|
errorMessage,
|
|
@@ -1403,6 +1715,7 @@ export {
|
|
|
1403
1715
|
formatResult,
|
|
1404
1716
|
formatTable,
|
|
1405
1717
|
query,
|
|
1406
|
-
withConnection
|
|
1718
|
+
withConnection,
|
|
1719
|
+
writeSqlBackup
|
|
1407
1720
|
};
|
|
1408
1721
|
//# sourceMappingURL=index.js.map
|