@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.
Files changed (3) hide show
  1. package/README.md +26 -24
  2. package/dist/index.js +90 -11
  3. 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` | 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) |
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:` | `sqlite:///path/to/database.db`, `sqlite:C:/Users/YourName/data/database.db (windows)` 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: `pnpm test`
346
- - Run tests in watch mode: `pnpm test:watch`
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
- return await client.query(sql2);
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 rows = this.db.prepare(sql2).all();
1008
- return { rows };
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 [rows, fields] = await this.pool.query(sql2);
1363
- return { rows, fields };
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 [rows, fields] = await this.pool.query(sql2);
1720
- return { rows, fields };
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 (SELECT only)")
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() && !isReadOnlySQL(sql2, connector.id)) {
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebase/dbhub",
3
- "version": "0.4.11",
3
+ "version": "0.5.0",
4
4
  "description": "Universal Database MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",