fix(mysql): preserve nested routine blocks

This commit is contained in:
zipg authored and GitHub committed 2026-08-13 20:35:16 +08:00
1 parent d64908c596
commit 65cd4cfe28
5 files changed
+179 -19

No files matched your search

+78 -5
View File
@@ -1164,13 +1164,19 @@ fn is_mysql_routine_ddl_start(sql: &str) -> bool {
false
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum MysqlRoutineBlockType {
Begin,
Case,
}
fn mysql_routine_block_is_complete(sql: &str) -> bool {
if !starts_with_mysql_routine_block(sql) {
return false;
}
let tokens = mysql_routine_tokens(sql);
let mut begin_depth = 0usize;
let mut block_stack = Vec::new();
let mut saw_begin = false;
for (index, token) in tokens.iter().enumerate() {
@@ -1183,18 +1189,33 @@ fn mysql_routine_block_is_complete(sql: &str) -> bool {
continue;
}
saw_begin = true;
begin_depth += 1;
block_stack.push(MysqlRoutineBlockType::Begin);
continue;
}
if token.eq_ignore_ascii_case("CASE") {
if previous_mysql_routine_word(&tokens, index).is_some_and(|previous| previous.eq_ignore_ascii_case("END"))
{
continue;
}
block_stack.push(MysqlRoutineBlockType::Case);
continue;
}
if token.eq_ignore_ascii_case("END") && saw_begin {
if next_mysql_routine_word(&tokens, index).is_some_and(is_mysql_control_block_suffix) {
let suffix = next_mysql_routine_word(&tokens, index);
if suffix.is_some_and(|next| next.eq_ignore_ascii_case("CASE")) {
if block_stack.last() == Some(&MysqlRoutineBlockType::Case) {
block_stack.pop();
}
continue;
}
begin_depth = begin_depth.saturating_sub(1);
if suffix.is_some_and(is_mysql_control_block_suffix) {
continue;
}
block_stack.pop();
}
}
saw_begin && begin_depth == 0 && tokens.last().is_some_and(|token| token == ";")
saw_begin && block_stack.is_empty() && tokens.last().is_some_and(|token| token == ";")
}
fn is_mysql_control_block_suffix(token: &str) -> bool {
@@ -3189,6 +3210,58 @@ SELECT 2;";
);
}
#[test]
fn mysql_routine_without_delimiter_handles_case_endings() {
let expression = "\
CREATE PROCEDURE p_case()
BEGIN
INSERT INTO audit_log (status_text)
SELECT CASE WHEN active = 1 THEN 'active' ELSE 'inactive' END;
DELETE FROM stale_rows WHERE expires_at < NOW();
END;
SELECT 2;";
assert_eq!(
split_sql_statements_for_database(expression, DatabaseType::Mysql),
vec![
"CREATE PROCEDURE p_case()\nBEGIN\n INSERT INTO audit_log (status_text)\n SELECT CASE WHEN active = 1 THEN 'active' ELSE 'inactive' END;\n DELETE FROM stale_rows WHERE expires_at < NOW();\nEND",
"SELECT 2",
]
);
let statement = "\
CREATE PROCEDURE p_case_statement()
BEGIN
CASE WHEN active = 1 THEN SELECT 1; ELSE SELECT 0; END CASE;
DELETE FROM stale_rows WHERE expires_at < NOW();
END;
SELECT 2;";
assert_eq!(
split_sql_statements_for_database(statement, DatabaseType::Mysql),
vec![
"CREATE PROCEDURE p_case_statement()\nBEGIN\n CASE WHEN active = 1 THEN SELECT 1; ELSE SELECT 0; END CASE;\n DELETE FROM stale_rows WHERE expires_at < NOW();\nEND",
"SELECT 2",
]
);
}
#[test]
fn mysql_routine_without_delimiter_closes_nested_case_begin_blocks_in_order() {
let sql = "\
CREATE PROCEDURE p_nested_case()
BEGIN
CASE WHEN active = 1 THEN BEGIN SELECT 1; END; ELSE SELECT 0; END CASE;
DELETE FROM stale_rows WHERE expires_at < NOW();
END;
SELECT 2;";
assert_eq!(
split_sql_statements_for_database(sql, DatabaseType::Mysql),
vec![
"CREATE PROCEDURE p_nested_case()\nBEGIN\n CASE WHEN active = 1 THEN BEGIN SELECT 1; END; ELSE SELECT 0; END CASE;\n DELETE FROM stale_rows WHERE expires_at < NOW();\nEND",
"SELECT 2",
]
);
}
#[test]
fn mysql_regular_begin_transaction_still_splits_without_delimiter() {
assert_eq!(
+23 -9
View File
@@ -630,30 +630,44 @@ END
async fn live_mysql_splitter_executes_routine_without_delimiter() {
let url = std::env::var("DBX_LIVE_MYSQL_PROCEDURE_URL").expect("DBX_LIVE_MYSQL_PROCEDURE_URL");
let pool = dbx_core::db::mysql::connect(&url, std::time::Duration::from_secs(10)).await.unwrap();
let procedure = "dbx_issue_2695_proc";
let procedure = format!("dbx_routine_range_{}", uuid::Uuid::new_v4().simple());
let sql = format!(
"\
DROP PROCEDURE IF EXISTS {procedure};
CREATE PROCEDURE {procedure}()
BEGIN
SET @dbx_issue_2695_value = 2695;
SELECT @dbx_issue_2695_value AS value;
SET @dbx_routine_range_value = CASE WHEN 1 = 1 THEN 2695 ELSE 0 END;
SELECT @dbx_routine_range_value AS value;
END;
CALL {procedure}();
DROP PROCEDURE IF EXISTS {procedure};"
);
let statements = split_sql_statements_for_database(&sql, DatabaseType::Mysql);
assert_eq!(statements.len(), 4);
assert!(statements[1].contains("SET @dbx_issue_2695_value = 2695;"));
assert!(statements[1].contains("SELECT @dbx_issue_2695_value AS value;"));
assert!(statements[1].contains("CASE WHEN 1 = 1 THEN 2695 ELSE 0 END;"));
assert!(statements[1].contains("SELECT @dbx_routine_range_value AS value;"));
assert!(statements[1].ends_with("END"));
for statement in statements {
dbx_core::db::mysql::execute_query_with_max_rows(&pool, &statement, false, Some(10), Default::default())
.await
.unwrap();
let execution = async {
for statement in &statements[..3] {
dbx_core::db::mysql::execute_query_with_max_rows(&pool, statement, false, Some(10), Default::default())
.await?;
}
Ok::<(), String>(())
}
.await;
let cleanup = dbx_core::db::mysql::execute_query_with_max_rows(
&pool,
statements.last().expect("cleanup statement"),
false,
Some(10),
Default::default(),
)
.await;
execution.unwrap();
cleanup.unwrap();
}
#[tokio::test]