fix(oracle): defer XMLTYPE in manual transactions

This commit is contained in:
zipg authored and GitHub committed 2026-08-21 19:44:13 +08:00
1 parent e50bd26141
commit 47e02b8da7
8 files changed
+209 -17

No files matched your search

+1 -1
View File
@@ -1216,7 +1216,7 @@ export async function beginManualTransaction(_connectionId: string, _database: s
throw new Error("Manual transaction management is only available in the desktop app.");
}
export async function executeInManualTransaction(_txnSessionId: string, _sql: string, _database: string, _schema?: string, _maxRows?: number): Promise<QueryResult[]> {
export async function executeInManualTransaction(_txnSessionId: string, _sql: string, _database: string, _schema?: string, _maxRows?: number, _tableDataPreview?: boolean): Promise<QueryResult[]> {
throw new Error("Manual transaction management is only available in the desktop app.");
}
+2 -1
View File
@@ -1390,13 +1390,14 @@ export async function beginManualTransaction(connectionId: string, database: str
return invoke("begin_manual_transaction", { connectionId, database, schema, catalog });
}
export async function executeInManualTransaction(txnSessionId: string, sql: string, database: string, schema?: string, maxRows?: number): Promise<QueryResult[]> {
export async function executeInManualTransaction(txnSessionId: string, sql: string, database: string, schema?: string, maxRows?: number, tableDataPreview?: boolean): Promise<QueryResult[]> {
return invoke("execute_in_manual_transaction", {
txnSessionId,
sql,
database,
schema,
maxRows,
tableDataPreview,
});
}
@@ -3,6 +3,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const executeMulti = vi.fn();
const executeQuery = vi.fn();
const beginManualTransaction = vi.fn();
const executeInManualTransaction = vi.fn();
const cancelQuery = vi.fn();
const analyzeEditableQueryEditability = vi.fn();
const getColumns = vi.fn();
const listIndexes = vi.fn();
@@ -38,6 +41,9 @@ vi.mock("@/lib/backend/api", () => ({
buildSortedQuerySql,
closeClientConnectionSession: vi.fn().mockResolvedValue(undefined),
closeQuerySession: vi.fn().mockResolvedValue(undefined),
beginManualTransaction,
cancelQuery,
executeInManualTransaction,
executeMulti,
executeQuery,
getColumns,
@@ -110,6 +116,7 @@ describe("queryStore hidden primary key editing", () => {
affected_rows: 0,
execution_time_ms: 1,
});
cancelQuery.mockResolvedValue(false);
executeMulti.mockResolvedValue([
{
columns: ["name", "__DBX_PK_0"],
@@ -118,6 +125,15 @@ describe("queryStore hidden primary key editing", () => {
execution_time_ms: 1,
},
]);
beginManualTransaction.mockResolvedValue("txn-1");
executeInManualTransaction.mockResolvedValue([
{
columns: ["name", "__DBX_PK_0"],
rows: [["Alice", 7]],
affected_rows: 0,
execution_time_ms: 1,
},
]);
});
afterEach(() => {
@@ -580,6 +596,7 @@ describe("queryStore hidden primary key editing", () => {
const execution = store.executeTabSql(tabId, "SELECT t.* FROM APP.WIDE_TABLE t");
await vi.waitFor(() => expect(executeMulti).toHaveBeenCalled());
expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ORCL", "SELECT t.* FROM APP.WIDE_TABLE t", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
expect(executeMulti.mock.calls[0]?.[5]).not.toHaveProperty("tableDataPreview");
columnsGate.resolve([
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
@@ -588,6 +605,104 @@ describe("queryStore hidden primary key editing", () => {
await execution;
});
it("waits for first-run Oracle XMLTYPE metadata before manual transaction execution", async () => {
const columnsGate = deferred<Awaited<ReturnType<typeof getColumns>>>();
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
getColumns.mockReturnValue(columnsGate.promise);
listIndexes.mockResolvedValue([{ name: "PK_WIDE_TABLE", columns: ["ID"], is_unique: true, is_primary: true }]);
lookupLocalCompletionTables.mockReturnValue([{ name: "WIDE_TABLE", type: "table", schema: "APP" }]);
analyzeEditableQueryEditability.mockImplementation(async () => ({
editable: true,
analysis: {
schema: "APP",
tableName: "WIDE_TABLE",
tableAlias: "t",
selectStar: true,
columns: [],
},
}));
executeInManualTransaction.mockResolvedValue([
{
columns: ["ID", "PAYLOAD"],
rows: [[1, "<XMLTYPE>"]],
affected_rows: 0,
execution_time_ms: 1,
large_value_cells: [{ row_index: 0, column_index: 1, original_bytes: 81920 }],
},
]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
const execution = store.executeTabSql(tabId, "SELECT t.* FROM APP.WIDE_TABLE t");
await vi.waitFor(() => expect(listIndexes).toHaveBeenCalled());
expect(executeInManualTransaction).not.toHaveBeenCalled();
columnsGate.resolve([
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "PAYLOAD", data_type: "SYS.XMLTYPE", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
await execution;
expect(beginManualTransaction).toHaveBeenCalledWith("oracle-1", "ORCL", undefined, undefined);
expect(executeInManualTransaction).toHaveBeenCalledWith("txn-1", "SELECT t.* FROM APP.WIDE_TABLE t", "ORCL", undefined, expect.any(Number), true);
expect(store.tabs.find((tab) => tab.id === tabId)?.result?.large_value_cells).toEqual([{ row_index: 0, column_index: 1, original_bytes: 81920 }]);
});
it("does not start an Oracle manual transaction after cancellation during metadata loading", async () => {
const columnsGate = deferred<Awaited<ReturnType<typeof getColumns>>>();
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
getColumns.mockReturnValue(columnsGate.promise);
listIndexes.mockResolvedValue([{ name: "PK_WIDE_TABLE", columns: ["ID"], is_unique: true, is_primary: true }]);
lookupLocalCompletionTables.mockReturnValue([{ name: "WIDE_TABLE", type: "table", schema: "APP" }]);
analyzeEditableQueryEditability.mockImplementation(async () => ({
editable: true,
analysis: {
schema: "APP",
tableName: "WIDE_TABLE",
tableAlias: "t",
selectStar: true,
columns: [],
},
}));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
const execution = store.executeTabSql(tabId, "SELECT t.* FROM APP.WIDE_TABLE t");
await vi.waitFor(() => expect(listIndexes).toHaveBeenCalled());
await expect(store.cancelTabExecution(tabId)).resolves.toBe(false);
columnsGate.resolve([
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "PAYLOAD", data_type: "SYS.XMLTYPE", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
await expect(execution).resolves.toBe(false);
expect(cancelQuery).toHaveBeenCalledWith(expect.any(String));
expect(beginManualTransaction).not.toHaveBeenCalled();
expect(executeInManualTransaction).not.toHaveBeenCalled();
expect(store.tabs.find((tab) => tab.id === tabId)).toMatchObject({
isExecuting: false,
isCancelling: false,
executionId: undefined,
});
expect(store.tabs.find((tab) => tab.id === tabId)?.txnSessionId).toBeUndefined();
});
it("keeps manual non-Oracle queries out of table-data preview mode", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("mysql-1", "app", "Query");
store.setAutoCommit(tabId, false);
await store.executeTabSql(tabId, "SELECT name FROM users");
expect(executeInManualTransaction).toHaveBeenCalledWith("txn-1", "SELECT name, `id` AS `__DBX_PK_0` FROM users", "app", undefined, expect.any(Number), false);
});
it("keeps a keyless Oracle query editable when its WHERE clause reads another table", async () => {
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
getColumns.mockResolvedValue([
@@ -686,11 +801,11 @@ describe("queryStore hidden primary key editing", () => {
expect(tab.result?.hidden_column_indexes).toBeUndefined();
});
it("enables deferred Oracle LOBs only when a base-table query has a stable key", async () => {
it.each(["CLOB", "XMLTYPE", "SYS.XMLTYPE"])("enables deferred Oracle %s values only when a base-table query has a stable key", async (dataType) => {
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
getColumns.mockResolvedValue([
{ name: "ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: true, extra: null },
{ name: "PAYLOAD", data_type: "CLOB", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
{ name: "PAYLOAD", data_type: dataType, is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
lookupLocalCompletionTables.mockReturnValue([{ name: "DOCUMENTS", type: "table", schema: "APP" }]);
analyzeEditableQueryEditability.mockImplementation(async (sql: string) => ({
+8 -3
View File
@@ -83,7 +83,7 @@ import type { DriverProfileWorkspaceScope } from "@/lib/database/driverProfileEx
import type { MultiDbExecutionTarget, MultiDbResultRunExecution } from "@/types/sqlExecution";
const ORACLE_LIKE_METADATA_TYPES = new Set<string>(["oracle", "dameng", "oceanbase-oracle"]);
const ORACLE_DEFERRED_LOB_TYPES = new Set<string>(["CLOB", "NCLOB", "BLOB", "BFILE"]);
const ORACLE_DEFERRED_LOB_TYPES = new Set<string>(["CLOB", "NCLOB", "BLOB", "BFILE", "XMLTYPE", "SYS.XMLTYPE"]);
// Bounded concurrency for grouped-query display column loads, scoped per
// connection so different connections never block each other. Matches the
@@ -3789,7 +3789,7 @@ export const useQueryStore = defineStore("query", () => {
});
void fullMetadataPromise.catch((error) => queryExecutionLog("warn", "metadata:table-prefetch:failed", { traceId, error, elapsed: elapsed() }));
const indexes = await loadTableIndexes(target.request);
if (primaryKeyIndex(indexes) && projectsAllColumnsForSource(target.analysis, target.source.key)) {
if (primaryKeyIndex(indexes) && projectsAllColumnsForSource(target.analysis, target.source.key) && tab.autoCommit !== false) {
return unchanged;
}
loaded = loadedEditableSourceFromMetadata(target, (await fullMetadataPromise).metadata);
@@ -4929,6 +4929,11 @@ export const useQueryStore = defineStore("query", () => {
const frontendTimeoutSecs = frontendQueryTimeoutSecsForSql(sqlToExecute, effectiveDbType, queryTimeoutSecs);
const sourceLabelDatabase = targetDatabase || conn?.database;
const executionClientSessionId = options?.pagination?.clientSessionId ?? (tab.mode === "query" || tab.mode === "data" ? tabClientSessionId(tab) : undefined);
const currentBeforeDispatch = findExecutionTab(id);
if (currentBeforeDispatch?.executionId !== executionId || currentBeforeDispatch.isCancelling) {
queryExecutionLog("info", "dispatch:skipped-cancelled", { traceId, elapsed: elapsed() });
return false;
}
let executionPromise: Promise<QueryResult[]>;
if (tab.autoCommit === false) {
@@ -4939,7 +4944,7 @@ export const useQueryStore = defineStore("query", () => {
}
queryExecutionLog("info", "execute-in-txn:invoke", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
executionDispatched = true;
executionPromise = api.executeInManualTransaction(tab.txnSessionId, sqlToExecute, executionDatabase, executionSchema, pageLimit ?? agentProtocolQueryResultMaxRows(queryResultMaxRows));
executionPromise = api.executeInManualTransaction(tab.txnSessionId, sqlToExecute, executionDatabase, executionSchema, pageLimit ?? agentProtocolQueryResultMaxRows(queryResultMaxRows), useOracleLobPreview);
} else {
queryExecutionLog("info", "execute-multi:start", { traceId, elapsed: elapsed() });
// Query and data tabs use a tab-scoped pool so repeated executions keep