mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 09:13:36 +08:00
MySQL 报 Error 1205 (HY000): Lock wait timeout exceeded 时,界面归类为「查询超时或被取消」, 建议「检查 SQL 执行计划、过滤条件和索引,必要时缩小查询范围或调整超时时间」—— 这套建议对行锁等待完全无效:成因是另一个事务持锁,调大超时只会让用户等更久。 根因:timeout_or_canceled 规则里有一条过宽的 /timeout/i,而 findSqlErrorSemantic 取首个 命中的规则,于是把锁等待超时也吞成了查询超时。 - 新增 lock_contention 规则并排在 timeout_or_canceled 之前,覆盖各方言的锁竞争与死锁: MySQL 1205/1213、PostgreSQL deadlock detected 与 lock timeout、 SQL Server 1222/1205、Oracle ORA-00060/00054/30006、SQLite database is locked - 文案改为指向真正的处置动作:先提交或回滚未完成的事务(含本应用 SQL 编辑器里自己未提交的 事务)再重试,并明确说明调大查询超时无效 - 6 个语言补齐 label/explanation/suggestion - 补 4 项测试:1205 不再归类为超时、建议不再提"调整超时"、各方言锁竞争均被识别、 普通查询超时不被新规则误吞。已确认清空新规则的 patterns 后 3 项必定失败, 且失败信息正好复现原缺陷
179 lines
8.6 KiB
TypeScript
179 lines
8.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import { formatSqlExecutionError } from './sqlErrorSemantics';
|
||
|
||
describe('formatSqlExecutionError', () => {
|
||
it('adds semantic explanation for SQL syntax errors and keeps raw text', () => {
|
||
const formatted = formatSqlExecutionError('pq: syntax error at or near "from"');
|
||
|
||
expect(formatted).toContain('Semantic meaning: SQL syntax error');
|
||
expect(formatted).toContain('Suggestion:');
|
||
expect(formatted).toContain('Raw error: pq: syntax error at or near "from"');
|
||
});
|
||
|
||
it('recognizes missing table errors', () => {
|
||
const formatted = formatSqlExecutionError('ERROR: relation "orders" does not exist');
|
||
|
||
expect(formatted).toContain('Semantic meaning: Table or object does not exist');
|
||
expect(formatted).toContain('Raw error: ERROR: relation "orders" does not exist');
|
||
});
|
||
|
||
it('recognizes duplicate key errors with statement prefix', () => {
|
||
const formatted = formatSqlExecutionError('Duplicate entry "1" for key "PRIMARY"', {
|
||
prefix: 'Statement 2 failed:',
|
||
});
|
||
|
||
expect(formatted.startsWith('Statement 2 failed:\nSemantic meaning: Unique constraint or primary key conflict')).toBe(true);
|
||
expect(formatted).toContain('Raw error: 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('Semantic meaning: Database execution error');
|
||
expect(formatted).toContain('Raw error: driver returned unexpected status 123');
|
||
});
|
||
|
||
it('recognizes driver bad connection during SQL execution as timeout semantics', () => {
|
||
const formatted = formatSqlExecutionError('第 1 条语句执行失败:driver: bad connection');
|
||
|
||
expect(formatted).toContain('Semantic meaning: Query timed out or was canceled');
|
||
expect(formatted).toContain('Raw error: 第 1 条语句执行失败:driver: bad connection');
|
||
});
|
||
|
||
it('recognizes localized connection-timeout wrappers as timeout semantics', () => {
|
||
const translate = (key: string, params?: Record<string, unknown>) => {
|
||
if (key === 'query_editor.sql_error.wrapper.semantic_line') {
|
||
return `SEM:${params?.label}|${params?.explanation}`;
|
||
}
|
||
if (key === 'query_editor.sql_error.wrapper.suggestion_line') {
|
||
return `SUG:${params?.suggestion}`;
|
||
}
|
||
if (key === 'query_editor.sql_error.wrapper.raw_line') {
|
||
return `RAW:${params?.error}`;
|
||
}
|
||
if (key === 'query_editor.sql_error.rule.timeout_or_canceled.label') {
|
||
return 'TIMEOUT_LABEL';
|
||
}
|
||
if (key === 'query_editor.sql_error.rule.timeout_or_canceled.explanation') {
|
||
return 'TIMEOUT_EXPLANATION';
|
||
}
|
||
if (key === 'query_editor.sql_error.rule.timeout_or_canceled.suggestion') {
|
||
return 'TIMEOUT_SUGGESTION';
|
||
}
|
||
if (key === 'query_editor.sql_error.rule.generic.label') {
|
||
return 'GENERIC_LABEL';
|
||
}
|
||
if (key === 'query_editor.sql_error.rule.generic.explanation') {
|
||
return 'GENERIC_EXPLANATION';
|
||
}
|
||
if (key === 'query_editor.sql_error.rule.generic.suggestion') {
|
||
return 'GENERIC_SUGGESTION';
|
||
}
|
||
return key;
|
||
};
|
||
|
||
const localizedTimeoutMessages = [
|
||
'\u8cc7\u6599\u5eab\u9023\u7dda\u903e\u6642\uff1amysql 127.0.0.1:3306/main\uff1a\u7db2\u8def\u903e\u6642',
|
||
'\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u63a5\u7d9a\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f: mysql 127.0.0.1:3306/main: \u30bf\u30a4\u30e0\u30a2\u30a6\u30c8',
|
||
'Zeit\u00fcberschreitung bei der Datenbankverbindung: mysql 127.0.0.1:3306/main: netzwerk-timeout',
|
||
'\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445: mysql 127.0.0.1:3306/main: \u0442\u0430\u0439\u043c-\u0430\u0443\u0442 \u0441\u0435\u0442\u0438',
|
||
] as const;
|
||
|
||
for (const raw of localizedTimeoutMessages) {
|
||
const formatted = formatSqlExecutionError(raw, { translate });
|
||
|
||
expect(formatted).toContain('SEM:TIMEOUT_LABEL|TIMEOUT_EXPLANATION');
|
||
expect(formatted).toContain('SUG:TIMEOUT_SUGGESTION');
|
||
expect(formatted).toContain(`RAW:${raw}`);
|
||
expect(formatted).not.toContain('SEM:GENERIC_LABEL|GENERIC_EXPLANATION');
|
||
}
|
||
});
|
||
|
||
it('localizes semantic wrapper copy with a supplied translator without translating raw database errors', () => {
|
||
const seen: Array<{ key: string; params?: Record<string, unknown> }> = [];
|
||
const formatted = formatSqlExecutionError('ERROR: relation "orders" does not exist', {
|
||
translate: (key, params) => {
|
||
seen.push({ key, params });
|
||
if (key === 'query_editor.sql_error.wrapper.semantic_line') {
|
||
return `语义:${params?.label}|${params?.explanation}`;
|
||
}
|
||
if (key === 'query_editor.sql_error.wrapper.suggestion_line') {
|
||
return `建议:${params?.suggestion}`;
|
||
}
|
||
if (key === 'query_editor.sql_error.wrapper.raw_line') {
|
||
return `RAW:${params?.error}`;
|
||
}
|
||
return `T:${key}`;
|
||
},
|
||
});
|
||
|
||
expect(formatted).toContain('语义:T:query_editor.sql_error.rule.object_missing.label|T:query_editor.sql_error.rule.object_missing.explanation');
|
||
expect(formatted).toContain('建议:T:query_editor.sql_error.rule.object_missing.suggestion');
|
||
expect(formatted).toContain('RAW:ERROR: relation "orders" does not exist');
|
||
expect(seen.map((entry) => entry.key)).toContain('query_editor.sql_error.rule.object_missing.label');
|
||
expect(seen.map((entry) => entry.key)).toContain('query_editor.sql_error.wrapper.raw_line');
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|
||
|
||
describe('锁竞争错误的语义分类', () => {
|
||
// 回归背景:timeout_or_canceled 规则的 /timeout/i 过宽,把 MySQL 的
|
||
// "Lock wait timeout exceeded" 也吞成「查询超时」,于是给出「检查执行计划、过滤条件和索引,
|
||
// 必要时调整超时时间」这种完全无效的建议 —— 行锁等待超时的成因是另一个事务持锁,
|
||
// 调大超时只会让用户等更久。lock_contention 规则必须先于 timeout 规则命中。
|
||
it('MySQL 1205 锁等待超时不再被归类为查询超时', () => {
|
||
const formatted = formatSqlExecutionError(
|
||
'Error 1205 (HY000): Lock wait timeout exceeded; try restarting transaction',
|
||
);
|
||
|
||
expect(formatted).toContain('Semantic meaning: Blocked by another transaction holding locks');
|
||
expect(formatted).not.toContain('Query timed out or was canceled');
|
||
expect(formatted).toContain('Raw error: Error 1205 (HY000): Lock wait timeout exceeded; try restarting transaction');
|
||
});
|
||
|
||
it('建议指向提交或回滚未完成事务,而不是调整超时', () => {
|
||
const formatted = formatSqlExecutionError('Lock wait timeout exceeded; try restarting transaction');
|
||
|
||
expect(formatted).toContain('Commit or roll back the pending transaction first');
|
||
expect(formatted).not.toContain('adjust the timeout');
|
||
});
|
||
|
||
it('覆盖各方言的锁竞争与死锁错误', () => {
|
||
const messages = [
|
||
'Error 1213 (40001): Deadlock found when trying to get lock; try restarting transaction',
|
||
'ERROR: deadlock detected',
|
||
'ERROR: canceling statement due to lock timeout',
|
||
'Lock request time out period exceeded.',
|
||
'Transaction (Process ID 52) was deadlocked on lock resources with another process',
|
||
'ORA-00060: deadlock detected while waiting for resource',
|
||
'ORA-00054: resource busy and acquire with NOWAIT specified',
|
||
'database is locked',
|
||
];
|
||
for (const message of messages) {
|
||
expect(
|
||
formatSqlExecutionError(message),
|
||
`未被识别为锁竞争:${message}`,
|
||
).toContain('Semantic meaning: Blocked by another transaction holding locks');
|
||
}
|
||
});
|
||
|
||
it('普通查询超时仍归类为查询超时,未被新规则误吞', () => {
|
||
for (const message of ['context deadline exceeded', 'sql: statement canceled', 'query execution timeout']) {
|
||
expect(
|
||
formatSqlExecutionError(message),
|
||
`被锁竞争规则误吞:${message}`,
|
||
).toContain('Semantic meaning: Query timed out or was canceled');
|
||
}
|
||
});
|
||
});
|