Skip to main content

activecube_rs/compiler/
filter.rs

1use async_graphql::dynamic::{ObjectAccessor, ValueAccessor};
2
3use crate::compiler::ir::{CompareOp, FilterNode, SqlValue};
4use crate::cube::definition::{DimType, DimensionNode};
5
6pub fn parse_where(
7    accessor: &ObjectAccessor,
8    dimensions: &[DimensionNode],
9) -> Result<FilterNode, async_graphql::Error> {
10    let mut conditions = Vec::new();
11
12    if let Ok(any_val) = accessor.try_get("any") {
13        if let Ok(list) = any_val.list() {
14            let mut or_children = Vec::new();
15            for item in list.iter() {
16                if let Ok(obj) = item.object() {
17                    let child = parse_where(&obj, dimensions)?;
18                    if !child.is_empty() {
19                        or_children.push(child);
20                    }
21                }
22            }
23            if !or_children.is_empty() {
24                conditions.push(FilterNode::Or(or_children));
25            }
26        }
27    }
28
29    for node in dimensions {
30        match node {
31            DimensionNode::Leaf(dim) => {
32                if let Ok(filter_val) = accessor.try_get(&dim.graphql_name) {
33                    if let Ok(filter_obj) = filter_val.object() {
34                        let leaf_conditions =
35                            parse_leaf_filter(&filter_obj, &dim.column, &dim.dim_type)?;
36                        conditions.extend(leaf_conditions);
37                    }
38                }
39            }
40            DimensionNode::Group { graphql_name, children } => {
41                if let Ok(group_val) = accessor.try_get(graphql_name) {
42                    if let Ok(group_obj) = group_val.object() {
43                        let child_filter = parse_where(&group_obj, children)?;
44                        if !child_filter.is_empty() {
45                            conditions.push(child_filter);
46                        }
47                    }
48                }
49            }
50        }
51    }
52
53    Ok(match conditions.len() {
54        0 => FilterNode::Empty,
55        1 => conditions.into_iter().next().unwrap(),
56        _ => FilterNode::And(conditions),
57    })
58}
59
60/// Public entry point for selector-based filters. Same logic as dimension leaf filters
61/// but callable from the parser for cube-level selector arguments.
62pub fn parse_leaf_filter_for_selector(
63    obj: &ObjectAccessor,
64    column: &str,
65    dim_type: &DimType,
66) -> Result<Vec<FilterNode>, async_graphql::Error> {
67    parse_leaf_filter(obj, column, dim_type)
68}
69
70fn parse_leaf_filter(
71    obj: &ObjectAccessor,
72    column: &str,
73    dim_type: &DimType,
74) -> Result<Vec<FilterNode>, async_graphql::Error> {
75    let mut conditions = Vec::new();
76
77    let ops: &[(&str, CompareOp)] = match dim_type {
78        DimType::Int | DimType::Float => &[
79            ("eq", CompareOp::Eq), ("ne", CompareOp::Ne),
80            ("gt", CompareOp::Gt), ("ge", CompareOp::Ge),
81            ("lt", CompareOp::Lt), ("le", CompareOp::Le),
82        ],
83        DimType::String => &[
84            ("is", CompareOp::Eq), ("not", CompareOp::Ne),
85            ("like", CompareOp::Like), ("includes", CompareOp::Includes),
86        ],
87        DimType::DateTime => &[
88            ("is", CompareOp::Eq), ("not", CompareOp::Ne),
89            ("after", CompareOp::Gt), ("since", CompareOp::Ge),
90            ("before", CompareOp::Lt), ("till", CompareOp::Le),
91        ],
92        DimType::Bool => &[("eq", CompareOp::Eq)],
93    };
94
95    for (key, op) in ops {
96        if let Ok(val) = obj.try_get(key) {
97            let sql_val = accessor_to_sql(&val, dim_type)?;
98            conditions.push(FilterNode::Condition {
99                column: column.to_string(),
100                op: op.clone(),
101                value: sql_val,
102            });
103        }
104    }
105
106    // isNull: true → IS NULL, isNull: false → IS NOT NULL
107    if let Ok(val) = obj.try_get("isNull") {
108        if let Ok(b) = val.boolean() {
109            conditions.push(FilterNode::Condition {
110                column: column.to_string(),
111                op: if b { CompareOp::IsNull } else { CompareOp::IsNotNull },
112                value: SqlValue::Bool(b),
113            });
114        }
115    }
116
117    if matches!(dim_type, DimType::String) {
118        for (key, op) in &[("in", CompareOp::In), ("notIn", CompareOp::NotIn)] {
119            if let Ok(val) = obj.try_get(key) {
120                if let Ok(list) = val.list() {
121                    let mut values = Vec::new();
122                    for item in list.iter() {
123                        if let Ok(s) = item.string() {
124                            values.push(s.to_string());
125                        }
126                    }
127                    if !values.is_empty() {
128                        conditions.push(FilterNode::Condition {
129                            column: column.to_string(),
130                            op: op.clone(),
131                            value: SqlValue::String(values.join(",")),
132                        });
133                    }
134                }
135            }
136        }
137    }
138
139    Ok(conditions)
140}
141
142fn accessor_to_sql(
143    val: &ValueAccessor,
144    dim_type: &DimType,
145) -> Result<SqlValue, async_graphql::Error> {
146    match dim_type {
147        DimType::Int => Ok(SqlValue::Int(val.i64()?)),
148        DimType::Float => Ok(SqlValue::Float(val.f64()?)),
149        DimType::Bool => Ok(SqlValue::Bool(val.boolean()?)),
150        DimType::String | DimType::DateTime => Ok(SqlValue::String(val.string()?.to_string())),
151    }
152}
153