feat(query): show live statement execution progress
This commit is contained in:
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 () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user