use rudb_common::Result;
use rudb_parse::{Kind, tokenize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Statement<'a> {
sql: &'a str,
offset: usize,
}
impl<'a> Statement<'a> {
#[must_use]
pub fn sql(&self) -> &'a str {
self.sql
}
#[must_use]
pub fn offset(&self) -> usize {
self.offset
}
}
pub fn statements(script: &str) -> Result<Vec<Statement<'_>>> {
let tokens = tokenize(script)?;
let mut found = Vec::new();
let mut start: Option<usize> = None;
let mut end = 0usize;
for token in tokens {
match token.kind {
Kind::Terminator | Kind::EndOfInput => {
if let Some(at) = start.take() {
found.push(Statement { sql: &script[at..end], offset: at });
}
}
_ => {
if start.is_none() {
start = Some(token.start as usize);
}
end = token.end as usize;
}
}
}
Ok(found)
}
#[must_use]
pub fn is_complete(script: &str) -> bool {
let Ok(tokens) = tokenize(script) else {
return false;
};
tokens
.iter()
.rev()
.find(|token| !matches!(token.kind, Kind::EndOfInput))
.is_none_or(|token| matches!(token.kind, Kind::Terminator))
}
#[cfg(test)]
mod tests {
use super::{is_complete, statements};
fn split(script: &str) -> Vec<&str> {
statements(script).expect("tokenizes").into_iter().map(|found| found.sql()).collect()
}
#[test]
fn one_statement_with_no_terminator_is_a_statement() {
assert_eq!(split("SELECT 1"), vec!["SELECT 1"]);
}
#[test]
fn a_terminator_ends_a_statement() {
assert_eq!(split("SELECT 1; SELECT 2;"), vec!["SELECT 1", "SELECT 2"]);
}
#[test]
fn an_empty_script_holds_nothing() {
assert!(split("").is_empty());
assert!(split(" \n\t ").is_empty());
assert!(split(";;;").is_empty());
assert!(split("-- just a comment\n").is_empty());
assert!(split("/* and a block one */").is_empty());
}
#[test]
fn a_semicolon_in_a_string_is_not_a_boundary() {
assert_eq!(split("SELECT ';'"), vec!["SELECT ';'"]);
assert_eq!(split("SELECT $$a;b$$"), vec!["SELECT $$a;b$$"]);
}
#[test]
fn a_semicolon_in_a_comment_is_not_a_boundary() {
assert_eq!(split("SELECT 1 -- ; not this one\n"), vec!["SELECT 1"]);
assert_eq!(split("SELECT /* ; */ 1"), vec!["SELECT /* ; */ 1"]);
}
#[test]
fn comments_around_a_statement_are_not_part_of_it() {
assert_eq!(split("-- before\nSELECT 1; -- after\n"), vec!["SELECT 1"]);
}
#[test]
fn a_statement_is_complete_when_it_ends_in_a_semicolon() {
assert!(is_complete("SELECT 1;"));
assert!(is_complete("SELECT 1; SELECT 2; \n"));
assert!(!is_complete("SELECT 1"));
assert!(!is_complete("SELECT 1; SELECT 2"));
assert!(!is_complete("CREATE TABLE t ("));
}
#[test]
fn nothing_at_all_is_complete() {
assert!(is_complete(""));
assert!(is_complete(" \n "));
assert!(is_complete("-- a comment\n"));
}
#[test]
fn an_unclosed_string_is_not_complete() {
assert!(!is_complete("SELECT 'half"));
}
#[test]
fn the_offset_points_back_into_the_script() {
let script = "SELECT 1;\nSELECT 2;";
let found = statements(script).expect("tokenizes");
assert_eq!(found[0].offset(), 0);
assert_eq!(found[1].offset(), 10);
assert_eq!(&script[found[1].offset()..], "SELECT 2;");
}
}