@bytebase/dbhub 0.7.0 → 0.8.2
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/README.md +107 -68
- package/dist/index.js +122 -542
- 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 |
|
|
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
|
|
57
|
-
|
|
|
58
|
-
| Execute SQL
|
|
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 |
|
|
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
|
|
321
|
-
| --------- | -------------------- |
|
|
322
|
-
| dsn | `DSN` | Database connection string
|
|
323
|
-
| transport | `TRANSPORT` | Transport mode: `stdio` or `http`
|
|
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
|
|
326
|
-
| demo | N/A | Run in demo mode with sample employee database
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
530
|
-
// Default to
|
|
531
|
-
trustServerCertificate: options.trustServerCertificate
|
|
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=
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
999
|
-
|
|
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
|
|
1029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.8.2",
|
|
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
|
}
|