fix(oracle): load CLOB values lazily and safely

This commit is contained in:
zipg authored and GitHub committed 2026-08-14 17:46:34 +08:00
1 parent 1cd962d9c7
commit 518a8b593c
6 files changed
+495 -62

No files matched your search

@@ -672,6 +672,119 @@ 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 () => {
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 },
]);
lookupLocalCompletionTables.mockReturnValue([{ name: "DOCUMENTS", type: "table", schema: "APP" }]);
analyzeEditableQueryEditability.mockImplementation(async (sql: string) => ({
editable: true,
analysis: {
schema: "APP",
tableName: "DOCUMENTS",
selectStar: false,
columns: [{ sourceName: "PAYLOAD", resultName: "PAYLOAD", expression: "PAYLOAD" }, ...(sql.includes("__DBX_PK_0") ? [{ sourceName: "ID", resultName: "__DBX_PK_0", expression: '"ID"' }] : [])],
},
}));
executeMulti.mockResolvedValue([{ columns: ["PAYLOAD", "__DBX_PK_0"], rows: [["<CLOB>", 1]], affected_rows: 0, execution_time_ms: 1 }]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
await store.executeTabSql(tabId, "SELECT PAYLOAD FROM APP.DOCUMENTS");
expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ORCL", 'SELECT PAYLOAD, "ID" AS "__DBX_PK_0" FROM APP.DOCUMENTS', undefined, expect.any(String), expect.objectContaining({ tableDataPreview: true, timeoutSecs: 30 }));
});
it("keeps deferred Oracle LOBs disabled for views", async () => {
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: false, extra: null },
{ name: "PAYLOAD", data_type: "CLOB", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
listIndexes.mockResolvedValue([]);
lookupLocalCompletionTables.mockReturnValue([{ name: "DOCUMENT_VIEW", type: "view", schema: "APP" }]);
analyzeEditableQueryEditability.mockResolvedValue({
editable: true,
analysis: {
schema: "APP",
tableName: "DOCUMENT_VIEW",
selectStar: true,
columns: [],
},
});
executeMulti.mockResolvedValue([{ columns: ["ID", "PAYLOAD"], rows: [[1, "value"]], affected_rows: 0, execution_time_ms: 1 }]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
await store.executeTabSql(tabId, "SELECT * FROM APP.DOCUMENT_VIEW");
expect(executeMulti).toHaveBeenCalledOnce();
expect(executeMulti.mock.calls[0]?.[5]).not.toHaveProperty("tableDataPreview");
});
it("keeps deferred Oracle LOBs disabled when a keyless source has no safe ROWID path", async () => {
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: false, extra: null },
{ name: "PAYLOAD", data_type: "CLOB", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
listIndexes.mockResolvedValue([]);
lookupLocalCompletionTables.mockReturnValue([]);
analyzeEditableQueryEditability.mockResolvedValue({
editable: true,
analysis: {
schema: undefined,
tableName: "DOCUMENTS",
selectStar: true,
columns: [],
},
});
executeMulti.mockResolvedValue([{ columns: ["ID", "PAYLOAD"], rows: [[1, "value"]], affected_rows: 0, execution_time_ms: 1 }]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
await store.executeTabSql(tabId, "SELECT * FROM DOCUMENTS");
expect(executeMulti).toHaveBeenCalledOnce();
expect(executeMulti.mock.calls[0]?.[5]).not.toHaveProperty("tableDataPreview");
});
it("does not request deferred Oracle LOB handling for ordinary non-LOB queries", async () => {
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: "NAME", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
]);
lookupLocalCompletionTables.mockReturnValue([{ name: "CUSTOMERS", type: "table", schema: "APP" }]);
analyzeEditableQueryEditability.mockImplementation(async (sql: string) => ({
editable: true,
analysis: {
schema: "APP",
tableName: "CUSTOMERS",
selectStar: false,
columns: [{ sourceName: "NAME", resultName: "NAME", expression: "NAME" }, ...(sql.includes("__DBX_PK_0") ? [{ sourceName: "ID", resultName: "__DBX_PK_0", expression: '"ID"' }] : [])],
},
}));
executeMulti.mockResolvedValue([{ columns: ["NAME", "__DBX_PK_0"], rows: [["Alice", 1]], affected_rows: 0, execution_time_ms: 1 }]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
await store.executeTabSql(tabId, "SELECT NAME FROM APP.CUSTOMERS");
expect(executeMulti).toHaveBeenCalledOnce();
expect(executeMulti.mock.calls[0]?.[5]).not.toHaveProperty("tableDataPreview");
});
it.each([
["only a foreign owner is cached", [{ name: "CUSTOMERS", type: "table", schema: "APP" }]],
[
+22 -5
View File
@@ -80,6 +80,7 @@ import type { SqlExecutionTargetContext } from "@/lib/database/sqlExecutionTarge
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 UPPERCASE_FOLDED_METADATA_TYPES = new Set<string>([...ORACLE_LIKE_METADATA_TYPES, "saphana"]);
const HIDDEN_QUERY_KEY_DATABASE_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlserver", "oracle"]);
const QUERY_RESULT_EXPORT_UNSUPPORTED_ERROR = "Streaming export is unsupported for this query. Simplify it or use a supported driver.";
@@ -530,6 +531,17 @@ function projectsAllColumnsForSource(analysis: EditableQueryInfo, sourceKey: str
return analysis.selectStar || analysis.columns.some((column) => column.star && (!column.sourceKey || column.sourceKey === sourceKey));
}
function oracleQueryProjectsDeferredLob(analysis: EditableQueryInfo, sourceKey: string, columns: readonly { name: string; data_type: string }[]): boolean {
const deferredColumns = new Set(columns.filter((column) => ORACLE_DEFERRED_LOB_TYPES.has(column.data_type.trim().toUpperCase())).map((column) => column.name.toLowerCase()));
if (deferredColumns.size === 0) return false;
if (projectsAllColumnsForSource(analysis, sourceKey)) return true;
return analysis.columns.some((column) => column.sourceName && column.sourceKey === sourceKey && deferredColumns.has(column.sourceName.toLowerCase()));
}
function oracleColumnsAllowDeferredLobMarkers(columns: readonly { name: string }[]): boolean {
return !columns.some((column) => column.name.toUpperCase().startsWith("__DBX_LARGE_VALUE_BYTES_"));
}
function cloneAnalysisForSource(analysis: EditableQueryInfo, source: EditableQuerySource): EditableQueryInfo {
return {
...analysis,
@@ -3395,6 +3407,7 @@ export const useQueryStore = defineStore("query", () => {
sql: string;
metadataSql: string;
hiddenPrimaryKeys: HiddenPrimaryKeyProjection[];
oracleLobPreview: boolean;
}
function applyQueryMetadataPatch(tab: QueryTab, patch: QueryMetadataPatch) {
@@ -3523,9 +3536,10 @@ export const useQueryStore = defineStore("query", () => {
return indexes.find((index) => !index.filter && index.columns.length > 0 && index.is_primary);
}
function buildHiddenPrimaryKeyPreparation(sql: string, databaseType: DatabaseType, loaded: LoadedEditableSource, primaryKeys: string[], declaredPrimaryKeys: string[], traceId: string, elapsed: () => string): EditableQueryExecutionPreparation {
const unchanged = { sql, metadataSql: sql, hiddenPrimaryKeys: [] };
function buildHiddenPrimaryKeyPreparation(tab: QueryTab, sql: string, databaseType: DatabaseType, loaded: LoadedEditableSource, primaryKeys: string[], declaredPrimaryKeys: string[], traceId: string, elapsed: () => string): EditableQueryExecutionPreparation {
const metadataAnalysis = expandStarProjectionColumnsForSource(bindColumnsForSource(databaseType, loaded.analysis, loaded.source, loaded.tableMeta.columns), loaded.source, loaded.tableMeta.columns);
const oracleLobPreview = databaseType === "oracle" && primaryKeys.length > 0 && oracleRowIdIsSafeForQuery(tab, loaded) && oracleColumnsAllowDeferredLobMarkers(loaded.tableMeta.columns) && oracleQueryProjectsDeferredLob(metadataAnalysis, loaded.source.key, loaded.tableMeta.columns);
const unchanged = { sql, metadataSql: sql, hiddenPrimaryKeys: [], oracleLobPreview };
const missingPrimaryKeys = declaredPrimaryKeys.length === 0 ? primaryKeys : missingPrimaryKeysForSource(primaryKeys, metadataAnalysis, loaded.source.key);
if (missingPrimaryKeys.length === 0) return unchanged;
const primaryKeySet = new Set(primaryKeys);
@@ -3546,11 +3560,11 @@ export const useQueryStore = defineStore("query", () => {
keyCount: rewritten.projections.length,
elapsed: elapsed(),
});
return { sql: rewritten.sql, metadataSql: rewritten.sql, hiddenPrimaryKeys: rewritten.projections };
return { sql: rewritten.sql, metadataSql: rewritten.sql, hiddenPrimaryKeys: rewritten.projections, oracleLobPreview };
}
async function prepareEditableQueryExecution(tab: QueryTab, sql: string, conn: ConnectionConfig | undefined, databaseType: DatabaseType | undefined, executionDatabase: string, traceId: string, elapsed: () => string): Promise<EditableQueryExecutionPreparation> {
const unchanged = { sql, metadataSql: sql, hiddenPrimaryKeys: [] };
const unchanged = { sql, metadataSql: sql, hiddenPrimaryKeys: [], oracleLobPreview: false };
if (!databaseType || !HIDDEN_QUERY_KEY_DATABASE_TYPES.has(databaseType) || !tab.connectionId) return unchanged;
try {
@@ -3591,7 +3605,7 @@ export const useQueryStore = defineStore("query", () => {
// ROWID from a view can fail with ORA-01445.
if (databaseType === "oracle" && declaredPrimaryKeys.length === 0 && !oracleRowIdIsSafeForQuery(tab, loaded)) return unchanged;
const primaryKeys = editablePrimaryKeys(databaseType, loaded.tableMeta.columns, loaded.tableMeta.tableType);
return buildHiddenPrimaryKeyPreparation(sql, databaseType, loaded, primaryKeys, declaredPrimaryKeys, traceId, elapsed);
return buildHiddenPrimaryKeyPreparation(tab, sql, databaseType, loaded, primaryKeys, declaredPrimaryKeys, traceId, elapsed);
} catch (error) {
// Metadata enrichment is optional. Query execution must retain its prior
// behavior when metadata is unavailable or the SQL cannot be rewritten.
@@ -3958,6 +3972,7 @@ export const useQueryStore = defineStore("query", () => {
let resultSortedSql = options?.resultSortedSql;
let queryMetadataSql = queryBaseSql;
let hiddenPrimaryKeys: HiddenPrimaryKeyProjection[] = [];
let useOracleLobPreview = false;
let pageSql: string | undefined;
let pageLimit: number | undefined;
let pageOffset: number | undefined;
@@ -4528,6 +4543,7 @@ export const useQueryStore = defineStore("query", () => {
// does not turn an otherwise editable result into a complex read-only one.
queryMetadataSql = options?.resultSortedSql && !options?.querySort ? queryBaseSql : prepared.metadataSql;
hiddenPrimaryKeys = prepared.hiddenPrimaryKeys;
useOracleLobPreview = prepared.oracleLobPreview;
if (options?.querySort) {
const sorted = await api.buildSortedQuerySql({
originalSql: sqlToExecute,
@@ -4627,6 +4643,7 @@ export const useQueryStore = defineStore("query", () => {
tableDataPreview: useTableDataPreview,
}
: {}),
...(useOracleLobPreview ? { tableDataPreview: true } : {}),
timeoutSecs: queryTimeoutSecs,
catalog: executionCatalog,
continueOnError: settingsStore.editorSettings.continueOnErrorOnBatch,