datafusion-ducklake-cli 0.1.0

Interactive and scriptable SQL shell for datafusion-ducklake-provider.
Documentation
#[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()
            }
        );
    }
}