fosk 0.2.0

In-memory SQL-like query engine and lightweight data store for testing and prototyping.
Documentation
// SELECT b.*, a.full_name as name, COUNT(*) as TotBy, * V
// FROM TableA A V
// INNER JOIN TableB B ON A.id = B.id V
// INNER JOIN (query...) Q ON Q.id = B.q_id V
// WHERE A.Age > 16 AND (B.city = 'Porto' OR B.city like "Matosinhos") V
// GROUP BY a.full_name V
// HAVING COUNT(*) > 3
// ORDER BY b.description DESC V

use crate::parser::{
    ParseError, Phase, QueryParser,
    ast::{
        Collection, CollectionsParser, Column, GroupBy, HavingParser, Identifier, Join,
        LimitAndOffsetParser, OrderBy, Predicate, ProjectionParser, WhereParser,
    },
};

#[derive(Default, Clone, PartialEq)]
pub struct Query {
    pub projection: Vec<Identifier>,
    pub collections: Vec<Collection>,
    pub joins: Vec<Join>,
    pub criteria: Option<Predicate>,
    pub group_by: Vec<Column>,
    pub having: Option<Predicate>,
    pub order_by: Vec<OrderBy>,
    pub limit: Option<i64>,
    pub offset: Option<i64>,
}

impl Query {
    pub fn parse(parser: &mut QueryParser) -> Result<Self, ParseError> {
        parser.next_non_whitespace();

        let mut query = Query::default();

        while parser.phase != Phase::EOF {
            match parser.phase {
                Phase::Projection => query.projection = ProjectionParser::parse(parser)?,
                Phase::Collections => query.collections = CollectionsParser::parse(parser)?,
                Phase::Joins => query.joins = Join::parse(parser)?,
                Phase::Criteria => query.criteria = Some(WhereParser::parse(parser)?),
                Phase::Aggregates => query.group_by = GroupBy::parse(parser)?,
                Phase::Having => query.having = Some(HavingParser::parse(parser)?),
                Phase::OrderBy => query.order_by = OrderBy::parse(parser)?,
                Phase::LimitAndOffset => {
                    let (limit, offset) = LimitAndOffsetParser::parse(parser)?;
                    query.limit = limit;
                    query.offset = offset;
                }
                Phase::EOF => todo!(),
            }
        }

        Ok(query)
    }
}

impl TryFrom<&str> for Query {
    type Error = ParseError;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        let mut parser = QueryParser::new(value);
        Query::parse(&mut parser)
    }
}

use std::fmt;

impl fmt::Display for Query {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let proj = self
            .projection
            .iter()
            .map(|p| format!("{:?}", p))
            .collect::<Vec<_>>()
            .join(", ");
        let cols = self
            .collections
            .iter()
            .map(|c| format!("{:?}", c))
            .collect::<Vec<_>>()
            .join(", ");
        let joins = self
            .joins
            .iter()
            .map(|j| format!("{:?}", j))
            .collect::<Vec<_>>()
            .join(", ");
        let crit = match &self.criteria {
            Some(c) => format!("{:?}", c),
            None => "None".to_string(),
        };
        let group = self
            .group_by
            .iter()
            .map(|g| format!("{:?}", g))
            .collect::<Vec<_>>()
            .join(", ");
        let having = match &self.having {
            Some(h) => format!("{:?}", h),
            None => "None".to_string(),
        };
        let order = self
            .order_by
            .iter()
            .map(|o| format!("{:?}", o))
            .collect::<Vec<_>>()
            .join(", ");

        write!(
            f,
            "Query(projection=[{}], collections=[{}], joins=[{}], criteria={}, group_by=[{}], having={}, order_by=[{}], limit={:?}, offset={:?})",
            proj, cols, joins, crit, group, having, order, self.limit, self.offset
        )
    }
}

impl fmt::Debug for Query {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Query({})", self)
    }
}

