UNPKG

@bytebase/dbhub

Version:

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

501 lines (493 loc) • 16.4 kB
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 };