mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 19:19:35 +08:00
🐛 fix(sidebar): 修复国产兼容库视图定位失败
- 统一 Sidebar 与 SQL 编辑器的元数据方言解析 - 兼容 GDB/GoldenDB/GreatDB 等 MySQL 兼容驱动的视图元数据 - 放宽左侧树视图定位对 objectType 节点的识别
This commit is contained in:
@@ -784,6 +784,17 @@ const getFirstRowValue = (row: Record<string, any>): string => {
|
||||
return '';
|
||||
};
|
||||
|
||||
const getMySQLShowTablesName = (row: Record<string, any>): 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<string>();
|
||||
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()}`;
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user