@bytebase/dbhub 0.23.0 → 1.0.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/dist/index.js CHANGED
@@ -15,7 +15,13 @@ import {
15
15
  resolveSourceConfigs,
16
16
  resolveTomlConfigPath,
17
17
  resolveTransport
18
- } from "./chunk-COEDRFSK.js";
18
+ } from "./chunk-MQR6VX5F.js";
19
+ import {
20
+ allowedKeywords,
21
+ isReadOnlySQL,
22
+ sqlServerDynamicSqlPattern,
23
+ sqlServerPassThroughPattern
24
+ } from "./chunk-JO7YQPBQ.js";
19
25
  import {
20
26
  loadConnectors
21
27
  } from "./chunk-WVVMH6FJ.js";
@@ -32,9 +38,9 @@ import {
32
38
  } from "./chunk-JEZZN2YZ.js";
33
39
 
34
40
  // src/server.ts
35
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
36
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
37
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
41
+ import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
42
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
43
+ import { toNodeHandler } from "@modelcontextprotocol/node";
38
44
  import express from "express";
39
45
  import http from "http";
40
46
  import path from "path";
@@ -95,100 +101,50 @@ function createToolSuccessResponse(data, meta = {}) {
95
101
  };
96
102
  }
97
103
 
98
- // src/utils/allowed-keywords.ts
99
- var allowedKeywords = {
100
- postgres: ["select", "with", "explain", "show"],
101
- mysql: ["select", "with", "explain", "show", "describe", "desc"],
102
- mariadb: ["select", "with", "explain", "show", "describe", "desc"],
103
- sqlite: ["select", "with", "explain", "pragma"],
104
- // SQL Server has no native EXPLAIN statement; the connector translates a
105
- // leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
106
- sqlserver: ["select", "with", "explain"]
107
- };
108
- var mutatingKeywords = [
109
- "insert",
110
- "update",
111
- "delete",
112
- "drop",
113
- "alter",
114
- "create",
115
- "truncate",
116
- "merge",
117
- "grant",
118
- "revoke",
119
- "rename"
120
- ];
121
- var mutatingPattern = new RegExp(
122
- `\\b(?:${mutatingKeywords.join("|")})\\b`,
123
- "i"
124
- );
125
- var mutatingPatternWithReplace = new RegExp(
126
- `\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`,
127
- "i"
128
- );
129
- var mutatingPatternSqlServer = new RegExp(
130
- `\\b(?:${[...mutatingKeywords, "execute", "exec", "sp_executesql", "xp_cmdshell"].join("|")})\\b`,
131
- "i"
132
- );
133
- var mutatingPatterns = {
134
- postgres: mutatingPattern,
135
- mysql: mutatingPatternWithReplace,
136
- mariadb: mutatingPatternWithReplace,
137
- sqlite: mutatingPatternWithReplace,
138
- sqlserver: mutatingPatternSqlServer
104
+ // src/utils/sql-access-policy.ts
105
+ var CLASS_SEVERITY = {
106
+ read: 0,
107
+ dml: 1,
108
+ ddl: 2,
109
+ admin: 3,
110
+ unknown: 4
139
111
  };
140
- var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
141
- var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([
142
- "table_info",
143
- "index_info",
144
- "index_list",
145
- "foreign_key_list"
146
- ]);
147
- var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/;
148
- var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
149
- function isReadOnlySQL(sql, connectorType) {
150
- return checkReadOnly(
151
- stripCommentsAndStrings(sql, connectorType).trim().toLowerCase(),
152
- connectorType
153
- );
112
+ var ddlPattern = /\b(?:create|alter|drop|truncate|rename)\b/i;
113
+ var dmlPattern = /\b(?:insert|update|delete|merge|replace\s+(?:(?:low_priority|delayed)\s+)?into)\b/i;
114
+ var grantRevokePattern = /\b(?:grant|revoke)\b/i;
115
+ function isAdminStatement(stripped, connectorType) {
116
+ if (grantRevokePattern.test(stripped)) return true;
117
+ return connectorType === "sqlserver" && (sqlServerDynamicSqlPattern.test(stripped) || sqlServerPassThroughPattern.test(stripped));
154
118
  }
155
- function checkReadOnly(cleanedSQL, connectorType) {
156
- if (!cleanedSQL) {
157
- return false;
158
- }
159
- const firstWord = cleanedSQL.match(/\S+/)?.[0] ?? "";
160
- const keywordList = allowedKeywords[connectorType] || [];
161
- if (!keywordList.includes(firstWord)) {
162
- return false;
163
- }
164
- if (firstWord === "with") {
165
- const pattern = mutatingPatterns[connectorType] ?? mutatingPattern;
166
- if (pattern.test(cleanedSQL)) {
167
- return false;
168
- }
169
- }
170
- if (firstWord === "pragma" && connectorType === "sqlite") {
171
- if (cleanedSQL.includes("=")) {
172
- return false;
173
- }
174
- const parenMatch = cleanedSQL.match(sqlitePragmaParenPattern);
175
- if (parenMatch && !sqliteReadOnlyArgPragmas.has(parenMatch[1])) {
176
- return false;
177
- }
178
- }
179
- if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
180
- return false;
181
- }
182
- if (firstWord === "explain") {
183
- const m = explainAnalyzePattern.exec(cleanedSQL);
184
- if (m) {
185
- const afterExplain = cleanedSQL.slice(m[0].length).trim();
186
- if (afterExplain && !checkReadOnly(afterExplain, connectorType)) {
187
- return false;
188
- }
119
+ function policyFromReadonly(readonly) {
120
+ const write = readonly === true ? "deny" : "allow";
121
+ return { read: "allow", dml: write, ddl: write, admin: write, unknown: write };
122
+ }
123
+ function isReadOnlyPolicy(policy) {
124
+ return policy.dml === "deny" && policy.ddl === "deny" && policy.admin === "deny" && policy.unknown === "deny";
125
+ }
126
+ function classifyStatement(sql, connectorType) {
127
+ if (isReadOnlySQL(sql, connectorType)) {
128
+ return "read";
129
+ }
130
+ const stripped = stripCommentsAndStrings(sql, connectorType);
131
+ if (isAdminStatement(stripped, connectorType)) return "admin";
132
+ if (ddlPattern.test(stripped)) return "ddl";
133
+ if (dmlPattern.test(stripped)) return "dml";
134
+ return "unknown";
135
+ }
136
+ function classifySQL(sql, connectorType) {
137
+ let strictest = "read";
138
+ for (const statement of splitSQLStatements(sql, connectorType)) {
139
+ const cls = classifyStatement(statement, connectorType);
140
+ if (CLASS_SEVERITY[cls] > CLASS_SEVERITY[strictest]) {
141
+ strictest = cls;
189
142
  }
190
143
  }
191
- return true;
144
+ return strictest;
145
+ }
146
+ function sqlVerdict(policy, sql, connectorType) {
147
+ return policy[classifySQL(sql, connectorType)];
192
148
  }
193
149
 
194
150
  // src/requests/store.ts
@@ -242,8 +198,8 @@ var RequestStore = class {
242
198
  var requestStore = new RequestStore();
243
199
 
244
200
  // src/utils/client-identifier.ts
245
- function getClientIdentifier(extra) {
246
- const userAgent = extra?.requestInfo?.headers?.["user-agent"];
201
+ function getClientIdentifier(ctx) {
202
+ const userAgent = ctx?.http?.req?.headers.get("user-agent");
247
203
  if (userAgent) {
248
204
  return userAgent;
249
205
  }
@@ -289,10 +245,7 @@ function tryClassifyConnectionError(error, rawSourceId, displaySourceId) {
289
245
  var executeSqlSchema = {
290
246
  sql: z.string().describe("SQL to execute (multiple statements separated by ;)")
291
247
  };
292
- function areAllStatementsReadOnly(sql, connectorType) {
293
- const statements = splitSQLStatements(sql, connectorType);
294
- return statements.every((statement) => isReadOnlySQL(statement, connectorType));
295
- }
248
+ var executeSqlInputSchema = z.object(executeSqlSchema);
296
249
  function createExecuteSqlToolHandler(sourceId) {
297
250
  return async (args, extra) => {
298
251
  const { sql } = args;
@@ -307,14 +260,14 @@ function createExecuteSqlToolHandler(sourceId) {
307
260
  const actualSourceId = connector.getId();
308
261
  const registry = getToolRegistry();
309
262
  const toolConfig = registry.getBuiltinToolConfig(BUILTIN_TOOL_EXECUTE_SQL, actualSourceId);
310
- const isReadonly = toolConfig?.readonly === true;
311
- if (isReadonly && !areAllStatementsReadOnly(sql, connector.id)) {
263
+ const policy = policyFromReadonly(toolConfig?.readonly);
264
+ if (sqlVerdict(policy, sql, connector.id) === "deny") {
312
265
  errorMessage = `Read-only mode is enabled. Only the following SQL operations are allowed: ${allowedKeywords[connector.id]?.join(", ") || "none"}`;
313
266
  success = false;
314
267
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
315
268
  }
316
269
  const executeOptions = {
317
- readonly: toolConfig?.readonly,
270
+ readonly: isReadOnlyPolicy(policy),
318
271
  maxRows: toolConfig?.max_rows
319
272
  };
320
273
  result = await connector.executeSQL(sql, executeOptions);
@@ -357,6 +310,7 @@ var searchDatabaseObjectsSchema = {
357
310
  detail_level: z2.enum(["names", "summary", "full"]).default("names").describe("Detail: names (minimal), summary (metadata), full (all)"),
358
311
  limit: z2.number().int().positive().max(1e3).default(100).describe("Max results")
359
312
  };
313
+ var searchDatabaseObjectsInputSchema = z2.object(searchDatabaseObjectsSchema);
360
314
  function likePatternToRegex(pattern) {
361
315
  const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/%/g, ".*").replace(/_/g, ".");
362
316
  return new RegExp(`^${escaped}$`, "i");
@@ -377,7 +331,7 @@ async function getTableRowCount(connector, tableName, schemaName) {
377
331
  }
378
332
  const qualifiedTable = quoteQualifiedIdentifier(tableName, schemaName, connector.id);
379
333
  const countQuery = `SELECT COUNT(*) as count FROM ${qualifiedTable}`;
380
- const result = await connector.executeSQL(countQuery, { maxRows: 1 });
334
+ const result = await connector.executeSQL(countQuery, { maxRows: 1, readonly: true });
381
335
  if (result.rows && result.rows.length > 0) {
382
336
  return Number(result.rows[0].count || result.rows[0].COUNT || 0);
383
337
  }
@@ -510,7 +464,7 @@ async function searchViews(connector, pattern, schemaFilter, detailLevel, limit)
510
464
  if (schemaFilter) {
511
465
  schemasToSearch = [schemaFilter];
512
466
  } else {
513
- schemasToSearch = await connector.getSchemas();
467
+ schemasToSearch = await resolveDefaultSchemas(connector);
514
468
  }
515
469
  for (const schemaName of schemasToSearch) {
516
470
  if (results.length >= limit) break;
@@ -888,7 +842,7 @@ function getExecuteSqlMetadata(sourceId) {
888
842
  const readonlyNote = executeOptions.readonly ? " [READ-ONLY MODE]" : "";
889
843
  const maxRowsNote = executeOptions.maxRows ? ` (limited to ${executeOptions.maxRows} rows)` : "";
890
844
  const description = isSingleSource ? `${userDescPrefix}Execute SQL queries on the ${dbType} database${readonlyNote}${maxRowsNote}` : `${userDescPrefix}Execute SQL queries on the '${sourceId}' ${dbType} database${readonlyNote}${maxRowsNote}`;
891
- const isReadonly = executeOptions.readonly === true;
845
+ const isReadonly = isReadOnlyPolicy(policyFromReadonly(executeOptions.readonly));
892
846
  const annotations = {
893
847
  title,
894
848
  readOnlyHint: isReadonly,
@@ -1064,9 +1018,17 @@ function buildZodSchemaFromParameters(parameters) {
1064
1018
  }
1065
1019
  return schemaShape;
1066
1020
  }
1021
+ var customToolInputSchemas = /* @__PURE__ */ new WeakMap();
1022
+ function getCustomToolInputSchema(toolConfig) {
1023
+ let schema = customToolInputSchemas.get(toolConfig);
1024
+ if (!schema) {
1025
+ schema = z4.object(buildZodSchemaFromParameters(toolConfig.parameters));
1026
+ customToolInputSchemas.set(toolConfig, schema);
1027
+ }
1028
+ return schema;
1029
+ }
1067
1030
  function createCustomToolHandler(toolConfig) {
1068
- const zodSchemaShape = buildZodSchemaFromParameters(toolConfig.parameters);
1069
- const zodSchema = z4.object(zodSchemaShape);
1031
+ const zodSchema = getCustomToolInputSchema(toolConfig);
1070
1032
  return async (args, extra) => {
1071
1033
  const startTime = Date.now();
1072
1034
  let success = true;
@@ -1076,12 +1038,12 @@ function createCustomToolHandler(toolConfig) {
1076
1038
  const validatedArgs = zodSchema.parse(args);
1077
1039
  await ConnectorManager.ensureConnected(toolConfig.source);
1078
1040
  const connector = ConnectorManager.getCurrentConnector(toolConfig.source);
1041
+ const policy = policyFromReadonly(toolConfig.readonly);
1079
1042
  const executeOptions = {
1080
- readonly: toolConfig.readonly,
1043
+ readonly: isReadOnlyPolicy(policy),
1081
1044
  maxRows: toolConfig.max_rows
1082
1045
  };
1083
- const isReadonly = executeOptions.readonly === true;
1084
- if (isReadonly && !isReadOnlySQL(toolConfig.statement, connector.id)) {
1046
+ if (sqlVerdict(policy, toolConfig.statement, connector.id) === "deny") {
1085
1047
  errorMessage = createReadonlyViolationMessage(toolConfig.name, toolConfig.source, connector.id);
1086
1048
  success = false;
1087
1049
  return createToolErrorResponse(errorMessage, "READONLY_VIOLATION");
@@ -1158,7 +1120,7 @@ function registerExecuteSqlTool(server, sourceId) {
1158
1120
  metadata.name,
1159
1121
  {
1160
1122
  description: metadata.description,
1161
- inputSchema: metadata.schema,
1123
+ inputSchema: executeSqlInputSchema,
1162
1124
  annotations: metadata.annotations
1163
1125
  },
1164
1126
  createExecuteSqlToolHandler(sourceId)
@@ -1170,7 +1132,7 @@ function registerSearchObjectsTool(server, sourceId) {
1170
1132
  metadata.name,
1171
1133
  {
1172
1134
  description: metadata.description,
1173
- inputSchema: searchDatabaseObjectsSchema,
1135
+ inputSchema: searchDatabaseObjectsInputSchema,
1174
1136
  annotations: {
1175
1137
  title: metadata.title,
1176
1138
  readOnlyHint: true,
@@ -1185,13 +1147,12 @@ function registerSearchObjectsTool(server, sourceId) {
1185
1147
  function registerCustomTool(server, sourceId, toolConfig) {
1186
1148
  const sourceConfig = ConnectorManager.getSourceConfig(sourceId);
1187
1149
  const dbType = sourceConfig.type;
1188
- const isReadOnly = isReadOnlySQL(toolConfig.statement, dbType);
1189
- const zodSchema = buildZodSchemaFromParameters(toolConfig.parameters);
1150
+ const isReadOnly = classifySQL(toolConfig.statement, dbType) === "read";
1190
1151
  server.registerTool(
1191
1152
  toolConfig.name,
1192
1153
  {
1193
1154
  description: toolConfig.description,
1194
- inputSchema: zodSchema,
1155
+ inputSchema: getCustomToolInputSchema(toolConfig),
1195
1156
  annotations: {
1196
1157
  title: `${toolConfig.name} (${dbType})`,
1197
1158
  readOnlyHint: isReadOnly,
@@ -1631,6 +1592,8 @@ var packageJsonPath = path.join(__dirname, "..", "package.json");
1631
1592
  var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1632
1593
  var SERVER_NAME = "DBHub MCP Server";
1633
1594
  var SERVER_VERSION = packageJson.version;
1595
+ var TOOLS_LIST_CACHE_TTL_MS = 3e5;
1596
+ var logMcpError = (scope) => (error) => console.error(`MCP ${scope} error:`, error);
1634
1597
  function generateBanner(version, modes = []) {
1635
1598
  const modeText = modes.length > 0 ? ` [${modes.join(" | ")}]` : "";
1636
1599
  return `
@@ -1676,7 +1639,7 @@ See documentation for more details on configuring database connections.
1676
1639
  const sources = sourceConfigsData.sources;
1677
1640
  console.error(`Configuration source: ${sourceConfigsData.source}`);
1678
1641
  await connectorManager.connectWithSources(sources);
1679
- const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-ZME64VB2.js");
1642
+ const { initializeToolRegistry: initializeToolRegistry2 } = await import("./registry-ZU6ZFNQ2.js");
1680
1643
  initializeToolRegistry2({
1681
1644
  sources: sourceConfigsData.sources,
1682
1645
  tools: sourceConfigsData.tools
@@ -1687,10 +1650,18 @@ See documentation for more details on configuring database connections.
1687
1650
  initialTools: sourceConfigsData.tools
1688
1651
  });
1689
1652
  const createServer = () => {
1690
- const server = new McpServer({
1691
- name: SERVER_NAME,
1692
- version: SERVER_VERSION
1693
- });
1653
+ const server = new McpServer(
1654
+ {
1655
+ name: SERVER_NAME,
1656
+ version: SERVER_VERSION
1657
+ },
1658
+ {
1659
+ // `private` keeps caching client-side (no shared/proxy caches).
1660
+ cacheHints: {
1661
+ "tools/list": { ttlMs: TOOLS_LIST_CACHE_TTL_MS, cacheScope: "private" }
1662
+ }
1663
+ }
1664
+ );
1694
1665
  registerTools(server);
1695
1666
  return server;
1696
1667
  };
@@ -1732,7 +1703,7 @@ See documentation for more details on configuring database connections.
1732
1703
  }
1733
1704
  res.header("Access-Control-Allow-Origin", origin || "http://localhost");
1734
1705
  res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1735
- res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
1706
+ res.header("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Mcp-Name");
1736
1707
  res.header("Access-Control-Allow-Credentials", "true");
1737
1708
  if (req.method === "OPTIONS") {
1738
1709
  return res.sendStatus(200);
@@ -1747,29 +1718,10 @@ See documentation for more details on configuring database connections.
1747
1718
  app.get("/api/sources", listSources);
1748
1719
  app.get("/api/sources/:sourceId", getSource);
1749
1720
  app.get("/api/requests", listRequests);
1750
- app.get("/mcp", (req, res) => {
1751
- res.status(405).json({
1752
- error: "Method Not Allowed",
1753
- message: "SSE streaming is not supported in stateless mode. Use POST requests with JSON responses."
1754
- });
1755
- });
1756
- app.post("/mcp", async (req, res) => {
1757
- try {
1758
- const transport = new StreamableHTTPServerTransport({
1759
- sessionIdGenerator: void 0,
1760
- // Disable session management for stateless mode
1761
- enableJsonResponse: true
1762
- // Use JSON responses (SSE not supported in stateless mode)
1763
- });
1764
- const server = createServer();
1765
- await server.connect(transport);
1766
- await transport.handleRequest(req, res, req.body);
1767
- } catch (error) {
1768
- console.error("Error handling request:", error);
1769
- if (!res.headersSent) {
1770
- res.status(500).json({ error: "Internal server error" });
1771
- }
1772
- }
1721
+ const mcpHandler = createMcpHandler(createServer, { onerror: logMcpError("handler") });
1722
+ const mcpNodeHandler = toNodeHandler(mcpHandler, { onerror: logMcpError("adapter") });
1723
+ app.all("/mcp", (req, res) => {
1724
+ void mcpNodeHandler(req, res, req.body);
1773
1725
  });
1774
1726
  if (process.env.NODE_ENV !== "development") {
1775
1727
  app.get("*", (req, res) => {
@@ -1805,16 +1757,14 @@ See documentation for more details on configuring database connections.
1805
1757
  console.error(`MCP server endpoint at http://${userHost}:${boundPort}/mcp`);
1806
1758
  });
1807
1759
  } else {
1808
- const server = createServer();
1809
- const transport = new StdioServerTransport();
1810
- await server.connect(transport);
1760
+ const stdioHandle = serveStdio(createServer, { onerror: logMcpError("stdio") });
1811
1761
  console.error("MCP server running on stdio");
1812
1762
  let isShuttingDown = false;
1813
1763
  const shutdown = async () => {
1814
1764
  if (isShuttingDown) return;
1815
1765
  isShuttingDown = true;
1816
1766
  console.error("Shutting down...");
1817
- await transport.close();
1767
+ await stdioHandle.close();
1818
1768
  await connectorManager.disconnect();
1819
1769
  process.exit(0);
1820
1770
  };
@@ -1830,11 +1780,11 @@ See documentation for more details on configuring database connections.
1830
1780
 
1831
1781
  // src/index.ts
1832
1782
  var connectorModules = [
1833
- { load: () => import("./postgres-BKBCGDZS.js"), name: "PostgreSQL", driver: "pg" },
1834
- { load: () => import("./sqlserver-65AGCEJX.js"), name: "SQL Server", driver: "mssql" },
1835
- { load: () => import("./sqlite-CQ4LCBB6.js"), name: "SQLite", driver: "node:sqlite" },
1836
- { load: () => import("./mysql-MZWCPISB.js"), name: "MySQL", driver: "mysql2" },
1837
- { load: () => import("./mariadb-SPFBL46R.js"), name: "MariaDB", driver: "mariadb" }
1783
+ { load: () => import("./postgres-YAYHDXVD.js"), name: "PostgreSQL", driver: "pg" },
1784
+ { load: () => import("./sqlserver-COKW7RIK.js"), name: "SQL Server", driver: "mssql" },
1785
+ { load: () => import("./sqlite-5M7SDAQZ.js"), name: "SQLite", driver: "node:sqlite" },
1786
+ { load: () => import("./mysql-CDUKJMNN.js"), name: "MySQL", driver: "mysql2" },
1787
+ { load: () => import("./mariadb-72C2BNCG.js"), name: "MariaDB", driver: "mariadb" }
1838
1788
  ];
1839
1789
  loadConnectors(connectorModules).then(() => main()).catch((error) => {
1840
1790
  console.error("Fatal error:", error);
@@ -1,13 +1,18 @@
1
1
  import {
2
+ MissingDatabaseError,
2
3
  extractAffectedRows,
3
- parseQueryResults
4
- } from "./chunk-RTB262PR.js";
4
+ isTiDBVersion,
5
+ parseQueryResults,
6
+ requireDatabaseInDSN,
7
+ withReadOnlyTransaction
8
+ } from "./chunk-DUCNPMNW.js";
5
9
  import {
6
10
  quoteIdentifier
7
11
  } from "./chunk-JFWX35TB.js";
8
12
  import {
9
- SQLRowLimiter
10
- } from "./chunk-BXXHWEQS.js";
13
+ SQLRowLimiter,
14
+ closeQuietly
15
+ } from "./chunk-S2V6RZ3C.js";
11
16
  import {
12
17
  ConnectorRegistry,
13
18
  SafeURL,
@@ -32,11 +37,12 @@ Expected: ${expectedFormat}`
32
37
  }
33
38
  try {
34
39
  const url = new SafeURL(dsn);
40
+ const database = url.pathname ? url.pathname.substring(1) : "";
41
+ requireDatabaseInDSN(database, dsn, "MariaDB");
35
42
  const connectionConfig = {
36
43
  host: url.hostname,
37
44
  port: url.port ? parseInt(url.port) : 3306,
38
- database: url.pathname ? url.pathname.substring(1) : "",
39
- // Remove leading '/' if exists
45
+ database,
40
46
  user: url.username,
41
47
  password: url.password,
42
48
  multipleStatements: true,
@@ -77,6 +83,9 @@ Expected: ${expectedFormat}`
77
83
  }
78
84
  return connectionConfig;
79
85
  } catch (error) {
86
+ if (error instanceof MissingDatabaseError) {
87
+ throw error;
88
+ }
80
89
  throw new Error(
81
90
  `Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}`
82
91
  );
@@ -101,6 +110,9 @@ var MariaDBConnector = class _MariaDBConnector {
101
110
  this.pool = null;
102
111
  // Source ID is set by ConnectorManager after cloning
103
112
  this.sourceId = "default";
113
+ // TiDB speaks the MySQL protocol but rejects `START TRANSACTION READ ONLY`
114
+ // unless tidb_enable_noop_functions is on. Detected once at connect time.
115
+ this.supportsReadOnlyTransaction = true;
104
116
  }
105
117
  getId() {
106
118
  return this.sourceId;
@@ -112,8 +124,14 @@ var MariaDBConnector = class _MariaDBConnector {
112
124
  try {
113
125
  const connectionConfig = await this.dsnParser.parse(dsn, config);
114
126
  this.pool = mariadb.createPool(connectionConfig);
115
- await this.pool.query("SELECT 1");
127
+ const rows = await this.pool.query("SELECT VERSION() AS version");
128
+ this.supportsReadOnlyTransaction = !isTiDBVersion(rows?.[0]?.version);
116
129
  } catch (err) {
130
+ if (this.pool) {
131
+ const pool = this.pool;
132
+ this.pool = null;
133
+ await closeQuietly(() => pool.end());
134
+ }
117
135
  console.error("Failed to connect to MariaDB database:", err);
118
136
  throw err;
119
137
  }
@@ -473,48 +491,33 @@ var MariaDBConnector = class _MariaDBConnector {
473
491
  }
474
492
  const conn = await this.pool.getConnection();
475
493
  try {
476
- if (options.readonly) {
477
- await conn.query("START TRANSACTION READ ONLY");
478
- }
479
- let processedSQL = sql;
480
- if (options.maxRows) {
481
- const statements = splitSQLStatements(sql, "mariadb");
482
- const processedStatements = statements.map(
483
- (statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
484
- );
485
- processedSQL = processedStatements.join("; ");
486
- if (sql.trim().endsWith(";")) {
487
- processedSQL += ";";
488
- }
489
- }
490
- let results;
491
- if (parameters && parameters.length > 0) {
492
- try {
493
- results = await conn.query(processedSQL, parameters);
494
- } catch (error) {
495
- console.error(`[MariaDB executeSQL] ERROR: ${error.message}`);
496
- console.error(`[MariaDB executeSQL] SQL: ${processedSQL}`);
497
- console.error(`[MariaDB executeSQL] Parameters: ${JSON.stringify(parameters)}`);
498
- throw error;
499
- }
500
- } else {
501
- results = await conn.query(processedSQL);
502
- }
503
- const rows = parseQueryResults(results);
504
- const rowCount = extractAffectedRows(results);
505
- if (options.readonly) {
506
- await conn.query("COMMIT");
507
- }
508
- return { rows, rowCount };
509
- } catch (error) {
510
- if (options.readonly) {
511
- try {
512
- await conn.query("ROLLBACK");
513
- } catch {
494
+ return await withReadOnlyTransaction(
495
+ conn,
496
+ options.readonly,
497
+ this.supportsReadOnlyTransaction,
498
+ async () => {
499
+ let processedSQL = sql;
500
+ if (options.maxRows) {
501
+ const statements = splitSQLStatements(sql, "mariadb");
502
+ const processedStatements = statements.map(
503
+ (statement) => SQLRowLimiter.applyMaxRows(statement, options.maxRows)
504
+ );
505
+ processedSQL = processedStatements.join("; ");
506
+ if (sql.trim().endsWith(";")) {
507
+ processedSQL += ";";
508
+ }
509
+ }
510
+ let results;
511
+ if (parameters && parameters.length > 0) {
512
+ results = await conn.query(processedSQL, parameters);
513
+ } else {
514
+ results = await conn.query(processedSQL);
515
+ }
516
+ const rows = parseQueryResults(results);
517
+ const rowCount = extractAffectedRows(results);
518
+ return { rows, rowCount };
514
519
  }
515
- }
516
- console.error("Error executing query:", error);
517
- throw error;
520
+ );
518
521
  } finally {
519
522
  conn.release();
520
523
  }