UNPKG

@bytebase/dbhub

Version:

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

180 lines (173 loc) • 6.71 kB
import { computeHitRatioPct, toNullableNumber } from "./chunk-FU2ZJE4E.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/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 function isClientSideTimeout(error) { return typeof error === "object" && error !== null && error.code === "PROTOCOL_SEQUENCE_TIMEOUT"; } 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, MissingDatabaseError, requireDatabaseInDSN, parseQueryResultSets, isClientSideTimeout, withReadOnlyTransaction, isTiDBVersion };