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

- 增加 MySQL 兼容视图元数据查询回退

- 统一编辑器和左侧树的视图元数据查询

- 放宽视图分组下缺失节点类型时的可视定位兜底
This commit is contained in:
Syngnat
2026-06-10 12:14:27 +08:00
parent 2c7962f5d3
commit e16082af9a
6 changed files with 102 additions and 38 deletions

View File

@@ -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 { isSidebarViewTableType, normalizeSidebarViewName } from '../utils/sidebarMetadata';
import { buildMySQLCompatibleViewMetadataSqls, isSidebarViewTableType, normalizeSidebarViewName } from '../utils/sidebarMetadata';
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, decodeSidebarSqlEditorDragPayload, hasSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag';
import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert';
import {
@@ -822,21 +822,9 @@ const buildCompletionViewsMetadataQuerySpecs = (dialect: string, dbName: string)
switch (dialect) {
case 'mysql':
case 'starrocks': {
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
return normalizeMetadataQuerySpecs([
{
sql: safeDbName
? `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' },
]);
return normalizeMetadataQuerySpecs(
buildMySQLCompatibleViewMetadataSqls(dbName).map((sql) => ({ sql })),
);
}
case 'postgres':
case 'kingbase':

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, resolveSidebarMetadataDialect, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
import { buildMySQLCompatibleViewMetadataSqls, isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
import { splitQualifiedNameLast } from '../utils/qualifiedName';
import { buildStarRocksMaterializedViewPreviewSql } from './tableDesignerSchemaSql';
import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
@@ -1386,21 +1386,9 @@ const Sidebar: React.FC<{
switch (dialect) {
case 'mysql':
case 'starrocks': {
const dbIdent = String(dbName || '').replace(/`/g, '``').trim();
return normalizeMetadataQuerySpecs([
{
sql: safeDbName
? `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` },
]);
return normalizeMetadataQuerySpecs(
buildMySQLCompatibleViewMetadataSqls(dbName).map((sql) => ({ sql })),
);
}
case 'postgres':
case 'kingbase':

View File

@@ -793,6 +793,48 @@ describe('sidebarLocate', () => {
]);
});
it('finds a view node by title under the views group when node type metadata is missing', () => {
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: 'conn-1-GDB_APP-view-generated-key',
title: 'V_ACCOUNT',
dataRef: {},
},
],
},
],
},
],
},
];
expect(findSidebarNodePathForLocate(tree, target)).toEqual([
'conn-1',
'conn-1-GDB_APP',
'conn-1-GDB_APP-views',
'conn-1-GDB_APP-view-generated-key',
]);
});
it('finds a schema-qualified view request by visual title when the node has no schema metadata', () => {
const target = resolveSidebarLocateTarget({
tabId: 'stale-view-tab-id',

View File

@@ -409,6 +409,24 @@ const getVisualNodeObjectName = (
return matchedPrefix ? nodeKey.slice(matchedPrefix.length) : '';
};
const getLocateObjectGroupPathSuffix = (objectGroup: SidebarLocateObjectGroup): string => {
if (objectGroup === 'externalSqlFiles') return 'external-sql-root';
return objectGroup.toLowerCase();
};
const isPathInsideLocateObjectGroup = (
path: string[],
target: SidebarLocateTarget,
): boolean => {
if (target.objectGroup === 'externalSqlFiles') return false;
const normalizedObjectGroupKey = normalizeLocateName(target.objectGroupKey);
const groupSuffix = getLocateObjectGroupPathSuffix(target.objectGroup);
return path.some((key) => {
const normalizedKey = normalizeLocateName(key);
return normalizedKey === normalizedObjectGroupKey || normalizedKey.endsWith(`-${groupSuffix}`);
});
};
const matchesLocateObjectNodeByVisualIdentity = (
node: SidebarLocateTreeNodeLike,
target: SidebarLocateTarget,
@@ -416,12 +434,13 @@ const matchesLocateObjectNodeByVisualIdentity = (
): boolean => {
if (!path.includes(target.databaseKey)) return false;
const nodeObjectType = normalizeLocateName(toTrimmedString(node.dataRef?.objectType || node.dataRef?.objectKind));
const insideExpectedGroup = isPathInsideLocateObjectGroup(path, target);
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;
if (target.objectGroup === 'views' && node.type !== 'view' && nodeObjectType !== 'view' && nodeObjectType !== 'views' && !insideExpectedGroup) return false;
if (target.objectGroup === 'materializedViews' && node.type !== 'materialized-view' && nodeObjectType !== 'materialized-view' && nodeObjectType !== 'materializedviews' && !insideExpectedGroup) return false;
if (target.objectGroup === 'triggers' && node.type !== 'db-trigger' && !insideExpectedGroup) return false;
if (target.objectGroup === 'routines' && node.type !== 'routine' && !insideExpectedGroup) return false;
if (target.objectGroup === 'tables' && node.type !== 'table' && !insideExpectedGroup) return false;
if (target.objectGroup === 'externalSqlFiles') return false;
const schemaName = toTrimmedString(node.dataRef?.schemaName);

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect } from './sidebarMetadata';
import { buildMySQLCompatibleViewMetadataSqls, isSidebarViewTableType, normalizeSidebarViewName, resolveSidebarMetadataDialect } from './sidebarMetadata';
describe('sidebarMetadata', () => {
it('normalizes MySQL-compatible view names without schema prefixes', () => {
@@ -22,4 +22,11 @@ describe('sidebarMetadata', () => {
expect(isSidebarViewTableType('BASE TABLE')).toBe(false);
expect(isSidebarViewTableType('MATERIALIZED VIEW')).toBe(false);
});
it('adds SHOW FULL TABLES view-only fallbacks for MySQL-compatible databases', () => {
expect(buildMySQLCompatibleViewMetadataSqls('GDB_APP')).toEqual(expect.arrayContaining([
"SHOW FULL TABLES FROM `GDB_APP` WHERE Table_type = 'VIEW'",
"SHOW FULL TABLES WHERE Table_type = 'VIEW'",
]));
});
});

View File

@@ -2,6 +2,9 @@ import { normalizeOceanBaseProtocol } from './oceanBaseProtocol';
import { splitQualifiedNameLast } from './qualifiedName';
import { resolveSqlDialect } from './sqlDialect';
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
const escapeBacktickIdentifier = (raw: string): string => String(raw || '').replace(/`/g, '``');
const normalizeSidebarConnectionDialect = (type: string, driver: string, oceanBaseProtocol?: string): string => {
const normalizedType = String(type || '').trim().toLowerCase();
if (normalizedType === 'custom') {
@@ -63,6 +66,23 @@ export const isSidebarViewTableType = (tableType: unknown): boolean => {
return normalizedType.includes('VIEW') && !normalizedType.includes('MATERIALIZED');
};
export const buildMySQLCompatibleViewMetadataSqls = (dbName: string): string[] => {
const safeDbName = escapeSQLLiteral(dbName);
const dbIdent = escapeBacktickIdentifier(dbName).trim();
return [
safeDbName
? `SELECT TABLE_NAME AS view_name, TABLE_SCHEMA AS schema_name FROM information_schema.views WHERE table_schema = '${safeDbName}' ORDER BY TABLE_NAME`
: '',
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`
: '',
dbIdent ? `SHOW FULL TABLES FROM \`${dbIdent}\` WHERE Table_type = 'VIEW'` : '',
dbIdent ? `SHOW FULL TABLES FROM \`${dbIdent}\`` : '',
`SHOW FULL TABLES WHERE Table_type = 'VIEW'`,
`SHOW FULL TABLES`,
].filter(Boolean);
};
export const resolveSidebarRuntimeDatabase = (
type: string,
driver: string,