@bytebase/dbhub 0.4.11 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -24
- package/dist/index.js +90 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,10 +49,10 @@ https://demo.dbhub.ai/sse connects a [sample employee database](https://github.c
|
|
|
49
49
|
|
|
50
50
|
### Database Tools
|
|
51
51
|
|
|
52
|
-
| Tool | Command Name | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite | Oracle |
|
|
53
|
-
| --------------- | ----------------- | :--------: | :---: | :-----: | :--------: | ------ | :----: |
|
|
54
|
-
| Execute SQL | `execute_sql` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
55
|
-
| List Connectors | `list_connectors` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
52
|
+
| Tool | Command Name | Description | PostgreSQL | MySQL | MariaDB | SQL Server | SQLite | Oracle |
|
|
53
|
+
| --------------- | ----------------- | ------------------------------------------------------------------- | :--------: | :---: | :-----: | :--------: | ------ | :----: |
|
|
54
|
+
| Execute SQL | `execute_sql` | Execute single or multiple SQL statements (separated by semicolons) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
55
|
+
| List Connectors | `list_connectors` | List all available database connectors | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
56
56
|
|
|
57
57
|
### Prompt Capabilities
|
|
58
58
|
|
|
@@ -99,7 +99,7 @@ docker run --rm --init \
|
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
|
-
# Oracle example with thick mode for connecting to 11g or older
|
|
102
|
+
# Oracle example with thick mode for connecting to 11g or older
|
|
103
103
|
docker run --rm --init \
|
|
104
104
|
--name dbhub \
|
|
105
105
|
--publish 8080:8080 \
|
|
@@ -179,14 +179,14 @@ npx @bytebase/dbhub --transport sse --port 8080 --demo
|
|
|
179
179
|
|
|
180
180
|
You can specify the SSL mode using the `sslmode` parameter in your DSN string:
|
|
181
181
|
|
|
182
|
-
| Database | `sslmode=disable` | `sslmode=require` |
|
|
183
|
-
|
|
184
|
-
| PostgreSQL |
|
|
185
|
-
| MySQL |
|
|
186
|
-
| MariaDB |
|
|
187
|
-
| SQL Server |
|
|
188
|
-
| Oracle |
|
|
189
|
-
| SQLite |
|
|
182
|
+
| Database | `sslmode=disable` | `sslmode=require` | Default SSL Behavior |
|
|
183
|
+
| ---------- | :---------------: | :---------------: | :----------------------------: |
|
|
184
|
+
| PostgreSQL | ✅ | ✅ | Certificate verification |
|
|
185
|
+
| MySQL | ✅ | ✅ | Certificate verification |
|
|
186
|
+
| MariaDB | ✅ | ✅ | Certificate verification |
|
|
187
|
+
| SQL Server | ✅ | ✅ | Certificate verification |
|
|
188
|
+
| Oracle | ✅ | ✅ | N/A (use Oracle client config) |
|
|
189
|
+
| SQLite | ❌ | ❌ | N/A (file-based) |
|
|
190
190
|
|
|
191
191
|
**SSL Mode Options:**
|
|
192
192
|
|
|
@@ -196,6 +196,7 @@ You can specify the SSL mode using the `sslmode` parameter in your DSN string:
|
|
|
196
196
|
Without specifying `sslmode`, most databases default to certificate verification, which provides the highest level of security.
|
|
197
197
|
|
|
198
198
|
Example usage:
|
|
199
|
+
|
|
199
200
|
```bash
|
|
200
201
|
# Disable SSL
|
|
201
202
|
postgres://user:password@localhost:5432/dbname?sslmode=disable
|
|
@@ -255,14 +256,14 @@ For real databases, a Database Source Name (DSN) is required. You can provide th
|
|
|
255
256
|
|
|
256
257
|
DBHub supports the following database connection string formats:
|
|
257
258
|
|
|
258
|
-
| Database | DSN Format | Example
|
|
259
|
-
| ---------- | --------------------------------------------------------- |
|
|
260
|
-
| MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname?sslmode=disable`
|
|
261
|
-
| MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname?sslmode=disable`
|
|
262
|
-
| PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable`
|
|
263
|
-
| SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname?sslmode=disable`
|
|
264
|
-
| SQLite | `sqlite:///[path/to/file]` or `sqlite:///:memory:`
|
|
265
|
-
| Oracle | `oracle://[user]:[password]@[host]:[port]/[service_name]` | `oracle://username:password@localhost:1521/service_name?sslmode=disable`
|
|
259
|
+
| Database | DSN Format | Example |
|
|
260
|
+
| ---------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
261
|
+
| MySQL | `mysql://[user]:[password]@[host]:[port]/[database]` | `mysql://user:password@localhost:3306/dbname?sslmode=disable` |
|
|
262
|
+
| MariaDB | `mariadb://[user]:[password]@[host]:[port]/[database]` | `mariadb://user:password@localhost:3306/dbname?sslmode=disable` |
|
|
263
|
+
| PostgreSQL | `postgres://[user]:[password]@[host]:[port]/[database]` | `postgres://user:password@localhost:5432/dbname?sslmode=disable` |
|
|
264
|
+
| SQL Server | `sqlserver://[user]:[password]@[host]:[port]/[database]` | `sqlserver://user:password@localhost:1433/dbname?sslmode=disable` |
|
|
265
|
+
| SQLite | `sqlite:///[path/to/file]` or `sqlite:///:memory:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` or `sqlite:///:memory:` |
|
|
266
|
+
| Oracle | `oracle://[user]:[password]@[host]:[port]/[service_name]` | `oracle://username:password@localhost:1521/service_name?sslmode=disable` |
|
|
266
267
|
|
|
267
268
|
#### Oracle
|
|
268
269
|
|
|
@@ -340,16 +341,17 @@ The demo mode uses an in-memory SQLite database loaded with the [sample employee
|
|
|
340
341
|
|
|
341
342
|
### Testing
|
|
342
343
|
|
|
343
|
-
The project uses Vitest for testing:
|
|
344
|
+
The project uses Vitest for comprehensive unit testing:
|
|
344
345
|
|
|
345
|
-
- Run tests
|
|
346
|
-
- Run tests in watch mode
|
|
346
|
+
- **Run all tests**: `pnpm test`
|
|
347
|
+
- **Run tests in watch mode**: `pnpm test:watch`
|
|
347
348
|
|
|
348
349
|
#### Pre-commit Hooks (for Developers)
|
|
349
350
|
|
|
350
351
|
The project includes pre-commit hooks to run tests automatically before each commit:
|
|
351
352
|
|
|
352
353
|
1. After cloning the repository, set up the pre-commit hooks:
|
|
354
|
+
|
|
353
355
|
```bash
|
|
354
356
|
./scripts/setup-husky.sh
|
|
355
357
|
```
|
package/dist/index.js
CHANGED
|
@@ -457,7 +457,26 @@ var PostgresConnector = class {
|
|
|
457
457
|
}
|
|
458
458
|
const client = await this.pool.connect();
|
|
459
459
|
try {
|
|
460
|
-
|
|
460
|
+
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
461
|
+
if (statements.length === 1) {
|
|
462
|
+
return await client.query(statements[0]);
|
|
463
|
+
} else {
|
|
464
|
+
let allRows = [];
|
|
465
|
+
await client.query("BEGIN");
|
|
466
|
+
try {
|
|
467
|
+
for (const statement of statements) {
|
|
468
|
+
const result = await client.query(statement);
|
|
469
|
+
if (result.rows && result.rows.length > 0) {
|
|
470
|
+
allRows.push(...result.rows);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
await client.query("COMMIT");
|
|
474
|
+
} catch (error) {
|
|
475
|
+
await client.query("ROLLBACK");
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
return { rows: allRows };
|
|
479
|
+
}
|
|
461
480
|
} finally {
|
|
462
481
|
client.release();
|
|
463
482
|
}
|
|
@@ -1004,8 +1023,31 @@ var SQLiteConnector = class {
|
|
|
1004
1023
|
throw new Error("Not connected to SQLite database");
|
|
1005
1024
|
}
|
|
1006
1025
|
try {
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1026
|
+
const statements = sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
1027
|
+
if (statements.length === 1) {
|
|
1028
|
+
const rows = this.db.prepare(statements[0]).all();
|
|
1029
|
+
return { rows };
|
|
1030
|
+
} else {
|
|
1031
|
+
const readStatements = [];
|
|
1032
|
+
const writeStatements = [];
|
|
1033
|
+
for (const statement of statements) {
|
|
1034
|
+
const trimmedStatement = statement.toLowerCase().trim();
|
|
1035
|
+
if (trimmedStatement.startsWith("select") || trimmedStatement.startsWith("with") || trimmedStatement.startsWith("explain") || trimmedStatement.startsWith("analyze") || trimmedStatement.startsWith("pragma")) {
|
|
1036
|
+
readStatements.push(statement);
|
|
1037
|
+
} else {
|
|
1038
|
+
writeStatements.push(statement);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (writeStatements.length > 0) {
|
|
1042
|
+
this.db.exec(writeStatements.join("; "));
|
|
1043
|
+
}
|
|
1044
|
+
let allRows = [];
|
|
1045
|
+
for (const statement of readStatements) {
|
|
1046
|
+
const result = this.db.prepare(statement).all();
|
|
1047
|
+
allRows.push(...result);
|
|
1048
|
+
}
|
|
1049
|
+
return { rows: allRows };
|
|
1050
|
+
}
|
|
1009
1051
|
} catch (error) {
|
|
1010
1052
|
throw error;
|
|
1011
1053
|
}
|
|
@@ -1029,7 +1071,9 @@ var MySQLDSNParser = class {
|
|
|
1029
1071
|
database: url.pathname ? url.pathname.substring(1) : "",
|
|
1030
1072
|
// Remove leading '/' if exists
|
|
1031
1073
|
user: url.username,
|
|
1032
|
-
password: url.password
|
|
1074
|
+
password: url.password,
|
|
1075
|
+
multipleStatements: true
|
|
1076
|
+
// Enable native multi-statement support
|
|
1033
1077
|
};
|
|
1034
1078
|
url.forEachSearchParam((value, key) => {
|
|
1035
1079
|
if (key === "sslmode") {
|
|
@@ -1359,8 +1403,19 @@ var MySQLConnector = class {
|
|
|
1359
1403
|
throw new Error("Not connected to database");
|
|
1360
1404
|
}
|
|
1361
1405
|
try {
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1406
|
+
const results = await this.pool.query(sql2);
|
|
1407
|
+
if (Array.isArray(results[0])) {
|
|
1408
|
+
let allRows = [];
|
|
1409
|
+
for (const result of results[0]) {
|
|
1410
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
1411
|
+
allRows.push(...result);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return { rows: allRows };
|
|
1415
|
+
} else {
|
|
1416
|
+
const [rows, fields] = results;
|
|
1417
|
+
return { rows, fields };
|
|
1418
|
+
}
|
|
1364
1419
|
} catch (error) {
|
|
1365
1420
|
console.error("Error executing query:", error);
|
|
1366
1421
|
throw error;
|
|
@@ -1385,7 +1440,9 @@ var MariadbDSNParser = class {
|
|
|
1385
1440
|
database: url.pathname ? url.pathname.substring(1) : "",
|
|
1386
1441
|
// Remove leading '/' if exists
|
|
1387
1442
|
user: url.username,
|
|
1388
|
-
password: url.password
|
|
1443
|
+
password: url.password,
|
|
1444
|
+
multipleStatements: true
|
|
1445
|
+
// Enable native multi-statement support
|
|
1389
1446
|
};
|
|
1390
1447
|
url.forEachSearchParam((value, key) => {
|
|
1391
1448
|
if (key === "sslmode") {
|
|
@@ -1716,8 +1773,23 @@ var MariaDBConnector = class {
|
|
|
1716
1773
|
throw new Error("Not connected to database");
|
|
1717
1774
|
}
|
|
1718
1775
|
try {
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1776
|
+
const results = await this.pool.query(sql2);
|
|
1777
|
+
if (Array.isArray(results)) {
|
|
1778
|
+
if (results.length > 1 || results[0] && Array.isArray(results[0])) {
|
|
1779
|
+
let allRows = [];
|
|
1780
|
+
for (const result of results) {
|
|
1781
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
1782
|
+
allRows.push(...result);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return { rows: allRows };
|
|
1786
|
+
} else {
|
|
1787
|
+
const [rows, fields] = results;
|
|
1788
|
+
return { rows, fields };
|
|
1789
|
+
}
|
|
1790
|
+
} else {
|
|
1791
|
+
return { rows: results };
|
|
1792
|
+
}
|
|
1721
1793
|
} catch (error) {
|
|
1722
1794
|
console.error("Error executing query:", error);
|
|
1723
1795
|
throw error;
|
|
@@ -2836,18 +2908,25 @@ var allowedKeywords = {
|
|
|
2836
2908
|
|
|
2837
2909
|
// src/tools/execute-sql.ts
|
|
2838
2910
|
var executeSqlSchema = {
|
|
2839
|
-
sql: z.string().describe("SQL query to execute (
|
|
2911
|
+
sql: z.string().describe("SQL query or multiple SQL statements to execute (separated by semicolons)")
|
|
2840
2912
|
};
|
|
2913
|
+
function splitSQLStatements(sql2) {
|
|
2914
|
+
return sql2.split(";").map((statement) => statement.trim()).filter((statement) => statement.length > 0);
|
|
2915
|
+
}
|
|
2841
2916
|
function isReadOnlySQL(sql2, connectorType) {
|
|
2842
2917
|
const normalizedSQL = sql2.trim().toLowerCase();
|
|
2843
2918
|
const firstWord = normalizedSQL.split(/\s+/)[0];
|
|
2844
2919
|
const keywordList = allowedKeywords[connectorType] || allowedKeywords.default || [];
|
|
2845
2920
|
return keywordList.includes(firstWord);
|
|
2846
2921
|
}
|
|
2922
|
+
function areAllStatementsReadOnly(sql2, connectorType) {
|
|
2923
|
+
const statements = splitSQLStatements(sql2);
|
|
2924
|
+
return statements.every((statement) => isReadOnlySQL(statement, connectorType));
|
|
2925
|
+
}
|
|
2847
2926
|
async function executeSqlToolHandler({ sql: sql2 }, _extra) {
|
|
2848
2927
|
const connector = ConnectorManager.getCurrentConnector();
|
|
2849
2928
|
try {
|
|
2850
|
-
if (isReadOnlyMode() && !
|
|
2929
|
+
if (isReadOnlyMode() && !areAllStatementsReadOnly(sql2, connector.id)) {
|
|
2851
2930
|
return createToolErrorResponse(
|
|
2852
2931
|
`Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`,
|
|
2853
2932
|
"READONLY_VIOLATION"
|