From 89639e36bc56fa1a2f3cc1d8bae7b44964a8bdb3 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 10 Jun 2026 19:13:54 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20SQL=20=E7=BC=96=E8=BE=91=E5=99=A8=20DML=20=E4=BA=8B?= =?UTF-8?q?=E5=8A=A1=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一前后端 DML 与数据修改 CTE 的受管事务判断 - 保留数据修改 CTE 返回行并补充事务回归测试 - 明确 SQL 编辑器事务提交策略文案 --- .../QueryEditor.external-sql-save.test.tsx | 55 +++++++++++++++++-- frontend/src/components/QueryEditor.tsx | 8 +-- .../src/utils/sqlEditorTransaction.test.ts | 6 ++ frontend/src/utils/sqlEditorTransaction.ts | 45 +++++++++++---- frontend/src/v2-theme.css | 6 +- internal/app/methods_db.go | 3 + internal/app/methods_db_multi_test.go | 53 ++++++++++++++++++ internal/app/sql_sanitize.go | 52 +++++++++++++----- internal/app/sql_sanitize_test.go | 15 +++++ 9 files changed, 206 insertions(+), 37 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index a21a02b..0f062af 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -48,6 +48,8 @@ const storeState = vi.hoisted(() => ({ autoCommitDelayMs: 5000, }, setSqlEditorTransactionOptions: vi.fn(), + sqlEditorPendingTransactions: {} as Record, + setSqlEditorPendingTransaction: vi.fn(), shortcutOptions: { runQuery: { mac: { enabled: false, combo: '' }, @@ -487,6 +489,15 @@ describe('QueryEditor external SQL save', () => { storeState.setSqlEditorTransactionOptions.mockImplementation((options: Record) => { storeState.sqlEditorTransactionOptions = { ...storeState.sqlEditorTransactionOptions, ...options }; }); + storeState.sqlEditorPendingTransactions = {}; + storeState.setSqlEditorPendingTransaction.mockReset(); + storeState.setSqlEditorPendingTransaction.mockImplementation((tabId: string, transaction: unknown) => { + if (!transaction) { + delete storeState.sqlEditorPendingTransactions[tabId]; + return; + } + storeState.sqlEditorPendingTransactions[tabId] = transaction; + }); messageApi.success.mockReset(); messageApi.error.mockReset(); messageApi.warning.mockReset(); @@ -2257,6 +2268,40 @@ describe('QueryEditor external SQL save', () => { expect(backendApp.DBQueryMultiTransactional).not.toHaveBeenCalled(); }); + it('runs SQL editor data-changing CTEs through a pending managed transaction', async () => { + const sql = 'WITH moved AS (DELETE FROM audit_logs WHERE created_at < NOW() RETURNING id) SELECT * FROM moved'; + backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({ + success: true, + transactionId: 'tx-write-cte', + transactionPending: true, + data: [ + { columns: ['affectedRows'], rows: [{ affectedRows: 3 }], statementIndex: 1 }, + ], + }); + + 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).toHaveBeenCalledWith( + expect.anything(), + 'main', + expect.stringContaining('DELETE FROM audit_logs'), + 'query-1', + ); + expect(backendApp.DBQueryMulti).not.toHaveBeenCalled(); + expect(textContent(renderer!.root)).toContain('事务待提交'); + }); + it('auto commits SQL editor DML transactions after the configured delay', async () => { vi.useFakeTimers(); storeState.sqlEditorTransactionOptions = { @@ -3521,9 +3566,9 @@ describe('QueryEditor external SQL save', () => { expect(source).toContain('gn-v2-query-toolbar-max-rows-select'); expect(source).toContain('gn-v2-query-toolbar-transaction-mode-select'); expect(source).toContain('gn-v2-query-toolbar-transaction-delay-select'); - expect(source).toContain('这里仅选择该事务执行成功后的提交方式'); - expect(source).toContain("label: '提交:手动'"); - expect(source).toContain("label: '提交:自动'"); + expect(source).toContain('这里仅选择事务执行成功后的 COMMIT 方式'); + expect(source).toContain("label: '事务:手动提交'"); + expect(source).toContain("label: '事务:自动提交'"); expect(source).toContain('gn-v2-query-toolbar-action-group'); expect(source).toContain('style={isV2Ui ? undefined : { width: 150 }}'); expect(source).toContain('style={isV2Ui ? undefined : { width: 200 }}'); @@ -3538,11 +3583,11 @@ describe('QueryEditor external SQL save', () => { expect(css).toContain('display: inline-flex !important;'); expect(css).toContain('gap: 6px;'); expect(css).toContain('margin-left: 0 !important;'); - expect(css).toContain('max-width: 720px;'); + expect(css).toContain('max-width: 760px;'); expect(css).toContain('width: 140px !important;'); expect(css).toContain('width: 166px !important;'); expect(css).toContain('width: 132px !important;'); - expect(css).toContain('width: 112px !important;'); + expect(css).toContain('width: 142px !important;'); expect(css).toContain('width: 82px !important;'); expect(css).toContain('width: 34px !important;'); expect(css).toContain('@media (max-width: 900px)'); diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index c441be8..16b0263 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -5328,15 +5328,15 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc ]} /> - +