mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-23 05:37:11 +08:00
# Conflicts: # frontend/package.json.md5 # frontend/src/App.tsx # frontend/src/components/AIChatPanel.message-boundary.test.tsx # frontend/src/components/AIChatPanel.tsx # frontend/src/components/AISettingsModal.tsx # frontend/src/components/ConnectionModal.tsx # frontend/src/components/DataGrid.ddl.test.tsx # frontend/src/components/DataGrid.layout.test.tsx # frontend/src/components/DataGrid.tsx # frontend/src/components/DataGridColumnTitle.test.tsx # frontend/src/components/DataGridLegacyCellContextMenu.tsx # frontend/src/components/DataGridSecondaryActions.tsx # frontend/src/components/DataGridToolbarFrame.tsx # frontend/src/components/DataSyncModal.tsx # frontend/src/components/DataViewer.tsx # frontend/src/components/DefinitionViewer.tsx # frontend/src/components/DriverManagerModal.tsx # frontend/src/components/QueryEditor.external-sql-save.test.tsx # frontend/src/components/QueryEditor.tsx # frontend/src/components/RedisViewer.tsx # frontend/src/components/Sidebar.locate-toolbar.test.tsx # frontend/src/components/Sidebar.tsx # frontend/src/components/TabManager.hover.test.tsx # frontend/src/components/TabManager.tsx # frontend/src/components/TableDesigner.tsx # frontend/src/components/V2TableContextMenu.tsx # frontend/src/components/ai/AIChatHeader.tsx # frontend/src/components/ai/AIHistoryDrawer.tsx # frontend/src/main.tsx # frontend/src/store.ts # frontend/src/utils/aiComposerNotice.test.ts # frontend/src/utils/aiComposerNotice.ts # frontend/src/utils/connectionModalPresentation.ts # frontend/src/utils/driverImportGuidance.ts # frontend/src/utils/externalSqlTree.test.ts # frontend/src/utils/externalSqlTree.ts # frontend/src/utils/sqlDialect.ts # internal/ai/service/service.go
748 lines
35 KiB
TypeScript
748 lines
35 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import Editor from './MonacoEditor';
|
|
import { Button, Spin, Alert } from 'antd';
|
|
import { EditOutlined } from '@ant-design/icons';
|
|
import { TabData } from '../types';
|
|
import { useStore } from '../store';
|
|
import { DBQuery } from '../../wailsjs/go/app/App';
|
|
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
|
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
|
|
import { useI18n } from '../i18n/provider';
|
|
import { splitQualifiedNameLast } from '../utils/qualifiedName';
|
|
import { buildSqlServerObjectDefinitionQueries } from '../utils/sqlServerObjectDefinition';
|
|
|
|
interface DefinitionViewerProps {
|
|
tab: TabData;
|
|
}
|
|
|
|
const normalizeMySQLViewDDL = (rawDefinition: unknown): string => {
|
|
const text = String(rawDefinition || '').trim();
|
|
if (!text) return '';
|
|
|
|
const normalized = text.replace(/\r\n/g, '\n').trim().replace(/;+\s*$/, '');
|
|
const createViewPrefixPattern = /^\s*create\s+(?:algorithm\s*=\s*\w+\s+)?(?:definer\s*=\s*(?:`[^`]+`|\S+)\s*@\s*(?:`[^`]+`|\S+)\s+)?(?:sql\s+security\s+(?:definer|invoker)\s+)?view\s+/i;
|
|
if (createViewPrefixPattern.test(normalized)) {
|
|
return `${normalized.replace(createViewPrefixPattern, 'CREATE OR REPLACE VIEW ')};`;
|
|
}
|
|
|
|
if (/^\s*(select|with)\b/i.test(normalized)) {
|
|
return normalized;
|
|
}
|
|
|
|
return `${normalized};`;
|
|
};
|
|
|
|
const ensureSqlStatementTerminator = (sql: string): string => {
|
|
const normalized = String(sql || '').trim();
|
|
if (!normalized) return '';
|
|
return /;\s*$/.test(normalized) ? normalized : `${normalized};`;
|
|
};
|
|
|
|
const buildEditableDefinitionSql = (tab: TabData, definition: string, objectLabel: string, objectName: string): string => {
|
|
const normalizedDefinition = String(definition || '').trim();
|
|
const header = `-- 修改${objectLabel}: ${objectName}\n-- 请确认语法兼容当前数据库后执行\n`;
|
|
if (!normalizedDefinition) {
|
|
return `${header}-- 当前对象定义为空,请补全 ${objectName} 的 DDL 后执行\n`;
|
|
}
|
|
|
|
if (/^\s*--\s*(未找到|暂不支持|当前)/.test(normalizedDefinition)) {
|
|
return `${header}${ensureSqlStatementTerminator(normalizedDefinition)}`;
|
|
}
|
|
|
|
if (tab.type === 'view-def' && !/^\s*create\b/i.test(normalizedDefinition)) {
|
|
if (/^\s*view\b/i.test(normalizedDefinition)) {
|
|
return `${header}${ensureSqlStatementTerminator(normalizedDefinition.replace(/^\s*view\b/i, 'CREATE OR REPLACE VIEW'))}`;
|
|
}
|
|
return `${header}CREATE OR REPLACE VIEW ${objectName} AS\n${ensureSqlStatementTerminator(normalizedDefinition)}`;
|
|
}
|
|
|
|
if (
|
|
tab.type === 'routine-def'
|
|
&& !/^\s*create\b/i.test(normalizedDefinition)
|
|
&& /^\s*(function|procedure)\b/i.test(normalizedDefinition)
|
|
) {
|
|
return `${header}${ensureSqlStatementTerminator(`CREATE OR REPLACE ${normalizedDefinition}`)}`;
|
|
}
|
|
|
|
return `${header}${ensureSqlStatementTerminator(normalizedDefinition)}`;
|
|
};
|
|
|
|
const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [definition, setDefinition] = useState<string>('');
|
|
const [openingObjectEdit, setOpeningObjectEdit] = useState(false);
|
|
const isMountedRef = useRef(true);
|
|
const loadedDefinitionKeyRef = useRef('');
|
|
|
|
const connections = useStore(state => state.connections);
|
|
const theme = useStore(state => state.theme);
|
|
const addTab = useStore(state => state.addTab);
|
|
const setActiveContext = useStore(state => state.setActiveContext);
|
|
const darkMode = theme === 'dark';
|
|
const { t } = useI18n();
|
|
const objectIdentityKey = [
|
|
tab.connectionId,
|
|
tab.dbName,
|
|
tab.type,
|
|
tab.viewName,
|
|
tab.viewKind,
|
|
tab.eventName,
|
|
tab.routineName,
|
|
tab.routineType,
|
|
].map((item) => String(item || '')).join('||');
|
|
|
|
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
|
|
|
|
const getMetadataDialect = (conn: any): string => {
|
|
const type = String(conn?.config?.type || '').trim().toLowerCase();
|
|
if (type === 'custom') {
|
|
const driver = String(conn?.config?.driver || '').trim().toLowerCase();
|
|
if (driver === 'diros' || driver === 'doris') return 'mysql';
|
|
if (driver === 'goldendb' || driver === 'greatdb' || driver === 'gdb') return 'mysql';
|
|
if (driver === 'oceanbase') return normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql';
|
|
if (driver === 'opengauss' || driver === 'open_gauss' || driver === 'open-gauss') return 'opengauss';
|
|
if (driver === 'gaussdb' || driver === 'gauss_db' || driver === 'gauss-db') return 'gaussdb';
|
|
return driver;
|
|
}
|
|
if (type === 'oceanbase' && normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle') return 'oracle';
|
|
if (type === 'goldendb' || type === 'mariadb' || type === 'oceanbase' || type === 'diros' || 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 parsed = splitQualifiedNameLast(fullName);
|
|
return { schema: parsed.parentPath, name: parsed.objectName };
|
|
};
|
|
|
|
const getCaseInsensitiveRawValue = (row: Record<string, any>, candidateKeys: string[]): any => {
|
|
const keyMap = new Map<string, any>();
|
|
Object.keys(row || {}).forEach((key) => keyMap.set(key.toLowerCase(), row[key]));
|
|
for (const key of candidateKeys) {
|
|
const value = keyMap.get(key.toLowerCase());
|
|
if (value !== undefined && value !== null) {
|
|
return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
const parseDuckDBParameterNames = (raw: any): string[] => {
|
|
if (Array.isArray(raw)) {
|
|
return raw
|
|
.map((item) => String(item ?? '').trim())
|
|
.filter((item) => item !== '' && item.toLowerCase() !== '<nil>');
|
|
}
|
|
const text = String(raw ?? '').trim();
|
|
if (!text) return [];
|
|
const normalized = text.startsWith('[') && text.endsWith(']')
|
|
? text.slice(1, -1)
|
|
: text;
|
|
return normalized
|
|
.split(',')
|
|
.map((part) => part.trim())
|
|
.filter((part) => part !== '' && part.toLowerCase() !== '<nil>');
|
|
};
|
|
|
|
const buildDuckDBMacroDDL = (
|
|
schemaName: string,
|
|
functionName: string,
|
|
parametersRaw: any,
|
|
macroDefinitionRaw: any
|
|
): string => {
|
|
const schema = String(schemaName || '').trim();
|
|
const name = String(functionName || '').trim();
|
|
const macroDefinition = String(macroDefinitionRaw || '').trim();
|
|
if (!name || !macroDefinition) return '';
|
|
|
|
const parameters = parseDuckDBParameterNames(parametersRaw).join(', ');
|
|
const qualifiedName = schema ? `${schema}.${name}` : name;
|
|
const isTableMacro = !macroDefinition.startsWith('(');
|
|
if (isTableMacro) {
|
|
return `CREATE OR REPLACE MACRO ${qualifiedName}(${parameters}) AS TABLE ${macroDefinition};`;
|
|
}
|
|
return `CREATE OR REPLACE MACRO ${qualifiedName}(${parameters}) AS ${macroDefinition};`;
|
|
};
|
|
|
|
const buildShowViewQueries = (dialect: string, viewName: string, dbName: string, viewKind?: string): string[] => {
|
|
const { schema, name } = parseSchemaAndName(viewName);
|
|
const safeName = escapeSQLLiteral(name);
|
|
const safeDbName = escapeSQLLiteral(dbName);
|
|
|
|
switch (dialect) {
|
|
case 'mysql':
|
|
case 'starrocks':
|
|
if (dialect === 'starrocks' && viewKind === 'materialized') {
|
|
const mvRef = schema
|
|
? `\`${schema.replace(/`/g, '``')}\`.\`${name.replace(/`/g, '``')}\``
|
|
: `\`${name.replace(/`/g, '``')}\``;
|
|
return [
|
|
`SHOW CREATE MATERIALIZED VIEW ${mvRef}`,
|
|
`SHOW CREATE TABLE ${mvRef}`,
|
|
];
|
|
}
|
|
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':
|
|
case 'opengauss':
|
|
case 'gaussdb': {
|
|
const schemaRef = schema || 'public';
|
|
return [`SELECT pg_get_viewdef('${escapeSQLLiteral(schemaRef)}.${safeName}'::regclass, true) AS view_definition`];
|
|
}
|
|
case 'sqlserver':
|
|
return buildSqlServerObjectDefinitionQueries('view', viewName, dbName, '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()}'`];
|
|
}
|
|
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 USER_VIEWS WHERE VIEW_NAME = '${safeName.toUpperCase()}'`];
|
|
case 'sqlite':
|
|
return [`SELECT sql AS view_definition FROM sqlite_master WHERE type='view' AND name='${safeName}'`];
|
|
case 'duckdb': {
|
|
const schemaRef = schema || 'main';
|
|
return [`SELECT view_definition FROM information_schema.views WHERE table_schema = '${escapeSQLLiteral(schemaRef)}' AND table_name = '${safeName}' LIMIT 1`];
|
|
}
|
|
default:
|
|
return [`-- ${t('definition_viewer.editor.unsupported_view_definition')}`];
|
|
}
|
|
};
|
|
|
|
const buildShowRoutineQueries = (dialect: string, routineName: string, routineType: string, dbName: string): string[] => {
|
|
const { schema, name } = parseSchemaAndName(routineName);
|
|
const safeName = escapeSQLLiteral(name);
|
|
const safeDbName = escapeSQLLiteral(dbName);
|
|
const upperType = (routineType || 'FUNCTION').toUpperCase();
|
|
|
|
switch (dialect) {
|
|
case 'mysql':
|
|
case 'starrocks':
|
|
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':
|
|
case 'opengauss':
|
|
case 'gaussdb': {
|
|
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`];
|
|
}
|
|
case 'sqlserver':
|
|
return buildSqlServerObjectDefinitionQueries('routine', routineName, dbName, '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 USER_SOURCE WHERE NAME = '${safeName.toUpperCase()}' AND TYPE = '${upperType}' ORDER BY LINE`];
|
|
}
|
|
case 'duckdb': {
|
|
const schemaRef = schema || 'main';
|
|
const safeSchema = escapeSQLLiteral(schemaRef);
|
|
return [
|
|
`SELECT schema_name, function_name, parameters, macro_definition FROM duckdb_functions() WHERE internal = false AND lower(function_type) = 'macro' AND schema_name = '${safeSchema}' AND function_name = '${safeName}' LIMIT 1`,
|
|
`SELECT schema_name, function_name, parameters, macro_definition FROM duckdb_functions() WHERE internal = false AND lower(function_type) = 'macro' AND function_name = '${safeName}' ORDER BY CASE WHEN schema_name = '${safeSchema}' THEN 0 ELSE 1 END, schema_name LIMIT 1`,
|
|
];
|
|
}
|
|
case 'sqlite':
|
|
return [`-- ${t('definition_viewer.editor.unsupported_sqlite_routine_definition')}`];
|
|
default:
|
|
return [`-- ${t('definition_viewer.editor.unsupported_routine_definition')}`];
|
|
}
|
|
};
|
|
|
|
const buildShowEventQueries = (dialect: string, eventName: string, dbName: string): string[] => {
|
|
const { schema, name } = parseSchemaAndName(eventName);
|
|
const safeName = escapeSQLLiteral(name);
|
|
const safeSchema = escapeSQLLiteral(schema || dbName);
|
|
const eventRef = schema
|
|
? `\`${schema.replace(/`/g, '``')}\`.\`${name.replace(/`/g, '``')}\``
|
|
: `\`${name.replace(/`/g, '``')}\``;
|
|
|
|
switch (dialect) {
|
|
case 'mysql':
|
|
return [
|
|
`SHOW CREATE EVENT ${eventRef}`,
|
|
safeSchema
|
|
? `SELECT EVENT_SCHEMA AS schema_name, EVENT_NAME AS event_name, EVENT_DEFINITION AS event_definition, EVENT_TYPE AS event_type, EXECUTE_AT AS execute_at, INTERVAL_VALUE AS interval_value, INTERVAL_FIELD AS interval_field, STARTS AS starts, ENDS AS ends, STATUS AS status, ON_COMPLETION AS on_completion, EVENT_COMMENT AS event_comment FROM information_schema.events WHERE event_schema = '${safeSchema}' AND event_name = '${safeName}' LIMIT 1`
|
|
: '',
|
|
].filter(Boolean);
|
|
default:
|
|
return [`-- ${t('definition_viewer.editor.unsupported_event_definition')}`];
|
|
}
|
|
};
|
|
|
|
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(buildRpcConnectionConfig(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(buildRpcConnectionConfig(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 `-- ${t('definition_viewer.editor.view_definition_not_found')}`;
|
|
const row = data[0];
|
|
|
|
switch (dialect) {
|
|
case 'mysql':
|
|
case 'starrocks': {
|
|
const keys = Object.keys(row);
|
|
const textDefinition = row.view_definition || row.VIEW_DEFINITION;
|
|
if (textDefinition) return normalizeMySQLViewDDL(textDefinition);
|
|
const sqlKey = keys.find(k => k.toLowerCase().includes('create view') || k.toLowerCase() === 'create view');
|
|
if (sqlKey) return normalizeMySQLViewDDL(row[sqlKey]);
|
|
const tableSqlKey = keys.find(k => k.toLowerCase().includes('create table'));
|
|
if (tableSqlKey) return normalizeMySQLViewDDL(row[tableSqlKey]);
|
|
for (const key of keys) {
|
|
const val = String(row[key] || '');
|
|
if (val.toUpperCase().includes('CREATE') && (val.toUpperCase().includes('VIEW') || val.toUpperCase().includes('TABLE'))) {
|
|
return normalizeMySQLViewDDL(val);
|
|
}
|
|
}
|
|
return JSON.stringify(row, null, 2);
|
|
}
|
|
case 'oracle':
|
|
case 'dm':
|
|
return row.view_definition || row.VIEW_DEFINITION || row.text || row.TEXT || Object.values(row)[0] || '';
|
|
case 'sqlserver': {
|
|
const directDefinition = getCaseInsensitiveRawValue(row, ['view_definition', 'definition']);
|
|
if (directDefinition !== undefined && directDefinition !== null && String(directDefinition).trim() !== '') {
|
|
return String(directDefinition);
|
|
}
|
|
const helpTextDefinition = data
|
|
.map((item) => getCaseInsensitiveRawValue(item, ['Text', 'text']))
|
|
.filter((value) => value !== undefined && value !== null)
|
|
.map((value) => String(value))
|
|
.join('');
|
|
if (helpTextDefinition.trim()) return helpTextDefinition;
|
|
return String(Object.values(row)[0] || '');
|
|
}
|
|
default:
|
|
return row.view_definition || row.VIEW_DEFINITION || row.sql || row.SQL || Object.values(row)[0] || '';
|
|
}
|
|
};
|
|
|
|
const extractRoutineDefinition = (dialect: string, data: any[]): string => {
|
|
if (!data || data.length === 0) return `-- ${t('definition_viewer.editor.routine_definition_not_found')}`;
|
|
|
|
switch (dialect) {
|
|
case 'mysql':
|
|
case 'starrocks': {
|
|
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) {
|
|
const val = String(row[key] || '');
|
|
if (val.toUpperCase().includes('CREATE') && (val.toUpperCase().includes('FUNCTION') || val.toUpperCase().includes('PROCEDURE'))) {
|
|
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 `-- ${t('definition_viewer.editor.metadata_fallback.header')}\n-- ${t('definition_viewer.editor.metadata_fallback.name_label')}: ${routineName}\n-- ${t('definition_viewer.editor.metadata_fallback.type_label')}: ${routineType}\n${JSON.stringify(row, null, 2)}`;
|
|
}
|
|
return JSON.stringify(row, null, 2);
|
|
}
|
|
case 'oracle':
|
|
case 'dm': {
|
|
// Oracle/DM ALL_SOURCE returns multiple rows, one per line
|
|
return data.map(row => row.text || row.TEXT || Object.values(row)[0] || '').join('');
|
|
}
|
|
case 'duckdb': {
|
|
const row = data[0] as Record<string, any>;
|
|
const ddl = buildDuckDBMacroDDL(
|
|
String(getCaseInsensitiveRawValue(row, ['schema_name']) || '').trim(),
|
|
String(getCaseInsensitiveRawValue(row, ['function_name', 'routine_name', 'name']) || '').trim(),
|
|
getCaseInsensitiveRawValue(row, ['parameters']),
|
|
getCaseInsensitiveRawValue(row, ['macro_definition'])
|
|
);
|
|
if (ddl) return ddl;
|
|
const fallback = getCaseInsensitiveRawValue(row, ['macro_definition', 'routine_definition', 'definition']);
|
|
if (fallback !== undefined && fallback !== null && String(fallback).trim() !== '') {
|
|
return String(fallback);
|
|
}
|
|
return JSON.stringify(row, null, 2);
|
|
}
|
|
case 'sqlserver': {
|
|
const directDefinition = getCaseInsensitiveRawValue(data[0], ['routine_definition', 'definition']);
|
|
if (directDefinition !== undefined && directDefinition !== null && String(directDefinition).trim() !== '') {
|
|
return String(directDefinition);
|
|
}
|
|
const helpTextDefinition = data
|
|
.map((row) => getCaseInsensitiveRawValue(row, ['Text', 'text']))
|
|
.filter((value) => value !== undefined && value !== null)
|
|
.map((value) => String(value))
|
|
.join('');
|
|
if (helpTextDefinition.trim()) return helpTextDefinition;
|
|
return String(Object.values(data[0])[0] || '');
|
|
}
|
|
default: {
|
|
const row = data[0];
|
|
return row.routine_definition || row.ROUTINE_DEFINITION || Object.values(row)[0] || '';
|
|
}
|
|
}
|
|
};
|
|
|
|
const extractEventDefinition = (dialect: string, data: any[]): string => {
|
|
if (!data || data.length === 0) return `-- ${t('definition_viewer.editor.event_definition_not_found')}`;
|
|
|
|
switch (dialect) {
|
|
case 'mysql': {
|
|
const row = data[0];
|
|
const keys = Object.keys(row);
|
|
const sqlKey = keys.find(k => k.toLowerCase().includes('create event'));
|
|
if (sqlKey && row[sqlKey]) return String(row[sqlKey]);
|
|
|
|
const definition = row.event_definition || row.EVENT_DEFINITION;
|
|
const eventName = row.event_name || row.EVENT_NAME || row.Name || row.name;
|
|
if (definition && eventName) {
|
|
return `-- ${t('definition_viewer.editor.event_fragment_fallback.header')}\n-- ${t('definition_viewer.editor.metadata_fallback.name_label')}: ${eventName}\n${String(definition)}`;
|
|
}
|
|
return JSON.stringify(row, null, 2);
|
|
}
|
|
default: {
|
|
const row = data[0];
|
|
return row.event_definition || row.EVENT_DEFINITION || Object.values(row)[0] || '';
|
|
}
|
|
}
|
|
};
|
|
|
|
const loadDefinition = async (): Promise<{ success: boolean; definition?: string; error?: string }> => {
|
|
const conn = connections.find(c => c.id === tab.connectionId);
|
|
if (!conn) {
|
|
return { success: false, error: t('definition_viewer.error.connection_not_found') };
|
|
}
|
|
|
|
const dbName = tab.dbName || '';
|
|
const dialect = getMetadataDialect(conn);
|
|
const sphinxLike = isSphinxConnection(conn) && dialect === 'mysql';
|
|
|
|
let queries: string[];
|
|
let extractFn: (dialect: string, data: any[]) => string;
|
|
let resolvedObjectLabel: string;
|
|
|
|
if (tab.type === 'view-def') {
|
|
const viewName = tab.viewName || '';
|
|
if (!viewName) {
|
|
return { success: false, error: t('definition_viewer.error.view_name_empty') };
|
|
}
|
|
queries = buildShowViewQueries(dialect, viewName, dbName, tab.viewKind);
|
|
extractFn = extractViewDefinition;
|
|
resolvedObjectLabel = tab.viewKind === 'materialized'
|
|
? t('definition_viewer.object.materialized_view')
|
|
: t('definition_viewer.object.view');
|
|
} else if (tab.type === 'event-def') {
|
|
const eventName = tab.eventName || '';
|
|
if (!eventName) {
|
|
return { success: false, error: t('definition_viewer.error.event_name_empty') };
|
|
}
|
|
queries = buildShowEventQueries(dialect, eventName, dbName);
|
|
extractFn = extractEventDefinition;
|
|
resolvedObjectLabel = t('definition_viewer.object.event');
|
|
} else {
|
|
const routineName = tab.routineName || '';
|
|
const routineType = tab.routineType || 'FUNCTION';
|
|
if (!routineName) {
|
|
return { success: false, error: t('definition_viewer.error.routine_name_empty') };
|
|
}
|
|
queries = buildShowRoutineQueries(dialect, routineName, routineType, dbName);
|
|
extractFn = extractRoutineDefinition;
|
|
resolvedObjectLabel = t('definition_viewer.object.routine');
|
|
}
|
|
|
|
if (!queries.length || String(queries[0] || '').startsWith('--')) {
|
|
return {
|
|
success: true,
|
|
definition: String(
|
|
queries[0] || `-- ${t('definition_viewer.editor.unsupported_object_definition')}`,
|
|
),
|
|
};
|
|
}
|
|
|
|
try {
|
|
const config = {
|
|
...conn.config,
|
|
port: Number(conn.config.port),
|
|
password: conn.config.password || '',
|
|
database: conn.config.database || '',
|
|
useSSH: conn.config.useSSH || false,
|
|
ssh: conn.config.ssh || { host: '', port: 22, user: '', password: '', keyPath: '' }
|
|
};
|
|
|
|
const result = await runQueryCandidates(config, dbName, queries);
|
|
|
|
if (result.success && Array.isArray(result.data) && result.data.length > 0) {
|
|
return { success: true, definition: extractFn(dialect, result.data) };
|
|
}
|
|
|
|
if (result.success) {
|
|
if (sphinxLike) {
|
|
const version = await getVersionHint(config, dbName);
|
|
const versionText = version
|
|
? t('definition_viewer.editor.sphinx.version_suffix', { version })
|
|
: '';
|
|
return {
|
|
success: true,
|
|
definition: `-- ${t('definition_viewer.editor.sphinx.empty_result', {
|
|
version: versionText,
|
|
object: resolvedObjectLabel,
|
|
})}\n-- ${t('definition_viewer.editor.sphinx.compat_queries_hint')}`,
|
|
};
|
|
}
|
|
return {
|
|
success: true,
|
|
definition: `-- ${t('definition_viewer.editor.object_definition_not_found', {
|
|
object: resolvedObjectLabel,
|
|
})}`,
|
|
};
|
|
}
|
|
|
|
if (sphinxLike) {
|
|
const version = await getVersionHint(config, dbName);
|
|
const versionText = version
|
|
? t('definition_viewer.editor.sphinx.version_suffix', { version })
|
|
: '';
|
|
const failedMessage = result.message
|
|
? `${t('definition_viewer.editor.sphinx.failed_message_label')}: ${result.message}`
|
|
: t('definition_viewer.editor.sphinx.failed_message_unknown');
|
|
return {
|
|
success: true,
|
|
definition: `-- ${t('definition_viewer.editor.sphinx.unsupported_query', {
|
|
version: versionText,
|
|
object: resolvedObjectLabel,
|
|
})}\n-- ${failedMessage}`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
error: result.message || t('definition_viewer.error.query_failed'),
|
|
};
|
|
} catch (e: any) {
|
|
return {
|
|
success: false,
|
|
error: t('definition_viewer.error.query_failed_detail', {
|
|
detail: e?.message || String(e),
|
|
}),
|
|
};
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
const syncDefinition = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
const result = await loadDefinition();
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
if (result.success) {
|
|
loadedDefinitionKeyRef.current = objectIdentityKey;
|
|
setDefinition(String(result.definition || ''));
|
|
} else {
|
|
setError(result.error || t('definition_viewer.error.query_failed'));
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
syncDefinition();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [tab.connectionId, tab.dbName, tab.viewName, tab.viewKind, tab.eventName, tab.routineName, tab.routineType, tab.type, connections, objectIdentityKey, t]);
|
|
|
|
useEffect(() => () => {
|
|
isMountedRef.current = false;
|
|
}, []);
|
|
|
|
const objectLabel = tab.type === 'view-def'
|
|
? (tab.viewKind === 'materialized' ? t('definition_viewer.object.materialized_view') : t('definition_viewer.object.view'))
|
|
: (tab.type === 'event-def' ? t('definition_viewer.object.event') : t('definition_viewer.object.routine'));
|
|
const objectName = tab.type === 'view-def'
|
|
? tab.viewName
|
|
: (tab.type === 'event-def' ? tab.eventName : tab.routineName);
|
|
const loadingTip = tab.type === 'view-def'
|
|
? t('definition_viewer.loading.view_definition')
|
|
: (tab.type === 'event-def' ? t('definition_viewer.loading.event_definition') : t('definition_viewer.loading.routine_definition'));
|
|
const normalizedObjectName = String(objectName || '').trim();
|
|
const displayedDefinition = loadedDefinitionKeyRef.current === objectIdentityKey ? definition : '';
|
|
const hasDefinition = String(displayedDefinition || '').trim() !== '';
|
|
|
|
const openObjectEditQuery = async () => {
|
|
if (!normalizedObjectName || openingObjectEdit) return;
|
|
const dbName = String(tab.dbName || '').trim();
|
|
setOpeningObjectEdit(true);
|
|
setError(null);
|
|
try {
|
|
const result = await loadDefinition();
|
|
if (!isMountedRef.current) {
|
|
return;
|
|
}
|
|
if (!result.success) {
|
|
setError(result.error || t('definition_viewer.error.query_failed'));
|
|
return;
|
|
}
|
|
const latestDefinition = String(result.definition || '');
|
|
loadedDefinitionKeyRef.current = objectIdentityKey;
|
|
setDefinition(latestDefinition);
|
|
const query = buildEditableDefinitionSql(tab, latestDefinition, objectLabel, normalizedObjectName);
|
|
setActiveContext({ connectionId: tab.connectionId, dbName });
|
|
addTab({
|
|
id: `query-edit-object-${tab.connectionId}-${dbName}-${Date.now()}`,
|
|
title: `修改${objectLabel}: ${normalizedObjectName}`,
|
|
type: 'query',
|
|
connectionId: tab.connectionId,
|
|
dbName,
|
|
query,
|
|
queryMode: 'object-edit',
|
|
});
|
|
} finally {
|
|
if (isMountedRef.current) {
|
|
setOpeningObjectEdit(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
|
<Spin tip={loadingTip} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error && !hasDefinition) {
|
|
return (
|
|
<div style={{ padding: 16 }}>
|
|
<Alert type="error" message={t('definition_viewer.error.load_failed')} description={error} showIcon />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
<div style={{ padding: '8px 16px', borderBottom: darkMode ? '1px solid #303030' : '1px solid #f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
|
<div style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
<strong>{objectLabel}: </strong>{objectName}
|
|
{tab.dbName && <span style={{ marginLeft: 16, color: '#888' }}>{t('definition_viewer.field.database')}: {tab.dbName}</span>}
|
|
{tab.routineType && <span style={{ marginLeft: 16, color: '#888' }}>{t('definition_viewer.field.type')}: {tab.routineType}</span>}
|
|
</div>
|
|
<Button size="small" icon={<EditOutlined />} onClick={openObjectEditQuery} disabled={!normalizedObjectName} loading={openingObjectEdit}>
|
|
对象修改
|
|
</Button>
|
|
</div>
|
|
{error && hasDefinition && (
|
|
<div style={{ padding: '8px 16px 0' }}>
|
|
<Alert type="warning" message="刷新最新定义失败" description={error} showIcon />
|
|
</div>
|
|
)}
|
|
<div style={{ flex: 1, minHeight: 0 }}>
|
|
<Editor
|
|
height="100%"
|
|
language="sql"
|
|
theme={darkMode ? 'transparent-dark' : 'transparent-light'}
|
|
value={displayedDefinition}
|
|
options={{
|
|
readOnly: true,
|
|
minimap: { enabled: false },
|
|
fontSize: 14,
|
|
lineNumbers: 'on',
|
|
scrollBeyondLastLine: false,
|
|
wordWrap: 'on',
|
|
automaticLayout: true,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DefinitionViewer;
|