csvsql 0.1.4

SQL like engine that works on CSV file.
Documentation
use std::cmp::Ordering;
use std::ops::Deref;

use crate::error::CvsSqlError;
use crate::group_by::{GroupRow, GroupedResultSet};
use crate::projections::Projection;
use crate::{engine::Engine, projections::SingleConvert};
use sqlparser::ast::{OrderBy, OrderByExpr, OrderByKind};

struct OrderByItem {
    by: Box<dyn Projection>,
    asc: bool,
    empty_first: bool,
}
impl OrderByItem {
    fn new(
        parent: &GroupedResultSet,
        engine: &Engine,
        expr: &OrderByExpr,
    ) -> Result<Self, CvsSqlError> {
        if expr.with_fill.is_some() {
            return Err(CvsSqlError::Unsupported("ORDER BY with fill".into()));
        }
        let by = expr.expr.convert_single(&parent.metadata, engine)?;
        let asc = expr.options.asc.unwrap_or(true);
        let empty_first = expr.options.nulls_first.unwrap_or(false);

        Ok(OrderByItem {
            by,
            asc,
            empty_first,
        })
    }

    fn compare(&self, left: &GroupRow, right: &GroupRow) -> Ordering {
        let ret = self.compare_as_is(left, right);
        if self.asc { ret } else { ret.reverse() }
    }
    fn compare_as_is(&self, left: &GroupRow, right: &GroupRow) -> Ordering {
        let left = self.by.get(left);
        let right = self.by.get(right);
        if left.is_empty() {
            if right.is_empty() {
                Ordering::Equal
            } else if self.empty_first {
                Ordering::Less
            } else {
                Ordering::Greater
            }
        } else if right.is_empty() {
            if self.empty_first {
                Ordering::Greater
            } else {
                Ordering::Less
            }
        } else {
            left.cmp(right.deref())
        }
    }
}
pub fn order_by(
    engine: &Engine,
    order_by: &Option<OrderBy>,
    results: &mut GroupedResultSet,
) -> Result<(), CvsSqlError> {
    let Some(order_by) = order_by else {
        return Ok(());
    };
    if order_by.interpolate.is_some() {
        return Err(CvsSqlError::Unsupported("interpolate ORDER BY".into()));
    }
    let OrderByKind::Expressions(ref exprs) = order_by.kind else {
        return Err(CvsSqlError::Unsupported("ORDER BY all".into()));
    };
    let items = exprs
        .iter()
        .map(|expr| OrderByItem::new(results, engine, expr))
        .collect::<Result<Vec<_>, _>>()?;
    if items.is_empty() {
        return Ok(());
    }

    results.rows.sort_by(|left, right| {
        for item in &items {
            let order = item.compare(left, right);
            if order != Ordering::Equal {
                return order;
            }
        }
        Ordering::Equal
    });
    Ok(())
}

#[cfg(test)]
mod tests {
    use sqlparser::{
        ast::{Interpolate, OrderByOptions, Statement, WithFill},
        parser::Parser,
    };

    use crate::{args::Args, dialect::FilesDialect, extractor::Extractor};

    use super::*;

    fn test_unsupported_order_by(change: fn(&mut OrderBy)) -> Result<(), CvsSqlError> {
        let args = Args {
            write_mode: true,
            ..Args::default()
        };

        let engine = Engine::try_from(&args)?;

        let sql = "SELECT * FROM tests.data.dates ORDER BY amount";
        let dialect = FilesDialect {};
        let statement = Parser::parse_sql(&dialect, sql)?;
        let Some(Statement::Query(mut query)) = statement.into_iter().next() else {
            panic!("Not a select statement");
        };
        let Some(order_by) = &query.order_by else {
            panic!("No order by?");
        };
        let mut order_by = order_by.clone();
        change(&mut order_by);

        query.order_by = Some(order_by);
        let Err(err) = query.extract(&engine) else {
            panic!("No error");
        };

        assert!(matches!(err, CvsSqlError::Unsupported(_)));

        Ok(())
    }

    #[test]
    fn interpolate_unsupported() -> Result<(), CvsSqlError> {
        test_unsupported_order_by(|order_by| {
            order_by.interpolate = Some(Interpolate { exprs: None });
        })
    }

    #[test]
    fn all_unsupported() -> Result<(), CvsSqlError> {
        test_unsupported_order_by(|order_by| {
            order_by.kind = OrderByKind::All(OrderByOptions {
                asc: None,
                nulls_first: None,
            })
        })
    }

    #[test]
    fn with_fill_unsupported() -> Result<(), CvsSqlError> {
        test_unsupported_order_by(|order_by| {
            let OrderByKind::Expressions(exprs) = &order_by.kind else {
                panic!("Need expressions");
            };
            let mut expr = exprs.first().unwrap().clone();
            expr.with_fill = Some(WithFill {
                from: None,
                to: None,
                step: None,
            });
            order_by.kind = OrderByKind::Expressions(vec![expr]);
        })
    }
}