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    for (key, op) in &[("in", CompareOp::In), ("notIn", CompareOp::NotIn)] {
118        if let Ok(val) = obj.try_get(key) {
119            if let Ok(list) = val.list() {
120                let mut values = Vec::new();
121                for item in list.iter() {
122                    match dim_type {
123                        DimType::String | DimType::DateTime => {
124                            if let Ok(s) = item.string() {
125                                values.push(s.to_string());
126                            }
127                        }
128                        DimType::Int => {
129                            if let Ok(n) = item.i64() {
130                                values.push(n.to_string());
131                            }
132                        }
133                        DimType::Float => {
134                            if let Ok(f) = item.f64() {
135                                values.push(f.to_string());
136                            }
137                        }
138                        _ => {}
139                    }
140                }
141                if !values.is_empty() {
142                    conditions.push(FilterNode::Condition {
143                        column: column.to_string(),
144                        op: op.clone(),
145                        value: SqlValue::String(values.join(",")),
146                    });
147                }
148            }
149        }
150    }
151
152    Ok(conditions)
153}
154
155/// Parse a filter from a raw `async_graphql::Value` (used when ObjectAccessor is unavailable,
156/// e.g. from selection-set arguments on metric fields).
157pub fn parse_filter_from_value(
158    val: &async_graphql::Value,
159    dimensions: &[DimensionNode],
160) -> Result<FilterNode, async_graphql::Error> {
161    let obj = match val {
162        async_graphql::Value::Object(map) => map,
163        _ => return Ok(FilterNode::Empty),
164    };
165
166    let mut conditions = Vec::new();
167
168    if let Some(async_graphql::Value::List(items)) = obj.get("any") {
169        let mut or_children = Vec::new();
170        for item in items {
171            let child = parse_filter_from_value(item, dimensions)?;
172            if !child.is_empty() {
173                or_children.push(child);
174            }
175        }
176        if !or_children.is_empty() {
177            conditions.push(FilterNode::Or(or_children));
178        }
179    }
180
181    for node in dimensions {
182        match node {
183            DimensionNode::Leaf(dim) => {
184                if let Some(async_graphql::Value::Object(filter_map)) = obj.get(dim.graphql_name.as_str()) {
185                    let leaf = parse_leaf_filter_from_value(filter_map, &dim.column, &dim.dim_type)?;
186                    conditions.extend(leaf);
187                }
188            }
189            DimensionNode::Group { graphql_name, children, .. } => {
190                if let Some(group_val) = obj.get(graphql_name.as_str()) {
191                    let child_filter = parse_filter_from_value(group_val, children)?;
192                    if !child_filter.is_empty() {
193                        conditions.push(child_filter);
194                    }
195                }
196            }
197        }
198    }
199
200    Ok(match conditions.len() {
201        0 => FilterNode::Empty,
202        1 => conditions.into_iter().next().unwrap(),
203        _ => FilterNode::And(conditions),
204    })
205}
206
207fn parse_leaf_filter_from_value(
208    obj: &indexmap::IndexMap<async_graphql::Name, async_graphql::Value>,
209    column: &str,
210    dim_type: &DimType,
211) -> Result<Vec<FilterNode>, async_graphql::Error> {
212    let mut conditions = Vec::new();
213
214    let ops: &[(&str, CompareOp)] = match dim_type {
215        DimType::Int | DimType::Float => &[
216            ("eq", CompareOp::Eq), ("ne", CompareOp::Ne),
217            ("gt", CompareOp::Gt), ("ge", CompareOp::Ge),
218            ("lt", CompareOp::Lt), ("le", CompareOp::Le),
219        ],
220        DimType::String => &[
221            ("is", CompareOp::Eq), ("not", CompareOp::Ne),
222            ("like", CompareOp::Like), ("includes", CompareOp::Includes),
223        ],
224        DimType::DateTime => &[
225            ("is", CompareOp::Eq), ("not", CompareOp::Ne),
226            ("after", CompareOp::Gt), ("since", CompareOp::Ge),
227            ("before", CompareOp::Lt), ("till", CompareOp::Le),
228        ],
229        DimType::Bool => &[("eq", CompareOp::Eq)],
230    };
231
232    for (key, op) in ops {
233        if let Some(val) = obj.get(*key) {
234            if let Some(sql_val) = value_to_sql(val, dim_type) {
235                conditions.push(FilterNode::Condition {
236                    column: column.to_string(),
237                    op: op.clone(),
238                    value: sql_val,
239                });
240            }
241        }
242    }
243
244    if let Some(async_graphql::Value::Boolean(b)) = obj.get("isNull") {
245        conditions.push(FilterNode::Condition {
246            column: column.to_string(),
247            op: if *b { CompareOp::IsNull } else { CompareOp::IsNotNull },
248            value: SqlValue::Bool(*b),
249        });
250    }
251
252    for (key, op) in &[("in", CompareOp::In), ("notIn", CompareOp::NotIn)] {
253        if let Some(async_graphql::Value::List(list)) = obj.get(*key) {
254            let values: Vec<String> = list.iter().filter_map(|item| match (dim_type, item) {
255                (DimType::String | DimType::DateTime, async_graphql::Value::String(s)) => Some(s.clone()),
256                (DimType::Int, async_graphql::Value::Number(n)) => n.as_i64().map(|i| i.to_string()),
257                (DimType::Float, async_graphql::Value::Number(n)) => n.as_f64().map(|f| f.to_string()),
258                _ => None,
259            }).collect();
260            if !values.is_empty() {
261                conditions.push(FilterNode::Condition {
262                    column: column.to_string(),
263                    op: op.clone(),
264                    value: SqlValue::String(values.join(",")),
265                });
266            }
267        }
268    }
269
270    Ok(conditions)
271}
272
273fn value_to_sql(val: &async_graphql::Value, dim_type: &DimType) -> Option<SqlValue> {
274    match (dim_type, val) {
275        (DimType::Int, async_graphql::Value::Number(n)) => n.as_i64().map(SqlValue::Int),
276        (DimType::Float, async_graphql::Value::Number(n)) => n.as_f64().map(SqlValue::Float),
277        (DimType::Bool, async_graphql::Value::Boolean(b)) => Some(SqlValue::Bool(*b)),
278        (DimType::String | DimType::DateTime, async_graphql::Value::String(s)) => {
279            Some(SqlValue::String(s.clone()))
280        }
281        (DimType::Int, async_graphql::Value::String(s)) => s.parse::<i64>().ok().map(SqlValue::Int),
282        (DimType::Float, async_graphql::Value::String(s)) => s.parse::<f64>().ok().map(SqlValue::Float),
283        _ => None,
284    }
285}
286
287fn accessor_to_sql(
288    val: &ValueAccessor,
289    dim_type: &DimType,
290) -> Result<SqlValue, async_graphql::Error> {
291    match dim_type {
292        DimType::Int => Ok(SqlValue::Int(val.i64()?)),
293        DimType::Float => Ok(SqlValue::Float(val.f64()?)),
294        DimType::Bool => Ok(SqlValue::Bool(val.boolean()?)),
295        DimType::String | DimType::DateTime => Ok(SqlValue::String(val.string()?.to_string())),
296    }
297}
298