fix(oracle): load CLOB values lazily and safely
This commit is contained in:
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" }]],
|
||||
[
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user