@bytebase/dbhub 0.24.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 -44
- 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/{chunk-D2BIPC7V.js → chunk-S2V6RZ3C.js} +4 -2
- package/dist/index.js +377 -97
- package/dist/{mariadb-VZMU7LYA.js → mariadb-DUDFBY5W.js} +12 -13
- package/dist/{mysql-MHIZK2UY.js → mysql-265M3KBD.js} +15 -13
- package/dist/{postgres-ZQFXCBYA.js → postgres-5ZOFYV6X.js} +63 -14
- package/dist/{registry-ZU6ZFNQ2.js → registry-JYRRNC3E.js} +1 -1
- package/dist/{sqlite-JH5CNV5E.js → sqlite-5M7SDAQZ.js} +4 -18
- package/dist/{sqlserver-LHWZ7WOP.js → sqlserver-PSAZRH7G.js} +307 -65
- package/package.json +5 -3
- 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,55 +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
|
-
**Demo Mode:**
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
npx @bytebase/dbhub@latest --transport http --port 8080 --demo
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
**Restrict to loopback (recommended for production):**
|
|
94
|
-
|
|
95
|
-
```bash
|
|
96
|
-
npx @bytebase/dbhub@latest --transport http --host 127.0.0.1 --port 8080 --demo
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
> 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.
|
|
100
|
-
>
|
|
101
|
-
> 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`.
|
|
102
|
-
|
|
103
|
-
See [Command-Line Options](https://dbhub.ai/config/command-line) for all available parameters.
|
|
104
|
-
|
|
105
|
-
### Multi-Database Setup
|
|
67
|
+
Also available as:
|
|
106
68
|
|
|
107
|
-
|
|
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)
|
|
108
72
|
|
|
109
|
-
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.
|
|
110
74
|
|
|
111
75
|
## Development
|
|
112
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
|
+
};
|
|
@@ -66,7 +66,8 @@ var SQLRowLimiter = class {
|
|
|
66
66
|
const trimmed = sql.trim();
|
|
67
67
|
const hasSemicolon = trimmed.endsWith(";");
|
|
68
68
|
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
69
|
-
return `${sqlWithoutSemicolon}
|
|
69
|
+
return `${sqlWithoutSemicolon}
|
|
70
|
+
LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
@@ -107,7 +108,8 @@ var SQLRowLimiter = class {
|
|
|
107
108
|
const trimmed = sql.trim();
|
|
108
109
|
const hasSemicolon = trimmed.endsWith(";");
|
|
109
110
|
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
110
|
-
return `SELECT * FROM (${sqlWithoutSemicolon}
|
|
111
|
+
return `SELECT * FROM (${sqlWithoutSemicolon}
|
|
112
|
+
) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
111
113
|
}
|
|
112
114
|
return this.applyLimitToQuery(sql, maxRows);
|
|
113
115
|
}
|