@saptools/cf-hana 0.1.4 → 0.1.6
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 -1
- package/README.md +2 -9
- package/dist/cli.js +26 -8
- package/dist/cli.js.map +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6 - 2026-06-23
|
|
4
|
+
|
|
5
|
+
- Expand fake-backed E2E coverage for complex `UPDATE` and `DELETE` backups,
|
|
6
|
+
including mixed-case keywords, comments, quoted identifiers, nested queries,
|
|
7
|
+
placeholder filtering, and unscoped writes.
|
|
8
|
+
- Verify backup SELECT failures, write failures, filesystem failures, read-only
|
|
9
|
+
mode, malformed SQL, and parameter mismatches cannot bypass backup safety.
|
|
10
|
+
- Add opt-in fake-driver statement tracing and deterministic failure injection
|
|
11
|
+
without recording parameter values.
|
|
12
|
+
|
|
13
|
+
## 0.1.5 - 2026-06-23
|
|
14
|
+
|
|
15
|
+
- Remove the CLI `--no-backup` opt-out so `cf-hana query` always attempts a
|
|
16
|
+
local backup before running `UPDATE` or `DELETE`.
|
|
17
|
+
- Keep backup paths on stderr and keep stdout parseable for table, JSON, and CSV
|
|
18
|
+
output.
|
|
19
|
+
|
|
3
20
|
## 0.1.4 - 2026-06-23
|
|
4
21
|
|
|
5
22
|
- Add automatic local CSV backups before CLI `query` runs an `UPDATE` or
|
|
@@ -7,7 +24,7 @@
|
|
|
7
24
|
- Derive the backup `SELECT` from the write target and top-level `WHERE`
|
|
8
25
|
clause, preserving only the WHERE parameters for `UPDATE` statements.
|
|
9
26
|
- Save each backup in its own non-expiring folder with `statement.sql` and
|
|
10
|
-
`backup.csv
|
|
27
|
+
`backup.csv`.
|
|
11
28
|
|
|
12
29
|
## 0.1.3 - 2026-06-23
|
|
13
30
|
|
package/README.md
CHANGED
|
@@ -88,8 +88,8 @@ cf-hana info <selector> Print the resolved connection metada
|
|
|
88
88
|
Common options: `--format <table|json|csv>`, `--refresh`, `--role <runtime|hdi>`,
|
|
89
89
|
`--binding <name>` / `--binding-index <n>`, `--timeout <ms>`, `--read-only`,
|
|
90
90
|
`--allow-destructive`, `--limit <n>`, `--no-auto-limit`. The `query` command also
|
|
91
|
-
accepts `--param <value>` (repeatable) to bind `?` placeholders
|
|
92
|
-
|
|
91
|
+
accepts `--param <value>` (repeatable) to bind `?` placeholders. CLI `UPDATE`
|
|
92
|
+
and `DELETE` statements are backed up automatically before the write runs.
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
95
|
cf-hana query eu10/example-org/space-demo/app-demo "SELECT ID, STATUS FROM ORDERS WHERE STATUS = ?" \
|
|
@@ -169,13 +169,6 @@ rows returned by the derived `SELECT`. Backup folders are not deleted by
|
|
|
169
169
|
`cf-hana`; clean them up manually when they are no longer needed. The backup path
|
|
170
170
|
is printed to stderr so JSON and CSV stdout remain parseable.
|
|
171
171
|
|
|
172
|
-
Use `--no-backup` when an external backup or transaction workflow already covers
|
|
173
|
-
the write:
|
|
174
|
-
|
|
175
|
-
```bash
|
|
176
|
-
cf-hana query app-demo "DELETE FROM ORDERS WHERE ID = ?" --param 42 --no-backup
|
|
177
|
-
```
|
|
178
|
-
|
|
179
172
|
## Safety
|
|
180
173
|
|
|
181
174
|
- **Read-only mode** (`readOnly` / `--read-only`) rejects every DML and DDL statement.
|
package/dist/cli.js
CHANGED
|
@@ -514,7 +514,7 @@ async function listColumns(connection, schema, table) {
|
|
|
514
514
|
|
|
515
515
|
// src/config.ts
|
|
516
516
|
var CLI_NAME = "cf-hana";
|
|
517
|
-
var CLI_VERSION = "0.1.
|
|
517
|
+
var CLI_VERSION = "0.1.6";
|
|
518
518
|
var ENV_PREFIX = "CF_HANA";
|
|
519
519
|
var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
520
520
|
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
@@ -633,7 +633,13 @@ function toConnectionTarget(binding, role) {
|
|
|
633
633
|
}
|
|
634
634
|
|
|
635
635
|
// src/driver/fake.ts
|
|
636
|
+
import { appendFile } from "fs/promises";
|
|
636
637
|
function fakeExec(sql) {
|
|
638
|
+
const kind = classifyStatement(sql);
|
|
639
|
+
const forcedFailure = readEnv(envName("FAKE_FAIL_STATEMENT"))?.toLowerCase();
|
|
640
|
+
if (forcedFailure === kind) {
|
|
641
|
+
throw new Error(`fake driver forced ${kind.toUpperCase()} failure`);
|
|
642
|
+
}
|
|
637
643
|
if (sql.toUpperCase().includes("DUMMY")) {
|
|
638
644
|
return {
|
|
639
645
|
rows: [{ "1": 1 }],
|
|
@@ -641,7 +647,6 @@ function fakeExec(sql) {
|
|
|
641
647
|
affectedRows: 0
|
|
642
648
|
};
|
|
643
649
|
}
|
|
644
|
-
const kind = classifyStatement(sql);
|
|
645
650
|
if (kind === "select") {
|
|
646
651
|
return {
|
|
647
652
|
rows: [
|
|
@@ -660,10 +665,23 @@ function fakeExec(sql) {
|
|
|
660
665
|
}
|
|
661
666
|
return { rows: [], columns: [], affectedRows: 0 };
|
|
662
667
|
}
|
|
668
|
+
async function traceFakeExec(sql, params) {
|
|
669
|
+
const tracePath = readEnv(envName("FAKE_TRACE_FILE"));
|
|
670
|
+
if (tracePath === void 0) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const entry = { sql, paramCount: params.length };
|
|
674
|
+
await appendFile(tracePath, `${JSON.stringify(entry)}
|
|
675
|
+
`, {
|
|
676
|
+
encoding: "utf8",
|
|
677
|
+
mode: 384
|
|
678
|
+
});
|
|
679
|
+
}
|
|
663
680
|
var FakeConnection = class {
|
|
664
681
|
closed = false;
|
|
665
|
-
exec(sql,
|
|
666
|
-
|
|
682
|
+
async exec(sql, params) {
|
|
683
|
+
await traceFakeExec(sql, params);
|
|
684
|
+
return fakeExec(sql);
|
|
667
685
|
}
|
|
668
686
|
setAutoCommit(_enabled) {
|
|
669
687
|
return Promise.resolve();
|
|
@@ -937,7 +955,7 @@ function createDriver(name) {
|
|
|
937
955
|
}
|
|
938
956
|
|
|
939
957
|
// src/history.ts
|
|
940
|
-
import { appendFile, mkdir as mkdir2, readdir, rm } from "fs/promises";
|
|
958
|
+
import { appendFile as appendFile2, mkdir as mkdir2, readdir, rm } from "fs/promises";
|
|
941
959
|
import { homedir as homedir2 } from "os";
|
|
942
960
|
import { join as join2 } from "path";
|
|
943
961
|
var SAPTOOLS_DIR_NAME2 = ".saptools";
|
|
@@ -999,7 +1017,7 @@ async function appendSqlHistory(input, options = {}) {
|
|
|
999
1017
|
...input
|
|
1000
1018
|
};
|
|
1001
1019
|
await mkdir2(historyDir, { recursive: true, mode: 448 });
|
|
1002
|
-
await
|
|
1020
|
+
await appendFile2(sqlHistoryFilePath(now, options.saptoolsRoot), `${JSON.stringify(entry)}
|
|
1003
1021
|
`, {
|
|
1004
1022
|
encoding: "utf8",
|
|
1005
1023
|
mode: 384
|
|
@@ -1780,7 +1798,7 @@ async function runQuery(selector, sql, command) {
|
|
|
1780
1798
|
const client = await connect(selector, toConnectOptions(opts));
|
|
1781
1799
|
try {
|
|
1782
1800
|
const params = opts.param ?? [];
|
|
1783
|
-
const backup =
|
|
1801
|
+
const backup = await client.backupWriteStatement(sql, params);
|
|
1784
1802
|
if (backup !== void 0) {
|
|
1785
1803
|
process.stderr.write(`${CLI_NAME}: backup saved to ${backup.directory}
|
|
1786
1804
|
`);
|
|
@@ -1861,7 +1879,7 @@ function buildProgram() {
|
|
|
1861
1879
|
const program = new Command();
|
|
1862
1880
|
program.name(CLI_NAME).description("Run SQL against SAP HANA Cloud databases bound to a Cloud Foundry app").version(CLI_VERSION);
|
|
1863
1881
|
withConnectionOptions(
|
|
1864
|
-
program.command("query <selector> <sql>").description("run a single SQL statement").option("--param <value>", "bind a SQL parameter (repeatable)", collectParam, [])
|
|
1882
|
+
program.command("query <selector> <sql>").description("run a single SQL statement").option("--param <value>", "bind a SQL parameter (repeatable)", collectParam, [])
|
|
1865
1883
|
).action(async (selector, sql, _options, command) => {
|
|
1866
1884
|
await runQuery(selector, sql, command);
|
|
1867
1885
|
});
|