🐛 fix(sidebar): 修复国产兼容库视图定位失败

- 统一 Sidebar 与 SQL 编辑器的元数据方言解析

- 兼容 GDB/GoldenDB/GreatDB 等 MySQL 兼容驱动的视图元数据

- 放宽左侧树视图定位对 objectType 节点的识别
This commit is contained in:
Syngnat
2026-06-10 11:07:50 +08:00
parent 8f86c4419b
commit d6f552d539
8 changed files with 99 additions and 18 deletions

View File

@@ -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()}`;

View File

@@ -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 => {

View File

@@ -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',

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();

View File

@@ -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');

View File

@@ -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';