From c5e25d2ff5365b9859af0587cb5c198c6a740f84 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Sun, 26 Jul 2026 22:17:04 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sql-error):=20=E9=94=81?= =?UTF-8?q?=E7=AD=89=E5=BE=85=E8=B6=85=E6=97=B6=E4=B8=8D=E5=86=8D=E8=A2=AB?= =?UTF-8?q?=E8=AF=AF=E5=88=A4=E4=B8=BA=E6=9F=A5=E8=AF=A2=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E5=B9=B6=E7=BB=99=E5=87=BA=E6=97=A0=E6=95=88=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 项必定失败, 且失败信息正好复现原缺陷 --- frontend/src/utils/sqlErrorSemantics.test.ts | 51 ++++++++++++++++++++ frontend/src/utils/sqlErrorSemantics.ts | 30 ++++++++++++ shared/i18n/de-DE.json | 3 ++ shared/i18n/en-US.json | 3 ++ shared/i18n/ja-JP.json | 3 ++ shared/i18n/ru-RU.json | 3 ++ shared/i18n/zh-CN.json | 3 ++ shared/i18n/zh-TW.json | 3 ++ 8 files changed, 99 insertions(+) diff --git a/frontend/src/utils/sqlErrorSemantics.test.ts b/frontend/src/utils/sqlErrorSemantics.test.ts index 0ef7d010..4e1135f8 100644 --- a/frontend/src/utils/sqlErrorSemantics.test.ts +++ b/frontend/src/utils/sqlErrorSemantics.test.ts @@ -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'); + } + }); +}); diff --git a/frontend/src/utils/sqlErrorSemantics.ts b/frontend/src/utils/sqlErrorSemantics.ts index f6f5f8c2..cd168dea 100644 --- a/frontend/src/utils/sqlErrorSemantics.ts +++ b/frontend/src/utils/sqlErrorSemantics.ts @@ -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 / MariaDB:1205 锁等待超时、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 Server:1222 锁请求超时、1205 死锁牺牲者 + /lock request time out period exceeded/i, + /was deadlocked on lock resources/i, + /deadlock victim/i, + // Oracle:ORA-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', diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index e307b24a..2c8daeac 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -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.", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index 20596024..5629520d 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -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.", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index 281195a6..e91a73e7 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -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 プレフィックス、およびこのクエリで選択したデータベースが正しいか確認してください。", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index 1a08a53f..d3549a45 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -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 и правильность выбранной для запроса базы данных.", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 24e0a456..8fc2f58e 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -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 前缀,以及当前查询所选数据库是否正确。", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index 45df3444..97897da6 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -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 前綴,以及目前查詢所選資料庫是否正確。",