feat(query): show live statement execution progress

This commit is contained in:
greathaoqi authored and GitHub committed 2026-07-30 00:44:10 +08:00
1 parent 1774126f76
commit ed018dbc77
27 files changed
+974 -85

No files matched your search

@@ -58,6 +58,19 @@ describe("queryStore cancel timeout recovery", () => {
const store = useQueryStore();
const tabId = store.createTab("duckdb-1", "main", "query_1");
store.setExecutingWithId(tabId, "exec-1");
store.tabs[0]!.batchSqlExecution = {
executionId: "exec-1",
submittedSql: "SELECT 1; SELECT 2",
editorFingerprint: "fingerprint",
sourceOffset: 0,
completed: 0,
total: 2,
startedAt: Date.now(),
items: [
{ statementIndex: 0, sql: "SELECT 1", from: 0, to: 8, status: "running" },
{ statementIndex: 1, sql: "SELECT 2", from: 10, to: 18, status: "pending" },
],
};
const cancel = store.cancelTabExecution(tabId);
await vi.advanceTimersByTimeAsync(1);
@@ -71,6 +84,10 @@ describe("queryStore cancel timeout recovery", () => {
isExecuting: false,
isCancelling: false,
executionId: undefined,
batchSqlExecution: {
finishedAt: expect.any(Number),
items: [{ status: "cancelled" }, { status: "skipped" }],
},
});
expect(store.tabs[0]?.result?.rows[0]?.[0]).toContain("Query canceled");
}, 15_000);
@@ -8,6 +8,7 @@ const mocks = vi.hoisted(() => ({
closeQuerySession: vi.fn(),
ensureConnected: vi.fn(),
executeMulti: vi.fn(),
executeMultiWithProgress: vi.fn(),
executeQuery: vi.fn(),
getConnectionConfig: vi.fn(),
prepareQueryPaginationExecutionPlan: vi.fn(),
@@ -21,6 +22,7 @@ vi.mock("@/lib/backend/api", () => ({
closeClientConnectionSession: mocks.closeClientConnectionSession,
closeQuerySession: mocks.closeQuerySession,
executeMulti: mocks.executeMulti,
executeMultiWithProgress: mocks.executeMultiWithProgress,
executeQuery: mocks.executeQuery,
prepareQueryPaginationExecutionPlan: mocks.prepareQueryPaginationExecutionPlan,
saveOpenTabsState: mocks.saveOpenTabsState,
@@ -99,6 +101,25 @@ describe("queryStore multi-statement errors", () => {
useAgentResultSession: false,
}));
mocks.analyzeEditableQueryEditability.mockResolvedValue({ editable: false, reason: "multiple-statements" });
mocks.executeMultiWithProgress.mockImplementation(async (connectionId, database, sql, onProgress, schema, options) => {
const results = await mocks.executeMulti(connectionId, database, sql, schema, options?.executionId, options);
const total = results.length;
results.forEach((result: any, index: number) => {
const statementIndex = result.statement_index ?? index;
const success = result.execution_error !== true;
onProgress({
executionId: options?.executionId,
statementIndex,
completed: index + 1,
total,
success,
executionTimeMs: result.execution_time_ms,
affectedRows: result.affected_rows,
error: success ? undefined : String(result.rows[0]?.[0] ?? ""),
});
});
return results;
});
});
it("opens the first error result from a mixed result batch", async () => {
@@ -117,6 +138,126 @@ describe("queryStore multi-statement errors", () => {
expect(tab.result?.columns).toEqual(["Error"]);
});
it("updates live per-statement progress before the batch promise resolves", async () => {
const pendingExecution = deferred<any[]>();
let reportProgress!: (progress: any) => void;
mocks.executeMultiWithProgress.mockImplementationOnce((_connectionId, _database, _sql, onProgress) => {
reportProgress = onProgress;
return pendingExecution.promise;
});
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("mysql-1", "app", "Query", "query", undefined, "SELECT 1;\nSELECT 2;\nSELECT bad");
const execution = store.executeTabSql(tabId, "SELECT 1;\nSELECT 2;\nSELECT bad");
await vi.waitFor(() => expect(store.tabs.find((item) => item.id === tabId)?.batchSqlExecution?.items[0]?.status).toBe("running"));
const executionId = store.tabs.find((item) => item.id === tabId)!.executionId!;
reportProgress({
executionId,
statementIndex: 0,
completed: 1,
total: 3,
success: true,
executionTimeMs: 4,
affectedRows: 1,
});
expect(store.tabs.find((item) => item.id === tabId)?.batchSqlExecution).toMatchObject({
completed: 1,
total: 3,
items: [{ status: "success", executionTimeMs: 4, affectedRows: 1 }, { status: "running" }, { status: "pending" }],
});
pendingExecution.resolve([
{ columns: [], rows: [], affected_rows: 1, execution_time_ms: 4, statement_index: 0 },
{ columns: ["Error"], rows: [["bad statement"]], affected_rows: 0, execution_time_ms: 2, statement_index: 1, execution_error: true },
]);
await execution;
expect(store.tabs.find((item) => item.id === tabId)?.batchSqlExecution).toMatchObject({
completed: 2,
items: [{ status: "success" }, { status: "error", error: "bad statement" }, { status: "skipped" }],
});
});
it("records a top-level batch failure on the current statement", async () => {
mocks.executeMultiWithProgress.mockRejectedValueOnce(new Error("transport failed"));
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("mysql-1", "app", "Query", "query", undefined, "SELECT 1;\nSELECT 2");
await store.executeTabSql(tabId, "SELECT 1;\nSELECT 2");
expect(store.tabs.find((item) => item.id === tabId)?.batchSqlExecution).toMatchObject({
completed: 1,
items: [{ status: "error", error: "transport failed" }, { status: "skipped" }],
});
});
it("keeps completed progress and records a later top-level batch failure", async () => {
mocks.executeMultiWithProgress.mockImplementationOnce((_connectionId, _database, _sql, onProgress, _schema, options) => {
onProgress({
executionId: options?.executionId,
statementIndex: 0,
completed: 1,
total: 3,
success: true,
executionTimeMs: 4,
affectedRows: 1,
});
return Promise.reject(new Error("connection lost"));
});
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("mysql-1", "app", "Query", "query", undefined, "SELECT 1;\nSELECT 2;\nSELECT 3");
await store.executeTabSql(tabId, "SELECT 1;\nSELECT 2;\nSELECT 3");
expect(store.tabs.find((item) => item.id === tabId)?.batchSqlExecution).toMatchObject({
completed: 2,
items: [{ status: "success" }, { status: "error", error: "connection lost" }, { status: "skipped" }],
});
});
it("updates the live marker state for a single statement", async () => {
const pendingExecution = deferred<any[]>();
mocks.executeMulti.mockImplementationOnce(() => pendingExecution.promise);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const sql = "SELECT 1";
const tabId = store.createTab("mysql-1", "app", "Query", "query", undefined, sql);
const execution = store.executeTabSql(tabId, sql);
await vi.waitFor(() => expect(store.tabs.find((item) => item.id === tabId)?.batchSqlExecution?.items[0]?.status).toBe("running"));
expect(mocks.executeMultiWithProgress).not.toHaveBeenCalled();
pendingExecution.resolve([{ columns: ["Error"], rows: [["bad statement"]], affected_rows: 0, execution_time_ms: 3, statement_index: 0, execution_error: true }]);
await execution;
expect(store.tabs.find((item) => item.id === tabId)?.batchSqlExecution).toMatchObject({
completed: 1,
total: 1,
items: [{ status: "error", executionTimeMs: 3, affectedRows: 0, error: "bad statement" }],
});
});
it("marks the active statement cancelled and leaves later statements unexecuted", async () => {
const pendingExecution = deferred<never>();
mocks.executeMultiWithProgress.mockImplementationOnce(() => pendingExecution.promise);
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("mysql-1", "app", "Query", "query", undefined, "SELECT 1;\nSELECT 2");
const execution = store.executeTabSql(tabId, "SELECT 1;\nSELECT 2");
await vi.waitFor(() => expect(store.tabs.find((item) => item.id === tabId)?.batchSqlExecution?.items[0]?.status).toBe("running"));
await expect(store.cancelTabExecution(tabId)).resolves.toBe(true);
pendingExecution.reject(new Error("Query canceled"));
await execution;
expect(store.tabs.find((item) => item.id === tabId)?.batchSqlExecution?.items).toMatchObject([{ status: "cancelled" }, { status: "skipped" }]);
});
it("opens a later PostgreSQL error result from a mixed result batch", async () => {
mocks.getConnectionConfig.mockReturnValue({
id: "postgres-1",
@@ -457,19 +598,37 @@ describe("queryStore multi-statement errors", () => {
const tab = store.tabs.find((item) => item.id === tabId)!;
const retainedRunId = tab.activeResultRunId!;
const retainedRunCacheKey = tab.resultRuns?.find((run) => run.id === retainedRunId)?.resultCacheKey;
expect(tab.batchSqlExecution?.submittedSql).toBe("SELECT 1 AS value");
const execution = store.executeCurrentSql("SELECT 2 AS value", { openInNewResultTab: true });
await vi.waitFor(() => expect(mocks.executeMulti).toHaveBeenCalledTimes(2));
expect(await store.setActiveResultRun(tabId, retainedRunId)).toBe(true);
expect(tab.batchSqlExecution?.submittedSql).toBe("SELECT 1 AS value");
pendingExecution.resolve([{ columns: ["value"], rows: [[2]], affected_rows: 0, execution_time_ms: 1 }]);
await execution;
expect(tab.resultRuns).toHaveLength(2);
expect(tab.activeResultRunId).not.toBe(retainedRunId);
expect(tab.result).toMatchObject({ rows: [[2]] });
expect(tab.batchSqlExecution?.submittedSql).toBe("SELECT 2 AS value");
expect(tab.resultRuns?.find((run) => run.id === tab.activeResultRunId)?.resultCacheKey).not.toBe(retainedRunCacheKey);
expect(await store.setActiveResultRun(tabId, retainedRunId)).toBe(true);
expect(tab.result).toMatchObject({ rows: [[1]] });
expect(tab.batchSqlExecution?.submittedSql).toBe("SELECT 1 AS value");
const newRunId = tab.resultRuns?.find((run) => run.id !== retainedRunId)?.id;
expect(newRunId).toBeTruthy();
expect(await store.setActiveResultRun(tabId, newRunId!)).toBe(true);
await vi.waitFor(() => expect(mocks.tabResultSnapshots.has(retainedRunCacheKey!)).toBe(true));
const retainedRun = tab.resultRuns?.find((run) => run.id === retainedRunId);
expect(retainedRun).toBeTruthy();
retainedRun!.result = undefined;
retainedRun!.results = undefined;
retainedRun!.batchSqlExecution = undefined;
expect(await store.setActiveResultRun(tabId, retainedRunId)).toBe(true);
expect(tab.result).toMatchObject({ rows: [[1]] });
expect(tab.batchSqlExecution?.submittedSql).toBe("SELECT 1 AS value");
});
it("restores the retained result when a new-result execution returns no result", async () => {
+161 -2
View File
@@ -2,7 +2,7 @@ import { defineStore } from "pinia";
import { uuid } from "@/lib/common/utils";
import { computed, markRaw, onScopeDispose, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import type { ConnectionConfig, DatabaseType, IndexInfo, ObjectBrowserViewport, QueryResult, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, ObjectBrowserViewport, QueryResult, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
import { orderPinnedFirst } from "@/lib/app/pinnedItems";
import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState";
import { buildExplainSql, parseExplainResult, parseDamengExplainText, parseOracleExplainText, sqlServerExplainResult, type BuildExplainSqlResult } from "@/lib/diagram/explainPlan";
@@ -209,6 +209,126 @@ function annotateQueryResultSources(results: QueryResult[], sql: string, databas
return results;
}
const NON_STREAMING_BATCH_DATABASE_TYPES = new Set<DatabaseType>(["sqlserver", "turso", "cloudflare-d1"]);
const liveBatchSqlExecutions = new WeakMap<QueryTab, BatchSqlExecution>();
function cloneBatchSqlExecution(batch: BatchSqlExecution | undefined): BatchSqlExecution | undefined {
return batch ? { ...batch, items: batch.items.map((item) => ({ ...item })) } : undefined;
}
function batchSqlExecutionFor(tab: QueryTab, executionId: string): BatchSqlExecution | undefined {
const liveBatch = liveBatchSqlExecutions.get(tab);
if (liveBatch?.executionId === executionId) return liveBatch;
return tab.batchSqlExecution?.executionId === executionId ? tab.batchSqlExecution : undefined;
}
function clearLiveBatchSqlExecution(tab: QueryTab, executionId: string) {
if (liveBatchSqlExecutions.get(tab)?.executionId === executionId) liveBatchSqlExecutions.delete(tab);
}
function createBatchSqlExecution(executionId: string, editorSql: string, submittedSql: string, databaseType: DatabaseType | undefined, sourceOffset: number | undefined): BatchSqlExecution | undefined {
const statements = splitSqlStatementRanges(submittedSql, databaseType);
if (statements.length === 0) return undefined;
if (statements.length > 1 && databaseType && NON_STREAMING_BATCH_DATABASE_TYPES.has(databaseType)) return undefined;
const offset = sourceOffset ?? 0;
return {
executionId,
submittedSql,
editorFingerprint: sqlTextFingerprint(editorSql),
sourceOffset: offset,
completed: 0,
total: statements.length,
startedAt: Date.now(),
items: statements.map((statement, statementIndex) => ({
statementIndex,
sql: statement.sql,
from: offset + statement.from,
to: offset + statement.to,
status: statementIndex === 0 ? "running" : "pending",
})),
};
}
function applyBatchSqlProgress(
tab: QueryTab,
progress: {
executionId: string;
statementIndex: number;
completed: number;
total: number;
success: boolean;
executionTimeMs: number;
affectedRows: number;
error?: string;
},
continueOnError: boolean,
) {
const batch = batchSqlExecutionFor(tab, progress.executionId);
if (!batch) return;
const item = batch.items[progress.statementIndex];
if (!item) return;
item.status = progress.success ? "success" : "error";
item.executionTimeMs = progress.executionTimeMs;
item.affectedRows = progress.affectedRows;
item.error = progress.error;
batch.completed = Math.max(batch.completed, progress.completed);
if ((progress.success || continueOnError) && progress.completed < batch.total) {
const next = batch.items[progress.statementIndex + 1];
if (next?.status === "pending") next.status = "running";
}
}
function reconcileBatchSqlResults(tab: QueryTab, executionId: string, results: QueryResult[]) {
const batch = batchSqlExecutionFor(tab, executionId);
if (!batch) return;
let fallbackIndex = 0;
for (const result of results) {
const statementIndex = Number.isInteger(result.statement_index) && result.statement_index! >= 0 ? result.statement_index! : fallbackIndex;
fallbackIndex = Math.max(fallbackIndex, statementIndex + 1);
const item = batch.items[statementIndex];
if (!item) continue;
const failed = result.execution_error === true;
item.status = failed ? "error" : "success";
item.executionTimeMs = result.execution_time_ms;
item.affectedRows = result.affected_rows;
item.error = failed ? String(result.rows[0]?.[0] ?? "") : undefined;
}
batch.completed = batch.items.filter((item) => item.status === "success" || item.status === "error").length;
}
function failBatchSqlExecution(tab: QueryTab, executionId: string, error: unknown, cancelled: boolean) {
const batch = batchSqlExecutionFor(tab, executionId);
if (!batch) return;
const item = batch.items.find((candidate) => candidate.status === "running") ?? batch.items.find((candidate) => candidate.status === "pending");
if (!item) return;
item.status = cancelled ? "cancelled" : "error";
item.error = cancelled ? undefined : error instanceof Error ? error.message : String(error);
batch.completed = batch.items.filter((candidate) => candidate.status === "success" || candidate.status === "error").length;
}
function finishBatchSqlExecution(tab: QueryTab, executionId: string, cancelled: boolean) {
const batch = batchSqlExecutionFor(tab, executionId);
if (!batch) return;
if (cancelled) {
const cancelledError = [...batch.items].reverse().find((item) => item.status === "error" && /cancel|取消/i.test(item.error ?? ""));
if (cancelledError) {
cancelledError.status = "cancelled";
cancelledError.error = undefined;
}
}
let markedCancelled = false;
for (const item of batch.items) {
if (item.status === "running" && cancelled && !markedCancelled) {
item.status = "cancelled";
markedCancelled = true;
} else if (item.status === "running" || item.status === "pending") {
item.status = "skipped";
}
}
batch.completed = batch.items.filter((item) => item.status === "success" || item.status === "error").length;
batch.finishedAt = Date.now();
}
function sqlStatementWithoutLeadingComments(statement: string | undefined): string {
let remaining = statement?.trimStart() ?? "";
while (remaining) {
@@ -630,6 +750,7 @@ export const useQueryStore = defineStore("query", () => {
tab.result = undefined;
tab.results = undefined;
tab.activeResultIndex = undefined;
tab.batchSqlExecution = undefined;
tab.resultEditorFingerprint = undefined;
tab.resultLocalSortOriginalRows = undefined;
tab.resultLocalSortOriginalMongoDocuments = undefined;
@@ -699,6 +820,7 @@ export const useQueryStore = defineStore("query", () => {
tab.result = run.result ?? run.results?.[activeIndex];
tab.results = run.results;
tab.activeResultIndex = run.activeResultIndex;
tab.batchSqlExecution = cloneBatchSqlExecution(run.batchSqlExecution);
tab.resultBaseSql = run.resultBaseSql;
tab.resultEditorFingerprint = run.resultEditorFingerprint;
tab.resultSortedSql = run.resultSortedSql;
@@ -863,6 +985,7 @@ export const useQueryStore = defineStore("query", () => {
result: tab.result,
results: tab.results,
activeResultIndex: tab.activeResultIndex,
batchSqlExecution: cloneBatchSqlExecution(tab.batchSqlExecution),
resultBaseSql: tab.resultBaseSql,
resultEditorFingerprint: tab.resultEditorFingerprint,
resultSortedSql: tab.resultSortedSql,
@@ -922,6 +1045,7 @@ export const useQueryStore = defineStore("query", () => {
result: tab.result,
results: tab.results,
activeResultIndex: tab.activeResultIndex,
batchSqlExecution: cloneBatchSqlExecution(tab.batchSqlExecution),
resultBaseSql: tab.resultBaseSql,
resultEditorFingerprint: tab.resultEditorFingerprint,
resultSortedSql: tab.resultSortedSql,
@@ -2568,6 +2692,7 @@ export const useQueryStore = defineStore("query", () => {
pendingResultRunRestores.delete(executionId);
return;
}
finishBatchSqlExecution(current, executionId, true);
current.isExecuting = false;
current.isCancelling = false;
current.executionId = undefined;
@@ -2579,6 +2704,7 @@ export const useQueryStore = defineStore("query", () => {
current.resultSessionId = undefined;
touchResult(current);
}
clearLiveBatchSqlExecution(current, executionId);
}, CANCEL_ACK_SETTLE_TIMEOUT_MS);
}
@@ -3129,6 +3255,8 @@ export const useQueryStore = defineStore("query", () => {
if (openInNewResultTab && tab.activeResultRunId) {
pendingResultRunRestores.set(executionId, tab.activeResultRunId);
}
tab.batchSqlExecution = undefined;
liveBatchSqlExecutions.delete(tab);
const preserveResultDuringExecution = options?.preserveResultDuringExecution === true || (tab.mode === "query" && !!tab.activeResultRunId && !tab.resultAutoSave && !openInNewResultTab);
const updateActiveResultRun = !!tab.activeResultRunId && preserveResultDuringExecution;
if (!updateActiveResultRun) {
@@ -3187,6 +3315,9 @@ export const useQueryStore = defineStore("query", () => {
const useAgentCursor = usesAgentCursorForQuery(conn?.db_type);
const queryTimeoutSecs = queryTimeoutSecsForConnection(conn);
const settingsStore = useSettingsStore();
const statementExecution = tab.mode === "query" ? createBatchSqlExecution(executionId, tab.sql, sql, effectiveDbType, options?.sourceOffset) : undefined;
tab.batchSqlExecution = statementExecution && (tab.autoCommit !== false || statementExecution.total === 1) ? statementExecution : undefined;
if (tab.batchSqlExecution) liveBatchSqlExecutions.set(tab, tab.batchSqlExecution);
queryExecutionLog("info", "previous-session-close:start", { traceId, elapsed: elapsed() });
await previousResultSessionClose;
queryExecutionLog("info", "previous-session-close:done", { traceId, elapsed: elapsed() });
@@ -3740,9 +3871,25 @@ export const useQueryStore = defineStore("query", () => {
clientSession: Boolean(clientSessionId),
});
executionDispatched = true;
executionPromise = api.executeMulti(tab.connectionId, executionDatabase, sqlToExecute, executionSchema, executionId, executionOptions);
executionPromise =
tab.batchSqlExecution && tab.batchSqlExecution.total > 1
? api.executeMultiWithProgress(
tab.connectionId,
executionDatabase,
sqlToExecute,
(progress) => {
const current = tabs.value.find((item) => item.id === id);
if (current?.executionId === executionId) {
applyBatchSqlProgress(current, progress, settingsStore.editorSettings.continueOnErrorOnBatch);
}
},
executionSchema,
{ ...executionOptions, executionId },
)
: api.executeMulti(tab.connectionId, executionDatabase, sqlToExecute, executionSchema, executionId, executionOptions);
}
const results = annotateQueryResultSources(markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))), queryBaseSql, sourceLabelDatabase, effectiveDbType, options?.sourceOffset);
reconcileBatchSqlResults(tab, executionId, results);
const successfulOracleSchemaChanges = effectiveDbType === "oracle" ? results.filter((result) => result.execution_error !== true && isOracleCurrentSchemaStatement(result.sourceStatement)).length : 0;
const successfulSapHanaSchemaChanges = effectiveDbType === "saphana" ? results.filter((result) => result.execution_error !== true && isSapHanaSetSchemaStatement(result.sourceStatement)).length : 0;
if (hiddenPrimaryKeys.length > 0 && results.length === 1) {
@@ -3916,6 +4063,7 @@ export const useQueryStore = defineStore("query", () => {
}
const current = tabs.value.find((t) => t.id === id);
if (current?.executionId === executionId) {
failBatchSqlExecution(current, executionId, e, current.isCancelling === true);
const restoredRetainedResult = openInNewResultTab && (current.isCancelling || !executionDispatched) && restorePendingResultRun(current, executionId);
if (restoredRetainedResult) {
queryExecutionLog("info", "retained-result:restored-after-abort", { traceId, elapsed: elapsed() });
@@ -3960,6 +4108,10 @@ export const useQueryStore = defineStore("query", () => {
} finally {
const current = tabs.value.find((t) => t.id === id);
if (current?.executionId === executionId) {
const liveBatch = liveBatchSqlExecutions.get(current);
if (liveBatch?.executionId === executionId) current.batchSqlExecution = liveBatch;
finishBatchSqlExecution(current, executionId, current.isCancelling === true);
if (current.activeResultRunId && current.result) syncActiveResultRunFromDisplayed(current);
if (openInNewResultTab && !current.activeResultRunId) {
restorePendingResultRun(current, executionId);
} else {
@@ -3969,9 +4121,11 @@ export const useQueryStore = defineStore("query", () => {
current.isCancelling = false;
current.queryExecutionStartedAt = undefined;
current.executionId = undefined;
clearLiveBatchSqlExecution(current, executionId);
queryExecutionLog("info", "finish", { traceId, elapsed: elapsed() });
} else {
pendingResultRunRestores.delete(executionId);
if (current) clearLiveBatchSqlExecution(current, executionId);
queryExecutionLog("warn", "finish-stale", {
traceId,
currentExecutionId: current?.executionId,
@@ -4331,11 +4485,13 @@ export const useQueryStore = defineStore("query", () => {
if (!canceled) {
const current = tabs.value.find((t) => t.id === id);
if (current && current.executionId === executionId) {
finishBatchSqlExecution(current, executionId, false);
restorePendingResultRun(current, executionId);
current.isExecuting = false;
current.isCancelling = false;
current.executionId = undefined;
current.queryExecutionStartedAt = undefined;
clearLiveBatchSqlExecution(current, executionId);
}
}
return canceled;
@@ -4344,6 +4500,8 @@ export const useQueryStore = defineStore("query", () => {
if (tab) useConnectionStore().recordConnectionLostError(tab.connectionId, e);
const current = tabs.value.find((t) => t.id === id);
if (current && current.executionId === executionId) {
failBatchSqlExecution(current, executionId, e, false);
finishBatchSqlExecution(current, executionId, false);
if (restorePendingResultRun(current, executionId)) {
current.isExecuting = false;
current.isCancelling = false;
@@ -4354,6 +4512,7 @@ export const useQueryStore = defineStore("query", () => {
// 估算值也会继续按旧的 results 计算
setErrorResult(id, e);
}
clearLiveBatchSqlExecution(current, executionId);
}
return false;
} finally {