#[derive(Debug, Eq, PartialEq)]
pub(crate) struct SplitResult {
pub statements: Vec<String>,
pub trailing: String,
}
pub(crate) fn split_script(sql: &str) -> Vec<String> {
let mut result = split_complete_statements(sql);
let trailing = result.trailing.trim();
if !trailing.is_empty() {
result.statements.push(trailing.to_string());
}
result.statements
}
pub(crate) fn split_complete_statements(sql: &str) -> SplitResult {
let mut statements = Vec::new();
let mut start = 0;
let mut index = 0;
let bytes = sql.as_bytes();
let mut state = SplitState::Normal;
while index < bytes.len() {
match state {
SplitState::Normal => match bytes[index] {
b'\'' => {
state = SplitState::SingleQuote;
index += 1;
}
b'"' => {
state = SplitState::DoubleQuote;
index += 1;
}
b'-' if bytes.get(index + 1) == Some(&b'-') => {
state = SplitState::LineComment;
index += 2;
}
b'/' if bytes.get(index + 1) == Some(&b'*') => {
state = SplitState::BlockComment;
index += 2;
}
b';' => {
push_statement(&mut statements, &sql[start..index]);
index += 1;
start = index;
}
_ => index += 1,
},
SplitState::SingleQuote => {
if bytes[index] == b'\'' {
if bytes.get(index + 1) == Some(&b'\'') {
index += 2;
} else {
state = SplitState::Normal;
index += 1;
}
} else {
index += 1;
}
}
SplitState::DoubleQuote => {
if bytes[index] == b'"' {
if bytes.get(index + 1) == Some(&b'"') {
index += 2;
} else {
state = SplitState::Normal;
index += 1;
}
} else {
index += 1;
}
}
SplitState::LineComment => {
if bytes[index] == b'\n' {
state = SplitState::Normal;
}
index += 1;
}
SplitState::BlockComment => {
if bytes[index] == b'*' && bytes.get(index + 1) == Some(&b'/') {
state = SplitState::Normal;
index += 2;
} else {
index += 1;
}
}
}
}
SplitResult {
statements,
trailing: sql[start..].to_string(),
}
}
fn push_statement(statements: &mut Vec<String>, raw: &str) {
let statement = raw.trim();
if !statement.is_empty() {
statements.push(statement.to_string());
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SplitState {
Normal,
SingleQuote,
DoubleQuote,
LineComment,
BlockComment,
}
#[cfg(test)]
mod tests {
use super::{SplitResult, split_complete_statements, split_script};
#[test]
fn splits_multiple_statements() {
assert_eq!(
split_script("SELECT 1; SELECT 2;"),
vec!["SELECT 1".to_string(), "SELECT 2".to_string()]
);
}
#[test]
fn ignores_semicolon_inside_string() {
assert_eq!(
split_script("SELECT ';' AS semi; SELECT 2"),
vec!["SELECT ';' AS semi".to_string(), "SELECT 2".to_string()]
);
}
#[test]
fn ignores_semicolon_inside_comments() {
assert_eq!(
split_script("SELECT 1; -- ;\n/* ; */ SELECT 2;"),
vec!["SELECT 1".to_string(), "-- ;\n/* ; */ SELECT 2".to_string()]
);
}
#[test]
fn returns_trailing_incomplete_statement() {
assert_eq!(
split_complete_statements("SELECT 1; SELECT"),
SplitResult {
statements: vec!["SELECT 1".to_string()],
trailing: " SELECT".to_string()
}
);
}
}