修改mysql编辑视图的时候保存失败。 (#377)

This commit is contained in:
Syngnat
2026-04-13 17:13:08 +08:00
committed by GitHub
3 changed files with 59 additions and 7 deletions

View File

@@ -10,6 +10,23 @@ 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 DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -257,15 +274,15 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
case 'mysql': {
const keys = Object.keys(row);
const textDefinition = row.view_definition || row.VIEW_DEFINITION;
if (textDefinition) return String(textDefinition);
if (textDefinition) return normalizeMySQLViewDDL(textDefinition);
const sqlKey = keys.find(k => k.toLowerCase().includes('create view') || k.toLowerCase() === 'create view');
if (sqlKey) return row[sqlKey];
if (sqlKey) return normalizeMySQLViewDDL(row[sqlKey]);
const tableSqlKey = keys.find(k => k.toLowerCase().includes('create table'));
if (tableSqlKey) return row[tableSqlKey];
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 val;
return normalizeMySQLViewDDL(val);
}
}
return JSON.stringify(row, null, 2);

View File

@@ -95,6 +95,23 @@ const SEARCH_SCOPE_ICON_MAP: Record<SearchScope, React.ReactNode> = {
tag: <TagOutlined />,
};
const normalizeMySQLViewDDLForEditing = (viewName: string, 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 `CREATE OR REPLACE VIEW ${viewName} AS\n${normalized};`;
}
return `${normalized};`;
};
const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }> = ({ onEditConnection }) => {
const connections = useStore(state => state.connections);
const savedQueries = useStore(state => state.savedQueries);
@@ -2419,7 +2436,11 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
const row = result.data[0] as Record<string, any>;
const def = row.view_definition || row.VIEW_DEFINITION || Object.values(row).find(v => typeof v === 'string' && String(v).length > 10) || '';
if (def) {
template = `-- 编辑视图 ${viewName}\nCREATE OR REPLACE VIEW ${viewName} AS\n${def}`;
if (dialect === 'mysql') {
template = `-- 编辑视图 ${viewName}\n${normalizeMySQLViewDDLForEditing(viewName, def)}`;
} else {
template = `-- 编辑视图 ${viewName}\nCREATE OR REPLACE VIEW ${viewName} AS\n${def}`;
}
}
}
}

View File

@@ -12,6 +12,7 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
@@ -29,6 +30,8 @@ import (
const minExportQueryTimeout = 5 * time.Minute
const minClickHouseExportQueryTimeout = 2 * time.Hour
var mysqlCreateViewPrefixPattern = regexp.MustCompile(`(?is)^\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+`)
func (a *App) OpenSQLFile() connection.QueryResult {
selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "Select SQL File",
@@ -1601,7 +1604,7 @@ func extractViewCreateSQL(row map[string]interface{}) string {
}
ddl := exportRowValueCI(row, "create view", "create_statement", "create_sql", "ddl", "sql", "view_definition", "definition")
if ddl != "" {
return ddl
return normalizeMySQLViewCreateSQL(ddl)
}
for _, value := range row {
if value == nil {
@@ -1613,12 +1616,23 @@ func extractViewCreateSQL(row map[string]interface{}) string {
}
lower := strings.ToLower(text)
if strings.HasPrefix(lower, "create ") || strings.HasPrefix(lower, "select ") || strings.HasPrefix(lower, "with ") {
return text
return normalizeMySQLViewCreateSQL(text)
}
}
return ""
}
func normalizeMySQLViewCreateSQL(sql string) string {
trimmed := strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(sql), ";"))
if trimmed == "" {
return ""
}
if mysqlCreateViewPrefixPattern.MatchString(trimmed) {
return mysqlCreateViewPrefixPattern.ReplaceAllString(trimmed, "CREATE OR REPLACE VIEW ")
}
return trimmed
}
func exportRowValueCI(row map[string]interface{}, candidates ...string) string {
if len(row) == 0 || len(candidates) == 0 {
return ""