fix(grid): bound large table previews by byte budget

This commit is contained in:
zipg authored and GitHub committed 2026-08-13 13:25:04 +08:00
1 parent 020654c33c
commit e85d3dcc59
19 files changed
+823 -23

No files matched your search

@@ -6,6 +6,7 @@ import type { QueryTab } from "@/types/database";
const mocks = vi.hoisted(() => ({
tabs: [] as QueryTab[],
activeTabId: "",
buildTableSelectSql: vi.fn(),
databaseType: "postgres" as string,
ensureConnected: vi.fn(),
executeTabSql: vi.fn(),
@@ -83,7 +84,7 @@ vi.mock("@/stores/settingsStore", () => ({
}));
vi.mock("@/lib/table/tableSelectSql", () => ({
buildTableSelectSql: async ({ tableName }: { tableName: string }) => `SELECT * FROM ${tableName}`,
buildTableSelectSql: mocks.buildTableSelectSql,
}));
const dialogs = {
@@ -103,12 +104,13 @@ describe("useNavigationTargets openTableTarget", () => {
mocks.tabs.length = 0;
mocks.databaseType = "postgres";
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.buildTableSelectSql.mockImplementation(async ({ tableName }: { tableName: string }) => `SELECT * FROM ${tableName}`);
mocks.executeTabSql.mockResolvedValue(undefined);
mocks.getColumns.mockResolvedValue([column("id")]);
mocks.listIndexes.mockResolvedValue([]);
});
it("marks row identity pending until real metadata lands", async () => {
it("loads stable row identity before the first PostgreSQL table query", async () => {
let releaseColumns: (columns: unknown[]) => void = () => {};
mocks.getColumns.mockReturnValueOnce(
new Promise((resolve) => {
@@ -124,10 +126,15 @@ describe("useNavigationTargets openTableTarget", () => {
releaseColumns([column("id")]);
await open;
// 数据查询执行期间元数据未落地:行标识等待必须已挂起
expect(pendingDuringQuery).toBe(true);
expect(pendingDuringQuery).toBe(false);
expect(mocks.tabs[0]?.tableMeta?.primaryKeys).toEqual(["id"]);
expect(mocks.tabs[0]?.tableMetaPending).toBe(false);
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(
expect.objectContaining({
columns: ["id"],
primaryKeys: ["id"],
}),
);
});
it("reuses cached metadata and force-refreshes it once per catalog after a structure save", async () => {
@@ -5,6 +5,7 @@ import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { buildTableSelectSql, quoteTableDataIdentifier } from "@/lib/table/tableSelectSql";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
import * as api from "@/lib/backend/api";
@@ -65,6 +66,7 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
// 结果(可能是失败结果的 ["Error"]),进入 SQL 会生成非法投影;
// 真实列缺失时省略 columns 让 builder 生成 SELECT *
const realColumns = tab.tableMeta?.columns.length ? tab.tableMeta.columns : undefined;
const limit = options.limit ?? tab.resultPageLimit ?? tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize);
return buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config?.driver_profile,
@@ -76,8 +78,9 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
catalog: tableMeta?.catalog,
columns: realColumns?.map((column) => column.name),
primaryKeys,
...tableDataLargeValuePreviewOptions(effectiveDbType, realColumns ?? [], primaryKeys, limit),
includeRowId: useRowId,
limit: options.limit ?? tab.resultPageLimit ?? tableOpenPageLimit(settingsStore.editorSettings.tableOpenPageSize),
limit,
...options,
});
}
@@ -4,6 +4,7 @@ import { invalidateTableMetadataCache, loadTableMetadata } from "@/lib/metadata/
import { canApplyDataTabMetadata } from "@/lib/sidebar/dataTabOpenPolicy";
import { isNoSnapshotErrorResult, isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
import { editableRowIdentifierColumns, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { uuid } from "@/lib/common/utils";
@@ -136,6 +137,21 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
await queryStore.executeTabSql(tabId, sql, { pagination: { limit: pageLimit, offset: 0 } });
return;
}
const eagerMetadata =
effectiveDbType === "mysql" || effectiveDbType === "postgres"
? await loadTableMetadata({
connectionId: target.connectionId,
database: target.database,
schema: querySchema,
tableName: target.tableName,
tableType: targetTableType,
databaseType: effectiveDbType,
driverProfile: config.driver_profile || config.db_type,
catalog: target.catalog,
})
: undefined;
const eagerColumns = eagerMetadata?.metadata.columns ?? [];
const eagerPrimaryKeys = eagerMetadata?.metadata.primaryKeys ?? [];
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
driverProfile: config.driver_profile,
@@ -145,6 +161,9 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
database: target.database,
tableName: target.tableName,
tableType: targetTableType,
columns: eagerColumns.map((column) => column.name),
primaryKeys: eagerPrimaryKeys,
...tableDataLargeValuePreviewOptions(effectiveDbType, eagerColumns, eagerPrimaryKeys, pageLimit),
whereInput: target.whereInput,
limit: pageLimit,
});
@@ -156,8 +175,8 @@ async function openTableTarget(target: NavigationTarget, options: { tableInfoTab
database: target.database,
tableName: target.tableName,
tableType: targetTableType,
columns: [],
primaryKeys: [],
columns: eagerColumns,
primaryKeys: eagerPrimaryKeys,
});
firstExecuteStarted = true;
// 取消计数快照:isCancelling 是瞬态的(取消失败/查询先完成会被清掉),
@@ -13,6 +13,7 @@ import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { tableOpenPageLimit } from "@/lib/table/tableOpenPageLimit";
import { tableDataLargeValuePreviewOptions } from "@/lib/dataGrid/dataGridLargeValues";
import { canActivateExistingDataTableTab } from "@/lib/tabs/dataTabActivation";
import { beginDataTabNavigation, endDataTabNavigation, isCurrentDataTabNavigation } from "@/lib/tabs/dataTabNavigationGeneration";
@@ -293,18 +294,23 @@ export function useSidebarDataOpenRuntime() {
} else if (deferTableMetaRefresh) {
logPhase("metadata-deferred", { tabId });
} else {
void refreshTableMetaInBackground(tabId);
logPhase("metadata-started", { tabId });
}
const metadataRefresh = shouldRefreshTableMeta && !deferTableMetaRefresh ? refreshTableMetaInBackground(tabId) : undefined;
if (!cachedTableMeta && (effectiveDbType === "mysql" || effectiveDbType === "postgres")) {
await metadataRefresh;
}
// Check if superseded by a newer openData call
if (!isActive()) {
logPhase("superseded-before-build-sql", { tabId });
return;
}
const columns = cachedTableMeta?.columns ?? [];
const primaryKeys = cachedTableMeta?.primaryKeys ?? [];
const loadedTableMeta = cachedTableMeta ?? queryStore.tabs.find((item) => item.id === tabId)?.tableMeta;
const columns = loadedTableMeta?.columns ?? [];
const primaryKeys = loadedTableMeta?.primaryKeys ?? [];
const includeRowId = usesSyntheticRowIdKey(effectiveDbType, primaryKeys, tableType);
const sql = await buildTableSelectSql({
databaseType: effectiveDbType,
@@ -317,6 +323,7 @@ export function useSidebarDataOpenRuntime() {
catalog: node.catalog,
columns: columns.map((column) => column.name),
primaryKeys,
...tableDataLargeValuePreviewOptions(effectiveDbType, columns, primaryKeys, limit),
limit,
includeRowId,
});