feat(es): paginate past 10k documents via pit and search_after
This commit is contained in:
1 parent
1d71a5fc23
commit
73a7b3f2c7
35 files changed
+1622
-153
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { computed } from "vue";
|
||||
import { computed, reactive } from "vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useDataGridActions } from "@/composables/useDataGridActions";
|
||||
import { clearTableMetadataCache } from "@/lib/metadata/tableMetadataCache";
|
||||
@@ -130,6 +130,7 @@ describe("useDataGridActions", () => {
|
||||
beforeEach(() => {
|
||||
clearTableMetadataCache();
|
||||
vi.clearAllMocks();
|
||||
mocks.executeTabSql.mockReset();
|
||||
mocks.tabs.length = 0;
|
||||
mocks.tableOpenPageSize = 100;
|
||||
mocks.infiniteScroll = true;
|
||||
@@ -543,6 +544,71 @@ describe("useDataGridActions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("advances Elasticsearch cursors when jumping across pages", async () => {
|
||||
mocks.infiniteScroll = false;
|
||||
mocks.getConfig.mockReturnValue({ id: "elasticsearch-1", db_type: "elasticsearch" });
|
||||
const tab = reactive({
|
||||
id: "tab-1",
|
||||
connectionId: "elasticsearch-1",
|
||||
database: "",
|
||||
title: "Query",
|
||||
sql: "SELECT * FROM `dbx-app-logs-v1` AS dalv",
|
||||
resultBaseSql: "SELECT * FROM `dbx-app-logs-v1` AS dalv",
|
||||
resultPageLimit: 100,
|
||||
resultPageOffset: 0,
|
||||
resultSessionId: "cursor-1",
|
||||
resultClientSessionId: "client-1",
|
||||
result: {
|
||||
columns: ["message"],
|
||||
rows: [["page-1"]],
|
||||
affected_rows: 100_000,
|
||||
execution_time_ms: 1,
|
||||
session_id: "cursor-1",
|
||||
has_more: true,
|
||||
},
|
||||
mode: "query",
|
||||
isDirty: false,
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
} as QueryTab);
|
||||
mocks.executeTabSql.mockImplementation(async (_id, _sql, options) => {
|
||||
const pagination = options.pagination as { offset: number };
|
||||
expect(tab.resultPageJumpProgress).toEqual({
|
||||
completedRequests: pagination.offset / 100 - 1,
|
||||
totalRequests: 3,
|
||||
targetPage: 4,
|
||||
});
|
||||
if (options.retainDisplayedResult) {
|
||||
expect(tab.resultPageOffset).toBe(0);
|
||||
tab.resultSessionId = `cursor-${pagination.offset / 100 + 1}`;
|
||||
return;
|
||||
}
|
||||
tab.resultPageOffset = pagination.offset;
|
||||
tab.resultSessionId = `cursor-${pagination.offset / 100 + 1}`;
|
||||
tab.result = {
|
||||
columns: ["message"],
|
||||
rows: [[`page-${pagination.offset / 100 + 1}`]],
|
||||
affected_rows: 100_000,
|
||||
execution_time_ms: 1,
|
||||
session_id: `cursor-${pagination.offset / 100 + 1}`,
|
||||
has_more: true,
|
||||
};
|
||||
});
|
||||
const actions = useDataGridActions(computed(() => tab));
|
||||
|
||||
await actions.onPaginate(300, 100);
|
||||
|
||||
expect(mocks.executeTabSql.mock.calls.map((call) => call[2].pagination)).toEqual([
|
||||
{ offset: 100, limit: 100, sessionId: "cursor-1", clientSessionId: "client-1" },
|
||||
{ offset: 200, limit: 100, sessionId: "cursor-2", clientSessionId: "client-1" },
|
||||
{ offset: 300, limit: 100, sessionId: "cursor-3", clientSessionId: "client-1" },
|
||||
]);
|
||||
expect(mocks.executeTabSql.mock.calls.map((call) => call[2].retainDisplayedResult)).toEqual([true, true, undefined]);
|
||||
expect(tab.resultPageOffset).toBe(300);
|
||||
expect(tab.resultPageJumpProgress).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses the active multi-database result target for pagination", async () => {
|
||||
const tab = {
|
||||
id: "tab-1",
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { elasticsearchCursorPageJumpRequestCount } from "@/lib/dataGrid/dataGridPagination";
|
||||
import { usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
|
||||
import * as api from "@/lib/backend/api";
|
||||
@@ -335,26 +336,100 @@ export function useDataGridActions(activeTab: ComputedRef<QueryTab | undefined>)
|
||||
const continuesResultSession = appendResult ? offset === expectedNextOffset : offset === expectedNextOffset && limit === tab.resultPageLimit;
|
||||
const sessionId = tab.result?.has_more && tab.result?.session_id && continuesResultSession ? tab.result.session_id : undefined;
|
||||
const resultBaseSql = queryResultBaseSql(tab);
|
||||
await queryStore.executeTabSql(tab.id, baseSql, {
|
||||
...activeQueryTargetOptions(tab),
|
||||
resultBaseSql,
|
||||
resultSortedSql: tab.resultSortedSql,
|
||||
...(hasDatabaseSort
|
||||
? {
|
||||
querySort: {
|
||||
resultColumns: sortColumns.resultColumns,
|
||||
columnIndex: sortColumns.columnIndex,
|
||||
column: tab.resultSortColumn!,
|
||||
direction: tab.resultSortDirection!,
|
||||
},
|
||||
const executePage = (pageOffset: number, pageSessionId?: string, retainDisplayedResult = false) =>
|
||||
queryStore.executeTabSql(tab.id, baseSql, {
|
||||
...activeQueryTargetOptions(tab),
|
||||
resultBaseSql,
|
||||
resultSortedSql: tab.resultSortedSql,
|
||||
...(hasDatabaseSort
|
||||
? {
|
||||
querySort: {
|
||||
resultColumns: sortColumns.resultColumns,
|
||||
columnIndex: sortColumns.columnIndex,
|
||||
column: tab.resultSortColumn!,
|
||||
direction: tab.resultSortDirection!,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
pagination: { offset: pageOffset, limit, sessionId: pageSessionId, clientSessionId: pageSessionId ? tab.resultClientSessionId : undefined },
|
||||
...appendOptions,
|
||||
preserveResultDuringExecution: true,
|
||||
preserveTotalRowCountDuringExecution: true,
|
||||
replaceActiveResultInGroup: true,
|
||||
...(retainDisplayedResult ? { retainDisplayedResult: true } : {}),
|
||||
});
|
||||
const executionTarget = queryStore.activeResultExecutionTarget(tab.id);
|
||||
const executionConnection = connectionStore.getConfig(executionTarget?.connectionId ?? tab.connectionId);
|
||||
const effectiveDbType = effectiveDatabaseTypeForConnection(executionConnection);
|
||||
const usesElasticsearchCursor = effectiveDbType === "elasticsearch" || effectiveDbType === "easysearch";
|
||||
|
||||
// Elasticsearch search_after has no random page access, so every missing
|
||||
// cursor must be fetched in order. This workaround still sends one ES
|
||||
// request per skipped page (page 1 -> 101 sends 100 requests);
|
||||
// retainDisplayedResult only hides those intermediate pages from the UI.
|
||||
const currentResultSessionId = tab.resultSessionId ?? tab.result?.session_id;
|
||||
if (usesElasticsearchCursor && !appendResult && typeof expectedNextOffset === "number" && limit === tab.resultPageLimit && currentResultSessionId) {
|
||||
let nextOffset = expectedNextOffset;
|
||||
let nextSessionId: string | undefined = currentResultSessionId;
|
||||
const currentPage = Math.floor(nextOffset / limit);
|
||||
const targetPage = Math.floor(offset / limit) + 1;
|
||||
const totalRequests = elasticsearchCursorPageJumpRequestCount(currentPage, targetPage);
|
||||
const jumpProgress =
|
||||
totalRequests > 1
|
||||
? {
|
||||
completedRequests: 0,
|
||||
totalRequests,
|
||||
targetPage,
|
||||
}
|
||||
: undefined;
|
||||
if (jumpProgress) {
|
||||
tab.resultPageJumpProgress = jumpProgress;
|
||||
}
|
||||
const activeJumpProgress = tab.resultPageJumpProgress;
|
||||
const executeCursorPage = async (pageOffset: number, pageSessionId?: string, retainDisplayedResult = false) => {
|
||||
await executePage(pageOffset, pageSessionId, retainDisplayedResult);
|
||||
if (activeJumpProgress) {
|
||||
activeJumpProgress.completedRequests += 1;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
if (offset < nextOffset) {
|
||||
await executeCursorPage(0, undefined, offset !== 0);
|
||||
if (offset === 0) {
|
||||
return;
|
||||
}
|
||||
: {}),
|
||||
pagination: { offset, limit, sessionId, clientSessionId: sessionId ? tab.resultClientSessionId : undefined },
|
||||
...appendOptions,
|
||||
preserveResultDuringExecution: true,
|
||||
preserveTotalRowCountDuringExecution: true,
|
||||
replaceActiveResultInGroup: true,
|
||||
});
|
||||
const restartedTab = activeTab.value;
|
||||
if (restartedTab?.id !== tab.id) {
|
||||
return;
|
||||
}
|
||||
nextOffset = limit;
|
||||
nextSessionId = restartedTab.resultSessionId;
|
||||
}
|
||||
while (nextOffset < offset) {
|
||||
if (!nextSessionId) {
|
||||
return;
|
||||
}
|
||||
await executeCursorPage(nextOffset, nextSessionId, true);
|
||||
const advancedTab = activeTab.value;
|
||||
if (advancedTab?.id !== tab.id) {
|
||||
return;
|
||||
}
|
||||
nextOffset += limit;
|
||||
nextSessionId = advancedTab.resultSessionId;
|
||||
}
|
||||
if (nextOffset === offset && nextSessionId) {
|
||||
await executeCursorPage(offset, nextSessionId);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
if (tab.resultPageJumpProgress === activeJumpProgress) {
|
||||
tab.resultPageJumpProgress = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await executePage(offset, sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user