feat(oracle): support manual query transactions

This commit is contained in:
t8y2 committed 2026-08-21 01:05:59 +08:00
1 parent a7edb8f8db
commit b5d3343f9a
22 files changed
+1011 -99

No files matched your search

@@ -150,9 +150,12 @@ function toggleSqlSemanticDiagnostics() {
sqlSemanticDiagnosticsMode: sqlSemanticDiagnosticsEnabled.value ? "disabled" : "enabled",
});
}
const isTransactionActive = computed(() => !!props.txnSessionId);
const isManualTransactionMode = computed(() => props.autoCommit === false || isTransactionActive.value);
const transactionModeBadge = computed(() => (isManualTransactionMode.value ? "M" : "A"));
const transactionTooltip = computed(() => {
const isAgent = (props.activeConnection?.db_type as string) === "agent";
const isManual = props.autoCommit === false;
const isManual = isManualTransactionMode.value;
if (isAgent && isManual) return t("toolbar.manualTransactionAgent");
if (isAgent) return t("toolbar.autoCommitAgent");
return isManual ? t("toolbar.manualTransaction") : t("toolbar.autoCommit");
@@ -162,7 +165,6 @@ const executeButtonClass = computed(() => {
return activeProductionContext.value.active ? "bg-red-500/10 text-red-700 hover:bg-red-500/20 hover:text-red-800 dark:text-red-300 dark:hover:text-red-200" : "bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/20 hover:text-emerald-800 dark:text-emerald-300 dark:hover:text-emerald-200";
});
const isTransactionActive = computed(() => !!props.txnSessionId);
const canMultiExecute = computed(() => {
if (!supportsQueryExecution(props.activeConnection?.db_type)) return false;
if (props.activeTab.isExecuting || props.activeTab.isExplaining || props.activeTab.isCancelling) return false;
@@ -306,41 +308,6 @@ async function changeCatalog(selectedCatalog: string) {
</TooltipTrigger>
<TooltipContent>{{ explainAnalyzeTooltip }}</TooltipContent>
</Tooltip>
<!-- Transaction toggle -->
<Tooltip v-if="supportsTransaction">
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-6 w-6"
:class="isTransactionActive || autoCommit === false ? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-300' : 'text-orange-600/70 hover:bg-orange-500/10 hover:text-orange-700 dark:text-orange-300/70 dark:hover:text-orange-200'"
:disabled="activeTab.isExecuting || activeTab.isExplaining"
@click="emit('update:autoCommit', autoCommit === false)"
>
<span class="text-xs font-bold leading-none">Tx</span>
</Button>
</TooltipTrigger>
<TooltipContent>{{ transactionTooltip }}</TooltipContent>
</Tooltip>
<!-- Commit button (only when transaction is active) -->
<Tooltip v-if="isTransactionActive">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-green-600 hover:bg-green-500/10 hover:text-green-700 dark:text-green-300 dark:hover:text-green-200" :disabled="activeTab.isExecuting" @click="emit('commit')">
<Check class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.commit") }}</TooltipContent>
</Tooltip>
<!-- Rollback button (only when transaction is active) -->
<Tooltip v-if="isTransactionActive">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-red-600 hover:bg-red-500/10 hover:text-red-700 dark:text-red-300 dark:hover:text-red-200" :disabled="activeTab.isExecuting" @click="emit('rollback')">
<RotateCcw class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.rollback") }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-amber-600 hover:bg-amber-500/10 hover:text-amber-700 dark:text-amber-300 dark:hover:text-amber-200" :disabled="activeTab.isExecuting || activeTab.isExplaining || !activeTab.sql.trim()" @click="emit('formatSql')">
@@ -441,6 +408,48 @@ async function changeCatalog(selectedCatalog: string) {
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.multiDbExecute") }}</TooltipContent>
</Tooltip>
<div v-if="supportsTransaction" class="ml-1 flex items-center gap-0.5 border-l border-border/60 pl-1" role="group" :aria-label="transactionTooltip">
<!-- Transaction toggle -->
<Tooltip>
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-6 w-8 px-1"
:class="isManualTransactionMode ? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-300' : 'text-orange-600/70 hover:bg-orange-500/10 hover:text-orange-700 dark:text-orange-300/70 dark:hover:text-orange-200'"
:disabled="activeTab.isExecuting || activeTab.isExplaining"
:aria-label="transactionTooltip"
:aria-pressed="isManualTransactionMode"
@click="emit('update:autoCommit', autoCommit === false)"
>
<span class="inline-flex items-center gap-px leading-none" aria-hidden="true">
<span class="text-[11px] font-bold">Tx:</span>
<span class="inline-flex h-3 min-w-3 items-center justify-center rounded-[3px] border border-current px-px text-[8px] font-extrabold leading-none">{{ transactionModeBadge }}</span>
</span>
</Button>
</TooltipTrigger>
<TooltipContent>{{ transactionTooltip }}</TooltipContent>
</Tooltip>
<!-- Commit button (only when transaction is active) -->
<Tooltip v-if="isTransactionActive">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-green-600 hover:bg-green-500/10 hover:text-green-700 dark:text-green-300 dark:hover:text-green-200" :disabled="activeTab.isExecuting" :aria-label="t('toolbar.commit')" @click="emit('commit')">
<Check class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.commit") }}</TooltipContent>
</Tooltip>
<!-- Rollback button (only when transaction is active) -->
<Tooltip v-if="isTransactionActive">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-red-600 hover:bg-red-500/10 hover:text-red-700 dark:text-red-300 dark:hover:text-red-200" :disabled="activeTab.isExecuting" :aria-label="t('toolbar.rollback')" @click="emit('rollback')">
<RotateCcw class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.rollback") }}</TooltipContent>
</Tooltip>
</div>
</div>
<span class="flex-1 min-w-0" />
<div class="flex items-center gap-2 shrink-0">
@@ -0,0 +1,34 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const toolbarSource = readFileSync(new URL("../EditorToolbar.vue", import.meta.url), "utf8");
describe("EditorToolbar transaction mode control", () => {
it("shows an A or M badge next to Tx instead of relying on color alone", () => {
expect(toolbarSource).toContain('const transactionModeBadge = computed(() => (isManualTransactionMode.value ? "M" : "A"));');
expect(toolbarSource).toContain('<span class="text-[11px] font-bold">Tx:</span>');
expect(toolbarSource).toContain(">{{ transactionModeBadge }}</span>");
});
it("exposes the transaction mode to assistive technology", () => {
expect(toolbarSource).toContain('class="ml-1 flex items-center gap-0.5 border-l border-border/60 pl-1" role="group" :aria-label="transactionTooltip"');
expect(toolbarSource).toContain(':aria-label="transactionTooltip"');
expect(toolbarSource).toContain(':aria-pressed="isManualTransactionMode"');
expect(toolbarSource).toContain(":aria-label=\"t('toolbar.commit')\"");
expect(toolbarSource).toContain(":aria-label=\"t('toolbar.rollback')\"");
});
it("places the transaction controls at the end in mode, commit, rollback order", () => {
const multiExecuteIndex = toolbarSource.indexOf("@click=\"emit('multiExecute')\"");
const transactionToggleIndex = toolbarSource.indexOf("@click=\"emit('update:autoCommit', autoCommit === false)\"");
const commitIndex = toolbarSource.indexOf("@click=\"emit('commit')\"");
const rollbackIndex = toolbarSource.indexOf("@click=\"emit('rollback')\"");
const actionGroupEndIndex = toolbarSource.indexOf('<span class="flex-1 min-w-0" />');
expect(multiExecuteIndex).toBeGreaterThan(-1);
expect(transactionToggleIndex).toBeGreaterThan(multiExecuteIndex);
expect(commitIndex).toBeGreaterThan(transactionToggleIndex);
expect(rollbackIndex).toBeGreaterThan(commitIndex);
expect(actionGroupEndIndex).toBeGreaterThan(rollbackIndex);
});
});
@@ -3,6 +3,7 @@ import { connectionNamespaceCreationTarget, databaseNodeNamespaceCreationTarget
import { editableDatabasePropertyGroups, editableSchemaPropertyGroups } from "@/lib/database/databasePropertyEditing";
import { buildGetDatabaseCommentSql } from "@/lib/database/dbAdminSql";
import {
defaultAutoCommitForDbType,
isSchemaAware,
supportsConnectionScopedQueryExecution,
supportsConnectionDatabaseBrowser,
@@ -89,6 +90,7 @@ describe("supportsTransaction", () => {
it("returns true for supported database types", () => {
expect(supportsTransaction("postgres")).toBe(true);
expect(supportsTransaction("mysql")).toBe(true);
expect(supportsTransaction("oracle")).toBe(true);
});
it("returns false for unsupported database types", () => {
@@ -101,7 +103,6 @@ describe("supportsTransaction", () => {
expect(supportsTransaction("sqlite")).toBe(false);
expect(supportsTransaction("clickhouse")).toBe(false);
expect(supportsTransaction("sqlserver")).toBe(false);
expect(supportsTransaction("oracle")).toBe(false);
expect(supportsTransaction("dameng")).toBe(false);
expect(supportsTransaction("rqlite")).toBe(false);
expect(supportsTransaction("agent")).toBe(false);
@@ -112,6 +113,19 @@ describe("supportsTransaction", () => {
});
});
describe("defaultAutoCommitForDbType", () => {
it("defaults Oracle query tabs to manual transactions", () => {
expect(defaultAutoCommitForDbType("oracle")).toBe(false);
});
it("defaults other databases to auto-commit", () => {
expect(defaultAutoCommitForDbType("mysql")).toBe(true);
expect(defaultAutoCommitForDbType("postgres")).toBe(true);
expect(defaultAutoCommitForDbType("dameng")).toBe(true);
expect(defaultAutoCommitForDbType(undefined)).toBe(true);
});
});
describe("supportsSqlInListPaste", () => {
it("allows generic and SQL-like editors", () => {
expect(supportsSqlInListPaste(undefined)).toBe(true);
@@ -42,6 +42,7 @@ export interface SavedOpenTab {
whereInput?: string;
pinned?: boolean;
mode?: QueryTab["mode"];
autoCommit?: boolean;
mqTenant?: string;
mqInitialTab?: QueryTab["mqInitialTab"];
nacosNamespace?: string;
@@ -117,6 +118,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
...(tab.whereInput !== undefined ? { whereInput: tab.whereInput } : {}),
pinned: tab.pinned,
mode: tab.mode,
...(tab.mode === "query" && tab.autoCommit !== undefined ? { autoCommit: tab.autoCommit } : {}),
...(tab.mqTenant !== undefined ? { mqTenant: tab.mqTenant } : {}),
...(tab.mqInitialTab !== undefined ? { mqInitialTab: tab.mqInitialTab } : {}),
...(tab.nacosNamespace !== undefined ? { nacosNamespace: tab.nacosNamespace } : {}),
@@ -229,7 +229,7 @@ export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
return !!dbType && PG_LIKE_STRUCTURE_TYPES.has(dbType);
}
const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql"];
const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql", "oracle"];
/**
* Returns true if the given database type supports explicit transaction control
@@ -238,3 +238,11 @@ const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql"];
export function supportsTransaction(dbType?: string): boolean {
return !!dbType && TRANSACTION_SUPPORTED_TYPES.includes(dbType);
}
/**
* Default auto-commit mode when opening a query tab for the given database type.
* Oracle defaults to manual transactions so edits require an explicit Commit.
*/
export function defaultAutoCommitForDbType(dbType?: string): boolean {
return dbType !== "oracle";
}
@@ -408,6 +408,8 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT t.* FROM SH_SMCVDMS_OVERSEAS_DRSSITB.TT_PLATFORM_CARS t WHERE t.PLATFORM = '轻卡'");
@@ -461,6 +463,7 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT * FROM CUSTOMERS");
@@ -494,6 +497,8 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT * FROM aa");
@@ -532,6 +537,7 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ncdb", "Query");
store.setAutoCommit(tabId, true);
const sql = "SELECT * FROM VITF_PRODUCT_INFOOA WHERE PK_MATERIAL IN ('1', '2')";
const execution = store.executeTabSql(tabId, sql);
@@ -568,6 +574,8 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
store.setAutoCommit(tabId, true);
const execution = store.executeTabSql(tabId, "SELECT t.* FROM APP.WIDE_TABLE t");
await vi.waitFor(() => expect(executeMulti).toHaveBeenCalled());
@@ -618,6 +626,8 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, sql);
@@ -665,6 +675,8 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
store.setAutoCommit(tabId, true);
const tab = store.tabs.find((item) => item.id === tabId)!;
await store.executeTabSql(tabId, "SELECT * FROM APP.PLATFORM_VIEW");
@@ -695,6 +707,7 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT PAYLOAD FROM APP.DOCUMENTS");
@@ -723,6 +736,7 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT * FROM APP.DOCUMENT_VIEW");
@@ -752,6 +766,7 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT * FROM DOCUMENTS");
@@ -780,6 +795,7 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT NAME FROM APP.CUSTOMERS");
@@ -817,6 +833,7 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "ORCL", "Query");
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT * FROM CUSTOMERS");
@@ -959,6 +976,8 @@ describe("queryStore hidden primary key editing", () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.createTab("oracle-1", "XEPDB1", "Query");
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
store.setAutoCommit(tabId, true);
await store.executeTabSql(tabId, "SELECT NAME FROM DBX_HIDDEN_PK_EDIT_TEST");
@@ -386,6 +386,9 @@ describe("queryStore multi-statement errors", () => {
const store = useQueryStore();
const tabA = store.createTab("oracle-1", "ORCL", "Tab A");
const tabB = store.createTab("oracle-1", "ORCL", "Tab B");
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
store.setAutoCommit(tabA, true);
store.setAutoCommit(tabB, true);
await store.executeTabSql(tabA, "ALTER SESSION SET CURRENT_SCHEMA = REPORTING");
expect(store.tabs.find((tab) => tab.id === tabA)?.completionContextVersion).toBe(1);
+21 -3
View File
@@ -371,7 +371,19 @@ export const useConnectionStore = defineStore("connection", () => {
let savedSqlFilesByDatabase = indexSavedSqlFilesByDatabase(savedSqlStore.allFiles);
const connections = ref<ConnectionConfig[]>([]);
const isDesktop = isTauriRuntime();
const activeConnectionId = ref<string | null>(localStorage.getItem(ACTIVE_CONNECTION_STORAGE_KEY));
// Prefer a safe read so Node/vitest (missing or partial storage mocks) do not
// throw when query tabs resolve connection db_type defaults via this store.
const activeConnectionId = ref<string | null>(
(() => {
try {
const storage = globalThis.localStorage;
if (!storage || typeof storage.getItem !== "function") return null;
return storage.getItem(ACTIVE_CONNECTION_STORAGE_KEY);
} catch {
return null;
}
})(),
);
const selectedTreeNodeId = ref<string | null>(null);
const selectedTreeNodeIds = ref<string[]>([]);
// O(1) membership set — rebuilds only when selectedTreeNodeIds changes.
@@ -386,8 +398,14 @@ export const useConnectionStore = defineStore("connection", () => {
const treeClipboard = ref<TreeClipboard | null>(null);
watch(activeConnectionId, (id) => {
if (id) localStorage.setItem(ACTIVE_CONNECTION_STORAGE_KEY, id);
else localStorage.removeItem(ACTIVE_CONNECTION_STORAGE_KEY);
try {
const storage = globalThis.localStorage;
if (!storage || typeof storage.setItem !== "function" || typeof storage.removeItem !== "function") return;
if (id) storage.setItem(ACTIVE_CONNECTION_STORAGE_KEY, id);
else storage.removeItem(ACTIVE_CONNECTION_STORAGE_KEY);
} catch {
// Ignore storage failures in non-browser / partial mock environments.
}
});
const treeNodes = ref<TreeNode[]>([]);
const sidebarDatabaseStorageCache = new Map<string, { expiresAt: number; value: DatabaseStorageInfo[] }>();
+25 -7
View File
@@ -35,7 +35,7 @@ import { redisCommandResultToQueryResult } from "@/lib/redis/redisQueryResult";
import { nextRedisCommandDb } from "@/lib/redis/redisCommandSession";
import { isRedisMutatingCommand } from "@/lib/redis/redisCommandTable";
import { usesAgentCursorForQuery } from "@/lib/database/databaseDriverManifest";
import { supportsClearableQuerySchema } from "@/lib/database/databaseFeatureSupport";
import { defaultAutoCommitForDbType, supportsClearableQuerySchema } from "@/lib/database/databaseFeatureSupport";
import { canInsertTableRows, canUseKeylessRowPredicate, DBX_ROWID_COLUMN, editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
import { TABLE_DATA_EXPORT_PAGE_SIZE } from "@/lib/table/tableDataExport";
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
@@ -1509,9 +1509,12 @@ export const useQueryStore = defineStore("query", () => {
function applyRestoredOpenTabs(restored: { tabs: QueryTab[]; activeTabId: string | null }) {
const connectionStore = useConnectionStore();
for (const tab of restored.tabs) {
if (tab.mode !== "data") continue;
const connection = connectionStore.getConfig(tab.connectionId);
if (connection) tab.schema = connectionObjectTreeNodeSchema(connection, tab.database, tab.schema);
if (tab.mode === "query" && tab.autoCommit === undefined) {
tab.autoCommit = defaultAutoCommitForDbType(connection?.db_type);
} else if (tab.mode === "data" && connection) {
tab.schema = connectionObjectTreeNodeSchema(connection, tab.database, tab.schema);
}
}
tabs.value = restored.tabs;
activeTabId.value = restored.activeTabId;
@@ -1657,6 +1660,7 @@ export const useQueryStore = defineStore("query", () => {
}
const id = uuid();
const dbType = useConnectionStore().getConfig(connectionId)?.db_type;
const tab: QueryTab = {
id,
title: title || `query_${tabs.value.length + 1}`,
@@ -1671,6 +1675,7 @@ export const useQueryStore = defineStore("query", () => {
isCancelling: false,
isExplaining: false,
mode,
...(mode === "query" ? { autoCommit: defaultAutoCommitForDbType(dbType) } : {}),
};
if (mode === "query") tab.originalSql = initialSql ?? "";
const activeIndex = options.insertAfterActive ? tabs.value.findIndex((item) => item.id === activeTabId.value) : -1;
@@ -1748,6 +1753,7 @@ export const useQueryStore = defineStore("query", () => {
// File-backed tabs are identified by their full path, not their basename.
// Bypassing createTab avoids overwriting another file with the same name.
const id = uuid();
const dbType = useConnectionStore().getConfig(connectionId)?.db_type;
const tab: QueryTab = {
id,
title: "",
@@ -1763,6 +1769,7 @@ export const useQueryStore = defineStore("query", () => {
isCancelling: false,
isExplaining: false,
mode: "query",
autoCommit: defaultAutoCommitForDbType(dbType),
};
tabs.value.push(tab);
activeTabId.value = id;
@@ -3022,7 +3029,10 @@ export const useQueryStore = defineStore("query", () => {
function rollbackTabTransaction(tab: QueryTab, options?: { resetAutoCommit?: boolean }) {
if (tab.txnSessionId) void rollbackTransaction(tab.id);
if (options?.resetAutoCommit) tab.autoCommit = true;
if (options?.resetAutoCommit) {
const dbType = useConnectionStore().getConfig(tab.connectionId)?.db_type;
tab.autoCommit = defaultAutoCommitForDbType(dbType);
}
tab.txnAutoRolledBack = false;
}
@@ -3161,6 +3171,7 @@ export const useQueryStore = defineStore("query", () => {
const id = uuid();
const restoredPosition = restoreSavedSqlEditorPosition(file.id, file.sql);
const dbType = useConnectionStore().getConfig(target.connectionId)?.db_type;
const tab: QueryTab = {
id,
title: file.name,
@@ -3176,6 +3187,7 @@ export const useQueryStore = defineStore("query", () => {
isCancelling: false,
isExplaining: false,
mode: "query",
autoCommit: defaultAutoCommitForDbType(dbType),
editorSelection: restoredPosition.selection,
editorViewport: restoredPosition.viewport,
};
@@ -5175,13 +5187,17 @@ export const useQueryStore = defineStore("query", () => {
queryExecutionLog("error", "error", { traceId, elapsed: elapsed(), error: e });
// Sync connection state if the error indicates a lost connection
useConnectionStore().recordConnectionLostError(executionConnectionId ?? tab.connectionId, e);
// Handle manual transaction auto-rollback (e.g. deadlock detected by server,
// statement error inside a manual transaction, or idle timeout).
// Handle manual transaction auto-rollback (idle timeout only for the banner;
// other statement failures still clear the session without the 5-minute notice).
if (tab.autoCommit === false) {
const errMsg: string = e?.message ?? String(e);
if (/rolled.?back/i.test(errMsg) || errMsg.includes("已自动回滚")) {
const idleTimeout = /5 minutes of inactivity/i.test(errMsg) || errMsg.includes("5 分钟无操作") || errMsg.includes("已自动回滚");
if (idleTimeout) {
tab.txnSessionId = undefined;
tab.txnAutoRolledBack = true;
} else if (/rolled.?back/i.test(errMsg) || /transaction session not found/i.test(errMsg) || /agent runtime terminated/i.test(errMsg)) {
tab.txnSessionId = undefined;
tab.txnAutoRolledBack = false;
}
}
const current = findExecutionTab(id);
@@ -5881,6 +5897,7 @@ export const useQueryStore = defineStore("query", () => {
function openResultArchiveTab(archive: DecodedQueryResultArchive): string | undefined {
const id = uuid();
const title = archive.tab.title.trim() || t("tabs.importedResultArchive");
const dbType = useConnectionStore().getConfig(archive.tab.connectionId)?.db_type;
const tab: QueryTab = {
id,
title,
@@ -5897,6 +5914,7 @@ export const useQueryStore = defineStore("query", () => {
isCancelling: false,
isExplaining: false,
mode: "query",
autoCommit: defaultAutoCommitForDbType(dbType),
};
if (!restoreCachedResultPayload(tab, archive.snapshot)) return undefined;
const activeRun = tab.resultRuns?.find((run) => run.id === tab.activeResultRunId) ?? tab.resultRuns?.[0];