🐛 fix(editor): 修正 SQL 编辑器 DML 事务提交语义

- SQL 编辑器 DML 固定进入托管事务

- 区分 WITH SELECT 和 WITH DML 的事务判定

- 调整提交方式文案并补充前后端回归测试
This commit is contained in:
Syngnat
2026-06-10 18:05:46 +08:00
parent 61d71cf1d0
commit d8da8d6abf
7 changed files with 696 additions and 52 deletions

View File

@@ -18,6 +18,7 @@ import { applyQueryAutoLimit } from '../utils/queryAutoLimit';
import { extractQueryResultTableRef, type QueryResultTableRef } from '../utils/queryResultTable';
import { quoteIdentPart } from '../utils/sql';
import { formatSqlExecutionError } from '../utils/sqlErrorSemantics';
import { shouldUseSqlEditorManagedTransaction } from '../utils/sqlEditorTransaction';
import { findSqlStatementRanges, resolveCurrentSqlStatementRange, resolveExecutableSql } from '../utils/sqlStatementSelection';
import { isMacLikePlatform } from '../utils/appearance';
import { splitSidebarQualifiedName } from '../utils/sidebarLocate';
@@ -750,9 +751,6 @@ 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 秒' },
@@ -760,50 +758,6 @@ const SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS = [
{ 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';
@@ -5369,15 +5323,15 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
]}
/>
</Tooltip>
<Tooltip title="SQL 编辑器直接执行 INSERT/UPDATE/DELETE 等增删改语句时启用事务;手动提交更安全,自动提交会在执行成功后按所选时间提交。">
<Tooltip title="SQL 编辑器执行 INSERT/UPDATE/DELETE 等 DML 时始终启用事务;这里仅选择该事务执行成功后的提交方式。">
<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' },
{ label: '提交:手动', value: 'manual' },
{ label: '提交:自动', value: 'auto' },
]}
/>
</Tooltip>