fix(oceanbase-oracle): bound first query result page

This commit is contained in:
efeire authored and GitHub committed 2026-09-20 00:49:12 +08:00
1 parent b5bf45e224
commit 30d8b3970a
8 files changed
+258 -14

No files matched your search

@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import type { QueryResult } from "@/types/database";
import { stripPaginationRowNumber } from "@/lib/query/queryPaginationResult";
const result: QueryResult = {
columns: ["__dbx_row_num", "shape", "__dbx_row_num"],
rows: [["user value", "POINT(1 2)", 101]],
column_types: ["VARCHAR", "GEOMETRY", "NUMBER"],
column_sortables: [true, false, true],
spatial_columns: [{ column_index: 1, srid: 4326 }],
spatial_values: [[null, 4326, null]],
large_value_cells: [{ row_index: 0, column_index: 1, original_bytes: 200 }],
hidden_column_indexes: [0],
affected_rows: 0,
execution_time_ms: 1,
};
describe("pagination result columns", () => {
it("removes only the generated trailing column and keeps user columns and metadata aligned", () => {
const cleaned = stripPaginationRowNumber(result, "__dbx_row_num");
expect(cleaned.columns).toEqual(["__dbx_row_num", "shape"]);
expect(cleaned.rows).toEqual([["user value", "POINT(1 2)"]]);
expect(cleaned.column_types).toEqual(["VARCHAR", "GEOMETRY"]);
expect(cleaned.column_sortables).toEqual([true, false]);
expect(cleaned.spatial_values).toEqual([[null, 4326]]);
expect(cleaned.spatial_columns).toEqual(result.spatial_columns);
expect(cleaned.large_value_cells).toEqual(result.large_value_cells);
expect(cleaned.hidden_column_indexes).toEqual([0]);
expect(result.columns).toHaveLength(3);
});
it("leaves user results untouched when the plan has no helper or the trailing name differs", () => {
expect(stripPaginationRowNumber(result, undefined)).toBe(result);
expect(stripPaginationRowNumber(result, "different_helper")).toBe(result);
});
});
+1
View File
@@ -438,6 +438,7 @@ export interface QueryPaginationExecutionPlan {
countSql?: string;
exactQueryRowBound?: number;
useAgentResultSession: boolean;
paginationRowNumberColumn?: string;
}
export type QuerySortDirection = "asc" | "desc";
@@ -0,0 +1,18 @@
import type { QueryResult } from "@/types/database";
/** Remove only the trailing helper identified by the pagination plan. */
export function stripPaginationRowNumber(result: QueryResult, column: string | undefined): QueryResult {
const index = result.columns.length - 1;
if (!column || index < 0 || result.columns[index] !== column) return result;
return {
...result,
columns: result.columns.slice(0, index),
rows: result.rows.map((row) => row.slice(0, index)),
column_types: result.column_types?.slice(0, index),
column_sortables: result.column_sortables?.slice(0, index),
spatial_values: result.spatial_values?.map((row) => row.slice(0, index)),
spatial_columns: result.spatial_columns?.filter((column) => column.column_index < index),
large_value_cells: result.large_value_cells?.filter((cell) => cell.column_index < index),
hidden_column_indexes: result.hidden_column_indexes?.filter((columnIndex) => columnIndex < index),
};
}
@@ -1787,6 +1787,71 @@ describe("queryStore hidden primary key editing", () => {
expect(tab.queryAnalysis).toBeUndefined();
});
it("removes the generated row number before appending OceanBase Oracle pages", async () => {
const sql = "SELECT LEVEL AS N FROM DUAL CONNECT BY LEVEL <= 4";
getConnectionConfig.mockReturnValue({ id: "ob-1", name: "OceanBase", db_type: "oceanbase-oracle", database: "app" });
analyzeEditableQueryEditability.mockResolvedValue({ editable: false });
prepareQueryPaginationExecutionPlan.mockImplementation(async (options) => ({
sqlToExecute: options.sql,
pageSql: options.sql,
pageLimit: options.pagination.limit,
pageOffset: options.pagination.offset,
countSql: undefined,
useAgentResultSession: false,
paginationRowNumberColumn: options.pagination.offset > 0 ? "__dbx_row_num" : undefined,
}));
executeMulti.mockResolvedValueOnce([{ columns: ["N"], rows: [[1], [2]], affected_rows: 0, execution_time_ms: 1 }]).mockResolvedValueOnce([
{
columns: ["N", "__dbx_row_num"],
rows: [
[3, 3],
[4, 4],
],
affected_rows: 0,
execution_time_ms: 1,
},
]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("ob-1", "app", "Query");
await store.executeTabSql(tabId, sql, { pagination: { limit: 2, offset: 0 } });
await store.executeTabSql(tabId, sql, {
pagination: { limit: 2, offset: 2 },
preserveResultDuringExecution: true,
appendResult: { maxRows: 10 },
});
const tab = store.tabs.find((item) => item.id === tabId)!;
expect(tab.result?.columns).toEqual(["N"]);
expect(tab.result?.rows).toEqual([[1], [2], [3], [4]]);
prepareQueryPaginationExecutionPlan.mockImplementation(async (options) => ({
sqlToExecute: options.sql,
pageSql: options.sql,
pageLimit: 2,
pageOffset: options.pagination.offset,
countSql: undefined,
useAgentResultSession: false,
paginationRowNumberColumn: options.pagination.offset > 0 ? "__dbx_row_num" : undefined,
}));
executeMulti
.mockResolvedValueOnce([{ columns: ["N"], rows: [[1], [2]], affected_rows: 0, execution_time_ms: 1 }])
.mockResolvedValueOnce([
{
columns: ["N", "__dbx_row_num"],
rows: [
[3, 3],
[4, 4],
],
affected_rows: 0,
execution_time_ms: 1,
},
])
.mockResolvedValueOnce([{ columns: ["N", "__dbx_row_num"], rows: [], affected_rows: 0, execution_time_ms: 1 }]);
const exported = await store.fetchTabResultForExport(tabId);
expect(exported?.columns).toEqual(["N"]);
expect(exported?.rows).toEqual([[1], [2], [3], [4]]);
});
it("records the returned row count when a page is known to be incomplete without count sql", async () => {
prepareQueryPaginationExecutionPlan.mockResolvedValue({
sqlToExecute: "SELECT name FROM users LIMIT 100 OFFSET 0",
+8 -2
View File
@@ -59,6 +59,7 @@ import { connectionObjectTreeNodeSchema, connectionQueryExecutionSchema, connect
import { frontendQueryTimeoutDelayMs, frontendQueryTimeoutSecsForSql, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
import { queryResultNameFromPreamble, queryResultSourceLabel } from "@/lib/sql/queryResultSource";
import { sqlServerCountUsesLocalTempTable } from "@/lib/query/queryResultCountSession";
import { stripPaginationRowNumber } from "@/lib/query/queryPaginationResult";
import { beginDataGridNativeSelectionBlock, finishDataGridNativeSelectionBlock } from "@/lib/dataGrid/dataGridNativeSelection";
import { appendLargeValueCells, canUseTableDataLargeValuePreview, remapLargeValueCells, tableDataLargeValuePreviewOptions, TABLE_DATA_RESULT_MAX_BYTES } from "@/lib/dataGrid/dataGridLargeValues";
import { simpleDataGridOrderByReferencesMissingColumn, sortDataGridRowIndexes, type DataGridSortDirection } from "@/lib/dataGrid/dataGridSort";
@@ -6310,6 +6311,7 @@ export const useQueryStore = defineStore("query", () => {
let countSql: string | undefined;
let exactQueryRowBound: number | undefined;
let useAgentResultSession = false;
let paginationRowNumberColumn: string | undefined;
let executionDispatched = false;
let producedResult = false;
const resumedExecutionTarget = batchResume?.batch.executionTarget;
@@ -7054,6 +7056,7 @@ export const useQueryStore = defineStore("query", () => {
countSql = sqlServerUseScript && plan.countSql ? replaceSqlServerLeadingUseQuery(queryBaseSql, sqlServerUseScript, plan.countSql) : plan.countSql;
exactQueryRowBound = plan.exactQueryRowBound;
useAgentResultSession = plan.useAgentResultSession;
paginationRowNumberColumn = plan.paginationRowNumberColumn;
}
const hasBoundedPagination = typeof pageLimit === "number" && typeof pageOffset === "number";
if (options?.appendResult && !hasBoundedPagination && !useAgentResultSession) {
@@ -7269,6 +7272,9 @@ export const useQueryStore = defineStore("query", () => {
options?.sourceOffset === undefined ? undefined : tab.sql,
);
const results = offsetBatchQueryResultIndexes(annotatedResults.results, batchResume?.startStatementIndex ?? 0);
if (paginationRowNumberColumn && results.length === 1) {
results[0] = markQueryResultsRowsRaw([stripPaginationRowNumber(results[0]!, paginationRowNumberColumn)])[0]!;
}
reconcileBatchSqlResults(tab, executionId, results);
// Sticky proven-read-only aggregation (Oracle/OceanBase-Oracle/MySQL/PG).
// Only the initial manual execution participates: a later cursor-page
@@ -8563,8 +8569,8 @@ export const useQueryStore = defineStore("query", () => {
}
: { maxRows: plan.pageLimit, fetchSize: plan.pageLimit, clientSessionId, catalog: location.catalog, timeoutSecs: queryTimeoutSecs };
const results = await api.executeMulti(location.connectionId, executionDatabase, plan.sqlToExecute, location.schema, exportExecutionId, executionOptions);
const result = results[0];
if (!result) break;
if (!results[0]) break;
const result = stripPaginationRowNumber(results[0], plan.paginationRowNumberColumn);
if (columns.length === 0) columns = result.columns;
rows.push(...result.rows);
executionTimeMs += result.execution_time_ms ?? 0;