@saptools/cf-hana 0.2.2 → 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 +12 -0
- package/README.md +33 -6
- package/dist/cli.js +696 -27
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +140 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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);
|
|
@@ -51,6 +75,15 @@ function errorMessage(error) {
|
|
|
51
75
|
return String(error);
|
|
52
76
|
}
|
|
53
77
|
|
|
78
|
+
// src/lob.ts
|
|
79
|
+
var TEXT_LOB_TYPES = /* @__PURE__ */ new Set(["CLOB", "NCLOB"]);
|
|
80
|
+
function normalizeTypeName(typeName2) {
|
|
81
|
+
return typeName2?.trim().toUpperCase() ?? "";
|
|
82
|
+
}
|
|
83
|
+
function isTextLobType(typeName2) {
|
|
84
|
+
return TEXT_LOB_TYPES.has(normalizeTypeName(typeName2));
|
|
85
|
+
}
|
|
86
|
+
|
|
54
87
|
// src/result-preview.ts
|
|
55
88
|
function normalizePreviewChar(value) {
|
|
56
89
|
return value === "\r" || value === "\n" || value === " " ? " " : value;
|
|
@@ -91,12 +124,12 @@ function scalarText(value) {
|
|
|
91
124
|
}
|
|
92
125
|
return value.toString();
|
|
93
126
|
}
|
|
94
|
-
function previewCell(value, limit) {
|
|
127
|
+
function previewCell(value, limit, typeName2) {
|
|
95
128
|
if (value === null) {
|
|
96
129
|
return { text: "", truncated: false, originalLength: 0, unit: "chars" };
|
|
97
130
|
}
|
|
98
131
|
if (Buffer.isBuffer(value)) {
|
|
99
|
-
return previewBuffer(value, limit);
|
|
132
|
+
return isTextLobType(typeName2) ? previewText(value.toString("utf8"), limit) : previewBuffer(value, limit);
|
|
100
133
|
}
|
|
101
134
|
if (typeof value === "string") {
|
|
102
135
|
return previewText(value, limit);
|
|
@@ -105,7 +138,7 @@ function previewCell(value, limit) {
|
|
|
105
138
|
}
|
|
106
139
|
|
|
107
140
|
// src/format.ts
|
|
108
|
-
function cellText(value, nullText) {
|
|
141
|
+
function cellText(value, nullText, column) {
|
|
109
142
|
if (value === null) {
|
|
110
143
|
return nullText;
|
|
111
144
|
}
|
|
@@ -113,14 +146,14 @@ function cellText(value, nullText) {
|
|
|
113
146
|
return value.toISOString();
|
|
114
147
|
}
|
|
115
148
|
if (Buffer.isBuffer(value)) {
|
|
116
|
-
return `0x${value.toString("hex")}`;
|
|
149
|
+
return isTextLobType(column?.typeName) ? value.toString("utf8") : `0x${value.toString("hex")}`;
|
|
117
150
|
}
|
|
118
151
|
if (typeof value === "boolean") {
|
|
119
152
|
return value ? "true" : "false";
|
|
120
153
|
}
|
|
121
154
|
return typeof value === "number" ? value.toString() : value;
|
|
122
155
|
}
|
|
123
|
-
function serializeCell(value) {
|
|
156
|
+
function serializeCell(value, column) {
|
|
124
157
|
if (value === null) {
|
|
125
158
|
return null;
|
|
126
159
|
}
|
|
@@ -128,7 +161,7 @@ function serializeCell(value) {
|
|
|
128
161
|
return value.toISOString();
|
|
129
162
|
}
|
|
130
163
|
if (Buffer.isBuffer(value)) {
|
|
131
|
-
return `0x${value.toString("hex")}`;
|
|
164
|
+
return isTextLobType(column?.typeName) ? value.toString("utf8") : `0x${value.toString("hex")}`;
|
|
132
165
|
}
|
|
133
166
|
return value;
|
|
134
167
|
}
|
|
@@ -144,7 +177,7 @@ function formatTable(result) {
|
|
|
144
177
|
}
|
|
145
178
|
const headers = result.columns.map((column) => column.name);
|
|
146
179
|
const rows = result.rows.map(
|
|
147
|
-
(row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL"))
|
|
180
|
+
(row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL", column))
|
|
148
181
|
);
|
|
149
182
|
const widths = headers.map((header, index) => {
|
|
150
183
|
const widest = rows.reduce(
|
|
@@ -162,7 +195,8 @@ function formatJson(result) {
|
|
|
162
195
|
const rows = result.rows.map((row) => {
|
|
163
196
|
const serialized = {};
|
|
164
197
|
for (const [key, value] of Object.entries(row)) {
|
|
165
|
-
|
|
198
|
+
const column = result.columns.find((item) => item.name === key);
|
|
199
|
+
serialized[key] = serializeCell(value, column);
|
|
166
200
|
}
|
|
167
201
|
return serialized;
|
|
168
202
|
});
|
|
@@ -173,7 +207,7 @@ function formatCsv(result) {
|
|
|
173
207
|
const lines = [headers.map((header) => csvEscape(header)).join(",")];
|
|
174
208
|
for (const row of result.rows) {
|
|
175
209
|
lines.push(
|
|
176
|
-
result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, ""))).join(",")
|
|
210
|
+
result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, "", column))).join(",")
|
|
177
211
|
);
|
|
178
212
|
}
|
|
179
213
|
return lines.join("\r\n");
|
|
@@ -184,7 +218,7 @@ function formatCompactCsv(result, cellLimit) {
|
|
|
184
218
|
let truncatedCells = 0;
|
|
185
219
|
for (const row of result.rows) {
|
|
186
220
|
const cells = result.columns.map((column) => {
|
|
187
|
-
const preview = previewCell(row[column.name] ?? null, cellLimit);
|
|
221
|
+
const preview = previewCell(row[column.name] ?? null, cellLimit, column.typeName);
|
|
188
222
|
if (preview.truncated) {
|
|
189
223
|
truncatedCells += 1;
|
|
190
224
|
}
|
|
@@ -634,6 +668,18 @@ async function listTables(connection, schema) {
|
|
|
634
668
|
rowCount: void 0
|
|
635
669
|
}));
|
|
636
670
|
}
|
|
671
|
+
async function listCatalogObjects(connection, schema) {
|
|
672
|
+
const result = await connection.query(
|
|
673
|
+
"SELECT SCHEMA_NAME, TABLE_NAME AS OBJECT_NAME, 'TABLE' AS OBJECT_TYPE FROM SYS.TABLES WHERE SCHEMA_NAME = ? UNION ALL SELECT SCHEMA_NAME, VIEW_NAME AS OBJECT_NAME, 'VIEW' AS OBJECT_TYPE FROM SYS.VIEWS WHERE SCHEMA_NAME = ? ORDER BY OBJECT_NAME",
|
|
674
|
+
[schema, schema],
|
|
675
|
+
{ autoLimit: false }
|
|
676
|
+
);
|
|
677
|
+
return result.rows.map((row) => ({
|
|
678
|
+
schema: row.SCHEMA_NAME,
|
|
679
|
+
name: row.OBJECT_NAME,
|
|
680
|
+
type: row.OBJECT_TYPE
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
637
683
|
async function listColumns(connection, schema, table) {
|
|
638
684
|
const result = await connection.query(
|
|
639
685
|
"SELECT COLUMN_NAME, DATA_TYPE_NAME, LENGTH, SCALE, IS_NULLABLE, POSITION FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME = ? AND TABLE_NAME = ? ORDER BY POSITION",
|
|
@@ -652,7 +698,7 @@ async function listColumns(connection, schema, table) {
|
|
|
652
698
|
|
|
653
699
|
// src/config.ts
|
|
654
700
|
var CLI_NAME = "cf-hana";
|
|
655
|
-
var CLI_VERSION = "0.
|
|
701
|
+
var CLI_VERSION = "0.3.1";
|
|
656
702
|
var ENV_PREFIX = "CF_HANA";
|
|
657
703
|
var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
658
704
|
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
@@ -661,7 +707,7 @@ var DEFAULT_POOL_IDLE_MS = 6e4;
|
|
|
661
707
|
var DEFAULT_AUTO_LIMIT = 100;
|
|
662
708
|
var DEFAULT_CELL_LIMIT = 128;
|
|
663
709
|
var MAX_CELL_LIMIT = 1e4;
|
|
664
|
-
var DEFAULT_RESULT_TTL_MINUTES =
|
|
710
|
+
var DEFAULT_RESULT_TTL_MINUTES = 10080;
|
|
665
711
|
var DEFAULT_RESULT_SEARCH_LIMIT = 20;
|
|
666
712
|
var MAX_RESULT_STORE_BYTES = 256 * 1024 * 1024;
|
|
667
713
|
function envName(suffix) {
|
|
@@ -731,9 +777,9 @@ function getApiEndpointForRegion(regionKey) {
|
|
|
731
777
|
return REGION_API_MAP[regionKey];
|
|
732
778
|
}
|
|
733
779
|
function getRegionKeyForApi(apiEndpoint) {
|
|
734
|
-
const
|
|
780
|
+
const norm2 = apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
|
|
735
781
|
for (const [key, endpoint] of Object.entries(REGION_API_MAP)) {
|
|
736
|
-
if (endpoint.toLowerCase() ===
|
|
782
|
+
if (endpoint.toLowerCase() === norm2) {
|
|
737
783
|
return key;
|
|
738
784
|
}
|
|
739
785
|
}
|
|
@@ -1114,12 +1160,88 @@ function toConnectionTarget(binding, role) {
|
|
|
1114
1160
|
|
|
1115
1161
|
// src/driver/fake.ts
|
|
1116
1162
|
import { appendFile } from "fs/promises";
|
|
1163
|
+
var catalogFailureInjected = false;
|
|
1164
|
+
function catalogObjects() {
|
|
1165
|
+
return [
|
|
1166
|
+
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "EXISTING_TABLE", OBJECT_TYPE: "TABLE" },
|
|
1167
|
+
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "MISSING_TABLE_FIXED", OBJECT_TYPE: "TABLE" },
|
|
1168
|
+
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "MISSING_TABLE_VIEW", OBJECT_TYPE: "VIEW" },
|
|
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 }
|
|
1178
|
+
];
|
|
1179
|
+
}
|
|
1117
1180
|
function fakeExec(sql) {
|
|
1118
1181
|
const kind = classifyStatement(sql);
|
|
1119
1182
|
const forcedFailure = readEnv(envName("FAKE_FAIL_STATEMENT"))?.toLowerCase();
|
|
1120
1183
|
if (forcedFailure === kind) {
|
|
1121
1184
|
throw new Error(`fake driver forced ${kind.toUpperCase()} failure`);
|
|
1122
1185
|
}
|
|
1186
|
+
const upperSql = sql.toUpperCase();
|
|
1187
|
+
if (upperSql.includes("SYS.TABLES") && upperSql.includes("SYS.VIEWS")) {
|
|
1188
|
+
if (readEnv(envName("FAKE_FAIL_CATALOG_ONCE")) === "1" && !catalogFailureInjected) {
|
|
1189
|
+
catalogFailureInjected = true;
|
|
1190
|
+
throw new QueryError("fake transient catalog metadata failure");
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
rows: catalogObjects(),
|
|
1194
|
+
columns: [
|
|
1195
|
+
{ name: "SCHEMA_NAME", typeName: "NVARCHAR" },
|
|
1196
|
+
{ name: "OBJECT_NAME", typeName: "NVARCHAR" },
|
|
1197
|
+
{ name: "OBJECT_TYPE", typeName: "NVARCHAR" }
|
|
1198
|
+
],
|
|
1199
|
+
affectedRows: 0
|
|
1200
|
+
};
|
|
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
|
+
}
|
|
1216
|
+
if (upperSql.includes("MISSING_TABLE") || upperSql.includes("MISSING_TABLES")) {
|
|
1217
|
+
throw new QueryError("invalid table name: MISSING_TABLE", { sqlState: "42S02" });
|
|
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
|
+
}
|
|
1228
|
+
if (sql.toUpperCase().includes("LOB_FIXTURE")) {
|
|
1229
|
+
return {
|
|
1230
|
+
rows: [
|
|
1231
|
+
{
|
|
1232
|
+
LOG_CONTENT: Buffer.from("Example log entry", "utf8"),
|
|
1233
|
+
CLOB_CONTENT: Buffer.from("Clob log entry", "utf8"),
|
|
1234
|
+
PAYLOAD: Buffer.from([0, 1, 2, 255])
|
|
1235
|
+
}
|
|
1236
|
+
],
|
|
1237
|
+
columns: [
|
|
1238
|
+
{ name: "LOG_CONTENT", typeName: "NCLOB" },
|
|
1239
|
+
{ name: "CLOB_CONTENT", typeName: "CLOB" },
|
|
1240
|
+
{ name: "PAYLOAD", typeName: "BLOB" }
|
|
1241
|
+
],
|
|
1242
|
+
affectedRows: 0
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1123
1245
|
if (sql.toUpperCase().includes("DUMMY")) {
|
|
1124
1246
|
return {
|
|
1125
1247
|
rows: [{ "1": 1 }],
|
|
@@ -1227,7 +1349,12 @@ function extractSqlState(error) {
|
|
|
1227
1349
|
}
|
|
1228
1350
|
function toQueryError(error) {
|
|
1229
1351
|
const sqlState = extractSqlState(error);
|
|
1230
|
-
|
|
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
|
+
});
|
|
1231
1358
|
}
|
|
1232
1359
|
function toColumns(metadata) {
|
|
1233
1360
|
if (metadata === void 0) {
|
|
@@ -2119,6 +2246,10 @@ var HanaClient = class _HanaClient {
|
|
|
2119
2246
|
async listTables(schema) {
|
|
2120
2247
|
return await this.pool.withConnection((connection) => listTables(connection, schema));
|
|
2121
2248
|
}
|
|
2249
|
+
/** List table and view names in a schema for typo suggestions. */
|
|
2250
|
+
async listCatalogObjects(schema) {
|
|
2251
|
+
return await this.pool.withConnection((connection) => listCatalogObjects(connection, schema));
|
|
2252
|
+
}
|
|
2122
2253
|
/** List the columns of a table. */
|
|
2123
2254
|
async listColumns(schema, table) {
|
|
2124
2255
|
return await this.pool.withConnection(
|
|
@@ -2244,12 +2375,15 @@ function textRange(value, offset, length) {
|
|
|
2244
2375
|
}
|
|
2245
2376
|
return { type: "text", originalLength: index, offset, value: selected };
|
|
2246
2377
|
}
|
|
2247
|
-
function readCellWindow(value, offset, length) {
|
|
2378
|
+
function readCellWindow(value, offset, length, typeName2) {
|
|
2248
2379
|
if (!Number.isSafeInteger(offset) || offset < 0) {
|
|
2249
2380
|
throw new CfHanaError("CONFIG", "offset must be a non-negative safe integer");
|
|
2250
2381
|
}
|
|
2251
2382
|
assertPositiveInteger("length", length);
|
|
2252
2383
|
if (Buffer.isBuffer(value)) {
|
|
2384
|
+
if (isTextLobType(typeName2)) {
|
|
2385
|
+
return textRange(value.toString("utf8"), offset, length);
|
|
2386
|
+
}
|
|
2253
2387
|
return {
|
|
2254
2388
|
type: "binary",
|
|
2255
2389
|
originalLength: value.length,
|
|
@@ -2421,18 +2555,19 @@ function jsonSearchMatches(root, term, row, column, limit, previewLength) {
|
|
|
2421
2555
|
}
|
|
2422
2556
|
return matches;
|
|
2423
2557
|
}
|
|
2424
|
-
function searchCell(value, term, row, column, limit, previewLength) {
|
|
2425
|
-
|
|
2558
|
+
function searchCell(value, term, row, column, typeName2, limit, previewLength) {
|
|
2559
|
+
const text = Buffer.isBuffer(value) && isTextLobType(typeName2) ? value.toString("utf8") : value;
|
|
2560
|
+
if (typeof text !== "string") {
|
|
2426
2561
|
return [];
|
|
2427
2562
|
}
|
|
2428
2563
|
try {
|
|
2429
|
-
const parsed = JSON.parse(
|
|
2564
|
+
const parsed = JSON.parse(text);
|
|
2430
2565
|
if (typeof parsed === "object" && parsed !== null) {
|
|
2431
2566
|
return jsonSearchMatches(parsed, term, row, column, limit, previewLength);
|
|
2432
2567
|
}
|
|
2433
2568
|
} catch {
|
|
2434
2569
|
}
|
|
2435
|
-
return plainTextMatches(
|
|
2570
|
+
return plainTextMatches(text, term, row, column, limit, previewLength);
|
|
2436
2571
|
}
|
|
2437
2572
|
function searchResultSession(session, searchTerm, options) {
|
|
2438
2573
|
assertPositiveInteger("limit", options.limit);
|
|
@@ -2454,6 +2589,7 @@ function searchResultSession(session, searchTerm, options) {
|
|
|
2454
2589
|
term,
|
|
2455
2590
|
item.rowNumber,
|
|
2456
2591
|
column.name,
|
|
2592
|
+
column.typeName,
|
|
2457
2593
|
remaining,
|
|
2458
2594
|
previewLength
|
|
2459
2595
|
)
|
|
@@ -2775,7 +2911,12 @@ async function runShow(ref, options) {
|
|
|
2775
2911
|
print(csv(rows, ["PATH", "TYPE", "VALUE"], length));
|
|
2776
2912
|
return;
|
|
2777
2913
|
}
|
|
2778
|
-
const window = readCellWindow(
|
|
2914
|
+
const window = readCellWindow(
|
|
2915
|
+
selected.value,
|
|
2916
|
+
nonNegative("--offset", options.offset),
|
|
2917
|
+
length,
|
|
2918
|
+
selected.typeName
|
|
2919
|
+
);
|
|
2779
2920
|
print(
|
|
2780
2921
|
csv(
|
|
2781
2922
|
[
|
|
@@ -2828,9 +2969,9 @@ async function runSearch(ref, text, options) {
|
|
|
2828
2969
|
}));
|
|
2829
2970
|
print(csv(rows, ["ROW", "COLUMN", "OFFSET", "PATH", "PREVIEW"], length));
|
|
2830
2971
|
}
|
|
2831
|
-
function cellExportValue(value) {
|
|
2972
|
+
function cellExportValue(value, typeName2) {
|
|
2832
2973
|
if (Buffer.isBuffer(value)) {
|
|
2833
|
-
return value;
|
|
2974
|
+
return isTextLobType(typeName2) ? value.toString("utf8") : value;
|
|
2834
2975
|
}
|
|
2835
2976
|
if (value instanceof Date) {
|
|
2836
2977
|
return value.toISOString();
|
|
@@ -2852,7 +2993,9 @@ async function runExport(ref, options) {
|
|
|
2852
2993
|
}
|
|
2853
2994
|
const session = await readResultSession(ref);
|
|
2854
2995
|
const selected = selectResultCell(session, options.row, options.column);
|
|
2855
|
-
await writeFile3(options.output, cellExportValue(selected.value), {
|
|
2996
|
+
await writeFile3(options.output, cellExportValue(selected.value, selected.typeName), {
|
|
2997
|
+
mode: 384
|
|
2998
|
+
});
|
|
2856
2999
|
print(`wrote=${options.output}`);
|
|
2857
3000
|
}
|
|
2858
3001
|
async function runList() {
|
|
@@ -2894,6 +3037,445 @@ function registerResultCommands(program) {
|
|
|
2894
3037
|
});
|
|
2895
3038
|
}
|
|
2896
3039
|
|
|
3040
|
+
// src/metadata-cache.ts
|
|
3041
|
+
import { createHash } from "crypto";
|
|
3042
|
+
import { mkdir as mkdir4, readFile as readFile2, rename as rename2, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
3043
|
+
import { homedir as homedir4 } from "os";
|
|
3044
|
+
import { join as join5 } from "path";
|
|
3045
|
+
var METADATA_CACHE_TTL_MS = 30 * 6e4;
|
|
3046
|
+
function metadataCacheRoot(saptoolsRoot) {
|
|
3047
|
+
return join5(saptoolsRoot ?? join5(homedir4(), ".saptools"), "cf-hana", "metadata");
|
|
3048
|
+
}
|
|
3049
|
+
function toMetadataCacheScope(info) {
|
|
3050
|
+
return {
|
|
3051
|
+
selector: info.selector,
|
|
3052
|
+
appName: info.appName,
|
|
3053
|
+
host: info.host,
|
|
3054
|
+
schema: info.schema,
|
|
3055
|
+
role: info.role,
|
|
3056
|
+
driver: info.driver
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
function metadataCacheKey(scope) {
|
|
3060
|
+
return createHash("sha256").update(JSON.stringify(scope)).digest("hex");
|
|
3061
|
+
}
|
|
3062
|
+
function metadataCachePath(scope, saptoolsRoot) {
|
|
3063
|
+
return join5(metadataCacheRoot(saptoolsRoot), `${metadataCacheKey(scope)}.json`);
|
|
3064
|
+
}
|
|
3065
|
+
function isRecord3(value) {
|
|
3066
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3067
|
+
}
|
|
3068
|
+
function isCatalogObject(value) {
|
|
3069
|
+
return isRecord3(value) && typeof value["schema"] === "string" && typeof value["name"] === "string" && (value["type"] === "TABLE" || value["type"] === "VIEW");
|
|
3070
|
+
}
|
|
3071
|
+
function isScope(value) {
|
|
3072
|
+
return isRecord3(value) && typeof value["selector"] === "string" && typeof value["appName"] === "string" && typeof value["host"] === "string" && typeof value["schema"] === "string" && typeof value["role"] === "string" && typeof value["driver"] === "string";
|
|
3073
|
+
}
|
|
3074
|
+
function scopesEqual(left, right) {
|
|
3075
|
+
return metadataCacheKey(left) === metadataCacheKey(right);
|
|
3076
|
+
}
|
|
3077
|
+
function isStored(value) {
|
|
3078
|
+
return isRecord3(value) && value["version"] === 1 && typeof value["createdAt"] === "string" && isScope(value["scope"]) && Array.isArray(value["objects"]) && value["objects"].every(isCatalogObject);
|
|
3079
|
+
}
|
|
3080
|
+
async function readMetadataCache(scope, options = {}) {
|
|
3081
|
+
try {
|
|
3082
|
+
const parsed = JSON.parse(
|
|
3083
|
+
await readFile2(metadataCachePath(scope, options.saptoolsRoot), "utf8")
|
|
3084
|
+
);
|
|
3085
|
+
if (!isStored(parsed) || !scopesEqual(parsed.scope, scope)) {
|
|
3086
|
+
return void 0;
|
|
3087
|
+
}
|
|
3088
|
+
const createdAt = Date.parse(parsed.createdAt);
|
|
3089
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
3090
|
+
const ageMs = now.getTime() - createdAt;
|
|
3091
|
+
if (!Number.isFinite(createdAt) || ageMs < 0 || ageMs >= METADATA_CACHE_TTL_MS) {
|
|
3092
|
+
return void 0;
|
|
3093
|
+
}
|
|
3094
|
+
return parsed.objects;
|
|
3095
|
+
} catch {
|
|
3096
|
+
return void 0;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
async function writeMetadataCache(scope, objects, options = {}) {
|
|
3100
|
+
const root = metadataCacheRoot(options.saptoolsRoot);
|
|
3101
|
+
const path = metadataCachePath(scope, options.saptoolsRoot);
|
|
3102
|
+
const tempPath = `${path}.tmp-${process.pid.toString()}`;
|
|
3103
|
+
const stored = {
|
|
3104
|
+
version: 1,
|
|
3105
|
+
createdAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
3106
|
+
scope,
|
|
3107
|
+
objects
|
|
3108
|
+
};
|
|
3109
|
+
await mkdir4(root, { recursive: true, mode: 448 });
|
|
3110
|
+
await rm4(tempPath, { force: true });
|
|
3111
|
+
await writeFile4(tempPath, `${JSON.stringify(stored)}
|
|
3112
|
+
`, { encoding: "utf8", mode: 384 });
|
|
3113
|
+
await rename2(tempPath, path);
|
|
3114
|
+
}
|
|
3115
|
+
async function loadCatalogObjectsWithCache(scope, refresh, loader, options = {}) {
|
|
3116
|
+
if (!refresh) {
|
|
3117
|
+
const cached = await readMetadataCache(scope, options);
|
|
3118
|
+
if (cached !== void 0) {
|
|
3119
|
+
return cached;
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
const objects = await loader();
|
|
3123
|
+
try {
|
|
3124
|
+
await writeMetadataCache(scope, objects, options);
|
|
3125
|
+
} catch {
|
|
3126
|
+
}
|
|
3127
|
+
return objects;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// src/suggestions.ts
|
|
3131
|
+
var INVALID_OBJECT_PATTERNS = [
|
|
3132
|
+
/invalid\s+(?:table|view|object)\s+name/i,
|
|
3133
|
+
/(?:table|view|object)\s+[^\n]*does\s+not\s+exist/i,
|
|
3134
|
+
/could\s+not\s+find\s+(?:table|view|object)/i
|
|
3135
|
+
];
|
|
3136
|
+
var REF_KEYWORDS = /* @__PURE__ */ new Set(["FROM", "JOIN", "UPDATE", "INTO", "TABLE"]);
|
|
3137
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
3138
|
+
"AS",
|
|
3139
|
+
"ON",
|
|
3140
|
+
"WHERE",
|
|
3141
|
+
"SET",
|
|
3142
|
+
"VALUES",
|
|
3143
|
+
"USING",
|
|
3144
|
+
"WHEN",
|
|
3145
|
+
"INNER",
|
|
3146
|
+
"LEFT",
|
|
3147
|
+
"RIGHT",
|
|
3148
|
+
"FULL",
|
|
3149
|
+
"OUTER",
|
|
3150
|
+
"CROSS",
|
|
3151
|
+
"JOIN"
|
|
3152
|
+
]);
|
|
3153
|
+
var CTE_FOLLOWERS = /* @__PURE__ */ new Set(["AS"]);
|
|
3154
|
+
function isInvalidCatalogObjectError(error) {
|
|
3155
|
+
if (!(error instanceof QueryError)) {
|
|
3156
|
+
return false;
|
|
3157
|
+
}
|
|
3158
|
+
if (error.sqlState === "42S02" || error.sqlState === "42S01") {
|
|
3159
|
+
return true;
|
|
3160
|
+
}
|
|
3161
|
+
return INVALID_OBJECT_PATTERNS.some((pattern) => pattern.test(error.message));
|
|
3162
|
+
}
|
|
3163
|
+
function skipLineComment3(sql, index) {
|
|
3164
|
+
let cursor = index + 2;
|
|
3165
|
+
while (cursor < sql.length && sql[cursor] !== "\n") {
|
|
3166
|
+
cursor += 1;
|
|
3167
|
+
}
|
|
3168
|
+
return cursor;
|
|
3169
|
+
}
|
|
3170
|
+
function skipBlockComment3(sql, index) {
|
|
3171
|
+
let cursor = index + 2;
|
|
3172
|
+
while (cursor < sql.length && !(sql[cursor] === "*" && sql[cursor + 1] === "/")) {
|
|
3173
|
+
cursor += 1;
|
|
3174
|
+
}
|
|
3175
|
+
return Math.min(cursor + 2, sql.length);
|
|
3176
|
+
}
|
|
3177
|
+
function skipStringLiteral(sql, index) {
|
|
3178
|
+
let cursor = index + 1;
|
|
3179
|
+
while (cursor < sql.length) {
|
|
3180
|
+
if (sql[cursor] === "'" && sql[cursor + 1] === "'") {
|
|
3181
|
+
cursor += 2;
|
|
3182
|
+
continue;
|
|
3183
|
+
}
|
|
3184
|
+
if (sql[cursor] === "'") {
|
|
3185
|
+
return cursor + 1;
|
|
3186
|
+
}
|
|
3187
|
+
cursor += 1;
|
|
3188
|
+
}
|
|
3189
|
+
return cursor;
|
|
3190
|
+
}
|
|
3191
|
+
function readQuotedIdentifier(sql, index) {
|
|
3192
|
+
let cursor = index + 1;
|
|
3193
|
+
let text = "";
|
|
3194
|
+
while (cursor < sql.length) {
|
|
3195
|
+
if (sql[cursor] === '"' && sql[cursor + 1] === '"') {
|
|
3196
|
+
text += '"';
|
|
3197
|
+
cursor += 2;
|
|
3198
|
+
continue;
|
|
3199
|
+
}
|
|
3200
|
+
if (sql[cursor] === '"') {
|
|
3201
|
+
return { text, next: cursor + 1 };
|
|
3202
|
+
}
|
|
3203
|
+
text += sql.charAt(cursor);
|
|
3204
|
+
cursor += 1;
|
|
3205
|
+
}
|
|
3206
|
+
return { text, next: cursor };
|
|
3207
|
+
}
|
|
3208
|
+
function readBareWord(sql, index) {
|
|
3209
|
+
let cursor = index;
|
|
3210
|
+
let text = "";
|
|
3211
|
+
while (cursor < sql.length && /[A-Za-z0-9_#$]/.test(sql.charAt(cursor))) {
|
|
3212
|
+
text += sql.charAt(cursor);
|
|
3213
|
+
cursor += 1;
|
|
3214
|
+
}
|
|
3215
|
+
return { text, next: cursor };
|
|
3216
|
+
}
|
|
3217
|
+
function tokenize(sql) {
|
|
3218
|
+
const tokens = [];
|
|
3219
|
+
let index = 0;
|
|
3220
|
+
while (index < sql.length) {
|
|
3221
|
+
const char = sql.charAt(index);
|
|
3222
|
+
if (/\s/.test(char)) {
|
|
3223
|
+
index += 1;
|
|
3224
|
+
continue;
|
|
3225
|
+
}
|
|
3226
|
+
if (char === "-" && sql[index + 1] === "-") {
|
|
3227
|
+
index = skipLineComment3(sql, index);
|
|
3228
|
+
continue;
|
|
3229
|
+
}
|
|
3230
|
+
if (char === "/" && sql[index + 1] === "*") {
|
|
3231
|
+
index = skipBlockComment3(sql, index);
|
|
3232
|
+
continue;
|
|
3233
|
+
}
|
|
3234
|
+
if (char === "'") {
|
|
3235
|
+
index = skipStringLiteral(sql, index);
|
|
3236
|
+
continue;
|
|
3237
|
+
}
|
|
3238
|
+
if (char === '"') {
|
|
3239
|
+
const quoted = readQuotedIdentifier(sql, index);
|
|
3240
|
+
tokens.push({ kind: "quoted", text: quoted.text });
|
|
3241
|
+
index = quoted.next;
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
if (/[A-Za-z_#$]/.test(char)) {
|
|
3245
|
+
const word = readBareWord(sql, index);
|
|
3246
|
+
tokens.push({ kind: "word", text: word.text });
|
|
3247
|
+
index = word.next;
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
if (char === ".") {
|
|
3251
|
+
tokens.push({ kind: "dot", text: char });
|
|
3252
|
+
} else if (char === ",") {
|
|
3253
|
+
tokens.push({ kind: "comma", text: char });
|
|
3254
|
+
} else if (char === "(") {
|
|
3255
|
+
tokens.push({ kind: "open", text: char });
|
|
3256
|
+
} else if (char === ")") {
|
|
3257
|
+
tokens.push({ kind: "close", text: char });
|
|
3258
|
+
} else {
|
|
3259
|
+
tokens.push({ kind: "other", text: char });
|
|
3260
|
+
}
|
|
3261
|
+
index += 1;
|
|
3262
|
+
}
|
|
3263
|
+
return tokens;
|
|
3264
|
+
}
|
|
3265
|
+
function upper(token) {
|
|
3266
|
+
return token?.text.toUpperCase() ?? "";
|
|
3267
|
+
}
|
|
3268
|
+
function isIdentifier(token) {
|
|
3269
|
+
return token?.kind === "word" || token?.kind === "quoted";
|
|
3270
|
+
}
|
|
3271
|
+
function readName(tokens, start, allowFollowingParens = false) {
|
|
3272
|
+
const first = tokens[start];
|
|
3273
|
+
if (!isIdentifier(first)) {
|
|
3274
|
+
return void 0;
|
|
3275
|
+
}
|
|
3276
|
+
let next = start + 1;
|
|
3277
|
+
let schema;
|
|
3278
|
+
let name = first.text;
|
|
3279
|
+
const maybeObject = tokens[next + 1];
|
|
3280
|
+
if (tokens[next]?.kind === "dot" && isIdentifier(maybeObject)) {
|
|
3281
|
+
schema = name;
|
|
3282
|
+
name = maybeObject.text;
|
|
3283
|
+
next += 2;
|
|
3284
|
+
}
|
|
3285
|
+
if (!allowFollowingParens && tokens[next]?.kind === "open") {
|
|
3286
|
+
return void 0;
|
|
3287
|
+
}
|
|
3288
|
+
return { name: schema === void 0 ? { name } : { schema, name }, next };
|
|
3289
|
+
}
|
|
3290
|
+
function firstNameFromText(text) {
|
|
3291
|
+
const tokens = tokenize(text);
|
|
3292
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
3293
|
+
const read = readName(tokens, index, true);
|
|
3294
|
+
if (read !== void 0) {
|
|
3295
|
+
return read.name;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
return void 0;
|
|
3299
|
+
}
|
|
3300
|
+
function extractMissingObjectNameFromError(error) {
|
|
3301
|
+
if (!(error instanceof QueryError)) {
|
|
3302
|
+
return void 0;
|
|
3303
|
+
}
|
|
3304
|
+
const message = error.message;
|
|
3305
|
+
const invalidName = /invalid\s+(?:table|view|object)\s+name\s*:?\s*(.+)$/i.exec(message);
|
|
3306
|
+
if (invalidName?.[1] !== void 0) {
|
|
3307
|
+
return firstNameFromText(invalidName[1]);
|
|
3308
|
+
}
|
|
3309
|
+
const missingObject = /(?:table|view|object)\s+(.+?)\s+(?:does\s+not\s+exist|not\s+found)/i.exec(message);
|
|
3310
|
+
if (missingObject?.[1] !== void 0) {
|
|
3311
|
+
return firstNameFromText(missingObject[1]);
|
|
3312
|
+
}
|
|
3313
|
+
return void 0;
|
|
3314
|
+
}
|
|
3315
|
+
function cteNames(tokens) {
|
|
3316
|
+
const names = /* @__PURE__ */ new Set();
|
|
3317
|
+
if (upper(tokens[0]) !== "WITH") {
|
|
3318
|
+
return names;
|
|
3319
|
+
}
|
|
3320
|
+
let depth = 0;
|
|
3321
|
+
for (let index = 1; index < tokens.length; index += 1) {
|
|
3322
|
+
const token = tokens[index];
|
|
3323
|
+
if (depth === 0 && upper(token) === "SELECT") {
|
|
3324
|
+
break;
|
|
3325
|
+
}
|
|
3326
|
+
if (depth === 0 && isIdentifier(token) && CTE_FOLLOWERS.has(upper(tokens[index + 1]))) {
|
|
3327
|
+
names.add(token.text.toUpperCase());
|
|
3328
|
+
}
|
|
3329
|
+
if (token?.kind === "open") {
|
|
3330
|
+
depth += 1;
|
|
3331
|
+
} else if (token?.kind === "close" && depth > 0) {
|
|
3332
|
+
depth -= 1;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
return names;
|
|
3336
|
+
}
|
|
3337
|
+
function extractMissingObjectName(sql) {
|
|
3338
|
+
const tokens = tokenize(sql.replace(/^;+|;+$/g, ""));
|
|
3339
|
+
const ctes = cteNames(tokens);
|
|
3340
|
+
let candidate;
|
|
3341
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
3342
|
+
const word = upper(tokens[index]);
|
|
3343
|
+
if (!REF_KEYWORDS.has(word)) {
|
|
3344
|
+
continue;
|
|
3345
|
+
}
|
|
3346
|
+
if (word === "TABLE" && upper(tokens[index - 1]) !== "TRUNCATE") {
|
|
3347
|
+
continue;
|
|
3348
|
+
}
|
|
3349
|
+
if (word === "INTO" && !(upper(tokens[index - 1]) === "INSERT" || upper(tokens[index - 1]) === "MERGE")) {
|
|
3350
|
+
continue;
|
|
3351
|
+
}
|
|
3352
|
+
const read = readName(tokens, index + 1, word === "INTO");
|
|
3353
|
+
if (read === void 0) {
|
|
3354
|
+
continue;
|
|
3355
|
+
}
|
|
3356
|
+
if (read.name.schema === void 0 && ctes.has(read.name.name.toUpperCase())) {
|
|
3357
|
+
continue;
|
|
3358
|
+
}
|
|
3359
|
+
if (STOP_WORDS.has(read.name.name.toUpperCase())) {
|
|
3360
|
+
continue;
|
|
3361
|
+
}
|
|
3362
|
+
candidate = read.name;
|
|
3363
|
+
let next = read.next;
|
|
3364
|
+
if (isIdentifier(tokens[next]) && !STOP_WORDS.has(upper(tokens[next]))) {
|
|
3365
|
+
next += 1;
|
|
3366
|
+
}
|
|
3367
|
+
while (tokens[next]?.kind === "comma") {
|
|
3368
|
+
const commaRead = readName(tokens, next + 1);
|
|
3369
|
+
if (commaRead === void 0) {
|
|
3370
|
+
break;
|
|
3371
|
+
}
|
|
3372
|
+
if (!(commaRead.name.schema === void 0 && ctes.has(commaRead.name.name.toUpperCase()))) {
|
|
3373
|
+
candidate = commaRead.name;
|
|
3374
|
+
}
|
|
3375
|
+
next = commaRead.next;
|
|
3376
|
+
if (isIdentifier(tokens[next]) && !STOP_WORDS.has(upper(tokens[next]))) {
|
|
3377
|
+
next += 1;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
return candidate;
|
|
3382
|
+
}
|
|
3383
|
+
function singular(value) {
|
|
3384
|
+
if (value.endsWith("ES")) {
|
|
3385
|
+
return value.slice(0, -2);
|
|
3386
|
+
}
|
|
3387
|
+
if (value.endsWith("S")) {
|
|
3388
|
+
return value.slice(0, -1);
|
|
3389
|
+
}
|
|
3390
|
+
return value;
|
|
3391
|
+
}
|
|
3392
|
+
function norm(value) {
|
|
3393
|
+
return value.toUpperCase().replace(/[^A-Z0-9]+/g, "");
|
|
3394
|
+
}
|
|
3395
|
+
function distance(a, b) {
|
|
3396
|
+
const previous = Array.from({ length: b.length + 1 }, (_value, index) => index);
|
|
3397
|
+
for (let leftIndex = 1; leftIndex <= a.length; leftIndex += 1) {
|
|
3398
|
+
let last = leftIndex - 1;
|
|
3399
|
+
previous[0] = leftIndex;
|
|
3400
|
+
for (let rightIndex = 1; rightIndex <= b.length; rightIndex += 1) {
|
|
3401
|
+
const old = previous[rightIndex] ?? 0;
|
|
3402
|
+
previous[rightIndex] = Math.min(
|
|
3403
|
+
(previous[rightIndex] ?? 0) + 1,
|
|
3404
|
+
(previous[rightIndex - 1] ?? 0) + 1,
|
|
3405
|
+
last + (a.charAt(leftIndex - 1) === b.charAt(rightIndex - 1) ? 0 : 1)
|
|
3406
|
+
);
|
|
3407
|
+
last = old;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
return previous[b.length] ?? 0;
|
|
3411
|
+
}
|
|
3412
|
+
function score(requested, candidate) {
|
|
3413
|
+
const requestedName = norm(requested.name);
|
|
3414
|
+
const candidateName = norm(candidate.name);
|
|
3415
|
+
if (requested.schema !== void 0 && norm(requested.schema) !== norm(candidate.schema)) {
|
|
3416
|
+
return 0;
|
|
3417
|
+
}
|
|
3418
|
+
if (requestedName === candidateName) {
|
|
3419
|
+
return 100;
|
|
3420
|
+
}
|
|
3421
|
+
if (singular(requestedName) === singular(candidateName)) {
|
|
3422
|
+
return 92;
|
|
3423
|
+
}
|
|
3424
|
+
let value = 0;
|
|
3425
|
+
if (candidateName.startsWith(requestedName) || requestedName.startsWith(candidateName)) {
|
|
3426
|
+
value = Math.max(value, 80 - Math.abs(candidateName.length - requestedName.length));
|
|
3427
|
+
}
|
|
3428
|
+
if (candidateName.endsWith(requestedName) || requestedName.endsWith(candidateName)) {
|
|
3429
|
+
value = Math.max(value, 70 - Math.abs(candidateName.length - requestedName.length));
|
|
3430
|
+
}
|
|
3431
|
+
const editDistance = distance(requestedName, candidateName);
|
|
3432
|
+
const max = Math.max(requestedName.length, candidateName.length, 1);
|
|
3433
|
+
if (editDistance <= Math.max(3, Math.floor(max * 0.35))) {
|
|
3434
|
+
value = Math.max(value, Math.round(75 * (1 - editDistance / max)));
|
|
3435
|
+
}
|
|
3436
|
+
return value;
|
|
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
|
+
}
|
|
3446
|
+
function rankCatalogSuggestions(requested, candidates, limit = 5) {
|
|
3447
|
+
return candidates.map((candidate) => ({ candidate, score: score(requested, candidate) })).filter((item) => item.score >= 45).sort(
|
|
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)
|
|
3449
|
+
).slice(0, limit).map((item) => item.candidate);
|
|
3450
|
+
}
|
|
3451
|
+
function formatSuggestions(suggestions) {
|
|
3452
|
+
if (suggestions.length === 0) {
|
|
3453
|
+
return void 0;
|
|
3454
|
+
}
|
|
3455
|
+
return [
|
|
3456
|
+
"Did you mean:",
|
|
3457
|
+
...suggestions.map((item) => ` ${item.schema}.${item.name} (${item.type})`)
|
|
3458
|
+
].join("\n");
|
|
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
|
+
}
|
|
3478
|
+
|
|
2897
3479
|
// src/cli.ts
|
|
2898
3480
|
function print2(text) {
|
|
2899
3481
|
process.stdout.write(`${text}
|
|
@@ -3020,7 +3602,7 @@ function formatInfo(info) {
|
|
|
3020
3602
|
].join("\n");
|
|
3021
3603
|
}
|
|
3022
3604
|
function withConnectionOptions(command) {
|
|
3023
|
-
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");
|
|
3605
|
+
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").option("--refresh-metadata", "bypass the 30-minute table/view metadata suggestion cache", false);
|
|
3024
3606
|
}
|
|
3025
3607
|
function withFormattedConnectionOptions(command) {
|
|
3026
3608
|
return withConnectionOptions(command).option(
|
|
@@ -3029,6 +3611,81 @@ function withFormattedConnectionOptions(command) {
|
|
|
3029
3611
|
"table"
|
|
3030
3612
|
);
|
|
3031
3613
|
}
|
|
3614
|
+
async function loadSuggestionCatalogObjects(client, refresh) {
|
|
3615
|
+
try {
|
|
3616
|
+
return await loadCatalogObjectsWithCache(
|
|
3617
|
+
toMetadataCacheScope(client.info),
|
|
3618
|
+
refresh,
|
|
3619
|
+
async () => await client.listCatalogObjects(client.info.schema)
|
|
3620
|
+
);
|
|
3621
|
+
} catch {
|
|
3622
|
+
return await client.listCatalogObjects(client.info.schema);
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
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) {
|
|
3663
|
+
if (!isInvalidCatalogObjectError(error)) {
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
const requested = extractMissingObjectNameFromError(error) ?? extractMissingObjectName(sql);
|
|
3667
|
+
if (requested === void 0) {
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
try {
|
|
3671
|
+
const objects = await loadSuggestionCatalogObjects(client, refresh);
|
|
3672
|
+
const text = formatSuggestions(rankCatalogSuggestions(requested, objects));
|
|
3673
|
+
if (text !== void 0) {
|
|
3674
|
+
process.stderr.write(`${text}
|
|
3675
|
+
`);
|
|
3676
|
+
}
|
|
3677
|
+
} catch {
|
|
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);
|
|
3687
|
+
throw error;
|
|
3688
|
+
}
|
|
3032
3689
|
async function runQuery(selector, sql, command) {
|
|
3033
3690
|
const opts = command.opts();
|
|
3034
3691
|
assertQueryOptions(sql, opts);
|
|
@@ -3036,12 +3693,24 @@ async function runQuery(selector, sql, command) {
|
|
|
3036
3693
|
const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
|
|
3037
3694
|
try {
|
|
3038
3695
|
const params = opts.param ?? [];
|
|
3039
|
-
|
|
3696
|
+
let backup;
|
|
3697
|
+
try {
|
|
3698
|
+
backup = await client.backupWriteStatement(sql, params);
|
|
3699
|
+
} catch (error) {
|
|
3700
|
+
await enrichAndRethrowQueryError(error, client, sql, opts.refreshMetadata || opts.refresh);
|
|
3701
|
+
}
|
|
3040
3702
|
if (backup !== void 0) {
|
|
3041
3703
|
process.stderr.write(`${CLI_NAME}: backup saved to ${backup.directory}
|
|
3042
3704
|
`);
|
|
3043
3705
|
}
|
|
3044
|
-
const result = await client.query(sql, params)
|
|
3706
|
+
const result = await client.query(sql, params).catch(async (error) => {
|
|
3707
|
+
return await enrichAndRethrowQueryError(
|
|
3708
|
+
error,
|
|
3709
|
+
client,
|
|
3710
|
+
sql,
|
|
3711
|
+
opts.refreshMetadata || opts.refresh
|
|
3712
|
+
);
|
|
3713
|
+
});
|
|
3045
3714
|
if (result.statement === "select") {
|
|
3046
3715
|
const compact = formatCompactCsv(result, cellLimit);
|
|
3047
3716
|
if (opts.save) {
|