feat(query-editor): 对齐 DBeaver 风格事务提交模式

This commit is contained in:
Syngnat
2026-06-10 20:24:45 +08:00
parent 55a52bb0f3
commit fb00f47031
5 changed files with 91 additions and 23 deletions

View File

@@ -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<string, unknown>,
@@ -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(<QueryEditor tab={createTab({ query: "UPDATE users SET active = 0 WHERE id = 1" })} />);
});
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 }}');

View File

@@ -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}

View File

@@ -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<QueryEditorTransactionSettingsPro
onAutoCommitDelayMsChange,
}) => (
<>
<Tooltip title="SQL 编辑器执行 INSERT/UPDATE/DELETE/MERGE/REPLACE 等 DML 时固定开启受管事务;这里仅选择事务执行成功后 COMMIT 时机。">
<Tooltip title="参考 DBeaverSQL 编辑器执行 INSERT/UPDATE/DELETE/MERGE/REPLACE 等 DML 时先进入 GoNavi 托管事务Manual Commit 需要手动提交/回滚Auto-commit 在执行成功后自动 COMMIT。">
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-transaction-mode-select' : undefined}
style={isV2Ui ? undefined : { width: 160 }}
value={commitMode}
onChange={(mode) => onCommitModeChange(mode === 'auto' ? 'auto' : 'manual')}
options={[
{ label: '提交:手动 COMMIT', value: 'manual' },
{ label: '提交:自动 COMMIT', value: 'auto' },
{ label: 'Manual Commit', value: 'manual' },
{ label: 'Auto-commit', value: 'auto' },
]}
/>
</Tooltip>

View File

@@ -28,6 +28,12 @@ const QueryEditorTransactionToolbar: React.FC<QueryEditorTransactionToolbarProps
return null;
}
const statusText = transaction.commitMode === 'auto'
? autoCommitRemainingSeconds !== null && autoCommitRemainingSeconds > 0
? `事务待提交,${autoCommitRemainingSeconds}s 后自动提交`
: '事务执行成功,正在自动提交'
: '事务待提交';
return (
<div
className={isV2Ui ? 'gn-v2-query-transaction-toolbar' : undefined}
@@ -40,9 +46,7 @@ const QueryEditorTransactionToolbar: React.FC<QueryEditorTransactionToolbarProps
}}
>
<span style={{ fontSize: 12, color: darkMode ? '#d4d4d4' : '#666' }}>
{transaction.commitMode === 'auto' && autoCommitRemainingSeconds !== null
? `事务待提交,${autoCommitRemainingSeconds}s 后自动提交`
: '事务待提交'}
{statusText}
</span>
<Button
size="small"

View File

@@ -1637,7 +1637,7 @@ const sanitizeQueryOptions = (value: unknown): QueryOptions => {
};
const DATA_EDIT_AUTO_COMMIT_DELAY_OPTIONS = new Set([3000, 5000, 10000, 30000]);
const SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS = new Set([3000, 5000, 10000, 30000]);
const SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS = new Set([0, 3000, 5000, 10000, 30000]);
const sanitizeDataEditTransactionOptions = (
value: unknown,
@@ -1667,7 +1667,7 @@ const sanitizeSqlEditorTransactionOptions = (
commitMode: raw.commitMode === "auto" ? "auto" : "manual",
autoCommitDelayMs: SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS.has(autoCommitDelayMs)
? autoCommitDelayMs
: 5000,
: 0,
};
};
@@ -2063,7 +2063,7 @@ export const useStore = create<AppState>()(
},
sqlEditorTransactionOptions: {
commitMode: "manual",
autoCommitDelayMs: 5000,
autoCommitDelayMs: 0,
},
sqlEditorPendingTransactions: {},
shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS),