From c17d867aa669dc8efcc39aea28179413a3dfbc11 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 9 Jun 2026 18:45:30 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sidebar):=20=E5=85=9C?= =?UTF-8?q?=E5=BA=95=E5=AE=9A=E4=BD=8D=E8=A1=A8=E5=88=86=E6=94=AF=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E8=A7=86=E5=9B=BE=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/utils/sidebarLocate.test.ts | 143 +++++++++++++++++++++++ frontend/src/utils/sidebarLocate.ts | 18 +++ 2 files changed, 161 insertions(+) diff --git a/frontend/src/utils/sidebarLocate.test.ts b/frontend/src/utils/sidebarLocate.test.ts index 1a1fdb3..357fd50 100644 --- a/frontend/src/utils/sidebarLocate.test.ts +++ b/frontend/src/utils/sidebarLocate.test.ts @@ -466,6 +466,149 @@ describe('sidebarLocate', () => { ]); }); + it('finds a unique bare view node when metadata supplies schema separately', () => { + const target = resolveSidebarLocateTarget({ + tabId: 'stale-view-tab-id', + connectionId: 'conn-1', + dbName: 'SYSDBA', + tableName: 'V_ACCOUNT', + schemaName: 'SYSDBA', + 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-V_ACCOUNT', + type: 'view', + dataRef: { + id: 'conn-1', + dbName: 'SYSDBA', + viewName: 'V_ACCOUNT', + }, + }, + ], + }, + ], + }, + ], + }, + ]; + + expect(findSidebarNodePathForLocate(tree, target)).toEqual([ + 'conn-1', + 'conn-1-SYSDBA', + 'conn-1-SYSDBA-views', + 'conn-1-SYSDBA-view-V_ACCOUNT', + ]); + }); + + it('falls back to a table-like node when a view is only present in the tables branch', () => { + 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-tables', + children: [ + { + key: 'conn-1-SYSDBA-V_ACCOUNT', + type: 'table', + dataRef: { + id: 'conn-1', + dbName: 'SYSDBA', + tableName: 'V_ACCOUNT', + }, + }, + ], + }, + ], + }, + ], + }, + ]; + + expect(findSidebarNodePathForLocate(tree, target)).toEqual([ + 'conn-1', + 'conn-1-SYSDBA', + 'conn-1-SYSDBA-tables', + 'conn-1-SYSDBA-V_ACCOUNT', + ]); + }); + + it('falls back to a unique schema-qualified table-like node for an unqualified view request', () => { + const target = resolveSidebarLocateTarget({ + tabId: 'stale-view-tab-id', + 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-tables', + children: [ + { + key: 'conn-1-SYSDBA-SYSDBA.V_ACCOUNT', + type: 'table', + dataRef: { + id: 'conn-1', + dbName: 'SYSDBA', + tableName: '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-tables', + 'conn-1-SYSDBA-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', diff --git a/frontend/src/utils/sidebarLocate.ts b/frontend/src/utils/sidebarLocate.ts index 3aa42d2..bb310d0 100644 --- a/frontend/src/utils/sidebarLocate.ts +++ b/frontend/src/utils/sidebarLocate.ts @@ -368,6 +368,10 @@ const hasLocateTargetSchema = (target: SidebarLocateTarget): boolean => { return Boolean(toTrimmedString(target.schemaName) || splitSidebarQualifiedName(target.tableName).schemaName); }; +const shouldFallbackViewLocateToTableNode = (target: SidebarLocateTarget): boolean => ( + target.objectGroup === 'views' || target.objectGroup === 'materializedViews' +); + export const findSidebarNodePathForLocate = ( nodes: SidebarLocateTreeNodeLike[], target: SidebarLocateTarget, @@ -378,6 +382,20 @@ export const findSidebarNodePathForLocate = ( const strictPath = findSidebarNodePathForLocateByObject(nodes, target); if (strictPath) return strictPath; + if (shouldFallbackViewLocateToTableNode(target)) { + const tableLikeTarget = { ...target, objectGroup: 'tables' as const }; + const tableLikePaths = collectSidebarNodePathsForLocateByObject(nodes, tableLikeTarget); + if (tableLikePaths.length === 1) return tableLikePaths[0]; + if (!hasLocateTargetSchema(target)) { + const relaxedTableLikePaths = collectSidebarNodePathsForLocateByObject( + nodes, + tableLikeTarget, + { allowUnqualifiedSchemaMatch: true }, + ); + if (relaxedTableLikePaths.length === 1) return relaxedTableLikePaths[0]; + } + } + if (hasLocateTargetSchema(target)) return null; const relaxedPaths = collectSidebarNodePathsForLocateByObject(nodes, target, { allowUnqualifiedSchemaMatch: true });