@bytebase/dbhub
Version:
Token-efficient database MCP server for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, SQLite
183 lines (180 loc) • 5.61 kB
JavaScript
import {
stripCommentsAndStrings
} from "./chunk-IXC4NQGC.js";
// src/utils/allowed-keywords.ts
var allowedKeywords = {
postgres: ["select", "with", "explain", "show"],
mysql: ["select", "with", "explain", "show", "describe", "desc"],
mariadb: ["select", "with", "explain", "show", "describe", "desc"],
sqlite: ["select", "with", "explain", "pragma"],
// SQL Server has no native EXPLAIN statement; the connector translates a
// leading `EXPLAIN` into a SET SHOWPLAN_XML request (see SQLServerConnector).
sqlserver: ["select", "with", "explain"],
// Oracle's native form is `EXPLAIN PLAN FOR <stmt>`; the connector accepts
// both that and a bare `EXPLAIN <stmt>` and reads the plan back through
// DBMS_XPLAN (see OracleConnector.explainQuery). EXPLAIN PLAN only parses
// the statement, it never executes it.
oracle: ["select", "with", "explain"]
};
var mutatingKeywords = [
"insert",
"update",
"delete",
"drop",
"alter",
"create",
"truncate",
"merge",
"grant",
"revoke",
"rename"
];
var mutatingPattern = new RegExp(
`\\b(?:${mutatingKeywords.join("|")})\\b`,
"i"
);
var mutatingPatternWithReplace = new RegExp(
`\\b(?:${mutatingKeywords.join("|")}|replace\\s+(?:(?:low_priority|delayed)\\s+)?into)\\b`,
"i"
);
var sqlServerDynamicSqlKeywords = [
"execute",
"exec",
"sp_executesql",
"xp_cmdshell"
];
var sqlServerDynamicSqlPattern = new RegExp(
`\\b(?:${sqlServerDynamicSqlKeywords.join("|")})\\b`,
"i"
);
var sqlServerPassThroughKeywords = [
"openquery",
"openrowset",
"opendatasource"
];
var sqlServerPassThroughPattern = new RegExp(
`\\b(?:${sqlServerPassThroughKeywords.join("|")})\\s*\\(`,
"i"
);
var escapeHatchFunctionKeywords = {
mysql: ["load_file", "get_lock", "release_lock", "release_all_locks"],
mariadb: ["load_file", "get_lock", "release_lock", "release_all_locks"],
postgres: ["pg_read_file", "pg_read_binary_file", "pg_ls_dir", "set_config"],
sqlserver: sqlServerPassThroughKeywords,
// Oracle packages a SELECT can call to reach outside the database, matched
// as `package.member(` (see escapeHatchCallSuffix).
oracle: [
"utl_http",
"utl_tcp",
"utl_smtp",
"utl_inaddr",
"utl_file",
"dbms_ldap",
"dbms_sql",
"dbms_scheduler",
"dbms_java",
"dbms_lob",
"dbms_xslprocessor",
"dbms_advisor"
]
};
var escapeHatchCallSuffix = {
oracle: "\\s*\\.\\s*[a-z_][a-z0-9_$#]*\\s*\\("
};
var DEFAULT_CALL_SUFFIX = "\\s*\\(";
var escapeHatchFunctionPatterns = Object.fromEntries(
Object.entries(escapeHatchFunctionKeywords).map(([type, keywords]) => [
type,
new RegExp(
`\\b(?:${keywords.join("|")})${escapeHatchCallSuffix[type] ?? DEFAULT_CALL_SUFFIX}`,
"i"
)
])
);
function hasEscapeHatchFunction(strippedSQL, connectorType) {
const pattern = escapeHatchFunctionPatterns[connectorType];
return pattern !== void 0 && pattern.test(strippedSQL);
}
var mutatingPatternSqlServer = new RegExp(
`\\b(?:${[...mutatingKeywords, ...sqlServerDynamicSqlKeywords].join("|")})\\b`,
"i"
);
var mutatingPatterns = {
postgres: mutatingPattern,
mysql: mutatingPatternWithReplace,
mariadb: mutatingPatternWithReplace,
sqlite: mutatingPatternWithReplace,
sqlserver: mutatingPatternSqlServer,
oracle: mutatingPattern
};
function hasMutatingKeyword(strippedSQL, connectorType) {
return (mutatingPatterns[connectorType] ?? mutatingPattern).test(strippedSQL);
}
var selectIntoPattern = /\bselect\b[\s\S]+\binto\b/i;
var sqliteReadOnlyArgPragmas = /* @__PURE__ */ new Set([
"table_info",
"index_info",
"index_list",
"foreign_key_list"
]);
var sqlitePragmaParenPattern = /^pragma\s+(?:[a-z0-9_]+\.)?([a-z0-9_]+)\s*\(/;
var explainAnalyzePattern = /^explain\s+(?:\([^)]*\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)[^)]*\)|\banalyze\b(?!\s*(?:=\s*)?(?:false|off|0)\b)(?:\s+verbose\b)?)/i;
function isReadOnlySQL(sql, connectorType) {
return checkReadOnly(
stripCommentsAndStrings(sql, connectorType).trim().toLowerCase(),
connectorType
);
}
function getFirstKeyword(sql, connectorType) {
const cleaned = stripCommentsAndStrings(sql, connectorType).trim();
return cleaned.match(/\S+/)?.[0]?.toLowerCase() ?? "";
}
function checkReadOnly(cleanedSQL, connectorType) {
if (!cleanedSQL) {
return false;
}
const firstWord = cleanedSQL.match(/\S+/)?.[0] ?? "";
const keywordList = allowedKeywords[connectorType] || [];
if (!keywordList.includes(firstWord)) {
return false;
}
if (hasEscapeHatchFunction(cleanedSQL, connectorType)) {
return false;
}
if (firstWord === "with" && hasMutatingKeyword(cleanedSQL, connectorType)) {
return false;
}
if (firstWord === "pragma" && connectorType === "sqlite") {
if (cleanedSQL.includes("=")) {
return false;
}
const parenMatch = cleanedSQL.match(sqlitePragmaParenPattern);
if (parenMatch && !sqliteReadOnlyArgPragmas.has(parenMatch[1])) {
return false;
}
}
if ((firstWord === "select" || firstWord === "with") && selectIntoPattern.test(cleanedSQL)) {
return false;
}
if (firstWord === "explain") {
const m = explainAnalyzePattern.exec(cleanedSQL);
if (m) {
const afterExplain = cleanedSQL.slice(m[0].length).trim();
if (afterExplain && !checkReadOnly(afterExplain, connectorType)) {
return false;
}
}
}
return true;
}
export {
allowedKeywords,
sqlServerDynamicSqlKeywords,
sqlServerDynamicSqlPattern,
sqlServerPassThroughKeywords,
sqlServerPassThroughPattern,
hasEscapeHatchFunction,
hasMutatingKeyword,
isReadOnlySQL,
getFirstKeyword
};