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