fix(grid): bound large table previews by byte budget
This commit is contained in:
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,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user