feat(editor): 支持 SQL 编辑器增删改事务提交

- 为 SQL 编辑器 DML 新增后端托管事务会话和提交回滚接口

- 增加手动提交与自动提交设置,并显示待提交状态

- 补充前后端事务执行、提交、回滚和自动提交测试
This commit is contained in:
Syngnat
2026-06-10 17:18:34 +08:00
parent 7eb086cade
commit 61d71cf1d0
12 changed files with 970 additions and 11 deletions

View File

@@ -43,6 +43,11 @@ const storeState = vi.hoisted(() => ({
showQueryResultsPanel: false,
},
setQueryOptions: vi.fn(),
sqlEditorTransactionOptions: {
commitMode: 'manual' as 'manual' | 'auto',
autoCommitDelayMs: 5000,
},
setSqlEditorTransactionOptions: vi.fn(),
shortcutOptions: {
runQuery: {
mac: { enabled: false, combo: '' },
@@ -70,6 +75,9 @@ const backendApp = vi.hoisted(() => ({
DBQuery: vi.fn(),
DBQueryWithCancel: vi.fn(),
DBQueryMulti: vi.fn(),
DBQueryMultiTransactional: vi.fn(),
DBCommitTransaction: vi.fn(),
DBRollbackTransaction: vi.fn(),
DBGetTables: vi.fn(),
DBGetAllColumns: vi.fn(),
DBGetDatabases: vi.fn(),
@@ -449,6 +457,10 @@ describe('QueryEditor external SQL save', () => {
showColumnType: true,
showQueryResultsPanel: false,
};
storeState.sqlEditorTransactionOptions = {
commitMode: 'manual',
autoCommitDelayMs: 5000,
};
storeState.shortcutOptions = {
runQuery: {
mac: { enabled: false, combo: '' },
@@ -471,13 +483,21 @@ describe('QueryEditor external SQL save', () => {
storeState.setQueryOptions.mockImplementation((options: Record<string, unknown>) => {
storeState.queryOptions = { ...storeState.queryOptions, ...options };
});
storeState.setSqlEditorTransactionOptions.mockReset();
storeState.setSqlEditorTransactionOptions.mockImplementation((options: Record<string, unknown>) => {
storeState.sqlEditorTransactionOptions = { ...storeState.sqlEditorTransactionOptions, ...options };
});
messageApi.success.mockReset();
messageApi.error.mockReset();
messageApi.warning.mockReset();
backendApp.DBQuery.mockResolvedValue({ success: true, data: [] });
backendApp.WriteSQLFile.mockResolvedValue({ success: true });
backendApp.ExportSQLFile.mockResolvedValue({ success: true });
backendApp.DBQueryWithCancel.mockResolvedValue({ success: true, data: [] });
backendApp.DBQueryMulti.mockResolvedValue({ success: true, data: [] });
backendApp.DBQueryMultiTransactional.mockResolvedValue({ success: true, data: [] });
backendApp.DBCommitTransaction.mockResolvedValue({ success: true, message: '事务已提交' });
backendApp.DBRollbackTransaction.mockResolvedValue({ success: true, message: '事务已回滚' });
backendApp.DBGetColumns.mockResolvedValue({ success: true, data: [] });
backendApp.DBGetIndexes.mockResolvedValue({ success: true, data: [] });
backendApp.DBGetAllColumns.mockResolvedValue({ success: true, data: [] });
@@ -2117,6 +2137,96 @@ describe('QueryEditor external SQL save', () => {
expect(pageText).toContain('原始错误pq: syntax error at or near "from"');
});
it('runs SQL editor DML through a pending managed transaction and commits manually', async () => {
backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({
success: true,
transactionId: 'tx-1',
transactionPending: true,
data: [
{ columns: ['affectedRows'], rows: [{ affectedRows: 2 }], statementIndex: 1 },
],
});
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ query: "UPDATE users SET name = 'new' WHERE id = 1" })} />);
});
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('UPDATE users SET name'),
'query-1',
);
expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
expect(textContent(renderer!.root)).toContain('事务待提交');
expect(textContent(renderer!.root)).toContain('影响行数2');
await act(async () => {
await findExactButton(renderer!, '提交').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(backendApp.DBCommitTransaction).toHaveBeenCalledWith('tx-1');
expect(textContent(renderer!.root)).not.toContain('事务待提交');
});
it('auto commits SQL editor DML transactions after the configured delay', async () => {
vi.useFakeTimers();
storeState.sqlEditorTransactionOptions = {
commitMode: 'auto',
autoCommitDelayMs: 3000,
};
backendApp.DBQueryMultiTransactional.mockResolvedValueOnce({
success: true,
transactionId: 'tx-auto',
transactionPending: true,
data: [
{ columns: ['affectedRows'], rows: [{ affectedRows: 1 }], statementIndex: 1 },
],
});
try {
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ query: "DELETE FROM users WHERE id = 1" })} />);
});
await act(async () => {
await findButton(renderer!, '运行').props.onClick();
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
expect(textContent(renderer!.root)).toContain('事务待提交');
expect(backendApp.DBCommitTransaction).not.toHaveBeenCalled();
await act(async () => {
vi.advanceTimersByTime(3000);
await Promise.resolve();
await Promise.resolve();
});
expect(backendApp.DBCommitTransaction).toHaveBeenCalledWith('tx-auto');
expect(backendApp.DBQueryMulti).not.toHaveBeenCalled();
} 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';
@@ -3334,6 +3444,8 @@ describe('QueryEditor external SQL save', () => {
expect(source).toContain('gn-v2-query-toolbar-connection-select');
expect(source).toContain('gn-v2-query-toolbar-database-select');
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('gn-v2-query-toolbar-action-group');
expect(source).toContain('style={isV2Ui ? undefined : { width: 150 }}');
expect(source).toContain('style={isV2Ui ? undefined : { width: 200 }}');
@@ -3348,10 +3460,12 @@ 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: 520px;');
expect(css).toContain('max-width: 720px;');
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: 82px !important;');
expect(css).toContain('width: 34px !important;');
expect(css).toContain('@media (max-width: 900px)');

View File

@@ -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, DBGetTables, DBGetAllColumns, DBGetDatabases, DBGetColumns, DBGetIndexes, CancelQuery, GenerateQueryID, WriteSQLFile, ExportSQLFile } from '../../wailsjs/go/app/App';
import { DBQuery, DBQueryWithCancel, DBQueryMulti, DBQueryMultiTransactional, DBCommitTransaction, DBRollbackTransaction, 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";
@@ -750,6 +750,67 @@ const areSqlStatementListsEqual = (left: string[], right: string[]): boolean =>
&& left.every((statement, index) => normalizeExecutedSqlKey(statement) === normalizeExecutedSqlKey(right[index]))
);
const SQL_EDITOR_DML_KEYWORDS = new Set(['insert', 'update', 'delete', 'replace', 'merge', 'upsert']);
const SQL_EDITOR_READ_KEYWORDS = new Set(['select', 'with', 'show', 'describe', 'desc', 'explain', 'pragma', 'values']);
const SQL_EDITOR_TRANSACTION_CONTROL_KEYWORDS = new Set(['begin', 'commit', 'rollback', 'savepoint', 'release']);
const SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS = [
{ value: 3000, label: '3 秒' },
{ value: 5000, label: '5 秒' },
{ value: 10000, label: '10 秒' },
{ value: 30000, label: '30 秒' },
];
const resolveLeadingSqlKeyword = (statement: string): string => {
let text = String(statement || '').trim();
while (text) {
if (text.startsWith('--') || text.startsWith('#')) {
const lineBreak = text.indexOf('\n');
if (lineBreak < 0) return '';
text = text.slice(lineBreak + 1).trimStart();
continue;
}
if (text.startsWith('/*')) {
const blockEnd = text.indexOf('*/');
if (blockEnd < 0) return '';
text = text.slice(blockEnd + 2).trimStart();
continue;
}
break;
}
const match = text.match(/^([A-Za-z0-9_]+)/);
return match?.[1]?.toLowerCase() || '';
};
const isSqlEditorTransactionControlStatement = (statement: string): boolean => {
const keyword = resolveLeadingSqlKeyword(statement);
if (SQL_EDITOR_TRANSACTION_CONTROL_KEYWORDS.has(keyword)) return true;
return keyword === 'start' && /\btransaction\b/i.test(statement);
};
const shouldUseSqlEditorManagedTransaction = (statements: string[]): boolean => {
let hasManagedWrite = false;
for (const statement of statements) {
const trimmed = String(statement || '').trim();
if (!trimmed) continue;
if (isSqlEditorTransactionControlStatement(trimmed)) return false;
const keyword = resolveLeadingSqlKeyword(trimmed);
if (SQL_EDITOR_READ_KEYWORDS.has(keyword)) continue;
if (SQL_EDITOR_DML_KEYWORDS.has(keyword)) {
hasManagedWrite = true;
continue;
}
return false;
}
return hasManagedWrite;
};
type PendingSqlEditorTransaction = {
id: string;
commitMode: 'manual' | 'auto';
autoCommitDelayMs: number;
createdAt: number;
};
const normalizeEditorPosition = (position: any): { lineNumber: number; column: number } | null => {
if (!position) return null;
const lineNumber = Number(position.positionLineNumber ?? position.lineNumber ?? position.endLineNumber ?? position.startLineNumber ?? position.selectionStartLineNumber);
@@ -2031,9 +2092,16 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const setSqlFormatOptions = useStore(state => state.setSqlFormatOptions);
const queryOptions = useStore(state => state.queryOptions);
const setQueryOptions = useStore(state => state.setQueryOptions);
const sqlEditorTransactionOptions = useStore(state => state.sqlEditorTransactionOptions);
const setSqlEditorTransactionOptions = useStore(state => state.setSqlEditorTransactionOptions);
const [isResultPanelVisible, setIsResultPanelVisible] = useState(
() => tab.resultPanelVisible === true
);
const [pendingSqlTransaction, setPendingSqlTransaction] = useState<PendingSqlEditorTransaction | null>(null);
const pendingSqlTransactionRef = useRef<PendingSqlEditorTransaction | null>(null);
const sqlEditorAutoCommitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const sqlEditorAutoCommitCountdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [sqlEditorAutoCommitRemainingSeconds, setSqlEditorAutoCommitRemainingSeconds] = useState<number | null>(null);
const shortcutOptions = useStore(state => state.shortcutOptions);
const activeShortcutPlatform = getShortcutPlatform(isMacLikePlatform());
const runQueryShortcutBinding = useMemo(
@@ -2070,6 +2138,89 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return nextVisible;
});
}, [tab.id, updateQueryTabDraft]);
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;
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);
}, []);
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();
updatePendingSqlTransaction(transaction);
if (transaction.commitMode !== 'auto') {
return;
}
const dueAt = Date.now() + transaction.autoCommitDelayMs;
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', transaction.id);
}, transaction.autoCommitDelayMs);
}, [clearSqlEditorAutoCommitTimer, finishPendingSqlTransaction, updatePendingSqlTransaction]);
useEffect(() => {
return () => {
clearSqlEditorAutoCommitTimer();
const transaction = pendingSqlTransactionRef.current;
if (transaction?.id) {
pendingSqlTransactionRef.current = null;
void DBRollbackTransaction(transaction.id);
}
};
}, [clearSqlEditorAutoCommitTimer]);
const autoFetchVisible = useAutoFetchVisibility();
const currentSavedQuery = useMemo(() => {
@@ -4297,6 +4448,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
message.info('没有可执行的 SQL。');
return;
}
const useManagedTransaction = shouldUseSqlEditorManagedTransaction(sourceStatements);
if (useManagedTransaction && pendingSqlTransactionRef.current) {
message.warning('当前 SQL 编辑器已有未提交事务,请先提交或回滚后再执行新的增删改语句。');
return;
}
const forceReadOnlyResult = connCaps.forceReadOnlyQueryResult;
const statementPlans: QueryStatementPlan[] = [];
@@ -4331,7 +4487,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}
setQueryId(queryId);
const res = await DBQueryMulti(buildRpcConnectionConfig(config) as any, currentDb, fullSQL, queryId);
const queryExecutor = useManagedTransaction ? DBQueryMultiTransactional : DBQueryMulti;
const res = await queryExecutor(buildRpcConnectionConfig(config) as any, currentDb, fullSQL, queryId);
const duration = Date.now() - startTime;
addSqlLog({
@@ -4373,6 +4530,15 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return;
}
if (useManagedTransaction && res.transactionPending && res.transactionId) {
activatePendingSqlTransaction({
id: String(res.transactionId),
commitMode: sqlEditorCommitMode,
autoCommitDelayMs: sqlEditorAutoCommitDelayMs,
createdAt: Date.now(),
});
}
// res.data 是 ResultSetData[] 数组
const resultSetDataArray = Array.isArray(res.data) ? (res.data as any[]) : [];
const nextResultSets: ResultSet[] = [];
@@ -5122,6 +5288,39 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}, wasClosed ? 350 : 0);
};
const sqlEditorTransactionToolbar = pendingSqlTransaction ? (
<div
className={isV2Ui ? 'gn-v2-query-transaction-toolbar' : undefined}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 8,
padding: '0 4px',
whiteSpace: 'nowrap',
}}
>
<span style={{ fontSize: 12, color: darkMode ? '#d4d4d4' : '#666' }}>
{pendingSqlTransaction.commitMode === 'auto' && sqlEditorAutoCommitRemainingSeconds !== null
? `事务待提交,${sqlEditorAutoCommitRemainingSeconds}s 后自动提交`
: '事务待提交'}
</span>
<Button
size="small"
type="primary"
onClick={() => void finishPendingSqlTransaction('commit', 'manual')}
>
</Button>
<Button
size="small"
danger
onClick={() => void finishPendingSqlTransaction('rollback', 'manual')}
>
</Button>
</div>
) : null;
return (
<div ref={queryEditorRootRef} className={isV2Ui ? 'gn-v2-query-editor' : undefined} style={{ flex: '1 1 auto', minHeight: 0, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
<div
@@ -5170,6 +5369,28 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
]}
/>
</Tooltip>
<Tooltip title="SQL 编辑器直接执行 INSERT/UPDATE/DELETE 等增删改语句时启用事务;手动提交更安全,自动提交会在执行成功后按所选时间提交。">
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-transaction-mode-select' : undefined}
style={isV2Ui ? undefined : { width: 128 }}
value={sqlEditorCommitMode}
onChange={(mode) => setSqlEditorTransactionOptions({ commitMode: mode === 'auto' ? 'auto' : 'manual' })}
options={[
{ label: '事务:手动', value: 'manual' },
{ label: '事务:自动', value: 'auto' },
]}
/>
</Tooltip>
{sqlEditorCommitMode === 'auto' && (
<Select
className={isV2Ui ? 'gn-v2-query-toolbar-select gn-v2-query-toolbar-transaction-delay-select' : undefined}
style={isV2Ui ? undefined : { width: 96 }}
value={sqlEditorAutoCommitDelayMs}
onChange={(delayMs) => setSqlEditorTransactionOptions({ autoCommitDelayMs: Number(delayMs) })}
options={SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS}
/>
)}
{pendingSqlTransaction && sqlEditorTransactionToolbar}
</div>
<div
className={isV2Ui ? 'gn-v2-query-toolbar-actions' : undefined}
@@ -5305,6 +5526,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
currentDb={currentDb}
currentConnectionId={currentConnectionId}
toggleShortcutLabel={toggleQueryResultsPanelShortcutLabel}
transactionToolbar={sqlEditorTransactionToolbar}
onActiveResultKeyChange={setActiveResultKey}
onHide={() => updateResultPanelVisibility(false)}
onCloseResult={handleCloseResult}

View File

@@ -33,6 +33,7 @@ interface QueryEditorResultsPanelProps {
currentDb: string;
currentConnectionId: string;
toggleShortcutLabel: string;
transactionToolbar?: React.ReactNode;
onActiveResultKeyChange: (key: string) => void;
onHide: () => void;
onCloseResult: (key: string) => void;
@@ -57,6 +58,7 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
currentDb,
currentConnectionId,
toggleShortcutLabel,
transactionToolbar,
onActiveResultKeyChange,
onHide,
onCloseResult,
@@ -132,6 +134,16 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
/>
</Tooltip>
);
const tabsExtraContent = transactionToolbar || !activeResultUsesDataGrid
? {
right: (
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
{transactionToolbar}
{!activeResultUsesDataGrid ? tabsHideButton : null}
</div>
),
}
: undefined;
const toolbarHideButton = (
<Tooltip title={hideTooltipTitle}>
@@ -321,7 +333,7 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
onChange={onActiveResultKeyChange}
animated={false}
style={{ flex: 1, minHeight: 0 }}
tabBarExtraContent={!activeResultUsesDataGrid ? { right: tabsHideButton } : undefined}
tabBarExtraContent={tabsExtraContent}
items={resultSets.map((rs, idx) => ({
key: rs.key,
label: (

View File

@@ -1119,6 +1119,11 @@ export interface DataEditTransactionOptions {
autoCommitDelayMs: number;
}
export interface SqlEditorTransactionOptions {
commitMode: "manual" | "auto";
autoCommitDelayMs: number;
}
interface AppState {
connections: SavedConnection[];
connectionTags: ConnectionTag[];
@@ -1137,6 +1142,7 @@ interface AppState {
sqlFormatOptions: { keywordCase: "upper" | "lower" };
queryOptions: QueryOptions;
dataEditTransactionOptions: DataEditTransactionOptions;
sqlEditorTransactionOptions: SqlEditorTransactionOptions;
shortcutOptions: ShortcutOptions;
sqlSnippets: SqlSnippet[];
sqlLogs: SqlLog[];
@@ -1245,6 +1251,9 @@ interface AppState {
setDataEditTransactionOptions: (
options: Partial<DataEditTransactionOptions>,
) => void;
setSqlEditorTransactionOptions: (
options: Partial<SqlEditorTransactionOptions>,
) => void;
updateShortcut: (
action: ShortcutAction,
binding: Partial<ShortcutPlatformBinding>,
@@ -1614,6 +1623,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 sanitizeDataEditTransactionOptions = (
value: unknown,
@@ -1631,6 +1641,22 @@ const sanitizeDataEditTransactionOptions = (
};
};
const sanitizeSqlEditorTransactionOptions = (
value: unknown,
): SqlEditorTransactionOptions => {
const raw =
value && typeof value === "object"
? (value as Record<string, unknown>)
: {};
const autoCommitDelayMs = Number(raw.autoCommitDelayMs);
return {
commitMode: raw.commitMode === "auto" ? "auto" : "manual",
autoCommitDelayMs: SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS.has(autoCommitDelayMs)
? autoCommitDelayMs
: 5000,
};
};
const sanitizeTableAccessCount = (value: unknown): Record<string, number> => {
const raw =
value && typeof value === "object"
@@ -2021,6 +2047,10 @@ export const useStore = create<AppState>()(
commitMode: "manual",
autoCommitDelayMs: 5000,
},
sqlEditorTransactionOptions: {
commitMode: "manual",
autoCommitDelayMs: 5000,
},
shortcutOptions: cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS),
sqlSnippets: DEFAULT_SQL_SNIPPETS,
sqlLogs: [],
@@ -2772,6 +2802,13 @@ export const useStore = create<AppState>()(
...options,
}),
})),
setSqlEditorTransactionOptions: (options) =>
set((state) => ({
sqlEditorTransactionOptions: sanitizeSqlEditorTransactionOptions({
...state.sqlEditorTransactionOptions,
...options,
}),
})),
updateShortcut: (action, binding, platform) => {
runWithExplicitShortcutPersistence(() => {
const targetPlatform = platform ?? getShortcutPlatform();
@@ -3180,6 +3217,8 @@ export const useStore = create<AppState>()(
nextState.queryOptions = sanitizeQueryOptions(state.queryOptions);
nextState.dataEditTransactionOptions =
sanitizeDataEditTransactionOptions(state.dataEditTransactionOptions);
nextState.sqlEditorTransactionOptions =
sanitizeSqlEditorTransactionOptions(state.sqlEditorTransactionOptions);
nextState.shortcutOptions = sanitizeShortcutOptions(
state.shortcutOptions,
);
@@ -3285,6 +3324,9 @@ export const useStore = create<AppState>()(
dataEditTransactionOptions: sanitizeDataEditTransactionOptions(
state.dataEditTransactionOptions,
),
sqlEditorTransactionOptions: sanitizeSqlEditorTransactionOptions(
state.sqlEditorTransactionOptions,
),
shortcutOptions: sanitizeShortcutOptions(state.shortcutOptions),
sqlLogs: sanitizeSqlLogs(state.sqlLogs),
sqlSnippets: sanitizeSqlSnippets(state.sqlSnippets),
@@ -3316,6 +3358,7 @@ export const useStore = create<AppState>()(
sqlFormatOptions: state.sqlFormatOptions,
queryOptions: state.queryOptions,
dataEditTransactionOptions: state.dataEditTransactionOptions,
sqlEditorTransactionOptions: state.sqlEditorTransactionOptions,
shortcutOptions: resolveShortcutOptionsForPersistence(state.shortcutOptions),
sqlLogs: sanitizeSqlLogs(state.sqlLogs),
sqlSnippets: state.sqlSnippets,

View File

@@ -4810,7 +4810,7 @@ body[data-ui-version="v2"] .gn-v2-query-toolbar-actions {
body[data-ui-version="v2"] .gn-v2-query-toolbar-selects {
flex: 0 1 auto !important;
flex-wrap: nowrap;
max-width: 520px;
max-width: 720px;
}
body[data-ui-version="v2"] .gn-v2-query-toolbar-actions {
@@ -4839,6 +4839,16 @@ body[data-ui-version="v2"] .gn-v2-query-toolbar-max-rows-select {
flex: 0 0 132px !important;
}
body[data-ui-version="v2"] .gn-v2-query-toolbar-transaction-mode-select {
width: 112px !important;
flex: 0 0 112px !important;
}
body[data-ui-version="v2"] .gn-v2-query-toolbar-transaction-delay-select {
width: 82px !important;
flex: 0 0 82px !important;
}
body[data-ui-version="v2"] .gn-v2-query-toolbar .ant-select-selector {
height: 32px !important;
padding: 0 10px !important;

View File

@@ -52,14 +52,20 @@ export function DBGetTriggers(arg1:connection.ConnectionConfig,arg2:string,arg3:
export function DBQuery(arg1:connection.ConnectionConfig,arg2:string,arg3:string):Promise<connection.QueryResult>;
export function DBCommitTransaction(arg1:string):Promise<connection.QueryResult>;
export function DBQueryIsolated(arg1:connection.ConnectionConfig,arg2:string,arg3:string):Promise<connection.QueryResult>;
export function DBQueryMulti(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise<connection.QueryResult>;
export function DBQueryMultiTransactional(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise<connection.QueryResult>;
export function DBQueryWithCancel(arg1:connection.ConnectionConfig,arg2:string,arg3:string,arg4:string):Promise<connection.QueryResult>;
export function DBShowCreateTable(arg1:connection.ConnectionConfig,arg2:string,arg3:string):Promise<connection.QueryResult>;
export function DBRollbackTransaction(arg1:string):Promise<connection.QueryResult>;
export function DataSync(arg1:sync.SyncConfig):Promise<sync.SyncResult>;
export function DataSyncAnalyze(arg1:sync.SyncConfig):Promise<connection.QueryResult>;

View File

@@ -94,6 +94,10 @@ export function DBQuery(arg1, arg2, arg3) {
return window['go']['app']['App']['DBQuery'](arg1, arg2, arg3);
}
export function DBCommitTransaction(arg1) {
return window['go']['app']['App']['DBCommitTransaction'](arg1);
}
export function DBQueryIsolated(arg1, arg2, arg3) {
return window['go']['app']['App']['DBQueryIsolated'](arg1, arg2, arg3);
}
@@ -102,6 +106,10 @@ export function DBQueryMulti(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['DBQueryMulti'](arg1, arg2, arg3, arg4);
}
export function DBQueryMultiTransactional(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['DBQueryMultiTransactional'](arg1, arg2, arg3, arg4);
}
export function DBQueryWithCancel(arg1, arg2, arg3, arg4) {
return window['go']['app']['App']['DBQueryWithCancel'](arg1, arg2, arg3, arg4);
}
@@ -110,6 +118,10 @@ export function DBShowCreateTable(arg1, arg2, arg3) {
return window['go']['app']['App']['DBShowCreateTable'](arg1, arg2, arg3);
}
export function DBRollbackTransaction(arg1) {
return window['go']['app']['App']['DBRollbackTransaction'](arg1);
}
export function DataSync(arg1) {
return window['go']['app']['App']['DataSync'](arg1);
}

View File

@@ -981,6 +981,8 @@ export namespace connection {
fields?: string[];
messages?: string[];
queryId?: string;
transactionId?: string;
transactionPending?: boolean;
static createFrom(source: any = {}) {
return new QueryResult(source);
@@ -994,6 +996,8 @@ export namespace connection {
this.fields = source["fields"];
this.messages = source["messages"];
this.queryId = source["queryId"];
this.transactionId = source["transactionId"];
this.transactionPending = source["transactionPending"];
}
}