fix(sidebar): discover object groups in collapsed databases

Closes #9174
This commit is contained in:
zipg
2026-09-22 01:13:47 +08:00
committed by GitHub
parent 140d4870b2
commit 6d74bcbadb
4 changed files with 87 additions and 2 deletions
@@ -25,6 +25,7 @@ import {
} from "@/lib/sidebar/sidebarSearchTree";
import { createSidebarLabelMatcher } from "@/lib/sidebar/sidebarSearch";
import { collectSidebarRegexIndexScopes, resolveSidebarRemoteSearchQuery, resolveSidebarSearchDispatchMode } from "@/lib/sidebar/sidebarRegexSearchIndex";
import { needsSidebarObjectGroupDiscovery } from "@/lib/sidebar/sidebarSearchDiscovery";
import { createSidebarSearchExpansionState } from "@/lib/sidebar/sidebarSearchExpansionState";
import { createSidebarSearchLoadingTracker } from "@/lib/sidebar/sidebarSearchLoadingTracker";
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut, isViewTableDdlShortcut } from "@/lib/editor/keyboardShortcuts";
@@ -405,7 +406,7 @@ function collectExpandedObjectSearchTargets(node: TreeNode, tasks: SidebarSearch
}
return;
}
if (refreshedNodeIds && isSidebarSearchContainer(node) && !node.children?.length && (!scheduledNodeIds || !scheduledNodeIds.has(node.id))) {
if (refreshedNodeIds && isSidebarSearchContainer(node) && needsSidebarObjectGroupDiscovery(node, searchableObjectGroupTypes) && (!scheduledNodeIds || !scheduledNodeIds.has(node.id))) {
scheduledNodeIds?.add(node.id);
const wasCollapsed = node.isExpanded !== true;
searchExpansionState.markFiltered(node.id, wasCollapsed);
@@ -7,7 +7,11 @@ const source = readFileSync(new URL("../ConnectionTree.vue", import.meta.url), "
describe("ConnectionTree global search loading", () => {
it("discovers collapsed database and schema containers before searching object groups", () => {
expect(source).toContain("function isSidebarSearchContainer(node: TreeNode)");
expect(source).toMatch(/isSidebarSearchContainer\(node\) && !node\.children\?\.length/);
// A collapsed database already holds the saved-SQL root, so it has children
// even when its object groups were never fetched; the search has to look for
// a searchable group instead of an empty child list.
expect(source).toMatch(/isSidebarSearchContainer\(node\) && needsSidebarObjectGroupDiscovery\(node, searchableObjectGroupTypes\)/);
expect(source).not.toMatch(/isSidebarSearchContainer\(node\) && !node\.children\?\.length/);
expect(source).toContain("store.loadTreeNodeChildren(node, { force: true, expectedSidebarSearchQuery: store.sidebarSearchQuery })");
expect(source).toContain("searchExpansionState.markFiltered(node.id, wasCollapsed)");
expect(source).toMatch(/if \(refreshedNodeIds && node\.children\) \{[\s\S]*?searchableObjectGroupTypes\.has\(child\.type\)/);
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import type { TreeNode, TreeNodeType } from "@/types/database";
import { needsSidebarObjectGroupDiscovery } from "@/lib/sidebar/sidebarSearchDiscovery";
const objectGroupTypes: ReadonlySet<TreeNodeType> = new Set<TreeNodeType>(["group-tables", "group-views"]);
function node(partial: Partial<TreeNode> & Pick<TreeNode, "id" | "type">): TreeNode {
return { label: partial.id, ...partial };
}
describe("needsSidebarObjectGroupDiscovery", () => {
it("asks for discovery while a container only holds its saved SQL root", () => {
// Reproduces #9174: the database node already has a child (the saved SQL
// root) before its object groups are fetched, so an empty-child check would
// skip it and its tables would never join the search.
const database = node({
id: "mysql/dbx",
type: "database",
connectionId: "c1",
database: "dbx",
children: [node({ id: "saved-sql-root", type: "saved-sql-root", connectionId: "c1", database: "dbx" })],
});
expect(needsSidebarObjectGroupDiscovery(database, objectGroupTypes)).toBe(true);
});
it("asks for discovery when the container has no children at all", () => {
const schema = node({ id: "pg/public", type: "schema", connectionId: "c1", database: "postgres" });
expect(needsSidebarObjectGroupDiscovery(schema, objectGroupTypes)).toBe(true);
expect(needsSidebarObjectGroupDiscovery({ ...schema, children: [] }, objectGroupTypes)).toBe(true);
});
it("stops asking once a searchable object group is loaded", () => {
const database = node({
id: "mysql/dbx",
type: "database",
connectionId: "c1",
database: "dbx",
children: [node({ id: "saved-sql-root", type: "saved-sql-root", connectionId: "c1", database: "dbx" }), node({ id: "group-views", type: "group-views", connectionId: "c1", database: "dbx" })],
});
expect(needsSidebarObjectGroupDiscovery(database, objectGroupTypes)).toBe(false);
});
it("ignores non-group children such as tables already listed under the container", () => {
const database = node({
id: "mysql/dbx",
type: "database",
connectionId: "c1",
database: "dbx",
children: [node({ id: "dbx.cells_probe", type: "table", connectionId: "c1", database: "dbx" }), node({ id: "user-admin", type: "user-admin" })],
});
expect(needsSidebarObjectGroupDiscovery(database, objectGroupTypes)).toBe(true);
});
it("ignores object groups owned by another connection", () => {
const database = node({
id: "mysql/dbx",
type: "database",
connectionId: "c1",
database: "dbx",
children: [node({ id: "group-tables", type: "group-tables" })],
});
expect(needsSidebarObjectGroupDiscovery(database, objectGroupTypes)).toBe(true);
});
});
@@ -0,0 +1,16 @@
import type { TreeNode, TreeNodeType } from "@/types/database";
/**
* Whether a sidebar search still has to fetch a container's on-demand object
* groups before it can look for matches below it.
*
* A collapsed container is not necessarily childless: every database keeps its
* saved-SQL root (`tree.queries`) as a child even before the object groups are
* fetched, so "no children" is not a reliable discovery signal. Checking for a
* searchable group instead keeps databases whose groups were never loaded out
* of the search, which made a table that exists in several databases appear
* under only one of them.
*/
export function needsSidebarObjectGroupDiscovery(node: TreeNode, objectGroupTypes: ReadonlySet<TreeNodeType>): boolean {
return !node.children?.some((child) => child.connectionId != null && objectGroupTypes.has(child.type));
}