@saptools/cf-hana 0.1.0

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/cli.js ADDED
@@ -0,0 +1,1425 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/errors.ts
7
+ var CfHanaError = class extends Error {
8
+ code;
9
+ constructor(code, message, options) {
10
+ super(message, options);
11
+ this.name = "CfHanaError";
12
+ this.code = code;
13
+ Object.setPrototypeOf(this, new.target.prototype);
14
+ }
15
+ };
16
+ var CredentialsNotFoundError = class extends CfHanaError {
17
+ constructor(message, options) {
18
+ super("CREDENTIALS_NOT_FOUND", message, options);
19
+ this.name = "CredentialsNotFoundError";
20
+ }
21
+ };
22
+ var QueryError = class extends CfHanaError {
23
+ sqlState;
24
+ constructor(message, options) {
25
+ super("QUERY", message, options);
26
+ this.name = "QueryError";
27
+ this.sqlState = options?.sqlState;
28
+ }
29
+ };
30
+ var ReadOnlyViolationError = class extends CfHanaError {
31
+ constructor(message, options) {
32
+ super("READ_ONLY_VIOLATION", message, options);
33
+ this.name = "ReadOnlyViolationError";
34
+ }
35
+ };
36
+ var DestructiveStatementError = class extends CfHanaError {
37
+ constructor(message, options) {
38
+ super("DESTRUCTIVE_BLOCKED", message, options);
39
+ this.name = "DestructiveStatementError";
40
+ }
41
+ };
42
+ function errorMessage(error) {
43
+ if (error instanceof Error) {
44
+ return error.message;
45
+ }
46
+ return String(error);
47
+ }
48
+
49
+ // src/statements.ts
50
+ var LEADING_NOISE = /^(?:\s|--[^\n]*\n?|\/\*[\s\S]*?\*\/)+/;
51
+ var SELECT_KEYWORDS = /* @__PURE__ */ new Set(["SELECT", "WITH"]);
52
+ var DML_KEYWORDS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE", "MERGE", "UPSERT", "REPLACE"]);
53
+ var DDL_KEYWORDS = /* @__PURE__ */ new Set(["CREATE", "DROP", "ALTER", "TRUNCATE", "RENAME", "COMMENT"]);
54
+ function firstKeyword(sql) {
55
+ const stripped = sql.replace(LEADING_NOISE, "");
56
+ const match = /^[A-Za-z]+/.exec(stripped);
57
+ return (match?.[0] ?? "").toUpperCase();
58
+ }
59
+ function classifyStatement(sql) {
60
+ const keyword = firstKeyword(sql);
61
+ if (SELECT_KEYWORDS.has(keyword)) {
62
+ return "select";
63
+ }
64
+ if (DML_KEYWORDS.has(keyword)) {
65
+ return "dml";
66
+ }
67
+ if (DDL_KEYWORDS.has(keyword)) {
68
+ return "ddl";
69
+ }
70
+ return "unknown";
71
+ }
72
+ function quoteIdentifier(identifier) {
73
+ if (identifier.length === 0) {
74
+ throw new QueryError("A SQL identifier must not be empty");
75
+ }
76
+ if (identifier.includes("\0")) {
77
+ throw new QueryError("A SQL identifier must not contain a NUL character");
78
+ }
79
+ return `"${identifier.replace(/"/g, '""')}"`;
80
+ }
81
+ function qualifiedName(schema, table) {
82
+ return `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
83
+ }
84
+ function countPlaceholders(sql) {
85
+ let count = 0;
86
+ let index = 0;
87
+ const length = sql.length;
88
+ while (index < length) {
89
+ const char = sql[index];
90
+ if (char === "'" || char === '"') {
91
+ const quote = char;
92
+ index += 1;
93
+ while (index < length) {
94
+ if (sql[index] === quote) {
95
+ if (sql[index + 1] === quote) {
96
+ index += 2;
97
+ continue;
98
+ }
99
+ index += 1;
100
+ break;
101
+ }
102
+ index += 1;
103
+ }
104
+ continue;
105
+ }
106
+ if (char === "-" && sql[index + 1] === "-") {
107
+ index += 2;
108
+ while (index < length && sql[index] !== "\n") {
109
+ index += 1;
110
+ }
111
+ continue;
112
+ }
113
+ if (char === "/" && sql[index + 1] === "*") {
114
+ index += 2;
115
+ while (index < length && !(sql[index] === "*" && sql[index + 1] === "/")) {
116
+ index += 1;
117
+ }
118
+ index += 2;
119
+ continue;
120
+ }
121
+ if (char === "?") {
122
+ count += 1;
123
+ }
124
+ index += 1;
125
+ }
126
+ return count;
127
+ }
128
+ function assertParamArity(sql, params) {
129
+ const expected = countPlaceholders(sql);
130
+ if (expected !== params.length) {
131
+ throw new QueryError(
132
+ `SQL expects ${String(expected)} bound parameter(s) but received ${String(params.length)} value(s)`
133
+ );
134
+ }
135
+ }
136
+
137
+ // src/builder.ts
138
+ function buildWhere(where) {
139
+ if (where === void 0) {
140
+ return { clause: "", params: [] };
141
+ }
142
+ const entries = Object.entries(where);
143
+ if (entries.length === 0) {
144
+ return { clause: "", params: [] };
145
+ }
146
+ const conditions = entries.map(([column]) => `${quoteIdentifier(column)} = ?`);
147
+ return {
148
+ clause: ` WHERE ${conditions.join(" AND ")}`,
149
+ params: entries.map(([, value]) => value)
150
+ };
151
+ }
152
+ function buildSelect(spec) {
153
+ const columns = spec.columns === void 0 || spec.columns.length === 0 ? "*" : spec.columns.map((column) => quoteIdentifier(column)).join(", ");
154
+ const where = buildWhere(spec.where);
155
+ const clauses = [
156
+ `SELECT ${columns} FROM ${qualifiedName(spec.schema, spec.table)}${where.clause}`
157
+ ];
158
+ if (spec.orderBy !== void 0 && spec.orderBy.length > 0) {
159
+ clauses.push(`ORDER BY ${spec.orderBy.map((column) => quoteIdentifier(column)).join(", ")}`);
160
+ }
161
+ if (spec.limit !== void 0) {
162
+ clauses.push(`LIMIT ${String(spec.limit)}`);
163
+ }
164
+ if (spec.offset !== void 0) {
165
+ clauses.push(`OFFSET ${String(spec.offset)}`);
166
+ }
167
+ return { sql: clauses.join(" "), params: where.params };
168
+ }
169
+ function buildCount(spec) {
170
+ const where = buildWhere(spec.where);
171
+ return {
172
+ sql: `SELECT COUNT(*) AS "COUNT" FROM ${qualifiedName(spec.schema, spec.table)}${where.clause}`,
173
+ params: where.params
174
+ };
175
+ }
176
+ function buildInsert(schema, table, values) {
177
+ const entries = Object.entries(values);
178
+ if (entries.length === 0) {
179
+ throw new QueryError("INSERT requires at least one column value");
180
+ }
181
+ const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
182
+ const placeholders = entries.map(() => "?").join(", ");
183
+ return {
184
+ sql: `INSERT INTO ${qualifiedName(schema, table)} (${columns}) VALUES (${placeholders})`,
185
+ params: entries.map(([, value]) => value)
186
+ };
187
+ }
188
+ function buildUpdate(schema, table, values, where) {
189
+ const valueEntries = Object.entries(values);
190
+ if (valueEntries.length === 0) {
191
+ throw new QueryError("UPDATE requires at least one column value");
192
+ }
193
+ const whereClause = buildWhere(where);
194
+ if (whereClause.clause === "") {
195
+ throw new QueryError("UPDATE requires a non-empty WHERE filter to avoid a full-table write");
196
+ }
197
+ const assignments = valueEntries.map(([column]) => `${quoteIdentifier(column)} = ?`).join(", ");
198
+ return {
199
+ sql: `UPDATE ${qualifiedName(schema, table)} SET ${assignments}${whereClause.clause}`,
200
+ params: [...valueEntries.map(([, value]) => value), ...whereClause.params]
201
+ };
202
+ }
203
+ function buildDelete(schema, table, where) {
204
+ const whereClause = buildWhere(where);
205
+ if (whereClause.clause === "") {
206
+ throw new QueryError("DELETE requires a non-empty WHERE filter to avoid a full-table delete");
207
+ }
208
+ return {
209
+ sql: `DELETE FROM ${qualifiedName(schema, table)}${whereClause.clause}`,
210
+ params: whereClause.params
211
+ };
212
+ }
213
+
214
+ // src/catalog.ts
215
+ async function listSchemas(connection) {
216
+ const result = await connection.query(
217
+ "SELECT SCHEMA_NAME FROM SYS.SCHEMAS ORDER BY SCHEMA_NAME"
218
+ );
219
+ return result.rows.map((row) => row.SCHEMA_NAME);
220
+ }
221
+ async function listTables(connection, schema) {
222
+ const result = await connection.query(
223
+ "SELECT SCHEMA_NAME, TABLE_NAME, TABLE_TYPE FROM SYS.TABLES WHERE SCHEMA_NAME = ? ORDER BY TABLE_NAME",
224
+ [schema]
225
+ );
226
+ return result.rows.map((row) => ({
227
+ schema: row.SCHEMA_NAME,
228
+ name: row.TABLE_NAME,
229
+ type: row.TABLE_TYPE,
230
+ rowCount: void 0
231
+ }));
232
+ }
233
+ async function listColumns(connection, schema, table) {
234
+ const result = await connection.query(
235
+ "SELECT COLUMN_NAME, DATA_TYPE_NAME, LENGTH, SCALE, IS_NULLABLE, POSITION FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME = ? AND TABLE_NAME = ? ORDER BY POSITION",
236
+ [schema, table]
237
+ );
238
+ return result.rows.map((row) => ({
239
+ name: row.COLUMN_NAME,
240
+ dataType: row.DATA_TYPE_NAME,
241
+ length: row.LENGTH ?? void 0,
242
+ scale: row.SCALE ?? void 0,
243
+ nullable: row.IS_NULLABLE === "TRUE",
244
+ position: row.POSITION
245
+ }));
246
+ }
247
+
248
+ // src/config.ts
249
+ var CLI_NAME = "cf-hana";
250
+ var CLI_VERSION = "0.1.0";
251
+ var ENV_PREFIX = "CF_HANA";
252
+ var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
253
+ var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
254
+ var DEFAULT_POOL_MAX = 4;
255
+ var DEFAULT_POOL_IDLE_MS = 6e4;
256
+ var DEFAULT_AUTO_LIMIT = 1e3;
257
+ function envName(suffix) {
258
+ return `${ENV_PREFIX}_${suffix}`;
259
+ }
260
+ function readEnv(name) {
261
+ const value = process.env[name];
262
+ if (value === void 0) {
263
+ return void 0;
264
+ }
265
+ const trimmed = value.trim();
266
+ return trimmed.length === 0 ? void 0 : trimmed;
267
+ }
268
+ function readSapCredentials(overrides) {
269
+ const email = overrides?.email ?? readEnv("SAP_EMAIL");
270
+ const password = overrides?.password ?? readEnv("SAP_PASSWORD");
271
+ if (email === void 0 || password === void 0) {
272
+ return void 0;
273
+ }
274
+ return { email, password };
275
+ }
276
+
277
+ // src/credentials.ts
278
+ import { fetchAppDbBindings, readDbAppView } from "@saptools/cf-sync";
279
+ async function resolveAppBindings(selector, options) {
280
+ if (options.refresh !== true) {
281
+ const view = await readDbAppView(selector);
282
+ if (view !== void 0 && view.entry.bindings.length > 0) {
283
+ return {
284
+ selector: view.entry.selector,
285
+ appName: view.entry.appName,
286
+ bindings: view.entry.bindings,
287
+ source: "cache"
288
+ };
289
+ }
290
+ }
291
+ const credentials = readSapCredentials({
292
+ email: options.email,
293
+ password: options.password
294
+ });
295
+ if (credentials === void 0) {
296
+ throw new CredentialsNotFoundError(
297
+ `No cached HANA credentials for "${selector}". Run \`cf-sync db-sync ${selector}\` first, or set SAP_EMAIL and SAP_PASSWORD to fetch them live.`
298
+ );
299
+ }
300
+ const fetched = await fetchAppDbBindings({
301
+ selector,
302
+ email: credentials.email,
303
+ password: credentials.password
304
+ });
305
+ if (fetched.bindings.length === 0) {
306
+ throw new CredentialsNotFoundError(
307
+ `App "${fetched.selector}" has no HANA service binding.`
308
+ );
309
+ }
310
+ return {
311
+ selector: fetched.selector,
312
+ appName: fetched.appName,
313
+ bindings: fetched.bindings,
314
+ source: "fresh"
315
+ };
316
+ }
317
+ function selectBinding(bindings, selector) {
318
+ if (selector.bindingName !== void 0) {
319
+ const match = bindings.find((binding) => binding.name === selector.bindingName);
320
+ if (match === void 0) {
321
+ throw new CfHanaError(
322
+ "AMBIGUOUS_BINDING",
323
+ `No HANA binding named "${selector.bindingName}"`
324
+ );
325
+ }
326
+ return match;
327
+ }
328
+ if (selector.bindingIndex !== void 0) {
329
+ const match = bindings[selector.bindingIndex];
330
+ if (match === void 0) {
331
+ throw new CfHanaError(
332
+ "AMBIGUOUS_BINDING",
333
+ `No HANA binding at index ${String(selector.bindingIndex)}`
334
+ );
335
+ }
336
+ return match;
337
+ }
338
+ const first = bindings[0];
339
+ if (first === void 0) {
340
+ throw new CredentialsNotFoundError("No HANA bindings are available for this app");
341
+ }
342
+ if (bindings.length > 1) {
343
+ const labels = bindings.map((binding, index) => binding.name ?? `#${String(index)}`).join(", ");
344
+ throw new CfHanaError(
345
+ "AMBIGUOUS_BINDING",
346
+ `App has multiple HANA bindings (${labels}); choose one with bindingName or bindingIndex`
347
+ );
348
+ }
349
+ return first;
350
+ }
351
+ function toConnectionTarget(binding, role) {
352
+ const credentials = binding.credentials;
353
+ const port = Number.parseInt(credentials.port, 10);
354
+ if (!Number.isInteger(port) || port <= 0) {
355
+ throw new CfHanaError("CONFIG", `Invalid HANA port in binding: "${credentials.port}"`);
356
+ }
357
+ return {
358
+ host: credentials.host,
359
+ port,
360
+ user: role === "hdi" ? credentials.hdiUser : credentials.user,
361
+ password: role === "hdi" ? credentials.hdiPassword : credentials.password,
362
+ schema: credentials.schema,
363
+ certificate: credentials.certificate,
364
+ databaseId: credentials.databaseId
365
+ };
366
+ }
367
+
368
+ // src/driver/fake.ts
369
+ function fakeExec(sql) {
370
+ if (sql.toUpperCase().includes("DUMMY")) {
371
+ return {
372
+ rows: [{ "1": 1 }],
373
+ columns: [{ name: "1", typeName: "INTEGER" }],
374
+ affectedRows: 0
375
+ };
376
+ }
377
+ const kind = classifyStatement(sql);
378
+ if (kind === "select") {
379
+ return {
380
+ rows: [
381
+ { ID: 1, NAME: "sample-row" },
382
+ { ID: 2, NAME: "second-row" }
383
+ ],
384
+ columns: [
385
+ { name: "ID", typeName: "INTEGER" },
386
+ { name: "NAME", typeName: "NVARCHAR" }
387
+ ],
388
+ affectedRows: 0
389
+ };
390
+ }
391
+ if (kind === "dml") {
392
+ return { rows: [], columns: [], affectedRows: 1 };
393
+ }
394
+ return { rows: [], columns: [], affectedRows: 0 };
395
+ }
396
+ var FakeConnection = class {
397
+ closed = false;
398
+ exec(sql, _params) {
399
+ return Promise.resolve(fakeExec(sql));
400
+ }
401
+ setAutoCommit(_enabled) {
402
+ return Promise.resolve();
403
+ }
404
+ commit() {
405
+ return Promise.resolve();
406
+ }
407
+ rollback() {
408
+ return Promise.resolve();
409
+ }
410
+ close() {
411
+ this.closed = true;
412
+ return Promise.resolve();
413
+ }
414
+ isClosed() {
415
+ return this.closed;
416
+ }
417
+ };
418
+ function createFakeDriver() {
419
+ return {
420
+ name: "fake",
421
+ connect: (_params) => Promise.resolve(new FakeConnection())
422
+ };
423
+ }
424
+
425
+ // src/driver/hdb.ts
426
+ import { createClient } from "hdb";
427
+ var TYPE_NAMES = {
428
+ 0: "NULL",
429
+ 1: "TINYINT",
430
+ 2: "SMALLINT",
431
+ 3: "INTEGER",
432
+ 4: "BIGINT",
433
+ 5: "DECIMAL",
434
+ 6: "REAL",
435
+ 7: "DOUBLE",
436
+ 8: "CHAR",
437
+ 9: "VARCHAR",
438
+ 10: "NCHAR",
439
+ 11: "NVARCHAR",
440
+ 12: "BINARY",
441
+ 13: "VARBINARY",
442
+ 14: "DATE",
443
+ 15: "TIME",
444
+ 16: "TIMESTAMP",
445
+ 25: "CLOB",
446
+ 26: "NCLOB",
447
+ 27: "BLOB",
448
+ 28: "BOOLEAN",
449
+ 29: "STRING",
450
+ 30: "NSTRING",
451
+ 47: "DECIMAL",
452
+ 51: "TEXT",
453
+ 52: "SHORTTEXT",
454
+ 62: "DECIMAL"
455
+ };
456
+ function typeName(code) {
457
+ return TYPE_NAMES[code] ?? `TYPE_${String(code)}`;
458
+ }
459
+ function extractSqlState(error) {
460
+ const value = error.sqlState;
461
+ return typeof value === "string" ? value : void 0;
462
+ }
463
+ function toQueryError(error) {
464
+ const sqlState = extractSqlState(error);
465
+ return sqlState === void 0 ? new QueryError(error.message, { cause: error }) : new QueryError(error.message, { cause: error, sqlState });
466
+ }
467
+ function toColumns(metadata) {
468
+ if (metadata === void 0) {
469
+ return [];
470
+ }
471
+ return metadata.map((column) => ({
472
+ name: column.columnDisplayName ?? column.columnName ?? "",
473
+ typeName: typeName(column.dataType)
474
+ }));
475
+ }
476
+ function toExecResult(statement, raw) {
477
+ if (typeof raw === "number") {
478
+ return { rows: [], columns: [], affectedRows: raw };
479
+ }
480
+ if (Array.isArray(raw)) {
481
+ return {
482
+ rows: raw,
483
+ columns: toColumns(statement.resultSetMetadata),
484
+ affectedRows: 0
485
+ };
486
+ }
487
+ return { rows: [], columns: [], affectedRows: 0 };
488
+ }
489
+ var HdbConnection = class {
490
+ constructor(client) {
491
+ this.client = client;
492
+ }
493
+ client;
494
+ closed = false;
495
+ async exec(sql, params) {
496
+ const statement = await this.prepareStatement(sql);
497
+ try {
498
+ const raw = await this.executeStatement(statement, params);
499
+ return toExecResult(statement, raw);
500
+ } finally {
501
+ statement.drop();
502
+ }
503
+ }
504
+ setAutoCommit(enabled) {
505
+ this.client.setAutoCommit(enabled);
506
+ return Promise.resolve();
507
+ }
508
+ async commit() {
509
+ await new Promise((resolve, reject) => {
510
+ this.client.commit((error) => {
511
+ if (error) {
512
+ reject(toQueryError(error));
513
+ } else {
514
+ resolve();
515
+ }
516
+ });
517
+ });
518
+ }
519
+ async rollback() {
520
+ await new Promise((resolve, reject) => {
521
+ this.client.rollback((error) => {
522
+ if (error) {
523
+ reject(toQueryError(error));
524
+ } else {
525
+ resolve();
526
+ }
527
+ });
528
+ });
529
+ }
530
+ async close() {
531
+ if (this.closed) {
532
+ return;
533
+ }
534
+ this.closed = true;
535
+ await new Promise((resolve) => {
536
+ this.client.disconnect(() => {
537
+ resolve();
538
+ });
539
+ });
540
+ this.client.close();
541
+ }
542
+ isClosed() {
543
+ return this.closed || this.client.readyState === "closed" || this.client.readyState === "disconnected";
544
+ }
545
+ async prepareStatement(sql) {
546
+ return await new Promise((resolve, reject) => {
547
+ this.client.prepare(sql, (error, statement) => {
548
+ if (error) {
549
+ reject(toQueryError(error));
550
+ } else {
551
+ resolve(statement);
552
+ }
553
+ });
554
+ });
555
+ }
556
+ async executeStatement(statement, params) {
557
+ return await new Promise((resolve, reject) => {
558
+ statement.exec([...params], (error, result) => {
559
+ if (error) {
560
+ reject(toQueryError(error));
561
+ } else {
562
+ resolve(result);
563
+ }
564
+ });
565
+ });
566
+ }
567
+ };
568
+ function openClient(client, timeoutMs) {
569
+ return new Promise((resolve, reject) => {
570
+ let settled = false;
571
+ const timer = setTimeout(() => {
572
+ if (settled) {
573
+ return;
574
+ }
575
+ settled = true;
576
+ client.close();
577
+ reject(
578
+ new CfHanaError(
579
+ "TIMEOUT",
580
+ `HANA connection timed out after ${String(timeoutMs)}ms`
581
+ )
582
+ );
583
+ }, timeoutMs);
584
+ timer.unref();
585
+ client.connect((error) => {
586
+ if (settled) {
587
+ return;
588
+ }
589
+ settled = true;
590
+ clearTimeout(timer);
591
+ if (error) {
592
+ reject(
593
+ new CfHanaError("CONNECTION", `Failed to connect to HANA: ${error.message}`, {
594
+ cause: error
595
+ })
596
+ );
597
+ } else {
598
+ resolve();
599
+ }
600
+ });
601
+ });
602
+ }
603
+ function setCurrentSchema(client, schema) {
604
+ return new Promise((resolve, reject) => {
605
+ client.exec(`SET SCHEMA ${quoteIdentifier(schema)}`, (error) => {
606
+ if (error) {
607
+ reject(toQueryError(error));
608
+ } else {
609
+ resolve();
610
+ }
611
+ });
612
+ });
613
+ }
614
+ async function connectHdb(params) {
615
+ const client = createClient({
616
+ host: params.host,
617
+ port: params.port,
618
+ user: params.user,
619
+ password: params.password,
620
+ ca: params.certificate,
621
+ useTLS: true
622
+ });
623
+ await openClient(client, params.connectTimeoutMs);
624
+ await setCurrentSchema(client, params.schema);
625
+ return new HdbConnection(client);
626
+ }
627
+ function createHdbDriver() {
628
+ return {
629
+ name: "hdb",
630
+ connect: connectHdb
631
+ };
632
+ }
633
+
634
+ // src/driver/index.ts
635
+ function createDriver(name) {
636
+ const resolved = name ?? readEnv(envName("DRIVER")) ?? "hdb";
637
+ switch (resolved) {
638
+ case "hdb":
639
+ return createHdbDriver();
640
+ case "fake":
641
+ return createFakeDriver();
642
+ default:
643
+ throw new CfHanaError("CONFIG", `Unknown HANA driver: ${resolved}`);
644
+ }
645
+ }
646
+
647
+ // src/safety.ts
648
+ var DESTRUCTIVE_DDL_KEYWORDS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "ALTER"]);
649
+ var UNSCOPED_WRITE_KEYWORDS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
650
+ function stripStringLiterals(sql) {
651
+ return sql.replace(/'(?:[^']|'')*'/g, "''");
652
+ }
653
+ function hasWhereClause(sql) {
654
+ return /\bwhere\b/i.test(stripStringLiterals(sql));
655
+ }
656
+ function inspectStatement(sql) {
657
+ const kind = classifyStatement(sql);
658
+ const keyword = firstKeyword(sql);
659
+ if (kind === "ddl") {
660
+ return { kind, destructive: DESTRUCTIVE_DDL_KEYWORDS.has(keyword) };
661
+ }
662
+ if (kind === "dml") {
663
+ return {
664
+ kind,
665
+ destructive: UNSCOPED_WRITE_KEYWORDS.has(keyword) && !hasWhereClause(sql)
666
+ };
667
+ }
668
+ return { kind, destructive: false };
669
+ }
670
+ function evaluateGuard(sql, config) {
671
+ const inspection = inspectStatement(sql);
672
+ if (config.readOnly && (inspection.kind === "dml" || inspection.kind === "ddl")) {
673
+ return {
674
+ allowed: false,
675
+ destructive: inspection.destructive,
676
+ violation: "read-only",
677
+ reason: `read-only mode blocks ${inspection.kind.toUpperCase()} statements`
678
+ };
679
+ }
680
+ if (inspection.destructive && !config.allowDestructive) {
681
+ return {
682
+ allowed: false,
683
+ destructive: true,
684
+ violation: "destructive",
685
+ reason: "destructive statement blocked (DROP/TRUNCATE/ALTER or unscoped UPDATE/DELETE); allow it explicitly to proceed"
686
+ };
687
+ }
688
+ return {
689
+ allowed: true,
690
+ destructive: inspection.destructive,
691
+ violation: void 0,
692
+ reason: void 0
693
+ };
694
+ }
695
+ function applyAutoLimit(sql, limit) {
696
+ if (limit === false || classifyStatement(sql) !== "select") {
697
+ return { sql, applied: false };
698
+ }
699
+ const stripped = stripStringLiterals(sql);
700
+ if (/\blimit\b/i.test(stripped) || /\btop\s+\d/i.test(stripped)) {
701
+ return { sql, applied: false };
702
+ }
703
+ const trimmed = sql.replace(/[\s;]+$/, "");
704
+ return { sql: `${trimmed} LIMIT ${String(limit)}`, applied: true };
705
+ }
706
+
707
+ // src/connection.ts
708
+ async function withTimeout(work, timeoutMs, onTimeout) {
709
+ let timer;
710
+ const timeout = new Promise((_resolve, reject) => {
711
+ timer = setTimeout(() => {
712
+ onTimeout();
713
+ reject(
714
+ new CfHanaError("TIMEOUT", `Statement timed out after ${String(timeoutMs)}ms`)
715
+ );
716
+ }, timeoutMs);
717
+ timer.unref();
718
+ });
719
+ try {
720
+ return await Promise.race([work, timeout]);
721
+ } finally {
722
+ if (timer !== void 0) {
723
+ clearTimeout(timer);
724
+ }
725
+ }
726
+ }
727
+ var Connection = class _Connection {
728
+ constructor(driverConnection, config) {
729
+ this.driverConnection = driverConnection;
730
+ this.config = config;
731
+ }
732
+ driverConnection;
733
+ config;
734
+ static async open(driver, config) {
735
+ const driverConnection = await driver.connect({
736
+ host: config.host,
737
+ port: config.port,
738
+ user: config.user,
739
+ password: config.password,
740
+ schema: config.schema,
741
+ certificate: config.certificate,
742
+ connectTimeoutMs: config.connectTimeoutMs
743
+ });
744
+ return new _Connection(driverConnection, config);
745
+ }
746
+ async query(sql, params, options) {
747
+ const result = await this.run(sql, params ?? [], options ?? {});
748
+ return result;
749
+ }
750
+ async execute(sql, params, options) {
751
+ return await this.run(sql, params ?? [], options ?? {});
752
+ }
753
+ async setAutoCommit(enabled) {
754
+ await this.driverConnection.setAutoCommit(enabled);
755
+ }
756
+ async commit() {
757
+ await this.driverConnection.commit();
758
+ }
759
+ async rollback() {
760
+ await this.driverConnection.rollback();
761
+ }
762
+ async close() {
763
+ await this.driverConnection.close();
764
+ }
765
+ get isClosed() {
766
+ return this.driverConnection.isClosed();
767
+ }
768
+ async run(sql, params, options) {
769
+ assertParamArity(sql, params);
770
+ const decision = evaluateGuard(sql, {
771
+ readOnly: this.config.readOnly,
772
+ allowDestructive: options.allowDestructive ?? this.config.allowDestructive
773
+ });
774
+ if (!decision.allowed) {
775
+ if (decision.violation === "read-only") {
776
+ throw new ReadOnlyViolationError(
777
+ decision.reason ?? "read-only mode blocks this statement"
778
+ );
779
+ }
780
+ throw new DestructiveStatementError(
781
+ decision.reason ?? "destructive statement blocked"
782
+ );
783
+ }
784
+ const kind = classifyStatement(sql);
785
+ const autoLimit = options.autoLimit ?? this.config.autoLimit;
786
+ const limited = kind === "select" ? applyAutoLimit(sql, autoLimit) : { sql, applied: false };
787
+ const timeoutMs = options.timeoutMs ?? this.config.queryTimeoutMs;
788
+ const started = Date.now();
789
+ const execResult = await withTimeout(
790
+ this.driverConnection.exec(limited.sql, params),
791
+ timeoutMs,
792
+ () => {
793
+ void this.closeQuietly();
794
+ }
795
+ );
796
+ const elapsedMs = Date.now() - started;
797
+ return {
798
+ rows: execResult.rows,
799
+ columns: execResult.columns,
800
+ rowCount: kind === "select" ? execResult.rows.length : execResult.affectedRows,
801
+ statement: kind,
802
+ truncated: limited.applied && execResult.rows.length === autoLimit,
803
+ elapsedMs
804
+ };
805
+ }
806
+ async closeQuietly() {
807
+ try {
808
+ await this.driverConnection.close();
809
+ } catch {
810
+ }
811
+ }
812
+ };
813
+
814
+ // src/pool.ts
815
+ async function closeQuietly(connection) {
816
+ try {
817
+ await connection.close();
818
+ } catch {
819
+ }
820
+ }
821
+ var ConnectionPool = class {
822
+ constructor(driver, config, options) {
823
+ this.driver = driver;
824
+ this.config = config;
825
+ this.maxSize = Math.max(1, options?.max ?? DEFAULT_POOL_MAX);
826
+ this.idleTimeoutMs = options?.idleTimeoutMs ?? DEFAULT_POOL_IDLE_MS;
827
+ }
828
+ driver;
829
+ config;
830
+ idle = [];
831
+ busy = /* @__PURE__ */ new Set();
832
+ waiters = [];
833
+ maxSize;
834
+ idleTimeoutMs;
835
+ created = 0;
836
+ draining = false;
837
+ /** Total connections currently owned by the pool (idle + busy). */
838
+ get size() {
839
+ return this.created;
840
+ }
841
+ /** Connections currently idle and immediately reusable. */
842
+ get available() {
843
+ return this.idle.length;
844
+ }
845
+ /** Borrow a connection, opening or queueing as needed. */
846
+ async acquire() {
847
+ if (this.draining) {
848
+ throw new CfHanaError("POOL_CLOSED", "Connection pool is draining");
849
+ }
850
+ const reused = this.takeIdleConnection();
851
+ if (reused !== void 0) {
852
+ this.busy.add(reused);
853
+ return reused;
854
+ }
855
+ if (this.created < this.maxSize) {
856
+ return await this.openBusyConnection();
857
+ }
858
+ return await new Promise((resolve, reject) => {
859
+ this.waiters.push({ resolve, reject });
860
+ });
861
+ }
862
+ /** Return a borrowed connection to the pool. */
863
+ release(connection) {
864
+ if (!this.busy.delete(connection)) {
865
+ return;
866
+ }
867
+ if (connection.isClosed) {
868
+ this.created -= 1;
869
+ this.servePendingWaiters();
870
+ return;
871
+ }
872
+ const waiter = this.waiters.shift();
873
+ if (waiter !== void 0) {
874
+ this.busy.add(connection);
875
+ waiter.resolve(connection);
876
+ return;
877
+ }
878
+ if (this.draining) {
879
+ this.created -= 1;
880
+ void closeQuietly(connection);
881
+ return;
882
+ }
883
+ this.idle.push({ connection, since: Date.now() });
884
+ }
885
+ /** Run `work` with a borrowed connection, releasing it afterwards. */
886
+ async withConnection(work) {
887
+ const connection = await this.acquire();
888
+ try {
889
+ return await work(connection);
890
+ } finally {
891
+ this.release(connection);
892
+ }
893
+ }
894
+ /** Reject queued waiters and close idle connections. */
895
+ async drain() {
896
+ this.draining = true;
897
+ while (this.waiters.length > 0) {
898
+ const waiter = this.waiters.shift();
899
+ if (waiter !== void 0) {
900
+ waiter.reject(new CfHanaError("POOL_CLOSED", "Connection pool was drained"));
901
+ }
902
+ }
903
+ const idleConnections = this.idle.splice(0).map((entry) => entry.connection);
904
+ this.created -= idleConnections.length;
905
+ await Promise.all(idleConnections.map((connection) => closeQuietly(connection)));
906
+ }
907
+ takeIdleConnection() {
908
+ for (; ; ) {
909
+ const entry = this.idle.pop();
910
+ if (entry === void 0) {
911
+ return void 0;
912
+ }
913
+ if (entry.connection.isClosed) {
914
+ this.created -= 1;
915
+ continue;
916
+ }
917
+ if (this.idleTimeoutMs > 0 && Date.now() - entry.since > this.idleTimeoutMs) {
918
+ this.created -= 1;
919
+ void closeQuietly(entry.connection);
920
+ continue;
921
+ }
922
+ return entry.connection;
923
+ }
924
+ }
925
+ async openBusyConnection() {
926
+ this.created += 1;
927
+ let connection;
928
+ try {
929
+ connection = await Connection.open(this.driver, this.config);
930
+ } catch (error) {
931
+ this.created -= 1;
932
+ throw error;
933
+ }
934
+ this.busy.add(connection);
935
+ return connection;
936
+ }
937
+ servePendingWaiters() {
938
+ while (this.waiters.length > 0 && this.created < this.maxSize) {
939
+ const waiter = this.waiters.shift();
940
+ if (waiter === void 0) {
941
+ return;
942
+ }
943
+ this.created += 1;
944
+ void Connection.open(this.driver, this.config).then(
945
+ (connection) => {
946
+ this.busy.add(connection);
947
+ waiter.resolve(connection);
948
+ },
949
+ (error) => {
950
+ this.created -= 1;
951
+ waiter.reject(error);
952
+ }
953
+ );
954
+ }
955
+ }
956
+ };
957
+
958
+ // src/transaction.ts
959
+ var Transaction = class {
960
+ constructor(connection) {
961
+ this.connection = connection;
962
+ }
963
+ connection;
964
+ finished = false;
965
+ /** Whether the transaction has already been committed or rolled back. */
966
+ get isFinished() {
967
+ return this.finished;
968
+ }
969
+ async query(sql, params, options) {
970
+ this.assertActive();
971
+ return await this.connection.query(sql, params, options);
972
+ }
973
+ async execute(sql, params, options) {
974
+ this.assertActive();
975
+ return await this.connection.execute(sql, params, options);
976
+ }
977
+ async commit() {
978
+ this.assertActive();
979
+ this.finished = true;
980
+ await this.connection.commit();
981
+ }
982
+ async rollback() {
983
+ this.assertActive();
984
+ this.finished = true;
985
+ await this.connection.rollback();
986
+ }
987
+ assertActive() {
988
+ if (this.finished) {
989
+ throw new CfHanaError(
990
+ "QUERY",
991
+ "This transaction has already been committed or rolled back"
992
+ );
993
+ }
994
+ }
995
+ };
996
+
997
+ // src/client.ts
998
+ var HanaClient = class _HanaClient {
999
+ constructor(pool, info) {
1000
+ this.pool = pool;
1001
+ this.info = info;
1002
+ }
1003
+ pool;
1004
+ info;
1005
+ /** Open a client for a `region/org/space/app` selector (or a bare app name). */
1006
+ static async connect(selector, options = {}) {
1007
+ const resolved = await resolveAppBindings(selector, options);
1008
+ const role = options.role ?? "runtime";
1009
+ const binding = selectBinding(resolved.bindings, options);
1010
+ const target = toConnectionTarget(binding, role);
1011
+ const driver = createDriver();
1012
+ const config = {
1013
+ host: target.host,
1014
+ port: target.port,
1015
+ user: target.user,
1016
+ password: target.password,
1017
+ schema: target.schema,
1018
+ certificate: target.certificate,
1019
+ connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
1020
+ queryTimeoutMs: options.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
1021
+ readOnly: options.readOnly ?? false,
1022
+ allowDestructive: options.allowDestructive ?? false,
1023
+ autoLimit: options.autoLimit ?? DEFAULT_AUTO_LIMIT
1024
+ };
1025
+ const poolOptions = options.pool === false ? { max: 1 } : options.pool ?? {};
1026
+ const info = {
1027
+ selector: resolved.selector,
1028
+ appName: resolved.appName,
1029
+ host: target.host,
1030
+ schema: target.schema,
1031
+ role,
1032
+ driver: driver.name,
1033
+ credentialSource: resolved.source
1034
+ };
1035
+ return new _HanaClient(new ConnectionPool(driver, config, poolOptions), info);
1036
+ }
1037
+ /** Run a SELECT (or any read) statement and return typed rows. */
1038
+ async query(sql, params, options) {
1039
+ return await this.pool.withConnection(
1040
+ (connection) => connection.query(sql, params, options)
1041
+ );
1042
+ }
1043
+ /** Run a DML/DDL statement and return its affected-row count. */
1044
+ async execute(sql, params, options) {
1045
+ return await this.pool.withConnection(
1046
+ (connection) => connection.execute(sql, params, options)
1047
+ );
1048
+ }
1049
+ /** Run a typed `SELECT` built from a spec. */
1050
+ async selectFrom(spec) {
1051
+ const built = buildSelect(spec);
1052
+ return await this.query(built.sql, built.params);
1053
+ }
1054
+ /** Count rows in a table, optionally filtered. */
1055
+ async count(spec) {
1056
+ const built = buildCount(spec);
1057
+ const result = await this.query(built.sql, built.params);
1058
+ return result.rows[0]?.COUNT ?? 0;
1059
+ }
1060
+ /** Insert a single row. */
1061
+ async insertInto(schema, table, values) {
1062
+ const built = buildInsert(schema, table, values);
1063
+ return await this.execute(built.sql, built.params);
1064
+ }
1065
+ /** Update rows matching a non-empty `where` filter. */
1066
+ async update(schema, table, values, where) {
1067
+ const built = buildUpdate(schema, table, values, where);
1068
+ return await this.execute(built.sql, built.params);
1069
+ }
1070
+ /** Delete rows matching a non-empty `where` filter. */
1071
+ async deleteFrom(schema, table, where) {
1072
+ const built = buildDelete(schema, table, where);
1073
+ return await this.execute(built.sql, built.params);
1074
+ }
1075
+ /** Run `work` inside a transaction, auto-committing on success. */
1076
+ async transaction(work) {
1077
+ return await this.pool.withConnection(async (connection) => {
1078
+ await connection.setAutoCommit(false);
1079
+ const tx = new Transaction(connection);
1080
+ try {
1081
+ const result = await work(tx);
1082
+ if (!tx.isFinished) {
1083
+ await tx.commit();
1084
+ }
1085
+ return result;
1086
+ } catch (error) {
1087
+ if (!tx.isFinished) {
1088
+ try {
1089
+ await tx.rollback();
1090
+ } catch {
1091
+ }
1092
+ }
1093
+ throw error;
1094
+ } finally {
1095
+ await connection.setAutoCommit(true);
1096
+ }
1097
+ });
1098
+ }
1099
+ /** List every schema visible to the connected user. */
1100
+ async listSchemas() {
1101
+ return await this.pool.withConnection((connection) => listSchemas(connection));
1102
+ }
1103
+ /** List the tables in a schema. */
1104
+ async listTables(schema) {
1105
+ return await this.pool.withConnection((connection) => listTables(connection, schema));
1106
+ }
1107
+ /** List the columns of a table. */
1108
+ async listColumns(schema, table) {
1109
+ return await this.pool.withConnection(
1110
+ (connection) => listColumns(connection, schema, table)
1111
+ );
1112
+ }
1113
+ /** Return the HANA execution plan for a statement. */
1114
+ async explain(sql, params) {
1115
+ return await this.pool.withConnection(async (connection) => {
1116
+ const statementName = `cf_hana_${String(Date.now())}`;
1117
+ await connection.execute(
1118
+ `EXPLAIN PLAN SET STATEMENT_NAME = '${statementName}' FOR ${sql}`,
1119
+ params
1120
+ );
1121
+ try {
1122
+ return await connection.query(
1123
+ "SELECT OPERATOR_NAME, TABLE_NAME, TABLE_TYPE, EXECUTION_ENGINE FROM EXPLAIN_PLAN_TABLE WHERE STATEMENT_NAME = ? ORDER BY OPERATOR_ID",
1124
+ [statementName]
1125
+ );
1126
+ } finally {
1127
+ await connection.execute(
1128
+ "DELETE FROM EXPLAIN_PLAN_TABLE WHERE STATEMENT_NAME = ?",
1129
+ [statementName]
1130
+ );
1131
+ }
1132
+ });
1133
+ }
1134
+ /** Close every pooled connection. The client must not be used afterwards. */
1135
+ async close() {
1136
+ await this.pool.drain();
1137
+ }
1138
+ };
1139
+
1140
+ // src/api.ts
1141
+ async function connect(selector, options) {
1142
+ return await HanaClient.connect(selector, options);
1143
+ }
1144
+
1145
+ // src/format.ts
1146
+ function cellText(value, nullText) {
1147
+ if (value === null) {
1148
+ return nullText;
1149
+ }
1150
+ if (value instanceof Date) {
1151
+ return value.toISOString();
1152
+ }
1153
+ if (Buffer.isBuffer(value)) {
1154
+ return `0x${value.toString("hex")}`;
1155
+ }
1156
+ if (typeof value === "boolean") {
1157
+ return value ? "true" : "false";
1158
+ }
1159
+ return typeof value === "number" ? value.toString() : value;
1160
+ }
1161
+ function serializeCell(value) {
1162
+ if (value === null) {
1163
+ return null;
1164
+ }
1165
+ if (value instanceof Date) {
1166
+ return value.toISOString();
1167
+ }
1168
+ if (Buffer.isBuffer(value)) {
1169
+ return `0x${value.toString("hex")}`;
1170
+ }
1171
+ return value;
1172
+ }
1173
+ function csvEscape(text) {
1174
+ if (/[",\r\n]/.test(text)) {
1175
+ return `"${text.replace(/"/g, '""')}"`;
1176
+ }
1177
+ return text;
1178
+ }
1179
+ function formatTable(result) {
1180
+ if (result.columns.length === 0) {
1181
+ return `(${String(result.rowCount)} row(s) affected)`;
1182
+ }
1183
+ const headers = result.columns.map((column) => column.name);
1184
+ const rows = result.rows.map(
1185
+ (row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL"))
1186
+ );
1187
+ const widths = headers.map((header, index) => {
1188
+ const widest = rows.reduce(
1189
+ (max, cells) => Math.max(max, (cells[index] ?? "").length),
1190
+ header.length
1191
+ );
1192
+ return widest;
1193
+ });
1194
+ const renderRow = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" | ");
1195
+ const separator = widths.map((width) => "-".repeat(width)).join("-+-");
1196
+ const body = rows.map((cells) => renderRow(cells));
1197
+ return [renderRow(headers), separator, ...body].join("\n");
1198
+ }
1199
+ function formatJson(result) {
1200
+ const rows = result.rows.map((row) => {
1201
+ const serialized = {};
1202
+ for (const [key, value] of Object.entries(row)) {
1203
+ serialized[key] = serializeCell(value);
1204
+ }
1205
+ return serialized;
1206
+ });
1207
+ return JSON.stringify(rows, null, 2);
1208
+ }
1209
+ function formatCsv(result) {
1210
+ const headers = result.columns.map((column) => column.name);
1211
+ const lines = [headers.map((header) => csvEscape(header)).join(",")];
1212
+ for (const row of result.rows) {
1213
+ lines.push(
1214
+ result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, ""))).join(",")
1215
+ );
1216
+ }
1217
+ return lines.join("\r\n");
1218
+ }
1219
+ function formatResult(result, format) {
1220
+ switch (format) {
1221
+ case "table":
1222
+ return formatTable(result);
1223
+ case "json":
1224
+ return formatJson(result);
1225
+ case "csv":
1226
+ return formatCsv(result);
1227
+ }
1228
+ }
1229
+
1230
+ // src/cli.ts
1231
+ function print(text) {
1232
+ process.stdout.write(`${text}
1233
+ `);
1234
+ }
1235
+ function fail(message) {
1236
+ process.stderr.write(`${CLI_NAME}: ${message}
1237
+ `);
1238
+ process.exit(1);
1239
+ }
1240
+ function parseIntOption(value) {
1241
+ const parsed = Number.parseInt(value, 10);
1242
+ if (!Number.isInteger(parsed)) {
1243
+ throw new CfHanaError("CONFIG", `Expected an integer but received "${value}"`);
1244
+ }
1245
+ return parsed;
1246
+ }
1247
+ function collectParam(value, previous) {
1248
+ return [...previous, value];
1249
+ }
1250
+ function parseRole(role) {
1251
+ if (role === "runtime" || role === "hdi") {
1252
+ return role;
1253
+ }
1254
+ throw new CfHanaError("CONFIG", `Invalid --role "${role}" (expected runtime or hdi)`);
1255
+ }
1256
+ function parseFormat(format) {
1257
+ if (format === "table" || format === "json" || format === "csv") {
1258
+ return format;
1259
+ }
1260
+ throw new CfHanaError("CONFIG", `Invalid --format "${format}" (expected table, json, or csv)`);
1261
+ }
1262
+ function parseQualifiedName(value) {
1263
+ const dot = value.indexOf(".");
1264
+ if (dot <= 0 || dot >= value.length - 1) {
1265
+ throw new CfHanaError("CONFIG", `Expected schema.table but received "${value}"`);
1266
+ }
1267
+ return { schema: value.slice(0, dot), table: value.slice(dot + 1) };
1268
+ }
1269
+ function toConnectOptions(opts) {
1270
+ return {
1271
+ refresh: opts.refresh,
1272
+ role: parseRole(opts.role),
1273
+ readOnly: opts.readOnly,
1274
+ allowDestructive: opts.allowDestructive,
1275
+ autoLimit: opts.autoLimit ? opts.limit ?? DEFAULT_AUTO_LIMIT : false,
1276
+ ...opts.binding === void 0 ? {} : { bindingName: opts.binding },
1277
+ ...opts.bindingIndex === void 0 ? {} : { bindingIndex: opts.bindingIndex },
1278
+ ...opts.timeout === void 0 ? {} : { queryTimeoutMs: opts.timeout, connectTimeoutMs: opts.timeout }
1279
+ };
1280
+ }
1281
+ function rowsToResult(rows) {
1282
+ const first = rows[0];
1283
+ const columns = first === void 0 ? [] : Object.keys(first).map((name) => ({ name, typeName: "" }));
1284
+ return {
1285
+ rows,
1286
+ columns,
1287
+ rowCount: rows.length,
1288
+ statement: "select",
1289
+ truncated: false,
1290
+ elapsedMs: 0
1291
+ };
1292
+ }
1293
+ function formatInfo(info) {
1294
+ return [
1295
+ `selector ${info.selector}`,
1296
+ `app ${info.appName}`,
1297
+ `host ${info.host}`,
1298
+ `schema ${info.schema}`,
1299
+ `role ${info.role}`,
1300
+ `driver ${info.driver}`,
1301
+ `credential source ${info.credentialSource}`
1302
+ ].join("\n");
1303
+ }
1304
+ function withConnectionOptions(command) {
1305
+ return command.option("--format <format>", "output format: table, json, or csv", "table").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", parseIntOption).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", parseIntOption).option("--limit <n>", "row cap auto-applied to bare SELECT statements", parseIntOption).option("--no-auto-limit", "disable the automatic SELECT row cap");
1306
+ }
1307
+ async function runQuery(selector, sql, command) {
1308
+ const opts = command.opts();
1309
+ const client = await connect(selector, toConnectOptions(opts));
1310
+ try {
1311
+ const result = await client.query(sql, opts.param ?? []);
1312
+ print(formatResult(result, parseFormat(opts.format)));
1313
+ } finally {
1314
+ await client.close();
1315
+ }
1316
+ }
1317
+ async function runTables(selector, schema, command) {
1318
+ const opts = command.opts();
1319
+ const client = await connect(selector, toConnectOptions(opts));
1320
+ try {
1321
+ const tables = await client.listTables(schema ?? client.info.schema);
1322
+ const rows = tables.map((table) => ({
1323
+ SCHEMA: table.schema,
1324
+ TABLE: table.name,
1325
+ TYPE: table.type
1326
+ }));
1327
+ print(formatResult(rowsToResult(rows), parseFormat(opts.format)));
1328
+ } finally {
1329
+ await client.close();
1330
+ }
1331
+ }
1332
+ async function runColumns(selector, target, command) {
1333
+ const opts = command.opts();
1334
+ const { schema, table } = parseQualifiedName(target);
1335
+ const client = await connect(selector, toConnectOptions(opts));
1336
+ try {
1337
+ const columns = await client.listColumns(schema, table);
1338
+ const rows = columns.map((column) => ({
1339
+ COLUMN: column.name,
1340
+ TYPE: column.dataType,
1341
+ LENGTH: column.length ?? null,
1342
+ NULLABLE: column.nullable,
1343
+ POSITION: column.position
1344
+ }));
1345
+ print(formatResult(rowsToResult(rows), parseFormat(opts.format)));
1346
+ } finally {
1347
+ await client.close();
1348
+ }
1349
+ }
1350
+ async function runCount(selector, target, command) {
1351
+ const opts = command.opts();
1352
+ const { schema, table } = parseQualifiedName(target);
1353
+ const client = await connect(selector, toConnectOptions(opts));
1354
+ try {
1355
+ const total = await client.count({ schema, table });
1356
+ print(String(total));
1357
+ } finally {
1358
+ await client.close();
1359
+ }
1360
+ }
1361
+ async function runPing(selector, command) {
1362
+ const opts = command.opts();
1363
+ const client = await connect(selector, toConnectOptions(opts));
1364
+ try {
1365
+ const started = Date.now();
1366
+ await client.query("SELECT 1 FROM DUMMY");
1367
+ print(
1368
+ `OK ${client.info.host} schema=${client.info.schema} ${String(Date.now() - started)}ms`
1369
+ );
1370
+ } finally {
1371
+ await client.close();
1372
+ }
1373
+ }
1374
+ async function runInfo(selector, command) {
1375
+ const opts = command.opts();
1376
+ const client = await connect(selector, toConnectOptions(opts));
1377
+ try {
1378
+ print(formatInfo(client.info));
1379
+ } finally {
1380
+ await client.close();
1381
+ }
1382
+ }
1383
+ function buildProgram() {
1384
+ const program = new Command();
1385
+ program.name(CLI_NAME).description("Run SQL against SAP HANA Cloud databases bound to a Cloud Foundry app").version(CLI_VERSION);
1386
+ withConnectionOptions(
1387
+ program.command("query <selector> <sql>").description("run a single SQL statement").option("--param <value>", "bind a SQL parameter (repeatable)", collectParam, [])
1388
+ ).action(async (selector, sql, _options, command) => {
1389
+ await runQuery(selector, sql, command);
1390
+ });
1391
+ withConnectionOptions(
1392
+ program.command("tables <selector> [schema]").description("list tables in a schema")
1393
+ ).action(
1394
+ async (selector, schema, _options, command) => {
1395
+ await runTables(selector, schema, command);
1396
+ }
1397
+ );
1398
+ withConnectionOptions(
1399
+ program.command("columns <selector> <schema.table>").description("list the columns of a table")
1400
+ ).action(async (selector, target, _options, command) => {
1401
+ await runColumns(selector, target, command);
1402
+ });
1403
+ withConnectionOptions(
1404
+ program.command("count <selector> <schema.table>").description("count rows in a table")
1405
+ ).action(async (selector, target, _options, command) => {
1406
+ await runCount(selector, target, command);
1407
+ });
1408
+ withConnectionOptions(
1409
+ program.command("ping <selector>").description("connect and measure round-trip latency")
1410
+ ).action(async (selector, _options, command) => {
1411
+ await runPing(selector, command);
1412
+ });
1413
+ withConnectionOptions(
1414
+ program.command("info <selector>").description("print the resolved connection metadata")
1415
+ ).action(async (selector, _options, command) => {
1416
+ await runInfo(selector, command);
1417
+ });
1418
+ return program;
1419
+ }
1420
+ try {
1421
+ await buildProgram().parseAsync(process.argv);
1422
+ } catch (error) {
1423
+ fail(errorMessage(error));
1424
+ }
1425
+ //# sourceMappingURL=cli.js.map