@saptools/cf-hana 0.3.0 → 0.3.2

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.2 - 2026-07-01
4
+
5
+ - Remove the `cf-hana: saved result expires at...` stderr notice from `query --save` output while keeping result refs available for inspection.
6
+
7
+ ## 0.3.1 - 2026-07-01
8
+
9
+ - Add actionable HANA LOB `ORDER BY`/`GROUP BY` hints that recommend removing LOB columns or wrapping them with `TO_VARCHAR(<column>)`.
10
+ - Add invalid-column typo suggestions that inspect target table columns and print close matches to stderr.
11
+ - Increase the default saved result TTL from 60 minutes to 7 days.
12
+
3
13
  ## 0.3.0 - 2026-07-01
4
14
 
5
15
  - Add invalid table/view recovery suggestions for failed `query` statements, with nearby table and view names printed to stderr so stdout remains parseable.
package/README.md CHANGED
@@ -129,7 +129,7 @@ ID,CONTENT
129
129
  ```
130
130
 
131
131
  The ref is not a CSV column. Exact returned rows are stored under
132
- `~/.saptools/cf-hana/results/` for 60 minutes by default. Only returned rows are
132
+ `~/.saptools/cf-hana/results/` for 7 days by default. Only returned rows are
133
133
  stored; rows beyond the selected `--limit` are not fetched or saved.
134
134
 
135
135
  Follow-up commands:
package/dist/cli.js CHANGED
@@ -26,12 +26,36 @@ var CredentialsNotFoundError = class extends CfHanaError {
26
26
  };
27
27
  var QueryError = class extends CfHanaError {
28
28
  sqlState;
29
+ databaseCode;
29
30
  constructor(message, options) {
30
31
  super("QUERY", message, options);
31
32
  this.name = "QueryError";
32
33
  this.sqlState = options?.sqlState;
34
+ this.databaseCode = options?.databaseCode;
33
35
  }
34
36
  };
37
+ function isDatabaseErrorShape(error) {
38
+ return typeof error === "object" && error !== null && "code" in error;
39
+ }
40
+ function safeDatabaseCode(value) {
41
+ if (typeof value === "number" && Number.isSafeInteger(value)) {
42
+ return value;
43
+ }
44
+ if (typeof value !== "string" || !/^\d+$/.test(value)) {
45
+ return void 0;
46
+ }
47
+ const parsed = Number(value);
48
+ return Number.isSafeInteger(parsed) ? parsed : void 0;
49
+ }
50
+ function databaseCode(error) {
51
+ if (error instanceof QueryError) {
52
+ return error.databaseCode ?? databaseCode(error.cause);
53
+ }
54
+ if (isDatabaseErrorShape(error)) {
55
+ return safeDatabaseCode(error.code);
56
+ }
57
+ return void 0;
58
+ }
35
59
  var ReadOnlyViolationError = class extends CfHanaError {
36
60
  constructor(message, options) {
37
61
  super("READ_ONLY_VIOLATION", message, options);
@@ -674,7 +698,7 @@ async function listColumns(connection, schema, table) {
674
698
 
675
699
  // src/config.ts
676
700
  var CLI_NAME = "cf-hana";
677
- var CLI_VERSION = "0.3.0";
701
+ var CLI_VERSION = "0.3.2";
678
702
  var ENV_PREFIX = "CF_HANA";
679
703
  var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
680
704
  var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
@@ -683,7 +707,7 @@ var DEFAULT_POOL_IDLE_MS = 6e4;
683
707
  var DEFAULT_AUTO_LIMIT = 100;
684
708
  var DEFAULT_CELL_LIMIT = 128;
685
709
  var MAX_CELL_LIMIT = 1e4;
686
- var DEFAULT_RESULT_TTL_MINUTES = 60;
710
+ var DEFAULT_RESULT_TTL_MINUTES = 10080;
687
711
  var DEFAULT_RESULT_SEARCH_LIMIT = 20;
688
712
  var MAX_RESULT_STORE_BYTES = 256 * 1024 * 1024;
689
713
  function envName(suffix) {
@@ -1142,7 +1166,15 @@ function catalogObjects() {
1142
1166
  { SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "EXISTING_TABLE", OBJECT_TYPE: "TABLE" },
1143
1167
  { SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "MISSING_TABLE_FIXED", OBJECT_TYPE: "TABLE" },
1144
1168
  { SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "MISSING_TABLE_VIEW", OBJECT_TYPE: "VIEW" },
1145
- { SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "STATUS_ITEMS", OBJECT_TYPE: "TABLE" }
1169
+ { SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "STATUS_ITEMS", OBJECT_TYPE: "TABLE" },
1170
+ { SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "CORE_AUTH_SCOPE", OBJECT_TYPE: "TABLE" }
1171
+ ];
1172
+ }
1173
+ function tableColumns() {
1174
+ return [
1175
+ { COLUMN_NAME: "ID", DATA_TYPE_NAME: "INTEGER", LENGTH: null, SCALE: null, IS_NULLABLE: "FALSE", POSITION: 1 },
1176
+ { COLUMN_NAME: "IS_ACTIVE", DATA_TYPE_NAME: "BOOLEAN", LENGTH: null, SCALE: null, IS_NULLABLE: "TRUE", POSITION: 2 },
1177
+ { COLUMN_NAME: "SCOPE_NAME", DATA_TYPE_NAME: "NVARCHAR", LENGTH: 255, SCALE: null, IS_NULLABLE: "TRUE", POSITION: 3 }
1146
1178
  ];
1147
1179
  }
1148
1180
  function fakeExec(sql) {
@@ -1167,9 +1199,32 @@ function fakeExec(sql) {
1167
1199
  affectedRows: 0
1168
1200
  };
1169
1201
  }
1202
+ if (upperSql.includes("SYS.TABLE_COLUMNS")) {
1203
+ return {
1204
+ rows: tableColumns(),
1205
+ columns: [
1206
+ { name: "COLUMN_NAME", typeName: "NVARCHAR" },
1207
+ { name: "DATA_TYPE_NAME", typeName: "NVARCHAR" },
1208
+ { name: "LENGTH", typeName: "INTEGER" },
1209
+ { name: "SCALE", typeName: "INTEGER" },
1210
+ { name: "IS_NULLABLE", typeName: "NVARCHAR" },
1211
+ { name: "POSITION", typeName: "INTEGER" }
1212
+ ],
1213
+ affectedRows: 0
1214
+ };
1215
+ }
1170
1216
  if (upperSql.includes("MISSING_TABLE") || upperSql.includes("MISSING_TABLES")) {
1171
1217
  throw new QueryError("invalid table name: MISSING_TABLE", { sqlState: "42S02" });
1172
1218
  }
1219
+ if (upperSql.includes("ISACTIVE")) {
1220
+ throw new QueryError("invalid column name: ISACTIVE: line 1 col 8 (at pos 7)", { databaseCode: 260 });
1221
+ }
1222
+ if (upperSql.includes("LOB_ORDER_ERROR")) {
1223
+ throw new QueryError("inconsistent datatype: LOB type is not allowed in ORDER BY clause", { databaseCode: 266 });
1224
+ }
1225
+ if (upperSql.includes("LOB_GROUP_ERROR")) {
1226
+ throw new QueryError("inconsistent datatype: LOB type is not allowed in GROUP BY clause", { databaseCode: 274 });
1227
+ }
1173
1228
  if (sql.toUpperCase().includes("LOB_FIXTURE")) {
1174
1229
  return {
1175
1230
  rows: [
@@ -1294,7 +1349,12 @@ function extractSqlState(error) {
1294
1349
  }
1295
1350
  function toQueryError(error) {
1296
1351
  const sqlState = extractSqlState(error);
1297
- return sqlState === void 0 ? new QueryError(error.message, { cause: error }) : new QueryError(error.message, { cause: error, sqlState });
1352
+ const code = databaseCode(error);
1353
+ return new QueryError(error.message, {
1354
+ cause: error,
1355
+ ...sqlState === void 0 ? {} : { sqlState },
1356
+ ...code === void 0 ? {} : { databaseCode: code }
1357
+ });
1298
1358
  }
1299
1359
  function toColumns(metadata) {
1300
1360
  if (metadata === void 0) {
@@ -3375,6 +3435,14 @@ function score(requested, candidate) {
3375
3435
  }
3376
3436
  return value;
3377
3437
  }
3438
+ function extractInvalidColumnNameFromError(error) {
3439
+ if (!(error instanceof QueryError)) {
3440
+ return void 0;
3441
+ }
3442
+ const match = /invalid column name:\s*([^:]+)/i.exec(error.message);
3443
+ const name = match?.[1]?.trim();
3444
+ return name === void 0 || name.length === 0 ? void 0 : name;
3445
+ }
3378
3446
  function rankCatalogSuggestions(requested, candidates, limit = 5) {
3379
3447
  return candidates.map((candidate) => ({ candidate, score: score(requested, candidate) })).filter((item) => item.score >= 45).sort(
3380
3448
  (left, right) => right.score - left.score || left.candidate.schema.localeCompare(right.candidate.schema) || (left.candidate.type === right.candidate.type ? 0 : left.candidate.type === "TABLE" ? -1 : 1) || left.candidate.name.localeCompare(right.candidate.name)
@@ -3389,6 +3457,24 @@ function formatSuggestions(suggestions) {
3389
3457
  ...suggestions.map((item) => ` ${item.schema}.${item.name} (${item.type})`)
3390
3458
  ].join("\n");
3391
3459
  }
3460
+ function rankNameSuggestions(requested, candidates, limit = 5) {
3461
+ const requestedName = { name: requested };
3462
+ return candidates.map((candidate) => ({
3463
+ candidate,
3464
+ score: score(requestedName, { schema: "", name: candidate.name, type: "TABLE" })
3465
+ })).filter((item) => item.score >= 45).sort(
3466
+ (left, right) => right.score - left.score || left.candidate.name.localeCompare(right.candidate.name)
3467
+ ).slice(0, limit).map((item) => item.candidate);
3468
+ }
3469
+ function formatColumnSuggestions(suggestions) {
3470
+ if (suggestions.length === 0) {
3471
+ return void 0;
3472
+ }
3473
+ return [
3474
+ "Did you mean column:",
3475
+ ...suggestions.map((item) => ` ${item.name}`)
3476
+ ].join("\n");
3477
+ }
3392
3478
 
3393
3479
  // src/cli.ts
3394
3480
  function print2(text) {
@@ -3536,13 +3622,50 @@ async function loadSuggestionCatalogObjects(client, refresh) {
3536
3622
  return await client.listCatalogObjects(client.info.schema);
3537
3623
  }
3538
3624
  }
3539
- async function enrichAndRethrowQueryError(error, client, sql, refresh) {
3625
+ function isLobSortOrGroupError(error) {
3626
+ const code = databaseCode(error);
3627
+ if (code !== 266 && code !== 274) {
3628
+ return false;
3629
+ }
3630
+ return error instanceof QueryError && /LOB type is not allowed in (?:ORDER BY|GROUP BY) clause/i.test(error.message);
3631
+ }
3632
+ function printLobSortOrGroupHint() {
3633
+ const lines = [
3634
+ `${CLI_NAME}: HANA cannot ORDER BY or GROUP BY NCLOB/CLOB/BLOB columns directly.`,
3635
+ `${CLI_NAME}: Remove the LOB column from ORDER BY/GROUP BY or wrap it as TO_VARCHAR(<column>).`
3636
+ ];
3637
+ process.stderr.write(`${lines.join("\n")}
3638
+ `);
3639
+ }
3640
+ async function printColumnSuggestions(error, client, sql) {
3641
+ if (databaseCode(error) !== 260) {
3642
+ return;
3643
+ }
3644
+ const columnName = extractInvalidColumnNameFromError(error);
3645
+ const tableName = extractMissingObjectName(sql);
3646
+ if (columnName === void 0 || tableName === void 0) {
3647
+ return;
3648
+ }
3649
+ try {
3650
+ const columns = await client.listColumns(
3651
+ tableName.schema ?? client.info.schema,
3652
+ tableName.name
3653
+ );
3654
+ const text = formatColumnSuggestions(rankNameSuggestions(columnName, columns));
3655
+ if (text !== void 0) {
3656
+ process.stderr.write(`${text}
3657
+ `);
3658
+ }
3659
+ } catch {
3660
+ }
3661
+ }
3662
+ async function printCatalogObjectSuggestions(error, client, sql, refresh) {
3540
3663
  if (!isInvalidCatalogObjectError(error)) {
3541
- throw error;
3664
+ return;
3542
3665
  }
3543
3666
  const requested = extractMissingObjectNameFromError(error) ?? extractMissingObjectName(sql);
3544
3667
  if (requested === void 0) {
3545
- throw error;
3668
+ return;
3546
3669
  }
3547
3670
  try {
3548
3671
  const objects = await loadSuggestionCatalogObjects(client, refresh);
@@ -3553,6 +3676,14 @@ async function enrichAndRethrowQueryError(error, client, sql, refresh) {
3553
3676
  }
3554
3677
  } catch {
3555
3678
  }
3679
+ }
3680
+ async function enrichAndRethrowQueryError(error, client, sql, refresh) {
3681
+ if (isLobSortOrGroupError(error)) {
3682
+ printLobSortOrGroupHint();
3683
+ throw error;
3684
+ }
3685
+ await printColumnSuggestions(error, client, sql);
3686
+ await printCatalogObjectSuggestions(error, client, sql, refresh);
3556
3687
  throw error;
3557
3688
  }
3558
3689
  async function runQuery(selector, sql, command) {
@@ -3589,8 +3720,6 @@ async function runQuery(selector, sql, command) {
3589
3720
  ...opts.resultTtlMinutes === void 0 ? {} : { ttlMinutes: opts.resultTtlMinutes }
3590
3721
  });
3591
3722
  print2(`ref=${session.ref}`);
3592
- process.stderr.write(`${CLI_NAME}: saved result expires at ${session.expiresAt}
3593
- `);
3594
3723
  }
3595
3724
  print2(compact.text);
3596
3725
  if (result.truncated) {