@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 CHANGED
@@ -1,5 +1,15 @@
1
1
  > [!NOTE]
2
- > Brought to you by [Bytebase](https://www.bytebase.com/), open-source database DevSecOps platform.
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, 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:
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
- - **Local Development First**: Zero dependency, token efficient with a minimal set of MCP tools to maximize context window
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, PostgreSQL/MySQL/MariaDB/SQL Server for now): Report connection pool state and buffer cache hit ratio
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-JEZZN2YZ.js";
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 signer = new Signer({
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-JEZZN2YZ.js";
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 extractRowsFromMultiStatement(results) {
115
- if (!Array.isArray(results)) {
116
- return [];
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
- allRows.push(...result);
118
+ return { sql: sql?.[index], rows: result, rowCount: result.length };
122
119
  }
123
- }
124
- return allRows;
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 extractAffectedRows(results) {
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
- 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;
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
- try {
170
- await conn.query("ROLLBACK");
171
- } catch {
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
- extractAffectedRows,
187
- parseQueryResults,
175
+ parseQueryResultSets,
176
+ isClientSideTimeout,
188
177
  withReadOnlyTransaction,
189
178
  isTiDBVersion
190
179
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  stripCommentsAndStrings
3
- } from "./chunk-JEZZN2YZ.js";
3
+ } from "./chunk-MF6LSVWK.js";
4
4
 
5
5
  // src/utils/allowed-keywords.ts
6
6
  var allowedKeywords = {
@@ -91,7 +91,9 @@ var SafeURL = class {
91
91
  }
92
92
  });
93
93
  }
94
- const atIndex = urlString.indexOf("@");
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
  };