fix(sidebar): respect table name filter during sidebar search
Closes #13456
This commit is contained in:
1 parent
8523d8d7ac
commit
ca367b6283
2 files changed
+146
-1
No files matched your search
+126
@@ -0,0 +1,126 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||
});
|
||||
}
|
||||
|
||||
function pgConnection(id: string): ConnectionConfig {
|
||||
return { id, name: id, db_type: "postgres", host: "127.0.0.1", port: 5432, username: "postgres", password: "", database: "app", read_only: false } as ConnectionConfig;
|
||||
}
|
||||
|
||||
function databaseNode(connectionId: string, database: string): TreeNode {
|
||||
return {
|
||||
id: `${connectionId}:${database}`,
|
||||
label: database,
|
||||
type: "database",
|
||||
connectionId,
|
||||
database,
|
||||
isExpanded: true,
|
||||
children: [{ id: `${connectionId}:${database}:__tables`, label: "tables", type: "group-tables", connectionId, database, children: [] }],
|
||||
};
|
||||
}
|
||||
|
||||
async function setup() {
|
||||
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
|
||||
const connectDb = vi.fn(async (config: ConnectionConfig) => config.id);
|
||||
const listTables = vi.fn().mockResolvedValue([
|
||||
{ name: "t_order", table_type: "BASE TABLE" },
|
||||
{ name: "t_user", table_type: "BASE TABLE" },
|
||||
]);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth,
|
||||
connectDb,
|
||||
listTables,
|
||||
disconnectDb: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
deleteTableVGroupsForConnection: vi.fn().mockResolvedValue(undefined),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
store.connections = [pgConnection("conn-a")];
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: "conn-a",
|
||||
label: "conn-a",
|
||||
type: "connection",
|
||||
connectionId: "conn-a",
|
||||
isExpanded: true,
|
||||
children: [databaseNode("conn-a", "app")],
|
||||
},
|
||||
];
|
||||
return { store, connectDb, listTables };
|
||||
}
|
||||
|
||||
function tablesGroupNode(store: ReturnType<typeof setup>["store"]): TreeNode {
|
||||
const database = store.treeNodes[0].children![0];
|
||||
return database.children![0];
|
||||
}
|
||||
|
||||
describe("sidebar table search index respects the per-connection table filter", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("passes the per-connection table filter when building the search index", async () => {
|
||||
const { store, connectDb, listTables } = await setup();
|
||||
const group = tablesGroupNode(store);
|
||||
store.setSidebarTableNameFilter(store.tableNameFilterScopeKey({ connectionId: group.connectionId!, database: group.database, schema: group.schema, nodeKind: group.type, catalog: group.catalog }), { includePatterns: ["t_order%"], excludePatterns: [] });
|
||||
|
||||
const entries = await store.refreshSidebarTableSearchIndex(group.id);
|
||||
|
||||
expect(connectDb).toHaveBeenCalledTimes(1);
|
||||
const call = listTables.mock.calls.at(-1)!;
|
||||
expect(call[8]).toEqual({ includePatterns: ["t_order%"], excludePatterns: [] });
|
||||
expect((entries ?? []).map((entry) => entry.name)).toEqual(["t_order", "t_user"]);
|
||||
}, 20_000);
|
||||
|
||||
it("builds the index without a filter when none is set", async () => {
|
||||
const { store, listTables } = await setup();
|
||||
const group = tablesGroupNode(store);
|
||||
|
||||
await store.refreshSidebarTableSearchIndex(group.id);
|
||||
|
||||
const call = listTables.mock.calls.at(-1)!;
|
||||
expect(call[8]).toBeUndefined();
|
||||
}, 20_000);
|
||||
|
||||
it("drops the stale index when the table filter changes and rebuilds with the filter", async () => {
|
||||
const { store, listTables } = await setup();
|
||||
const group = tablesGroupNode(store);
|
||||
const scopeKey = store.tableNameFilterScopeKey({ connectionId: group.connectionId!, database: group.database, schema: group.schema, nodeKind: group.type, catalog: group.catalog });
|
||||
|
||||
// 无过滤时先建一次索引(未过滤的旧索引会一直留在缓存里)。
|
||||
await store.refreshSidebarTableSearchIndex(group.id);
|
||||
const cached = await store.loadSidebarTableSearchIndex(group.id);
|
||||
expect((cached ?? []).map((entry) => entry.name)).toEqual(["t_order", "t_user"]);
|
||||
|
||||
// 用户修改过滤条件:树刷新 + 索引失效,重建时必须带上过滤器。
|
||||
store.setSidebarTableNameFilter(scopeKey, { includePatterns: ["t_user%"], excludePatterns: [] });
|
||||
await store.refreshTreeNodeForTableNameFilter(group, scopeKey, 1);
|
||||
|
||||
const invalidated = await store.loadSidebarTableSearchIndex(group.id);
|
||||
expect(invalidated).toBeNull();
|
||||
|
||||
const rebuilt = await store.refreshSidebarTableSearchIndex(group.id);
|
||||
const call = listTables.mock.calls.at(-1)!;
|
||||
expect(call[8]).toEqual({ includePatterns: ["t_user%"], excludePatterns: [] });
|
||||
expect((rebuilt ?? []).map((entry) => entry.name)).toEqual(["t_order", "t_user"]);
|
||||
}, 20_000);
|
||||
});
|
||||
@@ -3977,10 +3977,23 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const querySchema = connectionObjectTreeQuerySchema(config, parent.database, parent.schema);
|
||||
const objectTypes = sidebarIndexedObjectGroupTypes.has(parent.type) ? (objectTypesForGroupNode(parent.type) ?? undefined) : undefined;
|
||||
const pageSize = sidebarObjectGroupPageSize();
|
||||
// 搜索索引必须与连接的表过滤器保持一致:用户右键设置的过滤条件在正常加载路径
|
||||
// 由服务端(SQL LIKE)执行,索引若不带同一过滤器,过滤掉的表就会在搜索时重新出现。
|
||||
// scope 的各字段与保存过滤器的 tableNameFilterScopeForNode 使用同一节点,键必须一致。
|
||||
const tableNameFilter = effectiveTableNameFilterForNode(
|
||||
parent,
|
||||
activeTableNameFilterForScope({
|
||||
connectionId,
|
||||
database: parent.database,
|
||||
schema: parent.schema,
|
||||
nodeKind: parent.type,
|
||||
catalog: parent.catalog,
|
||||
}),
|
||||
);
|
||||
const entries: TableInfo[] = [];
|
||||
for (let offset = 0; ; offset += pageSize) {
|
||||
if (!sidebarTableSearchIndexGenerationIsCurrent(connectionId, cacheKey, connectionGeneration, scopeGeneration)) return [];
|
||||
const page = await listTablesWithOptionalTableNameFilter(connectionId, parent.database, querySchema, undefined, pageSize, offset, objectTypes, parent.catalog);
|
||||
const page = await listTablesWithOptionalTableNameFilter(connectionId, parent.database, querySchema, undefined, pageSize, offset, objectTypes, parent.catalog, tableNameFilter);
|
||||
entries.push(...page);
|
||||
if (page.length < pageSize) break;
|
||||
}
|
||||
@@ -8777,6 +8790,12 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
async function refreshTreeNodeForTableNameFilter(node: TreeNode, scopeKey: string, revision: number) {
|
||||
invalidateMetadataCachesForNode(node);
|
||||
// 侧栏搜索索引的生命周期独立于元数据缓存:过滤器变更后若不失效旧索引,
|
||||
// 顶部搜索会继续用未过滤的索引把过滤掉的表重新列出来。
|
||||
const searchIndexScope = sidebarTableSearchIndexScopeForNode(node);
|
||||
if (searchIndexScope) {
|
||||
await invalidateSidebarTableSearchIndexScopes([searchIndexScope], (scope) => scope.parentNodeId === node.id);
|
||||
}
|
||||
// !node.tableName is generic: table-level structure groups (columns/indexes/fkeys/constraints/partitions/table triggers)
|
||||
// stay off the schema-level object-group loader, not just triggers.
|
||||
if (objectTypesForGroupNode(node.type) && !node.tableName) {
|
||||
|
||||
Reference in new issue
Block a user