@bytebase/dbhub 0.7.0 → 0.8.4

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.
Files changed (3) hide show
  1. package/README.md +107 -68
  2. package/dist/index.js +122 -542
  3. package/package.json +7 -3
package/README.md CHANGED
@@ -1,3 +1,6 @@
1
+ > [!NOTE]
2
+ > Brought to you by [Bytebase](https://www.bytebase.com/), open-source database DevSecOps platform.
3
+
1
4
  <p align="center">
2
5
  <a href="https://dbhub.ai/" target="_blank">
3
6
  <picture>
@@ -8,6 +11,7 @@
8
11
 
9
12
  <p align="center">
10
13
  <a href="https://cursor.com/install-mcp?name=dbhub&config=eyJjb21tYW5kIjoibnB4IEBieXRlYmFzZS9kYmh1YiIsImVudiI6eyJUUkFOU1BPUlQiOiJzdGRpbyIsIkRTTiI6InBvc3RncmVzOi8vdXNlcjpwYXNzd29yZEBsb2NhbGhvc3Q6NTQzMi9kYm5hbWU%2Fc3NsbW9kZT1kaXNhYmxlIiwiUkVBRE9OTFkiOiJ0cnVlIn19"><img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Add dbhub MCP server to Cursor" height="32" /></a>
14
+ <a href="https://discord.gg/BjEkZpsJzn"><img src="https://img.shields.io/badge/%20-Hang%20out%20on%20Discord-5865F2?style=for-the-badge&logo=discord&labelColor=EEEEEE" alt="Join our Discord" height="32" /></a>
11
15
  </p>
12
16
 
13
17
  DBHub is a universal database gateway implementing the Model Context Protocol (MCP) server interface. This gateway allows MCP-compatible clients to connect to and explore different databases.
@@ -26,8 +30,6 @@ DBHub is a universal database gateway implementing the Model Context Protocol (M
26
30
  | | | | | |
27
31
  | | | +--->+ MariaDB |
28
32
  | | | | | |
29
- | | | +--->+ Oracle |
30
- | | | | | |
31
33
  +------------------+ +--------------+ +------------------+
32
34
  MCP Clients MCP Server Databases
33
35
  ```
@@ -42,27 +44,27 @@ https://demo.dbhub.ai/message connects a [sample employee database](https://gith
42
44
 
43
45
  ### Database Resources
44
46
 
45
- | Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite | Oracle |
46
- | --------------------------- | ------------------------------------------------------ | :--------: | :---: | :-----: | :--------: | :----: | :----: |
47
- | schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
48
- | tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
49
- | table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
50
- | indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
51
- | procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
52
- | procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
47
+ | Resource Name | URI Format | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
48
+ | --------------------------- | ------------------------------------------------------ | :--------: | :---: | :-----: | :--------: | :----: |
49
+ | schemas | `db://schemas` | ✅ | ✅ | ✅ | ✅ | ✅ |
50
+ | tables_in_schema | `db://schemas/{schemaName}/tables` | ✅ | ✅ | ✅ | ✅ | ✅ |
51
+ | table_structure_in_schema | `db://schemas/{schemaName}/tables/{tableName}` | ✅ | ✅ | ✅ | ✅ | ✅ |
52
+ | indexes_in_table | `db://schemas/{schemaName}/tables/{tableName}/indexes` | ✅ | ✅ | ✅ | ✅ | ✅ |
53
+ | procedures_in_schema | `db://schemas/{schemaName}/procedures` | ✅ | ✅ | ✅ | ✅ | ❌ |
54
+ | procedure_details_in_schema | `db://schemas/{schemaName}/procedures/{procedureName}` | ✅ | ✅ | ✅ | ✅ | ❌ |
53
55
 
54
56
  ### Database Tools
55
57
 
56
- | Tool | Command Name | Description | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite | Oracle |
57
- | --------------- | ----------------- | ------------------------------------------------------------------- | :--------: | :---: | :-----: | :--------: | ------ | :----: |
58
- | Execute SQL | `execute_sql` | Execute single or multiple SQL statements (separated by semicolons) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
58
+ | Tool | Command Name | Description | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
59
+ | ----------- | ------------- | ------------------------------------------------------------------- | :--------: | :---: | :-----: | :--------: | ------ |
60
+ | Execute SQL | `execute_sql` | Execute single or multiple SQL statements (separated by semicolons) | ✅ | ✅ | ✅ | ✅ | ✅ |
59
61
 
60
62
  ### Prompt Capabilities
61
63
 
62
- | Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite | Oracle |
63
- | ------------------- | -------------- | :--------: | :---: | :-----: | :--------: | ------ | :----: |
64
- | Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
65
- | Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
64
+ | Prompt | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite |
65
+ | ------------------- | -------------- | :--------: | :---: | :-----: | :--------: | ------ |
66
+ | Generate SQL | `generate_sql` | ✅ | ✅ | ✅ | ✅ | ✅ |
67
+ | Explain DB Elements | `explain_db` | ✅ | ✅ | ✅ | ✅ | ✅ |
66
68
 
67
69
  ## Installation
68
70
 
@@ -90,27 +92,6 @@ docker run --rm --init \
90
92
  --demo
91
93
  ```
92
94
 
93
- ```bash
94
- # Oracle example
95
- docker run --rm --init \
96
- --name dbhub \
97
- --publish 8080:8080 \
98
- bytebase/dbhub \
99
- --transport http \
100
- --port 8080 \
101
- --dsn "oracle://username:password@localhost:1521/service_name"
102
- ```
103
-
104
- ```bash
105
- # Oracle example with thick mode for connecting to 11g or older
106
- docker run --rm --init \
107
- --name dbhub \
108
- --publish 8080:8080 \
109
- bytebase/dbhub-oracle-thick \
110
- --transport http \
111
- --port 8080 \
112
- --dsn "oracle://username:password@localhost:1521/service_name"
113
- ```
114
95
 
115
96
  ### NPM
116
97
 
@@ -190,7 +171,6 @@ You can specify the SSL mode using the `sslmode` parameter in your DSN string:
190
171
  | MySQL | ✅ | ✅ | Certificate verification |
191
172
  | MariaDB | ✅ | ✅ | Certificate verification |
192
173
  | SQL Server | ✅ | ✅ | Certificate verification |
193
- | Oracle | ✅ | ✅ | N/A (use Oracle client config) |
194
174
  | SQLite | ❌ | ❌ | N/A (file-based) |
195
175
 
196
176
  **SSL Mode Options:**
@@ -235,7 +215,7 @@ npx @bytebase/dbhub --demo
235
215
  ```
236
216
 
237
217
  > [!WARNING]
238
- If your user/password contains special characters, you need to escape them first. (e.g. `pass#word` should be escaped as `pass%23word`)
218
+ > If your user/password contains special characters, you need to escape them first. (e.g. `pass#word` should be escaped as `pass%23word`)
239
219
 
240
220
  For real databases, a Database Source Name (DSN) is required. You can provide this in several ways:
241
221
 
@@ -271,28 +251,7 @@ DBHub supports the following database connection string formats:
271
251
  | PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
272
252
  | SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname?sslmode=disable` |
273
253
  | SQLite | `sqlite:///[path/to/file]` or `sqlite:///:memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite:///:memory:` |
274
- | Oracle | `oracle://[user]:[password]@[host]:[port]/[service_name]` | `oracle://username:password@localhost:1521/service_name?sslmode=disable` |
275
-
276
- #### Oracle
277
-
278
- If you see the error "NJS-138: connections to this database server version are not supported by node-oracledb in Thin mode", you need to use Thick mode as described below.
279
-
280
- ##### Docker
281
-
282
- Use `bytebase/dbhub-oracle-thick` instead of `bytebase/dbhub` docker image.
283
254
 
284
- ##### npx
285
-
286
- 1. Download and install [Oracle Instant Client](https://www.oracle.com/database/technologies/instant-client/downloads.html) for your platform
287
- 1. Set the `ORACLE_LIB_DIR` environment variable to the path of your Oracle Instant Client:
288
-
289
- ```bash
290
- # Set environment variable to Oracle Instant Client directory
291
- export ORACLE_LIB_DIR=/path/to/instantclient_19_8
292
-
293
- # Then run DBHub
294
- npx @bytebase/dbhub --dsn "oracle://username:password@localhost:1521/service_name"
295
- ```
296
255
 
297
256
  #### SQL Server
298
257
 
@@ -317,13 +276,13 @@ Extra query parameters:
317
276
 
318
277
  ### Command line options
319
278
 
320
- | Option | Environment Variable | Description | Default |
321
- | --------- | -------------------- | --------------------------------------------------------------- | ---------------------------- |
322
- | dsn | `DSN` | Database connection string | Required if not in demo mode |
323
- | transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
279
+ | Option | Environment Variable | Description | Default |
280
+ | --------- | -------------------- | ---------------------------------------------------------------- | ---------------------------- |
281
+ | dsn | `DSN` | Database connection string | Required if not in demo mode |
282
+ | transport | `TRANSPORT` | Transport mode: `stdio` or `http` | `stdio` |
324
283
  | port | `PORT` | HTTP server port (only applicable when using `--transport=http`) | `8080` |
325
- | readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
326
- | demo | N/A | Run in demo mode with sample employee database | `false` |
284
+ | readonly | `READONLY` | Restrict SQL execution to read-only operations | `false` |
285
+ | demo | N/A | Run in demo mode with sample employee database | `false` |
327
286
 
328
287
  The demo mode uses an in-memory SQLite database loaded with the [sample employee database](https://github.com/bytebase/dbhub/tree/main/resources/employee-sqlite) that includes tables for employees, departments, titles, salaries, department employees, and department managers. The sample database includes SQL scripts for table creation, data loading, and testing.
329
288
 
@@ -349,10 +308,90 @@ The demo mode uses an in-memory SQLite database loaded with the [sample employee
349
308
 
350
309
  ### Testing
351
310
 
352
- The project uses Vitest for comprehensive unit testing:
311
+ The project uses Vitest for comprehensive unit and integration testing:
353
312
 
354
313
  - **Run all tests**: `pnpm test`
355
314
  - **Run tests in watch mode**: `pnpm test:watch`
315
+ - **Run integration tests**: `pnpm test:integration`
316
+
317
+ #### Integration Tests
318
+
319
+ DBHub includes comprehensive integration tests for all supported database connectors using [Testcontainers](https://testcontainers.com/). These tests run against real database instances in Docker containers, ensuring full compatibility and feature coverage.
320
+
321
+ ##### Prerequisites
322
+
323
+ - **Docker**: Ensure Docker is installed and running on your machine
324
+ - **Docker Resources**: Allocate sufficient memory (recommended: 4GB+) for multiple database containers
325
+ - **Network Access**: Ability to pull Docker images from registries
326
+
327
+ ##### Running Integration Tests
328
+
329
+ **Note**: This command runs all integration tests in parallel, which may take 5-15 minutes depending on your system resources and network speed.
330
+
331
+ ```bash
332
+ # Run all database integration tests
333
+ pnpm test:integration
334
+ ```
335
+
336
+ ```bash
337
+ # Run only PostgreSQL integration tests
338
+ pnpm test src/connectors/__tests__/postgres.integration.test.ts
339
+ # Run only MySQL integration tests
340
+ pnpm test src/connectors/__tests__/mysql.integration.test.ts
341
+ # Run only MariaDB integration tests
342
+ pnpm test src/connectors/__tests__/mariadb.integration.test.ts
343
+ # Run only SQL Server integration tests
344
+ pnpm test src/connectors/__tests__/sqlserver.integration.test.ts
345
+ # Run only SQLite integration tests
346
+ pnpm test src/connectors/__tests__/sqlite.integration.test.ts
347
+ # Run JSON RPC integration tests
348
+ pnpm test src/__tests__/json-rpc-integration.test.ts
349
+ ```
350
+
351
+ All integration tests follow these patterns:
352
+
353
+ 1. **Container Lifecycle**: Start database container → Connect → Setup test data → Run tests → Cleanup
354
+ 2. **Shared Test Utilities**: Common test patterns implemented in `IntegrationTestBase` class
355
+ 3. **Database-Specific Features**: Each database includes tests for unique features and capabilities
356
+ 4. **Error Handling**: Comprehensive testing of connection errors, invalid SQL, and edge cases
357
+
358
+ ##### Troubleshooting Integration Tests
359
+
360
+ **Container Startup Issues:**
361
+
362
+ ```bash
363
+ # Check Docker is running
364
+ docker ps
365
+
366
+ # Check available memory
367
+ docker system df
368
+
369
+ # Pull images manually if needed
370
+ docker pull postgres:15-alpine
371
+ docker pull mysql:8.0
372
+ docker pull mariadb:10.11
373
+ docker pull mcr.microsoft.com/mssql/server:2019-latest
374
+ ```
375
+
376
+ **SQL Server Timeout Issues:**
377
+
378
+ - SQL Server containers require significant startup time (3-5 minutes)
379
+ - Ensure Docker has sufficient memory allocated (4GB+ recommended)
380
+ - Consider running SQL Server tests separately if experiencing timeouts
381
+
382
+
383
+ **Network/Resource Issues:**
384
+
385
+ ```bash
386
+ # Run tests with verbose output
387
+ pnpm test:integration --reporter=verbose
388
+
389
+ # Run single database test to isolate issues
390
+ pnpm test:integration -- --testNamePattern="PostgreSQL"
391
+
392
+ # Check Docker container logs if tests fail
393
+ docker logs <container_id>
394
+ ```
356
395
 
357
396
  #### Pre-commit Hooks (for Developers)
358
397
 
package/dist/index.js CHANGED
@@ -148,12 +148,55 @@ var SafeURL = class {
148
148
  }
149
149
  };
150
150
 
151
+ // src/utils/dsn-obfuscate.ts
152
+ function obfuscateDSNPassword(dsn) {
153
+ if (!dsn) {
154
+ return dsn;
155
+ }
156
+ try {
157
+ const protocolMatch = dsn.match(/^([^:]+):/);
158
+ if (!protocolMatch) {
159
+ return dsn;
160
+ }
161
+ const protocol = protocolMatch[1];
162
+ if (protocol === "sqlite") {
163
+ return dsn;
164
+ }
165
+ const protocolPart = dsn.split("://")[1];
166
+ if (!protocolPart) {
167
+ return dsn;
168
+ }
169
+ const lastAtIndex = protocolPart.lastIndexOf("@");
170
+ if (lastAtIndex === -1) {
171
+ return dsn;
172
+ }
173
+ const credentialsPart = protocolPart.substring(0, lastAtIndex);
174
+ const hostPart = protocolPart.substring(lastAtIndex + 1);
175
+ const colonIndex = credentialsPart.indexOf(":");
176
+ if (colonIndex === -1) {
177
+ return dsn;
178
+ }
179
+ const username = credentialsPart.substring(0, colonIndex);
180
+ const password = credentialsPart.substring(colonIndex + 1);
181
+ const obfuscatedPassword = "*".repeat(Math.min(password.length, 8));
182
+ return `${protocol}://${username}:${obfuscatedPassword}@${hostPart}`;
183
+ } catch (error) {
184
+ return dsn;
185
+ }
186
+ }
187
+
151
188
  // src/connectors/postgres/index.ts
152
189
  var { Pool } = pg;
153
190
  var PostgresDSNParser = class {
154
191
  async parse(dsn) {
155
192
  if (!this.isValidDSN(dsn)) {
156
- throw new Error(`Invalid PostgreSQL DSN: ${dsn}`);
193
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
194
+ const expectedFormat = this.getSampleDSN();
195
+ throw new Error(
196
+ `Invalid PostgreSQL DSN format.
197
+ Provided: ${obfuscatedDSN}
198
+ Expected: ${expectedFormat}`
199
+ );
157
200
  }
158
201
  try {
159
202
  const url = new SafeURL(dsn);
@@ -491,8 +534,12 @@ import { DefaultAzureCredential } from "@azure/identity";
491
534
  var SQLServerDSNParser = class {
492
535
  async parse(dsn) {
493
536
  if (!this.isValidDSN(dsn)) {
537
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
538
+ const expectedFormat = this.getSampleDSN();
494
539
  throw new Error(
495
- "Invalid SQL Server DSN format. Expected: sqlserver://username:password@host:port/database"
540
+ `Invalid SQL Server DSN format.
541
+ Provided: ${obfuscatedDSN}
542
+ Expected: ${expectedFormat}`
496
543
  );
497
544
  }
498
545
  try {
@@ -512,6 +559,7 @@ var SQLServerDSNParser = class {
512
559
  if (options.sslmode) {
513
560
  if (options.sslmode === "disable") {
514
561
  options.encrypt = false;
562
+ options.trustServerCertificate = false;
515
563
  } else if (options.sslmode === "require") {
516
564
  options.encrypt = true;
517
565
  options.trustServerCertificate = true;
@@ -526,9 +574,9 @@ var SQLServerDSNParser = class {
526
574
  database: url.pathname ? url.pathname.substring(1) : "",
527
575
  // Remove leading slash
528
576
  options: {
529
- encrypt: options.encrypt ?? true,
530
- // Default to encrypted connection
531
- trustServerCertificate: options.trustServerCertificate === true,
577
+ encrypt: options.encrypt ?? false,
578
+ // Default to unencrypted for development
579
+ trustServerCertificate: options.trustServerCertificate ?? false,
532
580
  connectTimeout: options.connectTimeout ?? 15e3,
533
581
  requestTimeout: options.requestTimeout ?? 15e3
534
582
  }
@@ -556,7 +604,7 @@ var SQLServerDSNParser = class {
556
604
  }
557
605
  }
558
606
  getSampleDSN() {
559
- return "sqlserver://username:password@localhost:1433/database?sslmode=require";
607
+ return "sqlserver://username:password@localhost:1433/database?sslmode=disable";
560
608
  }
561
609
  isValidDSN(dsn) {
562
610
  try {
@@ -836,7 +884,13 @@ import Database from "better-sqlite3";
836
884
  var SQLiteDSNParser = class {
837
885
  async parse(dsn) {
838
886
  if (!this.isValidDSN(dsn)) {
839
- throw new Error(`Invalid SQLite DSN: ${dsn}`);
887
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
888
+ const expectedFormat = this.getSampleDSN();
889
+ throw new Error(
890
+ `Invalid SQLite DSN format.
891
+ Provided: ${obfuscatedDSN}
892
+ Expected: ${expectedFormat}`
893
+ );
840
894
  }
841
895
  try {
842
896
  const url = new SafeURL(dsn);
@@ -907,7 +961,7 @@ var SQLiteConnector = class {
907
961
  if (!this.db) {
908
962
  throw new Error("Not connected to SQLite database");
909
963
  }
910
- return [this.dbPath === ":memory:" ? "main" : this.dbPath];
964
+ return ["main"];
911
965
  }
912
966
  async getTables(schema) {
913
967
  if (!this.db) {
@@ -951,15 +1005,17 @@ var SQLiteConnector = class {
951
1005
  `
952
1006
  SELECT
953
1007
  name as index_name,
954
- CASE
955
- WHEN "unique" = 1 THEN 1
956
- ELSE 0
957
- END as is_unique
1008
+ 0 as is_unique
958
1009
  FROM sqlite_master
959
1010
  WHERE type = 'index'
960
1011
  AND tbl_name = ?
961
1012
  `
962
1013
  ).all(tableName);
1014
+ const indexListRows = this.db.prepare(`PRAGMA index_list(${tableName})`).all();
1015
+ const indexUniqueMap = /* @__PURE__ */ new Map();
1016
+ for (const indexListRow of indexListRows) {
1017
+ indexUniqueMap.set(indexListRow.name, indexListRow.unique === 1);
1018
+ }
963
1019
  const tableInfo = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
964
1020
  const pkColumns = tableInfo.filter((col) => col.pk > 0).map((col) => col.name);
965
1021
  const results = [];
@@ -969,7 +1025,7 @@ var SQLiteConnector = class {
969
1025
  results.push({
970
1026
  index_name: indexInfo.index_name,
971
1027
  column_names: columnNames,
972
- is_unique: indexInfo.is_unique === 1,
1028
+ is_unique: indexUniqueMap.get(indexInfo.index_name) || false,
973
1029
  is_primary: false
974
1030
  });
975
1031
  }
@@ -995,8 +1051,8 @@ var SQLiteConnector = class {
995
1051
  const columns = rows.map((row) => ({
996
1052
  column_name: row.name,
997
1053
  data_type: row.type,
998
- is_nullable: row.notnull === 0 ? "YES" : "NO",
999
- // In SQLite, 0 means nullable
1054
+ // In SQLite, primary key columns are automatically NOT NULL even if notnull=0
1055
+ is_nullable: row.notnull === 1 || row.pk > 0 ? "NO" : "YES",
1000
1056
  column_default: row.dflt_value
1001
1057
  }));
1002
1058
  return columns;
@@ -1025,14 +1081,21 @@ var SQLiteConnector = class {
1025
1081
  try {
1026
1082
  const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
1027
1083
  if (statements.length === 1) {
1028
- const rows = this.db.prepare(statements[0]).all();
1029
- return { rows };
1084
+ const trimmedStatement = statements[0].toLowerCase().trim();
1085
+ const isReadStatement = trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"));
1086
+ if (isReadStatement) {
1087
+ const rows = this.db.prepare(statements[0]).all();
1088
+ return { rows };
1089
+ } else {
1090
+ this.db.prepare(statements[0]).run();
1091
+ return { rows: [] };
1092
+ }
1030
1093
  } else {
1031
1094
  const readStatements = [];
1032
1095
  const writeStatements = [];
1033
1096
  for (const statement of statements) {
1034
1097
  const trimmedStatement = statement.toLowerCase().trim();
1035
- if (trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma")) {
1098
+ if (trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma") && (trimmedStatement.includes("table_info") || trimmedStatement.includes("index_info") || trimmedStatement.includes("index_list") || trimmedStatement.includes("foreign_key_list"))) {
1036
1099
  readStatements.push(statement);
1037
1100
  } else {
1038
1101
  writeStatements.push(statement);
@@ -1061,7 +1124,13 @@ import mysql from "mysql2/promise";
1061
1124
  var MySQLDSNParser = class {
1062
1125
  async parse(dsn) {
1063
1126
  if (!this.isValidDSN(dsn)) {
1064
- throw new Error(`Invalid MySQL DSN: ${dsn}`);
1127
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
1128
+ const expectedFormat = this.getSampleDSN();
1129
+ throw new Error(
1130
+ `Invalid MySQL DSN format.
1131
+ Provided: ${obfuscatedDSN}
1132
+ Expected: ${expectedFormat}`
1133
+ );
1065
1134
  }
1066
1135
  try {
1067
1136
  const url = new SafeURL(dsn);
@@ -1430,7 +1499,13 @@ import mariadb from "mariadb";
1430
1499
  var MariadbDSNParser = class {
1431
1500
  async parse(dsn) {
1432
1501
  if (!this.isValidDSN(dsn)) {
1433
- throw new Error(`Invalid MariaDB DSN: ${dsn}`);
1502
+ const obfuscatedDSN = obfuscateDSNPassword(dsn);
1503
+ const expectedFormat = this.getSampleDSN();
1504
+ throw new Error(
1505
+ `Invalid MariaDB DSN format.
1506
+ Provided: ${obfuscatedDSN}
1507
+ Expected: ${expectedFormat}`
1508
+ );
1434
1509
  }
1435
1510
  try {
1436
1511
  const url = new SafeURL(dsn);
@@ -1441,8 +1516,10 @@ var MariadbDSNParser = class {
1441
1516
  // Remove leading '/' if exists
1442
1517
  user: url.username,
1443
1518
  password: url.password,
1444
- multipleStatements: true
1519
+ multipleStatements: true,
1445
1520
  // Enable native multi-statement support
1521
+ connectTimeout: 5e3
1522
+ // 5 second timeout for connections
1446
1523
  };
1447
1524
  url.forEachSearchParam((value, key) => {
1448
1525
  if (key === "sslmode") {
@@ -1485,7 +1562,7 @@ var MariaDBConnector = class {
1485
1562
  const config = await this.dsnParser.parse(dsn);
1486
1563
  this.pool = mariadb.createPool(config);
1487
1564
  console.error("Testing connection to MariaDB...");
1488
- const [rows] = await this.pool.query("SELECT 1");
1565
+ await this.pool.query("SELECT 1");
1489
1566
  console.error("Successfully connected to MariaDB database");
1490
1567
  } catch (err) {
1491
1568
  console.error("Failed to connect to MariaDB database:", err);
@@ -1503,7 +1580,7 @@ var MariaDBConnector = class {
1503
1580
  throw new Error("Not connected to database");
1504
1581
  }
1505
1582
  try {
1506
- const [rows] = await this.pool.query(`
1583
+ const rows = await this.pool.query(`
1507
1584
  SELECT SCHEMA_NAME
1508
1585
  FROM INFORMATION_SCHEMA.SCHEMATA
1509
1586
  ORDER BY SCHEMA_NAME
@@ -1521,7 +1598,7 @@ var MariaDBConnector = class {
1521
1598
  try {
1522
1599
  const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1523
1600
  const queryParams = schema ? [schema] : [];
1524
- const [rows] = await this.pool.query(
1601
+ const rows = await this.pool.query(
1525
1602
  `
1526
1603
  SELECT TABLE_NAME
1527
1604
  FROM INFORMATION_SCHEMA.TABLES
@@ -1543,7 +1620,7 @@ var MariaDBConnector = class {
1543
1620
  try {
1544
1621
  const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1545
1622
  const queryParams = schema ? [schema, tableName] : [tableName];
1546
- const [rows] = await this.pool.query(
1623
+ const rows = await this.pool.query(
1547
1624
  `
1548
1625
  SELECT COUNT(*) AS COUNT
1549
1626
  FROM INFORMATION_SCHEMA.TABLES
@@ -1565,7 +1642,7 @@ var MariaDBConnector = class {
1565
1642
  try {
1566
1643
  const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
1567
1644
  const queryParams = schema ? [schema, tableName] : [tableName];
1568
- const [indexRows] = await this.pool.query(
1645
+ const indexRows = await this.pool.query(
1569
1646
  `
1570
1647
  SELECT
1571
1648
  INDEX_NAME,
@@ -1621,7 +1698,7 @@ var MariaDBConnector = class {
1621
1698
  try {
1622
1699
  const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
1623
1700
  const queryParams = schema ? [schema, tableName] : [tableName];
1624
- const [rows] = await this.pool.query(
1701
+ const rows = await this.pool.query(
1625
1702
  `
1626
1703
  SELECT
1627
1704
  COLUMN_NAME as column_name,
@@ -1648,7 +1725,7 @@ var MariaDBConnector = class {
1648
1725
  try {
1649
1726
  const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
1650
1727
  const queryParams = schema ? [schema] : [];
1651
- const [rows] = await this.pool.query(
1728
+ const rows = await this.pool.query(
1652
1729
  `
1653
1730
  SELECT ROUTINE_NAME
1654
1731
  FROM INFORMATION_SCHEMA.ROUTINES
@@ -1670,7 +1747,7 @@ var MariaDBConnector = class {
1670
1747
  try {
1671
1748
  const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
1672
1749
  const queryParams = schema ? [schema, procedureName] : [procedureName];
1673
- const [rows] = await this.pool.query(
1750
+ const rows = await this.pool.query(
1674
1751
  `
1675
1752
  SELECT
1676
1753
  r.ROUTINE_NAME AS procedure_name,
@@ -1708,7 +1785,7 @@ var MariaDBConnector = class {
1708
1785
  const schemaValue = schema || await this.getCurrentSchema();
1709
1786
  if (procedure.procedure_type === "procedure") {
1710
1787
  try {
1711
- const [defRows] = await this.pool.query(`
1788
+ const defRows = await this.pool.query(`
1712
1789
  SHOW CREATE PROCEDURE ${schemaValue}.${procedureName}
1713
1790
  `);
1714
1791
  if (defRows && defRows.length > 0) {
@@ -1719,7 +1796,7 @@ var MariaDBConnector = class {
1719
1796
  }
1720
1797
  } else {
1721
1798
  try {
1722
- const [defRows] = await this.pool.query(`
1799
+ const defRows = await this.pool.query(`
1723
1800
  SHOW CREATE FUNCTION ${schemaValue}.${procedureName}
1724
1801
  `);
1725
1802
  if (defRows && defRows.length > 0) {
@@ -1730,7 +1807,7 @@ var MariaDBConnector = class {
1730
1807
  }
1731
1808
  }
1732
1809
  if (!definition) {
1733
- const [bodyRows] = await this.pool.query(
1810
+ const bodyRows = await this.pool.query(
1734
1811
  `
1735
1812
  SELECT ROUTINE_DEFINITION, ROUTINE_BODY
1736
1813
  FROM INFORMATION_SCHEMA.ROUTINES
@@ -1765,7 +1842,7 @@ var MariaDBConnector = class {
1765
1842
  }
1766
1843
  // Helper method to get current schema (database) name
1767
1844
  async getCurrentSchema() {
1768
- const [rows] = await this.pool.query("SELECT DATABASE() AS DB");
1845
+ const rows = await this.pool.query("SELECT DATABASE() AS DB");
1769
1846
  return rows[0].DB;
1770
1847
  }
1771
1848
  async executeSQL(sql2) {
@@ -1774,17 +1851,20 @@ var MariaDBConnector = class {
1774
1851
  }
1775
1852
  try {
1776
1853
  const results = await this.pool.query(sql2);
1777
- const [firstResult] = results;
1778
- if (Array.isArray(firstResult) && firstResult.length > 0 && Array.isArray(firstResult[0]) && firstResult[0].length === 2) {
1779
- let allRows = [];
1780
- for (const [rows, _fields] of firstResult) {
1781
- if (Array.isArray(rows)) {
1782
- allRows.push(...rows);
1854
+ if (Array.isArray(results)) {
1855
+ if (results.length > 0 && Array.isArray(results[0]) && results[0].length > 0) {
1856
+ let allRows = [];
1857
+ for (const result of results) {
1858
+ if (Array.isArray(result)) {
1859
+ allRows.push(...result);
1860
+ }
1783
1861
  }
1862
+ return { rows: allRows };
1863
+ } else {
1864
+ return { rows: results };
1784
1865
  }
1785
- return { rows: allRows };
1786
1866
  } else {
1787
- return { rows: Array.isArray(firstResult) ? firstResult : [] };
1867
+ return { rows: [] };
1788
1868
  }
1789
1869
  } catch (error) {
1790
1870
  console.error("Error executing query:", error);
@@ -1795,505 +1875,6 @@ var MariaDBConnector = class {
1795
1875
  var mariadbConnector = new MariaDBConnector();
1796
1876
  ConnectorRegistry.register(mariadbConnector);
1797
1877
 
1798
- // src/connectors/oracle/index.ts
1799
- import oracledb from "oracledb";
1800
- oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
1801
- var _OracleConnector = class _OracleConnector {
1802
- // constructor(config: ConnectionConfig) { // Removed config
1803
- constructor() {
1804
- // Connector ID and Name are part of the Connector interface
1805
- this.id = "oracle";
1806
- this.name = "Oracle Database";
1807
- this.pool = null;
1808
- this.currentSchema = null;
1809
- // Oracle DSN Parser implementation
1810
- this.dsnParser = {
1811
- parse: async (dsn) => {
1812
- if (!this.dsnParser.isValidDSN(dsn)) {
1813
- throw new Error(`Invalid Oracle DSN: ${dsn}`);
1814
- }
1815
- try {
1816
- const url = new SafeURL(dsn);
1817
- let serviceName = url.pathname;
1818
- if (serviceName.startsWith("/")) {
1819
- serviceName = serviceName.substring(1);
1820
- }
1821
- const port = url.port ? parseInt(url.port) : 1521;
1822
- const connectString = `${url.hostname}:${port}/${serviceName}`;
1823
- const config = {
1824
- user: url.username,
1825
- password: url.password,
1826
- connectString,
1827
- poolMin: 0,
1828
- poolMax: 10,
1829
- poolIncrement: 1
1830
- };
1831
- url.forEachSearchParam((value, key) => {
1832
- switch (key.toLowerCase()) {
1833
- case "poolmin":
1834
- config.poolMin = parseInt(value, 10);
1835
- break;
1836
- case "poolmax":
1837
- config.poolMax = parseInt(value, 10);
1838
- break;
1839
- case "poolincrement":
1840
- config.poolIncrement = parseInt(value, 10);
1841
- break;
1842
- case "sslmode":
1843
- switch (value.toLowerCase()) {
1844
- case "disable":
1845
- break;
1846
- case "require":
1847
- config.sslServerDNMatch = false;
1848
- break;
1849
- }
1850
- break;
1851
- }
1852
- });
1853
- return config;
1854
- } catch (error) {
1855
- throw new Error(`Failed to parse Oracle DSN: ${error instanceof Error ? error.message : String(error)}`);
1856
- }
1857
- },
1858
- getSampleDSN: () => {
1859
- return "oracle://username:password@host:1521/service_name?sslmode=require";
1860
- },
1861
- isValidDSN: (dsn) => {
1862
- try {
1863
- return dsn.startsWith("oracle://");
1864
- } catch (error) {
1865
- return false;
1866
- }
1867
- }
1868
- };
1869
- oracledb.autoCommit = true;
1870
- }
1871
- // Initialize Oracle client only once
1872
- initClient() {
1873
- if (_OracleConnector.clientInitialized) {
1874
- return;
1875
- }
1876
- try {
1877
- if (process.env.ORACLE_LIB_DIR) {
1878
- oracledb.initOracleClient({ libDir: process.env.ORACLE_LIB_DIR });
1879
- console.error("Oracle client initialized in Thick mode");
1880
- } else {
1881
- console.error("ORACLE_LIB_DIR not specified, will use Thin mode by default");
1882
- }
1883
- _OracleConnector.clientInitialized = true;
1884
- } catch (err) {
1885
- console.error("Failed to initialize Oracle client:", err);
1886
- }
1887
- }
1888
- async connect(dsn, initializationScript) {
1889
- try {
1890
- this.initClient();
1891
- const config = await this.dsnParser.parse(dsn);
1892
- this.pool = await oracledb.createPool(config);
1893
- const conn = await this.getConnection();
1894
- try {
1895
- const result = await conn.execute("SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') as SCHEMA FROM DUAL");
1896
- if (result.rows && result.rows.length > 0) {
1897
- this.currentSchema = result.rows[0].SCHEMA;
1898
- }
1899
- if (initializationScript) {
1900
- await conn.execute(initializationScript);
1901
- }
1902
- } finally {
1903
- await conn.close();
1904
- }
1905
- console.error("Successfully connected to Oracle database");
1906
- if (this.currentSchema) {
1907
- console.error(`Current schema: ${this.currentSchema}`);
1908
- }
1909
- } catch (error) {
1910
- console.error("Failed to connect to Oracle database:", error);
1911
- if (error instanceof Error && error.message.includes("NJS-138")) {
1912
- const enhancedError = new Error(
1913
- `${error.message}
1914
-
1915
- This error occurs when your Oracle database version is not supported by node-oracledb in Thin mode.
1916
- To resolve this, you need to use Thick mode:
1917
- 1. Download Oracle Instant Client from https://www.oracle.com/database/technologies/instant-client/downloads.html
1918
- 2. Set ORACLE_LIB_DIR environment variable to the path of your Oracle Instant Client
1919
- Example: export ORACLE_LIB_DIR=/path/to/instantclient_19_8
1920
- 3. Restart DBHub`
1921
- );
1922
- throw enhancedError;
1923
- }
1924
- throw error;
1925
- }
1926
- }
1927
- async disconnect() {
1928
- if (this.pool) {
1929
- try {
1930
- await this.pool.close();
1931
- this.pool = null;
1932
- this.currentSchema = null;
1933
- } catch (error) {
1934
- console.error("Error disconnecting from Oracle:", error);
1935
- throw error;
1936
- }
1937
- }
1938
- }
1939
- async getSchemas() {
1940
- try {
1941
- const conn = await this.getConnection();
1942
- try {
1943
- const result = await conn.execute(
1944
- `SELECT USERNAME AS SCHEMA_NAME
1945
- FROM ALL_USERS
1946
- ORDER BY USERNAME`
1947
- );
1948
- return result.rows?.map((row) => row.SCHEMA_NAME) || [];
1949
- } finally {
1950
- await conn.close();
1951
- }
1952
- } catch (error) {
1953
- console.error("Error getting schemas from Oracle:", error);
1954
- throw error;
1955
- }
1956
- }
1957
- async getTables(schemaName) {
1958
- try {
1959
- const conn = await this.getConnection();
1960
- try {
1961
- const schema = schemaName || this.currentSchema;
1962
- const result = await conn.execute(
1963
- `SELECT TABLE_NAME
1964
- FROM ALL_TABLES
1965
- WHERE OWNER = :schema
1966
- ORDER BY TABLE_NAME`,
1967
- { schema: schema?.toUpperCase() }
1968
- );
1969
- return result.rows?.map((row) => row.TABLE_NAME) || [];
1970
- } finally {
1971
- await conn.close();
1972
- }
1973
- } catch (error) {
1974
- console.error("Error getting tables from Oracle:", error);
1975
- throw error;
1976
- }
1977
- }
1978
- async getTableColumns(tableName, schemaName) {
1979
- try {
1980
- const conn = await this.getConnection();
1981
- try {
1982
- const schema = schemaName || this.currentSchema;
1983
- const result = await conn.execute(
1984
- `SELECT
1985
- COLUMN_NAME,
1986
- DATA_TYPE,
1987
- NULLABLE as IS_NULLABLE,
1988
- DATA_DEFAULT as COLUMN_DEFAULT
1989
- FROM ALL_TAB_COLUMNS
1990
- WHERE OWNER = :schema
1991
- AND TABLE_NAME = :tableName
1992
- ORDER BY COLUMN_ID`,
1993
- {
1994
- schema: schema?.toUpperCase(),
1995
- tableName: tableName.toUpperCase()
1996
- }
1997
- );
1998
- return result.rows?.map((row) => ({
1999
- column_name: row.COLUMN_NAME,
2000
- data_type: row.DATA_TYPE,
2001
- is_nullable: row.IS_NULLABLE === "Y" ? "YES" : "NO",
2002
- column_default: row.COLUMN_DEFAULT
2003
- })) || [];
2004
- } finally {
2005
- await conn.close();
2006
- }
2007
- } catch (error) {
2008
- console.error("Error getting columns from Oracle:", error);
2009
- throw error;
2010
- }
2011
- }
2012
- // Method to ensure boolean return type
2013
- ensureBoolean(value) {
2014
- return value === true;
2015
- }
2016
- async getTableIndexes(tableName, schemaName) {
2017
- try {
2018
- const conn = await this.getConnection();
2019
- try {
2020
- const schema = schemaName || this.currentSchema;
2021
- const indexesResult = await conn.execute(
2022
- `SELECT
2023
- i.INDEX_NAME,
2024
- i.UNIQUENESS
2025
- FROM ALL_INDEXES i
2026
- WHERE i.OWNER = :schema
2027
- AND i.TABLE_NAME = :tableName`,
2028
- {
2029
- schema: schema?.toUpperCase(),
2030
- tableName: tableName.toUpperCase()
2031
- }
2032
- );
2033
- if (!indexesResult.rows || indexesResult.rows.length === 0) {
2034
- return [];
2035
- }
2036
- const indexes = [];
2037
- for (const idx of indexesResult.rows) {
2038
- const indexRow = idx;
2039
- const indexName = indexRow.INDEX_NAME;
2040
- const isUnique = indexRow.UNIQUENESS === "UNIQUE";
2041
- const columnsResult = await conn.execute(
2042
- `SELECT
2043
- COLUMN_NAME
2044
- FROM ALL_IND_COLUMNS
2045
- WHERE INDEX_OWNER = :schema
2046
- AND INDEX_NAME = :indexName
2047
- ORDER BY COLUMN_POSITION`,
2048
- {
2049
- schema: schema?.toUpperCase(),
2050
- indexName
2051
- }
2052
- );
2053
- const columnNames = columnsResult.rows?.map((row) => row.COLUMN_NAME) || [];
2054
- const pkResult = await conn.execute(
2055
- `SELECT COUNT(*) AS IS_PK
2056
- FROM ALL_CONSTRAINTS
2057
- WHERE CONSTRAINT_TYPE = 'P'
2058
- AND OWNER = :schema
2059
- AND TABLE_NAME = :tableName
2060
- AND INDEX_NAME = :indexName`,
2061
- {
2062
- schema: schema?.toUpperCase(),
2063
- tableName: tableName.toUpperCase(),
2064
- indexName
2065
- }
2066
- );
2067
- const isPrimary = pkResult.rows && pkResult.rows.length > 0 && pkResult.rows[0].IS_PK > 0;
2068
- indexes.push({
2069
- index_name: indexName,
2070
- column_names: columnNames,
2071
- is_unique: isUnique,
2072
- is_primary: !!isPrimary
2073
- // Ensure boolean
2074
- });
2075
- }
2076
- return indexes;
2077
- } finally {
2078
- await conn.close();
2079
- }
2080
- } catch (error) {
2081
- console.error("Error getting indexes from Oracle:", error);
2082
- throw error;
2083
- }
2084
- }
2085
- async tableExists(tableName, schemaName) {
2086
- try {
2087
- const conn = await this.getConnection();
2088
- try {
2089
- const schema = schemaName || this.currentSchema;
2090
- const result = await conn.execute(
2091
- `SELECT COUNT(*) AS COUNT
2092
- FROM ALL_TABLES
2093
- WHERE OWNER = :schema
2094
- AND TABLE_NAME = :tableName`,
2095
- {
2096
- schema: schema?.toUpperCase(),
2097
- tableName: tableName.toUpperCase()
2098
- }
2099
- );
2100
- return !!(result.rows && result.rows.length > 0 && result.rows[0].COUNT > 0);
2101
- } finally {
2102
- await conn.close();
2103
- }
2104
- } catch (error) {
2105
- console.error("Error checking table existence in Oracle:", error);
2106
- throw error;
2107
- }
2108
- }
2109
- async getTableSchema(tableName, schema) {
2110
- return this.getTableColumns(tableName, schema);
2111
- }
2112
- async getStoredProcedures(schema) {
2113
- try {
2114
- const conn = await this.getConnection();
2115
- try {
2116
- const schemaName = schema || this.currentSchema;
2117
- const result = await conn.execute(
2118
- `SELECT OBJECT_NAME
2119
- FROM ALL_OBJECTS
2120
- WHERE OWNER = :schema
2121
- AND OBJECT_TYPE IN ('PROCEDURE', 'FUNCTION')
2122
- ORDER BY OBJECT_NAME`,
2123
- { schema: schemaName?.toUpperCase() }
2124
- );
2125
- return result.rows?.map((row) => row.OBJECT_NAME) || [];
2126
- } finally {
2127
- await conn.close();
2128
- }
2129
- } catch (error) {
2130
- console.error("Error getting stored procedures from Oracle:", error);
2131
- throw error;
2132
- }
2133
- }
2134
- async getStoredProcedureDetail(procedureName, schema) {
2135
- try {
2136
- const conn = await this.getConnection();
2137
- try {
2138
- const schemaName = schema || this.currentSchema;
2139
- const typeResult = await conn.execute(
2140
- `SELECT OBJECT_TYPE
2141
- FROM ALL_OBJECTS
2142
- WHERE OWNER = :schema
2143
- AND OBJECT_NAME = :procName`,
2144
- {
2145
- schema: schemaName?.toUpperCase(),
2146
- procName: procedureName.toUpperCase()
2147
- }
2148
- );
2149
- if (!typeResult.rows || typeResult.rows.length === 0) {
2150
- throw new Error(`Procedure or function ${procedureName} not found`);
2151
- }
2152
- const objectType = typeResult.rows[0].OBJECT_TYPE;
2153
- const isProcedure = objectType === "PROCEDURE";
2154
- const sourceResult = await conn.execute(
2155
- `SELECT TEXT
2156
- FROM ALL_SOURCE
2157
- WHERE OWNER = :schema
2158
- AND NAME = :procName
2159
- AND TYPE = :objectType
2160
- ORDER BY LINE`,
2161
- {
2162
- schema: schemaName?.toUpperCase(),
2163
- procName: procedureName.toUpperCase(),
2164
- objectType
2165
- }
2166
- );
2167
- let definition = "";
2168
- if (sourceResult.rows && sourceResult.rows.length > 0) {
2169
- definition = sourceResult.rows.map((row) => row.TEXT).join("");
2170
- }
2171
- const paramsResult = await conn.execute(
2172
- `SELECT
2173
- ARGUMENT_NAME,
2174
- IN_OUT,
2175
- DATA_TYPE,
2176
- DATA_LENGTH,
2177
- DATA_PRECISION,
2178
- DATA_SCALE
2179
- FROM ALL_ARGUMENTS
2180
- WHERE OWNER = :schema
2181
- AND OBJECT_NAME = :procName
2182
- AND POSITION > 0
2183
- ORDER BY SEQUENCE`,
2184
- {
2185
- schema: schemaName?.toUpperCase(),
2186
- procName: procedureName.toUpperCase()
2187
- }
2188
- );
2189
- let parameterList = "";
2190
- let returnType = "";
2191
- if (paramsResult.rows && paramsResult.rows.length > 0) {
2192
- const params = paramsResult.rows.map((row) => {
2193
- const argRow = row;
2194
- if (argRow.IN_OUT === "OUT" && !isProcedure) {
2195
- returnType = formatOracleDataType(
2196
- argRow.DATA_TYPE,
2197
- argRow.DATA_LENGTH,
2198
- argRow.DATA_PRECISION,
2199
- argRow.DATA_SCALE
2200
- );
2201
- return null;
2202
- }
2203
- const paramType = formatOracleDataType(
2204
- argRow.DATA_TYPE,
2205
- argRow.DATA_LENGTH,
2206
- argRow.DATA_PRECISION,
2207
- argRow.DATA_SCALE
2208
- );
2209
- return `${argRow.ARGUMENT_NAME} ${argRow.IN_OUT} ${paramType}`;
2210
- }).filter(Boolean);
2211
- parameterList = params.join(", ");
2212
- }
2213
- return {
2214
- procedure_name: procedureName,
2215
- procedure_type: isProcedure ? "procedure" : "function",
2216
- language: "PL/SQL",
2217
- parameter_list: parameterList,
2218
- return_type: returnType || void 0,
2219
- definition: definition || void 0
2220
- };
2221
- } finally {
2222
- await conn.close();
2223
- }
2224
- } catch (error) {
2225
- console.error("Error getting stored procedure details from Oracle:", error);
2226
- throw error;
2227
- }
2228
- }
2229
- async executeSQL(sql2, params) {
2230
- try {
2231
- const conn = await this.getConnection();
2232
- try {
2233
- let bindParams = void 0;
2234
- if (params && params.length > 0) {
2235
- bindParams = {};
2236
- for (let i = 0; i < params.length; i++) {
2237
- bindParams[`param${i + 1}`] = params[i];
2238
- }
2239
- let paramIndex = 1;
2240
- sql2 = sql2.replace(/\?/g, () => `:param${paramIndex++}`);
2241
- }
2242
- const options = {
2243
- outFormat: oracledb.OUT_FORMAT_OBJECT,
2244
- autoCommit: true
2245
- };
2246
- const result = await conn.execute(sql2, bindParams || {}, options);
2247
- return {
2248
- rows: result.rows || [],
2249
- rowCount: result.rows?.length || 0,
2250
- fields: result.metaData?.map((col) => ({
2251
- name: col.name,
2252
- type: col.dbType?.toString() || "UNKNOWN"
2253
- })) || []
2254
- };
2255
- } finally {
2256
- await conn.close();
2257
- }
2258
- } catch (error) {
2259
- console.error("Error executing query in Oracle:", error);
2260
- throw error;
2261
- }
2262
- }
2263
- // Helper method to get a connection from the pool
2264
- async getConnection() {
2265
- if (!this.pool) {
2266
- throw new Error("Connection pool not initialized. Call connect() first.");
2267
- }
2268
- return this.pool.getConnection();
2269
- }
2270
- };
2271
- // Track if we've already initialized the client
2272
- _OracleConnector.clientInitialized = false;
2273
- var OracleConnector = _OracleConnector;
2274
- function formatOracleDataType(dataType, dataLength, dataPrecision, dataScale) {
2275
- if (!dataType) {
2276
- return "UNKNOWN";
2277
- }
2278
- switch (dataType.toUpperCase()) {
2279
- case "VARCHAR2":
2280
- case "CHAR":
2281
- case "NVARCHAR2":
2282
- case "NCHAR":
2283
- return `${dataType}(${dataLength || ""})`;
2284
- case "NUMBER":
2285
- if (dataPrecision !== void 0 && dataScale !== void 0) {
2286
- return `NUMBER(${dataPrecision}, ${dataScale})`;
2287
- } else if (dataPrecision !== void 0) {
2288
- return `NUMBER(${dataPrecision})`;
2289
- }
2290
- return "NUMBER";
2291
- default:
2292
- return dataType;
2293
- }
2294
- }
2295
- ConnectorRegistry.register(new OracleConnector());
2296
-
2297
1878
  // src/server.ts
2298
1879
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
2299
1880
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -2898,8 +2479,7 @@ var allowedKeywords = {
2898
2479
  mysql: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2899
2480
  mariadb: ["select", "with", "explain", "analyze", "show", "describe", "desc"],
2900
2481
  sqlite: ["select", "with", "explain", "analyze", "pragma"],
2901
- sqlserver: ["select", "with", "explain", "showplan"],
2902
- oracle: ["select", "with", "explain"]
2482
+ sqlserver: ["select", "with", "explain", "showplan"]
2903
2483
  };
2904
2484
 
2905
2485
  // src/tools/execute-sql.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.7.0",
3
+ "version": "0.8.4",
4
4
  "description": "Universal Database MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -24,21 +24,24 @@
24
24
  "mariadb": "^3.4.0",
25
25
  "mssql": "^11.0.1",
26
26
  "mysql2": "^3.13.0",
27
- "oracledb": "^6.5.1",
28
27
  "pg": "^8.13.3",
29
28
  "zod": "^3.24.2"
30
29
  },
31
30
  "devDependencies": {
31
+ "@testcontainers/mariadb": "^11.0.3",
32
+ "@testcontainers/mssqlserver": "^11.0.3",
33
+ "@testcontainers/mysql": "^11.0.3",
34
+ "@testcontainers/postgresql": "^11.0.3",
32
35
  "@types/better-sqlite3": "^7.6.12",
33
36
  "@types/express": "^4.17.21",
34
37
  "@types/mssql": "^9.1.7",
35
38
  "@types/node": "^22.13.10",
36
- "@types/oracledb": "^6.6.0",
37
39
  "@types/pg": "^8.11.11",
38
40
  "cross-env": "^7.0.3",
39
41
  "husky": "^9.0.11",
40
42
  "lint-staged": "^15.2.2",
41
43
  "prettier": "^3.5.3",
44
+ "testcontainers": "^11.0.3",
42
45
  "ts-node": "^10.9.2",
43
46
  "tsup": "^8.4.0",
44
47
  "tsx": "^4.19.3",
@@ -67,6 +70,7 @@
67
70
  "crossdev": "cross-env NODE_ENV=development tsx src/index.ts",
68
71
  "test": "vitest run",
69
72
  "test:watch": "vitest",
73
+ "test:integration": "vitest run --testNamePattern='Integration Tests'",
70
74
  "pre-commit": "lint-staged"
71
75
  }
72
76
  }