mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-22 08:50:17 +08:00
✨ feat(sphinx): 新增Sphinx数据源并补齐对象能力兼容链路
- 新增 SphinxDB 驱动注册并复用 MySQL 协议连接 - 前端新增 sphinx 连接类型与默认端口 9306 - 函数/视图/触发器改为多语句回退查询与版本探测提示 - 后端对不支持能力返回稳定降级结果
This commit is contained in:
@@ -11,6 +11,7 @@ const { Text } = Typography;
|
||||
const getDefaultPortByType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'mysql': return 3306;
|
||||
case 'sphinx': return 9306;
|
||||
case 'postgres': return 5432;
|
||||
case 'redis': return 6379;
|
||||
case 'tdengine': return 6041;
|
||||
@@ -187,17 +188,18 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === 'mysql' || type === 'mariadb') {
|
||||
if (type === 'mysql' || type === 'mariadb' || type === 'sphinx') {
|
||||
const mysqlDefaultPort = getDefaultPortByType(type);
|
||||
const parsed = parseMultiHostUri(trimmedUri, 'mysql');
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
const hostList = normalizeAddressList(parsed.hosts, 3306);
|
||||
const primary = parseHostPort(hostList[0] || 'localhost:3306', 3306);
|
||||
const hostList = normalizeAddressList(parsed.hosts, mysqlDefaultPort);
|
||||
const primary = parseHostPort(hostList[0] || `localhost:${mysqlDefaultPort}`, mysqlDefaultPort);
|
||||
const timeoutValue = Number(parsed.params.get('timeout'));
|
||||
return {
|
||||
host: primary?.host || 'localhost',
|
||||
port: primary?.port || 3306,
|
||||
port: primary?.port || mysqlDefaultPort,
|
||||
user: parsed.username,
|
||||
password: parsed.password,
|
||||
database: parsed.database || '',
|
||||
@@ -259,8 +261,9 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
});
|
||||
|
||||
const getUriPlaceholder = () => {
|
||||
if (dbType === 'mysql' || dbType === 'mariadb') {
|
||||
return 'mysql://user:pass@127.0.0.1:3306,127.0.0.2:3306/db_name?topology=replica';
|
||||
if (dbType === 'mysql' || dbType === 'mariadb' || dbType === 'sphinx') {
|
||||
const defaultPort = getDefaultPortByType(dbType);
|
||||
return `mysql://user:pass@127.0.0.1:${defaultPort},127.0.0.2:${defaultPort}/db_name?topology=replica`;
|
||||
}
|
||||
if (dbType === 'mongodb') {
|
||||
return 'mongodb+srv://user:pass@cluster0.example.com/db_name?authSource=admin&authMechanism=SCRAM-SHA-256';
|
||||
@@ -281,12 +284,12 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
? `${encodeURIComponent(user)}${password ? `:${encodeURIComponent(password)}` : ''}@`
|
||||
: '';
|
||||
|
||||
if (type === 'mysql' || type === 'mariadb') {
|
||||
const primary = toAddress(host, port, 3306);
|
||||
if (type === 'mysql' || type === 'mariadb' || type === 'sphinx') {
|
||||
const primary = toAddress(host, port, defaultPort);
|
||||
const replicas = values.mysqlTopology === 'replica'
|
||||
? normalizeAddressList(values.mysqlReplicaHosts, 3306)
|
||||
? normalizeAddressList(values.mysqlReplicaHosts, defaultPort)
|
||||
: [];
|
||||
const hosts = normalizeAddressList([primary, ...replicas], 3306);
|
||||
const hosts = normalizeAddressList([primary, ...replicas], defaultPort);
|
||||
const params = new URLSearchParams();
|
||||
if (hosts.length > 1 || values.mysqlTopology === 'replica') {
|
||||
params.set('topology', 'replica');
|
||||
@@ -410,7 +413,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
);
|
||||
const primaryHost = primaryAddress?.host || String(config.host || 'localhost');
|
||||
const primaryPort = primaryAddress?.port || Number(config.port || defaultPort);
|
||||
const mysqlReplicaHosts = (configType === 'mysql' || configType === 'mariadb') ? normalizedHosts.slice(1) : [];
|
||||
const mysqlReplicaHosts = (configType === 'mysql' || configType === 'mariadb' || configType === 'sphinx') ? normalizedHosts.slice(1) : [];
|
||||
const mongoHosts = configType === 'mongodb' ? normalizedHosts.slice(1) : [];
|
||||
const mysqlIsReplica = String(config.topology || '').toLowerCase() === 'replica' || mysqlReplicaHosts.length > 0;
|
||||
const mongoIsReplica = String(config.topology || '').toLowerCase() === 'replica' || mongoHosts.length > 0 || !!config.replicaSet;
|
||||
@@ -638,7 +641,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
? mergedValues.savePassword !== false
|
||||
: true;
|
||||
|
||||
if (type === 'mysql' || type === 'mariadb') {
|
||||
if (type === 'mysql' || type === 'mariadb' || type === 'sphinx') {
|
||||
const replicas = mergedValues.mysqlTopology === 'replica'
|
||||
? normalizeAddressList(mergedValues.mysqlReplicaHosts, defaultPort)
|
||||
: [];
|
||||
@@ -753,6 +756,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
{ label: '关系型数据库', items: [
|
||||
{ key: 'mysql', name: 'MySQL', icon: <ConsoleSqlOutlined style={{ fontSize: 24, color: '#00758F' }} /> },
|
||||
{ key: 'mariadb', name: 'MariaDB', icon: <ConsoleSqlOutlined style={{ fontSize: 24, color: '#003545' }} /> },
|
||||
{ key: 'sphinx', name: 'Sphinx', icon: <ConsoleSqlOutlined style={{ fontSize: 24, color: '#2F5D62' }} /> },
|
||||
{ key: 'postgres', name: 'PostgreSQL', icon: <DatabaseOutlined style={{ fontSize: 24, color: '#336791' }} /> },
|
||||
{ key: 'sqlserver', name: 'SQL Server', icon: <DatabaseOutlined style={{ fontSize: 24, color: '#CC2927' }} /> },
|
||||
{ key: 'sqlite', name: 'SQLite', icon: <FileTextOutlined style={{ fontSize: 24, color: '#003B57' }} /> },
|
||||
@@ -920,7 +924,7 @@ const ConnectionModal: React.FC<{ open: boolean; onClose: () => void; initialVal
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(dbType === 'mysql' || dbType === 'mariadb') && (
|
||||
{(dbType === 'mysql' || dbType === 'mariadb' || dbType === 'sphinx') && (
|
||||
<>
|
||||
<Form.Item name="mysqlTopology" label="连接模式">
|
||||
<Select
|
||||
|
||||
@@ -25,11 +25,19 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
if (type === 'custom') {
|
||||
return String(conn?.config?.driver || '').trim().toLowerCase();
|
||||
}
|
||||
if (type === 'mariadb') return 'mysql';
|
||||
if (type === 'mariadb' || type === 'sphinx') return 'mysql';
|
||||
if (type === 'dameng') return 'dm';
|
||||
return type;
|
||||
};
|
||||
|
||||
const isSphinxConnection = (conn: any): boolean => {
|
||||
const type = String(conn?.config?.type || '').trim().toLowerCase();
|
||||
if (type === 'sphinx') return true;
|
||||
if (type !== 'custom') return false;
|
||||
const driver = String(conn?.config?.driver || '').trim().toLowerCase();
|
||||
return driver === 'sphinx' || driver === 'sphinxql';
|
||||
};
|
||||
|
||||
const parseSchemaAndName = (fullName: string): { schema: string; name: string } => {
|
||||
const raw = String(fullName || '').trim();
|
||||
const idx = raw.lastIndexOf('.');
|
||||
@@ -39,40 +47,46 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
return { schema: '', name: raw };
|
||||
};
|
||||
|
||||
const buildShowViewQuery = (dialect: string, viewName: string, dbName: string): string => {
|
||||
const buildShowViewQueries = (dialect: string, viewName: string, dbName: string): string[] => {
|
||||
const { schema, name } = parseSchemaAndName(viewName);
|
||||
const safeName = escapeSQLLiteral(name);
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
return `SHOW CREATE VIEW \`${name.replace(/`/g, '``')}\``;
|
||||
return [
|
||||
`SHOW CREATE VIEW \`${name.replace(/`/g, '``')}\``,
|
||||
safeDbName
|
||||
? `SELECT VIEW_DEFINITION AS view_definition FROM information_schema.views WHERE table_schema = '${safeDbName}' AND table_name = '${safeName}' LIMIT 1`
|
||||
: '',
|
||||
`SHOW CREATE TABLE \`${name.replace(/`/g, '``')}\``,
|
||||
].filter(Boolean);
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase': {
|
||||
const schemaRef = schema || 'public';
|
||||
return `SELECT pg_get_viewdef('${escapeSQLLiteral(schemaRef)}.${safeName}'::regclass, true) AS view_definition`;
|
||||
return [`SELECT pg_get_viewdef('${escapeSQLLiteral(schemaRef)}.${safeName}'::regclass, true) AS view_definition`];
|
||||
}
|
||||
case 'sqlserver':
|
||||
return `SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(viewName)}')) AS view_definition`;
|
||||
return [`SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(viewName)}')) AS view_definition`];
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
if (schema) {
|
||||
return `SELECT TEXT AS view_definition FROM ALL_VIEWS WHERE OWNER = '${escapeSQLLiteral(schema).toUpperCase()}' AND VIEW_NAME = '${safeName.toUpperCase()}'`;
|
||||
return [`SELECT TEXT AS view_definition FROM ALL_VIEWS WHERE OWNER = '${escapeSQLLiteral(schema).toUpperCase()}' AND VIEW_NAME = '${safeName.toUpperCase()}'`];
|
||||
}
|
||||
if (safeDbName) {
|
||||
return `SELECT TEXT AS view_definition FROM ALL_VIEWS WHERE OWNER = '${safeDbName.toUpperCase()}' AND VIEW_NAME = '${safeName.toUpperCase()}'`;
|
||||
return [`SELECT TEXT AS view_definition FROM ALL_VIEWS WHERE OWNER = '${safeDbName.toUpperCase()}' AND VIEW_NAME = '${safeName.toUpperCase()}'`];
|
||||
}
|
||||
return `SELECT TEXT AS view_definition FROM USER_VIEWS WHERE VIEW_NAME = '${safeName.toUpperCase()}'`;
|
||||
return [`SELECT TEXT AS view_definition FROM USER_VIEWS WHERE VIEW_NAME = '${safeName.toUpperCase()}'`];
|
||||
case 'sqlite':
|
||||
return `SELECT sql AS view_definition FROM sqlite_master WHERE type='view' AND name='${safeName}'`;
|
||||
return [`SELECT sql AS view_definition FROM sqlite_master WHERE type='view' AND name='${safeName}'`];
|
||||
default:
|
||||
return `-- 暂不支持该数据库类型的视图定义查看`;
|
||||
return [`-- 暂不支持该数据库类型的视图定义查看`];
|
||||
}
|
||||
};
|
||||
|
||||
const buildShowRoutineQuery = (dialect: string, routineName: string, routineType: string, dbName: string): string => {
|
||||
const buildShowRoutineQueries = (dialect: string, routineName: string, routineType: string, dbName: string): string[] => {
|
||||
const { schema, name } = parseSchemaAndName(routineName);
|
||||
const safeName = escapeSQLLiteral(name);
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
@@ -80,31 +94,97 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
return `SHOW CREATE ${upperType} \`${name.replace(/`/g, '``')}\``;
|
||||
return [
|
||||
`SHOW CREATE ${upperType} \`${name.replace(/`/g, '``')}\``,
|
||||
safeDbName
|
||||
? `SELECT ROUTINE_DEFINITION AS routine_definition, ROUTINE_TYPE AS routine_type FROM information_schema.routines WHERE routine_schema = '${safeDbName}' AND routine_name = '${safeName}' LIMIT 1`
|
||||
: '',
|
||||
upperType === 'PROCEDURE'
|
||||
? `SHOW PROCEDURE STATUS LIKE '${safeName}'`
|
||||
: `SHOW FUNCTION STATUS LIKE '${safeName}'`,
|
||||
].filter(Boolean);
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase': {
|
||||
const schemaRef = schema || 'public';
|
||||
return `SELECT pg_get_functiondef(p.oid) AS routine_definition FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${escapeSQLLiteral(schemaRef)}' AND p.proname = '${safeName}' LIMIT 1`;
|
||||
return [`SELECT pg_get_functiondef(p.oid) AS routine_definition FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = '${escapeSQLLiteral(schemaRef)}' AND p.proname = '${safeName}' LIMIT 1`];
|
||||
}
|
||||
case 'sqlserver':
|
||||
return `SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(routineName)}')) AS routine_definition`;
|
||||
return [`SELECT OBJECT_DEFINITION(OBJECT_ID('${escapeSQLLiteral(routineName)}')) AS routine_definition`];
|
||||
case 'oracle':
|
||||
case 'dm': {
|
||||
const owner = schema ? escapeSQLLiteral(schema).toUpperCase() : (safeDbName ? safeDbName.toUpperCase() : '');
|
||||
if (owner) {
|
||||
return `SELECT TEXT FROM ALL_SOURCE WHERE OWNER = '${owner}' AND NAME = '${safeName.toUpperCase()}' AND TYPE = '${upperType}' ORDER BY LINE`;
|
||||
return [`SELECT TEXT FROM ALL_SOURCE WHERE OWNER = '${owner}' AND NAME = '${safeName.toUpperCase()}' AND TYPE = '${upperType}' ORDER BY LINE`];
|
||||
}
|
||||
return `SELECT TEXT FROM USER_SOURCE WHERE NAME = '${safeName.toUpperCase()}' AND TYPE = '${upperType}' ORDER BY LINE`;
|
||||
return [`SELECT TEXT FROM USER_SOURCE WHERE NAME = '${safeName.toUpperCase()}' AND TYPE = '${upperType}' ORDER BY LINE`];
|
||||
}
|
||||
case 'sqlite':
|
||||
return `-- SQLite 不支持存储函数/存储过程`;
|
||||
return [`-- SQLite 不支持存储函数/存储过程`];
|
||||
default:
|
||||
return `-- 暂不支持该数据库类型的函数/存储过程定义查看`;
|
||||
return [`-- 暂不支持该数据库类型的函数/存储过程定义查看`];
|
||||
}
|
||||
};
|
||||
|
||||
const runQueryCandidates = async (
|
||||
config: Record<string, any>,
|
||||
dbName: string,
|
||||
queries: string[]
|
||||
): Promise<{ success: boolean; data: any[]; message?: string }> => {
|
||||
let lastMessage = '';
|
||||
let hasSuccessfulQuery = false;
|
||||
for (const query of queries) {
|
||||
const sql = String(query || '').trim();
|
||||
if (!sql) continue;
|
||||
try {
|
||||
const result = await DBQuery(config as any, dbName, sql);
|
||||
if (!result.success || !Array.isArray(result.data)) {
|
||||
lastMessage = result.message || lastMessage;
|
||||
continue;
|
||||
}
|
||||
hasSuccessfulQuery = true;
|
||||
if (result.data.length > 0) {
|
||||
return { success: true, data: result.data };
|
||||
}
|
||||
} catch (error: any) {
|
||||
lastMessage = error?.message || String(error);
|
||||
}
|
||||
}
|
||||
if (hasSuccessfulQuery) {
|
||||
return { success: true, data: [] };
|
||||
}
|
||||
return { success: false, data: [], message: lastMessage };
|
||||
};
|
||||
|
||||
const getVersionHint = async (config: Record<string, any>, dbName: string): Promise<string> => {
|
||||
const candidates = [
|
||||
`SELECT VERSION() AS version`,
|
||||
`SHOW VARIABLES LIKE 'version'`,
|
||||
];
|
||||
for (const query of candidates) {
|
||||
try {
|
||||
const result = await DBQuery(config as any, dbName, query);
|
||||
if (!result.success || !Array.isArray(result.data) || result.data.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const row = result.data[0] as Record<string, any>;
|
||||
const version =
|
||||
row.version
|
||||
|| row.VERSION
|
||||
|| row.Value
|
||||
|| row.value
|
||||
|| Object.values(row)[1]
|
||||
|| Object.values(row)[0];
|
||||
const text = String(version || '').trim();
|
||||
if (text) return text;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const extractViewDefinition = (dialect: string, data: any[]): string => {
|
||||
if (!data || data.length === 0) return '-- 未找到视图定义';
|
||||
const row = data[0];
|
||||
@@ -112,11 +192,15 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
switch (dialect) {
|
||||
case 'mysql': {
|
||||
const keys = Object.keys(row);
|
||||
const textDefinition = row.view_definition || row.VIEW_DEFINITION;
|
||||
if (textDefinition) return String(textDefinition);
|
||||
const sqlKey = keys.find(k => k.toLowerCase().includes('create view') || k.toLowerCase() === 'create view');
|
||||
if (sqlKey) return row[sqlKey];
|
||||
const tableSqlKey = keys.find(k => k.toLowerCase().includes('create table'));
|
||||
if (tableSqlKey) return row[tableSqlKey];
|
||||
for (const key of keys) {
|
||||
const val = String(row[key] || '');
|
||||
if (val.toUpperCase().includes('CREATE') && val.toUpperCase().includes('VIEW')) {
|
||||
if (val.toUpperCase().includes('CREATE') && (val.toUpperCase().includes('VIEW') || val.toUpperCase().includes('TABLE'))) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
@@ -137,6 +221,9 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
case 'mysql': {
|
||||
const row = data[0];
|
||||
const keys = Object.keys(row);
|
||||
if (row.routine_definition || row.ROUTINE_DEFINITION) {
|
||||
return String(row.routine_definition || row.ROUTINE_DEFINITION);
|
||||
}
|
||||
const sqlKey = keys.find(k => k.toLowerCase().includes('create function') || k.toLowerCase().includes('create procedure'));
|
||||
if (sqlKey) return row[sqlKey];
|
||||
for (const key of keys) {
|
||||
@@ -145,6 +232,11 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
const routineName = String(row.Name || row.name || '').trim();
|
||||
if (routineName) {
|
||||
const routineType = String(row.Type || row.type || row.ROUTINE_TYPE || row.routine_type || 'FUNCTION').trim().toUpperCase();
|
||||
return `-- 当前数据源未返回可执行定义文本,已返回元数据\n-- 名称: ${routineName}\n-- 类型: ${routineType}\n${JSON.stringify(row, null, 2)}`;
|
||||
}
|
||||
return JSON.stringify(row, null, 2);
|
||||
}
|
||||
case 'oracle':
|
||||
@@ -173,9 +265,11 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
|
||||
const dbName = tab.dbName || '';
|
||||
const dialect = getMetadataDialect(conn);
|
||||
const sphinxLike = isSphinxConnection(conn) && dialect === 'mysql';
|
||||
|
||||
let query: string;
|
||||
let queries: string[];
|
||||
let extractFn: (dialect: string, data: any[]) => string;
|
||||
let objectLabel: string;
|
||||
|
||||
if (tab.type === 'view-def') {
|
||||
const viewName = tab.viewName || '';
|
||||
@@ -184,8 +278,9 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
query = buildShowViewQuery(dialect, viewName, dbName);
|
||||
queries = buildShowViewQueries(dialect, viewName, dbName);
|
||||
extractFn = extractViewDefinition;
|
||||
objectLabel = '视图';
|
||||
} else {
|
||||
const routineName = tab.routineName || '';
|
||||
const routineType = tab.routineType || 'FUNCTION';
|
||||
@@ -194,12 +289,13 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
query = buildShowRoutineQuery(dialect, routineName, routineType, dbName);
|
||||
queries = buildShowRoutineQueries(dialect, routineName, routineType, dbName);
|
||||
extractFn = extractRoutineDefinition;
|
||||
objectLabel = '函数/存储过程';
|
||||
}
|
||||
|
||||
if (query.startsWith('--')) {
|
||||
setDefinition(query);
|
||||
if (!queries.length || String(queries[0] || '').startsWith('--')) {
|
||||
setDefinition(String(queries[0] || '-- 暂不支持该对象定义查看'));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -214,11 +310,26 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
ssh: conn.config.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' }
|
||||
};
|
||||
|
||||
const result = await DBQuery(config as any, dbName, query);
|
||||
const result = await runQueryCandidates(config, dbName, queries);
|
||||
|
||||
if (result.success && Array.isArray(result.data)) {
|
||||
if (result.success && Array.isArray(result.data) && result.data.length > 0) {
|
||||
const def = extractFn(dialect, result.data);
|
||||
setDefinition(def);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
if (sphinxLike) {
|
||||
const version = await getVersionHint(config, dbName);
|
||||
const versionText = version ? `(版本: ${version})` : '';
|
||||
setDefinition(`-- 当前 Sphinx 实例${versionText}未返回${objectLabel}定义。\n-- 已执行多套兼容查询,可能是版本能力限制或对象类型不支持。`);
|
||||
return;
|
||||
}
|
||||
setDefinition(`-- 未找到${objectLabel}定义`);
|
||||
} else if (sphinxLike) {
|
||||
const version = await getVersionHint(config, dbName);
|
||||
const versionText = version ? `(版本: ${version})` : '';
|
||||
setDefinition(`-- 当前 Sphinx 实例${versionText}不支持${objectLabel}定义查询。\n-- 已自动尝试兼容语句,返回失败信息: ${result.message || 'unknown error'}`);
|
||||
} else {
|
||||
setError(result.message || '查询定义失败');
|
||||
}
|
||||
|
||||
@@ -922,7 +922,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
|
||||
const applyAutoLimit = (sql: string, dbType: string, maxRows: number): { sql: string; applied: boolean; maxRows: number } => {
|
||||
const normalizedType = (dbType || 'mysql').toLowerCase();
|
||||
const supportsLimit = normalizedType === 'mysql' || normalizedType === 'postgres' || normalizedType === 'kingbase' || normalizedType === 'sqlite' || normalizedType === 'tdengine' || normalizedType === '';
|
||||
const supportsLimit = normalizedType === 'mysql' || normalizedType === 'mariadb' || normalizedType === 'sphinx' || normalizedType === 'postgres' || normalizedType === 'kingbase' || normalizedType === 'sqlite' || normalizedType === 'tdengine' || normalizedType === '';
|
||||
if (!supportsLimit) return { sql, applied: false, maxRows };
|
||||
if (!Number.isFinite(maxRows) || maxRows <= 0) return { sql, applied: false, maxRows };
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
if (type === 'custom') {
|
||||
return String((conn?.config as any)?.driver || '').trim().toLowerCase();
|
||||
}
|
||||
if (type === 'mariadb') return 'mysql';
|
||||
if (type === 'mariadb' || type === 'sphinx') return 'mysql';
|
||||
if (type === 'dameng') return 'dm';
|
||||
return type;
|
||||
};
|
||||
@@ -236,6 +236,38 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
|
||||
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;
|
||||
|
||||
type MetadataQuerySpec = {
|
||||
sql: string;
|
||||
inferredType?: 'FUNCTION' | 'PROCEDURE';
|
||||
};
|
||||
|
||||
type MetadataQueryResult = {
|
||||
rows: Record<string, any>[];
|
||||
inferredType?: 'FUNCTION' | 'PROCEDURE';
|
||||
};
|
||||
|
||||
const isSphinxConnection = (conn: SavedConnection | undefined): boolean => {
|
||||
const type = String(conn?.config?.type || '').trim().toLowerCase();
|
||||
if (type === 'sphinx') return true;
|
||||
if (type !== 'custom') return false;
|
||||
const driver = String((conn?.config as any)?.driver || '').trim().toLowerCase();
|
||||
return driver === 'sphinx' || driver === 'sphinxql';
|
||||
};
|
||||
|
||||
const normalizeMetadataQuerySpecs = (specs: MetadataQuerySpec[]): MetadataQuerySpec[] => {
|
||||
const seen = new Set<string>();
|
||||
const normalized: MetadataQuerySpec[] = [];
|
||||
specs.forEach((spec) => {
|
||||
const sql = String(spec.sql || '').trim();
|
||||
if (!sql) return;
|
||||
const key = `${spec.inferredType || ''}@@${sql}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
normalized.push({ sql, inferredType: spec.inferredType });
|
||||
});
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const getCaseInsensitiveValue = (row: Record<string, any>, candidateKeys: string[]): string => {
|
||||
const keyMap = new Map<string, any>();
|
||||
Object.keys(row || {}).forEach((key) => keyMap.set(key.toLowerCase(), row[key]));
|
||||
@@ -259,6 +291,17 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
return '';
|
||||
};
|
||||
|
||||
const getMySQLShowTablesName = (row: Record<string, any>): string => {
|
||||
for (const key of Object.keys(row || {})) {
|
||||
if (!key.toLowerCase().startsWith('tables_in_')) continue;
|
||||
const value = row[key];
|
||||
if (value === undefined || value === null) continue;
|
||||
const normalized = String(value).trim();
|
||||
if (normalized !== '') return normalized;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const buildQualifiedName = (schemaName: string, objectName: string): string => {
|
||||
const schema = String(schemaName || '').trim();
|
||||
const name = String(objectName || '').trim();
|
||||
@@ -281,162 +324,233 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
};
|
||||
};
|
||||
|
||||
const buildViewsMetadataQuery = (dialect: string, dbName: string): string => {
|
||||
const buildViewsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
if (!safeDbName) return '';
|
||||
return `SELECT TABLE_NAME AS view_name FROM information_schema.views WHERE table_schema = '${safeDbName}' ORDER BY TABLE_NAME`;
|
||||
case 'mysql': {
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT TABLE_NAME AS view_name, TABLE_SCHEMA AS schema_name FROM information_schema.views WHERE table_schema = '${safeDbName}' ORDER BY TABLE_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW FULL TABLES FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW FULL TABLES` },
|
||||
]);
|
||||
}
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
return `SELECT schemaname AS schema_name, viewname AS view_name FROM pg_catalog.pg_views WHERE schemaname != 'information_schema' AND schemaname NOT LIKE 'pg_%' ORDER BY schemaname, viewname`;
|
||||
return [{ sql: `SELECT schemaname AS schema_name, viewname AS view_name FROM pg_catalog.pg_views WHERE schemaname != 'information_schema' AND schemaname NOT LIKE 'pg_%' ORDER BY schemaname, viewname` }];
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return `SELECT s.name AS schema_name, v.name AS view_name FROM ${safeDb}.sys.views v JOIN ${safeDb}.sys.schemas s ON v.schema_id = s.schema_id ORDER BY s.name, v.name`;
|
||||
return [{ sql: `SELECT s.name AS schema_name, v.name AS view_name FROM ${safeDb}.sys.views v JOIN ${safeDb}.sys.schemas s ON v.schema_id = s.schema_id ORDER BY s.name, v.name` }];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm': {
|
||||
case 'dm':
|
||||
if (!safeDbName) {
|
||||
return `SELECT VIEW_NAME AS view_name FROM USER_VIEWS ORDER BY VIEW_NAME`;
|
||||
return [{ sql: `SELECT VIEW_NAME AS view_name FROM USER_VIEWS ORDER BY VIEW_NAME` }];
|
||||
}
|
||||
return `SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY VIEW_NAME`;
|
||||
}
|
||||
return [{ sql: `SELECT OWNER AS schema_name, VIEW_NAME AS view_name FROM ALL_VIEWS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY VIEW_NAME` }];
|
||||
case 'sqlite':
|
||||
return `SELECT name AS view_name FROM sqlite_master WHERE type = 'view' ORDER BY name`;
|
||||
return [{ sql: `SELECT name AS view_name FROM sqlite_master WHERE type = 'view' ORDER BY name` }];
|
||||
default:
|
||||
return '';
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildTriggersMetadataQuery = (dialect: string, dbName: string): string => {
|
||||
const buildTriggersMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
if (!safeDbName) return '';
|
||||
return `SELECT TRIGGER_NAME AS trigger_name, EVENT_OBJECT_TABLE AS table_name, TRIGGER_SCHEMA AS schema_name FROM information_schema.triggers WHERE trigger_schema = '${safeDbName}' ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME`;
|
||||
case 'mysql': {
|
||||
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT TRIGGER_NAME AS trigger_name, EVENT_OBJECT_TABLE AS table_name, TRIGGER_SCHEMA AS schema_name FROM information_schema.triggers WHERE trigger_schema = '${safeDbName}' ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME`
|
||||
: '',
|
||||
},
|
||||
{ sql: dbIdent ? `SHOW TRIGGERS FROM \`${dbIdent}\`` : '' },
|
||||
{ sql: `SHOW TRIGGERS` },
|
||||
]);
|
||||
}
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
return `SELECT DISTINCT event_object_schema AS schema_name, event_object_table AS table_name, trigger_name FROM information_schema.triggers WHERE trigger_schema NOT IN ('pg_catalog', 'information_schema') AND trigger_schema NOT LIKE 'pg_%' ORDER BY event_object_schema, event_object_table, trigger_name`;
|
||||
return [{ sql: `SELECT DISTINCT event_object_schema AS schema_name, event_object_table AS table_name, trigger_name FROM information_schema.triggers WHERE trigger_schema NOT IN ('pg_catalog', 'information_schema') AND trigger_schema NOT LIKE 'pg_%' ORDER BY event_object_schema, event_object_table, trigger_name` }];
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return `SELECT s.name AS schema_name, t.name AS table_name, tr.name AS trigger_name FROM ${safeDb}.sys.triggers tr JOIN ${safeDb}.sys.tables t ON tr.parent_id = t.object_id JOIN ${safeDb}.sys.schemas s ON t.schema_id = s.schema_id WHERE tr.parent_class = 1 ORDER BY s.name, t.name, tr.name`;
|
||||
return [{ sql: `SELECT s.name AS schema_name, t.name AS table_name, tr.name AS trigger_name FROM ${safeDb}.sys.triggers tr JOIN ${safeDb}.sys.tables t ON tr.parent_id = t.object_id JOIN ${safeDb}.sys.schemas s ON t.schema_id = s.schema_id WHERE tr.parent_class = 1 ORDER BY s.name, t.name, tr.name` }];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm': {
|
||||
case 'dm':
|
||||
if (!safeDbName) {
|
||||
return `SELECT TRIGGER_NAME AS trigger_name, TABLE_NAME AS table_name FROM USER_TRIGGERS ORDER BY TABLE_NAME, TRIGGER_NAME`;
|
||||
return [{ sql: `SELECT TRIGGER_NAME AS trigger_name, TABLE_NAME AS table_name FROM USER_TRIGGERS ORDER BY TABLE_NAME, TRIGGER_NAME` }];
|
||||
}
|
||||
return `SELECT OWNER AS schema_name, TABLE_NAME AS table_name, TRIGGER_NAME AS trigger_name FROM ALL_TRIGGERS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY TABLE_NAME, TRIGGER_NAME`;
|
||||
}
|
||||
return [{ sql: `SELECT OWNER AS schema_name, TABLE_NAME AS table_name, TRIGGER_NAME AS trigger_name FROM ALL_TRIGGERS WHERE OWNER = '${safeDbName.toUpperCase()}' ORDER BY TABLE_NAME, TRIGGER_NAME` }];
|
||||
case 'sqlite':
|
||||
return `SELECT name AS trigger_name, tbl_name AS table_name FROM sqlite_master WHERE type = 'trigger' ORDER BY tbl_name, name`;
|
||||
return [{ sql: `SELECT name AS trigger_name, tbl_name AS table_name FROM sqlite_master WHERE type = 'trigger' ORDER BY tbl_name, name` }];
|
||||
default:
|
||||
return '';
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const queryMetadataRows = async (conn: any, dbName: string, query: string): Promise<Record<string, any>[]> => {
|
||||
if (!query) return [];
|
||||
try {
|
||||
const config = buildRuntimeConfig(conn, dbName);
|
||||
const result = await DBQuery(config as any, dbName, query);
|
||||
if (!result.success || !Array.isArray(result.data)) return [];
|
||||
return result.data as Record<string, any>[];
|
||||
} catch {
|
||||
return [];
|
||||
const buildFunctionsMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT ROUTINE_NAME AS routine_name, ROUTINE_TYPE AS routine_type, ROUTINE_SCHEMA AS schema_name FROM information_schema.routines WHERE routine_schema = '${safeDbName}' ORDER BY ROUTINE_TYPE, ROUTINE_NAME`
|
||||
: '',
|
||||
},
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SHOW FUNCTION STATUS WHERE Db = '${safeDbName}'`
|
||||
: `SHOW FUNCTION STATUS`,
|
||||
inferredType: 'FUNCTION',
|
||||
},
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SHOW PROCEDURE STATUS WHERE Db = '${safeDbName}'`
|
||||
: `SHOW PROCEDURE STATUS`,
|
||||
inferredType: 'PROCEDURE',
|
||||
},
|
||||
]);
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
return [{ sql: `SELECT n.nspname AS schema_name, p.proname AS routine_name, CASE WHEN p.prokind = 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS routine_type FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg_%' ORDER BY n.nspname, routine_type, p.proname` }];
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return [{ sql: `SELECT s.name AS schema_name, o.name AS routine_name, CASE o.type WHEN 'P' THEN 'PROCEDURE' WHEN 'FN' THEN 'FUNCTION' WHEN 'IF' THEN 'FUNCTION' WHEN 'TF' THEN 'FUNCTION' END AS routine_type FROM ${safeDb}.sys.objects o JOIN ${safeDb}.sys.schemas s ON o.schema_id = s.schema_id WHERE o.type IN ('P','FN','IF','TF') ORDER BY o.type, s.name, o.name` }];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
if (!safeDbName) {
|
||||
return [{ sql: `SELECT OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM USER_OBJECTS WHERE OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME` }];
|
||||
}
|
||||
return [{ sql: `SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME` }];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const loadViews = async (conn: any, dbName: string): Promise<string[]> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const query = buildViewsMetadataQuery(dialect, dbName);
|
||||
const rows = await queryMetadataRows(conn, dbName, query);
|
||||
const queryMetadataRowsBySpecs = async (
|
||||
conn: any,
|
||||
dbName: string,
|
||||
specs: MetadataQuerySpec[]
|
||||
): Promise<{ results: MetadataQueryResult[]; hasSuccessfulQuery: boolean }> => {
|
||||
const normalizedSpecs = normalizeMetadataQuerySpecs(specs);
|
||||
if (normalizedSpecs.length === 0) {
|
||||
return { results: [], hasSuccessfulQuery: false };
|
||||
}
|
||||
const config = buildRuntimeConfig(conn, dbName);
|
||||
const results: MetadataQueryResult[] = [];
|
||||
let hasSuccessfulQuery = false;
|
||||
|
||||
for (const spec of normalizedSpecs) {
|
||||
try {
|
||||
const result = await DBQuery(config as any, dbName, spec.sql);
|
||||
if (!result.success || !Array.isArray(result.data)) {
|
||||
continue;
|
||||
}
|
||||
hasSuccessfulQuery = true;
|
||||
results.push({
|
||||
rows: result.data as Record<string, any>[],
|
||||
inferredType: spec.inferredType,
|
||||
});
|
||||
} catch {
|
||||
// 忽略单条查询失败,继续尝试后续回退语句
|
||||
}
|
||||
}
|
||||
return { results, hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadViews = async (conn: any, dbName: string): Promise<{ views: string[]; supported: boolean }> => {
|
||||
const savedConn = conn as SavedConnection;
|
||||
const dialect = getMetadataDialect(savedConn);
|
||||
const querySpecs = buildViewsMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const views: string[] = [];
|
||||
|
||||
rows.forEach((row) => {
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'table_schema']);
|
||||
const viewName = getCaseInsensitiveValue(row, ['view_name', 'viewname', 'table_name', 'name']) || getFirstRowValue(row);
|
||||
const fullName = buildQualifiedName(schemaName, viewName);
|
||||
if (!fullName || seen.has(fullName)) return;
|
||||
seen.add(fullName);
|
||||
views.push(fullName);
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const tableType = getCaseInsensitiveValue(row, ['table_type', 'table type', 'type']);
|
||||
if (tableType && tableType.toUpperCase() !== 'VIEW') return;
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'table_schema', 'db']);
|
||||
const viewName =
|
||||
getCaseInsensitiveValue(row, ['view_name', 'viewname', 'table_name', 'name'])
|
||||
|| getMySQLShowTablesName(row)
|
||||
|| getFirstRowValue(row);
|
||||
const fullName = buildQualifiedName(schemaName, viewName);
|
||||
if (!fullName || seen.has(fullName)) return;
|
||||
seen.add(fullName);
|
||||
views.push(fullName);
|
||||
});
|
||||
});
|
||||
return views;
|
||||
return { views, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabaseTriggers = async (conn: any, dbName: string): Promise<Array<{ displayName: string; triggerName: string; tableName: string }>> => {
|
||||
const loadDatabaseTriggers = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ triggers: Array<{ displayName: string; triggerName: string; tableName: string }>; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const query = buildTriggersMetadataQuery(dialect, dbName);
|
||||
const rows = await queryMetadataRows(conn, dbName, query);
|
||||
const querySpecs = buildTriggersMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const triggers: Array<{ displayName: string; triggerName: string; tableName: string }> = [];
|
||||
|
||||
rows.forEach((row) => {
|
||||
const triggerName = getCaseInsensitiveValue(row, ['trigger_name', 'triggername', 'name']) || getFirstRowValue(row);
|
||||
if (!triggerName) return;
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'event_object_schema', 'trigger_schema']);
|
||||
const tableName = getCaseInsensitiveValue(row, ['table_name', 'event_object_table', 'tbl_name']);
|
||||
const fullTableName = buildQualifiedName(schemaName, tableName);
|
||||
const uniqueKey = `${triggerName}@@${fullTableName}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
const displayName = fullTableName ? `${triggerName} (${fullTableName})` : triggerName;
|
||||
triggers.push({ displayName, triggerName, tableName: fullTableName });
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const triggerName = getCaseInsensitiveValue(row, ['trigger_name', 'triggername', 'trigger', 'name']) || getFirstRowValue(row);
|
||||
if (!triggerName) return;
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'event_object_schema', 'trigger_schema', 'db']);
|
||||
const tableName = getCaseInsensitiveValue(row, ['table_name', 'event_object_table', 'tbl_name', 'table']);
|
||||
const fullTableName = buildQualifiedName(schemaName, tableName);
|
||||
const uniqueKey = `${triggerName}@@${fullTableName}`;
|
||||
if (seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
const displayName = fullTableName ? `${triggerName} (${fullTableName})` : triggerName;
|
||||
triggers.push({ displayName, triggerName, tableName: fullTableName });
|
||||
});
|
||||
});
|
||||
return triggers;
|
||||
return { triggers, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const buildFunctionsMetadataQuery = (dialect: string, dbName: string): string => {
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
if (!safeDbName) return '';
|
||||
return `SELECT ROUTINE_NAME AS routine_name, ROUTINE_TYPE AS routine_type FROM information_schema.routines WHERE routine_schema = '${safeDbName}' ORDER BY ROUTINE_TYPE, ROUTINE_NAME`;
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
return `SELECT n.nspname AS schema_name, p.proname AS routine_name, CASE WHEN p.prokind = 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END AS routine_type FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname NOT LIKE 'pg_%' ORDER BY n.nspname, routine_type, p.proname`;
|
||||
case 'sqlserver': {
|
||||
const safeDb = quoteSqlServerIdentifier(dbName || 'master');
|
||||
return `SELECT s.name AS schema_name, o.name AS routine_name, CASE o.type WHEN 'P' THEN 'PROCEDURE' WHEN 'FN' THEN 'FUNCTION' WHEN 'IF' THEN 'FUNCTION' WHEN 'TF' THEN 'FUNCTION' END AS routine_type FROM ${safeDb}.sys.objects o JOIN ${safeDb}.sys.schemas s ON o.schema_id = s.schema_id WHERE o.type IN ('P','FN','IF','TF') ORDER BY o.type, s.name, o.name`;
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm': {
|
||||
if (!safeDbName) {
|
||||
return `SELECT OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM USER_OBJECTS WHERE OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME`;
|
||||
}
|
||||
return `SELECT OWNER AS schema_name, OBJECT_NAME AS routine_name, OBJECT_TYPE AS routine_type FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE IN ('FUNCTION','PROCEDURE') ORDER BY OBJECT_TYPE, OBJECT_NAME`;
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const loadFunctions = async (conn: any, dbName: string): Promise<Array<{ displayName: string; routineName: string; routineType: string }>> => {
|
||||
const loadFunctions = async (
|
||||
conn: any,
|
||||
dbName: string
|
||||
): Promise<{ routines: Array<{ displayName: string; routineName: string; routineType: string }>; supported: boolean }> => {
|
||||
const dialect = getMetadataDialect(conn as SavedConnection);
|
||||
const query = buildFunctionsMetadataQuery(dialect, dbName);
|
||||
const rows = await queryMetadataRows(conn, dbName, query);
|
||||
const querySpecs = buildFunctionsMetadataQuerySpecs(dialect, dbName);
|
||||
const { results, hasSuccessfulQuery } = await queryMetadataRowsBySpecs(conn, dbName, querySpecs);
|
||||
const seen = new Set<string>();
|
||||
const routines: Array<{ displayName: string; routineName: string; routineType: string }> = [];
|
||||
|
||||
rows.forEach((row) => {
|
||||
const routineName = getCaseInsensitiveValue(row, ['routine_name', 'object_name', 'proname', 'name']);
|
||||
if (!routineName) return;
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'nspname', 'owner']);
|
||||
const routineType = getCaseInsensitiveValue(row, ['routine_type', 'object_type']) || 'FUNCTION';
|
||||
const fullName = buildQualifiedName(schemaName, routineName);
|
||||
if (!fullName || seen.has(fullName)) return;
|
||||
seen.add(fullName);
|
||||
const typeLabel = routineType.toUpperCase() === 'PROCEDURE' ? 'P' : 'F';
|
||||
routines.push({ displayName: `${fullName} [${typeLabel}]`, routineName: fullName, routineType: routineType.toUpperCase() });
|
||||
results.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const routineName = getCaseInsensitiveValue(row, ['routine_name', 'object_name', 'proname', 'name']);
|
||||
if (!routineName) return;
|
||||
const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'nspname', 'owner', 'db', 'database']);
|
||||
const rawType = getCaseInsensitiveValue(row, ['routine_type', 'object_type', 'type']) || queryResult.inferredType || 'FUNCTION';
|
||||
const normalizedType = rawType.toUpperCase().includes('PROC') ? 'PROCEDURE' : 'FUNCTION';
|
||||
const fullName = buildQualifiedName(schemaName, routineName);
|
||||
const uniqueKey = `${fullName}@@${normalizedType}`;
|
||||
if (!fullName || seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
const typeLabel = normalizedType === 'PROCEDURE' ? 'P' : 'F';
|
||||
routines.push({ displayName: `${fullName} [${typeLabel}]`, routineName: fullName, routineType: normalizedType });
|
||||
});
|
||||
});
|
||||
return routines;
|
||||
return { routines, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabases = async (node: any) => {
|
||||
@@ -563,25 +677,25 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
};
|
||||
});
|
||||
|
||||
const [views, triggers, routines] = await Promise.all([
|
||||
loadViews(conn, conn.dbName),
|
||||
loadDatabaseTriggers(conn, conn.dbName),
|
||||
loadFunctions(conn, conn.dbName),
|
||||
]);
|
||||
const [viewsResult, triggersResult, routinesResult] = await Promise.all([
|
||||
loadViews(conn, conn.dbName),
|
||||
loadDatabaseTriggers(conn, conn.dbName),
|
||||
loadFunctions(conn, conn.dbName),
|
||||
]);
|
||||
|
||||
const viewEntries = views.map((viewName) => {
|
||||
const parsed = splitQualifiedName(viewName);
|
||||
return {
|
||||
viewName,
|
||||
const viewEntries = viewsResult.views.map((viewName) => {
|
||||
const parsed = splitQualifiedName(viewName);
|
||||
return {
|
||||
viewName,
|
||||
schemaName: parsed.schemaName,
|
||||
displayName: getSidebarTableDisplayName(conn, viewName),
|
||||
};
|
||||
});
|
||||
|
||||
const triggerEntries = triggers.map((trigger) => {
|
||||
const triggerParsed = splitQualifiedName(trigger.triggerName);
|
||||
const tableParsed = splitQualifiedName(trigger.tableName);
|
||||
const schemaName = tableParsed.schemaName || triggerParsed.schemaName;
|
||||
const triggerEntries = triggersResult.triggers.map((trigger) => {
|
||||
const triggerParsed = splitQualifiedName(trigger.triggerName);
|
||||
const tableParsed = splitQualifiedName(trigger.tableName);
|
||||
const schemaName = tableParsed.schemaName || triggerParsed.schemaName;
|
||||
const triggerObjectName = triggerParsed.objectName || trigger.triggerName;
|
||||
const tableObjectName = tableParsed.objectName || trigger.tableName;
|
||||
const displayName = tableObjectName ? `${triggerObjectName} (${tableObjectName})` : triggerObjectName;
|
||||
@@ -592,15 +706,28 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
};
|
||||
});
|
||||
|
||||
const routineEntries = routines.map((routine) => {
|
||||
const parsed = splitQualifiedName(routine.routineName);
|
||||
const typeLabel = routine.routineType === 'PROCEDURE' ? 'P' : 'F';
|
||||
return {
|
||||
const routineEntries = routinesResult.routines.map((routine) => {
|
||||
const parsed = splitQualifiedName(routine.routineName);
|
||||
const typeLabel = routine.routineType === 'PROCEDURE' ? 'P' : 'F';
|
||||
return {
|
||||
...routine,
|
||||
schemaName: parsed.schemaName,
|
||||
displayName: `${parsed.objectName || routine.routineName} [${typeLabel}]`,
|
||||
};
|
||||
});
|
||||
displayName: `${parsed.objectName || routine.routineName} [${typeLabel}]`,
|
||||
};
|
||||
});
|
||||
|
||||
if (isSphinxConnection(conn as SavedConnection)) {
|
||||
const unsupportedObjects: string[] = [];
|
||||
if (!viewsResult.supported) unsupportedObjects.push('视图');
|
||||
if (!routinesResult.supported) unsupportedObjects.push('函数/存储过程');
|
||||
if (!triggersResult.supported) unsupportedObjects.push('触发器');
|
||||
if (unsupportedObjects.length > 0) {
|
||||
message.info({
|
||||
key: `sphinx-capability-${conn.id}-${conn.dbName}`,
|
||||
content: `当前 Sphinx 实例未开放以下对象能力:${unsupportedObjects.join('、')}(已自动降级兼容)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前数据库的排序偏好
|
||||
const sortPreferenceKey = `${conn.id}-${conn.dbName}`;
|
||||
|
||||
@@ -405,7 +405,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
const getDbType = (): string => {
|
||||
const conn = connections.find(c => c.id === tab.connectionId);
|
||||
const type = String(conn?.config?.type || '').toLowerCase();
|
||||
if (type === 'mariadb') return 'mysql';
|
||||
if (type === 'mariadb' || type === 'sphinx') return 'mysql';
|
||||
if (type === 'dameng') return 'dm';
|
||||
return type;
|
||||
};
|
||||
|
||||
@@ -52,47 +52,121 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
|
||||
if (type === 'custom') {
|
||||
return String(conn?.config?.driver || '').trim().toLowerCase();
|
||||
}
|
||||
if (type === 'mariadb') return 'mysql';
|
||||
if (type === 'mariadb' || type === 'sphinx') return 'mysql';
|
||||
if (type === 'dameng') return 'dm';
|
||||
return type;
|
||||
};
|
||||
|
||||
const buildShowTriggerQuery = (dialect: string, triggerName: string, dbName: string): string => {
|
||||
const isSphinxConnection = (conn: any): boolean => {
|
||||
const type = String(conn?.config?.type || '').trim().toLowerCase();
|
||||
if (type === 'sphinx') return true;
|
||||
if (type !== 'custom') return false;
|
||||
const driver = String(conn?.config?.driver || '').trim().toLowerCase();
|
||||
return driver === 'sphinx' || driver === 'sphinxql';
|
||||
};
|
||||
|
||||
const buildShowTriggerQueries = (dialect: string, triggerName: string, dbName: string): string[] => {
|
||||
const safeTriggerName = escapeSQLLiteral(triggerName);
|
||||
const safeDbName = escapeSQLLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
return `SHOW CREATE TRIGGER \`${triggerName.replace(/`/g, '``')}\``;
|
||||
return [
|
||||
`SHOW CREATE TRIGGER \`${triggerName.replace(/`/g, '``')}\``,
|
||||
safeDbName
|
||||
? `SELECT ACTION_STATEMENT AS trigger_definition FROM information_schema.triggers WHERE trigger_schema = '${safeDbName}' AND trigger_name = '${safeTriggerName}' LIMIT 1`
|
||||
: '',
|
||||
safeDbName
|
||||
? `SHOW TRIGGERS FROM \`${dbName.replace(/`/g, '``')}\` LIKE '${safeTriggerName}'`
|
||||
: `SHOW TRIGGERS LIKE '${safeTriggerName}'`,
|
||||
].filter(Boolean);
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'highgo':
|
||||
case 'vastbase':
|
||||
return `SELECT pg_get_triggerdef(t.oid, true) AS trigger_definition
|
||||
return [`SELECT pg_get_triggerdef(t.oid, true) AS trigger_definition
|
||||
FROM pg_trigger t
|
||||
JOIN pg_class c ON t.tgrelid = c.oid
|
||||
WHERE t.tgname = '${safeTriggerName}'
|
||||
AND NOT t.tgisinternal
|
||||
LIMIT 1`;
|
||||
LIMIT 1`];
|
||||
case 'sqlserver': {
|
||||
return `SELECT OBJECT_DEFINITION(OBJECT_ID('${safeTriggerName.replace(/'/g, "''")}')) AS trigger_definition`;
|
||||
return [`SELECT OBJECT_DEFINITION(OBJECT_ID('${safeTriggerName.replace(/'/g, "''")}')) AS trigger_definition`];
|
||||
}
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
if (!safeDbName) {
|
||||
return `SELECT TRIGGER_BODY FROM USER_TRIGGERS WHERE TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`;
|
||||
return [`SELECT TRIGGER_BODY FROM USER_TRIGGERS WHERE TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`];
|
||||
}
|
||||
return `SELECT TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = '${safeDbName.toUpperCase()}' AND TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`;
|
||||
return [`SELECT TRIGGER_BODY FROM ALL_TRIGGERS WHERE OWNER = '${safeDbName.toUpperCase()}' AND TRIGGER_NAME = '${safeTriggerName.toUpperCase()}'`];
|
||||
case 'sqlite':
|
||||
return `SELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = '${safeTriggerName}'`;
|
||||
return [`SELECT sql FROM sqlite_master WHERE type = 'trigger' AND name = '${safeTriggerName}'`];
|
||||
case 'tdengine':
|
||||
return `-- TDengine 不支持触发器`;
|
||||
return [`-- TDengine 不支持触发器`];
|
||||
case 'mongodb':
|
||||
return `-- MongoDB 不支持触发器`;
|
||||
return [`-- MongoDB 不支持触发器`];
|
||||
default:
|
||||
return `-- 暂不支持该数据库类型的触发器定义查看`;
|
||||
return [`-- 暂不支持该数据库类型的触发器定义查看`];
|
||||
}
|
||||
};
|
||||
|
||||
const runQueryCandidates = async (
|
||||
config: Record<string, any>,
|
||||
dbName: string,
|
||||
queries: string[]
|
||||
): Promise<{ success: boolean; data: any[]; message?: string }> => {
|
||||
let lastMessage = '';
|
||||
let hasSuccessfulQuery = false;
|
||||
for (const query of queries) {
|
||||
const sql = String(query || '').trim();
|
||||
if (!sql) continue;
|
||||
try {
|
||||
const result = await DBQuery(config as any, dbName, sql);
|
||||
if (!result.success || !Array.isArray(result.data)) {
|
||||
lastMessage = result.message || lastMessage;
|
||||
continue;
|
||||
}
|
||||
hasSuccessfulQuery = true;
|
||||
if (result.data.length > 0) {
|
||||
return { success: true, data: result.data };
|
||||
}
|
||||
} catch (error: any) {
|
||||
lastMessage = error?.message || String(error);
|
||||
}
|
||||
}
|
||||
if (hasSuccessfulQuery) {
|
||||
return { success: true, data: [] };
|
||||
}
|
||||
return { success: false, data: [], message: lastMessage };
|
||||
};
|
||||
|
||||
const getVersionHint = async (config: Record<string, any>, dbName: string): Promise<string> => {
|
||||
const candidates = [
|
||||
`SELECT VERSION() AS version`,
|
||||
`SHOW VARIABLES LIKE 'version'`,
|
||||
];
|
||||
for (const query of candidates) {
|
||||
try {
|
||||
const result = await DBQuery(config as any, dbName, query);
|
||||
if (!result.success || !Array.isArray(result.data) || result.data.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const row = result.data[0] as Record<string, any>;
|
||||
const version =
|
||||
row.version
|
||||
|| row.VERSION
|
||||
|| row.Value
|
||||
|| row.value
|
||||
|| Object.values(row)[1]
|
||||
|| Object.values(row)[0];
|
||||
const text = String(version || '').trim();
|
||||
if (text) return text;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const extractTriggerDefinition = (dialect: string, data: any[]): string => {
|
||||
if (!data || data.length === 0) {
|
||||
return '-- 未找到触发器定义';
|
||||
@@ -104,6 +178,12 @@ LIMIT 1`;
|
||||
case 'mysql': {
|
||||
// MySQL SHOW CREATE TRIGGER returns: Trigger, sql_mode, SQL Original Statement, ...
|
||||
const keys = Object.keys(row);
|
||||
if (row.trigger_definition || row.TRIGGER_DEFINITION) {
|
||||
return String(row.trigger_definition || row.TRIGGER_DEFINITION);
|
||||
}
|
||||
if (row.ACTION_STATEMENT || row.action_statement) {
|
||||
return String(row.ACTION_STATEMENT || row.action_statement);
|
||||
}
|
||||
const sqlKey = keys.find(k => k.toLowerCase().includes('statement') || k.toLowerCase() === 'sql original statement');
|
||||
if (sqlKey) return row[sqlKey];
|
||||
// Fallback: try to find any key containing CREATE TRIGGER
|
||||
@@ -158,10 +238,11 @@ LIMIT 1`;
|
||||
}
|
||||
|
||||
const dialect = getMetadataDialect(conn);
|
||||
const query = buildShowTriggerQuery(dialect, triggerName, dbName);
|
||||
const queries = buildShowTriggerQueries(dialect, triggerName, dbName);
|
||||
const sphinxLike = isSphinxConnection(conn) && dialect === 'mysql';
|
||||
|
||||
if (query.startsWith('--')) {
|
||||
setTriggerDefinition(query);
|
||||
if (!queries.length || String(queries[0] || '').startsWith('--')) {
|
||||
setTriggerDefinition(String(queries[0] || '-- 暂不支持该数据库类型的触发器定义查看'));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -176,11 +257,26 @@ LIMIT 1`;
|
||||
ssh: conn.config.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' }
|
||||
};
|
||||
|
||||
const result = await DBQuery(config as any, dbName, query);
|
||||
const result = await runQueryCandidates(config, dbName, queries);
|
||||
|
||||
if (result.success && Array.isArray(result.data)) {
|
||||
if (result.success && Array.isArray(result.data) && result.data.length > 0) {
|
||||
const definition = extractTriggerDefinition(dialect, result.data);
|
||||
setTriggerDefinition(definition);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
if (sphinxLike) {
|
||||
const version = await getVersionHint(config, dbName);
|
||||
const versionText = version ? `(版本: ${version})` : '';
|
||||
setTriggerDefinition(`-- 当前 Sphinx 实例${versionText}未返回触发器定义。\n-- 已执行多套兼容查询,可能是版本能力限制或对象类型不支持。`);
|
||||
return;
|
||||
}
|
||||
setTriggerDefinition('-- 未找到触发器定义');
|
||||
} else if (sphinxLike) {
|
||||
const version = await getVersionHint(config, dbName);
|
||||
const versionText = version ? `(版本: ${version})` : '';
|
||||
setTriggerDefinition(`-- 当前 Sphinx 实例${versionText}不支持触发器定义查询。\n-- 已自动尝试兼容语句,返回失败信息: ${result.message || 'unknown error'}`);
|
||||
} else {
|
||||
setError(result.message || '查询触发器定义失败');
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export const quoteIdentPart = (dbType: string, ident: string) => {
|
||||
if (!raw) return raw;
|
||||
const dbTypeLower = (dbType || '').toLowerCase();
|
||||
|
||||
if (dbTypeLower === 'mysql' || dbTypeLower === 'tdengine') {
|
||||
if (dbTypeLower === 'mysql' || dbTypeLower === 'mariadb' || dbTypeLower === 'sphinx' || dbTypeLower === 'tdengine') {
|
||||
return `\`${raw.replace(/`/g, '``')}\``;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user