fix(mongodb): avoid exact count for unfiltered queries

This commit is contained in:
Guoyu Su 2026-07-25 19:40:09 +08:00 committed by GitHub
parent 7e0f77eded
commit 3da4dd5ff9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 304 additions and 36 deletions

View File

@ -388,7 +388,7 @@ public final class MongoAgent {
if (filterDoc == null) {
filterDoc = new Document();
}
long total = col.countDocuments(filterDoc);
CollectionTotal total = collectionTotal(col, filterDoc);
var iterable = col.find(filterDoc).skip((int) skip).limit(limit);
if (projectionDoc != null) {
@ -402,10 +402,7 @@ public final class MongoAgent {
for (Document document : iterable) {
documents.add(bsonToJson(document));
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("documents", documents);
result.put("total", total);
return result;
return documentQueryResult(documents, total);
}
/**
@ -426,7 +423,7 @@ public final class MongoAgent {
if (filterDoc == null) {
filterDoc = new Document();
}
long total = col.countDocuments(filterDoc);
CollectionTotal total = collectionTotal(col, filterDoc);
var iterable = col.find(filterDoc).skip((int) skip).limit(limit);
if (projectionDoc != null) {
@ -440,12 +437,28 @@ public final class MongoAgent {
for (Document document : iterable) {
documents.add(bsonToExtendedJson(document));
}
return documentQueryResult(documents, total);
}
static CollectionTotal collectionTotal(MongoCollection<Document> collection, Document filter) {
if (filter.isEmpty()) {
return new CollectionTotal(collection.estimatedDocumentCount(), false);
}
return new CollectionTotal(collection.countDocuments(filter), true);
}
static Map<String, Object> documentQueryResult(List<?> documents, CollectionTotal total) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("documents", documents);
result.put("total", total);
result.put("total", total.value());
if (!total.exact()) {
result.put("total_is_exact", false);
}
return result;
}
record CollectionTotal(long value, boolean exact) {}
private static Object countDocuments(JsonObject params) {
MongoClient c = requireClient();
String database = params.get("database").getAsString();

View File

@ -28,6 +28,7 @@ import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeAll;
@ -148,6 +149,53 @@ class MongoAgentTest {
assertFalse(json.getAsJsonObject("error").get("message").getAsString().contains("Unknown method"));
}
@Test
void collectionTotalUsesEstimatedCountForEmptyFilter() {
List<String> calls = new ArrayList<>();
MongoCollection<Document> collection = recordingCountCollection(calls);
MongoAgent.CollectionTotal total = MongoAgent.collectionTotal(collection, new Document());
assertEquals(10_000_000L, total.value());
assertFalse(total.exact());
assertEquals(List.of("estimatedDocumentCount"), calls);
}
@Test
void collectionTotalUsesExactCountForNonEmptyFilter() {
List<String> calls = new ArrayList<>();
MongoCollection<Document> collection = recordingCountCollection(calls);
Document filter = new Document("status", "active");
MongoAgent.CollectionTotal total = MongoAgent.collectionTotal(collection, filter);
assertEquals(42L, total.value());
assertTrue(total.exact());
assertEquals(List.of("countDocuments:{\"status\": \"active\"}"), calls);
}
@Test
void estimatedDocumentQueryResultMarksTotalAsInexact() {
Map<String, Object> result = MongoAgent.documentQueryResult(
List.of(new Document("_id", 1)),
new MongoAgent.CollectionTotal(10_000_000L, false)
);
assertEquals(10_000_000L, result.get("total"));
assertEquals(false, result.get("total_is_exact"));
}
@Test
void exactDocumentQueryResultKeepsExistingWireShape() {
Map<String, Object> result = MongoAgent.documentQueryResult(
List.of(new Document("_id", 1)),
new MongoAgent.CollectionTotal(42L, true)
);
assertEquals(42L, result.get("total"));
assertFalse(result.containsKey("total_is_exact"));
}
@Test
void parsesOptionalDocumentParameters() {
JsonObject params = new JsonObject();
@ -670,6 +718,26 @@ class MongoAgentTest {
// helpers
@SuppressWarnings("unchecked")
private static MongoCollection<Document> recordingCountCollection(List<String> calls) {
return (MongoCollection<Document>) Proxy.newProxyInstance(
MongoCollection.class.getClassLoader(),
new Class<?>[] {MongoCollection.class},
(proxy, method, args) -> {
if ("estimatedDocumentCount".equals(method.getName())) {
calls.add("estimatedDocumentCount");
return 10_000_000L;
}
if ("countDocuments".equals(method.getName())) {
Document filter = (Document) args[0];
calls.add("countDocuments:" + filter.toJson());
return 42L;
}
throw new UnsupportedOperationException(method.getName());
}
);
}
private static void assertRpcModifiedCount(MongoClient client, int id, String updateJson, boolean many) {
JsonObject params = new JsonObject();
params.addProperty("database", "app");

View File

@ -17,6 +17,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import { clampSearchSplitWidth } from "@/lib/dataGrid/dataGridSearchSplit";
import { documentViewerFontStyle } from "@/lib/document/documentViewerFontStyle";
import { clampDocumentPage, documentPageRequestLimit, resetElasticsearchDocumentTotals, resolveElasticsearchDocumentTotals } from "@/lib/document/elasticsearchDocumentTotals";
import { canGoNextDocumentPage, resolveDocumentQueryTotals } from "@/lib/document/documentQueryTotals";
import {
arrayObjectAncestorPathForDocumentField,
buildDocumentFilterCondition,
@ -135,7 +136,16 @@ let elasticsearchCountExecutionId = "";
let elasticsearchCountGeneration = 0;
const documentStoreProvider = computed(() => documentStoreProviderFor(props.databaseType));
const pageTotal = computed(() => paginationTotal.value ?? total.value ?? 0);
const pageTotal = computed(() => paginationTotal.value);
const documentPageCount = computed(() => (pageTotal.value === undefined ? undefined : Math.max(1, Math.ceil(pageTotal.value / pageSize.value))));
const canGoNextPage = computed(() =>
canGoNextDocumentPage({
page: page.value,
pageSize: pageSize.value,
rowCount: documents.value.length,
paginationTotal: pageTotal.value,
}),
);
const documentRequestLimit = computed(() => {
if (documentStoreProvider.value.kind !== "elasticsearch" || paginationTotal.value === undefined) return pageSize.value;
return documentPageRequestLimit(page.value, pageSize.value, paginationTotal.value);
@ -147,7 +157,7 @@ const tableFindPaneStyle = computed(() => {
});
const documentFontStyle = computed(() => documentViewerFontStyle(settingsStore.editorSettings));
const documentStoreLabels = computed(() => ({
documentsLabel: documentStoreProvider.value.documentsLabel({ total: total.value ?? 0, t }),
documentsLabel: documentStoreProvider.value.documentsLabel({ total: total.value ?? 0, totalIsExact: totalIsExact.value, t }),
queryPreview: documentQueryPreview.value,
}));
@ -843,9 +853,10 @@ async function load() {
applyElasticsearchSearchTotal(result.total, result.total_is_exact !== false, filter);
} else {
cancelElasticsearchCount();
total.value = result.total;
totalIsExact.value = true;
paginationTotal.value = result.total;
const totals = resolveDocumentQueryTotals(result.total, result.total_is_exact !== false);
total.value = totals.total;
totalIsExact.value = totals.totalIsExact;
paginationTotal.value = totals.paginationTotal;
}
syncSelectedDocumentAfterLoad(previousSelectedIdx, previousSelectedId);
} catch (e: unknown) {
@ -1313,7 +1324,7 @@ function prevPage() {
}
function nextPage() {
if ((page.value + 1) * pageSize.value >= pageTotal.value) return;
if (!canGoNextPage.value) return;
page.value++;
void load();
}
@ -1479,8 +1490,9 @@ defineExpose({ focusSearch });
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="page <= 0" @click="prevPage">
<ChevronLeft class="h-3 w-3" />
</Button>
<span>{{ page + 1 }} / {{ Math.max(1, Math.ceil(pageTotal / pageSize)) }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="(page + 1) * pageSize >= pageTotal" @click="nextPage">
<span v-if="documentPageCount !== undefined">{{ page + 1 }} / {{ documentPageCount }}</span>
<span v-else>{{ page + 1 }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!canGoNextPage" @click="nextPage">
<ChevronRight class="h-3 w-3" />
</Button>
</div>

View File

@ -133,7 +133,7 @@ import { applyColumnFormatter, buildColumnFormatterKey, getSupportedTimeZoneOpti
import { temporalCellEditorConfig, type TemporalCellEditorConfig } from "@/lib/dataGrid/dataGridTemporalEditor";
import { isCancelSearchShortcut, isCopyCurrentRowShortcut, isDeleteCurrentRowShortcut, isFocusSearchShortcut, isModRShortcut, isSaveShortcut, isToggleTransposeShortcut } from "@/lib/editor/keyboardShortcuts";
import { dataGridHeaderContentWidth, scrollbarGutterWidth } from "@/lib/dataGrid/dataGridScrollGutter";
import { canGoNextDataGridPage, hasCompleteLocalDataGridResult } from "@/lib/dataGrid/dataGridPagination";
import { canGoNextDataGridPage, hasCompleteLocalDataGridResult, resolveDataGridPaginationTotal } from "@/lib/dataGrid/dataGridPagination";
import { dataGridCountQueryOptions } from "@/lib/dataGrid/dataGridQueryOptions";
import { dataGridBottomScrollTop, dataGridScrollPosition, isDataGridAtScrollBottom, isDataGridNearScrollBottom, shouldCheckInfiniteScrollAfterScroll, type DataGridScrollPosition } from "@/lib/dataGrid/dataGridInfiniteScroll";
import { CANVAS_DATA_GRID_ROW_HEIGHT, dataGridSearchMatchKey, drawCanvasDataGrid } from "@/lib/dataGrid/canvasDataGridRenderer";
@ -2398,7 +2398,13 @@ const displayedTotalRowCount = computed(() => serverKnownTotalRowCount.value ??
const totalRowCountIsExact = computed(() => props.totalRowCountIsExact !== false);
// A backend can expose an exact display total while deliberately restricting
// offset pagination to a smaller safe range.
const paginationTotalRowCount = computed(() => props.paginationTotalRowCount ?? serverKnownTotalRowCount.value);
const paginationTotalRowCount = computed(() =>
resolveDataGridPaginationTotal({
paginationTotalRowCount: props.paginationTotalRowCount,
serverKnownTotalRowCount: serverKnownTotalRowCount.value,
totalRowCountIsExact: totalRowCountIsExact.value,
}),
);
// Only a server-confirmed total drives pagination an inferred total means
// rows exist that we never fetched, so navigation must stay inside rows.length.
const hasKnownPaginationTotalRowCount = computed(() => typeof paginationTotalRowCount.value === "number" && paginationTotalRowCount.value >= 0);

View File

@ -1190,6 +1190,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
:page-limit="activeTab.resultPageLimit"
:count-sql="activeTab.resultCountSql"
:total-row-count="activeTab.resultTotalRowCount"
:total-row-count-is-exact="activeTab.resultTotalRowCount !== undefined || activeTab.result.total_is_exact !== false"
:total-row-count-loading="activeTab.resultTotalRowCountLoading"
:on-execute-sql="async (sql: string) => emit('executeSql', sql)"
:full-export-result="(onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => queryStore.fetchTabResultForExport(activeTab.id, onProgress)"
@ -1517,6 +1518,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
:page-offset="activeTab.resultPageOffset"
:page-limit="activeTab.resultPageLimit"
:total-row-count="activeTab.resultTotalRowCount"
:total-row-count-is-exact="activeTab.resultTotalRowCount !== undefined || activeTab.result.total_is_exact !== false"
:total-row-count-loading="activeTab.resultTotalRowCountLoading"
:on-execute-sql="async (sql: string) => emit('executeSql', sql)"
:full-export-result="(onProgress?: (info: { rowsExported: number; totalRows: number | null }) => void) => queryStore.fetchTabResultForExport(activeTab.id, onProgress)"

View File

@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { canGoNextDocumentPage, resolveDocumentQueryTotals } from "@/lib/document/documentQueryTotals";
describe("document query totals", () => {
it("uses exact totals as the pagination bound", () => {
expect(resolveDocumentQueryTotals(42, true)).toEqual({
total: 42,
totalIsExact: true,
paginationTotal: 42,
});
expect(canGoNextDocumentPage({ page: 3, pageSize: 10, rowCount: 10, paginationTotal: 42 })).toBe(true);
expect(canGoNextDocumentPage({ page: 4, pageSize: 10, rowCount: 2, paginationTotal: 42 })).toBe(false);
});
it("does not use estimated totals as the pagination bound", () => {
expect(resolveDocumentQueryTotals(10_000_000, false)).toEqual({
total: 10_000_000,
totalIsExact: false,
paginationTotal: undefined,
});
expect(canGoNextDocumentPage({ page: 999_999, pageSize: 10, rowCount: 10 })).toBe(true);
expect(canGoNextDocumentPage({ page: 1_000_000, pageSize: 10, rowCount: 3 })).toBe(false);
});
});

View File

@ -44,7 +44,7 @@ export type DocumentStoreProvider = {
kind: DocumentStoreKind;
filterInputLabel: string;
sortInputLabel: string;
documentsLabel(options: { total: number; t: ComposerTranslation }): string;
documentsLabel(options: { total: number; totalIsExact: boolean; t: ComposerTranslation }): string;
queryPreview(options: DocumentStoreQueryPreviewOptions): string;
sortInputForColumn(column: string, direction: "asc" | "desc" | null): string;
};
@ -80,7 +80,7 @@ const mongoDocumentProvider: DocumentStoreProvider = {
kind: "mongodb",
filterInputLabel: "find",
sortInputLabel: "sort",
documentsLabel: ({ total, t }) => t("mongo.documents", { count: total }),
documentsLabel: ({ total, totalIsExact, t }) => `${totalIsExact ? "" : "≈"}${t("mongo.documents", { count: total })}`,
queryPreview: ({ collection, filterJson, sortJson, skip, limit }) => {
const collectionRef = `db.getCollection(${JSON.stringify(collection)})`;
const parts = [`${collectionRef}.find(${mongoShellPreviewLiteral(filterJson || "{}")})`];

View File

@ -21,6 +21,11 @@ export interface CompleteLocalDataGridResultOptions {
hasMore?: boolean;
}
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;
}
export function hasCompleteLocalDataGridResult(options: CompleteLocalDataGridResultOptions): boolean {
if (!options.isResultsContext || options.truncated === true || options.hasMore === true) return false;
if (options.pageLimit === undefined) return true;

View File

@ -0,0 +1,20 @@
export interface DocumentQueryTotals {
total: number;
totalIsExact: boolean;
paginationTotal: number | undefined;
}
export function resolveDocumentQueryTotals(total: number, totalIsExact: boolean): DocumentQueryTotals {
return {
total,
totalIsExact,
paginationTotal: totalIsExact ? total : undefined,
};
}
export function canGoNextDocumentPage(options: { page: number; pageSize: number; rowCount: number; paginationTotal?: number }): boolean {
if (typeof options.paginationTotal === "number") {
return (options.page + 1) * options.pageSize < options.paginationTotal;
}
return options.rowCount >= options.pageSize;
}

View File

@ -645,7 +645,7 @@ export function evaluateMongoAggregateSafety(command: MongoAggregateCommand, opt
return { allowed: true };
}
export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeMs: number, total: number, copyDocuments?: unknown[]): QueryResult {
export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeMs: number, total: number, copyDocuments?: unknown[], totalIsExact = true): QueryResult {
const columns: string[] = [];
for (const doc of documents) {
@ -668,6 +668,7 @@ export function mongoDocumentsToQueryResult(documents: unknown[], executionTimeM
rows,
mongo_documents: documents,
...(copyDocuments?.length === documents.length ? { mongo_copy_documents: copyDocuments } : {}),
...(totalIsExact ? {} : { total_is_exact: false }),
affected_rows: total,
execution_time_ms: Math.max(0, Math.round(executionTimeMs)),
truncated: total > documents.length,

View File

@ -3113,7 +3113,7 @@ export const useQueryStore = defineStore("query", () => {
case "find": {
queryExecutionLog("info", "mongo-find:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const result = await api.mongoFindDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.skip, mongoCommand.limit, mongoCommand.filter, mongoCommand.projection, mongoCommand.sort, executionId);
const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents)));
const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false)));
allResults.push(queryResult);
mongoEditTarget = mongoCommands.length === 1 && !mongoCommand.projection && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined;
queryExecutionLog("info", "mongo-find:done", {
@ -3129,7 +3129,7 @@ export const useQueryStore = defineStore("query", () => {
case "findOne": {
queryExecutionLog("info", "mongo-find-one:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const result = await api.mongoFindOne(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.projection, mongoCommand.options, executionId);
const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents)));
const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false)));
allResults.push(queryResult);
mongoEditTarget = mongoCommands.length === 1 && !mongoCommand.projection && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined;
queryExecutionLog("info", "mongo-find-one:done", {
@ -3176,7 +3176,7 @@ export const useQueryStore = defineStore("query", () => {
queryExecutionLog("info", "mongo-aggregate:start", { traceId, collection: mongoCommand.collection, database: currentDatabase });
const aggregateMaxRows = normalizeResultPageSize(pageLimit ?? options?.pagination?.limit ?? settingsStore.editorSettings.pageSize);
const result = await api.mongoAggregateDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.pipeline, aggregateMaxRows, mongoCommand.options, executionId);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents))));
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false))));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-aggregate:done", {
traceId,
@ -3255,7 +3255,7 @@ export const useQueryStore = defineStore("query", () => {
: mongoCommand.kind === "findOneAndReplace"
? await api.mongoFindOneAndReplace(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.replacement, mongoCommand.options)
: await api.mongoFindOneAndDelete(tab.connectionId, currentDatabase, mongoCommand.collection, mongoCommand.filter, mongoCommand.options);
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents))));
allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(result.documents, performance.now() - commandStartedAt, result.total, result.extended_documents, result.total_is_exact !== false))));
mongoEditTarget = undefined;
queryExecutionLog("info", "mongo-find-and-modify:done", {
traceId,
@ -3602,7 +3602,7 @@ export const useQueryStore = defineStore("query", () => {
// affected_rows. Use it directly so the result-grid can compute the
// page count without issuing a separate COUNT query.
let totalRowCountResolved = false;
if (current.result && current.mode === "query" && typeof pageLimit === "number" && !countSql && typeof current.result.affected_rows === "number" && current.result.affected_rows > current.result.rows.length) {
if (current.result && current.result.total_is_exact !== false && current.mode === "query" && typeof pageLimit === "number" && !countSql && typeof current.result.affected_rows === "number" && current.result.affected_rows > current.result.rows.length) {
current.resultTotalRowCount = current.result.affected_rows;
current.resultTotalRowCountLoading = false;
totalRowCountResolved = true;

View File

@ -573,6 +573,8 @@ export interface QueryResult {
mongo_copy_documents?: unknown[];
affected_rows: number;
execution_time_ms: number;
/** Whether a backend-reported result total is exact. */
total_is_exact?: boolean;
truncated?: boolean;
session_id?: string | null;
has_more?: boolean;

View File

@ -47,4 +47,18 @@ mod tests {
.unwrap();
assert!(deserialized.total_is_exact);
}
#[test]
fn inexact_results_preserve_the_explicit_flag() {
let deserialized: DocumentQueryResult = serde_json::from_value(serde_json::json!({
"documents": [],
"total": 10_000_000,
"total_is_exact": false,
}))
.unwrap();
assert!(!deserialized.total_is_exact);
let serialized = serde_json::to_value(deserialized).unwrap();
assert_eq!(serialized.get("total_is_exact"), Some(&serde_json::json!(false)));
}
}

View File

@ -637,10 +637,11 @@ pub async fn find_documents(
_ => doc! {},
};
let total = if filter_doc.is_empty() {
col.estimated_document_count().await.map_err(|e| e.to_string())?
} else {
let total_is_exact = !filter_doc.is_empty();
let total = if total_is_exact {
col.count_documents(filter_doc.clone()).await.map_err(|e| e.to_string())?
} else {
col.estimated_document_count().await.map_err(|e| e.to_string())?
};
let mut find = col.find(filter_doc).skip(skip).limit(limit);
@ -675,7 +676,7 @@ pub async fn find_documents(
raw_documents: None,
extended_documents: Some(extended_documents),
total,
total_is_exact: true,
total_is_exact,
})
}
@ -781,10 +782,11 @@ pub async fn find_documents_extended_json(
_ => doc! {},
};
let total = if filter_doc.is_empty() {
col.estimated_document_count().await.map_err(|e| e.to_string())?
} else {
let total_is_exact = !filter_doc.is_empty();
let total = if total_is_exact {
col.count_documents(filter_doc.clone()).await.map_err(|e| e.to_string())?
} else {
col.estimated_document_count().await.map_err(|e| e.to_string())?
};
let mut find = col.find(filter_doc).skip(skip).limit(limit);
@ -820,7 +822,7 @@ pub async fn find_documents_extended_json(
documents,
raw_documents: None,
total,
total_is_exact: true,
total_is_exact,
})
}

