🐛 fix(data-viewer): 修复表数据刷新后总数缓存过期

- 刷新时校验已知总数是否满足当前页 hasMore 信号

- 旧总数过期时清空 countKey 并重新统计总数

- 增加表数据增长后的分页回归测试
This commit is contained in:
Syngnat
2026-05-12 21:45:44 +08:00
parent 65567221ac
commit 10a695ba0f
2 changed files with 72 additions and 6 deletions

View File

@@ -78,6 +78,11 @@ const flushPromises = async () => {
});
};
const createRows = (count: number) => Array.from({ length: count }, (_, i) => ({
ID: i + 1,
NAME: `row-${i + 1}`,
}));
describe('DataViewer safe editing locator', () => {
const renderAndReload = async (tab: TabData = createTab()) => {
let renderer: ReactTestRenderer;
@@ -195,6 +200,58 @@ describe('DataViewer safe editing locator', () => {
renderer.unmount();
});
it('invalidates a stale known total when table data grows after a manual refresh', async () => {
storeState.connections[0].config.type = 'mysql';
storeState.connections[0].config.database = 'main';
backendApp.DBGetColumns.mockResolvedValue({
success: true,
data: [{ name: 'ID', key: 'PRI' }, { name: 'NAME', key: '' }],
});
let pageQueryCount = 0;
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
if (/count\s*\(/i.test(String(sql))) {
return {
success: true,
fields: ['total'],
data: [{ total: 500 }],
};
}
pageQueryCount += 1;
return {
success: true,
fields: ['ID', 'NAME'],
data: pageQueryCount === 1 ? createRows(100) : createRows(101),
};
});
let renderer: ReactTestRenderer;
await act(async () => {
renderer = create(<DataViewer tab={createTab({ dbName: 'main', tableName: 'users', title: 'users' })} />);
});
await flushPromises();
expect(dataGridState.latestProps?.pagination).toMatchObject({
total: 100,
totalKnown: true,
});
await act(async () => {
dataGridState.latestProps?.onReload();
await Promise.resolve();
await Promise.resolve();
});
await flushPromises();
expect(backendApp.DBQuery.mock.calls.some((call: any[]) => /count\s*\(/i.test(String(call[2] || '')))).toBe(true);
expect(dataGridState.latestProps?.pagination).toMatchObject({
total: 500,
totalKnown: true,
});
expect(dataGridState.latestProps?.data).toHaveLength(100);
renderer!.unmount();
});
it('shows an actionable message for DuckDB timeout interruption errors', async () => {
storeState.connections[0].config.type = 'duckdb';
storeState.connections[0].config.database = 'main';

View File

@@ -85,6 +85,11 @@ const parseTotalFromCountRow = (row: any): number | null => {
return null;
};
const isKnownTotalFreshForPage = (total: unknown, minExpectedTotal: number): boolean => {
const parsedTotal = toNonNegativeFiniteNumber(total);
return parsedTotal !== null && parsedTotal >= minExpectedTotal;
};
const buildDataViewerReadOnlyLocator = (reason: string): EditRowLocator => ({
strategy: 'none',
columns: [],
@@ -743,9 +748,16 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAct
const countKey = `${tab.connectionId}|${dbName}|${tableName}|${whereSQL}`;
const derivedTotalKnown = !hasMore;
const derivedTotal = derivedTotalKnown ? offset + resultData.length : currentPage * size + 1;
const isDuckDB = dbTypeLower === 'duckdb';
const minExpectedTotal = hasMore ? offset + resultData.length + 1 : offset + resultData.length;
if (derivedTotalKnown) countKeyRef.current = countKey;
const staleKnownTotalForCurrentPage =
!derivedTotalKnown &&
pagination.totalKnown &&
countKeyRef.current === countKey &&
!isKnownTotalFreshForPage(pagination.total, minExpectedTotal);
if (staleKnownTotalForCurrentPage) {
countKeyRef.current = '';
}
latestConfigRef.current = config;
latestDbTypeRef.current = dbTypeLower;
latestDbNameRef.current = dbName;
@@ -767,12 +779,9 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAct
};
}
if (prev.totalKnown && countKeyRef.current === countKey) {
if (!isDuckDB) {
return { ...prev, current: currentPage, pageSize: size };
}
// 当当前页存在“下一页”信号时,已知总数至少应大于当前页末尾。
// 若旧总数不满足该条件(例如历史统计值为 0降级为未知总数并回退到 derivedTotal
if (Number.isFinite(prev.total) && prev.total >= minExpectedTotal) {
// 若旧总数不满足该条件(例如清空表后又外部写入数据),降级为未知总数并重新统计
if (isKnownTotalFreshForPage(prev.total, minExpectedTotal)) {
return { ...prev, current: currentPage, pageSize: size };
}
}