fix(grid): mask loading only on executing result run
This commit is contained in:
1 parent
1aa9f06719
commit
f19439ad7a
9 files changed
+115
-8
No files matched your search
@@ -124,7 +124,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { TABLE_FONT_SIZE_MAX, TABLE_FONT_SIZE_MIN, useSettingsStore, type DataGridSearchMode, type ResultRunDisplayMode } from "@/stores/settingsStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { canCancelQueryExecution, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState";
|
||||
import { canCancelQueryExecution, isActiveResultLoading, queryExecutionLabelKey } from "@/lib/sql/queryExecutionState";
|
||||
import {
|
||||
databaseDisplayNameForTab,
|
||||
executionSummaryItems,
|
||||
@@ -491,6 +491,7 @@ const resultArchiveExporting = ref(false);
|
||||
const canExportResultArchive = computed(() => props.activeTab.mode === "query" && (!!props.activeTab.result || !!props.activeTab.results?.length || !!props.activeTab.resultRuns?.length));
|
||||
const resultAutoSave = computed(() => props.activeTab.resultAutoSave === true);
|
||||
const activeResultRunItem = computed(() => resultRuns.value.find((run) => run.active));
|
||||
const activeResultIsLoading = computed(() => isActiveResultLoading(props.activeTab));
|
||||
const showResultRunTabs = computed(() => resultRuns.value.length > 0 && resultRunDisplayMode.value === "tabs");
|
||||
const showResultRunSelector = computed(() => resultRuns.value.length > 0 && resultRunDisplayMode.value === "list");
|
||||
const canCloseQueryResult = computed(() => props.activeTab.mode === "query" && !props.activeTab.isExecuting && !props.activeTab.activeResultRunId && (!!props.activeTab.result || !!props.activeTab.results?.length || props.activeTab.resultEvicted === true));
|
||||
@@ -1784,7 +1785,7 @@ defineExpose({
|
||||
:initial-order-by-input="activeTab.orderByInput"
|
||||
:sql="activeResultSql"
|
||||
:export-sql="activeResultExportSql"
|
||||
:loading="activeTab.isExecuting"
|
||||
:loading="activeResultIsLoading"
|
||||
:editable="!!activeTab.queryAnalysis || !!mongoQueryResultSaveHandler"
|
||||
:source-columns="activeTab.querySourceColumns"
|
||||
:readonly-column-indexes="groupedQueryReadonlyColumnIndexes(activeTab)"
|
||||
|
||||
@@ -51,6 +51,19 @@ describe("openTabsPersistence originalSql round-trip", () => {
|
||||
expect(restored.originalSql).toBe("");
|
||||
});
|
||||
|
||||
it("does not persist the transient result execution target", () => {
|
||||
const tab = queryTab({ activeResultRunId: "run-1", executingResultRunId: "run-1", isExecuting: true });
|
||||
const [saved] = serializeOpenTabs([tab]);
|
||||
const [restored] = restoreOpenTabsPayload({
|
||||
tabs: [{ ...saved, executingResultRunId: "run-1" }],
|
||||
activeTabId: tab.id,
|
||||
}).tabs;
|
||||
|
||||
expect(saved).not.toHaveProperty("executingResultRunId");
|
||||
expect(restored.executingResultRunId).toBeUndefined();
|
||||
expect(restored.isExecuting).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves an external Doris catalog across tab restore", () => {
|
||||
const [restored] = roundTrip([queryTab({ database: "dbx_catalog_completion", catalog: "dbx_mysql_catalog" })]);
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isActiveResultLoading, type QueryResultLoadingStateLike } from "@/lib/sql/queryExecutionState";
|
||||
|
||||
function state(overrides: Partial<QueryResultLoadingStateLike> = {}): QueryResultLoadingStateLike {
|
||||
return {
|
||||
isExecuting: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("active query result loading", () => {
|
||||
it("keeps the single-result execution loading", () => {
|
||||
expect(isActiveResultLoading(state({ isExecuting: true, executingResultRunId: null }))).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the result run being updated loading", () => {
|
||||
expect(isActiveResultLoading(state({ isExecuting: true, activeResultRunId: "run-b", executingResultRunId: "run-b" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not load a completed result while a new run is executing", () => {
|
||||
expect(isActiveResultLoading(state({ isExecuting: true, activeResultRunId: "run-a", executingResultRunId: null }))).toBe(false);
|
||||
});
|
||||
|
||||
it("loads again when the user switches back to the executing run", () => {
|
||||
expect(isActiveResultLoading(state({ isExecuting: true, activeResultRunId: "run-b", executingResultRunId: "run-b" }))).toBe(true);
|
||||
});
|
||||
|
||||
it("clears loading after execution finishes", () => {
|
||||
expect(isActiveResultLoading(state({ isExecuting: false, activeResultRunId: "run-b", executingResultRunId: "run-b" }))).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps legacy unscoped executions conservative", () => {
|
||||
expect(isActiveResultLoading(state({ isExecuting: true, activeResultRunId: "run-a" }))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -190,6 +190,7 @@ function restoreOpenTabsArray(parsed: unknown, rawActiveTabId: string | null, op
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
queryExecutionStartedAt: undefined,
|
||||
executingResultRunId: undefined,
|
||||
editorViewport: undefined,
|
||||
editorSelection: undefined,
|
||||
isExplaining: false,
|
||||
|
||||
@@ -8,6 +8,21 @@ export function canCancelQueryExecution(state: QueryExecutionStateLike): boolean
|
||||
return state.isExecuting && !!state.executionId && !state.isCancelling;
|
||||
}
|
||||
|
||||
export interface QueryResultLoadingStateLike extends QueryExecutionStateLike {
|
||||
activeResultRunId?: string;
|
||||
/** The result run being updated; null means the execution will create a new run. */
|
||||
executingResultRunId?: string | null;
|
||||
}
|
||||
|
||||
export function isActiveResultLoading(state: QueryResultLoadingStateLike): boolean {
|
||||
if (!state.isExecuting) return false;
|
||||
if (!state.activeResultRunId) return true;
|
||||
// Keep unscoped legacy callers conservative while all new executions set an
|
||||
// explicit run id (or null when they are producing a new result run).
|
||||
if (state.executingResultRunId === undefined) return true;
|
||||
return state.executingResultRunId === state.activeResultRunId;
|
||||
}
|
||||
|
||||
export function queryExecutionLabelKey(state: Pick<QueryExecutionStateLike, "isCancelling">): "common.loading" | "common.stopping" {
|
||||
return state.isCancelling ? "common.stopping" : "common.loading";
|
||||
}
|
||||
@@ -45,6 +45,7 @@ describe("queryStore cancel timeout recovery", () => {
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
executionId: undefined,
|
||||
executingResultRunId: undefined,
|
||||
});
|
||||
expect(store.tabs[0]?.result?.rows[0]?.[0]).toContain("Cancel request timed out");
|
||||
}, 15_000);
|
||||
@@ -84,6 +85,7 @@ describe("queryStore cancel timeout recovery", () => {
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
executionId: undefined,
|
||||
executingResultRunId: undefined,
|
||||
batchSqlExecution: {
|
||||
finishedAt: expect.any(Number),
|
||||
items: [{ status: "cancelled" }, { status: "skipped" }],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { isActiveResultLoading } from "@/lib/sql/queryExecutionState";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -978,8 +979,7 @@ describe("queryStore multi-statement errors", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "app", "Query");
|
||||
await store.executeCurrentSql("SELECT 1 AS value");
|
||||
store.toggleResultAutoSave(tabId);
|
||||
await store.executeCurrentSql("SELECT 1 AS value", { openInNewResultTab: true });
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
const retainedRunId = tab.activeResultRunId!;
|
||||
const retainedRunCacheKey = tab.resultRuns?.find((run) => run.id === retainedRunId)?.resultCacheKey;
|
||||
@@ -987,11 +987,16 @@ describe("queryStore multi-statement errors", () => {
|
||||
|
||||
const execution = store.executeCurrentSql("SELECT 2 AS value", { openInNewResultTab: true });
|
||||
await vi.waitFor(() => expect(mocks.executeMulti).toHaveBeenCalledTimes(2));
|
||||
expect(tab.executingResultRunId).toBeNull();
|
||||
expect(isActiveResultLoading(tab)).toBe(true);
|
||||
expect(await store.setActiveResultRun(tabId, retainedRunId)).toBe(true);
|
||||
expect(tab.batchSqlExecution?.submittedSql).toBe("SELECT 1 AS value");
|
||||
expect(isActiveResultLoading(tab)).toBe(false);
|
||||
pendingExecution.resolve([{ columns: ["value"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 }]);
|
||||
await execution;
|
||||
|
||||
expect(tab.executingResultRunId).toBeUndefined();
|
||||
expect(isActiveResultLoading(tab)).toBe(false);
|
||||
expect(tab.resultRuns).toHaveLength(2);
|
||||
expect(tab.activeResultRunId).not.toBe(retainedRunId);
|
||||
expect(tab.result).toMatchObject({ rows: [[2]] });
|
||||
@@ -1016,6 +1021,28 @@ describe("queryStore multi-statement errors", () => {
|
||||
expect(tab.batchSqlExecution?.submittedSql).toBe("SELECT 1 AS value");
|
||||
});
|
||||
|
||||
it("keeps loading scoped to the result run being rerun", async () => {
|
||||
const pendingExecution = deferred<Array<{ columns: string[]; rows: number[][]; affected_rows: number; execution_time_ms: number }>>();
|
||||
mocks.executeMulti.mockResolvedValueOnce([{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]).mockImplementationOnce(() => pendingExecution.promise);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "app", "Query");
|
||||
await store.executeCurrentSql("SELECT 1 AS value", { openInNewResultTab: true });
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
const executingRunId = tab.activeResultRunId!;
|
||||
|
||||
const execution = store.executeCurrentSql("SELECT 2 AS value");
|
||||
await vi.waitFor(() => expect(mocks.executeMulti).toHaveBeenCalledTimes(2));
|
||||
|
||||
expect(tab.executingResultRunId).toBe(executingRunId);
|
||||
expect(isActiveResultLoading(tab)).toBe(true);
|
||||
pendingExecution.resolve([{ columns: ["value"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 }]);
|
||||
await execution;
|
||||
|
||||
expect(tab.executingResultRunId).toBeUndefined();
|
||||
expect(isActiveResultLoading(tab)).toBe(false);
|
||||
});
|
||||
|
||||
it("restores the retained result when a new-result execution returns no result", async () => {
|
||||
mocks.executeMulti.mockResolvedValueOnce([{ columns: ["value"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 }]).mockResolvedValueOnce([]);
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
|
||||
@@ -861,6 +861,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
executionId: undefined,
|
||||
executingResultRunId: undefined,
|
||||
queryExecutionStartedAt: undefined,
|
||||
batchSqlExecution: undefined,
|
||||
cancelRequestCount: 0,
|
||||
@@ -1903,6 +1904,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.isExecuting = false;
|
||||
tab.isCancelling = false;
|
||||
tab.executionId = undefined;
|
||||
tab.executingResultRunId = undefined;
|
||||
tab.queryExecutionStartedAt = undefined;
|
||||
if (tab.result) touchResult(tab);
|
||||
return id;
|
||||
@@ -3003,6 +3005,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
editorViewport: undefined,
|
||||
editorSelection: undefined,
|
||||
executionId: undefined,
|
||||
executingResultRunId: undefined,
|
||||
isExplaining: false,
|
||||
explainExecutionId: undefined,
|
||||
mode: original.mode,
|
||||
@@ -3687,6 +3690,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab) return;
|
||||
tab.isExecuting = isExecuting;
|
||||
tab.executingResultRunId = isExecuting ? null : undefined;
|
||||
tab.queryExecutionStartedAt = isExecuting ? Date.now() : undefined;
|
||||
if (!isExecuting) {
|
||||
tab.isCancelling = false;
|
||||
@@ -3698,6 +3702,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab) return;
|
||||
tab.isExecuting = true;
|
||||
tab.executingResultRunId = null;
|
||||
tab.executionId = executionId;
|
||||
tab.isCancelling = false;
|
||||
tab.queryExecutionStartedAt = Date.now();
|
||||
@@ -3743,6 +3748,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.isCancelling = false;
|
||||
tab.queryExecutionStartedAt = undefined;
|
||||
tab.executionId = undefined;
|
||||
tab.executingResultRunId = undefined;
|
||||
touchResult(tab);
|
||||
}
|
||||
|
||||
@@ -3757,6 +3763,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
current.isExecuting = false;
|
||||
current.isCancelling = false;
|
||||
current.executionId = undefined;
|
||||
current.executingResultRunId = undefined;
|
||||
current.queryExecutionStartedAt = undefined;
|
||||
if (!restorePendingResultRun(current, executionId)) {
|
||||
current.result = toErrorResult(new Error("Query canceled"));
|
||||
@@ -4617,7 +4624,12 @@ export const useQueryStore = defineStore("query", () => {
|
||||
const traceId = executionId.slice(0, 8);
|
||||
const startedAt = performance.now();
|
||||
const elapsed = () => `${Math.round(performance.now() - startedAt)}ms`;
|
||||
const batchResume = options?.batchResume;
|
||||
const continueOnBatchError = batchResume?.continueOnError ?? settingsStore.editorSettings.continueOnErrorOnBatch;
|
||||
const preserveResultDuringExecution = batchResume !== undefined || options?.preserveResultDuringExecution === true || captureAutoSavedResultRun || (tab.mode === "query" && !!tab.activeResultRunId && !tab.resultAutoSave && !captureResultRun);
|
||||
const updateActiveResultRun = !!tab.activeResultRunId && preserveResultDuringExecution;
|
||||
tab.isExecuting = true;
|
||||
tab.executingResultRunId = !captureResultRun && updateActiveResultRun ? tab.activeResultRunId : null;
|
||||
options?.onExecutionStarted?.();
|
||||
tab.isCancelling = false;
|
||||
if (!tab.queryExecutionStartedAt) {
|
||||
@@ -4638,8 +4650,6 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (captureResultRun && tab.activeResultRunId) {
|
||||
pendingResultRunRestores.set(executionId, tab.activeResultRunId);
|
||||
}
|
||||
const batchResume = options?.batchResume;
|
||||
const continueOnBatchError = batchResume?.continueOnError ?? settingsStore.editorSettings.continueOnErrorOnBatch;
|
||||
if (batchResume) {
|
||||
tab.batchSqlExecution = prepareBatchSqlRecovery(batchResume.batch, executionId, batchResume.startStatementIndex);
|
||||
liveBatchSqlExecutions.set(tab, tab.batchSqlExecution);
|
||||
@@ -4647,8 +4657,6 @@ export const useQueryStore = defineStore("query", () => {
|
||||
tab.batchSqlExecution = undefined;
|
||||
liveBatchSqlExecutions.delete(tab);
|
||||
}
|
||||
const preserveResultDuringExecution = batchResume !== undefined || options?.preserveResultDuringExecution === true || captureAutoSavedResultRun || (tab.mode === "query" && !!tab.activeResultRunId && !tab.resultAutoSave && !captureResultRun);
|
||||
const updateActiveResultRun = !!tab.activeResultRunId && preserveResultDuringExecution;
|
||||
if (!updateActiveResultRun) {
|
||||
tab.activeResultRunId = undefined;
|
||||
}
|
||||
@@ -5754,6 +5762,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
current.isCancelling = false;
|
||||
current.queryExecutionStartedAt = undefined;
|
||||
current.executionId = undefined;
|
||||
current.executingResultRunId = undefined;
|
||||
clearLiveBatchSqlExecution(current, executionId);
|
||||
queryExecutionLog("info", "finish", { traceId, elapsed: elapsed() });
|
||||
} else {
|
||||
@@ -6165,6 +6174,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
current.isExecuting = false;
|
||||
current.isCancelling = false;
|
||||
current.executionId = undefined;
|
||||
current.executingResultRunId = undefined;
|
||||
current.queryExecutionStartedAt = undefined;
|
||||
clearLiveBatchSqlExecution(current, executionId);
|
||||
}
|
||||
@@ -6182,6 +6192,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
current.isCancelling = false;
|
||||
current.queryExecutionStartedAt = undefined;
|
||||
current.executionId = undefined;
|
||||
current.executingResultRunId = undefined;
|
||||
} else {
|
||||
// 复用 setErrorResult 的完整清理:分组结果不清空的话,错误结果不会展示,
|
||||
// 估算值也会继续按旧的 results 计算
|
||||
|
||||
@@ -1199,6 +1199,8 @@ export interface QueryTab {
|
||||
head: number;
|
||||
};
|
||||
executionId?: string;
|
||||
/** Ephemeral result run targeted by the current execution; null means a new run is being produced. */
|
||||
executingResultRunId?: string | null;
|
||||
isExplaining?: boolean;
|
||||
explainExecutionId?: string;
|
||||
/** Per-run connection session for explain flows that require session state. */
|
||||
|
||||
Reference in new issue
Block a user