UNPKG

@bytebase/dbhub

Version:

Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite

271 lines (269 loc) • 9.07 kB
import { MissingDatabaseError, getDefaultSchema, getMySQLFamilyHealthCheck, getSchemas, getStoredProcedureDetail, getStoredProcedures, getTableComment, getTableIndexes, getTableSchema, getTables, getViews, isTiDBVersion, parseQueryResultSets, requireDatabaseInDSN, tableExists, withReadOnlyTransaction } from "./chunk-SJ5BRPQA.js"; import { clientQueryTimeoutMs, isClientSideTimeout, withClientQueryDeadline } from "./chunk-5XSBOK3W.js"; import "./chunk-FU2ZJE4E.js"; import "./chunk-X2EC47W4.js"; import { SQLRowLimiter, closeQuietly } from "./chunk-X5OCFRNL.js"; import "./chunk-55X2VY5D.js"; import { ConnectorRegistry, SafeURL, obfuscateDSNPassword, splitSQLStatements } from "./chunk-IXC4NQGC.js"; // src/connectors/mariadb/index.ts import * as mariadb from "mariadb"; var MariadbDSNParser = class { async parse(dsn, config) { const connectionTimeoutSeconds = config?.connectionTimeoutSeconds; const queryTimeoutSeconds = config?.queryTimeoutSeconds; if (!this.isValidDSN(dsn)) { const obfuscatedDSN = obfuscateDSNPassword(dsn); const expectedFormat = this.getSampleDSN(); throw new Error( `Invalid MariaDB DSN format. Provided: ${obfuscatedDSN} Expected: ${expectedFormat}` ); } try { const url = new SafeURL(dsn); const database = url.pathname ? url.pathname.substring(1) : ""; requireDatabaseInDSN(database, dsn, "MariaDB"); const connectionConfig = { host: url.hostname, port: url.port ? parseInt(url.port) : 3306, database, user: url.username, password: url.password, multipleStatements: true, // Enable native multi-statement support ...connectionTimeoutSeconds !== void 0 && { connectTimeout: connectionTimeoutSeconds * 1e3 }, // Server-side limit: the driver applies `queryTimeout` on each new // connection as the session variable max_statement_time. ...queryTimeoutSeconds !== void 0 && { queryTimeout: queryTimeoutSeconds * 1e3 }, // Controls how the driver interprets DATETIME values ("Z", "local", or "±HH:MM"). ...config?.timezone !== void 0 && { timezone: config.timezone }, // Connection character set (e.g. "utf8mb4") / collation (e.g. // "utf8mb4_general_ci"). The mariadb driver exposes both as distinct // options, but a configured collation is authoritative (it implies its // character set) and the driver ignores `collation` when `charset` is also // passed. So forward the collation when present, otherwise the charset // (which uses that character set's default collation). ...config?.collation !== void 0 ? { collation: config.collation } : config?.charset !== void 0 ? { charset: config.charset } : {} }; url.forEachSearchParam((value, key) => { if (key === "sslmode") { if (value === "disable") { connectionConfig.ssl = void 0; } else if (value === "require") { connectionConfig.ssl = { rejectUnauthorized: false }; } else { connectionConfig.ssl = {}; } } }); if (url.password && url.password.includes("X-Amz-Credential")) { if (connectionConfig.ssl === void 0) { connectionConfig.ssl = { rejectUnauthorized: false }; } } return connectionConfig; } catch (error) { if (error instanceof MissingDatabaseError) { throw error; } throw new Error( `Failed to parse MariaDB DSN: ${error instanceof Error ? error.message : String(error)}` ); } } getSampleDSN() { return "mariadb://root:password@localhost:3306/db?sslmode=require"; } isValidDSN(dsn) { try { return dsn.startsWith("mariadb://"); } catch (error) { return false; } } }; var MariaDBConnector = class _MariaDBConnector { constructor() { this.id = "mariadb"; this.name = "MariaDB"; this.dsnParser = new MariadbDSNParser(); this.pool = null; // Source ID is set by ConnectorManager after cloning this.sourceId = "default"; // TiDB speaks the MySQL protocol but rejects `START TRANSACTION READ ONLY` // unless tidb_enable_noop_functions is on. Detected once at connect time. this.supportsReadOnlyTransaction = true; } getId() { return this.sourceId; } clone() { return new _MariaDBConnector(); } async connect(dsn, initScript, config) { try { const connectionConfig = await this.dsnParser.parse(dsn, config); this.clientQueryTimeoutMs = config?.queryTimeoutSeconds !== void 0 ? clientQueryTimeoutMs(config.queryTimeoutSeconds * 1e3) : void 0; this.pool = mariadb.createPool(connectionConfig); this.pool.on("error", (err) => { console.error( `MariaDB pool (source "${this.sourceId}"): background connection error, pool will retry:`, err.message ); }); const rows = await this.pool.query("SELECT VERSION() AS version"); this.supportsReadOnlyTransaction = !isTiDBVersion(rows?.[0]?.version); } catch (err) { if (this.pool) { const pool = this.pool; this.pool = null; await closeQuietly(() => pool.end()); } console.error("Failed to connect to MariaDB database:", err); throw err; } } async disconnect() { if (this.pool) { await this.pool.end(); this.pool = null; } } /** * Row-array query adapter over the pool for the shared MySQL-family * introspection and health-check code. Throws when not connected, so the * shared functions never see a missing pool. The mariadb driver already * resolves to the rows themselves. */ requireQuery() { if (!this.pool) { throw new Error("Not connected to database"); } const pool = this.pool; return (sql, params) => params === void 0 ? pool.query(sql) : pool.query(sql, params); } async getSchemas() { return getSchemas(this.requireQuery()); } async getTables(schema) { return getTables(this.requireQuery(), schema); } async getViews(schema) { return getViews(this.requireQuery(), schema); } async tableExists(tableName, schema) { return tableExists(this.requireQuery(), tableName, schema); } async getTableIndexes(tableName, schema) { return getTableIndexes(this.requireQuery(), tableName, schema); } async getTableSchema(tableName, schema) { return getTableSchema(this.requireQuery(), tableName, schema); } async getTableComment(tableName, schema) { return getTableComment(this.requireQuery(), tableName, schema); } async getHealthCheck() { return getMySQLFamilyHealthCheck(this.requireQuery()); } async getStoredProcedures(schema, routineType) { return getStoredProcedures(this.requireQuery(), schema, routineType); } async getStoredProcedureDetail(procedureName, schema) { return getStoredProcedureDetail(this.requireQuery(), procedureName, schema); } async getDefaultSchema() { return getDefaultSchema(this.requireQuery()); } async executeSQL(sql, options, parameters) { if (!this.pool) { throw new Error("Not connected to database"); } const conn = await this.pool.getConnection(); let isConnectionDiscarded = false; try { return await withReadOnlyTransaction( conn, options.readonly, this.supportsReadOnlyTransaction, async () => { const statements = splitSQLStatements(sql, "mariadb"); let processedSQL = sql; let probes = []; if (options.maxRows) { const rewrites = statements.map( (statement) => SQLRowLimiter.applyMaxRowsWithTruncationProbe(statement, options.maxRows, "mariadb") ); probes = rewrites.map((rewrite) => rewrite.probeApplied); processedSQL = rewrites.map((rewrite) => rewrite.sql).join("; "); if (sql.trim().endsWith(";")) { processedSQL += ";"; } } const results = await withClientQueryDeadline( parameters && parameters.length > 0 ? conn.query(processedSQL, parameters) : conn.query(processedSQL), this.clientQueryTimeoutMs ); const resultSets = parseQueryResultSets(results, statements); if (resultSets.length === probes.length) { resultSets.forEach( (set, index) => SQLRowLimiter.flagTruncation(set, options.maxRows, probes[index]) ); } return { resultSets }; } ); } catch (error) { if (isClientSideTimeout(error)) { isConnectionDiscarded = true; conn.destroy(); } throw error; } finally { if (!isConnectionDiscarded) { conn.release(); } } } }; var mariadbConnector = new MariaDBConnector(); ConnectorRegistry.register(mariadbConnector); export { MariaDBConnector };