fix(mysql): optionally keep explicit transactions in autocommit tabs

This commit is contained in:
zipg authored and GitHub committed 2026-09-22 11:11:06 +08:00
1 parent 74f1704d09
commit dc6fee7e93
35 files changed
+1274 -47

No files matched your search

@@ -562,6 +562,7 @@ const showThemeCustomizer = ref(false);
const showDataGridTypeColorScheme = ref(false);
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editDefaultTransactionMode = ref(settingsStore.editorSettings.defaultTransactionMode);
const editKeepExplicitTransactionInAutoCommit = ref(settingsStore.editorSettings.keepExplicitTransactionInAutoCommit);
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
function translateWithExecuteShortcut(key: string): string {
return t(key, { shortcut: formatShortcutDisplay(editShortcuts.value.executeSql) });
@@ -930,6 +931,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
activeCustomThemeId: editActiveCustomThemeId.value,
executeMode: editExecuteMode.value,
defaultTransactionMode: editDefaultTransactionMode.value,
keepExplicitTransactionInAutoCommit: editKeepExplicitTransactionInAutoCommit.value,
executeAllOnBlankLine: editExecuteAllOnBlankLine.value,
showExecutionTargetPicker: editShowExecutionTargetPicker.value,
showStatementRunButtons: editShowStatementRunButtons.value,
@@ -1571,6 +1573,7 @@ function syncEditorSettingsDraftFromStore() {
editActiveCustomThemeId.value = settingsStore.editorSettings.activeCustomThemeId;
editExecuteMode.value = settingsStore.editorSettings.executeMode;
editDefaultTransactionMode.value = settingsStore.editorSettings.defaultTransactionMode;
editKeepExplicitTransactionInAutoCommit.value = settingsStore.editorSettings.keepExplicitTransactionInAutoCommit;
editExecuteAllOnBlankLine.value = settingsStore.editorSettings.executeAllOnBlankLine;
editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker;
editShowStatementRunButtons.value = settingsStore.editorSettings.showStatementRunButtons;
@@ -1815,6 +1818,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = {
clickTableNavigationTarget: editClickTableNavigationTarget,
completionTriggerMode: editCompletionTriggerMode,
defaultTransactionMode: editDefaultTransactionMode,
keepExplicitTransactionInAutoCommit: editKeepExplicitTransactionInAutoCommit,
tableColumnTemplateFields: editTableColumnTemplateRows,
};
@@ -2159,6 +2163,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editDefaultTransactionMode.value = DEFAULT_EDITOR_SETTINGS.defaultTransactionMode;
editKeepExplicitTransactionInAutoCommit.value = DEFAULT_EDITOR_SETTINGS.keepExplicitTransactionInAutoCommit;
editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine;
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
@@ -2310,6 +2315,7 @@ function resetAllDefaults() {
editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editDefaultTransactionMode.value = DEFAULT_EDITOR_SETTINGS.defaultTransactionMode;
editKeepExplicitTransactionInAutoCommit.value = DEFAULT_EDITOR_SETTINGS.keepExplicitTransactionInAutoCommit;
editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine;
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons;
@@ -6095,6 +6101,16 @@ onUnmounted(() => {
</Select>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2 md:col-span-2" data-editor-keep-explicit-transaction>
<div class="min-w-0 space-y-1">
<Label for="editor-keep-explicit-transaction">{{ t("settings.keepExplicitTransactionInAutoCommit") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.keepExplicitTransactionInAutoCommitDescription") }}
</p>
</div>
<Switch id="editor-keep-explicit-transaction" v-model="editKeepExplicitTransactionInAutoCommit" class="mt-0.5 shrink-0" />
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2" :class="{ 'opacity-50': editExecuteMode !== 'current' }">
<div class="space-y-1">
<Label for="editor-execute-all-on-blank-line">{{ t("settings.executeAllOnBlankLine") }}</Label>
@@ -186,6 +186,9 @@ const groupExecutableSql = computed(() => {
:txn-session-id="activeTab.txnSessionId"
:txn-auto-rolled-back="activeTab.txnAutoRolledBack"
:txn-possibly-dirty="activeTab.txnPossiblyDirty"
:auto-commit-open-transaction="activeTab.autoCommitOpenTransaction"
:auto-commit-txn-rolled-back="activeTab.autoCommitTxnRolledBack"
:auto-commit-session-txn-rolled-back="activeTab.autoCommitSessionTxnRolledBack"
:sticky-proven-read-only-state="isGroupStickyManualTransaction"
@update:explain-mode="(m: 'explain' | 'autotrace') => (toolbar.explainMode.value = m)"
@update:block-dangerous-redis-commands="(v: boolean) => (toolbar.blockDangerousRedisCommands.value = v)"
@@ -199,6 +202,8 @@ const groupExecutableSql = computed(() => {
@commit="activeTab && queryStore.commitTransaction(activeTab.id)"
@rollback="activeTab && queryStore.rollbackTransaction(activeTab.id)"
@dismiss-txn-rolled-back="activeTab && (activeTab.txnAutoRolledBack = false)"
@dismiss-auto-commit-txn-rolled-back="activeTab && (activeTab.autoCommitTxnRolledBack = false)"
@dismiss-auto-commit-session-txn-rolled-back="activeTab && (activeTab.autoCommitSessionTxnRolledBack = false)"
@execute-pointer-down="toolbar.captureExecutionSnapshot(activeTab.id)"
@toolbar-execute="toolbar.toolbarExecute($event, activeTab.id)"
@multi-execute="toolbar.multiExecute()"
@@ -48,6 +48,16 @@ const props = defineProps<{
/** Oracle manual mode derived from the resolved database type (not raw
* db_type, which can be the agent transport). */
stickyProvenReadOnlyState?: boolean;
/** Auto-commit tabs (`Tx:A`): the tab's own connection holds a transaction
* the user opened explicitly (`BEGIN` / `START TRANSACTION`) that DBX kept
* open. Commit/Rollback act on that transaction. */
autoCommitOpenTransaction?: boolean;
/** Auto-commit tab: the backend rolled back an explicit transaction this tab
* left open (the tab did not opt into keeping them). */
autoCommitTxnRolledBack?: boolean;
/** Auto-commit tab: the backend rolled back an implicitly opened transaction
* (`SET autocommit = 0`), reported once per connection. */
autoCommitSessionTxnRolledBack?: boolean;
}>();
const emit = defineEmits<{
@@ -76,6 +86,8 @@ const emit = defineEmits<{
commit: [];
rollback: [];
dismissTxnRolledBack: [];
dismissAutoCommitTxnRolledBack: [];
dismissAutoCommitSessionTxnRolledBack: [];
}>();
const { t } = useI18n();
@@ -277,16 +289,23 @@ function toggleInsertValueHints() {
settingsStore.updateEditorSettings({ showInsertValueHints: !insertValueHintsEnabled.value });
}
const isTransactionActive = computed(() => !!props.txnSessionId);
/** Auto-commit tab whose connection holds a transaction the user opened with
* `BEGIN` / `START TRANSACTION` and that DBX kept open (`Tx:A`). */
const hasOpenAutoCommitTransaction = computed(() => props.autoCommitOpenTransaction === true);
const isManualTransactionMode = computed(() => props.autoCommit === false || isTransactionActive.value);
const transactionModeBadge = computed(() => (isManualTransactionMode.value ? "M" : "A"));
// Sticky proven-read-only dialects (Oracle/OceanBase-Oracle/MySQL/PostgreSQL)
// hide Commit/Rollback while the session is clean (no unproven statement
// executed). Every other database keeps the existing rule.
// executed). Every other database keeps the existing rule. An auto-commit tab
// that kept the user's explicit transaction always offers both actions: that
// transaction exists only because the user asked for it.
const showTxnActions = computed(() => {
if (hasOpenAutoCommitTransaction.value) return true;
if (props.stickyProvenReadOnlyState) return isTransactionActive.value && props.txnPossiblyDirty === true;
return isTransactionActive.value;
});
const transactionTooltip = computed(() => {
if (hasOpenAutoCommitTransaction.value) return t("settings.keepExplicitTransactionInAutoCommitDescription");
const isAgent = (props.activeConnection?.db_type as string) === "agent";
const isManual = isManualTransactionMode.value;
if (isAgent && isManual) return t("toolbar.manualTransactionAgent");
@@ -699,12 +718,15 @@ async function changeCatalog(selectedCatalog: string) {
: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"
:aria-pressed="isManualTransactionMode || hasOpenAutoCommitTransaction"
@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>
<!-- Auto-commit tab with an uncommitted explicit transaction:
the commit/rollback actions next to the badge act on it. -->
<span v-if="hasOpenAutoCommitTransaction" data-toolbar-open-transaction-dot class="ml-px h-1.5 w-1.5 rounded-full bg-current" />
</span>
</Button>
</TooltipTrigger>
@@ -879,6 +901,20 @@ async function changeCatalog(selectedCatalog: string) {
<span class="truncate">{{ activeTab.tableMeta.columns.length }} {{ t("tree.columns") }}</span>
</div>
</div>
<div v-if="autoCommitTxnRolledBack" data-auto-commit-txn-rolled-back class="flex items-center gap-2 px-3 py-1 text-xs bg-amber-500/10 text-amber-700 dark:text-amber-300 border-b border-amber-500/20">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
<span>{{ t("toolbar.autoCommitTxnRolledBack") }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="emit('dismissAutoCommitTxnRolledBack')">
<X class="h-3 w-3" />
</Button>
</div>
<div v-else-if="autoCommitSessionTxnRolledBack" data-auto-commit-session-txn-rolled-back class="flex items-center gap-2 px-3 py-1 text-xs bg-amber-500/10 text-amber-700 dark:text-amber-300 border-b border-amber-500/20">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
<span>{{ t("toolbar.autoCommitSessionTxnRolledBack") }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="emit('dismissAutoCommitSessionTxnRolledBack')">
<X class="h-3 w-3" />
</Button>
</div>
<div v-if="txnAutoRolledBack" class="flex items-center gap-2 px-3 py-1 text-xs bg-amber-500/10 text-amber-700 dark:text-amber-300 border-b border-amber-500/20">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
<span>{{ t("toolbar.txnAutoRolledBack") }}</span>