🐛 fix(sidebar): 修复视图裸名定位失败

This commit is contained in:
Syngnat
2026-06-09 17:51:43 +08:00
parent 9fab48e64f
commit af51ead948
4 changed files with 213 additions and 20 deletions

View File

@@ -867,6 +867,30 @@ describe('QueryEditor external SQL save', () => {
});
});
it('prefers the unique schema-qualified view target when metadata also contains a bare view name', () => {
const views = [
{ dbName: 'SYSDBA', viewName: 'V_ACCOUNT', schemaName: undefined },
{ dbName: 'SYSDBA', viewName: 'SYSDBA.V_ACCOUNT', schemaName: 'SYSDBA' },
];
expect(resolveQueryEditorNavigationTarget(
'select * from V_ACCOUNT',
'select * from V_ACCOUNT'.length + 1,
'SYSDBA',
['SYSDBA'],
[],
views,
[],
[],
[],
)).toEqual({
type: 'view',
dbName: 'SYSDBA',
viewName: 'SYSDBA.V_ACCOUNT',
schemaName: 'SYSDBA',
});
});
it('opens a table tab on ctrl left click inside the editor', async () => {
editorState.value = 'select * from analytics.events where id = 1';
autoFetchState.visible = true;

View File

@@ -1432,6 +1432,16 @@ export const resolveQueryEditorNavigationTarget = (
&& meta.normalizedRawObjectName === exactQualifiedName
);
if (exact) {
if (!normalizedSchemaName && !exact.normalizedSchemaName) {
const schemaQualifiedMatches = metas.filter((meta) =>
meta.normalizedDbName === normalizedDbName
&& meta.normalizedObjectName === normalizedObjectName
&& Boolean(meta.normalizedSchemaName)
);
if (schemaQualifiedMatches.length === 1) {
return schemaQualifiedMatches[0];
}
}
return exact;
}

View File

@@ -414,6 +414,114 @@ describe('sidebarLocate', () => {
]);
});
it('finds a unique schema-qualified view when the locate request only has the view name', () => {
const target = resolveSidebarLocateTarget({
tabId: 'conn-1-SYSDBA-view-V_ACCOUNT',
connectionId: 'conn-1',
dbName: 'SYSDBA',
tableName: 'V_ACCOUNT',
objectGroup: 'views',
}, { groupBySchema: true });
const tree = [
{
key: 'conn-1',
children: [
{
key: 'conn-1-SYSDBA',
dataRef: { id: 'conn-1', dbName: 'SYSDBA' },
children: [
{
key: 'conn-1-SYSDBA-schema-SYSDBA',
children: [
{
key: 'conn-1-SYSDBA-schema-SYSDBA-views',
children: [
{
key: 'conn-1-SYSDBA-view-SYSDBA.V_ACCOUNT',
type: 'view',
dataRef: {
id: 'conn-1',
dbName: 'SYSDBA',
viewName: 'SYSDBA.V_ACCOUNT',
schemaName: 'SYSDBA',
},
},
],
},
],
},
],
},
],
},
];
expect(findSidebarNodePathForLocate(tree, target)).toEqual([
'conn-1',
'conn-1-SYSDBA',
'conn-1-SYSDBA-schema-SYSDBA',
'conn-1-SYSDBA-schema-SYSDBA-views',
'conn-1-SYSDBA-view-SYSDBA.V_ACCOUNT',
]);
});
it('does not guess a schema-qualified view when an unqualified locate request is ambiguous', () => {
const target = resolveSidebarLocateTarget({
tabId: 'conn-1-SYSDBA-view-V_ACCOUNT',
connectionId: 'conn-1',
dbName: 'SYSDBA',
tableName: 'V_ACCOUNT',
objectGroup: 'views',
}, { groupBySchema: true });
const tree = [
{
key: 'conn-1',
children: [
{
key: 'conn-1-SYSDBA',
dataRef: { id: 'conn-1', dbName: 'SYSDBA' },
children: [
{
key: 'conn-1-SYSDBA-schema-SYSDBA',
children: [
{
key: 'conn-1-SYSDBA-schema-SYSDBA-views',
children: [
{
key: 'conn-1-SYSDBA-view-SYSDBA.V_ACCOUNT',
type: 'view',
dataRef: { id: 'conn-1', dbName: 'SYSDBA', viewName: 'SYSDBA.V_ACCOUNT', schemaName: 'SYSDBA' },
},
],
},
],
},
{
key: 'conn-1-SYSDBA-schema-REPORT',
children: [
{
key: 'conn-1-SYSDBA-schema-REPORT-views',
children: [
{
key: 'conn-1-SYSDBA-view-REPORT.V_ACCOUNT',
type: 'view',
dataRef: { id: 'conn-1', dbName: 'SYSDBA', viewName: 'REPORT.V_ACCOUNT', schemaName: 'REPORT' },
},
],
},
],
},
],
},
],
},
];
expect(findSidebarNodePathForLocate(tree, target)).toBeNull();
});
it('finds external SQL file paths from loaded tree data', () => {
const target = resolveSidebarLocateTarget({
filePath: 'C:\\Users\\me\\sql\\report.sql',

View File

@@ -249,7 +249,12 @@ export const findSidebarNodePathByKey = (
return null;
};
const matchesLocateObjectName = (target: SidebarLocateTarget, nodeObjectName: string, nodeSchemaName: string): boolean => {
const matchesLocateObjectName = (
target: SidebarLocateTarget,
nodeObjectName: string,
nodeSchemaName: string,
options: { allowUnqualifiedSchemaMatch?: boolean } = {},
): boolean => {
const normalizedNodeName = toTrimmedString(nodeObjectName);
if (!normalizedNodeName) return false;
@@ -264,6 +269,9 @@ const matchesLocateObjectName = (target: SidebarLocateTarget, nodeObjectName: st
if (normalize(normalizedNodeName) === normalize(target.tableName)) return true;
if (!resolvedTargetSchema) {
if (options.allowUnqualifiedSchemaMatch) {
return normalize(nodeObject) === normalize(targetObject);
}
return !resolvedNodeSchema && normalize(nodeObject) === normalize(targetObject);
}
@@ -271,7 +279,11 @@ const matchesLocateObjectName = (target: SidebarLocateTarget, nodeObjectName: st
&& normalize(nodeObject) === normalize(targetObject);
};
const matchesLocateObjectNode = (node: SidebarLocateTreeNodeLike, target: SidebarLocateTarget): boolean => {
const matchesLocateObjectNode = (
node: SidebarLocateTreeNodeLike,
target: SidebarLocateTarget,
options: { allowUnqualifiedSchemaMatch?: boolean } = {},
): boolean => {
const dataRef = node.dataRef || {};
if (target.objectGroup === 'externalSqlFiles') {
@@ -288,26 +300,72 @@ const matchesLocateObjectNode = (node: SidebarLocateTreeNodeLike, target: Sideba
if (target.objectGroup === 'views') {
if (node.type !== 'view') return false;
return matchesLocateObjectName(target, toTrimmedString(dataRef.viewName || dataRef.tableName), toTrimmedString(dataRef.schemaName));
return matchesLocateObjectName(target, toTrimmedString(dataRef.viewName || dataRef.tableName), toTrimmedString(dataRef.schemaName), options);
}
if (target.objectGroup === 'materializedViews') {
if (node.type !== 'materialized-view') return false;
return matchesLocateObjectName(target, toTrimmedString(dataRef.viewName || dataRef.tableName), toTrimmedString(dataRef.schemaName));
return matchesLocateObjectName(target, toTrimmedString(dataRef.viewName || dataRef.tableName), toTrimmedString(dataRef.schemaName), options);
}
if (target.objectGroup === 'triggers') {
if (node.type !== 'db-trigger') return false;
return matchesLocateObjectName(target, toTrimmedString(dataRef.triggerName || dataRef.tableName), toTrimmedString(dataRef.schemaName));
return matchesLocateObjectName(target, toTrimmedString(dataRef.triggerName || dataRef.tableName), toTrimmedString(dataRef.schemaName), options);
}
if (target.objectGroup === 'routines') {
if (node.type !== 'routine') return false;
return matchesLocateObjectName(target, toTrimmedString(dataRef.routineName || dataRef.tableName), toTrimmedString(dataRef.schemaName));
return matchesLocateObjectName(target, toTrimmedString(dataRef.routineName || dataRef.tableName), toTrimmedString(dataRef.schemaName), options);
}
if (node.type !== 'table') return false;
return matchesLocateObjectName(target, toTrimmedString(dataRef.tableName), toTrimmedString(dataRef.schemaName));
return matchesLocateObjectName(target, toTrimmedString(dataRef.tableName), toTrimmedString(dataRef.schemaName), options);
};
const findSidebarNodePathForLocateByObject = (
nodes: SidebarLocateTreeNodeLike[],
target: SidebarLocateTarget,
options: { allowUnqualifiedSchemaMatch?: boolean } = {},
): string[] | null => {
for (const node of nodes) {
const nodeKey = String(node.key);
if (matchesLocateObjectNode(node, target, options)) {
return [nodeKey];
}
if (node.children) {
const childPath = findSidebarNodePathForLocateByObject(node.children, target, options);
if (childPath) {
return [nodeKey, ...childPath];
}
}
}
return null;
};
const collectSidebarNodePathsForLocateByObject = (
nodes: SidebarLocateTreeNodeLike[],
target: SidebarLocateTarget,
options: { allowUnqualifiedSchemaMatch?: boolean } = {},
ancestorPath: string[] = [],
): string[][] => {
const paths: string[][] = [];
for (const node of nodes) {
const nodeKey = String(node.key);
const path = [...ancestorPath, nodeKey];
if (matchesLocateObjectNode(node, target, options)) {
paths.push(path);
}
if (node.children) {
paths.push(...collectSidebarNodePathsForLocateByObject(node.children, target, options, path));
}
}
return paths;
};
const hasLocateTargetSchema = (target: SidebarLocateTarget): boolean => {
if (target.objectGroup === 'externalSqlFiles') return true;
return Boolean(toTrimmedString(target.schemaName) || splitSidebarQualifiedName(target.tableName).schemaName);
};
export const findSidebarNodePathForLocate = (
@@ -317,18 +375,11 @@ export const findSidebarNodePathForLocate = (
const exactPath = findSidebarNodePathByKey(nodes, target.targetKey);
if (exactPath) return exactPath;
for (const node of nodes) {
const nodeKey = String(node.key);
if (matchesLocateObjectNode(node, target)) {
return [nodeKey];
}
const strictPath = findSidebarNodePathForLocateByObject(nodes, target);
if (strictPath) return strictPath;
if (node.children) {
const childPath = findSidebarNodePathForLocate(node.children, target);
if (childPath) {
return [nodeKey, ...childPath];
}
}
}
return null;
if (hasLocateTargetSchema(target)) return null;
const relaxedPaths = collectSidebarNodePathsForLocateByObject(nodes, target, { allowUnqualifiedSchemaMatch: true });
return relaxedPaths.length === 1 ? relaxedPaths[0] : null;
};