@saptools/cf-hana 0.1.6 → 0.2.1

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/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { formatCurrentCfAppSelector, readCurrentCfTarget } from "@saptools/cf-sync";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // src/backup.ts
@@ -52,6 +53,59 @@ function errorMessage(error) {
52
53
  return String(error);
53
54
  }
54
55
 
56
+ // src/result-preview.ts
57
+ function normalizePreviewChar(value) {
58
+ return value === "\r" || value === "\n" || value === " " ? " " : value;
59
+ }
60
+ function previewText(value, limit) {
61
+ let originalLength = 0;
62
+ let text = "";
63
+ for (const char of value) {
64
+ if (originalLength < limit) {
65
+ text += normalizePreviewChar(char);
66
+ }
67
+ originalLength += 1;
68
+ }
69
+ return {
70
+ text,
71
+ truncated: originalLength > limit,
72
+ originalLength,
73
+ unit: "chars"
74
+ };
75
+ }
76
+ function previewBuffer(value, limit) {
77
+ const fullLength = 2 + value.length * 2;
78
+ const visibleBytes = Math.max(0, Math.floor((limit - 2) / 2));
79
+ const hex = value.subarray(0, visibleBytes).toString("hex");
80
+ return {
81
+ text: `0x${hex}`.slice(0, limit),
82
+ truncated: fullLength > limit,
83
+ originalLength: value.length,
84
+ unit: "bytes"
85
+ };
86
+ }
87
+ function scalarText(value) {
88
+ if (value instanceof Date) {
89
+ return value.toISOString();
90
+ }
91
+ if (typeof value === "boolean") {
92
+ return value ? "true" : "false";
93
+ }
94
+ return value.toString();
95
+ }
96
+ function previewCell(value, limit) {
97
+ if (value === null) {
98
+ return { text: "", truncated: false, originalLength: 0, unit: "chars" };
99
+ }
100
+ if (Buffer.isBuffer(value)) {
101
+ return previewBuffer(value, limit);
102
+ }
103
+ if (typeof value === "string") {
104
+ return previewText(value, limit);
105
+ }
106
+ return previewText(scalarText(value), limit);
107
+ }
108
+
55
109
  // src/format.ts
