perf(oracle): stop slow index metadata from blocking first query
This commit is contained in:
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 });
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user