@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/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
- serialized[key] = serializeCell(value);
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.2.1";
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
- return sqlState === void 0 ? new QueryError(error.message, { cause: error }) : new QueryError(error.message, { cause: error, sqlState });
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(