feat(sidebar): page full table search results under database
This commit is contained in:
5 files changed
+146
-43
No files matched your search
@@ -1094,8 +1094,9 @@ function openDriverStoreForInstallError(errMsg: string, node: TreeNode = activeN
|
||||
|
||||
async function loadMoreObjectGroupChildren() {
|
||||
const node = activeNode.value;
|
||||
const searchFilter = node.loadMore?.parentId ? connectionStore.sidebarTableSearchQueries[node.loadMore.parentId]?.trim() || "" : "";
|
||||
try {
|
||||
await connectionStore.loadMoreObjectGroupChildren(node);
|
||||
await connectionStore.loadMoreObjectGroupChildren(node, { searchFilter });
|
||||
} catch (e: any) {
|
||||
toast(t("connection.connectFailed", { message: translateBackendError(t, e) }), 5000);
|
||||
}
|
||||
|
||||
@@ -3244,6 +3244,88 @@ describe("connectionStore metadata loading", () => {
|
||||
expect(listTables).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("pages table-scoped search results instead of truncating at the search budget", async () => {
|
||||
const tables = Array.from({ length: 5 }, (_, index) => ({
|
||||
name: `t_${String(index + 1).padStart(4, "0")}`,
|
||||
table_type: "TABLE" as const,
|
||||
comment: null,
|
||||
}));
|
||||
const listTables = vi.fn((_connectionId: string, _database: string, _schema: string, searchFilter?: string, limit?: number, offset?: number) => {
|
||||
const matches = searchFilter ? tables.filter((table) => table.name.includes(searchFilter)) : tables;
|
||||
const start = offset ?? 0;
|
||||
return Promise.resolve(matches.slice(start, start + (limit ?? matches.length)));
|
||||
});
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listTables,
|
||||
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 { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
settingsStore.editorSettings.sidebarObjectDisplay = "grouped";
|
||||
settingsStore.desktopSettings.sidebar_table_page_size = 2;
|
||||
|
||||
const connection = mysqlConnection();
|
||||
const tablesGroup: TreeNode = {
|
||||
id: "mysql-1:app:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [
|
||||
{
|
||||
id: connection.id,
|
||||
label: connection.name,
|
||||
type: "connection",
|
||||
connectionId: connection.id,
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{
|
||||
id: "mysql-1:app",
|
||||
label: "app",
|
||||
type: "database",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: true,
|
||||
children: [tablesGroup],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
store.sidebarTableSearchQueries[tablesGroup.id] = "t";
|
||||
await store.loadObjectGroupChildren(tablesGroup, {
|
||||
force: true,
|
||||
searchFilter: "t",
|
||||
sidebarTableSearchParentId: tablesGroup.id,
|
||||
expectedSidebarTableSearchQuery: "t",
|
||||
});
|
||||
|
||||
expect(tablesGroup.children?.map((child) => child.label)).toEqual(["t_0001", "t_0002", "tree.loadMore"]);
|
||||
expect(tablesGroup.children?.at(-1)?.type).toBe("load-more");
|
||||
expect(listTables.mock.calls[0].slice(3, 6)).toEqual(["t", 3, 0]);
|
||||
|
||||
await store.loadMoreObjectGroupChildren(tablesGroup.children!.at(-1)!, { searchFilter: "t" });
|
||||
|
||||
expect(tablesGroup.children?.map((child) => child.label)).toEqual(["t_0001", "t_0002", "t_0003", "t_0004", "tree.loadMore"]);
|
||||
expect(tablesGroup.children?.at(-1)?.type).toBe("load-more");
|
||||
expect(listTables.mock.calls[1].slice(3, 6)).toEqual(["t", 3, 2]);
|
||||
});
|
||||
|
||||
it("keeps connection-name matches out of object metadata filters", async () => {
|
||||
const listTables = vi.fn((_connectionId: string, _database: string, _schema: string, searchFilter?: string) => Promise.resolve(searchFilter === "orders" || !searchFilter ? [{ name: "orders", table_type: "TABLE" as const, comment: null }] : []));
|
||||
|
||||
|
||||
@@ -2087,6 +2087,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
offset: number;
|
||||
pageSize: number;
|
||||
searchFilter?: string;
|
||||
pagedSearch?: boolean;
|
||||
force?: boolean;
|
||||
}): Promise<{ children: TreeNode[]; objectCount: number; hasMore: boolean; nextOffset: number; loadMoreParent?: TableTreeLoadMoreParent }> {
|
||||
if (!options.node.connectionId || options.node.database == null) {
|
||||
@@ -2102,15 +2103,12 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
});
|
||||
const tableNameFilter = effectiveTableNameFilterForNode(options.node, userTableNameFilter);
|
||||
const sourceRevision = tableListSourceRevision(options.node.connectionId);
|
||||
// A search must never truncate the fuzzy result set to the first page: the
|
||||
// target table can sort beyond it (e.g. "T_Erp_Nc_SuPlan_List" for
|
||||
// "erpncs" in a large ERP schema), which silently drops it from the first
|
||||
// search even though later, narrower queries succeed. Results are bounded
|
||||
// by SIDEBAR_TABLE_SEARCH_RESULT_BUDGET so a wide fuzzy query cannot push
|
||||
// an unbounded result set through database → IPC → store → tree rendering.
|
||||
// Unfiltered loads keep the page+1 probe used for load-more detection.
|
||||
const fetchLimit = searchFilter ? SIDEBAR_TABLE_SEARCH_RESULT_BUDGET : options.pageSize + 1;
|
||||
const fetchOffset = searchFilter ? undefined : options.offset;
|
||||
// Table-scoped search must page through the full ordered result set, just
|
||||
// like unfiltered loads, instead of silently stopping at the first budget
|
||||
// window. Global sidebar search keeps the bounded single-shot fetch.
|
||||
const paginate = options.pagedSearch || !searchFilter;
|
||||
const fetchLimit = paginate ? options.pageSize + 1 : SIDEBAR_TABLE_SEARCH_RESULT_BUDGET;
|
||||
const fetchOffset = paginate ? options.offset : undefined;
|
||||
const tables = await loadCachedMetadataListPage<TableInfo[]>(
|
||||
metadataListCacheScope({
|
||||
kind: "table-list-page",
|
||||
@@ -2128,7 +2126,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
() => listTablesWithOptionalTableNameFilter(options.node.connectionId!, options.node.database!, options.querySchema, searchFilter, fetchLimit, fetchOffset, options.objectTypes, options.node.catalog, tableNameFilter),
|
||||
{ force: options.force },
|
||||
);
|
||||
const hasMore = searchFilter ? false : tables.length > options.pageSize;
|
||||
const hasMore = paginate ? tables.length > options.pageSize : false;
|
||||
const pageTables = hasMore ? tables.slice(0, options.pageSize) : tables;
|
||||
if (tableListSourceRevision(options.node.connectionId) === sourceRevision) {
|
||||
indexCompletionTables(options.node.connectionId, options.node.database, options.effectiveSchema, tableInfosToCompletionTables(pageTables, options.effectiveSchema));
|
||||
@@ -2223,6 +2221,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
offset: number;
|
||||
pageSize: number;
|
||||
searchFilter?: string;
|
||||
pagedSearch?: boolean;
|
||||
force?: boolean;
|
||||
}): Promise<{ children: TreeNode[]; objectCount: number; hasMore: boolean; nextOffset: number; loadMoreParent?: TableTreeLoadMoreParent }> {
|
||||
const searchFilter = (options.searchFilter ?? sidebarSearchQuery.value) || undefined;
|
||||
@@ -2233,12 +2232,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
nodeKind: "simple-tables",
|
||||
});
|
||||
const sourceRevision = tableListSourceRevision(options.connectionId);
|
||||
// A search must never truncate the fuzzy result set to the first page (see
|
||||
// loadPagedTableGroupChildren); results are bounded by
|
||||
// SIDEBAR_TABLE_SEARCH_RESULT_BUDGET, and unfiltered loads keep the
|
||||
// page+1 probe.
|
||||
const fetchLimit = searchFilter ? SIDEBAR_TABLE_SEARCH_RESULT_BUDGET : options.pageSize + 1;
|
||||
const fetchOffset = searchFilter ? undefined : options.offset;
|
||||
const paginate = options.pagedSearch || !searchFilter;
|
||||
const fetchLimit = paginate ? options.pageSize + 1 : SIDEBAR_TABLE_SEARCH_RESULT_BUDGET;
|
||||
const fetchOffset = paginate ? options.offset : undefined;
|
||||
const tables = await loadCachedMetadataListPage<TableInfo[]>(
|
||||
metadataListCacheScope({
|
||||
kind: "table-list-page",
|
||||
@@ -2255,7 +2251,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
() => listTablesWithOptionalTableNameFilter(options.connectionId, options.database, options.querySchema, searchFilter, fetchLimit, fetchOffset, undefined, undefined, tableNameFilter),
|
||||
{ force: options.force },
|
||||
);
|
||||
const hasMore = searchFilter ? false : tables.length > options.pageSize;
|
||||
const hasMore = paginate ? tables.length > options.pageSize : false;
|
||||
const pageTables = hasMore ? tables.slice(0, options.pageSize) : tables;
|
||||
if (tableListSourceRevision(options.connectionId) === sourceRevision) {
|
||||
indexCompletionTables(options.connectionId, options.database, options.effectiveSchema, tableInfosToCompletionTables(pageTables, options.effectiveSchema));
|
||||
@@ -5553,9 +5549,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
offset: 0,
|
||||
pageSize,
|
||||
searchFilter: options?.searchFilter === "" ? "" : searchFilter || undefined,
|
||||
pagedSearch: isSidebarTableSearch,
|
||||
force: options?.force,
|
||||
});
|
||||
children = page.hasMore && !searchFilter ? appendTableTreeLoadMoreNode(page.children, buildLoadMoreNode(node, page.nextOffset, pageSize), page.loadMoreParent) : page.children;
|
||||
children = page.hasMore && (!searchFilter || isSidebarTableSearch) ? appendTableTreeLoadMoreNode(page.children, buildLoadMoreNode(node, page.nextOffset, pageSize), page.loadMoreParent) : page.children;
|
||||
nextObjectCount = page.objectCount;
|
||||
} else if (simpleObjectDisplay) {
|
||||
// The synthetic public scope contains no tables. Avoid issuing a
|
||||
@@ -5706,9 +5703,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
offset: 0,
|
||||
pageSize: sidebarObjectGroupPageSize(),
|
||||
searchFilter: options?.searchFilter === "" ? "" : searchFilter || undefined,
|
||||
pagedSearch: isSidebarTableSearch,
|
||||
force: options?.force,
|
||||
});
|
||||
children = page.hasMore && !searchFilter ? appendTableTreeLoadMoreNode(page.children, buildLoadMoreNode(node, page.nextOffset, sidebarObjectGroupPageSize()), page.loadMoreParent) : page.children;
|
||||
children = page.hasMore && (!searchFilter || isSidebarTableSearch) ? appendTableTreeLoadMoreNode(page.children, buildLoadMoreNode(node, page.nextOffset, sidebarObjectGroupPageSize()), page.loadMoreParent) : page.children;
|
||||
nextObjectCount = page.objectCount;
|
||||
} else {
|
||||
const pageSize = sidebarObjectGroupPageSize();
|
||||
@@ -5791,6 +5789,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
schema: parent.schema,
|
||||
nodeKind: parent.type,
|
||||
objectTypes: objectTypesForScope,
|
||||
searchFilter: options?.searchFilter,
|
||||
limit: loadMore.pageSize + 1,
|
||||
offset: loadMore.offset,
|
||||
sidebarDisplayMode: useSettingsStore().editorSettings.sidebarObjectDisplay,
|
||||
@@ -5819,6 +5818,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
nonTableObjectTypes: [],
|
||||
offset: loadMore.offset,
|
||||
pageSize: loadMore.pageSize,
|
||||
searchFilter: options?.searchFilter,
|
||||
pagedSearch: !!options?.searchFilter,
|
||||
force: false,
|
||||
});
|
||||
const targetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
@@ -5828,7 +5829,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const nextChildren = page.hasMore ? appendTableTreeLoadMoreNode(mergedChildren, buildLoadMoreNode(targetParent, page.nextOffset, loadMore.pageSize), page.loadMoreParent) : mergedChildren;
|
||||
targetParent.objectCount = mergedChildren.length;
|
||||
setChildren(targetParent, nextChildren);
|
||||
await savePersistedTreeChildren(schemaCacheKey(parentConnectionId, parentDatabase, parent.schema || "", ownerAwareMetadataCacheVersion(config, "objects-simple-v8")), nextChildren);
|
||||
if (!options?.searchFilter) {
|
||||
await savePersistedTreeChildren(schemaCacheKey(parentConnectionId, parentDatabase, parent.schema || "", ownerAwareMetadataCacheVersion(config, "objects-simple-v8")), nextChildren);
|
||||
}
|
||||
const currentTargetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
if (currentTargetParent && parentEpoch.isCurrent()) currentTargetParent.isExpanded = true;
|
||||
return;
|
||||
@@ -5855,6 +5858,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
offset: loadMore.offset,
|
||||
pageSize: loadMore.pageSize,
|
||||
searchFilter: options?.searchFilter,
|
||||
pagedSearch: !!options?.searchFilter,
|
||||
force: false,
|
||||
});
|
||||
const targetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
@@ -5881,7 +5885,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
nextChildren = page.hasMore ? [...mergedChildren, buildLoadMoreNode(targetParent, page.nextOffset, loadMore.pageSize)] : mergedChildren;
|
||||
targetParent.objectCount = mergedChildren.length;
|
||||
setChildren(targetParent, nextChildren);
|
||||
await savePersistedTreeChildren(objectGroupCacheKey(targetParent), nextChildren);
|
||||
if (!options?.searchFilter) {
|
||||
await savePersistedTreeChildren(objectGroupCacheKey(targetParent), nextChildren);
|
||||
}
|
||||
const currentTargetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
if (currentTargetParent && parentEpoch.isCurrent()) currentTargetParent.isExpanded = true;
|
||||
return;
|
||||
@@ -5890,7 +5896,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (!targetParent || !parentEpoch.isCurrent()) return;
|
||||
targetParent.objectCount = mergedChildren.length;
|
||||
setChildren(targetParent, nextChildren);
|
||||
await savePersistedTreeChildren(objectGroupCacheKey(targetParent), nextChildren);
|
||||
if (!options?.searchFilter) {
|
||||
await savePersistedTreeChildren(objectGroupCacheKey(targetParent), nextChildren);
|
||||
}
|
||||
const currentTargetParent = treeNodeLoadRelatedTarget(load, parent);
|
||||
if (currentTargetParent && parentEpoch.isCurrent()) currentTargetParent.isExpanded = true;
|
||||
} catch (e) {
|
||||
|
||||
Reference in new issue
Block a user