fix(mongodb): support query result pagination

This commit is contained in:
zipg authored and GitHub committed 2026-07-28 01:02:24 +08:00
1 parent 09f794fd97
commit 1672d64619
4 files changed
+367 -3

No files matched your search

@@ -26,6 +26,15 @@ export interface MongoFindCommand {
sort?: string;
}
export interface MongoFindPaginationPlan {
pageOffset: number;
pageLimit: number;
requestSkip: number;
requestLimit: number;
logicalSkip: number;
logicalLimit?: number;
}
export interface MongoFindOneCommand {
collection: string;
filter: string;
@@ -167,6 +176,42 @@ export function parseMongoFindCommand(input: string): MongoFindCommand | null {
};
}
export function planMongoFindPagination(input: string, command: MongoFindCommand, pageOffset: number, pageLimit: number): MongoFindPaginationPlan | null {
const source = input.trim().replace(/;$/, "").trim();
const target = parseFindTarget(source);
if (!target) return null;
const findOpenIndex = source.indexOf("(", target.findCallIndex);
const findCloseIndex = findMatchingParen(source, findOpenIndex);
if (findCloseIndex < 0) return null;
const chain = source.slice(findCloseIndex + 1).trim();
if (chain && !chain.startsWith(".")) return null;
const normalizedPageOffset = Math.max(0, Math.trunc(pageOffset));
const normalizedPageLimit = Math.max(1, Math.trunc(pageLimit));
const hasExplicitSkip = findChainedMethodCallIndex(chain, "skip") >= 0;
const hasExplicitLimit = findChainedMethodCallIndex(chain, "limit") >= 0;
const logicalSkip = hasExplicitSkip ? Math.max(0, Math.trunc(command.skip)) : 0;
// limit(0) is unbounded in MongoDB; a negative limit keeps the same row
// bound while requesting single-batch cursor semantics.
const logicalLimit = hasExplicitLimit && command.limit !== 0 ? Math.abs(Math.trunc(command.limit)) : undefined;
const remaining = logicalLimit === undefined ? normalizedPageLimit : Math.max(0, logicalLimit - normalizedPageOffset);
return {
pageOffset: normalizedPageOffset,
pageLimit: normalizedPageLimit,
requestSkip: logicalSkip + normalizedPageOffset,
requestLimit: Math.min(normalizedPageLimit, remaining),
logicalSkip,
logicalLimit,
};
}
export function mongoFindLogicalTotal(total: number, plan: Pick<MongoFindPaginationPlan, "logicalSkip" | "logicalLimit">): number {
const afterSkip = Math.max(0, Math.trunc(total) - plan.logicalSkip);
return plan.logicalLimit === undefined ? afterSkip : Math.min(afterSkip, plan.logicalLimit);
}
export function parseMongoFindOneCommand(input: string): MongoFindOneCommand | null {
const source = input.trim().replace(/;$/, "").trim();
const target = parseCollectionMethodTarget(source, "findOne");
+41 -3
View File
@@ -20,7 +20,9 @@ import {
mongoDocumentsToQueryResult,
describeMongoCommandParseFailure,
mongoDroppedIndexesToQueryResult,
mongoFindLogicalTotal,
mongoIndexesToQueryResult,
planMongoFindPagination,
mongoUseToQueryResult,
mongoVersionToQueryResult,
mongoWriteToQueryResult,
@@ -3219,6 +3221,7 @@ export const useQueryStore = defineStore("query", () => {
// earlier `use ...` statements in the same editor selection.
let currentDatabase = tab.database;
let mongoEditTarget: QueryTab["mongoEditTarget"] | undefined;
let mongoFindPageState: { pageLimit: number; pageOffset: number; total: number; totalIsExact: boolean } | undefined;
for (const parsedCommand of mongoCommands) {
let mongoCommand = parsedCommand.command;
@@ -3239,9 +3242,25 @@ export const useQueryStore = defineStore("query", () => {
switch (mongoCommand.kind) {
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, result.total_is_exact !== false)));
const pagePlan = planMongoFindPagination(sourceStatement, mongoCommand, options?.pagination?.offset ?? 0, normalizeResultPageSize(options?.pagination?.limit ?? settingsStore.editorSettings.pageSize));
if (!pagePlan) throw new Error(describeMongoCommandParseFailure(sourceStatement));
// A stale request can point past an explicit .limit() bound. Keep
// the backend call bounded so limit(0) cannot become unbounded.
const result = await api.mongoFindDocuments(tab.connectionId, currentDatabase, mongoCommand.collection, pagePlan.requestSkip, Math.max(1, pagePlan.requestLimit), mongoCommand.filter, mongoCommand.projection, mongoCommand.sort, executionId);
const documents = pagePlan.requestLimit === 0 ? [] : result.documents;
const extendedDocuments = pagePlan.requestLimit === 0 ? [] : result.extended_documents;
const totalIsExact = result.total_is_exact !== false;
const reportedTotal = mongoFindLogicalTotal(result.total, pagePlan);
const loadedLowerBound = pagePlan.pageOffset + documents.length;
const total = totalIsExact ? reportedTotal : Math.max(reportedTotal, loadedLowerBound);
const hasMore = totalIsExact ? loadedLowerBound < total : pagePlan.requestLimit > 0 && documents.length >= pagePlan.requestLimit && (pagePlan.logicalLimit === undefined || loadedLowerBound < pagePlan.logicalLimit);
const queryResult = markQueryResultRowsRaw(annotateMongoResult(mongoDocumentsToQueryResult(documents, performance.now() - commandStartedAt, total, extendedDocuments, totalIsExact)));
queryResult.truncated = hasMore;
queryResult.has_more = hasMore;
allResults.push(queryResult);
if (mongoCommands.length === 1) {
mongoFindPageState = { pageLimit: pagePlan.pageLimit, pageOffset: pagePlan.pageOffset, total, totalIsExact };
}
mongoEditTarget = mongoCommands.length === 1 && !mongoCommand.projection && queryResult.columns.includes("_id") ? { collection: mongoCommand.collection, idColumn: "_id" } : undefined;
queryExecutionLog("info", "mongo-find:done", {
traceId,
@@ -3472,8 +3491,20 @@ export const useQueryStore = defineStore("query", () => {
if (openInNewResultTab && current.isCancelling && restorePendingResultRun(current, executionId)) return false;
const activeGroupIndex = current.activeResultIndex;
const activeGroupResults = current.results;
const findPageState = mongoFindPageState;
const shouldAppendResult = !!findPageState && !!options?.appendResult && !!current.result && allResults.length === 1;
const shouldReplaceActiveResultInGroup = options?.replaceActiveResultInGroup === true && allResults.length === 1 && Array.isArray(activeGroupResults) && typeof activeGroupIndex === "number" && activeGroupIndex >= 0 && activeGroupIndex < activeGroupResults.length;
if (shouldReplaceActiveResultInGroup) {
if (shouldAppendResult) {
if (findPageState!.pageOffset !== current.result!.rows.length) {
throw new Error("Ignoring a stale MongoDB result segment whose offset no longer matches the loaded rows");
}
const appendedResult = appendQueryResultSegment(current.result!, allResults[0]!, options!.appendResult!.maxRows);
if (Array.isArray(activeGroupResults) && typeof activeGroupIndex === "number" && activeGroupIndex >= 0 && activeGroupIndex < activeGroupResults.length) {
current.results = activeGroupResults.slice();
current.results[activeGroupIndex] = appendedResult;
}
current.result = appendedResult;
} else if (shouldReplaceActiveResultInGroup) {
current.results = activeGroupResults.slice();
current.results[activeGroupIndex] = allResults[0];
current.result = allResults[0];
@@ -3499,6 +3530,13 @@ export const useQueryStore = defineStore("query", () => {
current.tableMeta = undefined;
current.resultBaseSql = shouldReplaceActiveResultInGroup ? (current.resultBaseSql ?? options?.resultBaseSql ?? sql) : (options?.resultBaseSql ?? sql);
current.resultSortedSql = options?.resultSortedSql;
current.resultPageSql = undefined;
current.resultPageLimit = mongoFindPageState?.pageLimit;
current.resultPageOffset = shouldAppendResult ? (current.resultPageOffset ?? 0) : mongoFindPageState?.pageOffset;
current.resultCountSql = undefined;
current.resultSessionId = undefined;
current.resultTotalRowCount = mongoFindPageState?.totalIsExact ? mongoFindPageState.total : undefined;
current.resultTotalRowCountLoading = false;
syncDisplayedResultRun(current, current.resultBaseSql ?? options?.resultBaseSql ?? sql, openInNewResultTab);
if (current.database !== currentDatabase) current.database = currentDatabase;
}