mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 18:39:54 +08:00
🐛 fix(sidebar): 修复 GDB 兼容库视图定位失败
- 增加 MySQL 兼容视图元数据查询回退 - 统一编辑器和左侧树的视图元数据查询 - 放宽视图分组下缺失节点类型时的可视定位兜底
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 { 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':
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'",
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user