mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 18:39:54 +08:00
🐛 fix(sidebar): 兼容国产库视图定位
- 兼容 MySQL 协议国产库返回的 SYSTEM VIEW / BASE VIEW 类型 - 同步 SQL 编辑器与左侧树的视图元数据识别逻辑 - 增加节点元数据缺失时的唯一可视标识兜底定位
This commit is contained in:
@@ -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<string>();
|
||||
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);
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
18
frontend/src/utils/sidebarMetadata.test.ts
Normal file
18
frontend/src/utils/sidebarMetadata.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user