diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index f0bc01bf5..eaad47767 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -6,5 +6,6 @@ pub mod query; pub mod query_cancel; pub mod redis_cmd; pub mod schema; +mod sql_file; pub mod transfer; pub mod update; diff --git a/src-tauri/src/commands/sql_file.rs b/src-tauri/src/commands/sql_file.rs new file mode 100644 index 000000000..9a7b043ba --- /dev/null +++ b/src-tauri/src/commands/sql_file.rs @@ -0,0 +1,143 @@ +#[derive(Default)] +struct SqlStatementSplitter { + buffer: String, + in_single_quote: bool, + in_double_quote: bool, + in_backtick: bool, + in_line_comment: bool, + in_block_comment: bool, + previous: Option, +} + +impl SqlStatementSplitter { + fn push_chunk(&mut self, chunk: &str) -> Vec { + let mut statements = Vec::new(); + let mut chars = chunk.chars().peekable(); + + while let Some(ch) = chars.next() { + let next = chars.peek().copied(); + + if self.in_line_comment { + self.buffer.push(ch); + if ch == '\n' { + self.in_line_comment = false; + } + self.previous = Some(ch); + continue; + } + + if self.in_block_comment { + self.buffer.push(ch); + if self.previous == Some('*') && ch == '/' { + self.in_block_comment = false; + } + self.previous = Some(ch); + continue; + } + + if !self.in_single_quote && !self.in_double_quote && !self.in_backtick { + if ch == '-' && next == Some('-') { + self.in_line_comment = true; + self.buffer.push(ch); + self.previous = Some(ch); + continue; + } + if ch == '/' && next == Some('*') { + self.in_block_comment = true; + self.buffer.push(ch); + self.previous = Some(ch); + continue; + } + } + + match ch { + '\'' if !self.in_double_quote + && !self.in_backtick + && self.previous != Some('\\') => + { + self.in_single_quote = !self.in_single_quote; + self.buffer.push(ch); + } + '"' if !self.in_single_quote + && !self.in_backtick + && self.previous != Some('\\') => + { + self.in_double_quote = !self.in_double_quote; + self.buffer.push(ch); + } + '`' if !self.in_single_quote && !self.in_double_quote => { + self.in_backtick = !self.in_backtick; + self.buffer.push(ch); + } + ';' if !self.in_single_quote && !self.in_double_quote && !self.in_backtick => { + self.push_current_statement(&mut statements); + } + _ => self.buffer.push(ch), + } + + self.previous = Some(ch); + } + + statements + } + + fn finish(mut self) -> Vec { + let mut statements = Vec::new(); + self.push_current_statement(&mut statements); + statements + } + + fn push_current_statement(&mut self, statements: &mut Vec) { + let statement = self.buffer.trim(); + if !statement.is_empty() { + statements.push(statement.to_string()); + } + self.buffer.clear(); + self.previous = None; + } +} + +#[cfg(test)] +fn split_sql_script(sql: &str) -> Result, String> { + let mut splitter = SqlStatementSplitter::default(); + let mut statements = splitter.push_chunk(sql); + statements.extend(splitter.finish()); + Ok(statements) +} + +#[cfg(test)] +mod tests { + use super::split_sql_script; + + #[test] + fn splits_semicolon_delimited_statements() { + assert_eq!( + split_sql_script("CREATE TABLE a(id int); INSERT INTO a VALUES (1);").unwrap(), + vec!["CREATE TABLE a(id int)", "INSERT INTO a VALUES (1)"] + ); + } + + #[test] + fn ignores_semicolons_inside_quotes_and_comments() { + let sql = "\ + INSERT INTO logs VALUES ('a;b', \"c;d\", `weird;name`);\n\ + -- comment ; ignored\n\ + /* block ; ignored */\n\ + SELECT 1;"; + assert_eq!( + split_sql_script(sql).unwrap(), + vec![ + "INSERT INTO logs VALUES ('a;b', \"c;d\", `weird;name`)", + "-- comment ; ignored\n/* block ; ignored */\nSELECT 1", + ] + ); + } + + #[test] + fn emits_trailing_statement_without_semicolon() { + assert_eq!( + split_sql_script("CREATE TABLE a(id int);\nINSERT INTO a VALUES (1)").unwrap(), + vec!["CREATE TABLE a(id int)", "INSERT INTO a VALUES (1)"] + ); + } +}