@bytebase/dbhub
Version:
Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite
501 lines (493 loc) • 16.4 kB
JavaScript
import {
isClientSideTimeout
} from "./chunk-5XSBOK3W.js";
import {
computeHitRatioPct,
toNullableNumber
} from "./chunk-FU2ZJE4E.js";
import {
quoteIdentifier
} from "./chunk-X2EC47W4.js";
import {
obfuscateDSNPassword
} from "./chunk-IXC4NQGC.js";
// src/connectors/mysql-family-health-check.ts
async function getMySQLFamilyHealthCheck(query) {
const [processlistRows, maxConnRows, bufferPoolRows, trxOutcome] = await Promise.all([
query(`
SELECT
COUNT(*) AS total,
SUM(CASE WHEN COMMAND != 'Sleep' THEN 1 ELSE 0 END) AS active,
SUM(CASE WHEN COMMAND = 'Sleep' THEN 1 ELSE 0 END) AS idle,
MAX(CASE WHEN COMMAND != 'Sleep' THEN TIME ELSE NULL END) AS longest_active_query_seconds
FROM information_schema.PROCESSLIST
WHERE ID <> CONNECTION_ID()
`),
query(`SHOW VARIABLES LIKE 'max_connections'`),
query(`
SHOW GLOBAL STATUS WHERE Variable_name IN ('Innodb_buffer_pool_read_requests', 'Innodb_buffer_pool_reads')
`),
// Joining PROCESSLIST against INNODB_TRX to detect idle-in-transaction
// sessions requires the PROCESS privilege - unlike a plain PROCESSLIST
// select, which silently narrows to the caller's own session instead of
// erroring. Run concurrently with the other queries above, but resolve
// rather than reject on failure so it degrades instead of failing the
// whole health check.
query(`
SELECT
COUNT(*) AS idle_in_transaction,
MAX(TIMESTAMPDIFF(SECOND, t.trx_started, NOW())) AS longest_idle_in_transaction_seconds
FROM information_schema.INNODB_TRX t
JOIN information_schema.PROCESSLIST p ON p.ID = t.trx_mysql_thread_id
WHERE p.COMMAND = 'Sleep'
`).then(
(rows) => ({ ok: true, rows }),
() => ({ ok: false })
)
]);
const notes = [];
let idleInTransaction = 0;
let longestIdleInTransactionSeconds = null;
if (trxOutcome.ok) {
idleInTransaction = Number(trxOutcome.rows[0].idle_in_transaction);
longestIdleInTransactionSeconds = toNullableNumber(trxOutcome.rows[0].longest_idle_in_transaction_seconds);
} else {
notes.push(
"Connected user lacks the PROCESS privilege: PROCESSLIST visibility is restricted to the connecting user's own sessions, and this diagnostic session is excluded from the count, so connection counts and active-query duration may under-report (down to 0). Idle-in-transaction detection is unavailable."
);
}
const proc = processlistRows[0];
const bufferPoolStats = Object.fromEntries(bufferPoolRows.map((row) => [row.Variable_name, row.Value]));
const readRequests = Number(bufferPoolStats.Innodb_buffer_pool_read_requests ?? 0);
const reads = Number(bufferPoolStats.Innodb_buffer_pool_reads ?? 0);
return {
connections: {
total: Number(proc.total),
active: Number(proc.active),
idle: Number(proc.idle),
idleInTransaction,
// MySQL/MariaDB's InnoDB has no equivalent of Postgres's "idle in
// transaction (aborted)" state - a failed statement doesn't leave the
// session's transaction in a distinct aborted-but-open state the way
// Postgres does - so idleInTransactionAborted is left undefined.
maxConnections: maxConnRows.length > 0 ? Number(maxConnRows[0].Value) : null,
longestIdleInTransactionSeconds,
longestActiveQuerySeconds: toNullableNumber(proc.longest_active_query_seconds)
},
bufferCache: {
hitRatioPct: computeHitRatioPct(readRequests, reads),
blocksHit: readRequests - reads,
blocksRead: reads
},
...notes.length > 0 ? { notes } : {}
};
}
// src/connectors/mysql-family-introspection.ts
async function getSchemas(query) {
try {
const rows = await query(`
SELECT SCHEMA_NAME
FROM INFORMATION_SCHEMA.SCHEMATA
WHERE SCHEMA_NAME NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
ORDER BY SCHEMA_NAME
`);
return rows.map((row) => row.SCHEMA_NAME);
} catch (error) {
console.error("Error getting schemas:", error);
throw error;
}
}
async function getTables(query, schema) {
try {
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
const queryParams = schema ? [schema] : [];
const rows = await query(
`
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
${schemaClause}
AND TABLE_TYPE = 'BASE TABLE'
ORDER BY TABLE_NAME
`,
queryParams
);
return rows.map((row) => row.TABLE_NAME);
} catch (error) {
console.error("Error getting tables:", error);
throw error;
}
}
async function getViews(query, schema) {
try {
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
const queryParams = schema ? [schema] : [];
const rows = await query(
`
SELECT TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
${schemaClause}
AND TABLE_TYPE = 'VIEW'
ORDER BY TABLE_NAME
`,
queryParams
);
return rows.map((row) => row.TABLE_NAME);
} catch (error) {
console.error("Error getting views:", error);
throw error;
}
}
async function tableExists(query, tableName, schema) {
try {
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
const queryParams = schema ? [schema, tableName] : [tableName];
const rows = await query(
`
SELECT COUNT(*) AS COUNT
FROM INFORMATION_SCHEMA.TABLES
${schemaClause}
AND TABLE_NAME = ?
`,
queryParams
);
return rows[0].COUNT > 0;
} catch (error) {
console.error("Error checking if table exists:", error);
throw error;
}
}
async function getTableIndexes(query, tableName, schema) {
try {
const schemaClause = schema ? "TABLE_SCHEMA = ?" : "TABLE_SCHEMA = DATABASE()";
const queryParams = schema ? [schema, tableName] : [tableName];
const indexRows = await query(
`
SELECT
INDEX_NAME,
COLUMN_NAME,
NON_UNIQUE,
SEQ_IN_INDEX
FROM
INFORMATION_SCHEMA.STATISTICS
WHERE
${schemaClause}
AND TABLE_NAME = ?
ORDER BY
INDEX_NAME,
SEQ_IN_INDEX
`,
queryParams
);
const indexMap = /* @__PURE__ */ new Map();
for (const row of indexRows) {
const indexName = row.INDEX_NAME;
const columnName = row.COLUMN_NAME;
const isUnique = row.NON_UNIQUE === 0;
const isPrimary = indexName === "PRIMARY";
if (!indexMap.has(indexName)) {
indexMap.set(indexName, {
columns: [],
is_unique: isUnique,
is_primary: isPrimary
});
}
const indexInfo = indexMap.get(indexName);
indexInfo.columns.push(columnName);
}
const results = [];
indexMap.forEach((indexInfo, indexName) => {
results.push({
index_name: indexName,
column_names: indexInfo.columns,
is_unique: indexInfo.is_unique,
is_primary: indexInfo.is_primary
});
});
return results;
} catch (error) {
console.error("Error getting table indexes:", error);
throw error;
}
}
async function getTableSchema(query, tableName, schema) {
try {
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
const queryParams = schema ? [schema, tableName] : [tableName];
const rows = await query(
`
SELECT
COLUMN_NAME as column_name,
DATA_TYPE as data_type,
IS_NULLABLE as is_nullable,
COLUMN_DEFAULT as column_default,
COLUMN_COMMENT as description
FROM INFORMATION_SCHEMA.COLUMNS
${schemaClause}
AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
`,
queryParams
);
return rows.map((row) => ({
...row,
description: row.description || null
}));
} catch (error) {
console.error("Error getting table schema:", error);
throw error;
}
}
async function getTableComment(query, tableName, schema) {
try {
const schemaClause = schema ? "WHERE TABLE_SCHEMA = ?" : "WHERE TABLE_SCHEMA = DATABASE()";
const queryParams = schema ? [schema, tableName] : [tableName];
const rows = await query(
`
SELECT TABLE_COMMENT
FROM INFORMATION_SCHEMA.TABLES
${schemaClause}
AND TABLE_NAME = ?
`,
queryParams
);
if (rows.length > 0) {
return rows[0].TABLE_COMMENT || null;
}
return null;
} catch (error) {
return null;
}
}
async function getStoredProcedures(query, schema, routineType) {
try {
const schemaClause = schema ? "WHERE ROUTINE_SCHEMA = ?" : "WHERE ROUTINE_SCHEMA = DATABASE()";
const queryParams = schema ? [schema] : [];
let typeFilter = "";
if (routineType === "function") {
typeFilter = " AND ROUTINE_TYPE = 'FUNCTION'";
} else if (routineType === "procedure") {
typeFilter = " AND ROUTINE_TYPE = 'PROCEDURE'";
}
const rows = await query(
`
SELECT ROUTINE_NAME
FROM INFORMATION_SCHEMA.ROUTINES
${schemaClause}${typeFilter}
ORDER BY ROUTINE_NAME
`,
queryParams
);
return rows.map((row) => row.ROUTINE_NAME);
} catch (error) {
console.error("Error getting stored procedures:", error);
throw error;
}
}
async function getStoredProcedureDetail(query, procedureName, schema) {
try {
const schemaClause = schema ? "WHERE r.ROUTINE_SCHEMA = ?" : "WHERE r.ROUTINE_SCHEMA = DATABASE()";
const queryParams = schema ? [schema, procedureName] : [procedureName];
const rows = await query(
`
SELECT
r.ROUTINE_NAME AS procedure_name,
CASE
WHEN r.ROUTINE_TYPE = 'PROCEDURE' THEN 'procedure'
ELSE 'function'
END AS procedure_type,
LOWER(r.ROUTINE_TYPE) AS routine_type,
r.ROUTINE_DEFINITION,
r.DTD_IDENTIFIER AS return_type,
(
SELECT GROUP_CONCAT(
CONCAT(p.PARAMETER_NAME, ' ', p.PARAMETER_MODE, ' ', p.DATA_TYPE)
ORDER BY p.ORDINAL_POSITION
SEPARATOR ', '
)
FROM INFORMATION_SCHEMA.PARAMETERS p
WHERE p.SPECIFIC_SCHEMA = r.ROUTINE_SCHEMA
AND p.SPECIFIC_NAME = r.ROUTINE_NAME
AND p.PARAMETER_NAME IS NOT NULL
) AS parameter_list
FROM INFORMATION_SCHEMA.ROUTINES r
${schemaClause}
AND r.ROUTINE_NAME = ?
`,
queryParams
);
if (rows.length === 0) {
const schemaName = schema || "current schema";
throw new Error(`Stored procedure '${procedureName}' not found in ${schemaName}`);
}
const procedure = rows[0];
let definition = procedure.ROUTINE_DEFINITION;
try {
const schemaValue = schema || await getCurrentSchema(query);
const quotedSchema = quoteIdentifier(schemaValue, "mysql");
const quotedProcName = quoteIdentifier(procedureName, "mysql");
if (procedure.procedure_type === "procedure") {
try {
const defRows = await query(`
SHOW CREATE PROCEDURE ${quotedSchema}.${quotedProcName}
`);
if (defRows && defRows.length > 0) {
definition = defRows[0]["Create Procedure"];
}
} catch (err) {
console.error(`Error getting procedure definition with SHOW CREATE: ${err}`);
}
} else {
try {
const defRows = await query(`
SHOW CREATE FUNCTION ${quotedSchema}.${quotedProcName}
`);
if (defRows && defRows.length > 0) {
definition = defRows[0]["Create Function"];
}
} catch (innerErr) {
console.error(`Error getting function definition with SHOW CREATE: ${innerErr}`);
}
}
if (!definition) {
const bodyRows = await query(
`
SELECT ROUTINE_DEFINITION, ROUTINE_BODY
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ?
`,
[schemaValue, procedureName]
);
if (bodyRows && bodyRows.length > 0) {
if (bodyRows[0].ROUTINE_DEFINITION) {
definition = bodyRows[0].ROUTINE_DEFINITION;
} else if (bodyRows[0].ROUTINE_BODY) {
definition = bodyRows[0].ROUTINE_BODY;
}
}
}
} catch (error) {
console.error(`Error getting procedure/function details: ${error}`);
}
return {
procedure_name: procedure.procedure_name,
procedure_type: procedure.procedure_type,
language: "sql",
// MySQL/MariaDB procedures are generally in SQL
parameter_list: procedure.parameter_list || "",
return_type: procedure.routine_type === "function" ? procedure.return_type : void 0,
definition: definition || void 0
};
} catch (error) {
console.error("Error getting stored procedure detail:", error);
throw error;
}
}
async function getCurrentSchema(query) {
const rows = await query("SELECT DATABASE() AS DB");
return rows[0].DB;
}
async function getDefaultSchema(query) {
const rows = await query("SELECT DATABASE() AS DB");
return rows[0]?.DB ?? null;
}
// src/utils/dsn-database.ts
var MissingDatabaseError = class extends Error {
constructor(message) {
super(message);
this.name = "MissingDatabaseError";
}
};
function requireDatabaseInDSN(database, dsn, label) {
if (database) {
return;
}
throw new MissingDatabaseError(
`${label} DSN must name a database.
Provided: ${obfuscateDSNPassword(dsn)}
Add the database to the DSN, e.g. ...:3306/mydb
To work with several databases, define one [[sources]] entry per database in a TOML config file: https://dbhub.ai/config/toml`
);
}
// src/utils/multi-statement-result-parser.ts
function isMetadataObject(element) {
if (!element || typeof element !== "object" || Array.isArray(element)) {
return false;
}
return "affectedRows" in element || "insertId" in element || "fieldCount" in element || "warningStatus" in element;
}
function isMultiStatementResult(results) {
if (!Array.isArray(results) || results.length === 0) {
return false;
}
const firstElement = results[0];
return isMetadataObject(firstElement) || Array.isArray(firstElement);
}
function buildResultSetsFromMultiStatement(results, statementTexts) {
const sql = statementTexts?.length === results.length ? statementTexts : void 0;
return results.map((result, index) => {
if (Array.isArray(result)) {
return { sql: sql?.[index], rows: result, rowCount: result.length };
}
if (isMetadataObject(result)) {
return { sql: sql?.[index], rows: [], rowCount: result.affectedRows || 0 };
}
return { sql: sql?.[index], rows: [], rowCount: 0 };
});
}
function parseQueryResultSets(results, statementTexts) {
const singleStatementSql = statementTexts?.length === 1 ? statementTexts[0] : void 0;
if (isMetadataObject(results)) {
return [{ sql: singleStatementSql, rows: [], rowCount: results.affectedRows || 0 }];
}
if (!Array.isArray(results)) {
return [{ sql: singleStatementSql, rows: [], rowCount: 0 }];
}
if (isMultiStatementResult(results)) {
return buildResultSetsFromMultiStatement(results, statementTexts);
}
return [{ sql: singleStatementSql, rows: results, rowCount: results.length }];
}
// src/utils/readonly-transaction.ts
async function withReadOnlyTransaction(conn, readonly, supportsReadOnlyTransaction, execute) {
if (!readonly) {
return execute();
}
try {
await conn.query(
supportsReadOnlyTransaction ? "START TRANSACTION READ ONLY" : "START TRANSACTION"
);
const result = await execute();
await conn.query(supportsReadOnlyTransaction ? "COMMIT" : "ROLLBACK");
return result;
} catch (error) {
if (!isClientSideTimeout(error)) {
try {
await conn.query("ROLLBACK");
} catch {
}
}
throw error;
}
}
// src/utils/server-flavor.ts
function isTiDBVersion(version) {
return typeof version === "string" && /tidb/i.test(version);
}
export {
getMySQLFamilyHealthCheck,
getSchemas,
getTables,
getViews,
tableExists,
getTableIndexes,
getTableSchema,
getTableComment,
getStoredProcedures,
getStoredProcedureDetail,
getDefaultSchema,
MissingDatabaseError,
requireDatabaseInDSN,
parseQueryResultSets,
withReadOnlyTransaction,
isTiDBVersion
};