From fdfad88590d1379a091f148fca6bde8051021c98 Mon Sep 17 00:00:00 2001 From: Abeautifulsnow Date: Fri, 28 Aug 2026 12:37:11 +0800 Subject: [PATCH] feat(editor): add global default transaction commit mode --- .../editor/EditorSettingsDialog.vue | 27 +++++ apps/desktop/src/i18n/locales/en.ts | 5 + apps/desktop/src/i18n/locales/es.ts | 5 + apps/desktop/src/i18n/locales/it.ts | 5 + apps/desktop/src/i18n/locales/ja.ts | 5 + apps/desktop/src/i18n/locales/ko.ts | 5 + apps/desktop/src/i18n/locales/pt-BR.ts | 5 + apps/desktop/src/i18n/locales/zh-CN.ts | 4 + apps/desktop/src/i18n/locales/zh-TW.ts | 4 + .../database/databaseFeatureSupport.spec.ts | 21 +++- .../lib/database/databaseFeatureSupport.ts | 15 ++- .../src/lib/settings/editorSettingsDraft.ts | 1 + .../src/lib/settings/settingsSearch.ts | 1 + .../queryStore.defaultTransactionMode.spec.ts | 105 ++++++++++++++++++ .../stores/__tests__/settingsStore.spec.ts | 6 + apps/desktop/src/stores/queryStore.ts | 15 ++- apps/desktop/src/stores/settingsStore.ts | 9 ++ 17 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 apps/desktop/src/stores/__tests__/queryStore.defaultTransactionMode.spec.ts diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 5923fbca5f..d0af3bdf82 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -351,6 +351,7 @@ const editActiveDataGridTypeColorSchemeId = ref(settingsStore.editorSettings.act const showThemeCustomizer = ref(false); const showDataGridTypeColorScheme = ref(false); const editExecuteMode = ref(settingsStore.editorSettings.executeMode); +const editDefaultTransactionMode = ref(settingsStore.editorSettings.defaultTransactionMode); const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts)); function translateWithExecuteShortcut(key: string): string { return t(key, { shortcut: formatShortcutDisplay(editShortcuts.value.executeSql) }); @@ -573,6 +574,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { customThemes: editCustomThemes.value, activeCustomThemeId: editActiveCustomThemeId.value, executeMode: editExecuteMode.value, + defaultTransactionMode: editDefaultTransactionMode.value, executeAllOnBlankLine: editExecuteAllOnBlankLine.value, showExecutionTargetPicker: editShowExecutionTargetPicker.value, showStatementRunButtons: editShowStatementRunButtons.value, @@ -837,6 +839,7 @@ function syncEditorSettingsDraftFromStore() { editCustomThemes.value = [...settingsStore.editorSettings.customThemes]; editActiveCustomThemeId.value = settingsStore.editorSettings.activeCustomThemeId; editExecuteMode.value = settingsStore.editorSettings.executeMode; + editDefaultTransactionMode.value = settingsStore.editorSettings.defaultTransactionMode; editExecuteAllOnBlankLine.value = settingsStore.editorSettings.executeAllOnBlankLine; editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker; editShowStatementRunButtons.value = settingsStore.editorSettings.showStatementRunButtons; @@ -1094,6 +1097,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editFontFamily.value = DEFAULT_EDITOR_SETTINGS.fontFamily; editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize; editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode; + editDefaultTransactionMode.value = DEFAULT_EDITOR_SETTINGS.defaultTransactionMode; editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine; editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker; editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons; @@ -1212,6 +1216,7 @@ function resetAllDefaults() { editCustomThemes.value = [...DEFAULT_EDITOR_SETTINGS.customThemes]; editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId; editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode; + editDefaultTransactionMode.value = DEFAULT_EDITOR_SETTINGS.defaultTransactionMode; editExecuteAllOnBlankLine.value = DEFAULT_EDITOR_SETTINGS.executeAllOnBlankLine; editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker; editShowStatementRunButtons.value = DEFAULT_EDITOR_SETTINGS.showStatementRunButtons; @@ -1429,6 +1434,10 @@ function onExecuteModeChange(v: any) { if (v === "all" || v === "current") editExecuteMode.value = v; } +function onDefaultTransactionModeChange(v: any) { + if (v === "auto" || v === "manual") editDefaultTransactionMode.value = v; +} + function onCompletionTriggerModeChange(v: any) { if (v === "manual" || v === "require-prefix" || v === "positional") { editCompletionTriggerMode.value = v; @@ -4031,6 +4040,24 @@ onUnmounted(() => { +
+
+ +

+ {{ t("settings.defaultTransactionModeDescription") }} +

+
+ +
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 94899fd9ba..4e6327d9c6 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -6515,6 +6515,11 @@ export default { executeModeDescription: "Run SQL shortcut: {shortcut}. Controls whether it executes all SQL or the statement at the cursor by default.", executeModeAll: "Execute all SQL", executeModeCurrent: "Execute statement at cursor", + defaultTransactionMode: "Default transaction commit mode", + defaultTransactionModeDescription: + '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 (PostgreSQL, MySQL, Oracle, JDBC).', + defaultTransactionModeAuto: "Auto commit", + defaultTransactionModeManual: "Manual transaction", 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", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 483cb25ff2..905d91f728 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -6183,6 +6183,11 @@ export default withEnglishFallback({ executeModeDescription: "Atajo para ejecutar SQL: {shortcut}. Controla si se ejecuta todo el SQL o la sentencia en el cursor de forma predeterminada.", executeModeAll: "Ejecutar todo el SQL", executeModeCurrent: "Ejecutar sentencia en el cursor", + defaultTransactionMode: "Modo de confirmación de transacción predeterminado", + defaultTransactionModeDescription: + '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 (PostgreSQL, MySQL, Oracle, JDBC).', + defaultTransactionModeAuto: "Confirmación automática", + defaultTransactionModeManual: "Transacción manual", 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", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index d5b4984e97..f391430dcc 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -6183,6 +6183,11 @@ export default withEnglishFallback({ executeModeDescription: "Scorciatoia Esegui SQL: {shortcut}. Stabilisce se eseguire tutto l'SQL o l'istruzione al cursore per impostazione predefinita.", executeModeAll: "Esegui tutto l'SQL", executeModeCurrent: "Esegui istruzione al cursore", + defaultTransactionMode: "Modalità di commit transazione predefinita", + defaultTransactionModeDescription: + '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 (PostgreSQL, MySQL, Oracle, JDBC).', + defaultTransactionModeAuto: "Commit automatico", + defaultTransactionModeManual: "Transazione manuale", 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", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index d2b810dede..53a13100bd 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -6209,6 +6209,11 @@ export default withEnglishFallback({ executeModeDescription: "SQL実行ショートカット: {shortcut}。既定でSQL全体またはカーソル位置のステートメントのどちらを実行するかを設定します。", executeModeAll: "すべてのSQLを実行", executeModeCurrent: "カーソル位置の文を実行", + defaultTransactionMode: "既定のトランザクションコミットモード", + defaultTransactionModeDescription: + "新しい SQL エディタを開いたときに既定で使用するトランザクションモード。「手動トランザクション」を選択すると、新しいエディタは手動トランザクションモードで開始され、手動でコミットまたはロールバックします。「自動コミット」を選択すると、各ステートメントが自動的にコミットされます。トランザクションをサポートするデータベース(PostgreSQL、MySQL、Oracle、JDBC)のみに適用されます。", + defaultTransactionModeAuto: "自動コミット", + defaultTransactionModeManual: "手動トランザクション", executeAllOnBlankLine: "空行ではすべてのSQLを実行", executeAllOnBlankLineDescription: "現在の文を実行するモードで、カーソル位置に文がない場合はすべてのSQLを実行します。既定では無効です。", showExecutionTargetPicker: "実行対象ピッカーを表示", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 64ebd65ef3..1dab66391d 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -5896,6 +5896,11 @@ export default withEnglishFallback({ executeModeDescription: "SQL 실행 단축키: {shortcut}. 기본적으로 전체 SQL 또는 커서가 있는 구문 중 무엇을 실행할지 설정합니다.", executeModeAll: "모든 SQL 실행", executeModeCurrent: "커서 위치의 구문 실행", + defaultTransactionMode: "기본 트랜잭션 커밋 모드", + defaultTransactionModeDescription: + '새 SQL 편집기를 열 때 기본적으로 사용되는 트랜잭션 모드입니다. "수동 트랜잭션"을 선택하면 새 편집기가 수동 트랜잭션 모드로 시작되어 수동으로 커밋 또는 롤백해야 합니다. "자동 커밋"을 선택하면 각 문이 자동으로 커밋됩니다. 트랜잭션을 지원하는 데이터베이스(PostgreSQL, MySQL, Oracle, JDBC)에만 적용됩니다.', + defaultTransactionModeAuto: "자동 커밋", + defaultTransactionModeManual: "수동 트랜잭션", executeAllOnBlankLine: "빈 줄에서 모든 SQL 실행", executeAllOnBlankLineDescription: "현재 구문 실행 모드에서 커서 위치에 구문이 없으면 모든 SQL을 실행합니다. 기본적으로 비활성화됩니다.", showExecutionTargetPicker: "실행 대상 선택기 표시", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index edb1bc46bf..4e221b1bed 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -6185,6 +6185,11 @@ export default withEnglishFallback({ executeModeDescription: "Atalho Executar SQL: {shortcut}. Controla se todo o SQL ou a instrução no cursor é executada por padrão.", executeModeAll: "Executar todo o SQL", executeModeCurrent: "Executar instrução no cursor", + defaultTransactionMode: "Modo de confirmação de transação padrão", + defaultTransactionModeDescription: + '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 (PostgreSQL, MySQL, Oracle, JDBC).', + defaultTransactionModeAuto: "Confirmação automática", + defaultTransactionModeManual: "Transação manual", 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", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 7ded26586b..3d66fabc17 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -6498,6 +6498,10 @@ export default withEnglishFallback({ executeModeDescription: "执行 SQL 快捷键:{shortcut}。用于控制默认执行全部 SQL 或光标所在语句。", executeModeAll: "执行全部 SQL", executeModeCurrent: "执行光标所在语句", + defaultTransactionMode: "默认事务提交方式", + defaultTransactionModeDescription: "新建 SQL 编辑器时默认采用的事务提交方式。选择“手动事务”后,新打开的编辑器将以手动事务模式开始,需手动提交或回滚;选择“自动提交”则每条语句自动提交。仅支持事务的数据库(PostgreSQL、MySQL、Oracle、JDBC)生效。", + defaultTransactionModeAuto: "自动提交", + defaultTransactionModeManual: "手动事务", executeAllOnBlankLine: "空白位置执行全部 SQL", executeAllOnBlankLineDescription: "在“执行光标所在语句”模式下,光标没有对应语句时执行全部 SQL。默认关闭。", showExecutionTargetPicker: "显示执行目标选择器", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index a7e27e79a8..440808d429 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -5501,6 +5501,10 @@ export default withEnglishFallback({ executeModeDescription: "執行 SQL 快捷鍵:{shortcut}。用於控制預設執行全部 SQL 或游標所在語句。", executeModeAll: "執行全部 SQL", executeModeCurrent: "執行指標所在語句", + defaultTransactionMode: "預設交易提交方式", + defaultTransactionModeDescription: "新增 SQL 編輯器時預設採用的交易提交方式。選擇「手動交易」後,新開啟的編輯器將以手動交易模式開始,需手動提交或回滾;選擇「自動提交」則每條語句自動提交。僅支援交易的資料庫(PostgreSQL、MySQL、Oracle、JDBC)生效。", + defaultTransactionModeAuto: "自動提交", + defaultTransactionModeManual: "手動交易", executeAllOnBlankLine: "空白位置執行全部 SQL", executeAllOnBlankLineDescription: "在「執行游標所在語句」模式下,游標沒有對應語句時執行全部 SQL。預設關閉。", showExecutionTargetPicker: "顯示執行目標選擇器", diff --git a/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts b/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts index d070182b3d..0cb0df991a 100644 --- a/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/databaseFeatureSupport.spec.ts @@ -129,7 +129,7 @@ describe("supportsTransaction", () => { }); describe("defaultAutoCommitForDbType", () => { - it("defaults query tabs to auto-commit", () => { + it("defaults query tabs to auto-commit unless the user configured manual", () => { expect(defaultAutoCommitForDbType("oceanbase-oracle")).toBe(true); expect(defaultAutoCommitForDbType("oracle")).toBe(true); expect(defaultAutoCommitForDbType("mysql")).toBe(true); @@ -137,6 +137,25 @@ describe("defaultAutoCommitForDbType", () => { expect(defaultAutoCommitForDbType("dameng")).toBe(true); expect(defaultAutoCommitForDbType(undefined)).toBe(true); }); + + it("honors the configured default transaction mode", () => { + expect(defaultAutoCommitForDbType("mysql", "manual")).toBe(false); + expect(defaultAutoCommitForDbType("postgres", "manual")).toBe(false); + expect(defaultAutoCommitForDbType("oracle", "manual")).toBe(false); + expect(defaultAutoCommitForDbType("jdbc", "manual")).toBe(false); + expect(defaultAutoCommitForDbType("mysql", "auto")).toBe(true); + expect(defaultAutoCommitForDbType(undefined, "auto")).toBe(true); + }); + + it("keeps non-transaction databases auto-commit even when manual is configured", () => { + expect(defaultAutoCommitForDbType("redis", "manual")).toBe(true); + expect(defaultAutoCommitForDbType("mongodb", "manual")).toBe(true); + expect(defaultAutoCommitForDbType("sqlite", "manual")).toBe(true); + expect(defaultAutoCommitForDbType("dameng", "manual")).toBe(true); + expect(defaultAutoCommitForDbType("clickhouse", "manual")).toBe(true); + expect(defaultAutoCommitForDbType("oceanbase-oracle", "manual")).toBe(true); + expect(defaultAutoCommitForDbType(undefined, "manual")).toBe(true); + }); }); describe("supportsSqlInListPaste", () => { diff --git a/apps/desktop/src/lib/database/databaseFeatureSupport.ts b/apps/desktop/src/lib/database/databaseFeatureSupport.ts index 385a84a8d0..5218ce16b8 100644 --- a/apps/desktop/src/lib/database/databaseFeatureSupport.ts +++ b/apps/desktop/src/lib/database/databaseFeatureSupport.ts @@ -253,7 +253,18 @@ export function supportsTransaction(dbType?: string): boolean { /** * Default auto-commit mode when opening a query tab for the given database type. * Query tabs default to auto-commit; users can explicitly switch to manual transactions. + * + * When the user has configured `manual` as their default, manual mode only applies to + * databases that actually support explicit transaction control — otherwise the tab is + * forced back to auto-commit so it does not start in an unsupported state. (For + * non-transaction databases, `queryStore` also forces auto-commit on first execution, + * but that would leave the new tab's initial state misleading; gating here avoids that.) + * + * The `defaultMode` selector is the user-configured default (Settings > Editor); + * it is supplied by the caller rather than read here so this module stays free of + * any Pinia store dependency. */ -export function defaultAutoCommitForDbType(_dbType?: string): boolean { - return true; +export function defaultAutoCommitForDbType(dbType: string | undefined, defaultMode: "auto" | "manual" = "auto"): boolean { + if (defaultMode !== "manual") return true; + return !supportsTransaction(dbType); } diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index d0a7a1570f..c91ba25ce9 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -94,6 +94,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "continueOnErrorOnBatch", "clickTableNavigationTarget", "completionTriggerMode", + "defaultTransactionMode", ] as const satisfies readonly (keyof EditorSettings)[]; export type EditorSettingsDraftKey = (typeof EDITOR_SETTINGS_DRAFT_KEYS)[number]; diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index 554ba8d072..19de12fa62 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -106,6 +106,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "editor-theme", category: "editor", titleKey: "settings.theme", targetId: "editor" }, { id: "editor-font-size", category: "editor", titleKey: "settings.fontSize", targetId: "editor" }, { id: "editor-execute-mode", category: "editor", titleKey: "settings.executeMode", targetId: "editor" }, + { id: "editor-default-transaction-mode", category: "editor", titleKey: "settings.defaultTransactionMode", descriptionKey: "settings.defaultTransactionModeDescription", targetId: "editor" }, { id: "editor-execute-all-on-blank-line", category: "editor", titleKey: "settings.executeAllOnBlankLine", descriptionKey: "settings.executeAllOnBlankLineDescription", targetId: "editor" }, { id: "editor-execution-target", category: "editor", titleKey: "settings.showExecutionTargetPicker", descriptionKey: "settings.showExecutionTargetPickerDescription", targetId: "editor" }, { id: "editor-run-buttons", category: "editor", titleKey: "settings.showStatementRunButtons", descriptionKey: "settings.showStatementRunButtonsDescription", targetId: "editor" }, diff --git a/apps/desktop/src/stores/__tests__/queryStore.defaultTransactionMode.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.defaultTransactionMode.spec.ts new file mode 100644 index 0000000000..a073ae1a8a --- /dev/null +++ b/apps/desktop/src/stores/__tests__/queryStore.defaultTransactionMode.spec.ts @@ -0,0 +1,105 @@ +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(), + executeInManualTransaction: vi.fn(), + executeMulti: vi.fn(), + getConnectionConfig: vi.fn(), + prepareQueryPaginationExecutionPlan: vi.fn(), + saveOpenTabsState: vi.fn(), +})); + +vi.mock("@/lib/backend/api", () => ({ + analyzeEditableQueryEditability: mocks.analyzeEditableQueryEditability, + beginManualTransaction: mocks.beginManualTransaction, + closeClientConnectionSession: mocks.closeClientConnectionSession, + closeQuerySession: mocks.closeQuerySession, + executeInManualTransaction: mocks.executeInManualTransaction, + executeMulti: mocks.executeMulti, + prepareQueryPaginationExecutionPlan: mocks.prepareQueryPaginationExecutionPlan, + saveOpenTabsState: mocks.saveOpenTabsState, +})); + +vi.mock("@/stores/connectionStore", () => ({ + useConnectionStore: () => ({ + ensureConnected: vi.fn().mockResolvedValue(undefined), + getConfig: mocks.getConnectionConfig, + recordConnectionLostError: vi.fn(), + }), +})); + +// The in-memory editor settings are mutated per test so a fresh tab reflects the +// configured default transaction mode. Missing key falls back to auto-commit. +const editorSettings: Record = { + autoCalculateTotalRows: false, + continueOnErrorOnBatch: false, + pageSize: 100, + queryResultMaxRowsEnabled: false, + queryResultMaxRows: 1000, + openTabsRestoreMode: "all", + confirmUnsavedSqlClose: false, +}; + +vi.mock("@/stores/settingsStore", () => ({ + useSettingsStore: () => ({ editorSettings }), +})); + +describe("queryStore default transaction mode", () => { + beforeEach(() => { + vi.clearAllMocks(); + setActivePinia(createPinia()); + mocks.getConnectionConfig.mockReturnValue({ + id: "oracle-1", + name: "Oracle", + db_type: "oracle", + database: "ORCL", + 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); + delete editorSettings.defaultTransactionMode; + }); + + it("creates a query tab in manual transaction mode when configured", async () => { + editorSettings.defaultTransactionMode = "manual"; + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("oracle-1", "ORCL", "Query", "query", "APP"); + + const tab = store.tabs.find((item) => item.id === tabId)!; + expect(tab.autoCommit).toBe(false); + }); + + it("creates a query tab in auto-commit mode by default", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("oracle-1", "ORCL", "Query", "query", "APP"); + + const tab = store.tabs.find((item) => item.id === tabId)!; + expect(tab.autoCommit).toBe(true); + }); + + it("creates a query tab in auto-commit mode when explicitly set to auto", async () => { + editorSettings.defaultTransactionMode = "auto"; + + const { useQueryStore } = await import("@/stores/queryStore"); + const store = useQueryStore(); + const tabId = store.createTab("oracle-1", "ORCL", "Query", "query", "APP"); + + const tab = store.tabs.find((item) => item.id === tabId)!; + expect(tab.autoCommit).toBe(true); + }); +}); diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index eed299e8d3..3fa584dd83 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -184,6 +184,12 @@ describe("normalizeEditorSettings", () => { expect(normalizeEditorSettings({ sqlSemanticDiagnosticsEnabled: false } as any).sqlSemanticDiagnosticsMode).toBe("disabled"); }); + it("defaults the transaction commit mode to auto and preserves manual", () => { + expect(normalizeEditorSettings({}).defaultTransactionMode).toBe("auto"); + expect(normalizeEditorSettings({ defaultTransactionMode: "manual" }).defaultTransactionMode).toBe("manual"); + expect(normalizeEditorSettings({ defaultTransactionMode: "bogus" } as any).defaultTransactionMode).toBe("auto"); + }); + it("defaults update downloads to the official source", () => { expect(normalizeEditorSettings({}).updateDownloadSource).toBe("official"); }); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index e925692dac..f0338a7b1e 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -733,6 +733,9 @@ export const useQueryStore = defineStore("query", () => { const t = getI18nT(); const settingsStore = useSettingsStore(); const tabs = ref([]); + // Default auto-commit for a fresh query tab follows the user's global + // "默认事务提交方式" setting (Settings > Editor), not a hard-coded constant. + const defaultAutoCommitForDbTypeWithSetting = (dbType?: string) => defaultAutoCommitForDbType(dbType, settingsStore.editorSettings.defaultTransactionMode); // A stable Set of "connectionId\x00database" keys. Computed only from the // minimal tab identity fields so that it does NOT invalidate when other // properties change (isExecuting, result, sql, tableMeta...). Previously @@ -1655,7 +1658,7 @@ export const useQueryStore = defineStore("query", () => { for (const tab of restored.tabs) { const connection = connectionStore.getConfig(tab.connectionId); if (tab.mode === "query" && tab.autoCommit === undefined) { - tab.autoCommit = defaultAutoCommitForDbType(connection?.db_type); + tab.autoCommit = defaultAutoCommitForDbTypeWithSetting(connection?.db_type); } else if (tab.mode === "data" && connection) { tab.schema = connectionObjectTreeNodeSchema(connection, tab.database, tab.schema); } @@ -1837,7 +1840,7 @@ export const useQueryStore = defineStore("query", () => { isCancelling: false, isExplaining: false, mode, - ...(mode === "query" ? { autoCommit: defaultAutoCommitForDbType(dbType) } : {}), + ...(mode === "query" ? { autoCommit: defaultAutoCommitForDbTypeWithSetting(dbType) } : {}), }; if (mode === "query") tab.originalSql = initialSql ?? ""; const activeIndex = options.insertAfterActive ? tabs.value.findIndex((item) => item.id === activeTabId.value) : -1; @@ -1931,7 +1934,7 @@ export const useQueryStore = defineStore("query", () => { isCancelling: false, isExplaining: false, mode: "query", - autoCommit: defaultAutoCommitForDbType(dbType), + autoCommit: defaultAutoCommitForDbTypeWithSetting(dbType), }; tabs.value.push(tab); activeTabId.value = id; @@ -3286,7 +3289,7 @@ export const useQueryStore = defineStore("query", () => { if (tab.txnSessionId) void rollbackTransaction(tab.id); if (options?.resetAutoCommit) { const dbType = useConnectionStore().getConfig(tab.connectionId)?.db_type; - tab.autoCommit = defaultAutoCommitForDbType(dbType); + tab.autoCommit = defaultAutoCommitForDbTypeWithSetting(dbType); } clearOracleTxnPossiblyDirty(tab); tab.txnAutoRolledBack = false; @@ -3441,7 +3444,7 @@ export const useQueryStore = defineStore("query", () => { isCancelling: false, isExplaining: false, mode: "query", - autoCommit: defaultAutoCommitForDbType(dbType), + autoCommit: defaultAutoCommitForDbTypeWithSetting(dbType), editorSelection: restoredPosition.selection, editorViewport: restoredPosition.viewport, }; @@ -6361,7 +6364,7 @@ export const useQueryStore = defineStore("query", () => { isCancelling: false, isExplaining: false, mode: "query", - autoCommit: defaultAutoCommitForDbType(dbType), + autoCommit: defaultAutoCommitForDbTypeWithSetting(dbType), }; if (!restoreCachedResultPayload(tab, archive.snapshot)) return undefined; const activeRun = tab.resultRuns?.find((run) => run.id === tab.activeResultRunId) ?? tab.resultRuns?.[0]; diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index d9763426df..8a7c90d135 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -61,6 +61,7 @@ export type UpdateDownloadSource = "official" | "cnb"; export type SqlSemanticDiagnosticsMode = "auto" | "enabled" | "disabled"; export type OpenTabsRestoreMode = "all" | "pinned" | "none"; export type AppCloseUnsavedTabsMode = "prompt" | "keep-drafts"; +export type DefaultTransactionMode = "auto" | "manual"; export const DEFAULT_SIDEBAR_TABLE_PAGE_SIZE = 1000; export const DUCKDB_WORKER_MAX_PROCESSES_MIN = 1; @@ -644,6 +645,7 @@ export interface EditorSettings { continueOnErrorOnBatch: boolean; clickTableNavigationTarget: ClickTableNavigationTarget; completionTriggerMode: SqlCompletionTriggerMode; + defaultTransactionMode: DefaultTransactionMode; } export interface ToolbarItems { @@ -857,6 +859,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { continueOnErrorOnBatch: false, clickTableNavigationTarget: "data", completionTriggerMode: "positional", + defaultTransactionMode: "auto", }; export const STORAGE_KEY = "dbx-editor-settings"; @@ -970,6 +973,10 @@ function normalizeClickTableNavigationTarget(value: unknown): ClickTableNavigati return value === "data" || value === "ddl" ? value : DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget; } +function normalizeDefaultTransactionMode(value: unknown): DefaultTransactionMode { + return value === "manual" ? value : DEFAULT_EDITOR_SETTINGS.defaultTransactionMode; +} + function normalizeOpenTabsRestoreMode(value: unknown, legacyRestoreOpenTabsOnLaunch?: unknown): OpenTabsRestoreMode { if (value === "all" || value === "pinned" || value === "none") return value; if (typeof legacyRestoreOpenTabsOnLaunch === "boolean") return legacyRestoreOpenTabsOnLaunch ? "all" : "none"; @@ -1273,6 +1280,7 @@ export function normalizeEditorSettings(settings: Partial, exist continueOnErrorOnBatch: settings.continueOnErrorOnBatch === true, clickTableNavigationTarget: normalizeClickTableNavigationTarget(settings.clickTableNavigationTarget), completionTriggerMode: normalizeCompletionTriggerMode(settings.completionTriggerMode), + defaultTransactionMode: normalizeDefaultTransactionMode(settings.defaultTransactionMode), }; } @@ -1852,6 +1860,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.continueOnErrorOnBatch !== undefined) editorSettings.value.continueOnErrorOnBatch = partial.continueOnErrorOnBatch === true; 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.flatteningMultiLineText !== undefined) editorSettings.value.flatteningMultiLineText = partial.flatteningMultiLineText; }