eugene 0.8.3

Careful with That Lock, Eugene
Documentation
use crate::error::InnerError;
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::{anychar, line_ending, multispace0, satisfy, space0};
use nom::combinator::{eof, map, peek, recognize};
use nom::multi::many_till;
use nom::sequence::{delimited, preceded, terminated};
use nom::IResult;

fn open_line_comment(s: &str) -> IResult<&str, &str> {
    terminated(tag("--"), space0)(s)
}

fn close_line_comment_or_eof(s: &str) -> IResult<&str, &str> {
    terminated(space0, alt((line_ending, eof)))(s)
}

fn file_name_char(c: char) -> bool {
    !c.is_whitespace() && c != ';' && c != ':'
}

fn ends_with_dot_sql(s: &str) -> IResult<&str, &str> {
    terminated(tag(".sql"), close_line_comment_or_eof)(s)
}

fn file_name(s: &str) -> IResult<&str, &str> {
    let (s, mut n) = recognize(many_till(satisfy(file_name_char), ends_with_dot_sql))(s)?;
    while let Some(c) = n.chars().last() {
        if c.is_whitespace() {
            n = &n[..n.len() - 1];
        } else {
            break;
        }
    }
    Ok((s, n))
}

fn file_colon(s: &str) -> IResult<&str, &str> {
    terminated(tag("file:"), space0)(s)
}

fn file_comment(s: &str) -> IResult<&str, &str> {
    let (s, _) = open_line_comment(s)?;
    let mut choice = alt((preceded(file_colon, file_name), file_name));
    let (s, file_name) = choice(s)?;
    Ok((s, file_name))
}

fn until_file_comment(s: &str) -> IResult<&str, &str> {
    recognize(many_till(anychar, alt((eof, peek(file_comment)))))(s)
}

fn file_comment_and_sql(s: &str) -> IResult<&str, (&str, &str)> {
    let (s, comment) = file_comment(s)?;
    let (s, sql) = until_file_comment(s)?;
    Ok((s, (sql, comment)))
}

fn script_section(s: &str) -> IResult<&str, (&str, &str)> {
    let (s, section) = recognize(file_comment_and_sql)(s)?;
    let (_, name) = file_comment(section)?;
    Ok((s, (name, section)))
}

pub fn break_into_files(s: &str) -> crate::Result<Vec<(Option<&str>, &str)>> {
    let each = delimited(
        multispace0,
        alt((
            map(script_section, |(name, sql)| (Some(name), sql)),
            map(until_file_comment, |sql| (None, sql)),
        )),
        multispace0,
    );

    many_till(each, eof)(s)
        .map(|(_, (files, _))| files)
        .map_err(|e| InnerError::ScriptParsingError(format!("{e:?}")).into())
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;

    #[test]
    fn parses_named_script_section() {
        let input = "-- file: foo.sql\nSELECT * FROM foo;";
        let result = script_section(input);
        assert_eq!(
            result,
            Ok(("", ("foo.sql", "-- file: foo.sql\nSELECT * FROM foo;")))
        );
    }

    #[test]
    fn parses_file_comment_and_sql() {
        let input = "-- file: foo.sql\nSELECT * FROM foo;";
        let result = file_comment_and_sql(input);
        assert_eq!(result, Ok(("", ("SELECT * FROM foo;", "foo.sql"))));
    }

    #[test]
    fn parses_until_file_comment() {
        let input = "select * from books; -- file: foo.sql";
        let result = until_file_comment(input);
        assert_eq!(result, Ok(("-- file: foo.sql", "select * from books; ")));
    }

    #[test]
    fn file_comment_examples() {
        let input = "-- foo.sql\nSELECT * FROM foo;";
        let result = file_comment(input);
        assert_eq!(result, Ok(("SELECT * FROM foo;", "foo.sql")));
        let input = "-- file:foo.sql\nSELECT * FROM foo;";
        let result = file_comment(input);
        assert_eq!(result, Ok(("SELECT * FROM foo;", "foo.sql")));
        let input = "-- file: foo.sql\nSELECT * FROM foo;";
        let result = file_comment(input);
        assert_eq!(result, Ok(("SELECT * FROM foo;", "foo.sql")));
    }

    #[test]
    fn when_input_has_no_file_comments() {
        let input = "SELECT * FROM foo;";
        let result = break_into_files(input).unwrap();
        assert_eq!(result, vec![(None, input)]);
    }

    #[test]
    fn when_input_has_single_file_comment() {
        let input = "-- foo.sql\nSELECT * FROM foo;";
        let result = break_into_files(input).unwrap();
        assert_eq!(
            result,
            vec![(Some("foo.sql"), "-- foo.sql\nSELECT * FROM foo;")]
        );
    }

    #[test]
    fn when_input_has_two_file_comments() {
        let input = "-- foo.sql\nSELECT * FROM foo;\n-- bar.sql\nSELECT * FROM bar;";
        let result = break_into_files(input).unwrap();
        assert_eq!(
            result,
            vec![
                (Some("foo.sql"), "-- foo.sql\nSELECT * FROM foo;\n"),
                (Some("bar.sql"), "-- bar.sql\nSELECT * FROM bar;")
            ]
        );
    }

    #[test]
    fn when_input_has_no_file_comment_but_other_comments() {
        let input = "-- eugene: ignore E3\nSELECT * FROM foo;";
        let result = break_into_files(input).unwrap();
        assert_eq!(result, vec![(None, input)]);
    }

    #[test]
    fn example_from_eugene_doc() {
        let example = "-- 1.sql

create table authors (
    id integer generated always as identity
        primary key,
    name text not null
);

-- 2.sql

set local lock_timeout = '2s';
alter table authors
    add column email text not null;
select count(*) from authors;
";
        let result = break_into_files(example).unwrap();
        let names = result.iter().map(|(name, _)| name).collect::<Vec<_>>();
        assert_eq!(names, vec![&Some("1.sql"), &Some("2.sql")]);
    }
}