fix(mysql): optionally keep explicit transactions in autocommit tabs
This commit is contained in:
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>
|
||||
|
||||
@@ -214,6 +214,8 @@ export default withEnglishFallback({
|
||||
commit: "Təsdiqlə",
|
||||
rollback: "Geri qaytar",
|
||||
txnAutoRolledBack: "5 dəqiqə fəaliyyətsizlikdən sonra tranzaksiya avtomatik geri qaytarıldı",
|
||||
autoCommitTxnRolledBack: 'Bu nişanın açıq qoyduğu tranzaksiya avtomatik geri qaytarıldı; icralar arasında saxlamaq üçün ayarlarda "Avtomatik commit rejimində açıq tranzaksiyaları saxla" seçimini aktivləşdirin',
|
||||
autoCommitSessionTxnRolledBack: "Bu bağlantı autocommit söndürülmüş (SET autocommit = 0) işləyir, ona görə örtük tranzaksiya icradan sonra geri qaytarıldı; DBX bunu hər bağlantı üçün bir dəfə bildirir",
|
||||
previewSql: "SQL önbaxışı",
|
||||
previewQuery: "Sorğu önbaxışı",
|
||||
hidePreviewSql: "SQL önbaxışını gizlət",
|
||||
@@ -7480,6 +7482,9 @@ export default withEnglishFallback({
|
||||
'Yeni SQL redaktoru açıldıqda standart olaraq istifadə olunan tranzaksiya rejimi. "Əl ilə tranzaksiya" seçildikdə yeni redaktorlar əl ilə tranzaksiya rejimində açılır və dəyişiklikləri əl ilə təsdiqləmək və ya geri qaytarmaq tələb olunur; "Avtomatik təsdiqləmə" seçildikdə hər əmr avtomatik təsdiqlənir. Yalnız tranzaksiyaları dəstəkləyən verilənlər bazalarına tətbiq olunur.',
|
||||
defaultTransactionModeAuto: "Avtomatik təsdiqləmə",
|
||||
defaultTransactionModeManual: "Əl ilə tranzaksiya",
|
||||
keepExplicitTransactionInAutoCommit: "Avtomatik commit rejimində açıq tranzaksiyaları saxla",
|
||||
keepExplicitTransactionInAutoCommitDescription:
|
||||
"Avtomatik commit nişanında BEGIN / START TRANSACTION icra etdikdən sonra tranzaksiya siz COMMIT və ya ROLLBACK icra edənə qədər açıq qalır. Söndürüldükdə hər icra açıq qalan tranzaksiyanı geri qaytarır ki, qalıq tranzaksiya nişanın oxuma snapshot-ını sabitləməsin.",
|
||||
executeAllOnBlankLine: "Boş sətirlərdən bütün SQL-i icra et",
|
||||
executeAllOnBlankLineDescription: "Cari əmr rejimində kursorun olduğu yerdə əmr olmadıqda bütün SQL-i icra et. Standart olaraq deaktivdir.",
|
||||
showExecutionTargetPicker: "İcra hədəfi seçicisini göstər",
|
||||
|
||||
@@ -380,6 +380,8 @@ export default {
|
||||
commit: "Commit",
|
||||
rollback: "Rollback",
|
||||
txnAutoRolledBack: "Transaction auto-rolled back after 5 minutes of inactivity",
|
||||
autoCommitTxnRolledBack: 'An explicit transaction left open by this tab was rolled back automatically; turn on "Keep explicit auto-commit transactions" to keep it across executions',
|
||||
autoCommitSessionTxnRolledBack: "This connection runs with auto-commit off (SET autocommit = 0), so its implicit transaction was rolled back after the execution; DBX reports that once per connection",
|
||||
previewSql: "Preview SQL",
|
||||
previewQuery: "Preview query",
|
||||
hidePreviewSql: "Hide SQL Preview",
|
||||
@@ -7978,6 +7980,9 @@ export default {
|
||||
'The transaction mode used by default when a new SQL editor is opened. With "Manual transaction", new editors start in manual transaction mode, requiring manual commit or rollback; with "Auto commit", each statement is committed automatically. Only applies to databases that support transactions.',
|
||||
defaultTransactionModeAuto: "Auto commit",
|
||||
defaultTransactionModeManual: "Manual transaction",
|
||||
keepExplicitTransactionInAutoCommit: "Keep explicit auto-commit transactions",
|
||||
keepExplicitTransactionInAutoCommitDescription:
|
||||
"After a BEGIN / START TRANSACTION in an auto-commit tab, keep the transaction open until you run COMMIT or ROLLBACK. When this is off, each execution rolls back the explicit transaction it left open, which stops a leftover transaction from pinning the tab's read view.",
|
||||
executeAllOnBlankLine: "Execute all SQL from blank lines",
|
||||
executeAllOnBlankLineDescription: "In current-statement mode, execute all SQL when there is no statement at the cursor. Disabled by default.",
|
||||
showExecutionTargetPicker: "Show execution target picker",
|
||||
|
||||
@@ -371,6 +371,8 @@ export default withEnglishFallback({
|
||||
commit: "Confirmar",
|
||||
rollback: "Revertir",
|
||||
txnAutoRolledBack: "Transacción revertida automáticamente tras 5 minutos de inactividad",
|
||||
autoCommitTxnRolledBack: 'La transacción explícita que esta pestaña dejó abierta se revirtió automáticamente; activa "Mantener transacciones explícitas en modo autocommit" para conservarla entre ejecuciones',
|
||||
autoCommitSessionTxnRolledBack: "Esta conexión trabaja con autocommit desactivado (SET autocommit = 0), así que su transacción implícita se revirtió tras la ejecución; DBX lo avisa una vez por conexión",
|
||||
previewSql: "Vista previa de SQL",
|
||||
previewQuery: "Vista previa de la consulta",
|
||||
hidePreviewSql: "Ocultar vista previa de SQL",
|
||||
@@ -7334,6 +7336,9 @@ export default withEnglishFallback({
|
||||
'El modo de transacción que se usa de forma predeterminada al abrir un nuevo editor SQL. Con "Transacción manual", los nuevos editores inician en modo de transacción manual y requieren confirmación o reversión manual; con "Confirmación automática", cada sentencia se confirma automáticamente. Solo se aplica a bases de datos que admiten transacciones.',
|
||||
defaultTransactionModeAuto: "Confirmación automática",
|
||||
defaultTransactionModeManual: "Transacción manual",
|
||||
keepExplicitTransactionInAutoCommit: "Mantener transacciones explícitas en modo autocommit",
|
||||
keepExplicitTransactionInAutoCommitDescription:
|
||||
"Tras ejecutar BEGIN / START TRANSACTION en una pestaña de autocommit, la transacción se mantiene hasta que ejecutes COMMIT o ROLLBACK. Si está desactivado, cada ejecución revierte la transacción explícita que quede abierta, lo que evita que una transacción residual fije la instantánea de lectura de la pestaña.",
|
||||
executeAllOnBlankLine: "Ejecutar todo el SQL desde líneas en blanco",
|
||||
executeAllOnBlankLineDescription: "En el modo de sentencia actual, ejecuta todo el SQL cuando no hay una sentencia en el cursor. Desactivado de forma predeterminada.",
|
||||
showExecutionTargetPicker: "Mostrar selector de objetivo",
|
||||
|
||||
@@ -370,6 +370,8 @@ export default withEnglishFallback({
|
||||
commit: "Commit",
|
||||
rollback: "Rollback",
|
||||
txnAutoRolledBack: "Transazione annullata automaticamente dopo 5 minuti di inattività",
|
||||
autoCommitTxnRolledBack: 'La transazione esplicita lasciata aperta da questa scheda è stata annullata automaticamente; attiva "Mantieni le transazioni esplicite in autocommit" per conservarla tra le esecuzioni',
|
||||
autoCommitSessionTxnRolledBack: "Questa connessione lavora con autocommit disattivato (SET autocommit = 0), quindi la sua transazione implicita è stata annullata dopo l'esecuzione; DBX lo segnala una volta per connessione",
|
||||
previewSql: "Anteprima SQL",
|
||||
previewQuery: "Anteprima query",
|
||||
hidePreviewSql: "Nascondi anteprima SQL",
|
||||
@@ -7333,6 +7335,9 @@ export default withEnglishFallback({
|
||||
'La modalità di transazione utilizzata per impostazione predefinita quando si apre un nuovo editor SQL. Con "Transazione manuale", i nuovi editori iniziano in modalità di transazione manuale, richiedendo commit o rollback manuale; con "Commit automatico", ogni istruzione viene committata automaticamente. Si applica solo ai database che supportano le transazioni.',
|
||||
defaultTransactionModeAuto: "Commit automatico",
|
||||
defaultTransactionModeManual: "Transazione manuale",
|
||||
keepExplicitTransactionInAutoCommit: "Mantieni le transazioni esplicite in autocommit",
|
||||
keepExplicitTransactionInAutoCommitDescription:
|
||||
"Dopo aver eseguito BEGIN / START TRANSACTION in una scheda autocommit, la transazione resta aperta finché non esegui COMMIT o ROLLBACK. Se disattivato, ogni esecuzione annulla la transazione esplicita lasciata aperta, evitando che una transazione residua fissi lo snapshot di lettura della scheda.",
|
||||
executeAllOnBlankLine: "Esegui tutto l'SQL dalle righe vuote",
|
||||
executeAllOnBlankLineDescription: "In modalità istruzione corrente, esegue tutto l'SQL quando non è presente un'istruzione al cursore. Disattivato per impostazione predefinita.",
|
||||
showExecutionTargetPicker: "Mostra selettore destinazione",
|
||||
|
||||
@@ -371,6 +371,8 @@ export default withEnglishFallback({
|
||||
commit: "コミット",
|
||||
rollback: "ロールバック",
|
||||
txnAutoRolledBack: "5分間操作がなかったため、トランザクションは自動的にロールバックされました",
|
||||
autoCommitTxnRolledBack: "このタブに残っていた明示的トランザクションは自動的にロールバックされました。実行をまたいで保持するには設定の「自動コミットでも明示的なトランザクションを保持」を有効にしてください",
|
||||
autoCommitSessionTxnRolledBack: "この接続は autocommit=0 で動作しているため、実行後に暗黙のトランザクションがロールバックされました(同じ接続では一度だけ通知します)",
|
||||
previewSql: "SQLをプレビュー",
|
||||
previewQuery: "クエリをプレビュー",
|
||||
hidePreviewSql: "SQLプレビューを非表示",
|
||||
@@ -7356,6 +7358,9 @@ export default withEnglishFallback({
|
||||
"新しい SQL エディタを開いたときに既定で使用するトランザクションモード。「手動トランザクション」を選択すると、新しいエディタは手動トランザクションモードで開始され、手動でコミットまたはロールバックします。「自動コミット」を選択すると、各ステートメントが自動的にコミットされます。トランザクションをサポートするデータベースにのみ適用されます。",
|
||||
defaultTransactionModeAuto: "自動コミット",
|
||||
defaultTransactionModeManual: "手動トランザクション",
|
||||
keepExplicitTransactionInAutoCommit: "自動コミットでも明示的なトランザクションを保持",
|
||||
keepExplicitTransactionInAutoCommitDescription:
|
||||
"自動コミットのタブで BEGIN / START TRANSACTION を実行した後、COMMIT または ROLLBACK を実行するまでトランザクションを保持します。オフの場合、実行が終わるたびに未確定の明示的トランザクションがロールバックされ、残留トランザクションがタブの読み取りスナップショットを固定するのを防ぎます。",
|
||||
executeAllOnBlankLine: "空行ではすべてのSQLを実行",
|
||||
executeAllOnBlankLineDescription: "現在の文を実行するモードで、カーソル位置に文がない場合はすべてのSQLを実行します。既定では無効です。",
|
||||
showExecutionTargetPicker: "実行対象ピッカーを表示",
|
||||
|
||||
@@ -379,6 +379,8 @@ export default withEnglishFallback({
|
||||
commit: "커밋",
|
||||
rollback: "롤백",
|
||||
txnAutoRolledBack: "5분간 활동이 없어 트랜잭션이 자동으로 롤백되었습니다",
|
||||
autoCommitTxnRolledBack: '이 탭에 남아 있던 명시적 트랜잭션이 자동으로 롤백되었습니다. 실행 간에 유지하려면 설정에서 "자동 커밋 모드에서 명시적 트랜잭션 유지"를 켜세요',
|
||||
autoCommitSessionTxnRolledBack: "이 연결은 autocommit=0 상태이므로 실행 후 암시적 트랜잭션이 롤백되었습니다(같은 연결에서는 한 번만 알립니다)",
|
||||
previewSql: "SQL 미리보기",
|
||||
previewQuery: "쿼리 미리보기",
|
||||
hidePreviewSql: "SQL 미리보기 숨기기",
|
||||
@@ -7119,6 +7121,8 @@ export default withEnglishFallback({
|
||||
'새 SQL 편집기를 열 때 기본적으로 사용되는 트랜잭션 모드입니다. "수동 트랜잭션"을 선택하면 새 편집기가 수동 트랜잭션 모드로 시작되어 수동으로 커밋 또는 롤백해야 합니다. "자동 커밋"을 선택하면 각 문이 자동으로 커밋됩니다. 트랜잭션을 지원하는 데이터베이스에만 적용됩니다.',
|
||||
defaultTransactionModeAuto: "자동 커밋",
|
||||
defaultTransactionModeManual: "수동 트랜잭션",
|
||||
keepExplicitTransactionInAutoCommit: "자동 커밋 모드에서 명시적 트랜잭션 유지",
|
||||
keepExplicitTransactionInAutoCommitDescription: "자동 커밋 탭에서 BEGIN / START TRANSACTION을 실행하면 COMMIT 또는 ROLLBACK을 실행할 때까지 트랜잭션을 유지합니다. 끄면 실행이 끝날 때마다 커밋되지 않은 명시적 트랜잭션이 롤백되어, 남은 트랜잭션이 탭의 읽기 스냅샷을 고정하는 것을 막습니다.",
|
||||
executeAllOnBlankLine: "빈 줄에서 모든 SQL 실행",
|
||||
executeAllOnBlankLineDescription: "현재 구문 실행 모드에서 커서 위치에 구문이 없으면 모든 SQL을 실행합니다. 기본적으로 비활성화됩니다.",
|
||||
showExecutionTargetPicker: "실행 대상 선택기 표시",
|
||||
|
||||
@@ -371,6 +371,8 @@ export default withEnglishFallback({
|
||||
commit: "Confirmar",
|
||||
rollback: "Reverter",
|
||||
txnAutoRolledBack: "Transação revertida automaticamente após 5 minutos de inatividade",
|
||||
autoCommitTxnRolledBack: 'A transação explícita deixada aberta por esta aba foi revertida automaticamente; ative "Manter transações explícitas no autocommit" para mantê-la entre execuções',
|
||||
autoCommitSessionTxnRolledBack: "Esta conexão está com o autocommit desativado (SET autocommit = 0), então sua transação implícita foi revertida após a execução; o DBX avisa uma vez por conexão",
|
||||
previewSql: "Visualizar SQL",
|
||||
previewQuery: "Visualizar consulta",
|
||||
hidePreviewSql: "Ocultar Visualização SQL",
|
||||
@@ -7335,6 +7337,9 @@ export default withEnglishFallback({
|
||||
'O modo de transação usado por padrão ao abrir um novo editor SQL. Com "Transação manual", novos editores iniciam no modo de transação manual, exigindo confirmação ou reversão manual; com "Confirmação automática", cada instrução é confirmada automaticamente. Aplica-se apenas a bancos de dados que oferecem suporte a transações.',
|
||||
defaultTransactionModeAuto: "Confirmação automática",
|
||||
defaultTransactionModeManual: "Transação manual",
|
||||
keepExplicitTransactionInAutoCommit: "Manter transações explícitas no autocommit",
|
||||
keepExplicitTransactionInAutoCommitDescription:
|
||||
"Depois de executar BEGIN / START TRANSACTION em uma aba de autocommit, a transação permanece aberta até você executar COMMIT ou ROLLBACK. Quando desativado, cada execução reverte a transação explícita deixada aberta, evitando que uma transação residual fixe o snapshot de leitura da aba.",
|
||||
executeAllOnBlankLine: "Executar todo o SQL em linhas em branco",
|
||||
executeAllOnBlankLineDescription: "No modo de instrução atual, executa todo o SQL quando não há instrução no cursor. Desativado por padrão.",
|
||||
showExecutionTargetPicker: "Mostrar seletor de destino",
|
||||
|
||||
@@ -216,6 +216,8 @@ export default withEnglishFallback({
|
||||
commit: "Commit",
|
||||
rollback: "Rollback",
|
||||
txnAutoRolledBack: "İşlem, 5 dakikalık hareketsizlik sonrası otomatik olarak geri alındı",
|
||||
autoCommitTxnRolledBack: 'Bu sekmenin açık bıraktığı açık işlem otomatik olarak geri alındı; yürütmeler arasında korumak için ayarlardan "Otomatik işlemede açık işlemleri koru" seçeneğini açın',
|
||||
autoCommitSessionTxnRolledBack: "Bu bağlantı autocommit kapalı (SET autocommit = 0) çalışıyor; bu nedenle örtük işlem yürütmeden sonra geri alındı. DBX bunu bağlantı başına bir kez bildirir",
|
||||
previewSql: "SQL Önizleme",
|
||||
previewQuery: "Sorguyu önizle",
|
||||
hidePreviewSql: "SQL Önizlemesini Gizle",
|
||||
@@ -7376,6 +7378,9 @@ export default withEnglishFallback({
|
||||
'Yeni bir SQL düzenleyici açıldığında varsayılan olarak kullanılan işlem modu. "Manuel işlem" ile yeni düzenleyiciler manuel işlem modunda başlar ve elle commit ya da rollback gerektirir; "Otomatik commit" ile her ifade otomatik olarak uygulanır. Yalnızca işlemleri destekleyen veritabanları için geçerlidir.',
|
||||
defaultTransactionModeAuto: "Otomatik commit",
|
||||
defaultTransactionModeManual: "Manuel işlem",
|
||||
keepExplicitTransactionInAutoCommit: "Otomatik işlemede açık işlemleri koru",
|
||||
keepExplicitTransactionInAutoCommitDescription:
|
||||
"Otomatik işlem (autocommit) sekmesinde BEGIN / START TRANSACTION çalıştırdıktan sonra işlem, siz COMMIT veya ROLLBACK çalıştırana kadar açık kalır. Kapalıyken her yürütme, açık kalan açık işlemi geri alır; böylece kalan bir işlemin sekmenin okuma anlık görüntüsünü sabitlemesi engellenir.",
|
||||
executeAllOnBlankLine: "Boş satırlarda tüm SQL'i çalıştır",
|
||||
executeAllOnBlankLineDescription: "Geçerli ifade modunda, imleçte bir ifade yoksa tüm SQL'i çalıştır. Varsayılan olarak kapalıdır.",
|
||||
showExecutionTargetPicker: "Yürütme hedefi seçicisini göster",
|
||||
|
||||
@@ -296,6 +296,8 @@ export default withEnglishFallback({
|
||||
commit: "提交",
|
||||
rollback: "回滚",
|
||||
txnAutoRolledBack: "事务已因 5 分钟无操作而自动回滚",
|
||||
autoCommitTxnRolledBack: "该标签页遗留的显式事务已被自动回滚;如需跨多次执行保留事务,可在设置中开启「自动提交模式下保留显式事务」",
|
||||
autoCommitSessionTxnRolledBack: "该连接处于 autocommit=0,本次执行留下的隐式事务已被自动回滚;同一连接只提示一次",
|
||||
previewSql: "预览 SQL",
|
||||
previewQuery: "预览查询",
|
||||
hidePreviewSql: "隐藏 SQL 预览",
|
||||
@@ -7948,6 +7950,8 @@ export default withEnglishFallback({
|
||||
defaultTransactionModeDescription: "新建 SQL 编辑器时默认采用的事务提交方式。选择“手动事务”后,新打开的编辑器将以手动事务模式开始,需手动提交或回滚;选择“自动提交”则每条语句自动提交。仅对支持事务的数据库生效。",
|
||||
defaultTransactionModeAuto: "自动提交",
|
||||
defaultTransactionModeManual: "手动事务",
|
||||
keepExplicitTransactionInAutoCommit: "自动提交模式下保留显式事务",
|
||||
keepExplicitTransactionInAutoCommitDescription: "在自动提交标签页执行 BEGIN / START TRANSACTION 后,事务会一直保留到你执行 COMMIT 或 ROLLBACK。关闭时,每次执行结束都会自动回滚未提交的显式事务,以避免遗留事务把该标签页的读取快照钉住。",
|
||||
executeAllOnBlankLine: "空白位置执行全部 SQL",
|
||||
executeAllOnBlankLineDescription: "在“执行光标所在语句”模式下,光标没有对应语句时执行全部 SQL。默认关闭。",
|
||||
showExecutionTargetPicker: "显示执行目标选择器",
|
||||
|
||||
@@ -374,6 +374,8 @@ export default withEnglishFallback({
|
||||
commit: "提交",
|
||||
rollback: "回溯",
|
||||
txnAutoRolledBack: "事務已因 5 分鐘無操作而自動回溯",
|
||||
autoCommitTxnRolledBack: "該分頁遺留的顯式交易已被自動回溯;如需跨多次執行保留交易,可在設定中開啟「自動提交模式下保留顯式交易」",
|
||||
autoCommitSessionTxnRolledBack: "該連線處於 autocommit=0,本次執行留下的隱式交易已被自動回溯;同一連線只提示一次",
|
||||
previewSql: "預覽 SQL",
|
||||
previewQuery: "預覽查詢",
|
||||
hidePreviewSql: "隱藏 SQL 預覽",
|
||||
@@ -6650,6 +6652,8 @@ export default withEnglishFallback({
|
||||
defaultTransactionModeDescription: "新增 SQL 編輯器時預設採用的交易提交方式。選擇「手動交易」後,新開啟的編輯器將以手動交易模式開始,需手動提交或回滾;選擇「自動提交」則每條語句自動提交。僅對支援交易的資料庫生效。",
|
||||
defaultTransactionModeAuto: "自動提交",
|
||||
defaultTransactionModeManual: "手動交易",
|
||||
keepExplicitTransactionInAutoCommit: "自動提交模式下保留顯式交易",
|
||||
keepExplicitTransactionInAutoCommitDescription: "在自動提交分頁執行 BEGIN / START TRANSACTION 後,交易會保留到你執行 COMMIT 或 ROLLBACK。關閉時,每次執行結束都會自動回溯未提交的顯式交易,以避免遺留交易把該分頁的讀取快照固定住。",
|
||||
executeAllOnBlankLine: "空白位置執行全部 SQL",
|
||||
executeAllOnBlankLineDescription: "在「執行游標所在語句」模式下,游標沒有對應語句時執行全部 SQL。預設關閉。",
|
||||
showExecutionTargetPicker: "顯示執行目標選擇器",
|
||||
|
||||
@@ -1382,6 +1382,10 @@ export async function executeMulti(
|
||||
useTransaction?: boolean;
|
||||
continueOnError?: boolean;
|
||||
executionMode?: "simple";
|
||||
/** MySQL auto-commit tabs: keep a transaction the user opened explicitly
|
||||
* (`BEGIN` / `START TRANSACTION`) open across executions until COMMIT /
|
||||
* ROLLBACK instead of rolling it back when the batch ends. */
|
||||
preserveExplicitTransaction?: boolean;
|
||||
},
|
||||
): Promise<QueryResult[]> {
|
||||
return postQueryWithDiagnostics(
|
||||
@@ -1430,6 +1434,7 @@ export async function executeMultiWithProgress(
|
||||
useTransaction?: boolean;
|
||||
continueOnError?: boolean;
|
||||
executionMode?: "simple";
|
||||
preserveExplicitTransaction?: boolean;
|
||||
executionId?: string;
|
||||
},
|
||||
): Promise<QueryResult[]> {
|
||||
|
||||
@@ -1618,6 +1618,10 @@ export async function executeMulti(
|
||||
useTransaction?: boolean;
|
||||
continueOnError?: boolean;
|
||||
executionMode?: "simple";
|
||||
/** MySQL auto-commit tabs: keep a transaction the user opened explicitly
|
||||
* (`BEGIN` / `START TRANSACTION`) open across executions until COMMIT /
|
||||
* ROLLBACK instead of rolling it back when the batch ends. */
|
||||
preserveExplicitTransaction?: boolean;
|
||||
},
|
||||
): Promise<QueryResult[]> {
|
||||
const diagnosticsEnabled = isDebugLoggingEnabled();
|
||||
@@ -1684,6 +1688,7 @@ export async function executeMultiWithProgress(
|
||||
useTransaction?: boolean;
|
||||
continueOnError?: boolean;
|
||||
executionMode?: "simple";
|
||||
preserveExplicitTransaction?: boolean;
|
||||
executionId?: string;
|
||||
},
|
||||
): Promise<QueryResult[]> {
|
||||
|
||||
@@ -129,6 +129,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"clickTableNavigationTarget",
|
||||
"completionTriggerMode",
|
||||
"defaultTransactionMode",
|
||||
"keepExplicitTransactionInAutoCommit",
|
||||
] as const satisfies readonly (keyof EditorSettings)[];
|
||||
|
||||
export type EditorSettingsDraftKey = (typeof EDITOR_SETTINGS_DRAFT_KEYS)[number];
|
||||
|
||||
@@ -75,6 +75,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
|
||||
editor: [
|
||||
"executeMode",
|
||||
"defaultTransactionMode",
|
||||
"keepExplicitTransactionInAutoCommit",
|
||||
"executeAllOnBlankLine",
|
||||
"showExecutionTargetPicker",
|
||||
"showStatementRunButtons",
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
analyzeEditableQueryEditability: vi.fn(),
|
||||
beginManualTransaction: vi.fn(),
|
||||
closeClientConnectionSession: vi.fn(),
|
||||
closeQuerySession: vi.fn(),
|
||||
commitManualTransaction: vi.fn(),
|
||||
executeInManualTransaction: vi.fn(),
|
||||
executeMulti: vi.fn(),
|
||||
getConnectionConfig: vi.fn(),
|
||||
prepareQueryPaginationExecutionPlan: vi.fn(),
|
||||
rollbackManualTransaction: vi.fn(),
|
||||
saveOpenTabsState: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
analyzeEditableQueryEditability: mocks.analyzeEditableQueryEditability,
|
||||
beginManualTransaction: mocks.beginManualTransaction,
|
||||
closeClientConnectionSession: mocks.closeClientConnectionSession,
|
||||
closeQuerySession: mocks.closeQuerySession,
|
||||
commitManualTransaction: mocks.commitManualTransaction,
|
||||
executeInManualTransaction: mocks.executeInManualTransaction,
|
||||
executeMulti: mocks.executeMulti,
|
||||
prepareQueryPaginationExecutionPlan: mocks.prepareQueryPaginationExecutionPlan,
|
||||
rollbackManualTransaction: mocks.rollbackManualTransaction,
|
||||
saveOpenTabsState: mocks.saveOpenTabsState,
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
useConnectionStore: () => ({
|
||||
ensureConnected: vi.fn().mockResolvedValue(undefined),
|
||||
getConfig: mocks.getConnectionConfig,
|
||||
recordConnectionLostError: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/settingsStore", () => ({
|
||||
useSettingsStore: () => ({
|
||||
editorSettings: {
|
||||
autoCalculateTotalRows: false,
|
||||
continueOnErrorOnBatch: false,
|
||||
pageSize: 100,
|
||||
queryResultMaxRowsEnabled: false,
|
||||
queryResultMaxRows: 1000,
|
||||
openTabsRestoreMode: "all",
|
||||
confirmUnsavedSqlClose: false,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: vi.fn((key: string) => data.get(key) ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => data.set(key, value)),
|
||||
removeItem: vi.fn((key: string) => data.delete(key)),
|
||||
});
|
||||
}
|
||||
|
||||
type AutoCommitRolledBackTab = {
|
||||
autoCommitOpenTransaction?: boolean;
|
||||
autoCommitTxnRolledBack?: boolean;
|
||||
autoCommitSessionTxnRolledBack?: boolean;
|
||||
autoCommitSessionTxnRolledBackNotified?: boolean;
|
||||
};
|
||||
|
||||
function emptyTab(): AutoCommitRolledBackTab {
|
||||
return {};
|
||||
}
|
||||
|
||||
describe("applyAutoCommitTransactionReport", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("mirrors the open-transaction marker the backend reported", async () => {
|
||||
const { applyAutoCommitTransactionReport } = await import("@/stores/queryStore");
|
||||
const tab = emptyTab();
|
||||
applyAutoCommitTransactionReport(tab as never, [{ auto_commit_open_transaction: true }] as never);
|
||||
expect(tab.autoCommitOpenTransaction).toBe(true);
|
||||
applyAutoCommitTransactionReport(tab as never, [{ auto_commit_open_transaction: false }] as never);
|
||||
expect(tab.autoCommitOpenTransaction).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps reporting an explicitly opened transaction that was rolled back", async () => {
|
||||
const { applyAutoCommitTransactionReport } = await import("@/stores/queryStore");
|
||||
const tab = emptyTab();
|
||||
applyAutoCommitTransactionReport(tab as never, [{ auto_commit_explicit_transaction_rolled_back: true }] as never);
|
||||
expect(tab.autoCommitTxnRolledBack).toBe(true);
|
||||
|
||||
// The user dismisses the notice; the next execution that rolls back another
|
||||
// explicit transaction must raise it again — the tab lost real work.
|
||||
tab.autoCommitTxnRolledBack = false;
|
||||
applyAutoCommitTransactionReport(tab as never, [{ auto_commit_explicit_transaction_rolled_back: true }] as never);
|
||||
expect(tab.autoCommitTxnRolledBack).toBe(true);
|
||||
});
|
||||
|
||||
it("reports a session-level implicit rollback once per connection", async () => {
|
||||
const { applyAutoCommitTransactionReport } = await import("@/stores/queryStore");
|
||||
const tab = emptyTab();
|
||||
applyAutoCommitTransactionReport(tab as never, [{ auto_commit_session_autocommit_rolled_back: true }] as never);
|
||||
expect(tab.autoCommitSessionTxnRolledBack).toBe(true);
|
||||
expect(tab.autoCommitTxnRolledBack).toBeUndefined();
|
||||
|
||||
// `SET autocommit = 0` rolls back an implicit transaction after *every*
|
||||
// execution; dismissing the notice must not bring it straight back.
|
||||
tab.autoCommitSessionTxnRolledBack = false;
|
||||
applyAutoCommitTransactionReport(tab as never, [{ auto_commit_session_autocommit_rolled_back: true }] as never);
|
||||
expect(tab.autoCommitSessionTxnRolledBack).toBe(false);
|
||||
|
||||
// Auto-commit is back on (or the execution never used the tab connection):
|
||||
// the next auto-commit-off session is reported again.
|
||||
applyAutoCommitTransactionReport(tab as never, [{ auto_commit_open_transaction: false }] as never);
|
||||
applyAutoCommitTransactionReport(tab as never, [{ auto_commit_session_autocommit_rolled_back: true }] as never);
|
||||
expect(tab.autoCommitSessionTxnRolledBack).toBe(true);
|
||||
});
|
||||
|
||||
it("prefers the explicit notice when a batch carries both markers", async () => {
|
||||
const { applyAutoCommitTransactionReport } = await import("@/stores/queryStore");
|
||||
const tab = emptyTab();
|
||||
applyAutoCommitTransactionReport(tab as never, [{ auto_commit_explicit_transaction_rolled_back: true, auto_commit_session_autocommit_rolled_back: true }] as never);
|
||||
expect(tab.autoCommitTxnRolledBack).toBe(true);
|
||||
expect(tab.autoCommitSessionTxnRolledBack).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("auto-commit tab transaction actions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
setActivePinia(createPinia());
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
id: "mysql-1",
|
||||
name: "MySQL",
|
||||
db_type: "mysql",
|
||||
database: "dbx",
|
||||
query_timeout_secs: 30,
|
||||
});
|
||||
mocks.prepareQueryPaginationExecutionPlan.mockImplementation(async (options) => ({
|
||||
sqlToExecute: options.sql,
|
||||
pageSql: undefined,
|
||||
pageLimit: undefined,
|
||||
pageOffset: undefined,
|
||||
countSql: undefined,
|
||||
useAgentResultSession: false,
|
||||
}));
|
||||
mocks.analyzeEditableQueryEditability.mockResolvedValue({ editable: false, reason: "not-select" });
|
||||
mocks.saveOpenTabsState.mockResolvedValue(undefined);
|
||||
mocks.executeMulti.mockResolvedValue({
|
||||
results: [{ columns: [], rows: [], affected_rows: 1, execution_time_ms: 1, auto_commit_open_transaction: false }],
|
||||
});
|
||||
});
|
||||
|
||||
async function setupAutoCommitTabWithKeptTransaction() {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("mysql-1", "dbx", "Query", "query");
|
||||
const tab = store.tabs.find((item) => item.id === tabId);
|
||||
if (!tab) throw new Error("tab missing");
|
||||
// The backend reported a kept explicit transaction on this tab connection.
|
||||
tab.autoCommitOpenTransaction = true;
|
||||
tab.autoCommitSessionTxnRolledBack = true;
|
||||
tab.autoCommitSessionTxnRolledBackNotified = true;
|
||||
return { store, tabId, tab };
|
||||
}
|
||||
|
||||
it("commits a kept auto-commit transaction as an ordinary statement", async () => {
|
||||
const { store, tabId } = await setupAutoCommitTabWithKeptTransaction();
|
||||
await store.commitTransaction(tabId);
|
||||
|
||||
expect(mocks.executeMulti).toHaveBeenCalledOnce();
|
||||
expect(mocks.executeMulti.mock.calls[0][2]).toBe("COMMIT");
|
||||
});
|
||||
|
||||
it("rolls back a kept auto-commit transaction as an ordinary statement", async () => {
|
||||
const { store, tabId } = await setupAutoCommitTabWithKeptTransaction();
|
||||
await store.rollbackTransaction(tabId);
|
||||
|
||||
expect(mocks.executeMulti).toHaveBeenCalledOnce();
|
||||
expect(mocks.executeMulti.mock.calls[0][2]).toBe("ROLLBACK");
|
||||
});
|
||||
|
||||
it("drops the session-level notice when the tab switches connection", async () => {
|
||||
const { store, tabId, tab } = await setupAutoCommitTabWithKeptTransaction();
|
||||
store.updateConnection(tabId, "mysql-2", "dbx");
|
||||
|
||||
expect(tab.autoCommitSessionTxnRolledBack).toBeUndefined();
|
||||
expect(tab.autoCommitSessionTxnRolledBackNotified).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -904,6 +904,39 @@ function getI18nT() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Mirrors the MySQL auto-commit settlement the backend reported for this
|
||||
* execution onto the tab. Absent markers (non-MySQL connections, executions
|
||||
* that never touched a tab connection) leave the previous state untouched.
|
||||
*
|
||||
* The two rollback notices are deliberately different: a `BEGIN` the user
|
||||
* typed is reported on every execution (the tab just lost real work), while a
|
||||
* session sitting on `SET autocommit = 0` has an implicit transaction rolled
|
||||
* back after *every* execution — that one is raised once per connection and
|
||||
* re-armed only after the connection stops reporting it, so dismissing it
|
||||
* does not bring it back on the next keystroke batch. */
|
||||
export function applyAutoCommitTransactionReport(tab: QueryTab, results: QueryResult[]) {
|
||||
const openTransaction = results.find((result) => result.auto_commit_open_transaction !== undefined)?.auto_commit_open_transaction;
|
||||
if (openTransaction !== undefined) tab.autoCommitOpenTransaction = openTransaction;
|
||||
const explicitRolledBack = results.some((result) => result.auto_commit_explicit_transaction_rolled_back === true);
|
||||
const sessionRolledBack = results.some((result) => result.auto_commit_session_autocommit_rolled_back === true);
|
||||
if (explicitRolledBack) {
|
||||
tab.autoCommitTxnRolledBack = true;
|
||||
tab.autoCommitSessionTxnRolledBackNotified = false;
|
||||
return;
|
||||
}
|
||||
if (sessionRolledBack) {
|
||||
if (!tab.autoCommitSessionTxnRolledBackNotified) {
|
||||
tab.autoCommitSessionTxnRolledBack = true;
|
||||
tab.autoCommitSessionTxnRolledBackNotified = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// This execution rolled nothing back: the auto-commit-off session may be gone
|
||||
// (or the execution never used the tab connection), so re-arm the notice for
|
||||
// the next time it happens.
|
||||
tab.autoCommitSessionTxnRolledBackNotified = false;
|
||||
}
|
||||
|
||||
export const useQueryStore = defineStore("query", () => {
|
||||
const redisMonitors = new Map<string, () => void>();
|
||||
const t = getI18nT();
|
||||
@@ -4881,6 +4914,16 @@ export const useQueryStore = defineStore("query", () => {
|
||||
if (tab.txnPossiblyDirty !== undefined) tab.txnPossiblyDirty = false;
|
||||
}
|
||||
|
||||
/** Auto-commit tabs mirror the backend's report of an open explicit
|
||||
* transaction. The flag is dropped whenever the tab stops pointing at the
|
||||
* connection that reported it (target switch, tab close), so a stale badge
|
||||
* can never outlive the session it describes. */
|
||||
function clearAutoCommitOpenTransaction(tab: { autoCommitOpenTransaction?: boolean; autoCommitSessionTxnRolledBack?: boolean; autoCommitSessionTxnRolledBackNotified?: boolean }) {
|
||||
if (tab.autoCommitOpenTransaction !== undefined) tab.autoCommitOpenTransaction = false;
|
||||
tab.autoCommitSessionTxnRolledBack = undefined;
|
||||
tab.autoCommitSessionTxnRolledBackNotified = undefined;
|
||||
}
|
||||
|
||||
/** Centralized manual-session cleanup. Clears every field tied to a manual
|
||||
* transaction session exactly when that session is conclusively ended or
|
||||
* discarded. Callers must not assign these fields individually. */
|
||||
@@ -4903,11 +4946,19 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
clearTxnPossiblyDirty(tab);
|
||||
tab.txnAutoRolledBack = false;
|
||||
clearAutoCommitOpenTransaction(tab);
|
||||
}
|
||||
|
||||
async function commitTransaction(id: string) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab?.txnSessionId) return;
|
||||
if (!tab) return;
|
||||
if (!tab.txnSessionId) {
|
||||
// Auto-commit tab (`Tx:A`) that keeps explicit user transactions: the
|
||||
// transaction lives on the tab's own connection, so COMMIT is an ordinary
|
||||
// statement on that connection.
|
||||
if (tab.autoCommitOpenTransaction) await executeCurrentSql("COMMIT", { tabId: tab.id });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.commitManualTransaction(tab.txnSessionId);
|
||||
} finally {
|
||||
@@ -4917,7 +4968,11 @@ export const useQueryStore = defineStore("query", () => {
|
||||
|
||||
async function rollbackTransaction(id: string) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab?.txnSessionId) return;
|
||||
if (!tab) return;
|
||||
if (!tab.txnSessionId) {
|
||||
if (tab.autoCommitOpenTransaction) await executeCurrentSql("ROLLBACK", { tabId: tab.id });
|
||||
return;
|
||||
}
|
||||
const sessionId = tab.txnSessionId;
|
||||
// Remove the old session before the backend responds: a target switch may
|
||||
// start a new transaction while this rollback is still in flight.
|
||||
@@ -7343,6 +7398,8 @@ export const useQueryStore = defineStore("query", () => {
|
||||
timeoutSecs: queryTimeoutSecs,
|
||||
catalog: executionCatalog,
|
||||
continueOnError: continueOnBatchError,
|
||||
// MySQL-family connections only use this; other drivers ignore it.
|
||||
...(settingsStore.editorSettings.keepExplicitTransactionInAutoCommit ? { preserveExplicitTransaction: true } : {}),
|
||||
};
|
||||
queryExecutionLog("info", "execute-multi:invoke", {
|
||||
traceId,
|
||||
@@ -7527,6 +7584,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
console.warn("[DBX] Failed to resolve SAP HANA CURRENT_SCHEMA", error);
|
||||
}
|
||||
}
|
||||
if (tab.autoCommit !== false) applyAutoCommitTransactionReport(tab, results);
|
||||
const current = findExecutionTab(id);
|
||||
if (current?.executionId === executionId && manualTransactionTargetEpoch(current) === executionTargetEpoch) {
|
||||
if (captureResultRun && current.isCancelling && restorePendingResultRun(current, executionId)) return false;
|
||||
|
||||
@@ -945,6 +945,12 @@ export interface EditorSettings {
|
||||
clickTableNavigationTarget: ClickTableNavigationTarget;
|
||||
completionTriggerMode: SqlCompletionTriggerMode;
|
||||
defaultTransactionMode: DefaultTransactionMode;
|
||||
/** Auto-commit (`Tx:A`) tabs with a MySQL-family connection: keep a
|
||||
* transaction the user opens explicitly (`BEGIN` / `START TRANSACTION`) open
|
||||
* across executions until COMMIT / ROLLBACK instead of rolling it back when
|
||||
* each execution ends. Off by default: the rollback is what stops a leftover
|
||||
* transaction from pinning the tab's read snapshot (#9479). */
|
||||
keepExplicitTransactionInAutoCommit: boolean;
|
||||
}
|
||||
|
||||
export interface ToolbarItems {
|
||||
@@ -1202,6 +1208,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
clickTableNavigationTarget: "data",
|
||||
completionTriggerMode: "positional",
|
||||
defaultTransactionMode: "auto",
|
||||
keepExplicitTransactionInAutoCommit: false,
|
||||
};
|
||||
|
||||
export const STORAGE_KEY = "dbx-editor-settings";
|
||||
@@ -1790,6 +1797,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
clickTableNavigationTarget: normalizeClickTableNavigationTarget(settings.clickTableNavigationTarget),
|
||||
completionTriggerMode: normalizeCompletionTriggerMode(settings.completionTriggerMode),
|
||||
defaultTransactionMode: normalizeDefaultTransactionMode(settings.defaultTransactionMode),
|
||||
keepExplicitTransactionInAutoCommit: settings.keepExplicitTransactionInAutoCommit === true,
|
||||
backgroundImage: normalizeBackgroundImageSettings(settings.backgroundImage),
|
||||
};
|
||||
}
|
||||
@@ -2542,6 +2550,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.clickTableNavigationTarget !== undefined) editorSettings.value.clickTableNavigationTarget = normalizeClickTableNavigationTarget(partial.clickTableNavigationTarget);
|
||||
if (partial.completionTriggerMode !== undefined) editorSettings.value.completionTriggerMode = normalizeCompletionTriggerMode(partial.completionTriggerMode);
|
||||
if (partial.defaultTransactionMode !== undefined) editorSettings.value.defaultTransactionMode = normalizeDefaultTransactionMode(partial.defaultTransactionMode);
|
||||
if (partial.keepExplicitTransactionInAutoCommit !== undefined) editorSettings.value.keepExplicitTransactionInAutoCommit = partial.keepExplicitTransactionInAutoCommit === true;
|
||||
if (partial.flatteningMultiLineText !== undefined) editorSettings.value.flatteningMultiLineText = partial.flatteningMultiLineText;
|
||||
if (partial.dataGridShowWhitespace !== undefined) editorSettings.value.dataGridShowWhitespace = partial.dataGridShowWhitespace;
|
||||
}
|
||||
|
||||
@@ -1118,6 +1118,21 @@ export interface QueryResult {
|
||||
/** Manual-transaction UX marker for the same dialects: set on the synthetic
|
||||
* successful result of an empty/whitespace/comments-only manual script. */
|
||||
manual_transaction_no_statement?: true;
|
||||
/** MySQL auto-commit tab session state reported by the backend for this
|
||||
* execution: true = the tab connection still holds a transaction the user
|
||||
* opened explicitly (`BEGIN` / `START TRANSACTION`) and DBX kept it open;
|
||||
* false = the backend settled the connection and no such transaction is
|
||||
* open. Absent when the execution never observed a tab-scoped MySQL
|
||||
* connection, so the tab must keep its previous state. */
|
||||
auto_commit_open_transaction?: boolean;
|
||||
/** MySQL auto-commit tab: the backend rolled back a transaction the user
|
||||
* opened explicitly and left open (the tab did not opt into keeping them). */
|
||||
auto_commit_explicit_transaction_rolled_back?: true;
|
||||
/** MySQL auto-commit tab: the backend rolled back a transaction the session
|
||||
* opened implicitly because auto-commit was off (`SET autocommit = 0`).
|
||||
* Nobody typed `BEGIN`, so the tab reports it separately — and only once per
|
||||
* connection instead of after every execution. */
|
||||
auto_commit_session_autocommit_rolled_back?: true;
|
||||
/** Structured backend error; authoritative when execution_error is true. */
|
||||
error?: BackendError;
|
||||
/** Zero-based index of the submitted statement that produced this result. */
|
||||
@@ -2004,6 +2019,25 @@ export interface QueryTab {
|
||||
* statement DBX cannot prove read-only. Commit/Rollback actions are hidden
|
||||
* while a session is clean. Never cleared by a later read. */
|
||||
txnPossiblyDirty?: boolean;
|
||||
/** Auto-commit tabs (`Tx:A`) with a MySQL-family connection: whether the tab's
|
||||
* connection currently holds a transaction the user opened explicitly
|
||||
* (`BEGIN` / `START TRANSACTION`). The backend reports it on every execution
|
||||
* that observed the connection; the tab mirrors it into the `Tx` badge and
|
||||
* the commit/rollback actions. Not persisted. */
|
||||
autoCommitOpenTransaction?: boolean;
|
||||
/** Auto-commit tab: show the notice that the backend rolled back an explicit
|
||||
* transaction this tab left open, so the cleanup is never silent. */
|
||||
autoCommitTxnRolledBack?: boolean;
|
||||
/** Same cleanup, but the rolled-back transaction came from a session with
|
||||
* auto-commit turned off (`SET autocommit = 0`) rather than from a `BEGIN`
|
||||
* the user typed. Shown with its own wording so the notice is not mistaken
|
||||
* for a lost explicit transaction. */
|
||||
autoCommitSessionTxnRolledBack?: boolean;
|
||||
/** Dedupe marker for {@link autoCommitSessionTxnRolledBack}: an
|
||||
* auto-commit-off session rolls back an implicit transaction after *every*
|
||||
* execution, so the notice is raised once and re-armed only after the
|
||||
* connection stops reporting that rollback. */
|
||||
autoCommitSessionTxnRolledBackNotified?: boolean;
|
||||
}
|
||||
|
||||
export interface SavedSqlFolder {
|
||||
|
||||
Reference in new issue
Block a user