From fb00f470315162bf986204f0301a0c23bb3e624b Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 10 Jun 2026 20:24:45 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(query-editor):=20=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20DBeaver=20=E9=A3=8E=E6=A0=BC=E4=BA=8B=E5=8A=A1?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QueryEditor.external-sql-save.test.tsx | 59 +++++++++++++++++-- frontend/src/components/QueryEditor.tsx | 24 ++++++-- .../QueryEditorTransactionSettings.tsx | 15 ++--- .../QueryEditorTransactionToolbar.tsx | 10 +++- frontend/src/store.ts | 6 +- 5 files changed, 91 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 25b48db..7e46c2e 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -45,7 +45,7 @@ const storeState = vi.hoisted(() => ({ setQueryOptions: vi.fn(), sqlEditorTransactionOptions: { commitMode: 'manual' as 'manual' | 'auto', - autoCommitDelayMs: 5000, + autoCommitDelayMs: 0, }, setSqlEditorTransactionOptions: vi.fn(), sqlEditorPendingTransactions: {} as Record, @@ -461,7 +461,7 @@ describe('QueryEditor external SQL save', () => { }; storeState.sqlEditorTransactionOptions = { commitMode: 'manual', - autoCommitDelayMs: 5000, + autoCommitDelayMs: 0, }; storeState.shortcutOptions = { runQuery: { @@ -2347,6 +2347,53 @@ describe('QueryEditor external SQL save', () => { } }); + it('supports DBeaver-style immediate auto-commit for SQL editor DML transactions', async () => { + vi.useFakeTimers(); + storeState.sqlEditorTransactionOptions = { + commitMode: 'auto', + autoCommitDelayMs: 0, + }; + backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({ + success: true, + transactionId: 'tx-auto-now', + transactionPending: true, + data: [ + { columns: ['affectedRows'], rows: [{ affectedRows: 1 }], statementIndex: 1 }, + ], + }); + + try { + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + await act(async () => { + await findButton(renderer!, '运行').props.onClick(); + }); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(backendApp.DBQueryMultiTransactional).toHaveBeenCalled(); + expect(backendApp.DBQueryMulti).not.toHaveBeenCalled(); + expect(textContent(renderer!.root)).toContain('事务执行成功,正在自动提交'); + expect(backendApp.DBCommitTransaction).not.toHaveBeenCalled(); + + await act(async () => { + vi.runOnlyPendingTimers(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(backendApp.DBCommitTransaction).toHaveBeenCalledWith('tx-auto-now'); + expect(textContent(renderer!.root)).not.toContain('事务执行成功,正在自动提交'); + } finally { + vi.useRealTimers(); + } + }); + it('automatically appends hidden primary key locator columns for editable query results', async () => { storeState.connections[0].config.type = 'oracle'; storeState.connections[0].config.database = 'ORCLPDB1'; @@ -3569,12 +3616,14 @@ describe('QueryEditor external SQL save', () => { expect(source).toContain('QueryEditorTransactionSettings'); expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-mode-select'); expect(transactionSettingsSource).toContain('gn-v2-query-toolbar-transaction-delay-select'); - expect(transactionSettingsSource).toContain('这里仅选择事务执行成功后的 COMMIT 时机'); - expect(transactionSettingsSource).toContain("label: '提交:手动 COMMIT'"); - expect(transactionSettingsSource).toContain("label: '提交:自动 COMMIT'"); + expect(transactionSettingsSource).toContain('参考 DBeaver'); + expect(transactionSettingsSource).toContain("label: 'Manual Commit'"); + expect(transactionSettingsSource).toContain("label: 'Auto-commit'"); + expect(transactionSettingsSource).toContain("label: '立即'"); expect(source).toContain('QueryEditorTransactionToolbar'); expect(transactionToolbarSource).toContain("className={isV2Ui ? 'gn-v2-query-transaction-toolbar' : undefined}"); expect(transactionToolbarSource).toContain('事务待提交'); + expect(transactionToolbarSource).toContain('事务执行成功,正在自动提交'); expect(transactionToolbarSource).toContain('onFinish'); expect(source).toContain('gn-v2-query-toolbar-action-group'); expect(transactionSettingsSource).toContain('style={isV2Ui ? undefined : { width: 160 }}'); diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index daf4cd0..15218b7 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -2086,7 +2086,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const sqlEditorCommitMode = sqlEditorTransactionOptions?.commitMode === 'auto' ? 'auto' : 'manual'; const sqlEditorAutoCommitDelayMs = SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS.some((item) => item.value === sqlEditorTransactionOptions?.autoCommitDelayMs) ? Number(sqlEditorTransactionOptions?.autoCommitDelayMs) - : 5000; + : 0; const clearSqlEditorAutoCommitTimer = useCallback(() => { if (sqlEditorAutoCommitTimerRef.current) { clearTimeout(sqlEditorAutoCommitTimerRef.current); @@ -2137,12 +2137,22 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }, [clearSqlEditorAutoCommitTimer, updatePendingSqlTransaction]); const activatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction) => { clearSqlEditorAutoCommitTimer(); - const dueAt = transaction.commitMode === 'auto' ? Date.now() + transaction.autoCommitDelayMs : null; - const nextTransaction = { ...transaction, autoCommitDueAt: dueAt }; + const autoCommitDelayMs = Math.max(0, Number(transaction.autoCommitDelayMs) || 0); + const dueAt = transaction.commitMode === 'auto' ? Date.now() + autoCommitDelayMs : null; + const nextTransaction = { ...transaction, autoCommitDelayMs, autoCommitDueAt: dueAt }; updatePendingSqlTransaction(nextTransaction); if (nextTransaction.commitMode !== 'auto' || !dueAt) { return; } + if (autoCommitDelayMs === 0) { + setSqlEditorAutoCommitRemainingSeconds(0); + sqlEditorAutoCommitTimerRef.current = setTimeout(() => { + sqlEditorAutoCommitTimerRef.current = null; + setSqlEditorAutoCommitRemainingSeconds(null); + void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id); + }, 0); + return; + } const updateRemaining = () => { setSqlEditorAutoCommitRemainingSeconds(Math.max(1, Math.ceil((dueAt - Date.now()) / 1000))); }; @@ -2156,7 +2166,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc } setSqlEditorAutoCommitRemainingSeconds(null); void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id); - }, nextTransaction.autoCommitDelayMs); + }, autoCommitDelayMs); }, [clearSqlEditorAutoCommitTimer, finishPendingSqlTransaction, updatePendingSqlTransaction]); useEffect(() => { return () => { @@ -5298,7 +5308,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc isV2Ui={isV2Ui} commitMode={sqlEditorCommitMode} autoCommitDelayMs={sqlEditorAutoCommitDelayMs} - onCommitModeChange={(mode) => setSqlEditorTransactionOptions({ commitMode: mode })} + onCommitModeChange={(mode) => setSqlEditorTransactionOptions( + mode === 'auto' + ? { commitMode: mode, autoCommitDelayMs: 0 } + : { commitMode: mode }, + )} onAutoCommitDelayMsChange={(delayMs) => setSqlEditorTransactionOptions({ autoCommitDelayMs: delayMs })} /> {pendingSqlTransaction && sqlEditorTransactionToolbar} diff --git a/frontend/src/components/QueryEditorTransactionSettings.tsx b/frontend/src/components/QueryEditorTransactionSettings.tsx index fb17b92..5f4a089 100644 --- a/frontend/src/components/QueryEditorTransactionSettings.tsx +++ b/frontend/src/components/QueryEditorTransactionSettings.tsx @@ -4,10 +4,11 @@ import { Select, Tooltip } from 'antd'; export type SqlEditorCommitMode = 'manual' | 'auto'; export const SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS = [ - { value: 3000, label: '3 秒' }, - { value: 5000, label: '5 秒' }, - { value: 10000, label: '10 秒' }, - { value: 30000, label: '30 秒' }, + { value: 0, label: '立即' }, + { value: 3000, label: '3 秒后' }, + { value: 5000, label: '5 秒后' }, + { value: 10000, label: '10 秒后' }, + { value: 30000, label: '30 秒后' }, ]; type QueryEditorTransactionSettingsProps = { @@ -26,15 +27,15 @@ const QueryEditorTransactionSettings: React.FC ( <> - +