mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 18:39:54 +08:00
✨ feat(sql-editor): 增加SQL错误中文语义提示
- 新增 SQL 执行错误语义化规则,覆盖语法、对象、字段、约束和连接类错误 - 执行失败和刷新失败展示中文语义、处理建议与原始错误 - 补充工具函数与 QueryEditor 回归测试,确保英文报错可读化
This commit is contained in:
@@ -1575,6 +1575,32 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(messageApi.success).toHaveBeenCalledWith('SQL 文件已导出!');
|
||||
});
|
||||
|
||||
it('shows Chinese semantic meaning for SQL execution errors', async () => {
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: false,
|
||||
message: 'pq: syntax error at or near "from"',
|
||||
});
|
||||
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ query: 'SELECT * from' })} />);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await findButton(renderer, '运行').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const pageText = textContent(renderer!.root);
|
||||
expect(pageText).toContain('执行失败');
|
||||
expect(pageText).toContain('中文语义:SQL 语法错误');
|
||||
expect(pageText).toContain('处理建议:');
|
||||
expect(pageText).toContain('原始错误:pq: syntax error at or near "from"');
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
@@ -17,6 +17,7 @@ import { isOracleLikeDialect, resolveSqlDialect, resolveSqlFunctions, resolveSql
|
||||
import { applyQueryAutoLimit } from '../utils/queryAutoLimit';
|
||||
import { extractQueryResultTableRef, type QueryResultTableRef } from '../utils/queryResultTable';
|
||||
import { quoteIdentPart } from '../utils/sql';
|
||||
import { formatSqlExecutionError } from '../utils/sqlErrorSemantics';
|
||||
import { resolveCurrentSqlStatementRange, resolveExecutableSql } from '../utils/sqlStatementSelection';
|
||||
import { isMacLikePlatform } from '../utils/appearance';
|
||||
import { splitSidebarQualifiedName } from '../utils/sidebarLocate';
|
||||
@@ -4031,7 +4032,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
}
|
||||
const res = await DBQueryMulti(buildRpcConnectionConfig(config) as any, currentDb, sql, queryId);
|
||||
if (!res?.success) {
|
||||
message.error('刷新失败: ' + (res?.message || '未知错误'));
|
||||
message.error('刷新失败: ' + formatSqlExecutionError(res?.message || '未知错误'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4073,7 +4074,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
: rs
|
||||
));
|
||||
} catch (err: any) {
|
||||
message.error('刷新失败: ' + (err?.message || '未知错误'));
|
||||
message.error('刷新失败: ' + formatSqlExecutionError(err?.message || err || '未知错误'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -4172,7 +4173,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
if (shellConvert.recognized) {
|
||||
if (shellConvert.error) {
|
||||
const prefix = statements.length > 1 ? `第 ${idx + 1} 条语句执行失败:` : '';
|
||||
setExecutionError(prefix + shellConvert.error);
|
||||
setExecutionError(formatSqlExecutionError(shellConvert.error, { prefix }));
|
||||
setResultSets([]);
|
||||
setActiveResultKey('');
|
||||
return;
|
||||
@@ -4211,7 +4212,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
});
|
||||
if (!res.success) {
|
||||
const prefix = statements.length > 1 ? `第 ${idx + 1} 条语句执行失败:` : '';
|
||||
setExecutionError(prefix + res.message);
|
||||
setExecutionError(formatSqlExecutionError(res.message, { prefix }));
|
||||
setResultSets([]);
|
||||
setActiveResultKey('');
|
||||
return;
|
||||
@@ -4376,7 +4377,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return;
|
||||
}
|
||||
|
||||
setExecutionError(res.message);
|
||||
setExecutionError(formatSqlExecutionError(res.message));
|
||||
setResultSets([]);
|
||||
setActiveResultKey('');
|
||||
return;
|
||||
@@ -4513,7 +4514,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error("Error executing query: " + e.message);
|
||||
const formattedError = formatSqlExecutionError(e?.message || e);
|
||||
message.error("执行失败: " + formattedError);
|
||||
addSqlLog({
|
||||
id: `log-${Date.now()}-error`,
|
||||
timestamp: Date.now(),
|
||||
@@ -4523,6 +4525,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
message: e.message,
|
||||
dbName: currentDb
|
||||
});
|
||||
setExecutionError(formattedError);
|
||||
setResultSets([]);
|
||||
setActiveResultKey('');
|
||||
} finally {
|
||||
|
||||
46
frontend/src/utils/sqlErrorSemantics.test.ts
Normal file
46
frontend/src/utils/sqlErrorSemantics.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatSqlExecutionError } from './sqlErrorSemantics';
|
||||
|
||||
describe('formatSqlExecutionError', () => {
|
||||
it('adds Chinese semantic explanation for SQL syntax errors and keeps raw text', () => {
|
||||
const formatted = formatSqlExecutionError('pq: syntax error at or near "from"');
|
||||
|
||||
expect(formatted).toContain('中文语义:SQL 语法错误');
|
||||
expect(formatted).toContain('处理建议:');
|
||||
expect(formatted).toContain('原始错误:pq: syntax error at or near "from"');
|
||||
});
|
||||
|
||||
it('recognizes missing table errors', () => {
|
||||
const formatted = formatSqlExecutionError('ERROR: relation "orders" does not exist');
|
||||
|
||||
expect(formatted).toContain('中文语义:表或对象不存在');
|
||||
expect(formatted).toContain('原始错误:ERROR: relation "orders" does not exist');
|
||||
});
|
||||
|
||||
it('recognizes duplicate key errors with statement prefix', () => {
|
||||
const formatted = formatSqlExecutionError('Duplicate entry "1" for key "PRIMARY"', {
|
||||
prefix: '第 2 条语句执行失败:',
|
||||
});
|
||||
|
||||
expect(formatted.startsWith('第 2 条语句执行失败:\n中文语义:唯一约束或主键冲突')).toBe(true);
|
||||
expect(formatted).toContain('原始错误:Duplicate entry "1" for key "PRIMARY"');
|
||||
});
|
||||
|
||||
it('falls back to a generic database execution error', () => {
|
||||
const formatted = formatSqlExecutionError('driver returned unexpected status 123');
|
||||
|
||||
expect(formatted).toContain('中文语义:数据库执行错误');
|
||||
expect(formatted).toContain('原始错误:driver returned unexpected status 123');
|
||||
});
|
||||
|
||||
it('does not format an already formatted message again', () => {
|
||||
const raw = [
|
||||
'中文语义:SQL 语法错误。通常是关键字、逗号、括号、引号、语句顺序或当前数据库方言不匹配。',
|
||||
'处理建议:检查报错位置附近的 SQL 片段,并确认当前连接的数据源类型与 SQL 方言一致。',
|
||||
'原始错误:pq: syntax error at or near "from"',
|
||||
].join('\n');
|
||||
|
||||
expect(formatSqlExecutionError(raw)).toBe(raw);
|
||||
});
|
||||
});
|
||||
177
frontend/src/utils/sqlErrorSemantics.ts
Normal file
177
frontend/src/utils/sqlErrorSemantics.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
export type SqlExecutionErrorFormatOptions = {
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
type SqlErrorSemanticRule = {
|
||||
label: string;
|
||||
explanation: string;
|
||||
suggestion: string;
|
||||
patterns: RegExp[];
|
||||
};
|
||||
|
||||
const SQL_ERROR_RULES: SqlErrorSemanticRule[] = [
|
||||
{
|
||||
label: 'SQL 语法错误',
|
||||
explanation: '通常是关键字、逗号、括号、引号、语句顺序或当前数据库方言不匹配。',
|
||||
suggestion: '检查报错位置附近的 SQL 片段,并确认当前连接的数据源类型与 SQL 方言一致。',
|
||||
patterns: [
|
||||
/syntax error/i,
|
||||
/sql syntax/i,
|
||||
/sqlstate\s*42601/i,
|
||||
/near\s+["'`].+["'`]\s*:?\s*syntax error/i,
|
||||
/ora-00933/i,
|
||||
/ora-00936/i,
|
||||
/you have an error in your sql syntax/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '表或对象不存在',
|
||||
explanation: 'SQL 引用了当前库或 schema 中找不到的表、视图、序列或其他数据库对象。',
|
||||
suggestion: '确认对象名称、大小写、schema/database 前缀,以及当前查询所选数据库是否正确。',
|
||||
patterns: [
|
||||
/relation\s+["'`].+["'`]\s+does not exist/i,
|
||||
/table\s+.+doesn'?t exist/i,
|
||||
/no such table/i,
|
||||
/invalid object name/i,
|
||||
/ora-00942/i,
|
||||
/object\s+.+does not exist/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '字段不存在',
|
||||
explanation: 'SQL 引用了结果集中不存在、拼写不一致或当前表没有的字段。',
|
||||
suggestion: '检查字段名、别名、大小写、引用表别名,以及字段是否属于当前 FROM/JOIN 的对象。',
|
||||
patterns: [
|
||||
/column\s+["'`].+["'`]\s+does not exist/i,
|
||||
/unknown column/i,
|
||||
/invalid column name/i,
|
||||
/ora-00904/i,
|
||||
/no such column/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '唯一约束或主键冲突',
|
||||
explanation: '插入或更新的数据与唯一索引、主键或唯一约束中的已有数据重复。',
|
||||
suggestion: '检查重复键值,必要时改为 UPDATE、UPSERT,或调整唯一键字段值。',
|
||||
patterns: [
|
||||
/duplicate key/i,
|
||||
/duplicate entry/i,
|
||||
/unique constraint failed/i,
|
||||
/violates unique constraint/i,
|
||||
/ora-00001/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '权限不足',
|
||||
explanation: '当前数据库账号没有执行该 SQL 或访问相关对象的权限。',
|
||||
suggestion: '确认账号权限、schema 授权、只读连接限制,以及是否需要由管理员授权。',
|
||||
patterns: [
|
||||
/permission denied/i,
|
||||
/access denied/i,
|
||||
/not authorized/i,
|
||||
/insufficient privileges/i,
|
||||
/ora-01031/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据类型或格式不匹配',
|
||||
explanation: '写入、比较或转换的数据格式不符合目标字段或表达式要求。',
|
||||
suggestion: '检查日期、数字、布尔值、枚举值、隐式转换和字段类型,必要时显式 CAST。',
|
||||
patterns: [
|
||||
/invalid input syntax/i,
|
||||
/incorrect\s+.+\s+value/i,
|
||||
/data truncated/i,
|
||||
/truncated incorrect/i,
|
||||
/conversion failed/i,
|
||||
/invalid number/i,
|
||||
/ora-01722/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '约束校验失败',
|
||||
explanation: '数据不满足外键、非空、检查约束或引用完整性规则。',
|
||||
suggestion: '检查关联父表记录、必填字段、CHECK 条件,以及写入顺序是否正确。',
|
||||
patterns: [
|
||||
/foreign key constraint/i,
|
||||
/violates foreign key constraint/i,
|
||||
/cannot be null/i,
|
||||
/not null constraint failed/i,
|
||||
/check constraint/i,
|
||||
/constraint failed/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '查询超时或被取消',
|
||||
explanation: 'SQL 执行时间超过超时限制,或执行过程被手动取消。',
|
||||
suggestion: '检查 SQL 执行计划、过滤条件和索引,必要时缩小查询范围或调整超时时间。',
|
||||
patterns: [
|
||||
/context deadline exceeded/i,
|
||||
/statement canceled/i,
|
||||
/statement cancelled/i,
|
||||
/context canceled/i,
|
||||
/context cancelled/i,
|
||||
/timeout/i,
|
||||
/timed out/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '数据库连接或认证失败',
|
||||
explanation: '客户端无法连接数据库,或认证信息、网络、实例状态存在问题。',
|
||||
suggestion: '检查主机、端口、账号密码、网络连通性、代理/SSH 隧道和数据库服务状态。',
|
||||
patterns: [
|
||||
/password authentication failed/i,
|
||||
/connection refused/i,
|
||||
/no route to host/i,
|
||||
/server has gone away/i,
|
||||
/too many connections/i,
|
||||
/connection reset/i,
|
||||
/connection timeout/i,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const normalizeErrorText = (raw: unknown): string => {
|
||||
if (raw instanceof Error) {
|
||||
return raw.message || String(raw);
|
||||
}
|
||||
if (typeof raw === 'string') {
|
||||
return raw;
|
||||
}
|
||||
if (raw == null) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(raw);
|
||||
} catch {
|
||||
return String(raw);
|
||||
}
|
||||
};
|
||||
|
||||
const findSqlErrorSemantic = (message: string): SqlErrorSemanticRule | null => {
|
||||
const text = String(message || '');
|
||||
return SQL_ERROR_RULES.find((rule) => rule.patterns.some((pattern) => pattern.test(text))) || null;
|
||||
};
|
||||
|
||||
export const formatSqlExecutionError = (
|
||||
raw: unknown,
|
||||
options: SqlExecutionErrorFormatOptions = {},
|
||||
): string => {
|
||||
const rawMessage = normalizeErrorText(raw).trim() || '未知错误';
|
||||
if (/中文语义:/.test(rawMessage) && /原始错误:/.test(rawMessage)) {
|
||||
return rawMessage;
|
||||
}
|
||||
|
||||
const semantic = findSqlErrorSemantic(rawMessage) || {
|
||||
label: '数据库执行错误',
|
||||
explanation: '数据库返回了执行失败信息,当前未匹配到更具体的错误类型。',
|
||||
suggestion: '结合原始错误、SQL 片段和当前数据库方言继续排查。',
|
||||
};
|
||||
const prefix = String(options.prefix || '').trim();
|
||||
|
||||
return [
|
||||
prefix,
|
||||
`中文语义:${semantic.label}。${semantic.explanation}`,
|
||||
`处理建议:${semantic.suggestion}`,
|
||||
`原始错误:${rawMessage}`,
|
||||
].filter(Boolean).join('\n');
|
||||
};
|
||||
Reference in New Issue
Block a user