@bytebase/dbhub 1.1.0 → 1.2.1
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 +33 -4
- package/dist/{chunk-GSSLHJB6.js → chunk-2DFLBX4D.js} +38 -4
- package/dist/chunk-6QETXS44.js +254 -0
- package/dist/{chunk-NIILHXTG.js → chunk-A4TSDCPY.js} +26 -37
- package/dist/{chunk-EPOKWSFC.js → chunk-IQRBLAB4.js} +1 -1
- package/dist/{chunk-JEZZN2YZ.js → chunk-MF6LSVWK.js} +19 -1
- package/dist/index.js +77 -26
- package/dist/{mariadb-DUDFBY5W.js → mariadb-DQ6P57IX.js} +17 -12
- package/dist/{mysql-265M3KBD.js → mysql-GXDUNLJ6.js} +66 -14
- package/dist/{postgres-5ZOFYV6X.js → postgres-2FHS64EO.js} +33 -15
- package/dist/public/assets/{index-DBYlgGks.js → index-DOZ5HOZ9.js} +21 -20
- package/dist/public/index.html +1 -1
- package/dist/{registry-JYRRNC3E.js → registry-D2QVU6AG.js} +2 -2
- package/dist/{sqlite-5M7SDAQZ.js → sqlite-ASYMU5BI.js} +28 -19
- package/dist/{sqlserver-PSAZRH7G.js → sqlserver-WNVSBFJ3.js} +90 -14
- package/package.json +5 -2
- package/dist/chunk-S2V6RZ3C.js +0 -138
package/README.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
> [!NOTE]
|
|
2
|
-
> Brought to you by [Bytebase](https://www.bytebase.com/), open-source database
|
|
2
|
+
> Brought to you by [Bytebase](https://www.bytebase.com/), open-source database governance platform.
|
|
3
|
+
|
|
4
|
+
<p align="center">
|
|
5
|
+
<a href="https://www.star-history.com/bytebase/dbhub">
|
|
6
|
+
<picture>
|
|
7
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=bytebase/dbhub&type=trending&theme=dark" />
|
|
8
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=bytebase/dbhub&type=trending" />
|
|
9
|
+
<img alt="GitHub Trending Repository of the Day" src="https://api.star-history.com/badge?repo=bytebase/dbhub&type=trending" />
|
|
10
|
+
</picture>
|
|
11
|
+
</a>
|
|
12
|
+
</p>
|
|
3
13
|
|
|
4
14
|
<p align="center">
|
|
5
15
|
<a href="https://dbhub.ai/" target="_blank">
|
|
@@ -30,14 +40,33 @@
|
|
|
30
40
|
MCP Clients MCP Server Databases
|
|
31
41
|
```
|
|
32
42
|
|
|
33
|
-
DBHub is a zero-dependency,
|
|
43
|
+
DBHub is a minimal MCP server: token-efficient, zero-dependency, and just two tools by default with opt-in extras. This lightweight gateway allows MCP-compatible clients to connect to and explore different databases:
|
|
34
44
|
|
|
35
|
-
- **
|
|
45
|
+
- **Minimal**: Zero dependency, token efficient with a minimal set of MCP tools to maximize context window
|
|
36
46
|
- **Multi-Database**: PostgreSQL, MySQL, MariaDB, SQL Server, and SQLite through a single interface
|
|
37
47
|
- **Multi-Connection**: Connect to multiple databases simultaneously with TOML configuration
|
|
38
48
|
- **Guardrails**: Read-only mode, row limiting, and query timeout to prevent runaway operations
|
|
39
49
|
- **Secure Access**: SSH tunneling and SSL/TLS encryption
|
|
40
50
|
|
|
51
|
+
> DBHub is the official example in the [Claude Code docs](https://code.claude.com/docs/en/mcp#example-query-your-postgresql-database) for connecting to PostgreSQL via MCP.
|
|
52
|
+
|
|
53
|
+
## Token Efficiency
|
|
54
|
+
|
|
55
|
+
DBHub loads just 2 tools by default at **1.4k tokens** — 13-14x fewer than alternatives — keeping the context window open for your actual work.
|
|
56
|
+
|
|
57
|
+
| MCP Server | Default Config | Default Tools |
|
|
58
|
+
|------------|---------------|--------------|
|
|
59
|
+
| **DBHub** | **1.4k** | 2 (`execute_sql`, `search_objects`) |
|
|
60
|
+
| MCP Toolbox | 19.0k | 28 |
|
|
61
|
+
| Supabase MCP | 19.3k | all |
|
|
62
|
+
|
|
63
|
+
## Use Cases
|
|
64
|
+
|
|
65
|
+
- **Local Development**: Schema exploration, query validation, and data debugging with Claude Code, VS Code, Cursor, etc.
|
|
66
|
+
- **Non-Technical Access**: Expose curated, read-only views to non-technical staff via Claude Desktop, VS Code, Cursor, etc.
|
|
67
|
+
- **Multi-Database Consolidation**: Replace separate MCP servers for each database with a single DBHub process
|
|
68
|
+
- **Production Troubleshooting**: Read-only diagnostics with guardrails against runaway queries
|
|
69
|
+
|
|
41
70
|
## Supported Databases
|
|
42
71
|
|
|
43
72
|
PostgreSQL, MySQL, SQL Server, MariaDB, and SQLite.
|
|
@@ -49,7 +78,7 @@ DBHub implements MCP tools for database operations:
|
|
|
49
78
|
- **[execute_sql](https://dbhub.ai/tools/execute-sql)**: Execute SQL queries with transaction support and safety controls
|
|
50
79
|
- **[search_objects](https://dbhub.ai/tools/search-objects)**: Search and explore database schemas, tables, columns, indexes, and procedures with progressive disclosure
|
|
51
80
|
- **[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
|
|
81
|
+
- **[health_check](https://dbhub.ai/tools/health-check)** (opt-in): Report connection pool state and buffer cache hit ratio
|
|
53
82
|
- **[Custom Tools](https://dbhub.ai/tools/custom-tools)**: Define reusable, parameterized SQL operations in your `dbhub.toml` configuration file
|
|
54
83
|
|
|
55
84
|
## Workbench
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
getDefaultPortForType,
|
|
9
9
|
parseConnectionInfoFromDSN,
|
|
10
10
|
stripCommentsAndStrings
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-MF6LSVWK.js";
|
|
12
12
|
|
|
13
13
|
// src/tools/builtin-tools.ts
|
|
14
14
|
var BUILTIN_TOOL_EXECUTE_SQL = "execute_sql";
|
|
@@ -805,6 +805,21 @@ function resolveAllowedHosts() {
|
|
|
805
805
|
function splitHostList(value) {
|
|
806
806
|
return value.split(",").map((h) => h.trim()).filter((h) => h.length > 0);
|
|
807
807
|
}
|
|
808
|
+
function resolveAuthTokens() {
|
|
809
|
+
const args = parseCommandLineArgs();
|
|
810
|
+
const cliValue = requireFlagValue("auth-token", args, "secret1,secret2");
|
|
811
|
+
if (cliValue !== void 0) {
|
|
812
|
+
return { tokens: splitTokenList(cliValue), source: "command line argument" };
|
|
813
|
+
}
|
|
814
|
+
const envValue = process.env.DBHUB_AUTH_TOKEN?.trim();
|
|
815
|
+
if (envValue) {
|
|
816
|
+
return { tokens: splitTokenList(envValue), source: "environment variable" };
|
|
817
|
+
}
|
|
818
|
+
return { tokens: [], source: "default" };
|
|
819
|
+
}
|
|
820
|
+
function splitTokenList(value) {
|
|
821
|
+
return value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
|
|
822
|
+
}
|
|
808
823
|
function redactDSN(dsn) {
|
|
809
824
|
try {
|
|
810
825
|
const url = new URL(dsn);
|
|
@@ -1292,6 +1307,18 @@ function validateSourceConfig(source, configPath) {
|
|
|
1292
1307
|
);
|
|
1293
1308
|
}
|
|
1294
1309
|
}
|
|
1310
|
+
if (source.aws_profile !== void 0) {
|
|
1311
|
+
if (typeof source.aws_profile !== "string" || source.aws_profile.trim().length === 0) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`Configuration file ${configPath}: source '${source.id}' has invalid aws_profile. Must be a non-empty string.`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
if (source.aws_iam_auth !== true) {
|
|
1317
|
+
throw new Error(
|
|
1318
|
+
`Configuration file ${configPath}: source '${source.id}' aws_profile requires aws_iam_auth = true.`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1295
1322
|
if (source.aws_iam_auth === true) {
|
|
1296
1323
|
const validIamTypes = ["postgres", "mysql", "mariadb"];
|
|
1297
1324
|
if (!source.type || !validIamTypes.includes(source.type)) {
|
|
@@ -1662,12 +1689,17 @@ async function generateRdsAuthToken(params) {
|
|
|
1662
1689
|
}
|
|
1663
1690
|
throw error;
|
|
1664
1691
|
}
|
|
1665
|
-
const
|
|
1692
|
+
const signerConfig = {
|
|
1666
1693
|
hostname: params.hostname,
|
|
1667
1694
|
port: params.port,
|
|
1668
1695
|
username: params.username,
|
|
1669
1696
|
region: params.region
|
|
1670
|
-
}
|
|
1697
|
+
};
|
|
1698
|
+
if (params.profile) {
|
|
1699
|
+
const { fromIni } = await import("@aws-sdk/credential-providers");
|
|
1700
|
+
signerConfig.credentials = fromIni({ profile: params.profile });
|
|
1701
|
+
}
|
|
1702
|
+
const signer = new Signer(signerConfig);
|
|
1671
1703
|
return signer.getAuthToken();
|
|
1672
1704
|
}
|
|
1673
1705
|
|
|
@@ -2143,7 +2175,8 @@ var ConnectorManager = class {
|
|
|
2143
2175
|
hostname,
|
|
2144
2176
|
port,
|
|
2145
2177
|
username,
|
|
2146
|
-
region: source.aws_region
|
|
2178
|
+
region: source.aws_region,
|
|
2179
|
+
profile: source.aws_profile
|
|
2147
2180
|
});
|
|
2148
2181
|
const queryParams = new Map(parsed.searchParams);
|
|
2149
2182
|
const currentSslMode = queryParams.get("sslmode");
|
|
@@ -2492,6 +2525,7 @@ export {
|
|
|
2492
2525
|
resolvePort,
|
|
2493
2526
|
resolveHost,
|
|
2494
2527
|
resolveAllowedHosts,
|
|
2528
|
+
resolveAuthTokens,
|
|
2495
2529
|
resolveSourceConfigs,
|
|
2496
2530
|
BUILTIN_TOOL_EXECUTE_SQL,
|
|
2497
2531
|
BUILTIN_TOOL_SEARCH_OBJECTS,
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
blankCommentsAndStrings,
|
|
3
|
+
stripCommentsAndStrings
|
|
4
|
+
} from "./chunk-MF6LSVWK.js";
|
|
5
|
+
|
|
6
|
+
// src/utils/sql-row-limiter.ts
|
|
7
|
+
var SQLRowLimiter = class {
|
|
8
|
+
/**
|
|
9
|
+
* Check if a SQL statement is a SELECT query that can benefit from row limiting
|
|
10
|
+
* Only handles SELECT queries
|
|
11
|
+
*/
|
|
12
|
+
static isSelectQuery(sql) {
|
|
13
|
+
const trimmed = sql.trim().toLowerCase();
|
|
14
|
+
return trimmed.startsWith("select");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if a SQL statement already has a LIMIT clause.
|
|
18
|
+
* Strips comments and string literals first to avoid false positives.
|
|
19
|
+
*/
|
|
20
|
+
static hasLimitClause(sql) {
|
|
21
|
+
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
22
|
+
const limitRegex = /\blimit\s+(?:\d+|\$\d+|\?|@p\d+)/i;
|
|
23
|
+
return limitRegex.test(cleanedSQL);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Check if a SQL statement already has a TOP clause (SQL Server).
|
|
27
|
+
* Strips comments and string literals first to avoid false positives.
|
|
28
|
+
*/
|
|
29
|
+
static hasTopClause(sql) {
|
|
30
|
+
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
31
|
+
const topRegex = /\bselect\s+top\s+\d+/i;
|
|
32
|
+
return topRegex.test(cleanedSQL);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract existing LIMIT value from SQL if present.
|
|
36
|
+
* Strips comments and string literals first to avoid false positives.
|
|
37
|
+
*/
|
|
38
|
+
static extractLimitValue(sql) {
|
|
39
|
+
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
40
|
+
const limitMatch = cleanedSQL.match(/\blimit\s+(\d+)/i);
|
|
41
|
+
if (limitMatch) {
|
|
42
|
+
return parseInt(limitMatch[1], 10);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Extract existing TOP value from SQL if present (SQL Server).
|
|
48
|
+
* Strips comments and string literals first to avoid false positives.
|
|
49
|
+
*/
|
|
50
|
+
static extractTopValue(sql) {
|
|
51
|
+
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
52
|
+
const topMatch = cleanedSQL.match(/\bselect\s+top\s+(\d+)/i);
|
|
53
|
+
if (topMatch) {
|
|
54
|
+
return parseInt(topMatch[1], 10);
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Add or modify LIMIT clause in a SQL statement
|
|
60
|
+
*/
|
|
61
|
+
static applyLimitToQuery(sql, maxRows) {
|
|
62
|
+
const existingLimit = this.extractLimitValue(sql);
|
|
63
|
+
if (existingLimit !== null) {
|
|
64
|
+
const effectiveLimit = Math.min(existingLimit, maxRows);
|
|
65
|
+
return sql.replace(/\blimit\s+\d+/i, `LIMIT ${effectiveLimit}`);
|
|
66
|
+
} else {
|
|
67
|
+
const trimmed = sql.trim();
|
|
68
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
69
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
70
|
+
return `${sqlWithoutSemicolon}
|
|
71
|
+
LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Scan blanked (comment/string-free, length-preserving) SQL for parenthesis
|
|
76
|
+
* depth, invoking onMatch for every regex hit at depth 0 (i.e. not nested
|
|
77
|
+
* inside a subquery, function call, or window OVER clause).
|
|
78
|
+
*/
|
|
79
|
+
static scanTopLevel(blankedSQL, regex, onMatch) {
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let m;
|
|
82
|
+
while ((m = regex.exec(blankedSQL)) !== null) {
|
|
83
|
+
const token = m[0];
|
|
84
|
+
if (token === "(") {
|
|
85
|
+
depth++;
|
|
86
|
+
} else if (token === ")") {
|
|
87
|
+
depth--;
|
|
88
|
+
} else if (depth === 0) {
|
|
89
|
+
onMatch(m);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if a SQL statement combines multiple SELECTs with a set operator
|
|
95
|
+
* (UNION [ALL], INTERSECT, EXCEPT) at the top level — i.e. not nested
|
|
96
|
+
* inside a subquery already wrapped in parentheses. Strips comments and
|
|
97
|
+
* string literals first to avoid false positives.
|
|
98
|
+
*/
|
|
99
|
+
static hasSetOperator(sql) {
|
|
100
|
+
const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
|
|
101
|
+
let found = false;
|
|
102
|
+
this.scanTopLevel(blankedSQL, /\(|\)|\bunion\b|\bintersect\b|\bexcept\b/gi, () => {
|
|
103
|
+
found = true;
|
|
104
|
+
});
|
|
105
|
+
return found;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Find the start index of a top-level trailing ORDER BY clause (not one
|
|
109
|
+
* nested inside a subquery or a window function's OVER (...) clause).
|
|
110
|
+
* Returns -1 if none exists. Operates on the original (non-blanked) SQL
|
|
111
|
+
* length, since blankCommentsAndStrings preserves length/position.
|
|
112
|
+
*/
|
|
113
|
+
static findTopLevelOrderByIndex(sql) {
|
|
114
|
+
const blankedSQL = blankCommentsAndStrings(sql, "sqlserver");
|
|
115
|
+
let lastIndex = -1;
|
|
116
|
+
this.scanTopLevel(blankedSQL, /\(|\)|\border\s+by\b/gi, (m) => {
|
|
117
|
+
lastIndex = m.index;
|
|
118
|
+
});
|
|
119
|
+
return lastIndex;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Add or modify TOP clause in a SQL statement (SQL Server)
|
|
123
|
+
*/
|
|
124
|
+
static applyTopToQuery(sql, maxRows) {
|
|
125
|
+
if (this.hasSetOperator(sql)) {
|
|
126
|
+
const trimmed = sql.trim();
|
|
127
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
128
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
129
|
+
const orderByIndex = this.findTopLevelOrderByIndex(sqlWithoutSemicolon);
|
|
130
|
+
if (orderByIndex !== -1) {
|
|
131
|
+
const innerSql = sqlWithoutSemicolon.slice(0, orderByIndex).trimEnd();
|
|
132
|
+
const orderByClause = sqlWithoutSemicolon.slice(orderByIndex).trim();
|
|
133
|
+
return `SELECT TOP ${maxRows} * FROM (${innerSql}
|
|
134
|
+
) AS subq ${orderByClause}${hasSemicolon ? ";" : ""}`;
|
|
135
|
+
}
|
|
136
|
+
return `SELECT TOP ${maxRows} * FROM (${sqlWithoutSemicolon}
|
|
137
|
+
) AS subq${hasSemicolon ? ";" : ""}`;
|
|
138
|
+
}
|
|
139
|
+
const existingTop = this.extractTopValue(sql);
|
|
140
|
+
if (existingTop !== null) {
|
|
141
|
+
const effectiveTop = Math.min(existingTop, maxRows);
|
|
142
|
+
return sql.replace(/\bselect\s+top\s+\d+/i, `SELECT TOP ${effectiveTop}`);
|
|
143
|
+
}
|
|
144
|
+
return sql.replace(/\bselect\s+/i, `SELECT TOP ${maxRows} `);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check if a LIMIT clause uses a parameter placeholder (not a literal number).
|
|
148
|
+
* Strips comments and string literals first to avoid false positives.
|
|
149
|
+
*/
|
|
150
|
+
static hasParameterizedLimit(sql) {
|
|
151
|
+
const cleanedSQL = stripCommentsAndStrings(sql);
|
|
152
|
+
const parameterizedLimitRegex = /\blimit\s+(?:\$\d+|\?|@p\d+)/i;
|
|
153
|
+
return parameterizedLimitRegex.test(cleanedSQL);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Apply maxRows limit to a SELECT query only
|
|
157
|
+
*
|
|
158
|
+
* This method is used by PostgreSQL, MySQL, MariaDB, and SQLite connectors which all support
|
|
159
|
+
* the LIMIT clause syntax. SQL Server uses applyMaxRowsForSQLServer() instead with TOP syntax.
|
|
160
|
+
*
|
|
161
|
+
* For parameterized LIMIT clauses (e.g., LIMIT $1 or LIMIT ?), we wrap the query in a subquery
|
|
162
|
+
* to enforce max_rows as a hard cap, since the parameter value is not known until runtime.
|
|
163
|
+
*/
|
|
164
|
+
static applyMaxRows(sql, maxRows) {
|
|
165
|
+
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
166
|
+
return sql;
|
|
167
|
+
}
|
|
168
|
+
if (this.hasParameterizedLimit(sql)) {
|
|
169
|
+
const trimmed = sql.trim();
|
|
170
|
+
const hasSemicolon = trimmed.endsWith(";");
|
|
171
|
+
const sqlWithoutSemicolon = hasSemicolon ? trimmed.slice(0, -1) : trimmed;
|
|
172
|
+
return `SELECT * FROM (${sqlWithoutSemicolon}
|
|
173
|
+
) AS subq LIMIT ${maxRows}${hasSemicolon ? ";" : ""}`;
|
|
174
|
+
}
|
|
175
|
+
return this.applyLimitToQuery(sql, maxRows);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Apply maxRows limit to a SELECT query using SQL Server TOP syntax
|
|
179
|
+
*/
|
|
180
|
+
static applyMaxRowsForSQLServer(sql, maxRows) {
|
|
181
|
+
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
182
|
+
return sql;
|
|
183
|
+
}
|
|
184
|
+
return this.applyTopToQuery(sql, maxRows);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Like applyMaxRows, but requests one probe row (maxRows + 1) whenever the
|
|
188
|
+
* cap is the binding constraint, so callers can detect truncation exactly
|
|
189
|
+
* instead of guessing from `rowCount === maxRows` (which a table with
|
|
190
|
+
* exactly maxRows rows also produces).
|
|
191
|
+
*
|
|
192
|
+
* `probeApplied` is false when the cap cannot fire: no maxRows, not a
|
|
193
|
+
* SELECT, or the query's own literal LIMIT is already within the cap (the
|
|
194
|
+
* user asked for fewer rows than the cap — that is their limit, not
|
|
195
|
+
* truncation).
|
|
196
|
+
*/
|
|
197
|
+
static applyMaxRowsWithTruncationProbe(sql, maxRows) {
|
|
198
|
+
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
199
|
+
return { sql, probeApplied: false };
|
|
200
|
+
}
|
|
201
|
+
if (!this.hasParameterizedLimit(sql)) {
|
|
202
|
+
const existingLimit = this.extractLimitValue(sql);
|
|
203
|
+
if (existingLimit !== null && existingLimit <= maxRows) {
|
|
204
|
+
return { sql, probeApplied: false };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { sql: this.applyMaxRows(sql, maxRows + 1), probeApplied: true };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* SQL Server variant of applyMaxRowsWithTruncationProbe, using TOP syntax.
|
|
211
|
+
* A set-operator statement is always wrapped (any TOP on an individual
|
|
212
|
+
* branch caps only that branch, not the combined output), so the probe
|
|
213
|
+
* applies there regardless of branch-level TOPs.
|
|
214
|
+
*/
|
|
215
|
+
static applyMaxRowsForSQLServerWithTruncationProbe(sql, maxRows) {
|
|
216
|
+
if (!maxRows || !this.isSelectQuery(sql)) {
|
|
217
|
+
return { sql, probeApplied: false };
|
|
218
|
+
}
|
|
219
|
+
if (!this.hasSetOperator(sql)) {
|
|
220
|
+
const existingTop = this.extractTopValue(sql);
|
|
221
|
+
if (existingTop !== null && existingTop <= maxRows) {
|
|
222
|
+
return { sql, probeApplied: false };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { sql: this.applyMaxRowsForSQLServer(sql, maxRows + 1), probeApplied: true };
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Post-process a result set produced by a truncation-probe rewrite: when
|
|
229
|
+
* the probe row came back (more than maxRows rows), drop it, clamp the
|
|
230
|
+
* count to maxRows, and mark the set truncated so consumers can tell a
|
|
231
|
+
* capped result from a complete one.
|
|
232
|
+
*/
|
|
233
|
+
static flagTruncation(resultSet, maxRows, probeApplied) {
|
|
234
|
+
if (!probeApplied || !maxRows || resultSet.rows.length <= maxRows) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
resultSet.rows = resultSet.rows.slice(0, maxRows);
|
|
238
|
+
resultSet.rowCount = maxRows;
|
|
239
|
+
resultSet.truncated = true;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/utils/resource-cleanup.ts
|
|
244
|
+
async function closeQuietly(close) {
|
|
245
|
+
try {
|
|
246
|
+
await close();
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export {
|
|
252
|
+
SQLRowLimiter,
|
|
253
|
+
closeQuietly
|
|
254
|
+
};
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-FU2ZJE4E.js";
|
|
5
5
|
import {
|
|
6
6
|
obfuscateDSNPassword
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-MF6LSVWK.js";
|
|
8
8
|
|
|
9
9
|
// src/connectors/mysql-family-health-check.ts
|
|
10
10
|
async function getMySQLFamilyHealthCheck(query) {
|
|
@@ -111,49 +111,36 @@ function isMultiStatementResult(results) {
|
|
|
111
111
|
const firstElement = results[0];
|
|
112
112
|
return isMetadataObject(firstElement) || Array.isArray(firstElement);
|
|
113
113
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
const allRows = [];
|
|
119
|
-
for (const result of results) {
|
|
114
|
+
function buildResultSetsFromMultiStatement(results, statementTexts) {
|
|
115
|
+
const sql = statementTexts?.length === results.length ? statementTexts : void 0;
|
|
116
|
+
return results.map((result, index) => {
|
|
120
117
|
if (Array.isArray(result)) {
|
|
121
|
-
|
|
118
|
+
return { sql: sql?.[index], rows: result, rowCount: result.length };
|
|
122
119
|
}
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
if (isMetadataObject(result)) {
|
|
121
|
+
return { sql: sql?.[index], rows: [], rowCount: result.affectedRows || 0 };
|
|
122
|
+
}
|
|
123
|
+
return { sql: sql?.[index], rows: [], rowCount: 0 };
|
|
124
|
+
});
|
|
125
125
|
}
|
|
126
|
-
function
|
|
126
|
+
function parseQueryResultSets(results, statementTexts) {
|
|
127
|
+
const singleStatementSql = statementTexts?.length === 1 ? statementTexts[0] : void 0;
|
|
127
128
|
if (isMetadataObject(results)) {
|
|
128
|
-
return results.affectedRows || 0;
|
|
129
|
+
return [{ sql: singleStatementSql, rows: [], rowCount: results.affectedRows || 0 }];
|
|
129
130
|
}
|
|
130
131
|
if (!Array.isArray(results)) {
|
|
131
|
-
return 0;
|
|
132
|
+
return [{ sql: singleStatementSql, rows: [], rowCount: 0 }];
|
|
132
133
|
}
|
|
133
134
|
if (isMultiStatementResult(results)) {
|
|
134
|
-
|
|
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;
|
|
135
|
+
return buildResultSetsFromMultiStatement(results, statementTexts);
|
|
143
136
|
}
|
|
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;
|
|
137
|
+
return [{ sql: singleStatementSql, rows: results, rowCount: results.length }];
|
|
154
138
|
}
|
|
155
139
|
|
|
156
140
|
// src/utils/readonly-transaction.ts
|
|
141
|
+
function isClientSideTimeout(error) {
|
|
142
|
+
return typeof error === "object" && error !== null && error.code === "PROTOCOL_SEQUENCE_TIMEOUT";
|
|
143
|
+
}
|
|
157
144
|
async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransaction, execute) {
|
|
158
145
|
if (!readonly) {
|
|
159
146
|
return execute();
|
|
@@ -166,9 +153,11 @@ async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransacti
|
|
|
166
153
|
await conn.query(supportsReadOnlyTransaction ? "COMMIT" : "ROLLBACK");
|
|
167
154
|
return result;
|
|
168
155
|
} catch (error) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
156
|
+
if (!isClientSideTimeout(error)) {
|
|
157
|
+
try {
|
|
158
|
+
await conn.query("ROLLBACK");
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
172
161
|
}
|
|
173
162
|
throw error;
|
|
174
163
|
}
|
|
@@ -183,8 +172,8 @@ export {
|
|
|
183
172
|
getMySQLFamilyHealthCheck,
|
|
184
173
|
MissingDatabaseError,
|
|
185
174
|
requireDatabaseInDSN,
|
|
186
|
-
|
|
187
|
-
|
|
175
|
+
parseQueryResultSets,
|
|
176
|
+
isClientSideTimeout,
|
|
188
177
|
withReadOnlyTransaction,
|
|
189
178
|
isTiDBVersion
|
|
190
179
|
};
|
|
@@ -91,7 +91,9 @@ var SafeURL = class {
|
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
-
const
|
|
94
|
+
const pathStart = urlString.indexOf("/");
|
|
95
|
+
const authorityEnd = pathStart === -1 ? urlString.length : pathStart;
|
|
96
|
+
const atIndex = urlString.lastIndexOf("@", authorityEnd - 1);
|
|
95
97
|
if (atIndex !== -1) {
|
|
96
98
|
const auth = urlString.substring(0, atIndex);
|
|
97
99
|
urlString = urlString.substring(atIndex + 1);
|
|
@@ -462,6 +464,21 @@ function stripCommentsAndStrings(sql, dialect) {
|
|
|
462
464
|
}
|
|
463
465
|
return parts.join("");
|
|
464
466
|
}
|
|
467
|
+
function blankCommentsAndStrings(sql, dialect) {
|
|
468
|
+
const scanToken = getScanner(dialect);
|
|
469
|
+
let result = "";
|
|
470
|
+
let i = 0;
|
|
471
|
+
while (i < sql.length) {
|
|
472
|
+
const token = scanToken(sql, i);
|
|
473
|
+
if (token.type === TokenType.Plain) {
|
|
474
|
+
result += sql[i];
|
|
475
|
+
} else {
|
|
476
|
+
result += " ".repeat(token.end - i);
|
|
477
|
+
}
|
|
478
|
+
i = token.end;
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
465
482
|
function splitSQLStatements(sql, dialect) {
|
|
466
483
|
const scanToken = getScanner(dialect);
|
|
467
484
|
const statements = [];
|
|
@@ -495,5 +512,6 @@ export {
|
|
|
495
512
|
getDatabaseTypeFromDSN,
|
|
496
513
|
getDefaultPortForType,
|
|
497
514
|
stripCommentsAndStrings,
|
|
515
|
+
blankCommentsAndStrings,
|
|
498
516
|
splitSQLStatements
|
|
499
517
|
};
|