fosk 0.2.0

In-memory SQL-like query engine and lightweight data store for testing and prototyping.
Documentation
use crate::parser::{
    ParseError, Phase, QueryParser,
    ast::{Collection, Predicate},
};

#[derive(Debug, Clone, PartialEq)]
pub enum JoinType {
    Inner,
    Left,
    Right,
    Full,
}

impl JoinType {
    pub fn parse(parser: &mut QueryParser) -> Result<JoinType, ParseError> {
        if parser.comparers.join.compare(parser) {
            parser.jump(parser.comparers.join.length);
            return Ok(JoinType::Inner);
        }

        if parser.comparers.inner_join.compare(parser) {
            parser.jump(parser.comparers.inner_join.length);
            return Ok(JoinType::Inner);
        }

        if parser.comparers.left_join.compare(parser) {
            parser.jump(parser.comparers.left_join.length);
            return Ok(JoinType::Left);
        }

        if parser.comparers.right_join.compare(parser) {
            parser.jump(parser.comparers.right_join.length);
            return Ok(JoinType::Right);
        }

        if parser.comparers.full_join.compare(parser) {
            parser.jump(parser.comparers.full_join.length);
            return Ok(JoinType::Full);
        }

        ParseError::new("Invalid Join type", parser.position, parser).err()
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct Join {
    pub join_type: JoinType,
    pub collection: Collection,
    pub predicate: Predicate,
}

impl Join {
    pub fn parse(parser: &mut QueryParser) -> Result<Vec<Join>, ParseError> {
        let mut joins: Vec<Join> = vec![];
        while parser.phase == Phase::Joins {
            let join_type = JoinType::parse(parser)?;
            let collection = Collection::parse(parser)?;

            if parser.comparers.on.compare(parser) {
                parser.jump(parser.comparers.on.length);
            } else {
                return ParseError::new("Invalid join statement", parser.position, parser).err();
            }

            let predicate = Predicate::parse(parser, false)?;

            joins.push(Join {
                join_type,
                collection,
                predicate,
            });
        }

        Ok(joins)
    }
}

#[cfg(test)]
mod tests {
    use crate::parser::{
        QueryParser,
        ast::{Collection, Join, JoinType, Predicate},
    };

    fn parse_joins(text: &str) -> Vec<Join> {
        let mut parser = QueryParser::new(text);
        assert!(parser.check_next_phase());
        match Join::parse(&mut parser) {
            Ok(joins) => joins,
            Err(err) => panic!("expected join parse from {text:?}, got {err:?}"),
        }
    }

    fn table_name_and_alias(collection: &Collection) -> (&str, Option<&str>) {
        match collection {
            Collection::Table { name, alias } => (name.as_str(), alias.as_deref()),
            Collection::Query { .. } => panic!("expected table collection, got {collection:?}"),
        }
    }

    fn parse_join_error(text: &str) -> (usize, usize, String) {
        let mut parser = QueryParser::new(text);
        parser.check_next_phase();
        match Join::parse(&mut parser) {
            Ok(joins) => panic!("expected join error from {text:?}, got {joins:?}"),
            Err(err) => (err.start, err.end, err.text),
        }
    }

    #[test]
    pub fn test_inner_join() {
        let text = "INNER JOIN tableA ON tableA.columnA = tableB.columnA";
        let result = parse_joins(text);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].join_type, JoinType::Inner);
        assert_eq!(
            table_name_and_alias(&result[0].collection),
            ("tableA", None)
        );
        assert!(matches!(result[0].predicate, Predicate::Compare { .. }));
    }

    #[test]
    pub fn test_inner_join_two_predicates() {
        let text = "INNER JOIN tableA ON tableA.columnA = tableB.columnA AND tableA.columnB = tableB.columnB";
        let result = parse_joins(text);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].join_type, JoinType::Inner);
        assert_eq!(
            table_name_and_alias(&result[0].collection),
            ("tableA", None)
        );
        assert!(matches!(result[0].predicate, Predicate::And(_)));
    }

    #[test]
    pub fn test_inner_join_with_alias_and_two_predicates() {
        let text =
            "INNER JOIN tableA a ON a.columnA = tableB.columnA AND a.columnB = tableB.columnB";
        let result = parse_joins(text);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].join_type, JoinType::Inner);
        assert_eq!(
            table_name_and_alias(&result[0].collection),
            ("tableA", Some("a"))
        );
    }

    #[test]
    pub fn test_inner_join_and_left_join() {
        let text = r#"INNER JOIN tableA ON tableA.columnA = tableB.columnA
        LEFT JOIN tableC ON tableC.columnB = tableA.columnB"#;
        let result = parse_joins(text);
        assert_eq!(result.len(), 2);

        let expect_names = ["tableA", "tableC"];
        let expect_types = [JoinType::Inner, JoinType::Left];

        for (i, item) in result.iter().enumerate() {
            assert_eq!(item.join_type, expect_types[i]);
            assert_eq!(
                table_name_and_alias(&item.collection),
                (expect_names[i], None)
            );
        }
    }

    #[test]
    pub fn test_all_joins() {
        let text = r#"
        INNER JOIN tableA ON tableA.columnA = tableB.columnA
        LEFT JOIN tableC ON tableC.columnB = tableA.columnB
        RIGHT JOIN tableD ON tableD.columnB = tableC.columnB
        FULL JOIN tableE ON tableE.columnB = tableA.columnB
        "#;

        let result = parse_joins(text);
        assert_eq!(result.len(), 4);

        let expect_names = ["tableA", "tableC", "tableD", "tableE"];
        let expect_types = [
            JoinType::Inner,
            JoinType::Left,
            JoinType::Right,
            JoinType::Full,
        ];

        for (i, item) in result.iter().enumerate() {
            assert_eq!(item.join_type, expect_types[i]);
            assert_eq!(
                table_name_and_alias(&item.collection),
                (expect_names[i], None)
            );
        }
    }

    #[test]
    fn short_join_keyword_is_inner_join() {
        let result = parse_joins("JOIN tableA ON tableA.id = tableB.id");
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].join_type, JoinType::Inner);
    }

    #[test]
    fn join_rejects_missing_on_clause() {
        let (start, end, text) = parse_join_error("INNER JOIN tableA WHERE tableA.id = tableB.id");
        assert_eq!((start, end, text), (18, 18, "W".to_string()));
    }

    #[test]
    fn join_type_rejects_unknown_keyword() {
        let mut parser = QueryParser::new("OUTER JOIN tableA ON tableA.id = tableB.id");
        parser.phase = crate::parser::Phase::Joins;
        let result = JoinType::parse(&mut parser);
        match result {
            Ok(join_type) => panic!("expected invalid join type, got {join_type:?}"),
            Err(err) => assert_eq!((err.start, err.end, err.text), (0, 0, "O".to_string())),
        }
    }
}