View File

@ -1,6 +1,31 @@
import { strict as assert } from "node:assert";
import { test } from "vitest";
import { canGoNextDataGridPage, hasCompleteLocalDataGridResult } from "../../apps/desktop/src/lib/dataGrid/dataGridPagination.ts";
import { canGoNextDataGridPage, hasCompleteLocalDataGridResult, resolveDataGridPaginationTotal } from "../../apps/desktop/src/lib/dataGrid/dataGridPagination.ts";
test("estimated display totals do not become pagination bounds", () => {
assert.equal(
resolveDataGridPaginationTotal({
serverKnownTotalRowCount: 10_000_000,
totalRowCountIsExact: false,
}),
undefined,
);
assert.equal(
resolveDataGridPaginationTotal({
serverKnownTotalRowCount: 10_000_000,
totalRowCountIsExact: true,
}),
10_000_000,
);
assert.equal(
resolveDataGridPaginationTotal({
paginationTotalRowCount: 500,
serverKnownTotalRowCount: 10_000_000,
totalRowCountIsExact: false,
}),
500,
);
});
test("first query page is complete when its known total is already loaded", () => {
assert.equal(

View File

@ -39,11 +39,12 @@ test("providers build store-specific query previews", () => {
const mongo = documentStoreProviderFor("mongodb");
const elasticsearch = documentStoreProviderFor("elasticsearch");
assert.equal(mongo.documentsLabel({ total: 7, t }), "mongo.documents:7");
assert.equal(mongo.documentsLabel({ total: 7, totalIsExact: true, t }), "mongo.documents:7");
assert.equal(mongo.documentsLabel({ total: 7, totalIsExact: false, t }), "≈mongo.documents:7");
assert.equal(mongo.queryPreview({ collection: "orders", filterJson: '{"city":"长治"}', sortJson: '{"createdAt":-1}', skip: 20, limit: 10 }), 'db.getCollection("orders").find({"city":"长治"}).sort({"createdAt":-1}).skip(20).limit(10)');
assert.equal(mongo.queryPreview({ collection: "order-events", filterJson: '{"city":"长治"}', sortJson: undefined, skip: 0, limit: 100 }), 'db.getCollection("order-events").find({"city":"长治"}).skip(0).limit(100)');
assert.equal(mongo.queryPreview({ collection: "orders", filterJson: '{"snowflake":{"$numberLong":"9007199254740993"}}', sortJson: undefined, skip: 0, limit: 100 }), 'db.getCollection("orders").find({"snowflake":NumberLong("9007199254740993")}).skip(0).limit(100)');
assert.equal(elasticsearch.documentsLabel({ total: 7, t }), "Documents");
assert.equal(elasticsearch.documentsLabel({ total: 7, totalIsExact: false, t }), "Documents");
assert.equal(elasticsearch.filterInputLabel, "filter");
assert.equal(
elasticsearch.queryPreview({ collection: "orders", filterJson: '{"city":"长治"}', sortJson: '{"createdAt":-1}', skip: 20, limit: 10 }),

View File

@ -1020,6 +1020,15 @@ test("mongoDocumentsToQueryResult keeps aligned extended documents for copying",
assert.equal(mongoDocumentsToQueryResult(documents, 5, 1, []).mongo_copy_documents, undefined);
});
test("mongoDocumentsToQueryResult preserves an inexact total marker", () => {
const result = mongoDocumentsToQueryResult([{ _id: "1" }], 5, 10_000_000, undefined, false);
assert.equal(result.total_is_exact, false);
assert.equal(result.affected_rows, 10_000_000);
assert.equal(result.truncated, true);
assert.equal(mongoDocumentsToQueryResult([{ _id: "1" }], 5, 1).total_is_exact, undefined);
});
test("mongoDocumentsToQueryResult displays ids without losing raw type metadata", () => {
const documents = [
{ _id: { $oid: "6743e4bfa3f6f84bc3fff6c8" }, name: "object id" },

View File

@ -4887,6 +4887,70 @@ test("query execution keeps automatically counting total rows in the background"
}
});
test("inexact backend totals do not become query pagination bounds", async () => {
const restoreStorage = installMemoryStorage();
setActivePinia(createPinia());
const connectionStore = useConnectionStore();
const settingsStore = useSettingsStore();
const store = useQueryStore();
const originalFetch = globalThis.fetch;
settingsStore.updateEditorSettings({ autoCalculateTotalRows: false });
connectionStore.addEphemeralConnection(conn("conn-1"));
const tabId = store.createTab("conn-1", "db", "Query", "query", "public");
const tab = store.tabs.find((item) => item.id === tabId);
assert.ok(tab);
globalThis.fetch = withConnectionHealthMock(async (input) => {
const url = String(input);
if (url === "/api/query/prepare-pagination-plan") {
return new Response(
JSON.stringify({
sqlToExecute: "select id from users limit 100",
pageSql: "select id from users limit 100",
pageLimit: 100,
pageOffset: 0,
useAgentResultSession: false,
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
}
if (url === "/api/query/execute-multi") {
return new Response(
JSON.stringify([
{
columns: ["id"],
rows: Array.from({ length: 100 }, (_, index) => [index + 1]),
affected_rows: 10_000_000,
execution_time_ms: 1,
total_is_exact: false,
},
]),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
}
if (url === "/api/query/analyze-editability") {
return new Response(JSON.stringify({ editable: false, reason: "complex-source" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response("unexpected request", { status: 500 });
});
try {
await store.executeTabSql(tabId, "select id from users");
assert.equal(tab.result?.total_is_exact, false);
assert.equal(tab.result?.affected_rows, 10_000_000);
assert.equal(tab.resultTotalRowCount, undefined);
assert.equal(tab.resultTotalRowCountLoading, false);
} finally {
globalThis.fetch = originalFetch;
restoreStorage();
}
});
for (const resultState of [
{ label: "truncated", result: { truncated: true, has_more: false } },
{ label: "ambiguous exhaustion", result: {} },