fix(oceanbase-oracle): bound first query result page
This commit is contained in:
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);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user