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::{ArrayFieldDef, ArrayFieldType, 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 matches!(dim.dim_type, DimType::Bool) {
34                        if let Ok(b) = filter_val.boolean() {
35                            conditions.push(FilterNode::Condition {
36                                column: dim.column.clone(),
37                                op: CompareOp::Eq,
38                                value: SqlValue::Bool(b),
39                            });
40                        } else if let Ok(filter_obj) = filter_val.object() {
41                            let leaf_conditions =
42                                parse_leaf_filter(&filter_obj, &dim.column, &dim.dim_type)?;
43                            conditions.extend(leaf_conditions);
44                        }
45                    } else if let Ok(filter_obj) = filter_val.object() {
46                        let leaf_conditions =
47                            parse_leaf_filter(&filter_obj, &dim.column, &dim.dim_type)?;
48                        conditions.extend(leaf_conditions);
49                    }
50                }
51            }
52            DimensionNode::Group { graphql_name, children, .. } => {
53                if let Ok(group_val) = accessor.try_get(graphql_name) {
54                    if let Ok(group_obj) = group_val.object() {
55                        let child_filter = parse_where(&group_obj, children)?;
56                        if !child_filter.is_empty() {
57                            conditions.push(child_filter);
58                        }
59                    }
60                }
61            }
62            DimensionNode::Array { graphql_name, children, .. } => {
63                if let Ok(arr_val) = accessor.try_get(graphql_name) {
64                    if let Ok(arr_obj) = arr_val.object() {
65                        let arr_filter = parse_array_includes_filter(&arr_obj, children)?;
66                        if !arr_filter.is_empty() {
67                            conditions.push(arr_filter);
68                        }
69                    }
70                }
71            }
72        }
73    }
74
75    Ok(match conditions.len() {
76        0 => FilterNode::Empty,
77        1 => conditions.into_iter().next().unwrap(),
78        _ => FilterNode::And(conditions),
79    })
80}
81
82/// Public entry point for selector-based filters. Same logic as dimension leaf filters
83/// but callable from the parser for cube-level selector arguments.
84pub fn parse_leaf_filter_for_selector(
85    obj: &ObjectAccessor,
86    column: &str,
87    dim_type: &DimType,
88) -> Result<Vec<FilterNode>, async_graphql::Error> {
89    parse_leaf_filter(obj, column, dim_type)
90}
91
92fn parse_leaf_filter(
93    obj: &ObjectAccessor,
94    column: &str,
95    dim_type: &DimType,
96) -> Result<Vec<FilterNode>, async_graphql::Error> {
97    let mut conditions = Vec::new();
98
99    let ops: &[(&str, CompareOp)] = match dim_type {
100        DimType::Int | DimType::Float => &[
101            ("eq", CompareOp::Eq), ("ne", CompareOp::Ne),
102            ("gt", CompareOp::Gt), ("ge", CompareOp::Ge),
103            ("lt", CompareOp::Lt), ("le", CompareOp::Le),
104        ],
105        DimType::Decimal => &[
106            ("eq", CompareOp::Eq), ("ne", CompareOp::Ne),
107            ("gt", CompareOp::Gt), ("ge", CompareOp::Ge),
108            ("lt", CompareOp::Lt), ("le", CompareOp::Le),
109        ],
110        DimType::String => &[
111            ("is", CompareOp::Eq), ("not", CompareOp::Ne),
112            ("like", CompareOp::Like), ("includes", CompareOp::Includes),
113        ],
114        DimType::DateTime => &[
115            ("is", CompareOp::Eq), ("not", CompareOp::Ne),
116            ("after", CompareOp::Gt), ("since", CompareOp::Ge),
117            ("before", CompareOp::Lt), ("till", CompareOp::Le),
118        ],
119        DimType::Bool => &[("eq", CompareOp::Eq)],
120    };
121
122    for (key, op) in ops {
123        if let Ok(val) = obj.try_get(key) {
124            let sql_val = accessor_to_sql(&val, dim_type)?;
125            conditions.push(FilterNode::Condition {
126                column: column.to_string(),
127                op: op.clone(),
128                value: sql_val,
129            });
130        }
131    }
132
133    for (key, op) in &[
134        ("since_relative", CompareOp::Ge),
135        ("after_relative", CompareOp::Gt),
136        ("till_relative", CompareOp::Le),
137    ] {
138        if let Ok(val) = obj.try_get(key) {
139            if let Ok(rel_obj) = val.object() {
140                if let Some(expr) = parse_relative_time_accessor(&rel_obj) {
141                    conditions.push(FilterNode::Condition {
142                        column: column.to_string(),
143                        op: op.clone(),
144                        value: SqlValue::Expression(expr),
145                    });
146                }
147            }
148        }
149    }
150
151    if let Ok(val) = obj.try_get("isNull") {
152        if let Ok(b) = val.boolean() {
153            conditions.push(FilterNode::Condition {
154                column: column.to_string(),
155                op: if b { CompareOp::IsNull } else { CompareOp::IsNotNull },
156                value: SqlValue::Bool(b),
157            });
158        }
159    }
160
161    for (key, op) in &[("in", CompareOp::In), ("notIn", CompareOp::NotIn)] {
162        if let Ok(val) = obj.try_get(key) {
163            if let Ok(list) = val.list() {
164                let mut values = Vec::new();
165                for item in list.iter() {
166                    match dim_type {
167                        DimType::String | DimType::DateTime | DimType::Decimal => {
168                            if let Ok(s) = item.string() {
169                                values.push(s.to_string());
170                            }
171                        }
172                        DimType::Int => {
173                            if let Ok(s) = item.string() {
174                                if let Ok(i) = s.parse::<i64>() {
175                                    values.push(i.to_string());
176                                }
177                            }
178                        }
179                        DimType::Float => {
180                            if let Ok(s) = item.string() {
181                                if let Ok(f) = s.parse::<f64>() {
182                                    values.push(f.to_string());
183                                }
184                            }
185                        }
186                        _ => {}
187                    }
188                }
189                if !values.is_empty() {
190                    conditions.push(FilterNode::Condition {
191                        column: column.to_string(),
192                        op: op.clone(),
193                        value: SqlValue::String(values.join(",")),
194                    });
195                }
196            }
197        }
198    }
199
200    Ok(conditions)
201}
202
203/// Parse a filter from a raw `async_graphql::Value` (used when ObjectAccessor is unavailable,
204/// e.g. from selection-set arguments on metric fields).
205pub fn parse_filter_from_value(
206    val: &async_graphql::Value,
207    dimensions: &[DimensionNode],
208) -> Result<FilterNode, async_graphql::Error> {
209    let obj = match val {
210        async_graphql::Value::Object(map) => map,
211        _ => return Ok(FilterNode::Empty),
212    };
213
214    let mut conditions = Vec::new();
215
216    if let Some(async_graphql::Value::List(items)) = obj.get("any") {
217        let mut or_children = Vec::new();
218        for item in items {
219            let child = parse_filter_from_value(item, dimensions)?;
220            if !child.is_empty() {
221                or_children.push(child);
222            }
223        }
224        if !or_children.is_empty() {
225            conditions.push(FilterNode::Or(or_children));
226        }
227    }
228
229    for node in dimensions {
230        match node {
231            DimensionNode::Leaf(dim) => {
232                if let Some(val) = obj.get(dim.graphql_name.as_str()) {
233                    if matches!(dim.dim_type, DimType::Bool) {
234                        if let async_graphql::Value::Boolean(b) = val {
235                            conditions.push(FilterNode::Condition {
236                                column: dim.column.clone(),
237                                op: CompareOp::Eq,
238                                value: SqlValue::Bool(*b),
239                            });
240                        } else if let async_graphql::Value::Object(filter_map) = val {
241                            let leaf = parse_leaf_filter_from_value(filter_map, &dim.column, &dim.dim_type)?;
242                            conditions.extend(leaf);
243                        }
244                    } else if let async_graphql::Value::Object(filter_map) = val {
245                        let leaf = parse_leaf_filter_from_value(filter_map, &dim.column, &dim.dim_type)?;
246                        conditions.extend(leaf);
247                    }
248                }
249            }
250            DimensionNode::Group { graphql_name, children, .. } => {
251                if let Some(group_val) = obj.get(graphql_name.as_str()) {
252                    let child_filter = parse_filter_from_value(group_val, children)?;
253                    if !child_filter.is_empty() {
254                        conditions.push(child_filter);
255                    }
256                }
257            }
258            DimensionNode::Array { graphql_name, children, .. } => {
259                if let Some(async_graphql::Value::Object(arr_map)) = obj.get(graphql_name.as_str()) {
260                    let arr_filter = parse_array_includes_from_value(arr_map, children)?;
261                    if !arr_filter.is_empty() {
262                        conditions.push(arr_filter);
263                    }
264                }
265            }
266        }
267    }
268
269    Ok(match conditions.len() {
270        0 => FilterNode::Empty,
271        1 => conditions.into_iter().next().unwrap(),
272        _ => FilterNode::And(conditions),
273    })
274}
275
276fn parse_leaf_filter_from_value(
277    obj: &indexmap::IndexMap<async_graphql::Name, async_graphql::Value>,
278    column: &str,
279    dim_type: &DimType,
280) -> Result<Vec<FilterNode>, async_graphql::Error> {
281    let mut conditions = Vec::new();
282
283    let ops: &[(&str, CompareOp)] = match dim_type {
284        DimType::Int | DimType::Float => &[
285            ("eq", CompareOp::Eq), ("ne", CompareOp::Ne),
286            ("gt", CompareOp::Gt), ("ge", CompareOp::Ge),
287            ("lt", CompareOp::Lt), ("le", CompareOp::Le),
288        ],
289        DimType::Decimal => &[
290            ("eq", CompareOp::Eq), ("ne", CompareOp::Ne),
291            ("gt", CompareOp::Gt), ("ge", CompareOp::Ge),
292            ("lt", CompareOp::Lt), ("le", CompareOp::Le),
293        ],
294        DimType::String => &[
295            ("is", CompareOp::Eq), ("not", CompareOp::Ne),
296            ("like", CompareOp::Like), ("includes", CompareOp::Includes),
297        ],
298        DimType::DateTime => &[
299            ("is", CompareOp::Eq), ("not", CompareOp::Ne),
300            ("after", CompareOp::Gt), ("since", CompareOp::Ge),
301            ("before", CompareOp::Lt), ("till", CompareOp::Le),
302        ],
303        DimType::Bool => &[("eq", CompareOp::Eq)],
304    };
305
306    for (key, op) in ops {
307        if let Some(val) = obj.get(*key) {
308            if let Some(sql_val) = value_to_sql(val, dim_type) {
309                conditions.push(FilterNode::Condition {
310                    column: column.to_string(),
311                    op: op.clone(),
312                    value: sql_val,
313                });
314            }
315        }
316    }
317
318    for (key, op) in &[
319        ("since_relative", CompareOp::Ge),
320        ("after_relative", CompareOp::Gt),
321        ("till_relative", CompareOp::Le),
322    ] {
323        if let Some(async_graphql::Value::Object(rel_map)) = obj.get(*key) {
324            if let Some(expr) = parse_relative_time_value(rel_map) {
325                conditions.push(FilterNode::Condition {
326                    column: column.to_string(),
327                    op: op.clone(),
328                    value: SqlValue::Expression(expr),
329                });
330            }
331        }
332    }
333
334    if let Some(async_graphql::Value::Boolean(b)) = obj.get("isNull") {
335        conditions.push(FilterNode::Condition {
336            column: column.to_string(),
337            op: if *b { CompareOp::IsNull } else { CompareOp::IsNotNull },
338            value: SqlValue::Bool(*b),
339        });
340    }
341
342    for (key, op) in &[("in", CompareOp::In), ("notIn", CompareOp::NotIn)] {
343        if let Some(async_graphql::Value::List(list)) = obj.get(*key) {
344            let values: Vec<String> = list.iter().filter_map(|item| match (dim_type, item) {
345                (DimType::String | DimType::DateTime | DimType::Decimal, async_graphql::Value::String(s)) => Some(s.clone()),
346                (DimType::Decimal, async_graphql::Value::Number(n)) => n.as_f64().map(|f| f.to_string()),
347                (DimType::Int, async_graphql::Value::String(s)) => s.parse::<i64>().ok().map(|i| i.to_string()),
348                (DimType::Int, async_graphql::Value::Number(n)) => n.as_i64().map(|i| i.to_string()),
349                (DimType::Float, async_graphql::Value::String(s)) => s.parse::<f64>().ok().map(|f| f.to_string()),
350                (DimType::Float, async_graphql::Value::Number(n)) => n.as_f64().map(|f| f.to_string()),
351                _ => None,
352            }).collect();
353            if !values.is_empty() {
354                conditions.push(FilterNode::Condition {
355                    column: column.to_string(),
356                    op: op.clone(),
357                    value: SqlValue::String(values.join(",")),
358                });
359            }
360        }
361    }
362
363    Ok(conditions)
364}
365
366fn normalize_datetime(s: &str) -> String {
367    let s = s.trim_end_matches('Z');
368    s.replacen('T', " ", 1)
369}
370
371fn value_to_sql(val: &async_graphql::Value, dim_type: &DimType) -> Option<SqlValue> {
372    match (dim_type, val) {
373        (DimType::Int, async_graphql::Value::Number(n)) => n.as_i64().map(SqlValue::Int),
374        (DimType::Int, async_graphql::Value::String(s)) => s.parse::<i64>().ok().map(SqlValue::Int),
375        (DimType::Float, async_graphql::Value::String(s)) => s.parse::<f64>().ok().map(SqlValue::Float),
376        (DimType::Float, async_graphql::Value::Number(n)) => n.as_f64().map(SqlValue::Float),
377        (DimType::Decimal, async_graphql::Value::String(s)) => Some(SqlValue::String(s.clone())),
378        (DimType::Decimal, async_graphql::Value::Number(n)) => n.as_f64().map(|f| SqlValue::String(f.to_string())),
379        (DimType::Bool, async_graphql::Value::Boolean(b)) => Some(SqlValue::Bool(*b)),
380        (DimType::DateTime, async_graphql::Value::String(s)) => {
381            Some(SqlValue::String(normalize_datetime(s)))
382        }
383        (DimType::String, async_graphql::Value::String(s)) => {
384            Some(SqlValue::String(s.clone()))
385        }
386        _ => None,
387    }
388}
389
390fn accessor_to_sql(
391    val: &ValueAccessor,
392    dim_type: &DimType,
393) -> Result<SqlValue, async_graphql::Error> {
394    match dim_type {
395        DimType::Int => {
396            let s = val.string()?;
397            let i = s.parse::<i64>().map_err(|_| {
398                async_graphql::Error::new(format!("Invalid integer value: {s}"))
399            })?;
400            Ok(SqlValue::Int(i))
401        }
402        DimType::Float => {
403            let s = val.string()?;
404            let f = s.parse::<f64>().map_err(|_| {
405                async_graphql::Error::new(format!("Invalid float value: {s}"))
406            })?;
407            Ok(SqlValue::Float(f))
408        }
409        DimType::Decimal => Ok(SqlValue::String(val.string()?.to_string())),
410        DimType::Bool => Ok(SqlValue::Bool(val.boolean()?)),
411        DimType::DateTime => Ok(SqlValue::String(normalize_datetime(val.string()?))),
412        DimType::String => Ok(SqlValue::String(val.string()?.to_string())),
413    }
414}
415
416// ---------------------------------------------------------------------------
417// Array includes filter parsing
418// ---------------------------------------------------------------------------
419
420fn array_columns_from_children(children: &[ArrayFieldDef]) -> Vec<String> {
421    children.iter().map(|f| f.column.clone()).collect()
422}
423
424fn dim_type_for_array_field(field: &ArrayFieldDef) -> DimType {
425    match &field.field_type {
426        ArrayFieldType::Scalar(dt) => dt.clone(),
427        ArrayFieldType::Union(_) => DimType::String,
428    }
429}
430
431fn parse_single_includes_object(
432    obj: &ObjectAccessor,
433    children: &[ArrayFieldDef],
434) -> Result<Vec<FilterNode>, async_graphql::Error> {
435    let mut conds = Vec::new();
436    for field in children {
437        if let Ok(filter_val) = obj.try_get(&field.graphql_name) {
438            if let Ok(filter_obj) = filter_val.object() {
439                let dt = dim_type_for_array_field(field);
440                let leaf = parse_leaf_filter(&filter_obj, &field.column, &dt)?;
441                conds.extend(leaf);
442            }
443        }
444    }
445    Ok(conds)
446}
447
448/// Parse `includes` from an ObjectAccessor (runtime GraphQL args).
449fn parse_array_includes_filter(
450    obj: &ObjectAccessor,
451    children: &[ArrayFieldDef],
452) -> Result<FilterNode, async_graphql::Error> {
453    if let Ok(includes_val) = obj.try_get("includes") {
454        let array_columns = array_columns_from_children(children);
455        if let Ok(list) = includes_val.list() {
456            let mut all_conditions = Vec::new();
457            for item in list.iter() {
458                if let Ok(item_obj) = item.object() {
459                    let conds = parse_single_includes_object(&item_obj, children)?;
460                    if !conds.is_empty() {
461                        all_conditions.push(conds);
462                    }
463                }
464            }
465            if all_conditions.is_empty() {
466                return Ok(FilterNode::Empty);
467            }
468            return Ok(FilterNode::ArrayIncludes { array_columns, element_conditions: all_conditions });
469        }
470        if let Ok(single_obj) = includes_val.object() {
471            let conds = parse_single_includes_object(&single_obj, children)?;
472            if conds.is_empty() {
473                return Ok(FilterNode::Empty);
474            }
475            return Ok(FilterNode::ArrayIncludes { array_columns, element_conditions: vec![conds] });
476        }
477    }
478    Ok(FilterNode::Empty)
479}
480
481fn parse_single_includes_from_value(
482    map: &indexmap::IndexMap<async_graphql::Name, async_graphql::Value>,
483    children: &[ArrayFieldDef],
484) -> Result<Vec<FilterNode>, async_graphql::Error> {
485    let mut conds = Vec::new();
486    for field in children {
487        if let Some(async_graphql::Value::Object(filter_map)) = map.get(field.graphql_name.as_str()) {
488            let dt = dim_type_for_array_field(field);
489            let leaf = parse_leaf_filter_from_value(filter_map, &field.column, &dt)?;
490            conds.extend(leaf);
491        }
492    }
493    Ok(conds)
494}
495
496/// Parse `includes` from a raw Value map (used in parse_filter_from_value path).
497fn parse_array_includes_from_value(
498    obj: &indexmap::IndexMap<async_graphql::Name, async_graphql::Value>,
499    children: &[ArrayFieldDef],
500) -> Result<FilterNode, async_graphql::Error> {
501    let array_columns = array_columns_from_children(children);
502
503    if let Some(async_graphql::Value::List(items)) = obj.get("includes") {
504        let mut all_conditions = Vec::new();
505        for item in items {
506            if let async_graphql::Value::Object(item_map) = item {
507                let conds = parse_single_includes_from_value(item_map, children)?;
508                if !conds.is_empty() {
509                    all_conditions.push(conds);
510                }
511            }
512        }
513        if all_conditions.is_empty() {
514            return Ok(FilterNode::Empty);
515        }
516        return Ok(FilterNode::ArrayIncludes { array_columns, element_conditions: all_conditions });
517    }
518
519    if let Some(async_graphql::Value::Object(single_map)) = obj.get("includes") {
520        let conds = parse_single_includes_from_value(single_map, children)?;
521        if conds.is_empty() {
522            return Ok(FilterNode::Empty);
523        }
524        return Ok(FilterNode::ArrayIncludes { array_columns, element_conditions: vec![conds] });
525    }
526
527    Ok(FilterNode::Empty)
528}
529
530fn parse_relative_time_accessor(obj: &ObjectAccessor) -> Option<String> {
531    if let Ok(v) = obj.try_get("minutes_ago") {
532        if let Ok(n) = v.i64() { return Some(format!("now() - INTERVAL {n} MINUTE")); }
533    }
534    if let Ok(v) = obj.try_get("hours_ago") {
535        if let Ok(n) = v.i64() { return Some(format!("now() - INTERVAL {n} HOUR")); }
536    }
537    if let Ok(v) = obj.try_get("days_ago") {
538        if let Ok(n) = v.i64() { return Some(format!("now() - INTERVAL {n} DAY")); }
539    }
540    None
541}
542
543fn parse_relative_time_value(
544    obj: &indexmap::IndexMap<async_graphql::Name, async_graphql::Value>,
545) -> Option<String> {
546    if let Some(async_graphql::Value::Number(n)) = obj.get("minutes_ago") {
547        if let Some(v) = n.as_i64() { return Some(format!("now() - INTERVAL {v} MINUTE")); }
548    }
549    if let Some(async_graphql::Value::Number(n)) = obj.get("hours_ago") {
550        if let Some(v) = n.as_i64() { return Some(format!("now() - INTERVAL {v} HOUR")); }
551    }
552    if let Some(async_graphql::Value::Number(n)) = obj.get("days_ago") {
553        if let Some(v) = n.as_i64() { return Some(format!("now() - INTERVAL {v} DAY")); }
554    }
555    None
556}
557