diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 5a13800..85a2126 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -784,6 +784,17 @@ const getFirstRowValue = (row: Record): string => { return ''; }; +const getMySQLShowTablesName = (row: Record): 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 normalizeMetadataQuerySpecs = (specs: MetadataQuerySpec[]): MetadataQuerySpec[] => { const seen = new Set(); const normalized: MetadataQuerySpec[] = []; @@ -2514,7 +2525,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc 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 rawViewName = String(getCaseInsensitiveValue(row, ['view_name', 'viewname', 'table_name', 'name']) || '').trim() + || getMySQLShowTablesName(row) + || getFirstRowValue(row); const normalizedViewName = normalizeSidebarViewName(metadataDialect, dbName, schemaName, rawViewName); if (!normalizedViewName) return; const uniqueKey = `${dbName.toLowerCase()}@@${normalizedViewName.toLowerCase()}`; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 7136b20..3e60159 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, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata'; +import { isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata'; import { splitQualifiedNameLast } from '../utils/qualifiedName'; import { buildStarRocksMaterializedViewPreviewSql } from './tableDesignerSchemaSql'; import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol'; @@ -1160,17 +1160,11 @@ const Sidebar: React.FC<{ }; const getMetadataDialect = (conn: SavedConnection | undefined): string => { - const type = normalizeDriverType(String(conn?.config?.type || '').trim()); - if (type === 'custom') { - const driver = normalizeDriverType(String(conn?.config?.driver || '').trim()); - if (driver === 'diros' || driver === 'doris') return 'mysql'; - if (driver === 'oceanbase') return normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle' ? 'oracle' : 'mysql'; - return driver; - } - if (type === 'oceanbase' && normalizeOceanBaseProtocol(conn?.config?.oceanBaseProtocol) === 'oracle') return 'oracle'; - if (type === 'mariadb' || type === 'oceanbase' || type === 'diros' || type === 'sphinx') return 'mysql'; - if (type === 'dameng') return 'dm'; - return type; + return resolveSidebarMetadataDialect( + conn?.config?.type || '', + conn?.config?.driver || '', + conn?.config?.oceanBaseProtocol, + ); }; const supportsDatabaseEvents = (conn: SavedConnection | undefined): boolean => { diff --git a/frontend/src/utils/sidebarLocate.test.ts b/frontend/src/utils/sidebarLocate.test.ts index 38a5eb7..d8f87be 100644 --- a/frontend/src/utils/sidebarLocate.test.ts +++ b/frontend/src/utils/sidebarLocate.test.ts @@ -573,6 +573,53 @@ describe('sidebarLocate', () => { ]); }); + it('finds a mysql-compatible view node when objectType carries the view identity', () => { + 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: 'opaque-view-node', + type: 'database-object', + dataRef: { + id: 'conn-1', + dbName: 'GDB_APP', + tableName: 'V_ACCOUNT', + objectType: 'view', + }, + }, + ], + }, + ], + }, + ], + }, + ]; + + expect(findSidebarNodePathForLocate(tree, target)).toEqual([ + 'conn-1', + 'conn-1-GDB_APP', + 'conn-1-GDB_APP-views', + 'opaque-view-node', + ]); + }); + it('falls back to a table-like node when a view is only present in the tables branch', () => { const target = resolveSidebarLocateTarget({ tabId: 'stale-view-tab-id', diff --git a/frontend/src/utils/sidebarLocate.ts b/frontend/src/utils/sidebarLocate.ts index 699d5de..e22bd27 100644 --- a/frontend/src/utils/sidebarLocate.ts +++ b/frontend/src/utils/sidebarLocate.ts @@ -308,6 +308,7 @@ const matchesLocateObjectNode = ( options: { allowUnqualifiedSchemaMatch?: boolean } = {}, ): boolean => { const dataRef = node.dataRef || {}; + const nodeObjectType = normalizeLocateName(toTrimmedString(dataRef.objectType || dataRef.objectKind)); if (target.objectGroup === 'externalSqlFiles') { return node.type === 'external-sql-file' @@ -322,12 +323,12 @@ const matchesLocateObjectNode = ( } if (target.objectGroup === 'views') { - if (node.type !== 'view') return false; + if (node.type !== 'view' && nodeObjectType !== 'view' && nodeObjectType !== 'views') return false; return matchesLocateObjectName(target, toTrimmedString(dataRef.viewName || dataRef.tableName), toTrimmedString(dataRef.schemaName), options); } if (target.objectGroup === 'materializedViews') { - if (node.type !== 'materialized-view') return false; + if (node.type !== 'materialized-view' && nodeObjectType !== 'materialized-view' && nodeObjectType !== 'materializedviews') return false; return matchesLocateObjectName(target, toTrimmedString(dataRef.viewName || dataRef.tableName), toTrimmedString(dataRef.schemaName), options); } @@ -414,9 +415,10 @@ const matchesLocateObjectNodeByVisualIdentity = ( path: string[], ): boolean => { if (!path.includes(target.databaseKey)) return false; + const nodeObjectType = normalizeLocateName(toTrimmedString(node.dataRef?.objectType || node.dataRef?.objectKind)); - if (target.objectGroup === 'views' && node.type !== 'view') return false; - if (target.objectGroup === 'materializedViews' && node.type !== 'materialized-view') return false; + 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; diff --git a/frontend/src/utils/sidebarMetadata.test.ts b/frontend/src/utils/sidebarMetadata.test.ts index 14a06a0..746025e 100644 --- a/frontend/src/utils/sidebarMetadata.test.ts +++ b/frontend/src/utils/sidebarMetadata.test.ts @@ -1,12 +1,19 @@ import { describe, expect, it } from 'vitest'; -import { isSidebarViewTableType, normalizeSidebarViewName } from './sidebarMetadata'; +import { isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect } 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('uses MySQL metadata queries for custom MySQL-compatible domestic drivers', () => { + expect(resolveSidebarMetadataDialect('custom', 'gdb')).toBe('mysql'); + expect(resolveSidebarMetadataDialect('custom', 'goldendb')).toBe('mysql'); + expect(resolveSidebarMetadataDialect('custom', 'greatdb')).toBe('mysql'); + expect(resolveSidebarMetadataDialect('custom', 'doris')).toBe('mysql'); + }); + it('accepts MySQL-compatible view type variants returned by domestic databases', () => { expect(isSidebarViewTableType(undefined)).toBe(true); expect(isSidebarViewTableType('VIEW')).toBe(true); diff --git a/frontend/src/utils/sidebarMetadata.ts b/frontend/src/utils/sidebarMetadata.ts index 4a1a8f6..8b6a3f1 100644 --- a/frontend/src/utils/sidebarMetadata.ts +++ b/frontend/src/utils/sidebarMetadata.ts @@ -1,5 +1,6 @@ import { normalizeOceanBaseProtocol } from './oceanBaseProtocol'; import { splitQualifiedNameLast } from './qualifiedName'; +import { resolveSqlDialect } from './sqlDialect'; const normalizeSidebarConnectionDialect = (type: string, driver: string, oceanBaseProtocol?: string): string => { const normalizedType = String(type || '').trim().toLowerCase(); @@ -22,6 +23,15 @@ const normalizeSidebarConnectionDialect = (type: string, driver: string, oceanBa return normalizedType; }; +export const resolveSidebarMetadataDialect = (type: string, driver = '', oceanBaseProtocol?: unknown): string => { + const dialect = String(resolveSqlDialect(type, driver, { oceanBaseProtocol })).trim().toLowerCase(); + if (dialect === 'diros' || dialect === 'sphinx' || dialect === 'mariadb' || dialect === 'oceanbase') { + return 'mysql'; + } + if (dialect === 'dameng') return 'dm'; + return dialect; +}; + export const normalizeSidebarViewName = (dialect: string, dbName: string, schemaName: string, viewName: string): string => { const normalizedDialect = String(dialect || '').trim().toLowerCase(); const normalizedDbName = String(dbName || '').trim(); diff --git a/frontend/src/utils/sqlDialect.test.ts b/frontend/src/utils/sqlDialect.test.ts index a16a625..5c279c0 100644 --- a/frontend/src/utils/sqlDialect.test.ts +++ b/frontend/src/utils/sqlDialect.test.ts @@ -24,6 +24,9 @@ describe('sqlDialect', () => { expect(resolveSqlDialect('custom', 'kingbase8')).toBe('kingbase'); expect(resolveSqlDialect('custom', 'dm8')).toBe('dameng'); expect(resolveSqlDialect('custom', 'mariadb')).toBe('mariadb'); + expect(resolveSqlDialect('custom', 'gdb')).toBe('mysql'); + expect(resolveSqlDialect('custom', 'goldendb')).toBe('mysql'); + expect(resolveSqlDialect('custom', 'greatdb')).toBe('mysql'); expect(resolveSqlDialect('custom', 'open_gauss')).toBe('opengauss'); expect(resolveSqlDialect('Elasticsearch')).toBe('elasticsearch'); expect(resolveSqlDialect('custom', 'elastic')).toBe('elasticsearch'); diff --git a/frontend/src/utils/sqlDialect.ts b/frontend/src/utils/sqlDialect.ts index b9893ab..ee58138 100644 --- a/frontend/src/utils/sqlDialect.ts +++ b/frontend/src/utils/sqlDialect.ts @@ -94,6 +94,10 @@ export const resolveSqlDialect = ( case 'kingbasees': case 'kingbasev8': return 'kingbase'; + case 'gdb': + case 'goldendb': + case 'greatdb': + return 'mysql'; case 'mariadb': case 'oceanbase': case 'mysql': @@ -119,6 +123,7 @@ export const resolveSqlDialect = ( if (source.includes('postgres')) return 'postgres'; if (source.includes('oceanbase')) return 'oceanbase'; if (source.includes('mariadb')) return 'mariadb'; + if (source.includes('goldendb') || source.includes('greatdb')) return 'mysql'; if (source.includes('mysql')) return 'mysql'; if (source.includes('doris') || source.includes('diros')) return 'diros'; if (source.includes('starrocks')) return 'starrocks';