@bytebase/dbhub
Version:
Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite
271 lines (269 loc) • 9.07 kB
JavaScript
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
};