@saptools/cf-hana 0.3.0 → 0.3.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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/dist/cli.js +138 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 - 2026-07-01
|
|
4
|
+
|
|
5
|
+
- Add actionable HANA LOB `ORDER BY`/`GROUP BY` hints that recommend removing LOB columns or wrapping them with `TO_VARCHAR(<column>)`.
|
|
6
|
+
- Add invalid-column typo suggestions that inspect target table columns and print close matches to stderr.
|
|
7
|
+
- Increase the default saved result TTL from 60 minutes to 7 days.
|
|
8
|
+
|
|
3
9
|
## 0.3.0 - 2026-07-01
|
|
4
10
|
|
|
5
11
|
- 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
|
|
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.
|
|
701
|
+
var CLI_VERSION = "0.3.1";
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3664
|
+
return;
|
|
3542
3665
|
}
|
|
3543
3666
|
const requested = extractMissingObjectNameFromError(error) ?? extractMissingObjectName(sql);
|
|
3544
3667
|
if (requested === void 0) {
|
|
3545
|
-
|
|
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) {
|