perf(oracle): stop slow index metadata from blocking first query

This commit is contained in:
zipg authored and GitHub committed 2026-08-27 22:25:39 +08:00
1 parent b198dd1327
commit 71d07348ad
2 files changed
+92 -4

No files matched your search

@@ -157,7 +157,7 @@ describe("queryStore hidden primary key editing", () => {
expect(tab.queryAnalysis).toBeDefined();
expect(tab.queryAnalysis?.allowInsert).toBe(false);
expect(tab.queryEditabilityReason).toBeUndefined();
});
}, 10_000);
it("keeps MySQL expression columns read-only without disabling direct columns", async () => {
const sql = "SELECT id, status, extra->>'$.mode' mode, extra->>'$.template' tmpl FROM items";
@@ -801,6 +801,68 @@ describe("queryStore hidden primary key editing", () => {
await execution;
});
it("starts an Oracle star query when index metadata exceeds the preflight budget", async () => {
vi.useFakeTimers();
const columnsGate = deferred<Awaited<ReturnType<typeof getColumns>>>();
const indexesGate = deferred<Awaited<ReturnType<typeof listIndexes>>>();
try {
getConnectionConfig.mockReturnValue({ id: "oracle-1", name: "Oracle", db_type: "oracle", database: "ORCL", query_timeout_secs: 30 });
getColumns.mockReturnValue(columnsGate.promise);
listIndexes.mockReturnValue(indexesGate.promise);
analyzeEditableQueryEditability.mockImplementation(async () => ({
editable: true,
analysis: {
schema: "APP",
tableName: "SLOW_METADATA_TABLE",
selectStar: true,
columns: [],
},
}));
executeMulti.mockResolvedValue([
{
columns: ["ID", "NAME"],
rows: [[1, "Alice"]],
affected_rows: 0,
execution_time_ms: 12,
},
]);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
store.setAutoCommit(tabId, true);
const execution = store.executeTabSql(tabId, "SELECT * FROM APP.SLOW_METADATA_TABLE");
await vi.waitFor(() => expect(listIndexes).toHaveBeenCalledOnce());
expect(executeMulti).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1_000);
await vi.waitFor(() => expect(executeMulti).toHaveBeenCalledOnce());
expect(executeMulti).toHaveBeenCalledWith("oracle-1", "ORCL", "SELECT * FROM APP.SLOW_METADATA_TABLE", undefined, expect.any(String), expect.objectContaining({ timeoutSecs: 30 }));
await execution;
indexesGate.resolve([{ name: "PK_SLOW_METADATA_TABLE", columns: ["ID"], is_unique: true, is_primary: true }]);
columnsGate.resolve([
{ 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 },
]);
await vi.waitFor(() => expect(store.tabs.find((item) => item.id === tabId)?.tableMeta?.primaryKeys).toEqual(["ID"]));
await store.executeTabSql(tabId, "SELECT * FROM APP.SLOW_METADATA_TABLE");
expect(executeMulti).toHaveBeenCalledTimes(2);
expect(listIndexes).toHaveBeenCalledOnce();
expect(getColumns).toHaveBeenCalledOnce();
} finally {
indexesGate.resolve([{ name: "PK_SLOW_METADATA_TABLE", columns: ["ID"], is_unique: true, is_primary: true }]);
columnsGate.resolve([
{ 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 },
]);
await vi.runAllTimersAsync();
vi.useRealTimers();
}
});
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 });
+29 -3
View File
@@ -104,6 +104,8 @@ const QUERY_RESULT_EXPORT_UNSUPPORTED_ERROR = "Streaming export is unsupported f
const BACKGROUND_CLIENT_SESSION_SUFFIXES = ["count", "explain", "export"] as const;
const CANCEL_QUERY_TIMEOUT_MS = 10_000;
const CANCEL_ACK_SETTLE_TIMEOUT_MS = 2_000;
const ORACLE_QUERY_METADATA_PREFLIGHT_BUDGET_MS = 1_000;
const ORACLE_QUERY_METADATA_PREFLIGHT_TIMEOUT = Symbol("oracle-query-metadata-preflight-timeout");
const SAVED_SQL_EDITOR_POSITION_PERSIST_DELAY_MS = 500;
type CloseConfirmContext = "tab" | "batch" | "app";
@@ -529,6 +531,20 @@ async function withCancelQueryTimeout<T>(promise: Promise<T>): Promise<T> {
}
}
async function waitForOracleQueryMetadataPreflight<T>(promise: Promise<T>): Promise<T | typeof ORACLE_QUERY_METADATA_PREFLIGHT_TIMEOUT> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<typeof ORACLE_QUERY_METADATA_PREFLIGHT_TIMEOUT>((resolve) => {
timer = setTimeout(() => resolve(ORACLE_QUERY_METADATA_PREFLIGHT_TIMEOUT), ORACLE_QUERY_METADATA_PREFLIGHT_BUDGET_MS);
}),
]);
} finally {
if (timer) clearTimeout(timer);
}
}
function normalizeUppercaseFoldedMetadataIdentifier(dbType: string, identifier: string | undefined, quoted?: boolean) {
if (!identifier || quoted || !UPPERCASE_FOLDED_METADATA_TYPES.has(dbType)) return identifier;
return identifier.toUpperCase();
@@ -4051,9 +4067,19 @@ export const useQueryStore = defineStore("query", () => {
traceLogger: (event) => queryExecutionLog("debug", "metadata:table-trace", { sourceTraceId: traceId, ...event }),
});
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) && tab.autoCommit !== false) {
return unchanged;
const wholeSourceAutoCommit = projectsAllColumnsForSource(target.analysis, target.source.key) && tab.autoCommit !== false;
if (wholeSourceAutoCommit) {
const indexes = await waitForOracleQueryMetadataPreflight(loadTableIndexes(target.request));
if (indexes === ORACLE_QUERY_METADATA_PREFLIGHT_TIMEOUT) {
queryExecutionLog("info", "metadata:preflight:timeout", {
traceId,
table: target.request.tableName,
budgetMs: ORACLE_QUERY_METADATA_PREFLIGHT_BUDGET_MS,
elapsed: elapsed(),
});
return unchanged;
}
if (primaryKeyIndex(indexes)) return unchanged;
}
loaded = loadedEditableSourceFromMetadata(target, (await fullMetadataPromise).metadata);
}