@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/index.d.ts
CHANGED
|
@@ -124,6 +124,12 @@ declare function cfHanaBackupRoot(saptoolsRoot?: string): string;
|
|
|
124
124
|
declare function buildWriteBackupPlan(sql: string, params?: readonly SqlParam[]): WriteBackupPlan | undefined;
|
|
125
125
|
declare function writeSqlBackup(input: SqlBackupWriteInput, options?: SqlBackupWriteOptions): Promise<SqlBackupRecord>;
|
|
126
126
|
|
|
127
|
+
interface CatalogObjectInfo {
|
|
128
|
+
readonly schema: string;
|
|
129
|
+
readonly name: string;
|
|
130
|
+
readonly type: "TABLE" | "VIEW";
|
|
131
|
+
}
|
|
132
|
+
|
|
127
133
|
interface DriverExecResult {
|
|
128
134
|
readonly rows: readonly Record<string, SqlParam>[];
|
|
129
135
|
readonly columns: readonly QueryResultColumn[];
|
|
@@ -265,6 +271,8 @@ declare class HanaClient {
|
|
|
265
271
|
listSchemas(): Promise<readonly string[]>;
|
|
266
272
|
/** List the tables in a schema. */
|
|
267
273
|
listTables(schema: string): Promise<readonly TableInfo[]>;
|
|
274
|
+
/** List table and view names in a schema for typo suggestions. */
|
|
275
|
+
listCatalogObjects(schema: string): Promise<readonly CatalogObjectInfo[]>;
|
|
268
276
|
/** List the columns of a table. */
|
|
269
277
|
listColumns(schema: string, table: string): Promise<readonly ColumnInfo[]>;
|
|
270
278
|
/** Return the HANA execution plan for a statement. */
|
|
@@ -320,6 +328,7 @@ interface CfHanaErrorOptions {
|
|
|
320
328
|
}
|
|
321
329
|
interface QueryErrorOptions extends CfHanaErrorOptions {
|
|
322
330
|
readonly sqlState?: string;
|
|
331
|
+
readonly databaseCode?: number;
|
|
323
332
|
}
|
|
324
333
|
/** Base error for every failure surfaced by `@saptools/cf-hana`. */
|
|
325
334
|
declare class CfHanaError extends Error {
|
|
@@ -333,6 +342,7 @@ declare class CredentialsNotFoundError extends CfHanaError {
|
|
|
333
342
|
/** A SQL statement failed on the HANA server. */
|
|
334
343
|
declare class QueryError extends CfHanaError {
|
|
335
344
|
readonly sqlState: string | undefined;
|
|
345
|
+
readonly databaseCode: number | undefined;
|
|
336
346
|
constructor(message: string, options?: QueryErrorOptions);
|
|
337
347
|
}
|
|
338
348
|
/** A write/DDL statement was issued on a read-only client. */
|
|
@@ -346,4 +356,4 @@ declare class DestructiveStatementError extends CfHanaError {
|
|
|
346
356
|
/** Narrow an unknown thrown value to a human-readable message. */
|
|
347
357
|
declare function errorMessage(error: unknown): string;
|
|
348
358
|
|
|
349
|
-
export { type BuiltStatement, CfHanaError, type CfHanaErrorCode, type ColumnInfo, type ConnectOptions, type CredentialSource, CredentialsNotFoundError, type DbUserRole, DestructiveStatementError, type DriverConnectParams, type DriverConnection, type DriverExecResult, HanaClient, type HanaClientInfo, type HanaDriver, type OutputFormat, type PoolOptions, QueryError, type QueryOptions, type QueryResult, type QueryResultColumn, type QueryRow, ReadOnlyViolationError, type SelectSpec, type SqlBackupRecord, type SqlBackupWriteInput, type SqlBackupWriteOptions, type SqlParam, type StatementKind, type TableInfo, Transaction, type WriteBackupOperation, type WriteBackupPlan, buildCount, buildDelete, buildInsert, buildSelect, buildUpdate, buildWriteBackupPlan, cfHanaBackupRoot, connect, createDriver, errorMessage, formatCsv, formatJson, formatResult, formatTable, query, withConnection, writeSqlBackup };
|
|
359
|
+
export { type BuiltStatement, type CatalogObjectInfo, CfHanaError, type CfHanaErrorCode, type ColumnInfo, type ConnectOptions, type CredentialSource, CredentialsNotFoundError, type DbUserRole, DestructiveStatementError, type DriverConnectParams, type DriverConnection, type DriverExecResult, HanaClient, type HanaClientInfo, type HanaDriver, type OutputFormat, type PoolOptions, QueryError, type QueryOptions, type QueryResult, type QueryResultColumn, type QueryRow, ReadOnlyViolationError, type SelectSpec, type SqlBackupRecord, type SqlBackupWriteInput, type SqlBackupWriteOptions, type SqlParam, type StatementKind, type TableInfo, Transaction, type WriteBackupOperation, type WriteBackupPlan, buildCount, buildDelete, buildInsert, buildSelect, buildUpdate, buildWriteBackupPlan, cfHanaBackupRoot, connect, createDriver, errorMessage, formatCsv, formatJson, formatResult, formatTable, query, withConnection, writeSqlBackup };
|
package/dist/index.js
CHANGED
|
@@ -23,12 +23,36 @@ var CredentialsNotFoundError = class extends CfHanaError {
|
|
|
23
23
|
};
|
|
24
24
|
var QueryError = class extends CfHanaError {
|
|
25
25
|
sqlState;
|
|
26
|
+
databaseCode;
|
|
26
27
|
constructor(message, options) {
|
|
27
28
|
super("QUERY", message, options);
|
|
28
29
|
this.name = "QueryError";
|
|
29
30
|
this.sqlState = options?.sqlState;
|
|
31
|
+
this.databaseCode = options?.databaseCode;
|
|
30
32
|
}
|
|
31
33
|
};
|
|
34
|
+
function isDatabaseErrorShape(error) {
|
|
35
|
+
return typeof error === "object" && error !== null && "code" in error;
|
|
36
|
+
}
|
|
37
|
+
function safeDatabaseCode(value) {
|
|
38
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
const parsed = Number(value);
|
|
45
|
+
return Number.isSafeInteger(parsed) ? parsed : void 0;
|
|
46
|
+
}
|
|
47
|
+
function databaseCode(error) {
|
|
48
|
+
if (error instanceof QueryError) {
|
|
49
|
+
return error.databaseCode ?? databaseCode(error.cause);
|
|
50
|
+
}
|
|
51
|
+
if (isDatabaseErrorShape(error)) {
|
|
52
|
+
return safeDatabaseCode(error.code);
|
|
53
|
+
}
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
32
56
|
var ReadOnlyViolationError = class extends CfHanaError {
|
|
33
57
|
constructor(message, options) {
|
|
34
58
|
super("READ_ONLY_VIOLATION", message, options);
|
|
@@ -48,8 +72,17 @@ function errorMessage(error) {
|
|
|
48
72
|
return String(error);
|
|
49
73
|
}
|
|
50
74
|
|
|
75
|
+
// src/lob.ts
|
|
76
|
+
var TEXT_LOB_TYPES = /* @__PURE__ */ new Set(["CLOB", "NCLOB"]);
|
|
77
|
+
function normalizeTypeName(typeName2) {
|
|
78
|
+
return typeName2?.trim().toUpperCase() ?? "";
|
|
79
|
+
}
|
|
80
|
+
function isTextLobType(typeName2) {
|
|
81
|
+
return TEXT_LOB_TYPES.has(normalizeTypeName(typeName2));
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
// src/format.ts
|
|
52
|
-
function cellText(value, nullText) {
|
|
85
|
+
function cellText(value, nullText, column) {
|
|
53
86
|
if (value === null) {
|
|
54
87
|
return nullText;
|
|
55
88
|
}
|
|
@@ -57,14 +90,14 @@ function cellText(value, nullText) {
|
|
|
57
90
|
return value.toISOString();
|
|
58
91
|
}
|
|
59
92
|
if (Buffer.isBuffer(value)) {
|
|
60
|
-
return `0x${value.toString("hex")}`;
|
|
93
|
+
return isTextLobType(column?.typeName) ? value.toString("utf8") : `0x${value.toString("hex")}`;
|
|
61
94
|
}
|
|
62
95
|
if (typeof value === "boolean") {
|
|
63
96
|
return value ? "true" : "false";
|
|
64
97
|
}
|
|
65
98
|
return typeof value === "number" ? value.toString() : value;
|
|
66
99
|
}
|
|
67
|
-
function serializeCell(value) {
|
|
100
|
+
function serializeCell(value, column) {
|
|
68
101
|
if (value === null) {
|
|
69
102
|
return null;
|
|
70
103
|
}
|
|
@@ -72,7 +105,7 @@ function serializeCell(value) {
|
|
|
72
105
|
return value.toISOString();
|
|
73
106
|
}
|
|
74
107
|
if (Buffer.isBuffer(value)) {
|
|
75
|
-
return `0x${value.toString("hex")}`;
|
|
108
|
+
return isTextLobType(column?.typeName) ? value.toString("utf8") : `0x${value.toString("hex")}`;
|
|
76
109
|
}
|
|
77
110
|
return value;
|
|
78
111
|
}
|
|
@@ -88,7 +121,7 @@ function formatTable(result) {
|
|
|
88
121
|
}
|
|
89
122
|
const headers = result.columns.map((column) => column.name);
|
|
90
123
|
const rows = result.rows.map(
|
|
91
|
-
(row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL"))
|
|
124
|
+
(row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL", column))
|
|
92
125
|
);
|
|
93
126
|
const widths = headers.map((header, index) => {
|
|
94
127
|
const widest = rows.reduce(
|
|
@@ -106,7 +139,8 @@ function formatJson(result) {
|
|
|
106
139
|
const rows = result.rows.map((row) => {
|
|
107
140
|
const serialized = {};
|
|
108
141
|
for (const [key, value] of Object.entries(row)) {
|
|
109
|
-
|
|
142
|
+
const column = result.columns.find((item) => item.name === key);
|
|
143
|
+
serialized[key] = serializeCell(value, column);
|
|
110
144
|
}
|
|
111
145
|
return serialized;
|
|
112
146
|
});
|
|
@@ -117,7 +151,7 @@ function formatCsv(result) {
|
|
|
117
151
|
const lines = [headers.map((header) => csvEscape(header)).join(",")];
|
|
118
152
|
for (const row of result.rows) {
|
|
119
153
|
lines.push(
|
|
120
|
-
result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, ""))).join(",")
|
|
154
|
+
result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, "", column))).join(",")
|
|
121
155
|
);
|
|
122
156
|
}
|
|
123
157
|
return lines.join("\r\n");
|
|
@@ -562,6 +596,18 @@ async function listTables(connection, schema) {
|
|
|
562
596
|
rowCount: void 0
|
|
563
597
|
}));
|
|
564
598
|
}
|
|
599
|
+
async function listCatalogObjects(connection, schema) {
|
|
600
|
+
const result = await connection.query(
|
|
601
|
+
"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",
|
|
602
|
+
[schema, schema],
|
|
603
|
+
{ autoLimit: false }
|
|
604
|
+
);
|
|
605
|
+
return result.rows.map((row) => ({
|
|
606
|
+
schema: row.SCHEMA_NAME,
|
|
607
|
+
name: row.OBJECT_NAME,
|
|
608
|
+
type: row.OBJECT_TYPE
|
|
609
|
+
}));
|
|
610
|
+
}
|
|
565
611
|
async function listColumns(connection, schema, table) {
|
|
566
612
|
const result = await connection.query(
|
|
567
613
|
"SELECT COLUMN_NAME, DATA_TYPE_NAME, LENGTH, SCALE, IS_NULLABLE, POSITION FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME = ? AND TABLE_NAME = ? ORDER BY POSITION",
|
|
@@ -579,7 +625,7 @@ async function listColumns(connection, schema, table) {
|
|
|
579
625
|
}
|
|
580
626
|
|
|
581
627
|
// src/config.ts
|
|
582
|
-
var CLI_VERSION = "0.
|
|
628
|
+
var CLI_VERSION = "0.3.1";
|
|
583
629
|
var ENV_PREFIX = "CF_HANA";
|
|
584
630
|
var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
585
631
|
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
@@ -1037,12 +1083,88 @@ function toConnectionTarget(binding, role) {
|
|
|
1037
1083
|
|
|
1038
1084
|
// src/driver/fake.ts
|
|
1039
1085
|
import { appendFile } from "fs/promises";
|
|
1086
|
+
var catalogFailureInjected = false;
|
|
1087
|
+
function catalogObjects() {
|
|
1088
|
+
return [
|
|
1089
|
+
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "EXISTING_TABLE", OBJECT_TYPE: "TABLE" },
|
|
1090
|
+
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "MISSING_TABLE_FIXED", OBJECT_TYPE: "TABLE" },
|
|
1091
|
+
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "MISSING_TABLE_VIEW", OBJECT_TYPE: "VIEW" },
|
|
1092
|
+
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "STATUS_ITEMS", OBJECT_TYPE: "TABLE" },
|
|
1093
|
+
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "CORE_AUTH_SCOPE", OBJECT_TYPE: "TABLE" }
|
|
1094
|
+
];
|
|
1095
|
+
}
|
|
1096
|
+
function tableColumns() {
|
|
1097
|
+
return [
|
|
1098
|
+
{ COLUMN_NAME: "ID", DATA_TYPE_NAME: "INTEGER", LENGTH: null, SCALE: null, IS_NULLABLE: "FALSE", POSITION: 1 },
|
|
1099
|
+
{ COLUMN_NAME: "IS_ACTIVE", DATA_TYPE_NAME: "BOOLEAN", LENGTH: null, SCALE: null, IS_NULLABLE: "TRUE", POSITION: 2 },
|
|
1100
|
+
{ COLUMN_NAME: "SCOPE_NAME", DATA_TYPE_NAME: "NVARCHAR", LENGTH: 255, SCALE: null, IS_NULLABLE: "TRUE", POSITION: 3 }
|
|
1101
|
+
];
|
|
1102
|
+
}
|
|
1040
1103
|
function fakeExec(sql) {
|
|
1041
1104
|
const kind = classifyStatement(sql);
|
|
1042
1105
|
const forcedFailure = readEnv(envName("FAKE_FAIL_STATEMENT"))?.toLowerCase();
|
|
1043
1106
|
if (forcedFailure === kind) {
|
|
1044
1107
|
throw new Error(`fake driver forced ${kind.toUpperCase()} failure`);
|
|
1045
1108
|
}
|
|
1109
|
+
const upperSql = sql.toUpperCase();
|
|
1110
|
+
if (upperSql.includes("SYS.TABLES") && upperSql.includes("SYS.VIEWS")) {
|
|
1111
|
+
if (readEnv(envName("FAKE_FAIL_CATALOG_ONCE")) === "1" && !catalogFailureInjected) {
|
|
1112
|
+
catalogFailureInjected = true;
|
|
1113
|
+
throw new QueryError("fake transient catalog metadata failure");
|
|
1114
|
+
}
|
|
1115
|
+
return {
|
|
1116
|
+
rows: catalogObjects(),
|
|
1117
|
+
columns: [
|
|
1118
|
+
{ name: "SCHEMA_NAME", typeName: "NVARCHAR" },
|
|
1119
|
+
{ name: "OBJECT_NAME", typeName: "NVARCHAR" },
|
|
1120
|
+
{ name: "OBJECT_TYPE", typeName: "NVARCHAR" }
|
|
1121
|
+
],
|
|
1122
|
+
affectedRows: 0
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
if (upperSql.includes("SYS.TABLE_COLUMNS")) {
|
|
1126
|
+
return {
|
|
1127
|
+
rows: tableColumns(),
|
|
1128
|
+
columns: [
|
|
1129
|
+
{ name: "COLUMN_NAME", typeName: "NVARCHAR" },
|
|
1130
|
+
{ name: "DATA_TYPE_NAME", typeName: "NVARCHAR" },
|
|
1131
|
+
{ name: "LENGTH", typeName: "INTEGER" },
|
|
1132
|
+
{ name: "SCALE", typeName: "INTEGER" },
|
|
1133
|
+
{ name: "IS_NULLABLE", typeName: "NVARCHAR" },
|
|
1134
|
+
{ name: "POSITION", typeName: "INTEGER" }
|
|
1135
|
+
],
|
|
1136
|
+
affectedRows: 0
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
if (upperSql.includes("MISSING_TABLE") || upperSql.includes("MISSING_TABLES")) {
|
|
1140
|
+
throw new QueryError("invalid table name: MISSING_TABLE", { sqlState: "42S02" });
|
|
1141
|
+
}
|
|
1142
|
+
if (upperSql.includes("ISACTIVE")) {
|
|
1143
|
+
throw new QueryError("invalid column name: ISACTIVE: line 1 col 8 (at pos 7)", { databaseCode: 260 });
|
|
1144
|
+
}
|
|
1145
|
+
if (upperSql.includes("LOB_ORDER_ERROR")) {
|
|
1146
|
+
throw new QueryError("inconsistent datatype: LOB type is not allowed in ORDER BY clause", { databaseCode: 266 });
|
|
1147
|
+
}
|
|
1148
|
+
if (upperSql.includes("LOB_GROUP_ERROR")) {
|
|
1149
|
+
throw new QueryError("inconsistent datatype: LOB type is not allowed in GROUP BY clause", { databaseCode: 274 });
|
|
1150
|
+
}
|
|
1151
|
+
if (sql.toUpperCase().includes("LOB_FIXTURE")) {
|
|
1152
|
+
return {
|
|
1153
|
+
rows: [
|
|
1154
|
+
{
|
|
1155
|
+
LOG_CONTENT: Buffer.from("Example log entry", "utf8"),
|
|
1156
|
+
CLOB_CONTENT: Buffer.from("Clob log entry", "utf8"),
|
|
1157
|
+
PAYLOAD: Buffer.from([0, 1, 2, 255])
|
|
1158
|
+
}
|
|
1159
|
+
],
|
|
1160
|
+
columns: [
|
|
1161
|
+
{ name: "LOG_CONTENT", typeName: "NCLOB" },
|
|
1162
|
+
{ name: "CLOB_CONTENT", typeName: "CLOB" },
|
|
1163
|
+
{ name: "PAYLOAD", typeName: "BLOB" }
|
|
1164
|
+
],
|
|
1165
|
+
affectedRows: 0
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1046
1168
|
if (sql.toUpperCase().includes("DUMMY")) {
|
|
1047
1169
|
return {
|
|
1048
1170
|
rows: [{ "1": 1 }],
|
|
@@ -1150,7 +1272,12 @@ function extractSqlState(error) {
|
|
|
1150
1272
|
}
|
|
1151
1273
|
function toQueryError(error) {
|
|
1152
1274
|
const sqlState = extractSqlState(error);
|
|
1153
|
-
|
|
1275
|
+
const code = databaseCode(error);
|
|
1276
|
+
return new QueryError(error.message, {
|
|
1277
|
+
cause: error,
|
|
1278
|
+
...sqlState === void 0 ? {} : { sqlState },
|
|
1279
|
+
...code === void 0 ? {} : { databaseCode: code }
|
|
1280
|
+
});
|
|
1154
1281
|
}
|
|
1155
1282
|
function toColumns(metadata) {
|
|
1156
1283
|
if (metadata === void 0) {
|
|
@@ -2042,6 +2169,10 @@ var HanaClient = class _HanaClient {
|
|
|
2042
2169
|
async listTables(schema) {
|
|
2043
2170
|
return await this.pool.withConnection((connection) => listTables(connection, schema));
|
|
2044
2171
|
}
|
|
2172
|
+
/** List table and view names in a schema for typo suggestions. */
|
|
2173
|
+
async listCatalogObjects(schema) {
|
|
2174
|
+
return await this.pool.withConnection((connection) => listCatalogObjects(connection, schema));
|
|
2175
|
+
}
|
|
2045
2176
|
/** List the columns of a table. */
|
|
2046
2177
|
async listColumns(schema, table) {
|
|
2047
2178
|
return await this.pool.withConnection(
|