#[cfg(test)]
mod tests {
    use crate::parser::ast::Query;

    #[test]
    pub fn test_query() {
        let text = r#"
SELECT b.*, a.full_name as name, COUNT(*) as TotBy, *
FROM TableA A, OtherTable
INNER JOIN TableB B ON A.id = B.id
LEFT JOIN TableC C ON C.id = B.q_id
WHERE A.Age > 16 AND (B.city = 'Porto' OR B.city like "Matosinhos")
GROUP BY a.full_name
HAVING COUNT(*) > 3
ORDER BY b.description DESC
OFFSET 60
LIMIT 20
        "#;

        let query = Query::try_from(text).expect("Failed to parse predicate");

        assert_eq!(query.projection.len(), 4);
        assert_eq!(query.collections.len(), 2);
        assert_eq!(query.joins.len(), 2);
        assert!(query.criteria.is_some());
        assert_eq!(query.group_by.len(), 1);
        assert!(query.having.is_some());
        assert_eq!(query.order_by.len(), 1);
        assert_eq!(query.offset.unwrap(), 60);
        assert_eq!(query.limit.unwrap(), 20);
    }

    #[test]
    fn try_from_parses_minimal_query() {
        let query = Query::try_from("SELECT * FROM people").expect("query should parse");

        assert_eq!(query.projection.len(), 1);
        assert_eq!(query.collections.len(), 1);
        assert!(query.joins.is_empty());
        assert!(query.criteria.is_none());
        assert!(query.group_by.is_empty());
        assert!(query.having.is_none());
        assert!(query.order_by.is_empty());
        assert_eq!(query.limit, None);
        assert_eq!(query.offset, None);
    }

    #[test]
    fn try_from_parses_from_subquery_with_alias() {
        let query = Query::try_from("SELECT p.name FROM (SELECT name FROM people) p")
            .expect("subquery should parse");

        assert_eq!(query.projection.len(), 1);
        assert_eq!(query.collections.len(), 1);
        match &query.collections[0] {
            crate::parser::ast::Collection::Query { query, alias } => {
                assert_eq!(alias.as_deref(), Some("p"));
                assert_eq!(query.projection.len(), 1);
                assert_eq!(query.collections.len(), 1);
            }
            other => panic!("expected query collection, got {other:?}"),
        }
    }

    #[test]
    fn try_from_rejects_from_subquery_without_alias() {
        let err = Query::try_from("SELECT name FROM (SELECT name FROM people)");

        assert!(err.is_err(), "FROM subqueries require aliases");
    }

    #[test]
    fn display_and_debug_include_all_query_sections() {
        let query = Query::try_from("SELECT * FROM people LIMIT 5").expect("query should parse");
        let display = query.to_string();

        assert!(display.starts_with("Query(projection=["));
        assert!(display.contains("collections=["));
        assert!(display.contains("joins=[]"));
        assert!(display.contains("criteria=None"));
        assert!(display.contains("group_by=[]"));
        assert!(display.contains("having=None"));
        assert!(display.contains("order_by=[]"));
        assert!(display.contains("limit=Some(5)"));
        assert!(display.contains("offset=None"));
        assert_eq!(format!("{:?}", query), format!("Query({display})"));
    }

    #[test]
    fn display_includes_present_optional_sections() {
        let query = Query::try_from(
            r#"
            SELECT people.city, COUNT(*) AS total
            FROM people
            WHERE people.age > 20
            GROUP BY people.city
            HAVING COUNT(*) > 1
            ORDER BY people.city DESC
            OFFSET 2
            LIMIT 3
            "#,
        )
        .expect("query should parse");
        let display = query.to_string();

        assert!(display.contains("criteria=Predicate"));
        assert!(display.contains("having=Predicate"));
        assert!(display.contains("order_by=["));
        assert!(display.contains("limit=Some(3)"));
        assert!(display.contains("offset=Some(2)"));
    }
}