@bytebase/dbhub
Version:
Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite
831 lines (825 loc) • 27.1 kB
JavaScript
import {
CANCEL_QUERY_TIMEOUT_MS,
clientQueryTimeoutMs,
isClientSideTimeout
} from "./chunk-5XSBOK3W.js";
import {
toNullableNumber
} from "./chunk-FU2ZJE4E.js";
import {
quoteIdentifier
} from "./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/postgres/index.ts
import pg3 from "pg";
// src/connectors/postgres/failed-to-read-certificate.ts
var FailedToReadCertificate = class extends Error {
constructor(message) {
super(message);
this.name = "FailedToReadCertificate";
}
};
// src/connectors/postgres/type-parsers.ts
import pg from "pg";
var VERBATIM_DATE_TIME_OIDS = /* @__PURE__ */ new Set([
1082,
// date
1114,
// timestamp without time zone
1184,
// timestamp with time zone
1182,
// date[]
1115,
// timestamp[]
1185
// timestamptz[]
]);
var passthrough = (value) => value;
var postgresTypeParsers = {
getTypeParser(oid, format) {
if (VERBATIM_DATE_TIME_OIDS.has(oid) && (format === void 0 || format === "text")) {
return passthrough;
}
return pg.types.getTypeParser(oid, format);
}
};
// src/connectors/postgres/ssl-pems.ts
import fs from "fs";
import os from "os";
import path from "path";
import pg2 from "pg";
var CLIENT_CERT_SSL_MODES = ["require", "verify-ca", "verify-full"];
var SUPPORTED_SSL_MODES = ["disable", ...CLIENT_CERT_SSL_MODES];
var SSL_ROOT_CERT_LABEL = "SSL root certificate";
var SSL_CLIENT_CERT_LABEL = "SSL client certificate";
var SSL_CLIENT_KEY_LABEL = "SSL client key";
function resolvePemPath(filePath) {
return filePath.startsWith("~/") ? path.join(os.homedir(), filePath.slice(2)) : filePath;
}
function failedToRead(label, resolved, err) {
return new FailedToReadCertificate(
`Failed to read ${label} at '${resolved}': ${err instanceof Error ? err.message : String(err)}`
);
}
async function readPemFile(filePath, label) {
const resolved = resolvePemPath(filePath);
try {
return await fs.promises.readFile(resolved, "utf-8");
} catch (err) {
throw failedToRead(label, resolved, err);
}
}
function readPemFileSync(filePath, label) {
const resolved = resolvePemPath(filePath);
try {
return fs.readFileSync(resolved, "utf-8");
} catch (err) {
throw failedToRead(label, resolved, err);
}
}
function isEncryptedPemKey(pem) {
return pem.includes("ENCRYPTED PRIVATE KEY") || pem.includes("Proc-Type: 4,ENCRYPTED");
}
function encryptedKeyMessage(keyPath) {
return `SSL client key at '${keyPath}' is encrypted; encrypted private keys are not supported. Decrypt it first, e.g. 'openssl pkey -in client.key -out client-plain.key'`;
}
function loadPems(paths) {
const pems = {};
if (paths.sslrootcert !== void 0) {
pems.ca = readPemFileSync(paths.sslrootcert, SSL_ROOT_CERT_LABEL);
}
if (paths.sslcert !== void 0 && paths.sslkey !== void 0) {
const key = readPemFileSync(paths.sslkey, SSL_CLIENT_KEY_LABEL);
if (isEncryptedPemKey(key)) {
throw new FailedToReadCertificate(encryptedKeyMessage(paths.sslkey));
}
pems.key = key;
pems.cert = readPemFileSync(paths.sslcert, SSL_CLIENT_CERT_LABEL);
}
return pems;
}
function rotatingPemClient(paths) {
return class RotatingPemClient extends pg2.Client {
connect(callback) {
let pems;
try {
pems = loadPems(paths);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
if (callback) {
callback(error);
return;
}
return Promise.reject(error);
}
Object.assign(this.ssl, pems);
return callback ? super.connect(callback) : super.connect();
}
};
}
// src/connectors/postgres/index.ts
var { Pool } = pg3;
var PostgresDSNParser = class {
async parse(dsn, config) {
const connectionTimeoutSeconds = config?.connectionTimeoutSeconds;
const queryTimeoutSeconds = config?.queryTimeoutSeconds;
const poolMaxConnections = config?.poolMaxConnections;
if (!this.isValidDSN(dsn)) {
const obfuscatedDSN = obfuscateDSNPassword(dsn);
const expectedFormat = this.getSampleDSN();
throw new Error(
`Invalid PostgreSQL DSN format.
Provided: ${obfuscatedDSN}
Expected: ${expectedFormat}`
);
}
try {
const url = new SafeURL(dsn);
const poolConfig = {
host: url.hostname,
port: url.port ? parseInt(url.port) : 5432,
database: url.pathname ? url.pathname.substring(1) : "",
// Remove leading '/' if exists
user: url.username,
password: config?.password ?? url.password
};
let sslmode;
let sslrootcert;
let sslcert;
let sslkey;
url.forEachSearchParam((value, key) => {
if (key === "sslmode") {
sslmode = value;
} else if (key === "sslrootcert") {
sslrootcert = value;
} else if (key === "sslcert") {
sslcert = value;
} else if (key === "sslkey") {
sslkey = value;
}
});
if (sslmode !== void 0 && !SUPPORTED_SSL_MODES.includes(sslmode)) {
throw new Error(
`Unsupported sslmode '${sslmode}'. Valid values: ${SUPPORTED_SSL_MODES.join(", ")}`
);
}
if (sslrootcert !== void 0 && sslmode !== "verify-ca" && sslmode !== "verify-full") {
throw new FailedToReadCertificate(
`sslrootcert requires sslmode 'verify-ca' or 'verify-full' (got '${sslmode ?? "not set"}'). Use sslmode=verify-ca to verify the server certificate against it, or remove sslrootcert.`
);
}
if (sslcert === void 0 !== (sslkey === void 0)) {
throw new FailedToReadCertificate(
"sslcert and sslkey must be set together for client certificate authentication"
);
}
if (sslcert !== void 0 && !CLIENT_CERT_SSL_MODES.includes(sslmode ?? "")) {
throw new FailedToReadCertificate(
`sslcert/sslkey require sslmode to be one of ${CLIENT_CERT_SSL_MODES.join(", ")} (got '${sslmode ?? "not set"}')`
);
}
if (sslmode === "disable") {
poolConfig.ssl = false;
} else if (sslmode === "require") {
poolConfig.ssl = { rejectUnauthorized: false };
} else if (sslmode === "verify-ca" || sslmode === "verify-full") {
const sslConfig = { rejectUnauthorized: true };
if (sslmode === "verify-ca") {
sslConfig.checkServerIdentity = () => void 0;
}
if (sslrootcert) {
sslConfig.ca = await readPemFile(sslrootcert, SSL_ROOT_CERT_LABEL);
}
poolConfig.ssl = sslConfig;
}
if (sslcert !== void 0 && sslkey !== void 0) {
const key = await readPemFile(sslkey, SSL_CLIENT_KEY_LABEL);
if (isEncryptedPemKey(key)) {
throw new FailedToReadCertificate(encryptedKeyMessage(sslkey));
}
Object.assign(poolConfig.ssl, {
cert: await readPemFile(sslcert, SSL_CLIENT_CERT_LABEL),
key
});
}
if (sslrootcert !== void 0 || sslcert !== void 0) {
poolConfig.Client = rotatingPemClient({ sslrootcert, sslcert, sslkey });
}
if (connectionTimeoutSeconds !== void 0) {
poolConfig.connectionTimeoutMillis = connectionTimeoutSeconds * 1e3;
}
if (queryTimeoutSeconds !== void 0) {
const queryTimeoutMs = queryTimeoutSeconds * 1e3;
poolConfig.statement_timeout = queryTimeoutMs;
poolConfig.query_timeout = clientQueryTimeoutMs(queryTimeoutMs);
}
if (poolMaxConnections !== void 0) {
poolConfig.max = poolMaxConnections;
}
return poolConfig;
} catch (error) {
if (error instanceof FailedToReadCertificate) {
throw error;
}
const originalError = error instanceof Error ? error : new Error(String(error));
throw new Error(
`Failed to parse PostgreSQL DSN: ${originalError.message}`,
{ cause: originalError }
);
}
}
getSampleDSN() {
return "postgres://postgres:password@localhost:5432/postgres?sslmode=require";
}
isValidDSN(dsn) {
try {
return dsn.startsWith("postgres://") || dsn.startsWith("postgresql://");
} catch (error) {
return false;
}
}
};
var PostgresConnector = class _PostgresConnector {
constructor() {
this.id = "postgres";
this.name = "PostgreSQL";
this.dsnParser = new PostgresDSNParser();
this.pool = null;
// Source ID is set by ConnectorManager after cloning
this.sourceId = "default";
// Default schema for discovery methods (first entry from search_path, or "public")
this.defaultSchema = "public";
}
getId() {
return this.sourceId;
}
clone() {
return new _PostgresConnector();
}
async connect(dsn, initScript, config) {
this.defaultSchema = "public";
try {
const poolConfig = await this.dsnParser.parse(dsn, config);
poolConfig.types = postgresTypeParsers;
if (config?.readonly) {
poolConfig.options = (poolConfig.options || "") + " -c default_transaction_read_only=on";
}
if (config?.searchPath) {
const schemas = config.searchPath.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
if (schemas.length > 0) {
this.defaultSchema = schemas[0];
const quotedSchemas = schemas.map((s) => quoteIdentifier(s, "postgres"));
const optionsValue = quotedSchemas.join(",").replace(/\\/g, "\\\\").replace(/ /g, "\\ ");
poolConfig.options = (poolConfig.options || "") + ` -c search_path=${optionsValue}`;
}
}
this.pool = new Pool(poolConfig);
this.pool.on("error", (err) => {
console.error(
`PostgreSQL pool (source "${this.sourceId}"): idle connection dropped, will reconnect on next query:`,
err.message
);
});
const client = await this.pool.connect();
client.release();
} catch (err) {
if (this.pool) {
const pool = this.pool;
this.pool = null;
await closeQuietly(() => pool.end());
}
console.error("Failed to connect to PostgreSQL database:", err);
throw err;
}
}
async disconnect() {
if (this.pool) {
await this.pool.end();
this.pool = null;
}
}
async getSchemas() {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const result = await client.query(`
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
ORDER BY schema_name
`);
return result.rows.map((row) => row.schema_name);
} finally {
client.release();
}
}
async getTables(schema) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const schemaToUse = schema || this.defaultSchema;
const result = await client.query(
`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = $1
AND table_type IN ('BASE TABLE', 'FOREIGN')
ORDER BY table_name
`,
[schemaToUse]
);
return result.rows.map((row) => row.table_name);
} finally {
client.release();
}
}
async getViews(schema) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const schemaToUse = schema || this.defaultSchema;
const result = await client.query(
`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = $1
AND table_type = 'VIEW'
ORDER BY table_name
`,
[schemaToUse]
);
return result.rows.map((row) => row.table_name);
} finally {
client.release();
}
}
async tableExists(tableName, schema) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const schemaToUse = schema || this.defaultSchema;
const result = await client.query(
`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = $1
AND table_name = $2
)
`,
[schemaToUse, tableName]
);
return result.rows[0].exists;
} finally {
client.release();
}
}
async getTableIndexes(tableName, schema) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const schemaToUse = schema || this.defaultSchema;
const result = await client.query(
`
SELECT
i.relname as index_name,
array_agg(a.attname::text) as column_names,
ix.indisunique as is_unique,
ix.indisprimary as is_primary
FROM
pg_class t,
pg_class i,
pg_index ix,
pg_attribute a,
pg_namespace ns
WHERE
t.oid = ix.indrelid
AND i.oid = ix.indexrelid
AND a.attrelid = t.oid
AND a.attnum = ANY(ix.indkey)
AND t.relkind IN ('r','p')
AND t.relname = $1
AND ns.oid = t.relnamespace
AND ns.nspname = $2
GROUP BY
i.relname,
ix.indisunique,
ix.indisprimary
ORDER BY
i.relname
`,
[tableName, schemaToUse]
);
return result.rows.map((row) => ({
index_name: row.index_name,
column_names: row.column_names,
is_unique: row.is_unique,
is_primary: row.is_primary
}));
} finally {
client.release();
}
}
async getTableSchema(tableName, schema) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const schemaToUse = schema || this.defaultSchema;
const result = await client.query(
`
SELECT
c.column_name,
c.data_type,
c.is_nullable,
c.column_default,
pgd.description
FROM information_schema.columns c
LEFT JOIN pg_catalog.pg_namespace nsp
ON nsp.nspname = c.table_schema
LEFT JOIN pg_catalog.pg_class cls
ON cls.relnamespace = nsp.oid
AND cls.relname = c.table_name
LEFT JOIN pg_catalog.pg_description pgd
ON pgd.objoid = cls.oid
AND pgd.objsubid = c.ordinal_position
WHERE c.table_schema = $1
AND c.table_name = $2
ORDER BY c.ordinal_position
`,
[schemaToUse, tableName]
);
return result.rows;
} finally {
client.release();
}
}
async getTableRowCount(tableName, schema) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const schemaToUse = schema || this.defaultSchema;
const result = await client.query(
`
SELECT c.reltuples::bigint as count
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = $1
AND n.nspname = $2
AND c.relkind IN ('r','p','m','f')
`,
[tableName, schemaToUse]
);
if (result.rows.length > 0) {
const count = Number(result.rows[0].count);
return count >= 0 ? count : null;
}
return null;
} finally {
client.release();
}
}
async getTableComment(tableName, schema) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const schemaToUse = schema || this.defaultSchema;
const result = await client.query(
`
SELECT obj_description(c.oid) as table_comment
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = $1
AND n.nspname = $2
AND c.relkind IN ('r','p','m','f','v')
`,
[tableName, schemaToUse]
);
if (result.rows.length > 0) {
return result.rows[0].table_comment || null;
}
return null;
} finally {
client.release();
}
}
async getHealthCheck() {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const [connectionsResult, maxConnectionsResult, bufferCacheResult] = await Promise.all([
client.query(`
SELECT
count(*) AS total,
count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE state = 'idle') AS idle,
count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_transaction,
count(*) FILTER (WHERE state = 'idle in transaction (aborted)') AS idle_in_transaction_aborted,
max(EXTRACT(EPOCH FROM (now() - state_change)))
FILTER (WHERE state = 'idle in transaction') AS longest_idle_in_transaction_seconds,
max(EXTRACT(EPOCH FROM (now() - query_start)))
FILTER (WHERE state = 'active') AS longest_active_query_seconds
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
`),
client.query(`SHOW max_connections`),
client.query(
`
SELECT
blks_hit,
blks_read,
CASE WHEN (blks_hit + blks_read) = 0 THEN NULL
ELSE round(100.0 * blks_hit / (blks_hit + blks_read), 2)
END AS hit_ratio_pct
FROM pg_stat_database
WHERE datname = current_database()
`
)
]);
const conn = connectionsResult.rows[0];
const cache = bufferCacheResult.rows[0];
return {
connections: {
total: Number(conn.total),
active: Number(conn.active),
idle: Number(conn.idle),
idleInTransaction: Number(conn.idle_in_transaction),
idleInTransactionAborted: Number(conn.idle_in_transaction_aborted),
maxConnections: Number(maxConnectionsResult.rows[0].max_connections),
longestIdleInTransactionSeconds: toNullableNumber(conn.longest_idle_in_transaction_seconds),
longestActiveQuerySeconds: toNullableNumber(conn.longest_active_query_seconds)
},
bufferCache: {
hitRatioPct: toNullableNumber(cache.hit_ratio_pct),
blocksHit: Number(cache.blks_hit),
blocksRead: Number(cache.blks_read)
}
};
} finally {
client.release();
}
}
async getStoredProcedures(schema, routineType) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const schemaToUse = schema || this.defaultSchema;
const params = [schemaToUse];
let typeFilter = "";
if (routineType === "function") {
typeFilter = " AND routine_type = 'FUNCTION'";
} else if (routineType === "procedure") {
typeFilter = " AND routine_type = 'PROCEDURE'";
}
const result = await client.query(
`
SELECT
routine_name
FROM information_schema.routines
WHERE routine_schema = $1${typeFilter}
ORDER BY routine_name
`,
params
);
return result.rows.map((row) => row.routine_name);
} finally {
client.release();
}
}
async getStoredProcedureDetail(procedureName, schema) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
try {
const schemaToUse = schema || this.defaultSchema;
const result = await client.query(
`
SELECT
routine_name as procedure_name,
routine_type,
CASE WHEN routine_type = 'PROCEDURE' THEN 'procedure' ELSE 'function' END as procedure_type,
external_language as language,
data_type as return_type,
routine_definition as definition,
(
SELECT string_agg(
parameter_name || ' ' ||
parameter_mode || ' ' ||
data_type,
', '
)
FROM information_schema.parameters
WHERE specific_schema = $1
AND specific_name = $2
AND parameter_name IS NOT NULL
) as parameter_list
FROM information_schema.routines
WHERE routine_schema = $1
AND routine_name = $2
`,
[schemaToUse, procedureName]
);
if (result.rows.length === 0) {
throw new Error(`Stored procedure '${procedureName}' not found in schema '${schemaToUse}'`);
}
const procedure = result.rows[0];
let definition = procedure.definition;
try {
const oidResult = await client.query(
`
SELECT p.oid, p.prosrc
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE p.proname = $1
AND n.nspname = $2
`,
[procedureName, schemaToUse]
);
if (oidResult.rows.length > 0) {
if (!definition) {
const oid = oidResult.rows[0].oid;
const defResult = await client.query(`SELECT pg_get_functiondef($1)`, [oid]);
if (defResult.rows.length > 0) {
definition = defResult.rows[0].pg_get_functiondef;
} else {
definition = oidResult.rows[0].prosrc;
}
}
}
} catch (err) {
console.error(`Error getting procedure definition: ${err}`);
}
return {
procedure_name: procedure.procedure_name,
procedure_type: procedure.procedure_type,
language: procedure.language || "sql",
parameter_list: procedure.parameter_list || "",
return_type: procedure.return_type !== "void" ? procedure.return_type : void 0,
definition: definition || void 0
};
} finally {
client.release();
}
}
async executeSQL(sql, options, parameters) {
if (!this.pool) {
throw new Error("Not connected to database");
}
const client = await this.pool.connect();
const backendPid = client.processID;
let isClientDiscarded = false;
try {
const statements = splitSQLStatements(sql, "postgres");
if (statements.length === 1) {
const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
statements[0],
options.maxRows,
"postgres"
);
if (options.readonly) {
await client.query("BEGIN READ ONLY");
try {
const result2 = parameters && parameters.length > 0 ? await client.query(processedStatement, parameters) : await client.query(processedStatement);
await client.query("COMMIT");
const resultSet2 = {
sql: statements[0],
rows: result2.rows,
rowCount: result2.rowCount ?? result2.rows.length
};
SQLRowLimiter.flagTruncation(resultSet2, options.maxRows, probeApplied);
return { resultSets: [resultSet2] };
} catch (error) {
await this.rollbackQuietly(client, error);
throw error;
}
}
let result;
if (parameters && parameters.length > 0) {
result = await client.query(processedStatement, parameters);
} else {
result = await client.query(processedStatement);
}
const resultSet = {
sql: statements[0],
rows: result.rows,
rowCount: result.rowCount ?? result.rows.length
};
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
return { resultSets: [resultSet] };
} else {
if (parameters && parameters.length > 0) {
throw new Error("Parameters are not supported for multi-statement queries in PostgreSQL");
}
const resultSets = [];
await client.query(options.readonly ? "BEGIN READ ONLY" : "BEGIN");
try {
for (let statement of statements) {
const { sql: processedStatement, probeApplied } = SQLRowLimiter.applyMaxRowsWithTruncationProbe(
statement,
options.maxRows,
"postgres"
);
const result = await client.query(processedStatement);
const resultSet = {
sql: statement,
rows: result.rows ?? [],
rowCount: result.rowCount ?? result.rows?.length ?? 0
};
SQLRowLimiter.flagTruncation(resultSet, options.maxRows, probeApplied);
resultSets.push(resultSet);
}
await client.query("COMMIT");
} catch (error) {
await this.rollbackQuietly(client, error);
throw error;
}
return { resultSets };
}
} catch (error) {
if (isClientSideTimeout(error)) {
isClientDiscarded = true;
client.release(true);
await this.cancelBackend(backendPid);
}
throw error;
} finally {
if (!isClientDiscarded) {
client.release();
}
}
}
/**
* Best-effort rollback after a failed statement, so a failed ROLLBACK (e.g.
* a dropped connection) cannot mask the original error. Skipped for a
* client-side timeout: the client is still busy with the abandoned
* statement, so the ROLLBACK would queue behind it (see isClientSideTimeout).
* executeSQL discards that client instead.
*/
async rollbackQuietly(client, error) {
if (isClientSideTimeout(error)) {
return;
}
try {
await client.query("ROLLBACK");
} catch {
}
}
/**
* Best-effort server-side cancel for a statement abandoned by the
* client-side timeout, sent over a separate pooled connection. Closing the
* abandoned client's socket is not enough: PostgreSQL only notices a
* vanished client when it next writes to it, so a statement that is
* sleeping or computing would run to completion.
*
* Bounded by its own short timeout (CANCEL_QUERY_TIMEOUT_MS), independent
* of the user's query_timeout. pool.query discards its client on error, so
* a cancel that itself times out does not leak a stuck client.
*/
async cancelBackend(backendPid) {
if (!this.pool || backendPid === null) return;
try {
const cancel = {
text: "SELECT pg_cancel_backend($1)",
values: [backendPid],
query_timeout: CANCEL_QUERY_TIMEOUT_MS
};
await this.pool.query(cancel);
} catch {
}
}
};
var postgresConnector = new PostgresConnector();
ConnectorRegistry.register(postgresConnector);
export {
PostgresConnector
};