From e16082af9a58b57f7a7684a63656bcba1ed36376 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 10 Jun 2026 12:14:27 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sidebar):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20GDB=20=E5=85=BC=E5=AE=B9=E5=BA=93=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加 MySQL 兼容视图元数据查询回退 - 统一编辑器和左侧树的视图元数据查询 - 放宽视图分组下缺失节点类型时的可视定位兜底 --- frontend/src/components/QueryEditor.tsx | 20 +++-------- frontend/src/components/Sidebar.tsx | 20 +++-------- frontend/src/utils/sidebarLocate.test.ts | 42 ++++++++++++++++++++++ frontend/src/utils/sidebarLocate.ts | 29 ++++++++++++--- frontend/src/utils/sidebarMetadata.test.ts | 9 ++++- frontend/src/utils/sidebarMetadata.ts | 20 +++++++++++ 6 files changed, 102 insertions(+), 38 deletions(-) diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 85a2126..ab1dfed 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -21,7 +21,7 @@ import { formatSqlExecutionError } from '../utils/sqlErrorSemantics'; import { findSqlStatementRanges, resolveCurrentSqlStatementRange, resolveExecutableSql } from '../utils/sqlStatementSelection'; import { isMacLikePlatform } from '../utils/appearance'; import { splitSidebarQualifiedName } from '../utils/sidebarLocate'; -import { isSidebarViewTableType, normalizeSidebarViewName } from '../utils/sidebarMetadata'; +import { buildMySQLCompatibleViewMetadataSqls, isSidebarViewTableType, normalizeSidebarViewName } from '../utils/sidebarMetadata'; import { SIDEBAR_SQL_EDITOR_DRAG_MIME, decodeSidebarSqlEditorDragPayload, hasSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag'; import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert'; import { @@ -822,21 +822,9 @@ const buildCompletionViewsMetadataQuerySpecs = (dialect: string, dbName: string) switch (dialect) { case 'mysql': case 'starrocks': { - 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: safeDbName - ? `SELECT TABLE_NAME AS view_name, TABLE_SCHEMA AS schema_name, TABLE_TYPE AS table_type FROM information_schema.tables WHERE table_schema = '${safeDbName}' AND UPPER(TABLE_TYPE) LIKE '%VIEW%' ORDER BY TABLE_NAME` - : '', - }, - { sql: dbIdent ? `SHOW FULL TABLES FROM \`${dbIdent}\`` : '' }, - { sql: 'SHOW FULL TABLES' }, - ]); + return normalizeMetadataQuerySpecs( + buildMySQLCompatibleViewMetadataSqls(dbName).map((sql) => ({ sql })), + ); } case 'postgres': case 'kingbase': diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 3e60159..a554564 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -63,7 +63,7 @@ import FindInDatabaseModal from './FindInDatabaseModal'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; import { getDataSourceCapabilities, resolveDataSourceType } from '../utils/dataSourceCapabilities'; import { noAutoCapInputProps } from '../utils/inputAutoCap'; -import { isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata'; +import { buildMySQLCompatibleViewMetadataSqls, isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata'; import { splitQualifiedNameLast } from '../utils/qualifiedName'; import { buildStarRocksMaterializedViewPreviewSql } from './tableDesignerSchemaSql'; import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol'; @@ -1386,21 +1386,9 @@ const Sidebar: React.FC<{ switch (dialect) { case 'mysql': case 'starrocks': { - 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: safeDbName - ? `SELECT TABLE_NAME AS view_name, TABLE_SCHEMA AS schema_name, TABLE_TYPE AS table_type FROM information_schema.tables WHERE table_schema = '${safeDbName}' AND UPPER(TABLE_TYPE) LIKE '%VIEW%' ORDER BY TABLE_NAME` - : '', - }, - { sql: dbIdent ? `SHOW FULL TABLES FROM \`${dbIdent}\`` : '' }, - { sql: `SHOW FULL TABLES` }, - ]); + return normalizeMetadataQuerySpecs( + buildMySQLCompatibleViewMetadataSqls(dbName).map((sql) => ({ sql })), + ); } case 'postgres': case 'kingbase': diff --git a/frontend/src/utils/sidebarLocate.test.ts b/frontend/src/utils/sidebarLocate.test.ts index d8f87be..df1cf35 100644 --- a/frontend/src/utils/sidebarLocate.test.ts +++ b/frontend/src/utils/sidebarLocate.test.ts @@ -793,6 +793,48 @@ describe('sidebarLocate', () => { ]); }); + it('finds a view node by title under the views group when node type metadata is missing', () => { + const target = resolveSidebarLocateTarget({ + tabId: 'stale-view-tab-id', + connectionId: 'conn-1', + dbName: 'GDB_APP', + tableName: 'V_ACCOUNT', + schemaName: 'SYSDBA', + objectGroup: 'views', + }, { groupBySchema: false }); + + const tree = [ + { + key: 'conn-1', + children: [ + { + key: 'conn-1-GDB_APP', + dataRef: { id: 'conn-1', dbName: 'GDB_APP' }, + children: [ + { + key: 'conn-1-GDB_APP-views', + children: [ + { + key: 'conn-1-GDB_APP-view-generated-key', + title: 'V_ACCOUNT', + dataRef: {}, + }, + ], + }, + ], + }, + ], + }, + ]; + + expect(findSidebarNodePathForLocate(tree, target)).toEqual([ + 'conn-1', + 'conn-1-GDB_APP', + 'conn-1-GDB_APP-views', + 'conn-1-GDB_APP-view-generated-key', + ]); + }); + it('finds a schema-qualified view request by visual title when the node has no schema metadata', () => { const target = resolveSidebarLocateTarget({ tabId: 'stale-view-tab-id', diff --git a/frontend/src/utils/sidebarLocate.ts b/frontend/src/utils/sidebarLocate.ts index e22bd27..bf83885 100644 --- a/frontend/src/utils/sidebarLocate.ts +++ b/frontend/src/utils/sidebarLocate.ts @@ -409,6 +409,24 @@ const getVisualNodeObjectName = ( return matchedPrefix ? nodeKey.slice(matchedPrefix.length) : ''; }; +const getLocateObjectGroupPathSuffix = (objectGroup: SidebarLocateObjectGroup): string => { + if (objectGroup === 'externalSqlFiles') return 'external-sql-root'; + return objectGroup.toLowerCase(); +}; + +const isPathInsideLocateObjectGroup = ( + path: string[], + target: SidebarLocateTarget, +): boolean => { + if (target.objectGroup === 'externalSqlFiles') return false; + const normalizedObjectGroupKey = normalizeLocateName(target.objectGroupKey); + const groupSuffix = getLocateObjectGroupPathSuffix(target.objectGroup); + return path.some((key) => { + const normalizedKey = normalizeLocateName(key); + return normalizedKey === normalizedObjectGroupKey || normalizedKey.endsWith(`-${groupSuffix}`); + }); +}; + const matchesLocateObjectNodeByVisualIdentity = ( node: SidebarLocateTreeNodeLike, target: SidebarLocateTarget, @@ -416,12 +434,13 @@ const matchesLocateObjectNodeByVisualIdentity = ( ): boolean => { if (!path.includes(target.databaseKey)) return false; const nodeObjectType = normalizeLocateName(toTrimmedString(node.dataRef?.objectType || node.dataRef?.objectKind)); + const insideExpectedGroup = isPathInsideLocateObjectGroup(path, target); - if (target.objectGroup === 'views' && node.type !== 'view' && nodeObjectType !== 'view' && nodeObjectType !== 'views') return false; - if (target.objectGroup === 'materializedViews' && node.type !== 'materialized-view' && nodeObjectType !== 'materialized-view' && nodeObjectType !== 'materializedviews') return false; - if (target.objectGroup === 'triggers' && node.type !== 'db-trigger') return false; - if (target.objectGroup === 'routines' && node.type !== 'routine') return false; - if (target.objectGroup === 'tables' && node.type !== 'table') return false; + if (target.objectGroup === 'views' && node.type !== 'view' && nodeObjectType !== 'view' && nodeObjectType !== 'views' && !insideExpectedGroup) return false; + if (target.objectGroup === 'materializedViews' && node.type !== 'materialized-view' && nodeObjectType !== 'materialized-view' && nodeObjectType !== 'materializedviews' && !insideExpectedGroup) return false; + if (target.objectGroup === 'triggers' && node.type !== 'db-trigger' && !insideExpectedGroup) return false; + if (target.objectGroup === 'routines' && node.type !== 'routine' && !insideExpectedGroup) return false; + if (target.objectGroup === 'tables' && node.type !== 'table' && !insideExpectedGroup) return false; if (target.objectGroup === 'externalSqlFiles') return false; const schemaName = toTrimmedString(node.dataRef?.schemaName); diff --git a/frontend/src/utils/sidebarMetadata.test.ts b/frontend/src/utils/sidebarMetadata.test.ts index 746025e..ecdb4e0 100644 --- a/frontend/src/utils/sidebarMetadata.test.ts +++ b/frontend/src/utils/sidebarMetadata.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect } from './sidebarMetadata'; +import { buildMySQLCompatibleViewMetadataSqls, isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect } from './sidebarMetadata'; describe('sidebarMetadata', () => { it('normalizes MySQL-compatible view names without schema prefixes', () => { @@ -22,4 +22,11 @@ describe('sidebarMetadata', () => { expect(isSidebarViewTableType('BASE TABLE')).toBe(false); expect(isSidebarViewTableType('MATERIALIZED VIEW')).toBe(false); }); + + it('adds SHOW FULL TABLES view-only fallbacks for MySQL-compatible databases', () => { + expect(buildMySQLCompatibleViewMetadataSqls('GDB_APP')).toEqual(expect.arrayContaining([ + "SHOW FULL TABLES FROM `GDB_APP` WHERE Table_type = 'VIEW'", + "SHOW FULL TABLES WHERE Table_type = 'VIEW'", + ])); + }); }); diff --git a/frontend/src/utils/sidebarMetadata.ts b/frontend/src/utils/sidebarMetadata.ts index 8b6a3f1..2410716 100644 --- a/frontend/src/utils/sidebarMetadata.ts +++ b/frontend/src/utils/sidebarMetadata.ts @@ -2,6 +2,9 @@ import { normalizeOceanBaseProtocol } from './oceanBaseProtocol'; import { splitQualifiedNameLast } from './qualifiedName'; import { resolveSqlDialect } from './sqlDialect'; +const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''"); +const escapeBacktickIdentifier = (raw: string): string => String(raw || '').replace(/`/g, '``'); + const normalizeSidebarConnectionDialect = (type: string, driver: string, oceanBaseProtocol?: string): string => { const normalizedType = String(type || '').trim().toLowerCase(); if (normalizedType === 'custom') { @@ -63,6 +66,23 @@ export const isSidebarViewTableType = (tableType: unknown): boolean => { return normalizedType.includes('VIEW') && !normalizedType.includes('MATERIALIZED'); }; +export const buildMySQLCompatibleViewMetadataSqls = (dbName: string): string[] => { + const safeDbName = escapeSQLLiteral(dbName); + const dbIdent = escapeBacktickIdentifier(dbName).trim(); + return [ + safeDbName + ? `SELECT TABLE_NAME AS view_name, TABLE_SCHEMA AS schema_name FROM information_schema.views WHERE table_schema = '${safeDbName}' ORDER BY TABLE_NAME` + : '', + safeDbName + ? `SELECT TABLE_NAME AS view_name, TABLE_SCHEMA AS schema_name, TABLE_TYPE AS table_type FROM information_schema.tables WHERE table_schema = '${safeDbName}' AND UPPER(TABLE_TYPE) LIKE '%VIEW%' ORDER BY TABLE_NAME` + : '', + dbIdent ? `SHOW FULL TABLES FROM \`${dbIdent}\` WHERE Table_type = 'VIEW'` : '', + dbIdent ? `SHOW FULL TABLES FROM \`${dbIdent}\`` : '', + `SHOW FULL TABLES WHERE Table_type = 'VIEW'`, + `SHOW FULL TABLES`, + ].filter(Boolean); +}; + export const resolveSidebarRuntimeDatabase = ( type: string, driver: string,