From bb588bb3cd895041c4276deabf1a278863f0877d Mon Sep 17 00:00:00 2001 From: Stiwar0098 Date: Fri, 26 Jun 2026 00:56:16 -0500 Subject: [PATCH] fix(mcp): classify compound routines as ddl --- .gitignore | 1 + src-tauri/src/ai_activity.rs | 67 ++++++++++++++++++++++++++++++ src-tauri/src/ai_activity_tests.rs | 66 +++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) diff --git a/.gitignore b/.gitignore index 47321a6e..0d87623b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ plugins/**/target .gitnexus .pi/ .atl/ +.opencode/ diff --git a/src-tauri/src/ai_activity.rs b/src-tauri/src/ai_activity.rs index b69a65c3..d4312062 100644 --- a/src-tauri/src/ai_activity.rs +++ b/src-tauri/src/ai_activity.rs @@ -360,6 +360,12 @@ pub fn classify_query_kind(sql: &str) -> &'static str { if trimmed.is_empty() { return "unknown"; } + let trimmed_upper = trimmed.to_uppercase(); + + if is_create_compound_routine(&trimmed_upper) { + return "ddl"; + } + // Fail closed for multi-statement payloads: a leading `SELECT 1; DROP …` // must NOT be tagged as a clean read just because the first keyword is // SELECT — the read-only and approval gates rely on this classification. @@ -402,6 +408,67 @@ pub fn classify_query_kind(sql: &str) -> &'static str { } } +fn is_create_compound_routine(upper_sql: &str) -> bool { + if first_keyword(upper_sql) != "CREATE" { + return false; + } + + let header = upper_sql.split("BEGIN").next().unwrap_or(upper_sql); + let is_routine = ["PROCEDURE", "FUNCTION", "TRIGGER", "EVENT"] + .iter() + .any(|keyword| contains_keyword(header, keyword)); + if !is_routine { + return false; + } + + let without_terminal_semicolon = upper_sql.trim_end().trim_end_matches(';').trim_end(); + if !without_terminal_semicolon.ends_with("END") { + return false; + } + + !has_complete_end_before_terminal_statement(without_terminal_semicolon) +} + +fn has_complete_end_before_terminal_statement(upper_sql: &str) -> bool { + let terminal_end = match upper_sql.rfind("END") { + Some(index) => index, + None => return false, + }; + let before_terminal = &upper_sql[..terminal_end]; + let bytes = before_terminal.as_bytes(); + let is_word = |b: u8| b.is_ascii_alphanumeric() || b == b'_'; + + for i in 0..bytes.len().saturating_sub(2) { + if &bytes[i..i + 3] != b"END" { + continue; + } + let prev_ok = i == 0 || !is_word(bytes[i - 1]); + let next_ok = i + 3 == bytes.len() || !is_word(bytes[i + 3]); + if !prev_ok || !next_ok { + continue; + } + + let mut cursor = i + 3; + while cursor < bytes.len() && bytes[cursor].is_ascii_whitespace() { + cursor += 1; + } + if cursor >= bytes.len() || bytes[cursor] != b';' { + continue; + } + cursor += 1; + while cursor < bytes.len() + && (bytes[cursor].is_ascii_whitespace() || bytes[cursor] == b';') + { + cursor += 1; + } + if cursor < bytes.len() && bytes[cursor].is_ascii_alphabetic() { + return true; + } + } + + false +} + /// Returns true when `stripped` contains a semicolon followed by additional /// non-whitespace SQL content — i.e., more than one statement. /// diff --git a/src-tauri/src/ai_activity_tests.rs b/src-tauri/src/ai_activity_tests.rs index fb54be65..72ddfa02 100644 --- a/src-tauri/src/ai_activity_tests.rs +++ b/src-tauri/src/ai_activity_tests.rs @@ -384,6 +384,72 @@ mod tests { #[test] fn classify_ddl() { assert_eq!(classify_query_kind("CREATE TABLE t (id INT)"), "ddl"); + assert_eq!( + classify_query_kind( + "CREATE PROCEDURE tabularis_fix_test_proc()\n\ + BEGIN\n\ + SELECT 1 AS ok;\n\ + END" + ), + "ddl" + ); + assert_eq!( + classify_query_kind( + "CREATE FUNCTION tabularis_fix_test_fn() RETURNS INT\n\ + BEGIN\n\ + RETURN 1;\n\ + END;" + ), + "ddl" + ); + assert_eq!( + classify_query_kind( + "CREATE TRIGGER tabularis_fix_test_trigger BEFORE INSERT ON t\n\ + FOR EACH ROW\n\ + BEGIN\n\ + SET NEW.id = NEW.id;\n\ + END;" + ), + "ddl" + ); + assert_eq!( + classify_query_kind( + "CREATE EVENT tabularis_fix_test_event\n\ + ON SCHEDULE EVERY 1 DAY\n\ + DO\n\ + BEGIN\n\ + SELECT 1;\n\ + END;" + ), + "ddl" + ); + assert_eq!( + classify_query_kind( + "CREATE PROCEDURE tabularis_fix_test_proc()\n\ + BEGIN\n\ + SELECT 1;\n\ + END; DROP TABLE t" + ), + "unknown" + ); + assert_eq!( + classify_query_kind( + "CREATE PROCEDURE tabularis_fix_test_proc()\n\ + BEGIN\n\ + SELECT 1;\n\ + END; DROP TABLE t; END" + ), + "unknown" + ); + assert_eq!( + classify_query_kind( + "CREATE PROCEDURE tabularis_fix_test_proc()\n\ + BEGIN\n\ + SELECT 1;\n\ + END;; DROP TABLE t; END" + ), + "unknown" + ); assert_eq!(classify_query_kind("DROP TABLE t"), "ddl"); assert_eq!(classify_query_kind("ALTER TABLE t ADD COLUMN x INT"), "ddl"); assert_eq!(classify_query_kind("TRUNCATE t"), "ddl");