From fd9ca86fc1bceaa36018d4c45fc5e952e6aabc4b Mon Sep 17 00:00:00 2001 From: Roland Walker Date: Sat, 7 Feb 2026 08:16:26 -0500 Subject: [PATCH] give destructive warning on multi-table UPDATEs Only suppress the destructive-command warning for the simple case of a single-table UPDATE with a WHERE clause. --- changelog.md | 1 + mycli/packages/parseutils.py | 21 ++++++++++++++++++++- test/test_parseutils.py | 5 +++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 5b5e68a5..b4c89836 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ Features Bug Fixes -------- * Correct mangled schema info sent in LLM prompts. +* Give destructive warning on multi-table `UPDATE`s. 1.50.0 (2026/02/07) diff --git a/mycli/packages/parseutils.py b/mycli/packages/parseutils.py index b5d0d5b4..559b5a18 100644 --- a/mycli/packages/parseutils.py +++ b/mycli/packages/parseutils.py @@ -275,6 +275,25 @@ def query_has_where_clause(query: str) -> bool: return any(isinstance(token, sqlparse.sql.Where) for token_list in sqlparse.parse(query) for token in token_list) +# todo: handle "UPDATE LOW_PRIORITY" and "UPDATE IGNORE" +def query_is_single_table_update(query: str) -> bool: + """Check if a query is a simple single-table UPDATE.""" + cleaned_query = sqlparse.format(query, strip_comments=True) + if not cleaned_query: + return False + parsed = sqlparse.parse(cleaned_query) + if not parsed: + return False + statement = parsed[0] + return ( + statement[0].value.lower() == 'update' + and statement[1].is_whitespace + and ',' not in statement[2].value # multiple tables + and statement[3].is_whitespace + and statement[4].value.lower() == 'set' + ) + + def is_destructive(keywords: list[str], queries: str) -> bool: """Returns True if any of the queries in *queries* is destructive.""" for query in sqlparse.split(queries): @@ -282,7 +301,7 @@ def is_destructive(keywords: list[str], queries: str) -> bool: continue # subtle: if "UPDATE" is one of our keywords AND "query" starts with "UPDATE" if query_starts_with(query, keywords) and query_starts_with(query, ["update"]): - if query_has_where_clause(query): + if query_has_where_clause(query) and query_is_single_table_update(query): return False else: return True diff --git a/test/test_parseutils.py b/test/test_parseutils.py index eb3972c1..aa0b4632 100644 --- a/test/test_parseutils.py +++ b/test/test_parseutils.py @@ -157,6 +157,11 @@ def test_is_destructive_update_with_where_clause(): assert is_destructive(["update"], sql) is False +def test_is_destructive_update_multiple_tables_with_where_clause(): + sql = "use test;\nshow databases;\nUPDATE test, foo SET x = 1 WHERE id = 1;" + assert is_destructive(["update"], sql) is True + + def test_is_destructive_update_without_where_clause(): sql = "use test;\nshow databases;\nUPDATE test SET x = 1;" assert is_destructive(["update"], sql) is True