🐛 fix(sql-error): 锁等待超时不再被误判为查询超时并给出无效建议

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 项必定失败,
  且失败信息正好复现原缺陷
This commit is contained in:
Syngnat
2026-07-26 22:17:04 +08:00
parent e6d77cd7ab
commit c5e25d2ff5
8 changed files with 99 additions and 0 deletions

View File

@@ -125,3 +125,54 @@ describe('formatSqlExecutionError', () => {
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');
}
});
});

View File

@@ -122,6 +122,36 @@ const SQL_ERROR_RULES: SqlErrorSemanticRule[] = [
/constraint failed/i,
],
},
{
// 必须排在 timeout_or_canceled 之前findSqlErrorSemantic 取首个命中的规则,
// 而那条规则的 /timeout/i 过宽,会把 "Lock wait timeout exceeded" 也吞成「查询超时」,
// 于是给出「检查执行计划、过滤条件和索引,必要时调整超时时间」这种完全无效的建议 ——
// 行锁等待超时的成因是另一个事务持锁,调大超时只会让用户等更久。
key: 'lock_contention',
fallbackLabel: 'Blocked by another transaction holding locks',
fallbackExplanation: 'Another uncommitted transaction holds locks on the target rows or table, so this statement waited past the lock timeout or was picked as a deadlock victim.',
fallbackSuggestion: 'Commit or roll back the pending transaction first — including this app\'s own uncommitted SQL editor transaction — then retry. Raising the query timeout does not help.',
patterns: [
// MySQL / MariaDB1205 锁等待超时、1213 死锁
/lock wait timeout exceeded/i,
/deadlock found when trying to get lock/i,
// PostgreSQL 系
/deadlock detected/i,
/canceling statement due to lock timeout/i,
/could not obtain lock/i,
// SQL Server1222 锁请求超时、1205 死锁牺牲者
/lock request time out period exceeded/i,
/was deadlocked on lock resources/i,
/deadlock victim/i,
// OracleORA-00060 死锁、ORA-00054/ORA-30006 资源忙
/ORA-00060/i,
/ORA-00054/i,
/ORA-30006/i,
// SQLite
/database is locked/i,
/database table is locked/i,
],
},
{
key: 'timeout_or_canceled',
fallbackLabel: 'Query timed out or was canceled',

View File

@@ -6724,6 +6724,9 @@
"query_editor.sql_error.rule.generic.explanation": "Die Datenbank hat einen Ausführungsfehler zurückgegeben, ohne dass ein spezifischerer Fehlertyp erkannt wurde.",
"query_editor.sql_error.rule.generic.label": "Datenbankausführungsfehler",
"query_editor.sql_error.rule.generic.suggestion": "Untersuche weiter mit dem ursprünglichen Fehler, dem SQL-Fragment und dem aktuellen Datenbankdialekt.",
"query_editor.sql_error.rule.lock_contention.explanation": "Eine andere, nicht committete Transaktion hält Sperren auf den Zielzeilen oder der Tabelle. Diese Anweisung hat daher das Sperr-Timeout überschritten oder wurde als Deadlock-Opfer ausgewählt.",
"query_editor.sql_error.rule.lock_contention.label": "Durch Sperren einer anderen Transaktion blockiert",
"query_editor.sql_error.rule.lock_contention.suggestion": "Committen oder rollen Sie die offene Transaktion zuerst zurück einschließlich der eigenen, nicht committeten Transaktion im SQL-Editor dieser Anwendung und versuchen Sie es erneut. Ein höheres Abfrage-Timeout hilft nicht.",
"query_editor.sql_error.rule.object_missing.explanation": "Das SQL verweist auf eine Tabelle, View, Sequenz oder ein anderes Datenbankobjekt, das in der aktuellen Datenbank oder im schema nicht gefunden wurde.",
"query_editor.sql_error.rule.object_missing.label": "Tabelle oder Objekt existiert nicht",
"query_editor.sql_error.rule.object_missing.suggestion": "Prüfe Objektname, Groß-/Kleinschreibung, schema/database-Präfix und ob die für diese Abfrage ausgewählte Datenbank korrekt ist.",

View File

@@ -6724,6 +6724,9 @@
"query_editor.sql_error.rule.generic.explanation": "The database returned an execution failure, and no more specific error type was matched.",
"query_editor.sql_error.rule.generic.label": "Database execution error",
"query_editor.sql_error.rule.generic.suggestion": "Continue troubleshooting with the raw error, SQL fragment, and current database dialect.",
"query_editor.sql_error.rule.lock_contention.explanation": "Another uncommitted transaction holds locks on the target rows or table, so this statement waited past the lock timeout or was picked as a deadlock victim.",
"query_editor.sql_error.rule.lock_contention.label": "Blocked by another transaction holding locks",
"query_editor.sql_error.rule.lock_contention.suggestion": "Commit or roll back the pending transaction first — including this app's own uncommitted SQL editor transaction — then retry. Raising the query timeout does not help.",
"query_editor.sql_error.rule.object_missing.explanation": "The SQL references a table, view, sequence, or other database object that cannot be found in the current database or schema.",
"query_editor.sql_error.rule.object_missing.label": "Table or object does not exist",
"query_editor.sql_error.rule.object_missing.suggestion": "Check the object name, casing, schema/database prefix, and whether the selected database for this query is correct.",

View File

@@ -6724,6 +6724,9 @@
"query_editor.sql_error.rule.generic.explanation": "データベースから実行失敗情報が返されましたが、より具体的なエラー種別には一致しませんでした。",
"query_editor.sql_error.rule.generic.label": "データベース実行エラー",
"query_editor.sql_error.rule.generic.suggestion": "元のエラー、SQL 断片、現在のデータベース方言を合わせて引き続き調査してください。",
"query_editor.sql_error.rule.lock_contention.explanation": "未コミットの別トランザクションが対象行またはテーブルのロックを保持しているため、本ステートメントはロックタイムアウトを超えて待機したか、デッドロックの犠牲者に選ばれました。",
"query_editor.sql_error.rule.lock_contention.label": "他のトランザクションのロックによりブロックされました",
"query_editor.sql_error.rule.lock_contention.suggestion": "先に未完了のトランザクション(本アプリの SQL エディターで未コミットのものを含む)をコミットまたはロールバックしてから再試行してください。クエリタイムアウトを延ばしても解決しません。",
"query_editor.sql_error.rule.object_missing.explanation": "SQL が、現在のデータベースまたは schema に存在しない表、ビュー、シーケンス、その他のデータベースオブジェクトを参照しています。",
"query_editor.sql_error.rule.object_missing.label": "表またはオブジェクトが存在しません",
"query_editor.sql_error.rule.object_missing.suggestion": "オブジェクト名、大文字小文字、schema/database プレフィックス、およびこのクエリで選択したデータベースが正しいか確認してください。",

View File

@@ -6724,6 +6724,9 @@
"query_editor.sql_error.rule.generic.explanation": "База данных вернула ошибку выполнения, но более конкретный тип ошибки не был определен.",
"query_editor.sql_error.rule.generic.label": "Ошибка выполнения в базе данных",
"query_editor.sql_error.rule.generic.suggestion": "Продолжайте диагностику по исходной ошибке, фрагменту SQL и текущему диалекту базы данных.",
"query_editor.sql_error.rule.lock_contention.explanation": "Другая незафиксированная транзакция удерживает блокировки целевых строк или таблицы, поэтому этот запрос превысил таймаут блокировки или был выбран жертвой взаимоблокировки.",
"query_editor.sql_error.rule.lock_contention.label": "Заблокировано блокировками другой транзакции",
"query_editor.sql_error.rule.lock_contention.suggestion": "Сначала зафиксируйте или откатите незавершённую транзакцию — включая собственную незафиксированную транзакцию в SQL-редакторе этого приложения — затем повторите. Увеличение таймаута запроса не поможет.",
"query_editor.sql_error.rule.object_missing.explanation": "SQL ссылается на таблицу, представление, последовательность или другой объект базы данных, которого нет в текущей базе или schema.",
"query_editor.sql_error.rule.object_missing.label": "Таблица или объект не существует",
"query_editor.sql_error.rule.object_missing.suggestion": "Проверьте имя объекта, регистр, префикс schema/database и правильность выбранной для запроса базы данных.",

View File

@@ -6724,6 +6724,9 @@
"query_editor.sql_error.rule.generic.explanation": "数据库返回了执行失败信息,当前未匹配到更具体的错误类型。",
"query_editor.sql_error.rule.generic.label": "数据库执行错误",
"query_editor.sql_error.rule.generic.suggestion": "结合原始错误、SQL 片段和当前数据库方言继续排查。",
"query_editor.sql_error.rule.lock_contention.explanation": "另一个未提交的事务正持有目标行或表的锁,本语句等待超过锁超时时间,或被选为死锁牺牲者。",
"query_editor.sql_error.rule.lock_contention.label": "被其他事务的锁阻塞",
"query_editor.sql_error.rule.lock_contention.suggestion": "先提交或回滚那个未完成的事务(也包括本应用 SQL 编辑器里自己未提交的事务),然后重试。调大查询超时时间无效。",
"query_editor.sql_error.rule.object_missing.explanation": "SQL 引用了当前库或 schema 中找不到的表、视图、序列或其他数据库对象。",
"query_editor.sql_error.rule.object_missing.label": "表或对象不存在",
"query_editor.sql_error.rule.object_missing.suggestion": "确认对象名称、大小写、schema/database 前缀,以及当前查询所选数据库是否正确。",

View File

@@ -6724,6 +6724,9 @@
"query_editor.sql_error.rule.generic.explanation": "資料庫傳回了執行失敗資訊,目前未匹配到更具體的錯誤類型。",
"query_editor.sql_error.rule.generic.label": "資料庫執行錯誤",
"query_editor.sql_error.rule.generic.suggestion": "結合原始錯誤、SQL 片段和目前資料庫方言繼續排查。",
"query_editor.sql_error.rule.lock_contention.explanation": "另一個未提交的交易正持有目標列或資料表的鎖,本語句等待超過鎖定超時時間,或被選為死鎖犧牲者。",
"query_editor.sql_error.rule.lock_contention.label": "被其他交易的鎖阻塞",
"query_editor.sql_error.rule.lock_contention.suggestion": "請先提交或回滾那個未完成的交易(也包括本應用 SQL 編輯器中自己未提交的交易),然後重試。調大查詢超時時間無效。",
"query_editor.sql_error.rule.object_missing.explanation": "SQL 引用了目前資料庫或 schema 中找不到的表、檢視、序列或其他資料庫物件。",
"query_editor.sql_error.rule.object_missing.label": "表或物件不存在",
"query_editor.sql_error.rule.object_missing.suggestion": "確認物件名稱、大小寫、schema/database 前綴,以及目前查詢所選資料庫是否正確。",