Skip to main content

activecube_rs/compiler/
validator.rs

1use crate::compiler::ir::{FilterNode, QueryIR};
2
3const MAX_QUERY_DEPTH: usize = 6;
4const ABSOLUTE_MAX_LIMIT: u32 = 10000;
5
6pub fn validate(ir: QueryIR) -> Result<QueryIR, async_graphql::Error> {
7    if ir.limit > ABSOLUTE_MAX_LIMIT {
8        return Err(async_graphql::Error::new(format!(
9            "Limit {} exceeds maximum allowed ({})", ir.limit, ABSOLUTE_MAX_LIMIT
10        )));
11    }
12
13    let depth = filter_depth(&ir.filters);
14    if depth > MAX_QUERY_DEPTH {
15        return Err(async_graphql::Error::new(format!(
16            "Filter nesting depth {} exceeds maximum ({})", depth, MAX_QUERY_DEPTH
17        )));
18    }
19
20    let having_depth = filter_depth(&ir.having);
21    if having_depth > MAX_QUERY_DEPTH {
22        return Err(async_graphql::Error::new(format!(
23            "HAVING filter depth {} exceeds maximum ({})", having_depth, MAX_QUERY_DEPTH
24        )));
25    }
26
27    if ir.selects.is_empty() {
28        return Err(async_graphql::Error::new("Query must select at least one field"));
29    }
30
31    Ok(ir)
32}
33
34fn filter_depth(node: &FilterNode) -> usize {
35    match node {
36        FilterNode::Empty | FilterNode::Condition { .. } => 1,
37        FilterNode::And(children) | FilterNode::Or(children) => {
38            1 + children.iter().map(filter_depth).max().unwrap_or(0)
39        }
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use crate::compiler::ir::*;
47
48    fn minimal_ir() -> QueryIR {
49        QueryIR {
50            cube: "Test".into(), schema: "test".into(), table: "test_table".into(),
51            selects: vec![SelectExpr::Column { column: "id".into(), alias: None }],
52            filters: FilterNode::Empty, having: FilterNode::Empty,
53            group_by: vec![], order_by: vec![], limit: 25, offset: 0,
54            limit_by: None,
55            use_final: false,
56            joins: vec![],
57            custom_query_builder: None,
58            from_subquery: None,
59        }
60    }
61
62    #[test]
63    fn test_valid_ir_passes() { assert!(validate(minimal_ir()).is_ok()); }
64
65    #[test]
66    fn test_excessive_limit_rejected() {
67        let mut ir = minimal_ir(); ir.limit = 99999;
68        assert!(validate(ir).is_err());
69    }
70
71    #[test]
72    fn test_empty_selects_rejected() {
73        let mut ir = minimal_ir(); ir.selects.clear();
74        assert!(validate(ir).is_err());
75    }
76
77    #[test]
78    fn test_deep_filter_rejected() {
79        fn deep_filter(depth: usize) -> FilterNode {
80            if depth == 0 {
81                FilterNode::Condition { column: "x".into(), op: CompareOp::Eq, value: SqlValue::Int(1) }
82            } else {
83                FilterNode::And(vec![deep_filter(depth - 1)])
84            }
85        }
86        let mut ir = minimal_ir(); ir.filters = deep_filter(10);
87        assert!(validate(ir).is_err());
88    }
89}