From 856a5158e4d903ceea4faa2b6f075919db89f326 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 10 Jun 2026 20:36:25 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(query-editor):=20?= =?UTF-8?q?=E6=8A=BD=E7=A6=BB=20SQL=20=E4=BA=8B=E5=8A=A1=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/QueryEditor.tsx | 110 ++------------- .../useSqlEditorTransactionController.ts | 130 ++++++++++++++++++ 2 files changed, 140 insertions(+), 100 deletions(-) create mode 100644 frontend/src/components/useSqlEditorTransactionController.ts diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 15218b7..be49141 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -6,7 +6,7 @@ import { format } from 'sql-formatter'; import { v4 as uuidv4 } from 'uuid'; import { TabData, ColumnDefinition, IndexDefinition } from '../types'; import { useStore } from '../store'; -import { DBQuery, DBQueryWithCancel, DBQueryMulti, DBQueryMultiTransactional, DBCommitTransaction, DBRollbackTransaction, DBGetTables, DBGetAllColumns, DBGetDatabases, DBGetColumns, DBGetIndexes, CancelQuery, GenerateQueryID, WriteSQLFile, ExportSQLFile } from '../../wailsjs/go/app/App'; +import { DBQuery, DBQueryWithCancel, DBQueryMulti, DBQueryMultiTransactional, DBGetTables, DBGetAllColumns, DBGetDatabases, DBGetColumns, DBGetIndexes, CancelQuery, GenerateQueryID, WriteSQLFile, ExportSQLFile } from '../../wailsjs/go/app/App'; import { GONAVI_ROW_KEY } from './DataGrid'; import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities'; import { applyMongoQueryAutoLimit, convertMongoShellToJsonCommand } from "../utils/mongodb"; @@ -39,7 +39,8 @@ import QueryEditorResultsPanel, { type QueryEditorResultSet } from './QueryEdito import QueryEditorTransactionSettings, { SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS, } from './QueryEditorTransactionSettings'; -import QueryEditorTransactionToolbar, { type PendingSqlEditorTransaction } from './QueryEditorTransactionToolbar'; +import QueryEditorTransactionToolbar from './QueryEditorTransactionToolbar'; +import { useSqlEditorTransactionController } from './useSqlEditorTransactionController'; const SQL_KEYWORDS = [ 'SELECT', 'FROM', 'WHERE', 'LIMIT', 'INSERT', 'UPDATE', 'DELETE', 'JOIN', 'LEFT', 'RIGHT', @@ -2038,15 +2039,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const setQueryOptions = useStore(state => state.setQueryOptions); const sqlEditorTransactionOptions = useStore(state => state.sqlEditorTransactionOptions); const setSqlEditorTransactionOptions = useStore(state => state.setSqlEditorTransactionOptions); - const setSqlEditorPendingTransaction = useStore(state => state.setSqlEditorPendingTransaction); const [isResultPanelVisible, setIsResultPanelVisible] = useState( () => tab.resultPanelVisible === true ); - const [pendingSqlTransaction, setPendingSqlTransaction] = useState(null); - const pendingSqlTransactionRef = useRef(null); - const sqlEditorAutoCommitTimerRef = useRef | null>(null); - const sqlEditorAutoCommitCountdownRef = useRef | null>(null); - const [sqlEditorAutoCommitRemainingSeconds, setSqlEditorAutoCommitRemainingSeconds] = useState(null); const shortcutOptions = useStore(state => state.shortcutOptions); const activeShortcutPlatform = getShortcutPlatform(isMacLikePlatform()); const runQueryShortcutBinding = useMemo( @@ -2087,98 +2082,13 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc const sqlEditorAutoCommitDelayMs = SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS.some((item) => item.value === sqlEditorTransactionOptions?.autoCommitDelayMs) ? Number(sqlEditorTransactionOptions?.autoCommitDelayMs) : 0; - const clearSqlEditorAutoCommitTimer = useCallback(() => { - if (sqlEditorAutoCommitTimerRef.current) { - clearTimeout(sqlEditorAutoCommitTimerRef.current); - sqlEditorAutoCommitTimerRef.current = null; - } - if (sqlEditorAutoCommitCountdownRef.current) { - clearInterval(sqlEditorAutoCommitCountdownRef.current); - sqlEditorAutoCommitCountdownRef.current = null; - } - setSqlEditorAutoCommitRemainingSeconds(null); - }, []); - const updatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction | null) => { - pendingSqlTransactionRef.current = transaction; - setPendingSqlTransaction(transaction); - setSqlEditorPendingTransaction(tab.id, transaction); - }, [setSqlEditorPendingTransaction, tab.id]); - const finishPendingSqlTransaction = useCallback(async ( - action: 'commit' | 'rollback', - source: 'manual' | 'auto' = 'manual', - transactionId?: string, - ) => { - const transaction = pendingSqlTransactionRef.current; - if (!transaction || (transactionId && transaction.id !== transactionId)) { - return; - } - clearSqlEditorAutoCommitTimer(); - try { - const res = action === 'commit' - ? await DBCommitTransaction(transaction.id) - : await DBRollbackTransaction(transaction.id); - if (res?.success) { - updatePendingSqlTransaction(null); - if (action === 'commit') { - message.success(source === 'auto' ? 'SQL 事务已自动提交' : 'SQL 事务已提交'); - } else { - message.success('SQL 事务已回滚'); - } - return; - } - updatePendingSqlTransaction(null); - const fallback = action === 'commit' ? '提交失败' : '回滚失败'; - message.error(`${source === 'auto' ? '自动提交失败' : fallback}: ${formatSqlExecutionError(res?.message || '未知错误')}`); - } catch (err: any) { - updatePendingSqlTransaction(null); - const fallback = action === 'commit' ? '提交失败' : '回滚失败'; - message.error(`${source === 'auto' ? '自动提交失败' : fallback}: ${formatSqlExecutionError(err?.message || err || '未知错误')}`); - } - }, [clearSqlEditorAutoCommitTimer, updatePendingSqlTransaction]); - const activatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction) => { - clearSqlEditorAutoCommitTimer(); - 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))); - }; - updateRemaining(); - sqlEditorAutoCommitCountdownRef.current = setInterval(updateRemaining, 250); - sqlEditorAutoCommitTimerRef.current = setTimeout(() => { - sqlEditorAutoCommitTimerRef.current = null; - if (sqlEditorAutoCommitCountdownRef.current) { - clearInterval(sqlEditorAutoCommitCountdownRef.current); - sqlEditorAutoCommitCountdownRef.current = null; - } - setSqlEditorAutoCommitRemainingSeconds(null); - void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id); - }, autoCommitDelayMs); - }, [clearSqlEditorAutoCommitTimer, finishPendingSqlTransaction, updatePendingSqlTransaction]); - useEffect(() => { - return () => { - clearSqlEditorAutoCommitTimer(); - const transaction = pendingSqlTransactionRef.current; - if (transaction?.id) { - pendingSqlTransactionRef.current = null; - setSqlEditorPendingTransaction(tab.id, null); - void DBRollbackTransaction(transaction.id); - } - }; - }, [clearSqlEditorAutoCommitTimer, setSqlEditorPendingTransaction, tab.id]); + const { + activatePendingSqlTransaction, + autoCommitRemainingSeconds: sqlEditorAutoCommitRemainingSeconds, + finishPendingSqlTransaction, + pendingSqlTransaction, + pendingSqlTransactionRef, + } = useSqlEditorTransactionController({ tabId: tab.id }); const autoFetchVisible = useAutoFetchVisibility(); const currentSavedQuery = useMemo(() => { diff --git a/frontend/src/components/useSqlEditorTransactionController.ts b/frontend/src/components/useSqlEditorTransactionController.ts new file mode 100644 index 0000000..0ac92e2 --- /dev/null +++ b/frontend/src/components/useSqlEditorTransactionController.ts @@ -0,0 +1,130 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { message } from 'antd'; + +import { DBCommitTransaction, DBRollbackTransaction } from '../../wailsjs/go/app/App'; +import { useStore } from '../store'; +import { formatSqlExecutionError } from '../utils/sqlErrorSemantics'; +import type { PendingSqlEditorTransaction } from './QueryEditorTransactionToolbar'; + +type FinishSqlEditorTransactionAction = 'commit' | 'rollback'; +type FinishSqlEditorTransactionSource = 'manual' | 'auto'; + +type UseSqlEditorTransactionControllerOptions = { + tabId: string; +}; + +export const useSqlEditorTransactionController = ({ + tabId, +}: UseSqlEditorTransactionControllerOptions) => { + const setSqlEditorPendingTransaction = useStore(state => state.setSqlEditorPendingTransaction); + const [pendingSqlTransaction, setPendingSqlTransaction] = useState(null); + const pendingSqlTransactionRef = useRef(null); + const autoCommitTimerRef = useRef | null>(null); + const autoCommitCountdownRef = useRef | null>(null); + const [autoCommitRemainingSeconds, setAutoCommitRemainingSeconds] = useState(null); + + const clearAutoCommitTimer = useCallback(() => { + if (autoCommitTimerRef.current) { + clearTimeout(autoCommitTimerRef.current); + autoCommitTimerRef.current = null; + } + if (autoCommitCountdownRef.current) { + clearInterval(autoCommitCountdownRef.current); + autoCommitCountdownRef.current = null; + } + setAutoCommitRemainingSeconds(null); + }, []); + + const updatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction | null) => { + pendingSqlTransactionRef.current = transaction; + setPendingSqlTransaction(transaction); + setSqlEditorPendingTransaction(tabId, transaction); + }, [setSqlEditorPendingTransaction, tabId]); + + const finishPendingSqlTransaction = useCallback(async ( + action: FinishSqlEditorTransactionAction, + source: FinishSqlEditorTransactionSource = 'manual', + transactionId?: string, + ) => { + const transaction = pendingSqlTransactionRef.current; + if (!transaction || (transactionId && transaction.id !== transactionId)) { + return; + } + clearAutoCommitTimer(); + try { + const res = action === 'commit' + ? await DBCommitTransaction(transaction.id) + : await DBRollbackTransaction(transaction.id); + if (res?.success) { + updatePendingSqlTransaction(null); + if (action === 'commit') { + message.success(source === 'auto' ? 'SQL 事务已自动提交' : 'SQL 事务已提交'); + } else { + message.success('SQL 事务已回滚'); + } + return; + } + updatePendingSqlTransaction(null); + const fallback = action === 'commit' ? '提交失败' : '回滚失败'; + message.error(`${source === 'auto' ? '自动提交失败' : fallback}: ${formatSqlExecutionError(res?.message || '未知错误')}`); + } catch (err: any) { + updatePendingSqlTransaction(null); + const fallback = action === 'commit' ? '提交失败' : '回滚失败'; + message.error(`${source === 'auto' ? '自动提交失败' : fallback}: ${formatSqlExecutionError(err?.message || err || '未知错误')}`); + } + }, [clearAutoCommitTimer, updatePendingSqlTransaction]); + + const activatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction) => { + clearAutoCommitTimer(); + 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) { + setAutoCommitRemainingSeconds(0); + autoCommitTimerRef.current = setTimeout(() => { + autoCommitTimerRef.current = null; + setAutoCommitRemainingSeconds(null); + void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id); + }, 0); + return; + } + const updateRemaining = () => { + setAutoCommitRemainingSeconds(Math.max(1, Math.ceil((dueAt - Date.now()) / 1000))); + }; + updateRemaining(); + autoCommitCountdownRef.current = setInterval(updateRemaining, 250); + autoCommitTimerRef.current = setTimeout(() => { + autoCommitTimerRef.current = null; + if (autoCommitCountdownRef.current) { + clearInterval(autoCommitCountdownRef.current); + autoCommitCountdownRef.current = null; + } + setAutoCommitRemainingSeconds(null); + void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id); + }, autoCommitDelayMs); + }, [clearAutoCommitTimer, finishPendingSqlTransaction, updatePendingSqlTransaction]); + + useEffect(() => { + return () => { + clearAutoCommitTimer(); + const transaction = pendingSqlTransactionRef.current; + if (transaction?.id) { + pendingSqlTransactionRef.current = null; + setSqlEditorPendingTransaction(tabId, null); + void DBRollbackTransaction(transaction.id); + } + }; + }, [clearAutoCommitTimer, setSqlEditorPendingTransaction, tabId]); + + return { + activatePendingSqlTransaction, + autoCommitRemainingSeconds, + finishPendingSqlTransaction, + pendingSqlTransaction, + pendingSqlTransactionRef, + }; +};