🐛 fix(sidebar): 兼容国产库视图定位

- 兼容 MySQL 协议国产库返回的 SYSTEM VIEW / BASE VIEW 类型
- 同步 SQL 编辑器与左侧树的视图元数据识别逻辑
- 增加节点元数据缺失时的唯一可视标识兜底定位
This commit is contained in:
Syngnat
2026-06-09 20:31:24 +08:00
parent 75b60f94d2
commit d78c4481f0
6 changed files with 147 additions and 5 deletions

View File

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

View File

@@ -40,6 +40,7 @@ export interface SidebarLocateTarget {
export interface SidebarLocateTreeNodeLike {
key: string | number;
title?: unknown;
type?: string;
dataRef?: Record<string, any>;
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);

View File

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

View File

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