56
110
  function cellText(value, nullText) {
57
111
  if (value === null) {
@@ -126,6 +180,22 @@ function formatCsv(result) {
126
180
  }
127
181
  return lines.join("\r\n");
128
182
  }
183
+ function formatCompactCsv(result, cellLimit) {
184
+ const headers = result.columns.map((column) => column.name);
185
+ const lines = [headers.map((header) => csvEscape(header)).join(",")];
186
+ let truncatedCells = 0;
187
+ for (const row of result.rows) {
188
+ const cells = result.columns.map((column) => {
189
+ const preview = previewCell(row[column.name] ?? null, cellLimit);
190
+ if (preview.truncated) {
191
+ truncatedCells += 1;
192
+ }
193
+ return csvEscape(preview.text);
194
+ });
195
+ lines.push(cells.join(","));
196
+ }
197
+ return { text: lines.join("\r\n"), truncatedCells };
198
+ }
129
199
  function formatResult(result, format) {
130
200
  switch (format) {
131
201
  case "table":
@@ -481,14 +551,17 @@ function buildDelete(schema, table, where) {
481
551
  // src/catalog.ts
482
552
  async function listSchemas(connection) {
483
553
  const result = await connection.query(
484
- "SELECT SCHEMA_NAME FROM SYS.SCHEMAS ORDER BY SCHEMA_NAME"
554
+ "SELECT SCHEMA_NAME FROM SYS.SCHEMAS ORDER BY SCHEMA_NAME",
555
+ [],
556
+ { autoLimit: false }
485
557
  );
486
558
  return result.rows.map((row) => row.SCHEMA_NAME);
487
559
  }
488
560
  async function listTables(connection, schema) {
489
561
  const result = await connection.query(
490
562
  "SELECT SCHEMA_NAME, TABLE_NAME, TABLE_TYPE FROM SYS.TABLES WHERE SCHEMA_NAME = ? ORDER BY TABLE_NAME",
491
- [schema]
563
+ [schema],
564
+ { autoLimit: false }
492
565
  );
493
566
  return result.rows.map((row) => ({
494
567
  schema: row.SCHEMA_NAME,
@@ -500,7 +573,8 @@ async function listTables(connection, schema) {
500
573
  async function listColumns(connection, schema, table) {
501
574
  const result = await connection.query(
502
575
  "SELECT COLUMN_NAME, DATA_TYPE_NAME, LENGTH, SCALE, IS_NULLABLE, POSITION FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME = ? AND TABLE_NAME = ? ORDER BY POSITION",
503
- [schema, table]
576
+ [schema, table],
577
+ { autoLimit: false }
504
578
  );
505
579
  return result.rows.map((row) => ({
506
580
  name: row.COLUMN_NAME,
@@ -514,13 +588,18 @@ async function listColumns(connection, schema, table) {
514
588
 
515
589
  // src/config.ts
516
590
  var CLI_NAME = "cf-hana";
517
- var CLI_VERSION = "0.1.6";
591
+ var CLI_VERSION = "0.2.1";
518
592
  var ENV_PREFIX = "CF_HANA";
519
593
  var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
520
594
  var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
521
595
  var DEFAULT_POOL_MAX = 4;
522
596
  var DEFAULT_POOL_IDLE_MS = 6e4;
523
- var DEFAULT_AUTO_LIMIT = 1e3;
597
+ var DEFAULT_AUTO_LIMIT = 100;
598
+ var DEFAULT_CELL_LIMIT = 128;
599
+ var MAX_CELL_LIMIT = 1e4;
600
+ var DEFAULT_RESULT_TTL_MINUTES = 60;
601
+ var DEFAULT_RESULT_SEARCH_LIMIT = 20;
602
+ var MAX_RESULT_STORE_BYTES = 256 * 1024 * 1024;
524
603
  function envName(suffix) {
525
604
  return `${ENV_PREFIX}_${suffix}`;
526
605
  }
@@ -1174,10 +1253,28 @@ function applyAutoLimit(sql, limit) {
1174
1253
  if (/\blimit\b/i.test(stripped) || /\btop\s+\d/i.test(stripped)) {
1175
1254
  return { sql, applied: false };
1176
1255
  }
1177
- return { sql: appendLimit(sql, limit), applied: true };
1256
+ return {
1257
+ sql: appendLimit(sql, limit + 1),
1258
+ applied: true,
1259
+ requestedLimit: limit
1260
+ };
1178
1261
  }
1179
1262
 
1180
1263
  // src/connection.ts
1264
+ function assertValidAutoLimit(value) {
1265
+ if (value !== false && (!Number.isSafeInteger(value) || value <= 0 || value >= Number.MAX_SAFE_INTEGER)) {
1266
+ throw new CfHanaError("CONFIG", "autoLimit must be a positive safe integer");
1267
+ }
1268
+ }
1269
+ function boundedRows(rows, requestedLimit) {
1270
+ if (requestedLimit === void 0) {
1271
+ return { rows, truncated: false };
1272
+ }
1273
+ return {
1274
+ rows: rows.slice(0, requestedLimit),
1275
+ truncated: rows.length > requestedLimit
1276
+ };
1277
+ }
1181
1278
  async function withTimeout(work, timeoutMs, onTimeout) {
1182
1279
  let timer;
1183
1280
  const timeout = new Promise((_resolve, reject) => {
@@ -1251,6 +1348,7 @@ var Connection = class _Connection {
1251
1348
  }
1252
1349
  const kind = classifyStatement(sql);
1253
1350
  const autoLimit = options.autoLimit ?? this.config.autoLimit;
1351
+ assertValidAutoLimit(autoLimit);
1254
1352
  const limited = kind === "select" ? applyAutoLimit(sql, autoLimit) : { sql, applied: false };
1255
1353
  const timeoutMs = options.timeoutMs ?? this.config.queryTimeoutMs;
1256
1354
  const started = Date.now();
@@ -1262,12 +1360,13 @@ var Connection = class _Connection {
1262
1360
  }
1263
1361
  );
1264
1362
  const elapsedMs = Date.now() - started;
1363
+ const selected = boundedRows(execResult.rows, limited.requestedLimit);
1265
1364
  return {
1266
- rows: execResult.rows,
1365
+ rows: selected.rows,
1267
1366
  columns: execResult.columns,
1268
- rowCount: kind === "select" ? execResult.rows.length : execResult.affectedRows,
1367
+ rowCount: kind === "select" ? selected.rows.length : execResult.affectedRows,
1269
1368
  statement: kind,
1270
- truncated: limited.applied && execResult.rows.length === autoLimit,
1369
+ truncated: selected.truncated,
1271
1370
  elapsedMs
1272
1371
  };
1273
1372
  }
@@ -1699,17 +1798,711 @@ async function connect(selector, options) {
1699
1798
  return await HanaClient.connect(selector, options);
1700
1799
  }
1701
1800
 
1702
- // src/cli.ts
1801
+ // src/cli-results.ts
1802
+ import { writeFile as writeFile3 } from "fs/promises";
1803
+
1804
+ // src/result-inspect.ts
1805
+ function assertPositiveInteger(name, value) {
1806
+ if (!Number.isSafeInteger(value) || value <= 0) {
1807
+ throw new CfHanaError(
1808
+ "CONFIG",
1809
+ `${name} ${String(value)} must be a positive safe integer`
1810
+ );
1811
+ }
1812
+ }
1813
+ function selectResultRow(session, rowNumber) {
1814
+ assertPositiveInteger("row", rowNumber);
1815
+ const row = session.result.rows[rowNumber - 1];
1816
+ if (row === void 0) {
1817
+ throw new CfHanaError("QUERY", `Saved result row ${String(rowNumber)} not found`);
1818
+ }
1819
+ return row;
1820
+ }
1821
+ function selectResultCell(session, rowNumber, columnName) {
1822
+ const row = selectResultRow(session, rowNumber);
1823
+ const column = session.result.columns.find((item) => item.name === columnName);
1824
+ if (column === void 0) {
1825
+ throw new CfHanaError("QUERY", `Saved result column "${columnName}" not found`);
1826
+ }
1827
+ return {
1828
+ row: rowNumber,
1829
+ column: column.name,
1830
+ typeName: column.typeName,
1831
+ value: row[column.name] ?? null
1832
+ };
1833
+ }
1834
+ function textRange(value, offset, length) {
1835
+ let index = 0;
1836
+ let selected = "";
1837
+ for (const char of value) {
1838
+ if (index >= offset && index < offset + length) {
1839
+ selected += char;
1840
+ }
1841
+ index += 1;
1842
+ }
1843
+ return { type: "text", originalLength: index, offset, value: selected };
1844
+ }
1845
+ function readCellWindow(value, offset, length) {
1846
+ if (!Number.isSafeInteger(offset) || offset < 0) {
1847
+ throw new CfHanaError("CONFIG", "offset must be a non-negative safe integer");
1848
+ }
1849
+ assertPositiveInteger("length", length);
1850
+ if (Buffer.isBuffer(value)) {
1851
+ return {
1852
+ type: "binary",
1853
+ originalLength: value.length,
1854
+ offset,
1855
+ value: `0x${value.subarray(offset, offset + length).toString("hex")}`
1856
+ };
1857
+ }
1858
+ if (typeof value === "string") {
1859
+ return textRange(value, offset, length);
1860
+ }
1861
+ const text = previewCell(value, Number.MAX_SAFE_INTEGER).text;
1862
+ const window = textRange(text, offset, length);
1863
+ return { ...window, type: "scalar" };
1864
+ }
1865
+ function decodePointerToken(token) {
1866
+ if (/~(?:[^01]|$)/.test(token)) {
1867
+ throw new CfHanaError("CONFIG", "Invalid JSON Pointer escape");
1868
+ }
1869
+ return token.replaceAll("~1", "/").replaceAll("~0", "~");
1870
+ }
1871
+ function pointerTokens(pointer) {
1872
+ if (pointer === "") {
1873
+ return [];
1874
+ }
1875
+ if (!pointer.startsWith("/")) {
1876
+ throw new CfHanaError("CONFIG", "JSON Pointer must be empty or start with /");
1877
+ }
1878
+ return pointer.slice(1).split("/").map(decodePointerToken);
1879
+ }
1880
+ function resolvePointer(root, pointer) {
1881
+ let current = root;
1882
+ for (const token of pointerTokens(pointer)) {
1883
+ if (Array.isArray(current)) {
1884
+ const index = Number(token);
1885
+ if (!Number.isSafeInteger(index) || index < 0 || index >= current.length) {
1886
+ throw new CfHanaError("QUERY", `JSON Pointer path "${pointer}" not found`);
1887
+ }
1888
+ current = current[index];
1889
+ continue;
1890
+ }
1891
+ if (typeof current === "object" && current !== null && token in current) {
1892
+ current = current[token];
1893
+ continue;
1894
+ }
1895
+ throw new CfHanaError("QUERY", `JSON Pointer path "${pointer}" not found`);
1896
+ }
1897
+ return current;
1898
+ }
1899
+ function escapePointerToken(token) {
1900
+ return token.replaceAll("~", "~0").replaceAll("/", "~1");
1901
+ }
1902
+ function jsonType(value) {
1903
+ if (value === null) {
1904
+ return "null";
1905
+ }
1906
+ if (Array.isArray(value)) {
1907
+ return "array";
1908
+ }
1909
+ return typeof value;
1910
+ }
1911
+ function jsonValueText(value, limit) {
1912
+ if (Array.isArray(value)) {
1913
+ return `items=${String(value.length)}`;
1914
+ }
1915
+ if (typeof value === "object" && value !== null) {
1916
+ return `keys=${String(Object.keys(value).length)}`;
1917
+ }
1918
+ if (typeof value === "string") {
1919
+ return previewCell(value, limit).text;
1920
+ }
1921
+ if (value === null) {
1922
+ return "null";
1923
+ }
1924
+ if (typeof value === "number" || typeof value === "boolean") {
1925
+ return String(value);
1926
+ }
1927
+ return "";
1928
+ }
1929
+ function jsonSearchText(value) {
1930
+ if (typeof value === "string") {
1931
+ return value;
1932
+ }
1933
+ if (value === null) {
1934
+ return "null";
1935
+ }
1936
+ if (typeof value === "number" || typeof value === "boolean") {
1937
+ return String(value);
1938
+ }
1939
+ return "";
1940
+ }
1941
+ function inspectionRow(path, value, limit) {
1942
+ return { path, type: jsonType(value), value: jsonValueText(value, limit) };
1943
+ }
1944
+ function inspectJsonCell(value, pointer, limit) {
1945
+ if (typeof value !== "string") {
1946
+ throw new CfHanaError("QUERY", "JSON Pointer requires a text cell");
1947
+ }
1948
+ let parsed;
1949
+ try {
1950
+ parsed = JSON.parse(value);
1951
+ } catch (error) {
1952
+ throw new CfHanaError("QUERY", "Saved result cell does not contain valid JSON", {
1953
+ cause: error
1954
+ });
1955
+ }
1956
+ const selected = resolvePointer(parsed, pointer);
1957
+ if (Array.isArray(selected)) {
1958
+ return selected.map(
1959
+ (item, index) => inspectionRow(`${pointer}/${String(index)}`, item, limit)
1960
+ );
1961
+ }
1962
+ if (typeof selected === "object" && selected !== null) {
1963
+ return Object.entries(selected).map(
1964
+ ([key, item]) => inspectionRow(`${pointer}/${escapePointerToken(key)}`, item, limit)
1965
+ );
1966
+ }
1967
+ return [inspectionRow(pointer, selected, limit)];
1968
+ }
1969
+ function plainTextMatches(text, term, row, column, limit, previewLength) {
1970
+ const matches = [];
1971
+ const lowerText = text.toLowerCase();
1972
+ let offset = lowerText.indexOf(term);
1973
+ while (offset >= 0 && matches.length < limit) {
1974
+ const start = Math.max(0, offset - 32);
1975
+ matches.push({
1976
+ row,
1977
+ column,
1978
+ offset,
1979
+ path: "",
1980
+ preview: textRange(text, start, previewLength).value.replaceAll(/[\r\n\t]/g, " ")
1981
+ });
1982
+ offset = lowerText.indexOf(term, offset + Math.max(1, term.length));
1983
+ }
1984
+ return matches;
1985
+ }
1986
+ function jsonSearchMatches(root, term, row, column, limit, previewLength) {
1987
+ const matches = [];
1988
+ const stack = [
1989
+ { path: "", value: root }
1990
+ ];
1991
+ while (stack.length > 0 && matches.length < limit) {
1992
+ const entry = stack.pop();
1993
+ if (entry === void 0) {
1994
+ break;
1995
+ }
1996
+ if (typeof entry.value === "object" && entry.value !== null) {
1997
+ const entries = Array.isArray(entry.value) ? entry.value.map((item, index) => [String(index), item]) : Object.entries(entry.value);
1998
+ for (const [key, value] of entries.reverse()) {
1999
+ const path = `${entry.path}/${escapePointerToken(key)}`;
2000
+ if (key.toLowerCase().includes(term)) {
2001
+ matches.push({ row, column, path, preview: jsonValueText(value, previewLength) });
2002
+ if (matches.length >= limit) {
2003
+ break;
2004
+ }
2005
+ }
2006
+ stack.push({ path, value });
2007
+ }
2008
+ continue;
2009
+ }
2010
+ const text = jsonSearchText(entry.value);
2011
+ if (text.toLowerCase().includes(term)) {
2012
+ matches.push({
2013
+ row,
2014
+ column,
2015
+ path: entry.path,
2016
+ preview: previewCell(text, previewLength).text
2017
+ });
2018
+ }
2019
+ }
2020
+ return matches;
2021
+ }
2022
+ function searchCell(value, term, row, column, limit, previewLength) {
2023
+ if (typeof value !== "string") {
2024
+ return [];
2025
+ }
2026
+ try {
2027
+ const parsed = JSON.parse(value);
2028
+ if (typeof parsed === "object" && parsed !== null) {
2029
+ return jsonSearchMatches(parsed, term, row, column, limit, previewLength);
2030
+ }
2031
+ } catch {
2032
+ }
2033
+ return plainTextMatches(value, term, row, column, limit, previewLength);
2034
+ }
2035
+ function searchResultSession(session, searchTerm, options) {
2036
+ assertPositiveInteger("limit", options.limit);
2037
+ const term = searchTerm.trim().toLowerCase();
2038
+ if (term.length === 0) {
2039
+ throw new CfHanaError("CONFIG", "search text must not be empty");
2040
+ }
2041
+ const previewLength = options.previewLength ?? 128;
2042
+ assertPositiveInteger("preview length", previewLength);
2043
+ const rows = options.row === void 0 ? session.result.rows.map((row, index) => ({ row, rowNumber: index + 1 })) : [{ row: selectResultRow(session, options.row), rowNumber: options.row }];
2044
+ const columns = options.column === void 0 ? session.result.columns : [selectResultCell(session, rows[0]?.rowNumber ?? 1, options.column)].map((item) => ({ name: item.column, typeName: item.typeName }));
2045
+ const matches = [];
2046
+ for (const item of rows) {
2047
+ for (const column of columns) {
2048
+ const remaining = options.limit - matches.length;
2049
+ matches.push(
2050
+ ...searchCell(
2051
+ item.row[column.name] ?? null,
2052
+ term,
2053
+ item.rowNumber,
2054
+ column.name,
2055
+ remaining,
2056
+ previewLength
2057
+ )
2058
+ );
2059
+ if (matches.length >= options.limit) {
2060
+ return matches;
2061
+ }
2062
+ }
2063
+ }
2064
+ return matches;
2065
+ }
2066
+
2067
+ // src/result-store.ts
2068
+ import { randomBytes } from "crypto";
2069
+ import { mkdir as mkdir3, readFile, readdir as readdir2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2070
+ import { homedir as homedir3 } from "os";
2071
+ import { join as join3 } from "path";
2072
+ var RESULT_REF_PATTERN = /^q[0-9a-f]{8}$/;
2073
+ var MANIFEST_FILE_NAME = "manifest.json";
2074
+ function resultsRoot(saptoolsRoot) {
2075
+ return join3(saptoolsRoot ?? join3(homedir3(), ".saptools"), "cf-hana", "results");
2076
+ }
2077
+ function sessionDirectory(ref, saptoolsRoot) {
2078
+ return join3(resultsRoot(saptoolsRoot), ref);
2079
+ }
2080
+ function manifestPath(ref, saptoolsRoot) {
2081
+ return join3(sessionDirectory(ref, saptoolsRoot), MANIFEST_FILE_NAME);
2082
+ }
2083
+ function encodeCell(value) {
2084
+ if (value === null) {
2085
+ return { kind: "null" };
2086
+ }
2087
+ if (Buffer.isBuffer(value)) {
2088
+ return { kind: "buffer", value: value.toString("base64") };
2089
+ }
2090
+ if (value instanceof Date) {
2091
+ return { kind: "date", value: value.toISOString() };
2092
+ }
2093
+ if (typeof value === "string") {
2094
+ return { kind: "string", value };
2095
+ }
2096
+ if (typeof value === "number") {
2097
+ return { kind: "number", value };
2098
+ }
2099
+ return { kind: "boolean", value };
2100
+ }
2101
+ function decodeCell(cell) {
2102
+ switch (cell.kind) {
2103
+ case "null":
2104
+ return null;
2105
+ case "buffer":
2106
+ return Buffer.from(cell.value, "base64");
2107
+ case "date":
2108
+ return new Date(cell.value);
2109
+ case "string":
2110
+ return cell.value;
2111
+ case "number":
2112
+ return cell.value;
2113
+ case "boolean":
2114
+ return cell.value;
2115
+ }
2116
+ }
2117
+ function assertUniqueColumns(columns) {
2118
+ const names = /* @__PURE__ */ new Set();
2119
+ for (const column of columns) {
2120
+ if (names.has(column.name)) {
2121
+ throw new CfHanaError(
2122
+ "CONFIG",
2123
+ `Saved results require unique SQL aliases; duplicate column "${column.name}"`
2124
+ );
2125
+ }
2126
+ names.add(column.name);
2127
+ }
2128
+ }
2129
+ function encodeResult(result) {
2130
+ assertUniqueColumns(result.columns);
2131
+ return {
2132
+ columns: result.columns,
2133
+ rows: result.rows.map(
2134
+ (row) => result.columns.map((column) => encodeCell(row[column.name] ?? null))
2135
+ ),
2136
+ rowCount: result.rowCount,
2137
+ statement: result.statement,
2138
+ truncated: result.truncated,
2139
+ elapsedMs: result.elapsedMs
2140
+ };
2141
+ }
2142
+ function decodeResult(result) {
2143
+ const rows = result.rows.map((cells) => {
2144
+ const row = {};
2145
+ let index = 0;
2146
+ for (const column of result.columns) {
2147
+ row[column.name] = decodeCell(cells[index] ?? { kind: "null" });
2148
+ index += 1;
2149
+ }
2150
+ return row;
2151
+ });
2152
+ return { ...result, rows };
2153
+ }
2154
+ function resolveRef(value) {
2155
+ const ref = value ?? `q${randomBytes(4).toString("hex")}`;
2156
+ if (!RESULT_REF_PATTERN.test(ref)) {
2157
+ throw new CfHanaError("CONFIG", "Invalid saved result ref");
2158
+ }
2159
+ return ref;
2160
+ }
2161
+ function resolveTtl(value) {
2162
+ const ttl = value ?? DEFAULT_RESULT_TTL_MINUTES;
2163
+ if (!Number.isSafeInteger(ttl) || ttl <= 0) {
2164
+ throw new CfHanaError("CONFIG", "Result TTL must be a positive safe integer");
2165
+ }
2166
+ return ttl;
2167
+ }
2168
+ function toStoredSession(input, ref, now) {
2169
+ const ttlMinutes = resolveTtl(input.ttlMinutes);
2170
+ return {
2171
+ version: 1,
2172
+ ref,
2173
+ createdAt: now.toISOString(),
2174
+ expiresAt: new Date(now.getTime() + ttlMinutes * 6e4).toISOString(),
2175
+ ttlMinutes,
2176
+ info: input.info,
2177
+ result: encodeResult(input.result)
2178
+ };
2179
+ }
2180
+ function isRecord(value) {
2181
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2182
+ }
2183
+ function isStoredSession(value) {
2184
+ if (!isRecord(value) || !isRecord(value["result"]) || !isRecord(value["info"])) {
2185
+ return false;
2186
+ }
2187
+ const result = value["result"];
2188
+ return value["version"] === 1 && typeof value["ref"] === "string" && typeof value["createdAt"] === "string" && typeof value["expiresAt"] === "string" && typeof value["ttlMinutes"] === "number" && Array.isArray(result["columns"]) && Array.isArray(result["rows"]);
2189
+ }
2190
+ async function readStoredSession(path) {
2191
+ try {
2192
+ const parsed = JSON.parse(await readFile(path, "utf8"));
2193
+ return isStoredSession(parsed) ? parsed : void 0;
2194
+ } catch (error) {
2195
+ if (error.code === "ENOENT") {
2196
+ return void 0;
2197
+ }
2198
+ return void 0;
2199
+ }
2200
+ }
2201
+ function toSession(stored, saptoolsRoot) {
2202
+ return {
2203
+ ...stored,
2204
+ result: decodeResult(stored.result),
2205
+ directory: sessionDirectory(stored.ref, saptoolsRoot),
2206
+ path: manifestPath(stored.ref, saptoolsRoot)
2207
+ };
2208
+ }
2209
+ async function listSessionRefs(saptoolsRoot) {
2210
+ try {
2211
+ const entries = await readdir2(resultsRoot(saptoolsRoot), { withFileTypes: true });
2212
+ return entries.filter((entry) => entry.isDirectory() && RESULT_REF_PATTERN.test(entry.name)).map((entry) => entry.name);
2213
+ } catch (error) {
2214
+ if (error.code === "ENOENT") {
2215
+ return [];
2216
+ }
2217
+ throw error;
2218
+ }
2219
+ }
2220
+ async function createResultSession(input, options = {}) {
2221
+ await pruneResultSessions(options);
2222
+ const ref = resolveRef(options.ref);
2223
+ const stored = toStoredSession(input, ref, options.now?.() ?? /* @__PURE__ */ new Date());
2224
+ const serialized = `${JSON.stringify(stored)}
2225
+ `;
2226
+ if (Buffer.byteLength(serialized) > (options.maxBytes ?? MAX_RESULT_STORE_BYTES)) {
2227
+ throw new CfHanaError("CONFIG", "Saved result exceeds the storage limit");
2228
+ }
2229
+ const root = resultsRoot(options.saptoolsRoot);
2230
+ const finalDirectory = sessionDirectory(ref, options.saptoolsRoot);
2231
+ const tempDirectory = `${finalDirectory}.tmp-${process.pid.toString()}`;
2232
+ await mkdir3(root, { recursive: true, mode: 448 });
2233
+ await rm2(tempDirectory, { recursive: true, force: true });
2234
+ await mkdir3(tempDirectory, { mode: 448 });
2235
+ try {
2236
+ await writeFile2(join3(tempDirectory, MANIFEST_FILE_NAME), serialized, {
2237
+ encoding: "utf8",
2238
+ mode: 384
2239
+ });
2240
+ await rename(tempDirectory, finalDirectory);
2241
+ } catch (error) {
2242
+ await rm2(tempDirectory, { recursive: true, force: true });
2243
+ throw error;
2244
+ }
2245
+ return toSession(stored, options.saptoolsRoot);
2246
+ }
2247
+ async function readResultSession(ref, options = {}) {
2248
+ const resolvedRef = resolveRef(ref);
2249
+ await pruneResultSessions(options);
2250
+ const stored = await readStoredSession(manifestPath(resolvedRef, options.saptoolsRoot));
2251
+ if (stored === void 0) {
2252
+ throw new CfHanaError("QUERY", "Saved result not found or expired");
2253
+ }
2254
+ return toSession(stored, options.saptoolsRoot);
2255
+ }
2256
+ async function listResultSessions(options = {}) {
2257
+ await pruneResultSessions(options);
2258
+ const refs = await listSessionRefs(options.saptoolsRoot);
2259
+ const stored = await Promise.all(
2260
+ refs.map(async (ref) => await readStoredSession(manifestPath(ref, options.saptoolsRoot)))
2261
+ );
2262
+ return stored.filter((item) => item !== void 0).map((item) => ({
2263
+ ref: item.ref,
2264
+ createdAt: item.createdAt,
2265
+ expiresAt: item.expiresAt,
2266
+ rowCount: item.result.rowCount,
2267
+ columnCount: item.result.columns.length,
2268
+ truncated: item.result.truncated
2269
+ })).sort((left, right) => left.ref.localeCompare(right.ref));
2270
+ }
2271
+ async function pruneResultSessions(options = {}) {
2272
+ const refs = await listSessionRefs(options.saptoolsRoot);
2273
+ const now = (options.now?.() ?? /* @__PURE__ */ new Date()).getTime();
2274
+ let removed = 0;
2275
+ for (const ref of refs) {
2276
+ const stored = await readStoredSession(manifestPath(ref, options.saptoolsRoot));
2277
+ if (stored === void 0 || Date.parse(stored.expiresAt) <= now) {
2278
+ await rm2(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2279
+ removed += 1;
2280
+ }
2281
+ }
2282
+ return removed;
2283
+ }
2284
+ async function clearResultSessions(options = {}) {
2285
+ const refs = await listSessionRefs(options.saptoolsRoot);
2286
+ await Promise.all(
2287
+ refs.map(async (ref) => {
2288
+ await rm2(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2289
+ })
2290
+ );
2291
+ return refs.length;
2292
+ }
2293
+
2294
+ // src/cli-results.ts
1703
2295
  function print(text) {
1704
2296
  process.stdout.write(`${text}
1705
2297
  `);
1706
2298
  }
2299
+ function parseIntOption(value) {
2300
+ const trimmed = value.trim();
2301
+ if (!/^-?\d+$/.test(trimmed)) {
2302
+ throw new CfHanaError("CONFIG", `Expected an integer but received "${value}"`);
2303
+ }
2304
+ const parsed = Number(trimmed);
2305
+ if (!Number.isSafeInteger(parsed)) {
2306
+ throw new CfHanaError("CONFIG", `Expected a safe integer but received "${value}"`);
2307
+ }
2308
+ return parsed;
2309
+ }
2310
+ function positive(name, value, fallback) {
2311
+ const resolved = value ?? fallback;
2312
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
2313
+ throw new CfHanaError("CONFIG", `${name} must be a positive safe integer`);
2314
+ }
2315
+ return resolved;
2316
+ }
2317
+ function boundedLength(value) {
2318
+ const resolved = positive("--length", value, DEFAULT_CELL_LIMIT);
2319
+ if (resolved > MAX_CELL_LIMIT) {
2320
+ throw new CfHanaError("CONFIG", `--length must be at most ${String(MAX_CELL_LIMIT)}`);
2321
+ }
2322
+ return resolved;
2323
+ }
2324
+ function nonNegative(name, value) {
2325
+ const resolved = value ?? 0;
2326
+ if (!Number.isSafeInteger(resolved) || resolved < 0) {
2327
+ throw new CfHanaError("CONFIG", `${name} must be a non-negative safe integer`);
2328
+ }
2329
+ return resolved;
2330
+ }
2331
+ function resultFromRows(rows, columns) {
2332
+ return {
2333
+ rows,
2334
+ columns,
2335
+ rowCount: rows.length,
2336
+ statement: "select",
2337
+ truncated: false,
2338
+ elapsedMs: 0
2339
+ };
2340
+ }
2341
+ function csv(rows, columns, limit) {
2342
+ const result = resultFromRows(
2343
+ rows,
2344
+ columns.map((name) => ({ name, typeName: "" }))
2345
+ );
2346
+ return formatCompactCsv(result, limit).text;
2347
+ }
2348
+ async function runShow(ref, options) {
2349
+ const session = await readResultSession(ref);
2350
+ const length = boundedLength(options.length);
2351
+ if (options.row === void 0) {
2352
+ if (options.column !== void 0 || options.path !== void 0 || options.offset !== void 0) {
2353
+ throw new CfHanaError("CONFIG", "--column, --path, and --offset require --row");
2354
+ }
2355
+ print(summaryCsv(session, length));
2356
+ return;
2357
+ }
2358
+ if (options.column === void 0) {
2359
+ if (options.path !== void 0 || options.offset !== void 0) {
2360
+ throw new CfHanaError("CONFIG", "--path and --offset require --column");
2361
+ }
2362
+ const row = selectResultRow(session, options.row);
2363
+ print(formatCompactCsv(resultFromRows([row], session.result.columns), length).text);
2364
+ return;
2365
+ }
2366
+ const selected = selectResultCell(session, options.row, options.column);
2367
+ if (options.path !== void 0) {
2368
+ const rows = inspectJsonCell(selected.value, options.path, length).map((row) => ({
2369
+ PATH: row.path,
2370
+ TYPE: row.type,
2371
+ VALUE: row.value
2372
+ }));
2373
+ print(csv(rows, ["PATH", "TYPE", "VALUE"], length));
2374
+ return;
2375
+ }
2376
+ const window = readCellWindow(selected.value, nonNegative("--offset", options.offset), length);
2377
+ print(
2378
+ csv(
2379
+ [
2380
+ {
2381
+ ROW: selected.row,
2382
+ COLUMN: selected.column,
2383
+ TYPE: window.type,
2384
+ ORIGINAL_LENGTH: window.originalLength,
2385
+ OFFSET: window.offset,
2386
+ VALUE: window.value
2387
+ }
2388
+ ],
2389
+ ["ROW", "COLUMN", "TYPE", "ORIGINAL_LENGTH", "OFFSET", "VALUE"],
2390
+ length
2391
+ )
2392
+ );
2393
+ }
2394
+ function summaryCsv(session, length) {
2395
+ const compact = formatCompactCsv(session.result, DEFAULT_CELL_LIMIT);
2396
+ return csv(
2397
+ [
2398
+ {
2399
+ REF: session.ref,
2400
+ ROWS: session.result.rowCount,
2401
+ COLUMNS: session.result.columns.length,
2402
+ ROW_TRUNCATED: session.result.truncated,
2403
+ TRUNCATED_CELLS: compact.truncatedCells,
2404
+ EXPIRES_AT: session.expiresAt
2405
+ }
2406
+ ],
2407
+ ["REF", "ROWS", "COLUMNS", "ROW_TRUNCATED", "TRUNCATED_CELLS", "EXPIRES_AT"],
2408
+ length
2409
+ );
2410
+ }
2411
+ async function runSearch(ref, text, options) {
2412
+ const session = await readResultSession(ref);
2413
+ const length = boundedLength(options.length);
2414
+ const matches = searchResultSession(session, text, {
2415
+ limit: positive("--limit", options.limit, DEFAULT_RESULT_SEARCH_LIMIT),
2416
+ previewLength: length,
2417
+ ...options.row === void 0 ? {} : { row: options.row },
2418
+ ...options.column === void 0 ? {} : { column: options.column }
2419
+ });
2420
+ const rows = matches.map((match) => ({
2421
+ ROW: match.row,
2422
+ COLUMN: match.column,
2423
+ OFFSET: match.offset ?? null,
2424
+ PATH: match.path,
2425
+ PREVIEW: match.preview
2426
+ }));
2427
+ print(csv(rows, ["ROW", "COLUMN", "OFFSET", "PATH", "PREVIEW"], length));
2428
+ }
2429
+ function cellExportValue(value) {
2430
+ if (Buffer.isBuffer(value)) {
2431
+ return value;
2432
+ }
2433
+ if (value instanceof Date) {
2434
+ return value.toISOString();
2435
+ }
2436
+ if (value === null) {
2437
+ return "";
2438
+ }
2439
+ if (typeof value === "boolean") {
2440
+ return value ? "true" : "false";
2441
+ }
2442
+ return value.toString();
2443
+ }
2444
+ async function runExport(ref, options) {
2445
+ if (options.row === void 0 || options.column === void 0) {
2446
+ throw new CfHanaError("CONFIG", "result export requires --row and --column");
2447
+ }
2448
+ if (options.output === void 0 || options.output.trim().length === 0) {
2449
+ throw new CfHanaError("CONFIG", "result export requires --output");
2450
+ }
2451
+ const session = await readResultSession(ref);
2452
+ const selected = selectResultCell(session, options.row, options.column);
2453
+ await writeFile3(options.output, cellExportValue(selected.value), { mode: 384 });
2454
+ print(`wrote=${options.output}`);
2455
+ }
2456
+ async function runList() {
2457
+ const summaries = await listResultSessions();
2458
+ const rows = summaries.map((summary) => ({
2459
+ REF: summary.ref,
2460
+ ROWS: summary.rowCount,
2461
+ COLUMNS: summary.columnCount,
2462
+ ROW_TRUNCATED: summary.truncated,
2463
+ EXPIRES_AT: summary.expiresAt
2464
+ }));
2465
+ print(csv(rows, ["REF", "ROWS", "COLUMNS", "ROW_TRUNCATED", "EXPIRES_AT"], DEFAULT_CELL_LIMIT));
2466
+ }
2467
+ async function runPrune() {
2468
+ print(`removed=${String(await pruneResultSessions())}`);
2469
+ }
2470
+ async function runClear() {
2471
+ print(`removed=${String(await clearResultSessions())}`);
2472
+ }
2473
+ function registerResultCommands(program) {
2474
+ const result = program.command("result").description("inspect saved query refs");
2475
+ result.command("show <ref>").description("show a saved result, row, cell, or JSON path").option("--row <n>", "one-based result row", parseIntOption).option("--column <name>", "exact column name").option("--offset <n>", "text code-point or binary byte offset", parseIntOption).option("--length <n>", "maximum characters or bytes to print", parseIntOption).option("--path <pointer>", "JSON Pointer inside a saved text cell").action(async (ref, _options, command) => {
2476
+ await runShow(ref, command.opts());
2477
+ });
2478
+ result.command("search <ref> <text>").description("search saved text and JSON values").option("--row <n>", "one-based result row", parseIntOption).option("--column <name>", "exact column name").option("--limit <n>", "maximum matches to print", parseIntOption).option("--length <n>", "maximum preview characters", parseIntOption).action(async (ref, text, _options, command) => {
2479
+ await runSearch(ref, text, command.opts());
2480
+ });
2481
+ result.command("export <ref>").description("write one exact saved cell to a file").requiredOption("--output <path>", "output file path").option("--row <n>", "one-based result row", parseIntOption).option("--column <name>", "exact column name").action(async (ref, _options, command) => {
2482
+ await runExport(ref, command.opts());
2483
+ });
2484
+ result.command("list").description("list active saved refs").action(async () => {
2485
+ await runList();
2486
+ });
2487
+ result.command("prune").description("remove expired saved refs").action(async () => {
2488
+ await runPrune();
2489
+ });
2490
+ result.command("clear").description("remove all saved refs").action(async () => {
2491
+ await runClear();
2492
+ });
2493
+ }
2494
+
2495
+ // src/cli.ts
2496
+ function print2(text) {
2497
+ process.stdout.write(`${text}
2498
+ `);
2499
+ }
1707
2500
  function fail(message) {
1708
2501
  process.stderr.write(`${CLI_NAME}: ${message}
1709
2502
  `);
1710
2503
  process.exit(1);
1711
2504
  }
1712
- function parseIntOption(value) {
2505
+ function parseIntOption2(value) {
1713
2506
  const trimmed = value.trim();
1714
2507
  if (!/^-?\d+$/.test(trimmed)) {
1715
2508
  throw new CfHanaError("CONFIG", `Expected an integer but received "${value}"`);
@@ -1752,6 +2545,25 @@ function parseQualifiedName(value) {
1752
2545
  }
1753
2546
  return { schema: value.slice(0, dot), table: value.slice(dot + 1) };
1754
2547
  }
2548
+ async function resolveSelectorArgument(selector) {
2549
+ if (selector.includes("/")) {
2550
+ return selector;
2551
+ }
2552
+ const current = await readCurrentCfTarget().catch((error) => {
2553
+ throw new CfHanaError(
2554
+ "CONFIG",
2555
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector.",
2556
+ { cause: error }
2557
+ );
2558
+ });
2559
+ if (current === void 0) {
2560
+ throw new CfHanaError(
2561
+ "CONFIG",
2562
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
2563
+ );
2564
+ }
2565
+ return formatCurrentCfAppSelector(current, selector);
2566
+ }
1755
2567
  function toConnectOptions(opts) {
1756
2568
  assertPositiveOption("--limit", opts.limit);
1757
2569
  assertPositiveOption("--timeout", opts.timeout);
@@ -1767,6 +2579,21 @@ function toConnectOptions(opts) {
1767
2579
  ...opts.timeout === void 0 ? {} : { queryTimeoutMs: opts.timeout, connectTimeoutMs: opts.timeout }
1768
2580
  };
1769
2581
  }
2582
+ function resolveCellLimit(value) {
2583
+ const limit = value ?? DEFAULT_CELL_LIMIT;
2584
+ assertPositiveOption("--cell-limit", limit);
2585
+ if (limit > MAX_CELL_LIMIT) {
2586
+ throw new CfHanaError("CONFIG", `--cell-limit must be at most ${String(MAX_CELL_LIMIT)}`);
2587
+ }
2588
+ return limit;
2589
+ }
2590
+ function assertQueryOptions(sql, opts) {
2591
+ resolveCellLimit(opts.cellLimit);
2592
+ assertPositiveOption("--result-ttl-minutes", opts.resultTtlMinutes);
2593
+ if (opts.save && classifyStatement(sql) !== "select") {
2594
+ throw new CfHanaError("CONFIG", "--save is only available for SELECT/WITH statements");
2595
+ }
2596
+ }
1770
2597
  function rowsToResult(rows) {
1771
2598
  const first = rows[0];
1772
2599
  const columns = first === void 0 ? [] : Object.keys(first).map((name) => ({ name, typeName: "" }));
@@ -1791,11 +2618,20 @@ function formatInfo(info) {
1791
2618
  ].join("\n");
1792
2619
  }
1793
2620
  function withConnectionOptions(command) {
1794
- return command.option("--format <format>", "output format: table, json, or csv", "table").option("--refresh", "bypass cached credentials and fetch them live", false).option("--role <role>", "HANA user role: runtime or hdi", "runtime").option("--binding <name>", "select a HANA binding by service name").option("--binding-index <n>", "select a HANA binding by index", parseIntOption).option("--read-only", "block every DML and DDL statement", false).option("--allow-destructive", "permit destructive statements", false).option("--timeout <ms>", "connection and query timeout in milliseconds", parseIntOption).option("--limit <n>", "row cap auto-applied to bare SELECT statements", parseIntOption).option("--no-auto-limit", "disable the automatic SELECT row cap");
2621
+ return command.option("--refresh", "bypass cached credentials and fetch them live", false).option("--role <role>", "HANA user role: runtime or hdi", "runtime").option("--binding <name>", "select a HANA binding by service name").option("--binding-index <n>", "select a HANA binding by index", parseIntOption2).option("--read-only", "block every DML and DDL statement", false).option("--allow-destructive", "permit destructive statements", false).option("--timeout <ms>", "connection and query timeout in milliseconds", parseIntOption2).option("--limit <n>", "row cap auto-applied to bare SELECT statements", parseIntOption2).option("--no-auto-limit", "disable the automatic SELECT row cap");
2622
+ }
2623
+ function withFormattedConnectionOptions(command) {
2624
+ return withConnectionOptions(command).option(
2625
+ "--format <format>",
2626
+ "output format: table, json, or csv",
2627
+ "table"
2628
+ );
1795
2629
  }
1796
2630
  async function runQuery(selector, sql, command) {
1797
2631
  const opts = command.opts();
1798
- const client = await connect(selector, toConnectOptions(opts));
2632
+ assertQueryOptions(sql, opts);
2633
+ const cellLimit = resolveCellLimit(opts.cellLimit);
2634
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
1799
2635
  try {
1800
2636
  const params = opts.param ?? [];
1801
2637
  const backup = await client.backupWriteStatement(sql, params);
@@ -1804,14 +2640,39 @@ async function runQuery(selector, sql, command) {
1804
2640
  `);
1805
2641
  }
1806
2642
  const result = await client.query(sql, params);
1807
- print(formatResult(result, parseFormat(opts.format)));
2643
+ if (result.statement === "select") {
2644
+ const compact = formatCompactCsv(result, cellLimit);
2645
+ if (opts.save) {
2646
+ const session = await createResultSession({
2647
+ result,
2648
+ info: client.info,
2649
+ ...opts.resultTtlMinutes === void 0 ? {} : { ttlMinutes: opts.resultTtlMinutes }
2650
+ });
2651
+ print2(`ref=${session.ref}`);
2652
+ process.stderr.write(`${CLI_NAME}: saved result expires at ${session.expiresAt}
2653
+ `);
2654
+ }
2655
+ print2(compact.text);
2656
+ if (result.truncated) {
2657
+ process.stderr.write(`${CLI_NAME}: row limit reached; rerun with --limit for more rows
2658
+ `);
2659
+ }
2660
+ if (compact.truncatedCells > 0) {
2661
+ process.stderr.write(
2662
+ `${CLI_NAME}: compacted ${String(compact.truncatedCells)} cell(s); use --save to inspect exact values by ref
2663
+ `
2664
+ );
2665
+ }
2666
+ return;
2667
+ }
2668
+ print2(formatTable(result));
1808
2669
  } finally {
1809
2670
  await client.close();
1810
2671
  }
1811
2672
  }
1812
2673
  async function runTables(selector, schema, command) {
1813
2674
  const opts = command.opts();
1814
- const client = await connect(selector, toConnectOptions(opts));
2675
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
1815
2676
  try {
1816
2677
  const tables = await client.listTables(schema ?? client.info.schema);
1817
2678
  const rows = tables.map((table) => ({
@@ -1819,7 +2680,7 @@ async function runTables(selector, schema, command) {
1819
2680
  TABLE: table.name,
1820
2681
  TYPE: table.type
1821
2682
  }));
1822
- print(formatResult(rowsToResult(rows), parseFormat(opts.format)));
2683
+ print2(formatResult(rowsToResult(rows), parseFormat(opts.format)));
1823
2684
  } finally {
1824
2685
  await client.close();
1825
2686
  }
@@ -1827,7 +2688,7 @@ async function runTables(selector, schema, command) {
1827
2688
  async function runColumns(selector, target, command) {
1828
2689
  const opts = command.opts();
1829
2690
  const { schema, table } = parseQualifiedName(target);
1830
- const client = await connect(selector, toConnectOptions(opts));
2691
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
1831
2692
  try {
1832
2693
  const columns = await client.listColumns(schema, table);
1833
2694
  const rows = columns.map((column) => ({
@@ -1837,7 +2698,7 @@ async function runColumns(selector, target, command) {
1837
2698
  NULLABLE: column.nullable,
1838
2699
  POSITION: column.position
1839
2700
  }));
1840
- print(formatResult(rowsToResult(rows), parseFormat(opts.format)));
2701
+ print2(formatResult(rowsToResult(rows), parseFormat(opts.format)));
1841
2702
  } finally {
1842
2703
  await client.close();
1843
2704
  }
@@ -1845,21 +2706,21 @@ async function runColumns(selector, target, command) {
1845
2706
  async function runCount(selector, target, command) {
1846
2707
  const opts = command.opts();
1847
2708
  const { schema, table } = parseQualifiedName(target);
1848
- const client = await connect(selector, toConnectOptions(opts));
2709
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
1849
2710
  try {
1850
2711
  const total = await client.count({ schema, table });
1851
- print(String(total));
2712
+ print2(String(total));
1852
2713
  } finally {
1853
2714
  await client.close();
1854
2715
  }
1855
2716
  }
1856
2717
  async function runPing(selector, command) {
1857
2718
  const opts = command.opts();
1858
- const client = await connect(selector, toConnectOptions(opts));
2719
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
1859
2720
  try {
1860
2721
  const started = Date.now();
1861
2722
  await client.query("SELECT 1 FROM DUMMY");
1862
- print(
2723
+ print2(
1863
2724
  `OK ${client.info.host} schema=${client.info.schema} ${String(Date.now() - started)}ms`
1864
2725
  );
1865
2726
  } finally {
@@ -1868,9 +2729,9 @@ async function runPing(selector, command) {
1868
2729
  }
1869
2730
  async function runInfo(selector, command) {
1870
2731
  const opts = command.opts();
1871
- const client = await connect(selector, toConnectOptions(opts));
2732
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
1872
2733
  try {
1873
- print(formatInfo(client.info));
2734
+ print2(formatInfo(client.info));
1874
2735
  } finally {
1875
2736
  await client.close();
1876
2737
  }
@@ -1879,18 +2740,22 @@ function buildProgram() {
1879
2740
  const program = new Command();
1880
2741
  program.name(CLI_NAME).description("Run SQL against SAP HANA Cloud databases bound to a Cloud Foundry app").version(CLI_VERSION);
1881
2742
  withConnectionOptions(
1882
- program.command("query <selector> <sql>").description("run a single SQL statement").option("--param <value>", "bind a SQL parameter (repeatable)", collectParam, [])
2743
+ program.command("query <selector> <sql>").description("run a single SQL statement").option("--param <value>", "bind a SQL parameter (repeatable)", collectParam, []).option("--save", "save exact returned rows for follow-up inspection", false).option("--cell-limit <n>", "maximum visible characters per data cell", parseIntOption2).option(
2744
+ "--result-ttl-minutes <n>",
2745
+ "minutes before a saved result expires",
2746
+ parseIntOption2
2747
+ )
1883
2748
  ).action(async (selector, sql, _options, command) => {
1884
2749
  await runQuery(selector, sql, command);
1885
2750
  });
1886
- withConnectionOptions(
2751
+ withFormattedConnectionOptions(
1887
2752
  program.command("tables <selector> [schema]").description("list tables in a schema")
1888
2753
  ).action(
1889
2754
  async (selector, schema, _options, command) => {
1890
2755
  await runTables(selector, schema, command);
1891
2756
  }
1892
2757
  );
1893
- withConnectionOptions(
2758
+ withFormattedConnectionOptions(
1894
2759
  program.command("columns <selector> <schema.table>").description("list the columns of a table")
1895
2760
  ).action(async (selector, target, _options, command) => {
1896
2761
  await runColumns(selector, target, command);
@@ -1910,6 +2775,7 @@ function buildProgram() {
1910
2775
  ).action(async (selector, _options, command) => {
1911
2776
  await runInfo(selector, command);
1912
2777
  });
2778
+ registerResultCommands(program);
1913
2779
  return program;
1914
2780
  }
1915
2781
  try {