fix(oracle): defer XMLTYPE in manual transactions
This commit is contained in:
8 files changed
+209
-17
No files matched your search
@@ -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.");
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user