fix(mongo): stabilize estimated-total pagination

This commit is contained in:
Freedom authored and GitHub committed 2026-07-31 14:51:25 +08:00
1 parent 96038d2b8a
commit 8f3fd0ebfb
23 files changed
+462 -124

No files matched your search

@@ -63,8 +63,7 @@ function createBaseOptions(overrides: Partial<DrawCanvasDataGridOptions> = {}):
cellIsSelected: () => false,
cellCanHover: () => true,
infiniteScrollEnabled: false,
pageSize: 100,
currentPage: 1,
pageOffset: 0,
...overrides,
};
}
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { canGoNextDocumentPage, resolveDocumentQueryTotals } from "@/lib/document/documentQueryTotals";
import { canGoNextDocumentPage, exactTotalFromIncompleteDocumentPage, isSameDocumentQueryTotalCountRequest, resolveDocumentQueryTotals, type DocumentQueryTotalCountRequest } from "@/lib/document/documentQueryTotals";
describe("document query totals", () => {
it("uses exact totals as the pagination bound", () => {
@@ -21,4 +21,49 @@ describe("document query totals", () => {
expect(canGoNextDocumentPage({ page: 999_999, pageSize: 10, rowCount: 10 })).toBe(true);
expect(canGoNextDocumentPage({ page: 1_000_000, pageSize: 10, rowCount: 3 })).toBe(false);
});
it("treats a short page as an exact total even when the backend estimate is inexact", () => {
expect(exactTotalFromIncompleteDocumentPage({ page: 0, pageSize: 500, rowCount: 0 })).toBe(0);
expect(exactTotalFromIncompleteDocumentPage({ page: 0, pageSize: 500, rowCount: 1 })).toBe(1);
expect(resolveDocumentQueryTotals(1, false, { page: 0, pageSize: 500, rowCount: 1 })).toEqual({
total: 1,
totalIsExact: true,
paginationTotal: 1,
});
expect(resolveDocumentQueryTotals(50, false, { page: 1, pageSize: 500, rowCount: 12 })).toEqual({
total: 512,
totalIsExact: true,
paginationTotal: 512,
});
expect(resolveDocumentQueryTotals(658_320, false, { page: 0, pageSize: 500, rowCount: 500 })).toEqual({
total: 658_320,
totalIsExact: false,
paginationTotal: undefined,
});
});
it("does not infer an exact total from an empty page after the first page", () => {
expect(exactTotalFromIncompleteDocumentPage({ page: 1, pageSize: 500, rowCount: 0 })).toBeUndefined();
expect(resolveDocumentQueryTotals(658_320, false, { page: 1, pageSize: 500, rowCount: 0 })).toEqual({
total: 658_320,
totalIsExact: false,
paginationTotal: undefined,
});
});
it("matches exact-count requests across identity, filter, and generation", () => {
const request: DocumentQueryTotalCountRequest = {
connectionId: "connection-a",
database: "database-a",
collection: "collection-a",
filter: '{"active":true}',
generation: 3,
};
expect(isSameDocumentQueryTotalCountRequest(request, { ...request })).toBe(true);
expect(isSameDocumentQueryTotalCountRequest(request, { ...request, connectionId: "connection-b" })).toBe(false);
expect(isSameDocumentQueryTotalCountRequest(request, { ...request, database: "database-b" })).toBe(false);
expect(isSameDocumentQueryTotalCountRequest(request, { ...request, collection: "collection-b" })).toBe(false);
expect(isSameDocumentQueryTotalCountRequest(request, { ...request, filter: undefined })).toBe(false);
expect(isSameDocumentQueryTotalCountRequest(request, { ...request, generation: 4 })).toBe(false);
});
});
@@ -70,8 +70,7 @@ export interface DrawCanvasDataGridOptions {
cellIsSelected: (rowIndex: number, visibleColIdx: number) => boolean;
cellCanHover: (row: CanvasDataGridRow, actualColIdx: number) => boolean;
infiniteScrollEnabled: boolean;
pageSize: number;
currentPage: number;
pageOffset: number;
frozenColumnCount?: number;
columnAligns?: readonly ("left" | "right")[];
rightAlignedActionCell?: CanvasRightAlignedActionCell | null;
@@ -267,8 +266,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
cellIsSelected,
cellCanHover,
infiniteScrollEnabled,
pageSize,
currentPage,
pageOffset,
frozenColumnCount = 0,
columnAligns,
rightAlignedActionCell,
@@ -362,13 +360,18 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
ctx.font = item.status === "new" || item.status === "edited" || item.status === "draft" ? semiboldFont : normalFont;
ctx.textAlign = "center";
const textY = alignCanvasPixel(y + rowTextOffsetY, dpr);
ctx.save();
ctx.beginPath();
ctx.rect(0, y, rowNumberWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
ctx.clip();
if (item.isDraft) {
ctx.fillText("*", rowNumberTextX, textY);
} else if (infiniteScrollEnabled) {
ctx.fillText(String(item.displayIndex + 1), rowNumberTextX, textY);
} else {
ctx.fillText(String(item.displayIndex + 1 + pageSize * (currentPage - 1)), rowNumberTextX, textY);
ctx.fillText(String(item.displayIndex + 1 + pageOffset), rowNumberTextX, textY);
}
ctx.restore();
ctx.font = normalFont;
ctx.strokeStyle = theme.border;
@@ -29,6 +29,13 @@ export interface CanFetchNextDataGridSegmentOptions {
allRowsLoaded?: boolean;
}
export type DataGridInexactTotalRowCountMode = "at-least" | "estimated";
export function dataGridTotalRowCountLabelKey(totalRowCountIsExact: boolean, inexactMode: DataGridInexactTotalRowCountMode): "grid.totalRowCount" | "grid.totalRowCountAtLeast" | "grid.totalRowCountEstimated" {
if (totalRowCountIsExact) return "grid.totalRowCount";
return inexactMode === "estimated" ? "grid.totalRowCountEstimated" : "grid.totalRowCountAtLeast";
}
export function resolveDataGridPaginationTotal(options: { paginationTotalRowCount?: number; serverKnownTotalRowCount?: number; totalRowCountIsExact: boolean }): number | undefined {
if (options.paginationTotalRowCount !== undefined) return options.paginationTotalRowCount;
return options.totalRowCountIsExact ? options.serverKnownTotalRowCount : undefined;
@@ -4,7 +4,46 @@ export interface DocumentQueryTotals {
paginationTotal: number | undefined;
}
export function resolveDocumentQueryTotals(total: number, totalIsExact: boolean): DocumentQueryTotals {
export interface DocumentQueryTotalsPageOptions {
page?: number;
pageSize?: number;
rowCount?: number;
}
export interface DocumentQueryTotalCountRequest {
connectionId: string;
database: string;
collection: string;
filter: string | undefined;
generation: number;
}
export function isSameDocumentQueryTotalCountRequest(left: DocumentQueryTotalCountRequest, right: DocumentQueryTotalCountRequest): boolean {
return left.connectionId === right.connectionId && left.database === right.database && left.collection === right.collection && left.filter === right.filter && left.generation === right.generation;
}
/** When the current page is short, the true total is exactly offset + rowCount. */
export function exactTotalFromIncompleteDocumentPage(options: DocumentQueryTotalsPageOptions): number | undefined {
const page = options.page ?? 0;
const pageSize = options.pageSize;
const rowCount = options.rowCount;
if (!Number.isInteger(page) || page < 0 || typeof pageSize !== "number" || !Number.isInteger(pageSize) || pageSize <= 0 || typeof rowCount !== "number" || !Number.isInteger(rowCount) || rowCount < 0 || rowCount >= pageSize || (page > 0 && rowCount === 0)) {
return undefined;
}
return page * pageSize + rowCount;
}
export function resolveDocumentQueryTotals(total: number, totalIsExact: boolean, pageOptions?: DocumentQueryTotalsPageOptions): DocumentQueryTotals {
if (!totalIsExact) {
const exactFromPage = exactTotalFromIncompleteDocumentPage(pageOptions ?? {});
if (typeof exactFromPage === "number") {
return {
total: exactFromPage,
totalIsExact: true,
paginationTotal: exactFromPage,
};
}
}
return {
total,
totalIsExact,