From d78c4481f0aca6b8956f76afbbbdd8b2157ef19c Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 9 Jun 2026 20:31:24 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sidebar):=20=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E5=9B=BD=E4=BA=A7=E5=BA=93=E8=A7=86=E5=9B=BE=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 兼容 MySQL 协议国产库返回的 SYSTEM VIEW / BASE VIEW 类型 - 同步 SQL 编辑器与左侧树的视图元数据识别逻辑 - 增加节点元数据缺失时的唯一可视标识兜底定位 --- frontend/src/components/QueryEditor.tsx | 11 ++-- frontend/src/components/Sidebar.tsx | 9 +++- frontend/src/utils/sidebarLocate.test.ts | 45 ++++++++++++++++ frontend/src/utils/sidebarLocate.ts | 63 ++++++++++++++++++++++ frontend/src/utils/sidebarMetadata.test.ts | 18 +++++++ frontend/src/utils/sidebarMetadata.ts | 6 +++ 6 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 frontend/src/utils/sidebarMetadata.test.ts diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index ee42d3d..5a13800 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 { normalizeSidebarViewName } from '../utils/sidebarMetadata'; +import { isSidebarViewTableType, normalizeSidebarViewName } from '../utils/sidebarMetadata'; import { SIDEBAR_SQL_EDITOR_DRAG_MIME, decodeSidebarSqlEditorDragPayload, hasSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag'; import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert'; import { @@ -818,6 +818,11 @@ const buildCompletionViewsMetadataQuerySpecs = (dialect: string, dbName: string) ? `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' }, ]); @@ -2506,8 +2511,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const seenViews = new Set(); viewResults.forEach((queryResult) => { queryResult.rows.forEach((row) => { - const tableType = String(getCaseInsensitiveValue(row, ['table_type', 'table type', 'type']) || '').trim().toUpperCase(); - if (tableType && tableType !== 'VIEW') return; + const tableType = getCaseInsensitiveValue(row, ['table_type', 'table type', 'type']); + if (!isSidebarViewTableType(tableType)) return; const schemaName = String(getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'table_schema', 'db']) || '').trim(); const rawViewName = String(getCaseInsensitiveValue(row, ['view_name', 'viewname', 'table_name', 'name']) || '').trim() || getFirstRowValue(row); const normalizedViewName = normalizeSidebarViewName(metadataDialect, dbName, schemaName, rawViewName); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index d9cf7b7..ae2a0b4 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 { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata'; +import { isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata'; import { splitQualifiedNameLast } from '../utils/qualifiedName'; import { buildStarRocksMaterializedViewPreviewSql } from './tableDesignerSchemaSql'; import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol'; @@ -1399,6 +1399,11 @@ const Sidebar: React.FC<{ ? `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` }, ]); @@ -1610,7 +1615,7 @@ const Sidebar: React.FC<{ results.forEach((queryResult) => { queryResult.rows.forEach((row) => { const tableType = getCaseInsensitiveValue(row, ['table_type', 'table type', 'type']); - if (tableType && tableType.toUpperCase() !== 'VIEW') return; + if (!isSidebarViewTableType(tableType)) return; const schemaName = getCaseInsensitiveValue(row, ['schema_name', 'schemaname', 'owner', 'table_schema', 'db']); const viewName = getCaseInsensitiveValue(row, ['view_name', 'viewname', 'table_name', 'name']) diff --git a/frontend/src/utils/sidebarLocate.test.ts b/frontend/src/utils/sidebarLocate.test.ts index 987ce94..c14e4c7 100644 --- a/frontend/src/utils/sidebarLocate.test.ts +++ b/frontend/src/utils/sidebarLocate.test.ts @@ -557,6 +557,51 @@ describe('sidebarLocate', () => { ]); }); + it('finds a view node by title when the tree node is missing object metadata', () => { + const target = resolveSidebarLocateTarget({ + tabId: 'stale-view-tab-id', + connectionId: 'conn-1', + dbName: 'SYSDBA', + tableName: 'V_ACCOUNT', + objectGroup: 'views', + }, { groupBySchema: false }); + + const tree = [ + { + key: 'conn-1', + children: [ + { + key: 'conn-1-SYSDBA', + dataRef: { id: 'conn-1', dbName: 'SYSDBA' }, + children: [ + { + key: 'conn-1-SYSDBA-views', + children: [ + { + key: 'conn-1-SYSDBA-view-generated-key', + title: 'V_ACCOUNT', + type: 'view', + dataRef: { + id: 'conn-1', + dbName: 'SYSDBA', + }, + }, + ], + }, + ], + }, + ], + }, + ]; + + expect(findSidebarNodePathForLocate(tree, target)).toEqual([ + 'conn-1', + 'conn-1-SYSDBA', + 'conn-1-SYSDBA-views', + 'conn-1-SYSDBA-view-generated-key', + ]); + }); + it('falls back from a schema-qualified view request to a bare table-like node in the same database', () => { const target = resolveSidebarLocateTarget({ tabId: 'stale-view-tab-id', diff --git a/frontend/src/utils/sidebarLocate.ts b/frontend/src/utils/sidebarLocate.ts index 62debbd..795b6c4 100644 --- a/frontend/src/utils/sidebarLocate.ts +++ b/frontend/src/utils/sidebarLocate.ts @@ -40,6 +40,7 @@ export interface SidebarLocateTarget { export interface SidebarLocateTreeNodeLike { key: string | number; + title?: unknown; type?: string; dataRef?: Record; children?: SidebarLocateTreeNodeLike[]; @@ -372,6 +373,65 @@ const collectSidebarNodePathsForLocateByObject = ( return paths; }; +const getVisualNodeObjectName = ( + node: SidebarLocateTreeNodeLike, + target: SidebarLocateTarget, +): string => { + const title = toTrimmedString(node.title); + if (title && title !== '[object Object]') return title; + + const nodeKey = toTrimmedString(node.key); + const keyPrefixes = target.objectGroup === 'materializedViews' + ? [`${target.databaseKey}-materialized-view-`] + : target.objectGroup === 'views' + ? [`${target.databaseKey}-view-`] + : target.objectGroup === 'triggers' + ? [`${target.databaseKey}-trigger-`] + : target.objectGroup === 'routines' + ? [`${target.databaseKey}-routine-`] + : [`${target.databaseKey}-`]; + + const matchedPrefix = keyPrefixes.find((prefix) => nodeKey.startsWith(prefix)); + return matchedPrefix ? nodeKey.slice(matchedPrefix.length) : ''; +}; + +const matchesLocateObjectNodeByVisualIdentity = ( + node: SidebarLocateTreeNodeLike, + target: SidebarLocateTarget, + path: string[], +): boolean => { + if (!path.includes(target.databaseKey)) return false; + + if (target.objectGroup === 'views' && node.type !== 'view') return false; + if (target.objectGroup === 'materializedViews' && node.type !== 'materialized-view') 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 === 'externalSqlFiles') return false; + + const schemaName = toTrimmedString(node.dataRef?.schemaName); + return matchesLocateObjectName(target, getVisualNodeObjectName(node, target), schemaName, { allowUnqualifiedSchemaMatch: true }); +}; + +const collectSidebarNodePathsForLocateByVisualIdentity = ( + nodes: SidebarLocateTreeNodeLike[], + target: SidebarLocateTarget, + ancestorPath: string[] = [], +): string[][] => { + const paths: string[][] = []; + for (const node of nodes) { + const nodeKey = String(node.key); + const path = [...ancestorPath, nodeKey]; + if (matchesLocateObjectNodeByVisualIdentity(node, target, path)) { + paths.push(path); + } + if (node.children) { + paths.push(...collectSidebarNodePathsForLocateByVisualIdentity(node.children, target, path)); + } + } + return paths; +}; + const hasLocateTargetSchema = (target: SidebarLocateTarget): boolean => { if (target.objectGroup === 'externalSqlFiles') return true; return Boolean(toTrimmedString(target.schemaName) || splitSidebarQualifiedName(target.tableName).schemaName); @@ -391,6 +451,9 @@ export const findSidebarNodePathForLocate = ( const strictPath = findSidebarNodePathForLocateByObject(nodes, target); if (strictPath) return strictPath; + const visualIdentityPaths = collectSidebarNodePathsForLocateByVisualIdentity(nodes, target); + if (visualIdentityPaths.length === 1) return visualIdentityPaths[0]; + if (shouldFallbackViewLocateToTableNode(target)) { const tableLikeTarget = { ...target, objectGroup: 'tables' as const }; const tableLikePaths = collectSidebarNodePathsForLocateByObject(nodes, tableLikeTarget); diff --git a/frontend/src/utils/sidebarMetadata.test.ts b/frontend/src/utils/sidebarMetadata.test.ts new file mode 100644 index 0000000..14a06a0 --- /dev/null +++ b/frontend/src/utils/sidebarMetadata.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; + +import { isSidebarViewTableType, normalizeSidebarViewName } from './sidebarMetadata'; + +describe('sidebarMetadata', () => { + it('normalizes MySQL-compatible view names without schema prefixes', () => { + expect(normalizeSidebarViewName('mysql', 'SYSDBA', 'SYSDBA', 'SYSDBA.V_ACCOUNT')).toBe('V_ACCOUNT'); + }); + + it('accepts MySQL-compatible view type variants returned by domestic databases', () => { + expect(isSidebarViewTableType(undefined)).toBe(true); + expect(isSidebarViewTableType('VIEW')).toBe(true); + expect(isSidebarViewTableType('SYSTEM VIEW')).toBe(true); + expect(isSidebarViewTableType('BASE VIEW')).toBe(true); + expect(isSidebarViewTableType('BASE TABLE')).toBe(false); + expect(isSidebarViewTableType('MATERIALIZED VIEW')).toBe(false); + }); +}); diff --git a/frontend/src/utils/sidebarMetadata.ts b/frontend/src/utils/sidebarMetadata.ts index 2950749..4a1a8f6 100644 --- a/frontend/src/utils/sidebarMetadata.ts +++ b/frontend/src/utils/sidebarMetadata.ts @@ -47,6 +47,12 @@ export const normalizeSidebarViewName = (dialect: string, dbName: string, schema return `${normalizedSchemaName}.${normalizedViewName}`; }; +export const isSidebarViewTableType = (tableType: unknown): boolean => { + const normalizedType = String(tableType ?? '').trim().toUpperCase(); + if (!normalizedType) return true; + return normalizedType.includes('VIEW') && !normalizedType.includes('MATERIALIZED'); +}; + export const resolveSidebarRuntimeDatabase = ( type: string, driver: string,