@bytebase/dbhub 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -50
- package/dist/{chunk-JO7YQPBQ.js → chunk-EPOKWSFC.js} +24 -2
- package/dist/chunk-FU2ZJE4E.js +16 -0
- package/dist/{chunk-MQR6VX5F.js → chunk-GSSLHJB6.js} +13 -4
- package/dist/chunk-NIILHXTG.js +190 -0
- package/dist/index.js +280 -43
- package/dist/{mariadb-72C2BNCG.js → mariadb-DUDFBY5W.js} +10 -1
- package/dist/{mysql-CDUKJMNN.js → mysql-265M3KBD.js} +13 -1
- package/dist/{postgres-YAYHDXVD.js → postgres-5ZOFYV6X.js} +61 -0
- package/dist/{registry-ZU6ZFNQ2.js → registry-JYRRNC3E.js} +1 -1
- package/dist/{sqlserver-COKW7RIK.js → sqlserver-PSAZRH7G.js} +80 -1
- package/package.json +1 -1
- package/dist/chunk-DUCNPMNW.js +0 -113
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
DBHub is a zero-dependency, token efficient MCP server implementing the Model Context Protocol (MCP) server interface. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
|
|
34
34
|
|
|
35
|
-
- **Local Development First**: Zero dependency, token efficient with
|
|
35
|
+
- **Local Development First**: Zero dependency, token efficient with a minimal set of MCP tools to maximize context window
|
|
36
36
|
- **Multi-Database**: PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite through a single interface
|
|
37
37
|
- **Multi-Connection**: Connect to multiple databases simultaneously with TOML configuration
|
|
38
38
|
- **Guardrails**: Read-only mode, row limiting, and query timeout to prevent runaway operations
|
|
@@ -48,6 +48,8 @@ DBHub implements MCP tools for database operations:
|
|
|
48
48
|
|
|
49
49
|
- **[execute_sql](https://dbhub.ai/tools/execute-sql)**: Execute SQL queries with transaction support and safety controls
|
|
50
50
|
- **[search_objects](https://dbhub.ai/tools/search-objects)**: Search and explore database schemas, tables, columns, indexes, and procedures with progressive disclosure
|
|
51
|
+
- **[explain_sql](https://dbhub.ai/tools/explain-sql)** (opt-in): Show a query's execution plan without running it
|
|
52
|
+
- **[health_check](https://dbhub.ai/tools/health-check)** (opt-in, PostgreSQL/MySQL/MariaDB/SQL Server for now): Report connection pool state and buffer cache hit ratio
|
|
51
53
|
- **[Custom Tools](https://dbhub.ai/tools/custom-tools)**: Define reusable, parameterized SQL operations in your `dbhub.toml` configuration file
|
|
52
54
|
|
|
53
55
|
## Workbench
|
|
@@ -58,61 +60,17 @@ DBHub includes a [built-in web interface](https://dbhub.ai/workbench/overview) f
|
|
|
58
60
|
|
|
59
61
|
## Installation
|
|
60
62
|
|
|
61
|
-
See the full [Installation Guide](https://dbhub.ai/installation) for detailed instructions.
|
|
62
|
-
|
|
63
|
-
### Quick Start
|
|
64
|
-
|
|
65
|
-
**Docker:**
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
docker run --rm --init \
|
|
69
|
-
--name dbhub \
|
|
70
|
-
--publish 8080:8080 \
|
|
71
|
-
bytebase/dbhub \
|
|
72
|
-
--transport http \
|
|
73
|
-
--port 8080 \
|
|
74
|
-
--dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
**NPM:** (requires Node.js >= 22.5.0)
|
|
78
|
-
|
|
79
63
|
```bash
|
|
80
64
|
npx @bytebase/dbhub@latest --transport http --port 8080 --dsn "postgres://user:password@localhost:5432/dbname?sslmode=disable"
|
|
81
65
|
```
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Download `dbhub-<version>.mcpb` from the [latest release](https://github.com/bytebase/dbhub/releases/latest) and install it in any [MCPB-compatible client](https://github.com/modelcontextprotocol/mcpb) — Claude Desktop (double-click, or drag into Settings → Extensions), Claude Code, or MCP for Windows — then enter your database connection string. The bundle runs locally over stdio, is **read-only by design** (writes are rejected and the database session is set to read-only at the engine level), and needs no remote endpoint or OAuth setup — ideal for giving non-technical teammates curated, read-only database access. Pair it with a least-privilege, read-only database account. See the [MCP Bundle guide](https://dbhub.ai/mcpb) for details and for packaging your own bundle.
|
|
86
|
-
|
|
87
|
-
**Claude Code Plugin:**
|
|
88
|
-
|
|
89
|
-
For Claude Code, the [DBHub plugin](https://dbhub.ai/claude-code-plugin) registers the MCP server with one install command — Claude Code prompts for your connection string and stores it in secure storage — and adds `/dbhub:setup` and `/dbhub:explore` skills. Read-only by design, like the MCP Bundle.
|
|
90
|
-
|
|
91
|
-
**Demo Mode:**
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
npx @bytebase/dbhub@latest --transport http --port 8080 --demo
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
**Restrict to loopback (recommended for production):**
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
npx @bytebase/dbhub@latest --transport http --host 127.0.0.1 --port 8080 --demo
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
> The HTTP transport defaults to `--host 0.0.0.0`, exposing DBHub on every network interface. For production, bind to `127.0.0.1` and front DBHub with a reverse proxy (nginx/Caddy) or firewall — DBHub does not authenticate HTTP clients.
|
|
104
|
-
>
|
|
105
|
-
> The HTTP transport also has built-in DNS-rebinding protection: it only accepts requests whose `Host` is loopback, this machine's own hostname/IPs, or a name you allow via [`--allowed-hosts`](https://dbhub.ai/config/command-line#allowed-hosts). If a client behind a reverse proxy or custom DNS name gets a `403`, add that hostname with `--allowed-hosts`.
|
|
106
|
-
>
|
|
107
|
-
> DBHub serves both MCP protocol eras on the same `/mcp` endpoint: the stateless **2026-07-28** revision (cacheable `tools/list`, no session state — safe behind round-robin load balancers) and the 2025-era Streamable HTTP protocol for older clients. 2026-era clients send the standard `Mcp-Method` / `Mcp-Name` headers on every request, so a fronting gateway or WAF can route and rate-limit `execute_sql` separately from `search_objects` without parsing JSON bodies.
|
|
108
|
-
|
|
109
|
-
See [Command-Line Options](https://dbhub.ai/config/command-line) for all available parameters.
|
|
110
|
-
|
|
111
|
-
### Multi-Database Setup
|
|
67
|
+
Also available as:
|
|
112
68
|
|
|
113
|
-
|
|
69
|
+
- [Docker image](https://dbhub.ai/installation#docker)
|
|
70
|
+
- [MCP Bundle](https://dbhub.ai/mcpb) (one-click install, read-only)
|
|
71
|
+
- [Claude Code plugin](https://dbhub.ai/claude-code-plugin)
|
|
114
72
|
|
|
115
|
-
See [Multi-Database Configuration](https://dbhub.ai/config/toml) for
|
|
73
|
+
See the [Installation Guide](https://dbhub.ai/installation) for all options, [Command-Line Options](https://dbhub.ai/config/command-line) for parameters, and [Multi-Database Configuration](https://dbhub.ai/config/toml) for connecting several databases at once.
|
|
116
74
|
|
|
117
75
|
## Development
|
|
118
76
|
|
|
@@ -52,6 +52,22 @@ var sqlServerPassThroughPattern = new RegExp(
|
|
|
52
52
|
`\\b(?:${sqlServerPassThroughKeywords.join("|")})\\s*\\(`,
|
|
53
53
|
"i"
|
|
54
54
|
);
|
|
55
|
+
var escapeHatchFunctionKeywords = {
|
|
56
|
+
mysql: ["load_file", "get_lock", "release_lock", "release_all_locks"],
|
|
57
|
+
mariadb: ["load_file", "get_lock", "release_lock", "release_all_locks"],
|
|
58
|
+
postgres: ["pg_read_file", "pg_read_binary_file", "pg_ls_dir"],
|
|
59
|
+
sqlserver: sqlServerPassThroughKeywords
|
|
60
|
+
};
|
|
61
|
+
var escapeHatchFunctionPatterns = Object.fromEntries(
|
|
62
|
+
Object.entries(escapeHatchFunctionKeywords).map(([type, keywords]) => [
|
|
63
|
+
type,
|
|
64
|
+
new RegExp(`\\b(?:${keywords.join("|")})\\s*\\(`, "i")
|
|
65
|
+
])
|
|
66
|
+
);
|
|
67
|
+
function hasEscapeHatchFunction(strippedSQL, connectorType) {
|
|
68
|
+
const pattern = escapeHatchFunctionPatterns[connectorType];
|
|
69
|
+
return pattern !== void 0 && pattern.test(strippedSQL);
|
|
70
|
+
}
|
|
55
71
|
var mutatingPatternSqlServer = new RegExp(
|
|
56
72
|
`\\b(?:${[...mutatingKeywords, ...sqlServerDynamicSqlKeywords].join("|")})\\b`,
|
|
57
73
|
"i"
|
|
@@ -78,6 +94,10 @@ function isReadOnlySQL(sql, connectorType) {
|
|
|
78
94
|
connectorType
|
|
79
95
|
);
|
|
80
96
|
}
|
|
97
|
+
function getFirstKeyword(sql, connectorType) {
|
|
98
|
+
const cleaned = stripCommentsAndStrings(sql, connectorType).trim();
|
|
99
|
+
return cleaned.match(/\S+/)?.[0]?.toLowerCase() ?? "";
|
|
100
|
+
}
|
|
81
101
|
function checkReadOnly(cleanedSQL, connectorType) {
|
|
82
102
|
if (!cleanedSQL) {
|
|
83
103
|
return false;
|
|
@@ -87,7 +107,7 @@ function checkReadOnly(cleanedSQL, connectorType) {
|
|
|
87
107
|
if (!keywordList.includes(firstWord)) {
|
|
88
108
|
return false;
|
|
89
109
|
}
|
|
90
|
-
if (
|
|
110
|
+
if (hasEscapeHatchFunction(cleanedSQL, connectorType)) {
|
|
91
111
|
return false;
|
|
92
112
|
}
|
|
93
113
|
if (firstWord === "with") {
|
|
@@ -126,5 +146,7 @@ export {
|
|
|
126
146
|
sqlServerDynamicSqlPattern,
|
|
127
147
|
sqlServerPassThroughKeywords,
|
|
128
148
|
sqlServerPassThroughPattern,
|
|
129
|
-
|
|
149
|
+
hasEscapeHatchFunction,
|
|
150
|
+
isReadOnlySQL,
|
|
151
|
+
getFirstKeyword
|
|
130
152
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/connectors/health-check-utils.ts
|
|
2
|
+
function toNullableNumber(value) {
|
|
3
|
+
return value === null || value === void 0 ? null : Number(value);
|
|
4
|
+
}
|
|
5
|
+
function computeHitRatioPct(logicalReads, physicalReads) {
|
|
6
|
+
if (!Number.isFinite(logicalReads) || !Number.isFinite(physicalReads) || logicalReads === 0) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const pct = Math.round((logicalReads - physicalReads) / logicalReads * 1e4) / 100;
|
|
10
|
+
return Math.min(100, Math.max(0, pct));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
toNullableNumber,
|
|
15
|
+
computeHitRatioPct
|
|
16
|
+
};
|
|
@@ -13,10 +13,17 @@ import {
|
|
|
13
13
|
// src/tools/builtin-tools.ts
|
|
14
14
|
var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
|
|
15
15
|
var BUILTIN_TOOL_SEARCH_OBJECTS = "search_objects";
|
|
16
|
+
var BUILTIN_TOOL_EXPLAIN_SQL = "explain_sql";
|
|
17
|
+
var BUILTIN_TOOL_HEALTH_CHECK = "health_check";
|
|
16
18
|
var BUILTIN_TOOLS = [
|
|
17
19
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
18
20
|
BUILTIN_TOOL_SEARCH_OBJECTS
|
|
19
21
|
];
|
|
22
|
+
var ALL_BUILTIN_TOOL_NAMES = [
|
|
23
|
+
...BUILTIN_TOOLS,
|
|
24
|
+
BUILTIN_TOOL_EXPLAIN_SQL,
|
|
25
|
+
BUILTIN_TOOL_HEALTH_CHECK
|
|
26
|
+
];
|
|
20
27
|
|
|
21
28
|
// src/utils/ssh-tunnel.ts
|
|
22
29
|
import { Client } from "ssh2";
|
|
@@ -1130,7 +1137,7 @@ function validateToolsConfig(tools, sources, configPath) {
|
|
|
1130
1137
|
`Configuration file ${configPath}: tool '${tool.name}' references unknown source '${tool.source}'`
|
|
1131
1138
|
);
|
|
1132
1139
|
}
|
|
1133
|
-
const isBuiltin =
|
|
1140
|
+
const isBuiltin = ALL_BUILTIN_TOOL_NAMES.includes(tool.name);
|
|
1134
1141
|
const isExecuteSql = tool.name === BUILTIN_TOOL_EXECUTE_SQL;
|
|
1135
1142
|
if (isBuiltin) {
|
|
1136
1143
|
if (tool.description || tool.statement || tool.parameters) {
|
|
@@ -2278,7 +2285,7 @@ var ToolRegistry = class {
|
|
|
2278
2285
|
* Check if a tool name is a built-in tool
|
|
2279
2286
|
*/
|
|
2280
2287
|
isBuiltinTool(toolName) {
|
|
2281
|
-
return
|
|
2288
|
+
return ALL_BUILTIN_TOOL_NAMES.includes(toolName);
|
|
2282
2289
|
}
|
|
2283
2290
|
/**
|
|
2284
2291
|
* Validate a custom tool parameter definition
|
|
@@ -2350,10 +2357,10 @@ var ToolRegistry = class {
|
|
|
2350
2357
|
`Tool '${toolConfig.name}' references unknown source '${toolConfig.source}'. Available sources: ${availableSources.join(", ")}`
|
|
2351
2358
|
);
|
|
2352
2359
|
}
|
|
2353
|
-
for (const builtinName of
|
|
2360
|
+
for (const builtinName of ALL_BUILTIN_TOOL_NAMES) {
|
|
2354
2361
|
if (toolConfig.name === builtinName || toolConfig.name.startsWith(`${builtinName}_`)) {
|
|
2355
2362
|
throw new Error(
|
|
2356
|
-
`Tool name '${toolConfig.name}' conflicts with built-in tool naming pattern. Custom tools cannot use names starting with: ${
|
|
2363
|
+
`Tool name '${toolConfig.name}' conflicts with built-in tool naming pattern. Custom tools cannot use names starting with: ${ALL_BUILTIN_TOOL_NAMES.join(", ")}`
|
|
2357
2364
|
);
|
|
2358
2365
|
}
|
|
2359
2366
|
}
|
|
@@ -2488,6 +2495,8 @@ export {
|
|
|
2488
2495
|
resolveSourceConfigs,
|
|
2489
2496
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
2490
2497
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
2498
|
+
BUILTIN_TOOL_EXPLAIN_SQL,
|
|
2499
|
+
BUILTIN_TOOL_HEALTH_CHECK,
|
|
2491
2500
|
loadTomlConfig,
|
|
2492
2501
|
resolveTomlConfigPath,
|
|
2493
2502
|
classifyConnectionError,
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeHitRatioPct,
|
|
3
|
+
toNullableNumber
|
|
4
|
+
} from "./chunk-FU2ZJE4E.js";
|
|
5
|
+
import {
|
|
6
|
+
obfuscateDSNPassword
|
|
7
|
+
} from "./chunk-JEZZN2YZ.js";
|
|
8
|
+
|
|
9
|
+
// src/connectors/mysql-family-health-check.ts
|
|
10
|
+
async function getMySQLFamilyHealthCheck(query) {
|
|
11
|
+
const [processlistRows, maxConnRows, bufferPoolRows, trxOutcome] = await Promise.all([
|
|
12
|
+
query(`
|
|
13
|
+
SELECT
|
|
14
|
+
COUNT(*) AS total,
|
|
15
|
+
SUM(CASE WHEN COMMAND != 'Sleep' THEN 1 ELSE 0 END) AS active,
|
|
16
|
+
SUM(CASE WHEN COMMAND = 'Sleep' THEN 1 ELSE 0 END) AS idle,
|
|
17
|
+
MAX(CASE WHEN COMMAND != 'Sleep' THEN TIME ELSE NULL END) AS longest_active_query_seconds
|
|
18
|
+
FROM information_schema.PROCESSLIST
|
|
19
|
+
WHERE ID <> CONNECTION_ID()
|
|
20
|
+
`),
|
|
21
|
+
query(`SHOW VARIABLES LIKE 'max_connections'`),
|
|
22
|
+
query(`
|
|
23
|
+
SHOW GLOBAL STATUS WHERE Variable_name IN ('Innodb_buffer_pool_read_requests', 'Innodb_buffer_pool_reads')
|
|
24
|
+
`),
|
|
25
|
+
// Joining PROCESSLIST against INNODB_TRX to detect idle-in-transaction
|
|
26
|
+
// sessions requires the PROCESS privilege - unlike a plain PROCESSLIST
|
|
27
|
+
// select, which silently narrows to the caller's own session instead of
|
|
28
|
+
// erroring. Run concurrently with the other queries above, but resolve
|
|
29
|
+
// rather than reject on failure so it degrades instead of failing the
|
|
30
|
+
// whole health check.
|
|
31
|
+
query(`
|
|
32
|
+
SELECT
|
|
33
|
+
COUNT(*) AS idle_in_transaction,
|
|
34
|
+
MAX(TIMESTAMPDIFF(SECOND, t.trx_started, NOW())) AS longest_idle_in_transaction_seconds
|
|
35
|
+
FROM information_schema.INNODB_TRX t
|
|
36
|
+
JOIN information_schema.PROCESSLIST p ON p.ID = t.trx_mysql_thread_id
|
|
37
|
+
WHERE p.COMMAND = 'Sleep'
|
|
38
|
+
`).then(
|
|
39
|
+
(rows) => ({ ok: true, rows }),
|
|
40
|
+
() => ({ ok: false })
|
|
41
|
+
)
|
|
42
|
+
]);
|
|
43
|
+
const notes = [];
|
|
44
|
+
let idleInTransaction = 0;
|
|
45
|
+
let longestIdleInTransactionSeconds = null;
|
|
46
|
+
if (trxOutcome.ok) {
|
|
47
|
+
idleInTransaction = Number(trxOutcome.rows[0].idle_in_transaction);
|
|
48
|
+
longestIdleInTransactionSeconds = toNullableNumber(trxOutcome.rows[0].longest_idle_in_transaction_seconds);
|
|
49
|
+
} else {
|
|
50
|
+
notes.push(
|
|
51
|
+
"Connected user lacks the PROCESS privilege: PROCESSLIST visibility is restricted to the connecting user's own sessions, and this diagnostic session is excluded from the count, so connection counts and active-query duration may under-report (down to 0). Idle-in-transaction detection is unavailable."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const proc = processlistRows[0];
|
|
55
|
+
const bufferPoolStats = Object.fromEntries(bufferPoolRows.map((row) => [row.Variable_name, row.Value]));
|
|
56
|
+
const readRequests = Number(bufferPoolStats.Innodb_buffer_pool_read_requests ?? 0);
|
|
57
|
+
const reads = Number(bufferPoolStats.Innodb_buffer_pool_reads ?? 0);
|
|
58
|
+
return {
|
|
59
|
+
connections: {
|
|
60
|
+
total: Number(proc.total),
|
|
61
|
+
active: Number(proc.active),
|
|
62
|
+
idle: Number(proc.idle),
|
|
63
|
+
idleInTransaction,
|
|
64
|
+
// MySQL/MariaDB's InnoDB has no equivalent of Postgres's "idle in
|
|
65
|
+
// transaction (aborted)" state - a failed statement doesn't leave the
|
|
66
|
+
// session's transaction in a distinct aborted-but-open state the way
|
|
67
|
+
// Postgres does - so idleInTransactionAborted is left undefined.
|
|
68
|
+
maxConnections: maxConnRows.length > 0 ? Number(maxConnRows[0].Value) : null,
|
|
69
|
+
longestIdleInTransactionSeconds,
|
|
70
|
+
longestActiveQuerySeconds: toNullableNumber(proc.longest_active_query_seconds)
|
|
71
|
+
},
|
|
72
|
+
bufferCache: {
|
|
73
|
+
hitRatioPct: computeHitRatioPct(readRequests, reads),
|
|
74
|
+
blocksHit: readRequests - reads,
|
|
75
|
+
blocksRead: reads
|
|
76
|
+
},
|
|
77
|
+
...notes.length > 0 ? { notes } : {}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/utils/dsn-database.ts
|
|
82
|
+
var MissingDatabaseError = class extends Error {
|
|
83
|
+
constructor(message) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = "MissingDatabaseError";
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
function requireDatabaseInDSN(database, dsn, label) {
|
|
89
|
+
if (database) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
throw new MissingDatabaseError(
|
|
93
|
+
`${label} DSN must name a database.
|
|
94
|
+
Provided: ${obfuscateDSNPassword(dsn)}
|
|
95
|
+
Add the database to the DSN, e.g. ...:3306/mydb
|
|
96
|
+
To work with several databases, define one [[sources]] entry per database in a TOML config file: https://dbhub.ai/config/toml`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/utils/multi-statement-result-parser.ts
|
|
101
|
+
function isMetadataObject(element) {
|
|
102
|
+
if (!element || typeof element !== "object" || Array.isArray(element)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return "affectedRows" in element || "insertId" in element || "fieldCount" in element || "warningStatus" in element;
|
|
106
|
+
}
|
|
107
|
+
function isMultiStatementResult(results) {
|
|
108
|
+
if (!Array.isArray(results) || results.length === 0) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const firstElement = results[0];
|
|
112
|
+
return isMetadataObject(firstElement) || Array.isArray(firstElement);
|
|
113
|
+
}
|
|
114
|
+
function extractRowsFromMultiStatement(results) {
|
|
115
|
+
if (!Array.isArray(results)) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const allRows = [];
|
|
119
|
+
for (const result of results) {
|
|
120
|
+
if (Array.isArray(result)) {
|
|
121
|
+
allRows.push(...result);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return allRows;
|
|
125
|
+
}
|
|
126
|
+
function extractAffectedRows(results) {
|
|
127
|
+
if (isMetadataObject(results)) {
|
|
128
|
+
return results.affectedRows || 0;
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(results)) {
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
if (isMultiStatementResult(results)) {
|
|
134
|
+
let totalAffected = 0;
|
|
135
|
+
for (const result of results) {
|
|
136
|
+
if (isMetadataObject(result)) {
|
|
137
|
+
totalAffected += result.affectedRows || 0;
|
|
138
|
+
} else if (Array.isArray(result)) {
|
|
139
|
+
totalAffected += result.length;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return totalAffected;
|
|
143
|
+
}
|
|
144
|
+
return results.length;
|
|
145
|
+
}
|
|
146
|
+
function parseQueryResults(results) {
|
|
147
|
+
if (!Array.isArray(results)) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
if (isMultiStatementResult(results)) {
|
|
151
|
+
return extractRowsFromMultiStatement(results);
|
|
152
|
+
}
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/utils/readonly-transaction.ts
|
|
157
|
+
async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransaction, execute) {
|
|
158
|
+
if (!readonly) {
|
|
159
|
+
return execute();
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
await conn.query(
|
|
163
|
+
supportsReadOnlyTransaction ? "START TRANSACTION READ ONLY" : "START TRANSACTION"
|
|
164
|
+
);
|
|
165
|
+
const result = await execute();
|
|
166
|
+
await conn.query(supportsReadOnlyTransaction ? "COMMIT" : "ROLLBACK");
|
|
167
|
+
return result;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
try {
|
|
170
|
+
await conn.query("ROLLBACK");
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/utils/server-flavor.ts
|
|
178
|
+
function isTiDBVersion(version) {
|
|
179
|
+
return typeof version === "string" && /tidb/i.test(version);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export {
|
|
183
|
+
getMySQLFamilyHealthCheck,
|
|
184
|
+
MissingDatabaseError,
|
|
185
|
+
requireDatabaseInDSN,
|
|
186
|
+
extractAffectedRows,
|
|
187
|
+
parseQueryResults,
|
|
188
|
+
withReadOnlyTransaction,
|
|
189
|
+
isTiDBVersion
|
|
190
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
4
|
+
BUILTIN_TOOL_EXPLAIN_SQL,
|
|
5
|
+
BUILTIN_TOOL_HEALTH_CHECK,
|
|
4
6
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
5
7
|
ConnectorManager,
|
|
6
8
|
classifyConnectionError,
|
|
@@ -15,13 +17,14 @@ import {
|
|
|
15
17
|
resolveSourceConfigs,
|
|
16
18
|
resolveTomlConfigPath,
|
|
17
19
|
resolveTransport
|
|
18
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-GSSLHJB6.js";
|
|
19
21
|
import {
|
|
20
22
|
allowedKeywords,
|
|
23
|
+
getFirstKeyword,
|
|
24
|
+
hasEscapeHatchFunction,
|
|
21
25
|
isReadOnlySQL,
|
|
22
|
-
sqlServerDynamicSqlPattern
|
|
23
|
-
|
|
24
|
-
} from "./chunk-JO7YQPBQ.js";
|
|
26
|
+
sqlServerDynamicSqlPattern
|
|
27
|
+
} from "./chunk-EPOKWSFC.js";
|
|
25
28
|
import {
|
|
26
29
|
loadConnectors
|
|
27
30
|
} from "./chunk-WVVMH6FJ.js";
|
|
@@ -114,7 +117,8 @@ var dmlPattern = /\b(?:insert|update|delete|merge|replace\s+(?:(?:low_priority|d
|
|
|
114
117
|
var grantRevokePattern = /\b(?:grant|revoke)\b/i;
|
|
115
118
|
function isAdminStatement(stripped, connectorType) {
|
|
116
119
|
if (grantRevokePattern.test(stripped)) return true;
|
|
117
|
-
|
|
120
|
+
if (hasEscapeHatchFunction(stripped, connectorType)) return true;
|
|
121
|
+
return connectorType === "sqlserver" && sqlServerDynamicSqlPattern.test(stripped);
|
|
118
122
|
}
|
|
119
123
|
function policyFromReadonly(readonly) {
|
|
120
124
|
const write = readonly === true ? "deny" : "allow";
|
|
@@ -206,10 +210,22 @@ function getClientIdentifier(ctx) {
|
|
|
206
210
|
return "stdio";
|
|
207
211
|
}
|
|
208
212
|
|
|
213
|
+
// src/utils/normalize-id.ts
|
|
214
|
+
function normalizeSourceId(id) {
|
|
215
|
+
return id.replace(/[^a-zA-Z0-9]/g, "_");
|
|
216
|
+
}
|
|
217
|
+
|
|
209
218
|
// src/utils/tool-handler-helpers.ts
|
|
210
219
|
function getEffectiveSourceId(sourceId) {
|
|
211
220
|
return sourceId || "default";
|
|
212
221
|
}
|
|
222
|
+
function resolveTrackedToolName(sourceId, baseName) {
|
|
223
|
+
const isSingleSource = ConnectorManager.getAvailableSourceIds().length === 1;
|
|
224
|
+
if (isSingleSource) {
|
|
225
|
+
return baseName;
|
|
226
|
+
}
|
|
227
|
+
return `${baseName}_${normalizeSourceId(getEffectiveSourceId(sourceId))}`;
|
|
228
|
+
}
|
|
213
229
|
function createReadonlyViolationMessage(toolName, sourceId, connectorType) {
|
|
214
230
|
return `Tool '${toolName}' cannot execute in readonly mode for source '${sourceId}'. Only read-only SQL operations are allowed: ${allowedKeywords[connectorType]?.join(", ") || "none"}`;
|
|
215
231
|
}
|
|
@@ -785,15 +801,151 @@ function createSearchDatabaseObjectsToolHandler(sourceId) {
|
|
|
785
801
|
};
|
|
786
802
|
}
|
|
787
803
|
|
|
788
|
-
// src/
|
|
804
|
+
// src/tools/explain-sql.ts
|
|
789
805
|
import { z as z3 } from "zod";
|
|
806
|
+
var explainSqlSchema = {
|
|
807
|
+
sql: z3.string().describe("Single SQL statement to explain (no trailing semicolon-separated statements)")
|
|
808
|
+
};
|
|
809
|
+
var explainSqlInputSchema = z3.object(explainSqlSchema);
|
|
810
|
+
function buildExplainStatement(connectorType, sql) {
|
|
811
|
+
if (connectorType === "sqlite") {
|
|
812
|
+
return `EXPLAIN QUERY PLAN ${sql}`;
|
|
813
|
+
}
|
|
814
|
+
return `EXPLAIN ${sql}`;
|
|
815
|
+
}
|
|
816
|
+
function validateExplainInput(sql, connectorType) {
|
|
817
|
+
const statements = splitSQLStatements(sql, connectorType);
|
|
818
|
+
if (statements.length !== 1) {
|
|
819
|
+
return "explain_sql only supports a single SQL statement";
|
|
820
|
+
}
|
|
821
|
+
const firstWord = getFirstKeyword(statements[0], connectorType);
|
|
822
|
+
if (firstWord === "explain") {
|
|
823
|
+
return "explain_sql input must not itself start with EXPLAIN";
|
|
824
|
+
}
|
|
825
|
+
if (firstWord === "analyze") {
|
|
826
|
+
return "explain_sql does not support ANALYZE (it must never execute the statement)";
|
|
827
|
+
}
|
|
828
|
+
const cleaned = stripCommentsAndStrings(statements[0], connectorType).trim();
|
|
829
|
+
const leadingOptions = cleaned.match(/^\(([\s\S]*?)\)/)?.[1];
|
|
830
|
+
if (leadingOptions && /\banalyze\b/i.test(leadingOptions)) {
|
|
831
|
+
return "explain_sql does not support ANALYZE (it must never execute the statement)";
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
function createExplainSqlToolHandler(sourceId) {
|
|
836
|
+
return async (args, extra) => {
|
|
837
|
+
const { sql } = args;
|
|
838
|
+
const startTime = Date.now();
|
|
839
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
840
|
+
let success = true;
|
|
841
|
+
let errorMessage;
|
|
842
|
+
let result;
|
|
843
|
+
try {
|
|
844
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
845
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
846
|
+
const validationError = validateExplainInput(sql, connector.id);
|
|
847
|
+
if (validationError) {
|
|
848
|
+
success = false;
|
|
849
|
+
errorMessage = validationError;
|
|
850
|
+
return createToolErrorResponse(errorMessage, "INVALID_INPUT");
|
|
851
|
+
}
|
|
852
|
+
const explainStatement = buildExplainStatement(connector.id, sql);
|
|
853
|
+
result = await connector.executeSQL(explainStatement, { readonly: true });
|
|
854
|
+
const responseData = {
|
|
855
|
+
rows: result.rows,
|
|
856
|
+
count: result.rowCount,
|
|
857
|
+
source_id: effectiveSourceId,
|
|
858
|
+
...result.messages && result.messages.length > 0 ? { messages: result.messages } : {}
|
|
859
|
+
};
|
|
860
|
+
return createToolSuccessResponse(responseData);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
success = false;
|
|
863
|
+
errorMessage = error.message;
|
|
864
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
865
|
+
if (classified) return classified;
|
|
866
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
867
|
+
} finally {
|
|
868
|
+
trackToolRequest(
|
|
869
|
+
{
|
|
870
|
+
sourceId: effectiveSourceId,
|
|
871
|
+
toolName: resolveTrackedToolName(sourceId, "explain_sql"),
|
|
872
|
+
sql
|
|
873
|
+
},
|
|
874
|
+
startTime,
|
|
875
|
+
extra,
|
|
876
|
+
success,
|
|
877
|
+
errorMessage
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
790
882
|
|
|
791
|
-
// src/
|
|
792
|
-
|
|
793
|
-
|
|
883
|
+
// src/tools/health-check.ts
|
|
884
|
+
import { z as z4 } from "zod";
|
|
885
|
+
var healthCheckSchema = {};
|
|
886
|
+
var healthCheckInputSchema = z4.object(healthCheckSchema);
|
|
887
|
+
function createHealthCheckToolHandler(sourceId) {
|
|
888
|
+
return async (_args, extra) => {
|
|
889
|
+
const startTime = Date.now();
|
|
890
|
+
const effectiveSourceId = getEffectiveSourceId(sourceId);
|
|
891
|
+
let success = true;
|
|
892
|
+
let errorMessage;
|
|
893
|
+
try {
|
|
894
|
+
await ConnectorManager.ensureConnected(sourceId);
|
|
895
|
+
const connector = ConnectorManager.getCurrentConnector(sourceId);
|
|
896
|
+
if (!connector.getHealthCheck) {
|
|
897
|
+
success = false;
|
|
898
|
+
errorMessage = `health_check is not supported for '${connector.id}' sources yet`;
|
|
899
|
+
return createToolErrorResponse(errorMessage, "UNSUPPORTED");
|
|
900
|
+
}
|
|
901
|
+
const health = await connector.getHealthCheck();
|
|
902
|
+
return createToolSuccessResponse({
|
|
903
|
+
source_id: effectiveSourceId,
|
|
904
|
+
...health.connections ? {
|
|
905
|
+
connections: {
|
|
906
|
+
total: health.connections.total,
|
|
907
|
+
active: health.connections.active,
|
|
908
|
+
idle: health.connections.idle,
|
|
909
|
+
idle_in_transaction: health.connections.idleInTransaction,
|
|
910
|
+
...health.connections.idleInTransactionAborted !== void 0 ? { idle_in_transaction_aborted: health.connections.idleInTransactionAborted } : {},
|
|
911
|
+
max_connections: health.connections.maxConnections,
|
|
912
|
+
longest_idle_in_transaction_seconds: health.connections.longestIdleInTransactionSeconds,
|
|
913
|
+
longest_active_query_seconds: health.connections.longestActiveQuerySeconds
|
|
914
|
+
}
|
|
915
|
+
} : {},
|
|
916
|
+
...health.bufferCache ? {
|
|
917
|
+
buffer_cache: {
|
|
918
|
+
hit_ratio_pct: health.bufferCache.hitRatioPct,
|
|
919
|
+
blocks_hit: health.bufferCache.blocksHit,
|
|
920
|
+
blocks_read: health.bufferCache.blocksRead
|
|
921
|
+
}
|
|
922
|
+
} : {},
|
|
923
|
+
...health.notes && health.notes.length > 0 ? { notes: health.notes } : {}
|
|
924
|
+
});
|
|
925
|
+
} catch (error) {
|
|
926
|
+
success = false;
|
|
927
|
+
errorMessage = error.message;
|
|
928
|
+
const classified = tryClassifyConnectionError(error, sourceId, effectiveSourceId);
|
|
929
|
+
if (classified) return classified;
|
|
930
|
+
return createToolErrorResponse(errorMessage, "EXECUTION_ERROR");
|
|
931
|
+
} finally {
|
|
932
|
+
trackToolRequest(
|
|
933
|
+
{
|
|
934
|
+
sourceId: effectiveSourceId,
|
|
935
|
+
toolName: effectiveSourceId === "default" ? "health_check" : `health_check_${effectiveSourceId}`,
|
|
936
|
+
sql: ""
|
|
937
|
+
},
|
|
938
|
+
startTime,
|
|
939
|
+
extra,
|
|
940
|
+
success,
|
|
941
|
+
errorMessage
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
794
945
|
}
|
|
795
946
|
|
|
796
947
|
// src/utils/tool-metadata.ts
|
|
948
|
+
import { z as z5 } from "zod";
|
|
797
949
|
function buildSourceDescriptionPrefix(description) {
|
|
798
950
|
const trimmed = description?.trim() ?? "";
|
|
799
951
|
if (!trimmed) return "";
|
|
@@ -803,17 +955,17 @@ function zodToParameters(schema) {
|
|
|
803
955
|
const parameters = [];
|
|
804
956
|
for (const [key, zodType] of Object.entries(schema)) {
|
|
805
957
|
const description = zodType.description || "";
|
|
806
|
-
const required = !(zodType instanceof
|
|
958
|
+
const required = !(zodType instanceof z5.ZodOptional);
|
|
807
959
|
let type = "string";
|
|
808
|
-
if (zodType instanceof
|
|
960
|
+
if (zodType instanceof z5.ZodString) {
|
|
809
961
|
type = "string";
|
|
810
|
-
} else if (zodType instanceof
|
|
962
|
+
} else if (zodType instanceof z5.ZodNumber) {
|
|
811
963
|
type = "number";
|
|
812
|
-
} else if (zodType instanceof
|
|
964
|
+
} else if (zodType instanceof z5.ZodBoolean) {
|
|
813
965
|
type = "boolean";
|
|
814
|
-
} else if (zodType instanceof
|
|
966
|
+
} else if (zodType instanceof z5.ZodArray) {
|
|
815
967
|
type = "array";
|
|
816
|
-
} else if (zodType instanceof
|
|
968
|
+
} else if (zodType instanceof z5.ZodObject) {
|
|
817
969
|
type = "object";
|
|
818
970
|
}
|
|
819
971
|
parameters.push({
|
|
@@ -825,20 +977,28 @@ function zodToParameters(schema) {
|
|
|
825
977
|
}
|
|
826
978
|
return parameters;
|
|
827
979
|
}
|
|
828
|
-
function
|
|
980
|
+
function resolveBuiltinToolNaming(sourceId, baseName, titleLabel) {
|
|
829
981
|
const sourceIds = ConnectorManager.getAvailableSourceIds();
|
|
830
982
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
831
983
|
const dbType = sourceConfig.type;
|
|
832
984
|
const isSingleSource = sourceIds.length === 1;
|
|
985
|
+
const name = isSingleSource ? baseName : `${baseName}_${normalizeSourceId(sourceId)}`;
|
|
986
|
+
const title = isSingleSource ? `${titleLabel} (${dbType})` : `${titleLabel} on ${sourceId} (${dbType})`;
|
|
987
|
+
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
988
|
+
return { dbType, isSingleSource, name, title, userDescPrefix };
|
|
989
|
+
}
|
|
990
|
+
function getExecuteSqlMetadata(sourceId) {
|
|
991
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
992
|
+
sourceId,
|
|
993
|
+
"execute_sql",
|
|
994
|
+
"Execute SQL"
|
|
995
|
+
);
|
|
833
996
|
const registry = getToolRegistry();
|
|
834
997
|
const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, sourceId);
|
|
835
998
|
const executeOptions = {
|
|
836
999
|
readonly: toolConfig?.readonly,
|
|
837
1000
|
maxRows: toolConfig?.max_rows
|
|
838
1001
|
};
|
|
839
|
-
const toolName = isSingleSource ? "execute_sql" : `execute_sql_${normalizeSourceId(sourceId)}`;
|
|
840
|
-
const title = isSingleSource ? `Execute SQL (${dbType})` : `Execute SQL on ${sourceId} (${dbType})`;
|
|
841
|
-
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
842
1002
|
const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
|
|
843
1003
|
const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
|
|
844
1004
|
const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
|
|
@@ -855,25 +1015,62 @@ function getExecuteSqlMetadata(sourceId) {
|
|
|
855
1015
|
openWorldHint: false
|
|
856
1016
|
};
|
|
857
1017
|
return {
|
|
858
|
-
name
|
|
1018
|
+
name,
|
|
859
1019
|
description,
|
|
860
1020
|
schema: executeSqlSchema,
|
|
861
1021
|
annotations
|
|
862
1022
|
};
|
|
863
1023
|
}
|
|
864
1024
|
function getSearchObjectsMetadata(sourceId) {
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const title = isSingleSource ? `Search Database Objects (${dbType})` : `Search Database Objects on ${sourceId} (${dbType})`;
|
|
871
|
-
const userDescPrefix = buildSourceDescriptionPrefix(sourceConfig.description);
|
|
1025
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1026
|
+
sourceId,
|
|
1027
|
+
"search_objects",
|
|
1028
|
+
"Search Database Objects"
|
|
1029
|
+
);
|
|
872
1030
|
const description = isSingleSource ? `${userDescPrefix}Search and list database objects on the ${dbType} database` : `${userDescPrefix}Search and list database objects on the '${sourceId}' ${dbType} database`;
|
|
1031
|
+
return { name, description, title };
|
|
1032
|
+
}
|
|
1033
|
+
function getExplainSqlMetadata(sourceId) {
|
|
1034
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1035
|
+
sourceId,
|
|
1036
|
+
"explain_sql",
|
|
1037
|
+
"Explain Query Plan"
|
|
1038
|
+
);
|
|
1039
|
+
const description = isSingleSource ? `${userDescPrefix}Show the execution plan for a SQL statement on the ${dbType} database without running it. Always read-only and safe, independent of the source's read/write mode.` : `${userDescPrefix}Show the execution plan for a SQL statement on the '${sourceId}' ${dbType} database without running it. Always read-only and safe, independent of the source's read/write mode.`;
|
|
1040
|
+
const annotations = {
|
|
1041
|
+
title,
|
|
1042
|
+
readOnlyHint: true,
|
|
1043
|
+
destructiveHint: false,
|
|
1044
|
+
idempotentHint: true,
|
|
1045
|
+
openWorldHint: false
|
|
1046
|
+
};
|
|
1047
|
+
return {
|
|
1048
|
+
name,
|
|
1049
|
+
description,
|
|
1050
|
+
schema: explainSqlSchema,
|
|
1051
|
+
annotations
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function getHealthCheckMetadata(sourceId) {
|
|
1055
|
+
const { dbType, isSingleSource, name, title, userDescPrefix } = resolveBuiltinToolNaming(
|
|
1056
|
+
sourceId,
|
|
1057
|
+
"health_check",
|
|
1058
|
+
"Health Check"
|
|
1059
|
+
);
|
|
1060
|
+
const description = isSingleSource ? `${userDescPrefix}Report operational health metrics (connection pool state, buffer cache hit ratio) for the ${dbType} database. Read-only, independent of the source's read/write mode.` : `${userDescPrefix}Report operational health metrics (connection pool state, buffer cache hit ratio) for the '${sourceId}' ${dbType} database. Read-only, independent of the source's read/write mode.`;
|
|
1061
|
+
const annotations = {
|
|
1062
|
+
title,
|
|
1063
|
+
readOnlyHint: true,
|
|
1064
|
+
destructiveHint: false,
|
|
1065
|
+
idempotentHint: false,
|
|
1066
|
+
// Metrics change from call to call, unlike explain_sql's plan output
|
|
1067
|
+
openWorldHint: false
|
|
1068
|
+
};
|
|
873
1069
|
return {
|
|
874
|
-
name
|
|
1070
|
+
name,
|
|
875
1071
|
description,
|
|
876
|
-
|
|
1072
|
+
schema: healthCheckSchema,
|
|
1073
|
+
annotations
|
|
877
1074
|
};
|
|
878
1075
|
}
|
|
879
1076
|
function customParamsToToolParams(params) {
|
|
@@ -947,6 +1144,16 @@ function buildSearchObjectsTool(sourceId) {
|
|
|
947
1144
|
// search_objects is always readonly
|
|
948
1145
|
};
|
|
949
1146
|
}
|
|
1147
|
+
function buildExplainSqlTool(sourceId) {
|
|
1148
|
+
const explainMetadata = getExplainSqlMetadata(sourceId);
|
|
1149
|
+
return {
|
|
1150
|
+
name: explainMetadata.name,
|
|
1151
|
+
description: explainMetadata.description,
|
|
1152
|
+
parameters: zodToParameters(explainMetadata.schema),
|
|
1153
|
+
readonly: true
|
|
1154
|
+
// explain_sql is always readonly
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
950
1157
|
function buildCustomTool(toolConfig) {
|
|
951
1158
|
return {
|
|
952
1159
|
name: toolConfig.name,
|
|
@@ -965,6 +1172,8 @@ function getToolsForSource(sourceId) {
|
|
|
965
1172
|
return buildExecuteSqlTool(sourceId, toolConfig);
|
|
966
1173
|
} else if (toolConfig.name === "search_objects") {
|
|
967
1174
|
return buildSearchObjectsTool(sourceId);
|
|
1175
|
+
} else if (toolConfig.name === "explain_sql") {
|
|
1176
|
+
return buildExplainSqlTool(sourceId);
|
|
968
1177
|
} else {
|
|
969
1178
|
return buildCustomTool(toolConfig);
|
|
970
1179
|
}
|
|
@@ -972,7 +1181,7 @@ function getToolsForSource(sourceId) {
|
|
|
972
1181
|
}
|
|
973
1182
|
|
|
974
1183
|
// src/tools/custom-tool-handler.ts
|
|
975
|
-
import { z as
|
|
1184
|
+
import { z as z6 } from "zod";
|
|
976
1185
|
function buildZodSchemaFromParameters(parameters) {
|
|
977
1186
|
if (!parameters || parameters.length === 0) {
|
|
978
1187
|
return {};
|
|
@@ -982,26 +1191,26 @@ function buildZodSchemaFromParameters(parameters) {
|
|
|
982
1191
|
let fieldSchema;
|
|
983
1192
|
switch (param.type) {
|
|
984
1193
|
case "string":
|
|
985
|
-
fieldSchema =
|
|
1194
|
+
fieldSchema = z6.string().describe(param.description);
|
|
986
1195
|
break;
|
|
987
1196
|
case "integer":
|
|
988
|
-
fieldSchema =
|
|
1197
|
+
fieldSchema = z6.number().int().describe(param.description);
|
|
989
1198
|
break;
|
|
990
1199
|
case "float":
|
|
991
|
-
fieldSchema =
|
|
1200
|
+
fieldSchema = z6.number().describe(param.description);
|
|
992
1201
|
break;
|
|
993
1202
|
case "boolean":
|
|
994
|
-
fieldSchema =
|
|
1203
|
+
fieldSchema = z6.boolean().describe(param.description);
|
|
995
1204
|
break;
|
|
996
1205
|
case "array":
|
|
997
|
-
fieldSchema =
|
|
1206
|
+
fieldSchema = z6.array(z6.unknown()).describe(param.description);
|
|
998
1207
|
break;
|
|
999
1208
|
default:
|
|
1000
1209
|
throw new Error(`Unsupported parameter type: ${param.type}`);
|
|
1001
1210
|
}
|
|
1002
1211
|
if (param.allowed_values && param.allowed_values.length > 0) {
|
|
1003
1212
|
if (param.type === "string") {
|
|
1004
|
-
fieldSchema =
|
|
1213
|
+
fieldSchema = z6.enum(param.allowed_values).describe(param.description);
|
|
1005
1214
|
} else {
|
|
1006
1215
|
fieldSchema = fieldSchema.refine(
|
|
1007
1216
|
(val) => param.allowed_values.includes(val),
|
|
@@ -1022,7 +1231,7 @@ var customToolInputSchemas = /* @__PURE__ */ new WeakMap();
|
|
|
1022
1231
|
function getCustomToolInputSchema(toolConfig) {
|
|
1023
1232
|
let schema = customToolInputSchemas.get(toolConfig);
|
|
1024
1233
|
if (!schema) {
|
|
1025
|
-
schema =
|
|
1234
|
+
schema = z6.object(buildZodSchemaFromParameters(toolConfig.parameters));
|
|
1026
1235
|
customToolInputSchemas.set(toolConfig, schema);
|
|
1027
1236
|
}
|
|
1028
1237
|
return schema;
|
|
@@ -1068,7 +1277,7 @@ function createCustomToolHandler(toolConfig) {
|
|
|
1068
1277
|
errorMessage = error.message;
|
|
1069
1278
|
const classified = tryClassifyConnectionError(error, toolConfig.source, toolConfig.source);
|
|
1070
1279
|
if (classified) return classified;
|
|
1071
|
-
if (error instanceof
|
|
1280
|
+
if (error instanceof z6.ZodError) {
|
|
1072
1281
|
const issues = error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
1073
1282
|
errorMessage = `Parameter validation failed: ${issues}`;
|
|
1074
1283
|
} else {
|
|
@@ -1108,6 +1317,10 @@ function registerTools(server) {
|
|
|
1108
1317
|
registerExecuteSqlTool(server, sourceId);
|
|
1109
1318
|
} else if (toolConfig.name === BUILTIN_TOOL_SEARCH_OBJECTS) {
|
|
1110
1319
|
registerSearchObjectsTool(server, sourceId);
|
|
1320
|
+
} else if (toolConfig.name === BUILTIN_TOOL_EXPLAIN_SQL) {
|
|
1321
|
+
registerExplainSqlTool(server, sourceId);
|
|
1322
|
+
} else if (toolConfig.name === BUILTIN_TOOL_HEALTH_CHECK) {
|
|
1323
|
+
registerHealthCheckTool(server, sourceId);
|
|
1111
1324
|
} else {
|
|
1112
1325
|
registerCustomTool(server, sourceId, toolConfig);
|
|
1113
1326
|
}
|
|
@@ -1144,6 +1357,30 @@ function registerSearchObjectsTool(server, sourceId) {
|
|
|
1144
1357
|
createSearchDatabaseObjectsToolHandler(sourceId)
|
|
1145
1358
|
);
|
|
1146
1359
|
}
|
|
1360
|
+
function registerExplainSqlTool(server, sourceId) {
|
|
1361
|
+
const metadata = getExplainSqlMetadata(sourceId);
|
|
1362
|
+
server.registerTool(
|
|
1363
|
+
metadata.name,
|
|
1364
|
+
{
|
|
1365
|
+
description: metadata.description,
|
|
1366
|
+
inputSchema: explainSqlInputSchema,
|
|
1367
|
+
annotations: metadata.annotations
|
|
1368
|
+
},
|
|
1369
|
+
createExplainSqlToolHandler(sourceId)
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
function registerHealthCheckTool(server, sourceId) {
|
|
1373
|
+
const metadata = getHealthCheckMetadata(sourceId);
|
|
1374
|
+
server.registerTool(
|
|
1375
|
+
metadata.name,
|
|
1376
|
+
{
|
|
1377
|
+
description: metadata.description,
|
|
1378
|
+
inputSchema: healthCheckInputSchema,
|
|
1379
|
+
annotations: metadata.annotations
|
|
1380
|
+
},
|
|
1381
|
+
createHealthCheckToolHandler(sourceId)
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1147
1384
|
function registerCustomTool(server, sourceId, toolConfig) {
|
|
1148
1385
|
const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
|
|
1149
1386
|
const dbType = sourceConfig.type;
|
|
@@ -1639,7 +1876,7 @@ See documentation for more details on configuring database connections.
|
|
|
1639
1876
|
const sources = sourceConfigsData.sources;
|
|
1640
1877
|
console.error(`Configuration source: ${sourceConfigsData.source}`);
|
|
1641
1878
|
await connectorManager.connectWithSources(sources);
|
|
1642
|
-
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-
|
|
1879
|
+
const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-JYRRNC3E.js");
|
|
1643
1880
|
initializeToolRegistry2({
|
|
1644
1881
|
sources: sourceConfigsData.sources,
|
|
1645
1882
|
tools: sourceConfigsData.tools
|
|
@@ -1780,11 +2017,11 @@ See documentation for more details on configuring database connections.
|
|
|
1780
2017
|
|
|
1781
2018
|
// src/index.ts
|
|
1782
2019
|
var connectorModules = [
|
|
1783
|
-
{ load: () => import("./postgres-
|
|
1784
|
-
{ load: () => import("./sqlserver-
|
|
2020
|
+
{ load: () => import("./postgres-5ZOFYV6X.js"), name: "PostgreSQL", driver: "pg" },
|
|
2021
|
+
{ load: () => import("./sqlserver-PSAZRH7G.js"), name: "SQL Server", driver: "mssql" },
|
|
1785
2022
|
{ load: () => import("./sqlite-5M7SDAQZ.js"), name: "SQLite", driver: "node:sqlite" },
|
|
1786
|
-
{ load: () => import("./mysql-
|
|
1787
|
-
{ load: () => import("./mariadb-
|
|
2023
|
+
{ load: () => import("./mysql-265M3KBD.js"), name: "MySQL", driver: "mysql2" },
|
|
2024
|
+
{ load: () => import("./mariadb-DUDFBY5W.js"), name: "MariaDB", driver: "mariadb" }
|
|
1788
2025
|
];
|
|
1789
2026
|
loadConnectors(connectorModules).then(() => main()).catch((error) => {
|
|
1790
2027
|
console.error("Fatal error:", error);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MissingDatabaseError,
|
|
3
3
|
extractAffectedRows,
|
|
4
|
+
getMySQLFamilyHealthCheck,
|
|
4
5
|
isTiDBVersion,
|
|
5
6
|
parseQueryResults,
|
|
6
7
|
requireDatabaseInDSN,
|
|
7
8
|
withReadOnlyTransaction
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-NIILHXTG.js";
|
|
10
|
+
import "./chunk-FU2ZJE4E.js";
|
|
9
11
|
import {
|
|
10
12
|
quoteIdentifier
|
|
11
13
|
} from "./chunk-JFWX35TB.js";
|
|
@@ -338,6 +340,13 @@ var MariaDBConnector = class _MariaDBConnector {
|
|
|
338
340
|
return null;
|
|
339
341
|
}
|
|
340
342
|
}
|
|
343
|
+
async getHealthCheck() {
|
|
344
|
+
if (!this.pool) {
|
|
345
|
+
throw new Error("Not connected to database");
|
|
346
|
+
}
|
|
347
|
+
const pool = this.pool;
|
|
348
|
+
return getMySQLFamilyHealthCheck((sql) => pool.query(sql));
|
|
349
|
+
}
|
|
341
350
|
async getStoredProcedures(schema, routineType) {
|
|
342
351
|
if (!this.pool) {
|
|
343
352
|
throw new Error("Not connected to database");
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MissingDatabaseError,
|
|
3
3
|
extractAffectedRows,
|
|
4
|
+
getMySQLFamilyHealthCheck,
|
|
4
5
|
isTiDBVersion,
|
|
5
6
|
parseQueryResults,
|
|
6
7
|
requireDatabaseInDSN,
|
|
7
8
|
withReadOnlyTransaction
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-NIILHXTG.js";
|
|
10
|
+
import "./chunk-FU2ZJE4E.js";
|
|
9
11
|
import {
|
|
10
12
|
quoteIdentifier
|
|
11
13
|
} from "./chunk-JFWX35TB.js";
|
|
@@ -343,6 +345,16 @@ var MySQLConnector = class _MySQLConnector {
|
|
|
343
345
|
return null;
|
|
344
346
|
}
|
|
345
347
|
}
|
|
348
|
+
async getHealthCheck() {
|
|
349
|
+
if (!this.pool) {
|
|
350
|
+
throw new Error("Not connected to database");
|
|
351
|
+
}
|
|
352
|
+
const pool = this.pool;
|
|
353
|
+
return getMySQLFamilyHealthCheck(async (sql) => {
|
|
354
|
+
const [rows] = await pool.query(sql);
|
|
355
|
+
return rows;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
346
358
|
async getStoredProcedures(schema, routineType) {
|
|
347
359
|
if (!this.pool) {
|
|
348
360
|
throw new Error("Not connected to database");
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
toNullableNumber
|
|
3
|
+
} from "./chunk-FU2ZJE4E.js";
|
|
1
4
|
import {
|
|
2
5
|
quoteIdentifier
|
|
3
6
|
} from "./chunk-JFWX35TB.js";
|
|
@@ -383,6 +386,64 @@ var PostgresConnector = class _PostgresConnector {
|
|
|
383
386
|
client.release();
|
|
384
387
|
}
|
|
385
388
|
}
|
|
389
|
+
async getHealthCheck() {
|
|
390
|
+
if (!this.pool) {
|
|
391
|
+
throw new Error("Not connected to database");
|
|
392
|
+
}
|
|
393
|
+
const client = await this.pool.connect();
|
|
394
|
+
try {
|
|
395
|
+
const [connectionsResult, maxConnectionsResult, bufferCacheResult] = await Promise.all([
|
|
396
|
+
client.query(`
|
|
397
|
+
SELECT
|
|
398
|
+
count(*) AS total,
|
|
399
|
+
count(*) FILTER (WHERE state = 'active') AS active,
|
|
400
|
+
count(*) FILTER (WHERE state = 'idle') AS idle,
|
|
401
|
+
count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_transaction,
|
|
402
|
+
count(*) FILTER (WHERE state = 'idle in transaction (aborted)') AS idle_in_transaction_aborted,
|
|
403
|
+
max(EXTRACT(EPOCH FROM (now() - state_change)))
|
|
404
|
+
FILTER (WHERE state = 'idle in transaction') AS longest_idle_in_transaction_seconds,
|
|
405
|
+
max(EXTRACT(EPOCH FROM (now() - query_start)))
|
|
406
|
+
FILTER (WHERE state = 'active') AS longest_active_query_seconds
|
|
407
|
+
FROM pg_stat_activity
|
|
408
|
+
WHERE pid <> pg_backend_pid()
|
|
409
|
+
`),
|
|
410
|
+
client.query(`SHOW max_connections`),
|
|
411
|
+
client.query(
|
|
412
|
+
`
|
|
413
|
+
SELECT
|
|
414
|
+
blks_hit,
|
|
415
|
+
blks_read,
|
|
416
|
+
CASE WHEN (blks_hit + blks_read) = 0 THEN NULL
|
|
417
|
+
ELSE round(100.0 * blks_hit / (blks_hit + blks_read), 2)
|
|
418
|
+
END AS hit_ratio_pct
|
|
419
|
+
FROM pg_stat_database
|
|
420
|
+
WHERE datname = current_database()
|
|
421
|
+
`
|
|
422
|
+
)
|
|
423
|
+
]);
|
|
424
|
+
const conn = connectionsResult.rows[0];
|
|
425
|
+
const cache = bufferCacheResult.rows[0];
|
|
426
|
+
return {
|
|
427
|
+
connections: {
|
|
428
|
+
total: Number(conn.total),
|
|
429
|
+
active: Number(conn.active),
|
|
430
|
+
idle: Number(conn.idle),
|
|
431
|
+
idleInTransaction: Number(conn.idle_in_transaction),
|
|
432
|
+
idleInTransactionAborted: Number(conn.idle_in_transaction_aborted),
|
|
433
|
+
maxConnections: Number(maxConnectionsResult.rows[0].max_connections),
|
|
434
|
+
longestIdleInTransactionSeconds: toNullableNumber(conn.longest_idle_in_transaction_seconds),
|
|
435
|
+
longestActiveQuerySeconds: toNullableNumber(conn.longest_active_query_seconds)
|
|
436
|
+
},
|
|
437
|
+
bufferCache: {
|
|
438
|
+
hitRatioPct: toNullableNumber(cache.hit_ratio_pct),
|
|
439
|
+
blocksHit: Number(cache.blks_hit),
|
|
440
|
+
blocksRead: Number(cache.blks_read)
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
} finally {
|
|
444
|
+
client.release();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
386
447
|
async getStoredProcedures(schema, routineType) {
|
|
387
448
|
if (!this.pool) {
|
|
388
449
|
throw new Error("Not connected to database");
|
|
@@ -3,10 +3,14 @@ import {
|
|
|
3
3
|
sqlServerDynamicSqlPattern,
|
|
4
4
|
sqlServerPassThroughKeywords,
|
|
5
5
|
sqlServerPassThroughPattern
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-EPOKWSFC.js";
|
|
7
7
|
import {
|
|
8
8
|
isDriverNotInstalled
|
|
9
9
|
} from "./chunk-WVVMH6FJ.js";
|
|
10
|
+
import {
|
|
11
|
+
computeHitRatioPct,
|
|
12
|
+
toNullableNumber
|
|
13
|
+
} from "./chunk-FU2ZJE4E.js";
|
|
10
14
|
import {
|
|
11
15
|
SQLRowLimiter,
|
|
12
16
|
closeQuietly
|
|
@@ -372,6 +376,81 @@ var _SQLServerConnector = class _SQLServerConnector {
|
|
|
372
376
|
return null;
|
|
373
377
|
}
|
|
374
378
|
}
|
|
379
|
+
async getHealthCheck() {
|
|
380
|
+
if (!this.connection) {
|
|
381
|
+
throw new Error("Not connected to SQL Server database");
|
|
382
|
+
}
|
|
383
|
+
const notes = [];
|
|
384
|
+
const result = {};
|
|
385
|
+
try {
|
|
386
|
+
const [connResult, maxConnResult] = await Promise.all([
|
|
387
|
+
this.connection.request().query(`
|
|
388
|
+
SELECT
|
|
389
|
+
COUNT(*) AS total,
|
|
390
|
+
SUM(CASE WHEN r.session_id IS NOT NULL THEN 1 ELSE 0 END) AS active,
|
|
391
|
+
SUM(CASE WHEN r.session_id IS NULL THEN 1 ELSE 0 END) AS idle,
|
|
392
|
+
SUM(CASE WHEN r.session_id IS NULL AND t.session_id IS NOT NULL THEN 1 ELSE 0 END) AS idle_in_transaction,
|
|
393
|
+
MAX(CASE WHEN r.session_id IS NULL AND t.session_id IS NOT NULL
|
|
394
|
+
THEN DATEDIFF(SECOND, s.last_request_end_time, SYSDATETIME()) ELSE NULL END) AS longest_idle_in_transaction_seconds,
|
|
395
|
+
MAX(CASE WHEN r.session_id IS NOT NULL
|
|
396
|
+
THEN DATEDIFF(SECOND, r.start_time, SYSDATETIME()) ELSE NULL END) AS longest_active_query_seconds
|
|
397
|
+
FROM sys.dm_exec_sessions s
|
|
398
|
+
LEFT JOIN sys.dm_exec_requests r ON r.session_id = s.session_id
|
|
399
|
+
LEFT JOIN (SELECT DISTINCT session_id FROM sys.dm_tran_session_transactions) t ON t.session_id = s.session_id
|
|
400
|
+
WHERE s.is_user_process = 1 AND s.session_id <> @@SPID
|
|
401
|
+
`),
|
|
402
|
+
this.connection.request().query(`
|
|
403
|
+
SELECT CAST(value_in_use AS INT) AS max_connections
|
|
404
|
+
FROM sys.configurations
|
|
405
|
+
WHERE name = 'user connections'
|
|
406
|
+
`)
|
|
407
|
+
]);
|
|
408
|
+
const conn = connResult.recordset[0];
|
|
409
|
+
const maxConnections = maxConnResult.recordset.length > 0 && maxConnResult.recordset[0].max_connections > 0 ? maxConnResult.recordset[0].max_connections : null;
|
|
410
|
+
result.connections = {
|
|
411
|
+
total: conn.total ?? 0,
|
|
412
|
+
active: conn.active ?? 0,
|
|
413
|
+
idle: conn.idle ?? 0,
|
|
414
|
+
idleInTransaction: conn.idle_in_transaction ?? 0,
|
|
415
|
+
// SQL Server has no equivalent of Postgres's "idle in transaction
|
|
416
|
+
// (aborted)" state - a failed statement doesn't leave the session's
|
|
417
|
+
// transaction in a distinct aborted-but-open state the way Postgres does.
|
|
418
|
+
maxConnections,
|
|
419
|
+
longestIdleInTransactionSeconds: toNullableNumber(conn.longest_idle_in_transaction_seconds),
|
|
420
|
+
longestActiveQuerySeconds: toNullableNumber(conn.longest_active_query_seconds)
|
|
421
|
+
};
|
|
422
|
+
} catch {
|
|
423
|
+
notes.push(
|
|
424
|
+
"Connection pool metrics unavailable: connecting user lacks the VIEW SERVER STATE permission (VIEW DATABASE STATE on Azure SQL Database)."
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const bufferResult = await this.connection.request().query(`
|
|
429
|
+
SELECT counter_name, CAST(cntr_value AS BIGINT) AS cntr_value
|
|
430
|
+
FROM sys.dm_os_performance_counters
|
|
431
|
+
WHERE object_name LIKE '%Buffer Manager%'
|
|
432
|
+
AND counter_name IN ('Page lookups/sec', 'Page reads/sec')
|
|
433
|
+
`);
|
|
434
|
+
const counters = Object.fromEntries(
|
|
435
|
+
bufferResult.recordset.map((row) => [row.counter_name.trim(), Number(row.cntr_value)])
|
|
436
|
+
);
|
|
437
|
+
const pageLookups = counters["Page lookups/sec"] ?? 0;
|
|
438
|
+
const pageReads = counters["Page reads/sec"] ?? 0;
|
|
439
|
+
result.bufferCache = {
|
|
440
|
+
hitRatioPct: computeHitRatioPct(pageLookups, pageReads),
|
|
441
|
+
blocksHit: pageLookups - pageReads,
|
|
442
|
+
blocksRead: pageReads
|
|
443
|
+
};
|
|
444
|
+
} catch {
|
|
445
|
+
notes.push(
|
|
446
|
+
"Buffer cache metrics unavailable: connecting user lacks the VIEW SERVER STATE permission (VIEW DATABASE STATE on Azure SQL Database)."
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (notes.length > 0) {
|
|
450
|
+
result.notes = notes;
|
|
451
|
+
}
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
375
454
|
async getStoredProcedures(schema, routineType) {
|
|
376
455
|
if (!this.connection) {
|
|
377
456
|
throw new Error("Not connected to SQL Server database");
|
package/package.json
CHANGED
package/dist/chunk-DUCNPMNW.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
obfuscateDSNPassword
|
|
3
|
-
} from "./chunk-JEZZN2YZ.js";
|
|
4
|
-
|
|
5
|
-
// src/utils/dsn-database.ts
|
|
6
|
-
var MissingDatabaseError = class extends Error {
|
|
7
|
-
constructor(message) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = "MissingDatabaseError";
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
function requireDatabaseInDSN(database, dsn, label) {
|
|
13
|
-
if (database) {
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
throw new MissingDatabaseError(
|
|
17
|
-
`${label} DSN must name a database.
|
|
18
|
-
Provided: ${obfuscateDSNPassword(dsn)}
|
|
19
|
-
Add the database to the DSN, e.g. ...:3306/mydb
|
|
20
|
-
To work with several databases, define one [[sources]] entry per database in a TOML config file: https://dbhub.ai/config/toml`
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// src/utils/multi-statement-result-parser.ts
|
|
25
|
-
function isMetadataObject(element) {
|
|
26
|
-
if (!element || typeof element !== "object" || Array.isArray(element)) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
return "affectedRows" in element || "insertId" in element || "fieldCount" in element || "warningStatus" in element;
|
|
30
|
-
}
|
|
31
|
-
function isMultiStatementResult(results) {
|
|
32
|
-
if (!Array.isArray(results) || results.length === 0) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
const firstElement = results[0];
|
|
36
|
-
return isMetadataObject(firstElement) || Array.isArray(firstElement);
|
|
37
|
-
}
|
|
38
|
-
function extractRowsFromMultiStatement(results) {
|
|
39
|
-
if (!Array.isArray(results)) {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
const allRows = [];
|
|
43
|
-
for (const result of results) {
|
|
44
|
-
if (Array.isArray(result)) {
|
|
45
|
-
allRows.push(...result);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return allRows;
|
|
49
|
-
}
|
|
50
|
-
function extractAffectedRows(results) {
|
|
51
|
-
if (isMetadataObject(results)) {
|
|
52
|
-
return results.affectedRows || 0;
|
|
53
|
-
}
|
|
54
|
-
if (!Array.isArray(results)) {
|
|
55
|
-
return 0;
|
|
56
|
-
}
|
|
57
|
-
if (isMultiStatementResult(results)) {
|
|
58
|
-
let totalAffected = 0;
|
|
59
|
-
for (const result of results) {
|
|
60
|
-
if (isMetadataObject(result)) {
|
|
61
|
-
totalAffected += result.affectedRows || 0;
|
|
62
|
-
} else if (Array.isArray(result)) {
|
|
63
|
-
totalAffected += result.length;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return totalAffected;
|
|
67
|
-
}
|
|
68
|
-
return results.length;
|
|
69
|
-
}
|
|
70
|
-
function parseQueryResults(results) {
|
|
71
|
-
if (!Array.isArray(results)) {
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
if (isMultiStatementResult(results)) {
|
|
75
|
-
return extractRowsFromMultiStatement(results);
|
|
76
|
-
}
|
|
77
|
-
return results;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// src/utils/readonly-transaction.ts
|
|
81
|
-
async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransaction, execute) {
|
|
82
|
-
if (!readonly) {
|
|
83
|
-
return execute();
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
await conn.query(
|
|
87
|
-
supportsReadOnlyTransaction ? "START TRANSACTION READ ONLY" : "START TRANSACTION"
|
|
88
|
-
);
|
|
89
|
-
const result = await execute();
|
|
90
|
-
await conn.query(supportsReadOnlyTransaction ? "COMMIT" : "ROLLBACK");
|
|
91
|
-
return result;
|
|
92
|
-
} catch (error) {
|
|
93
|
-
try {
|
|
94
|
-
await conn.query("ROLLBACK");
|
|
95
|
-
} catch {
|
|
96
|
-
}
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// src/utils/server-flavor.ts
|
|
102
|
-
function isTiDBVersion(version) {
|
|
103
|
-
return typeof version === "string" && /tidb/i.test(version);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export {
|
|
107
|
-
MissingDatabaseError,
|
|
108
|
-
requireDatabaseInDSN,
|
|
109
|
-
extractAffectedRows,
|
|
110
|
-
parseQueryResults,
|
|
111
|
-
withReadOnlyTransaction,
|
|
112
|
-
isTiDBVersion
|
|
113
|
-
};
|