Skip to main content

activecube_rs/compiler/
parser.rs

1use std::collections::HashSet;
2
3use async_graphql::dynamic::ObjectAccessor;
4
5use crate::compiler::filter;
6use crate::compiler::ir::*;
7use crate::cube::definition::{CubeDefinition, SelectorDef};
8
9/// Describes a metric requested in the GraphQL selection set.
10pub struct MetricRequest {
11    pub function: String,
12    pub of_dimension: String,
13    /// The raw selectWhere value extracted from GraphQL arguments.
14    pub select_where_value: Option<async_graphql::Value>,
15    /// Pre-parsed condition filter for conditional aggregation (countIf/sumIf).
16    pub condition_filter: Option<FilterNode>,
17}
18
19pub fn parse_cube_query(
20    cube: &CubeDefinition,
21    network: &str,
22    args: &ObjectAccessor,
23    metrics: &[MetricRequest],
24    requested_fields: Option<HashSet<String>>,
25) -> Result<QueryIR, async_graphql::Error> {
26    let table = cube.table_for_chain(network);
27
28    let filters = if let Ok(where_val) = args.try_get("where") {
29        if let Ok(where_obj) = where_val.object() {
30            filter::parse_where(&where_obj, &cube.dimensions)?
31        } else {
32            FilterNode::Empty
33        }
34    } else {
35        FilterNode::Empty
36    };
37
38    let filters = merge_selector_filters(filters, args, &cube.selectors)?;
39    // For tables that use a chain column instead of chain-prefixed table names,
40    // inject a WHERE chain = ? filter automatically.
41    let filters = if let Some(ref chain_col) = cube.chain_column {
42        let chain_filter = FilterNode::Condition {
43            column: chain_col.clone(),
44            op: CompareOp::Eq,
45            value: SqlValue::String(network.to_string()),
46        };
47        if filters.is_empty() {
48            chain_filter
49        } else {
50            FilterNode::And(vec![chain_filter, filters])
51        }
52    } else {
53        filters
54    };
55    let filters = apply_default_filters(filters, &cube.default_filters);
56    let (limit, offset) = parse_limit(args, cube.default_limit, cube.max_limit)?;
57    let order_by = parse_order_by(args, cube)?;
58
59    let flat = cube.flat_dimensions();
60    let mut selects: Vec<SelectExpr> = flat
61        .iter()
62        .filter(|(path, _)| {
63            requested_fields
64                .as_ref()
65                .is_none_or(|rf| rf.contains(path))
66        })
67        .map(|(_, dim)| SelectExpr::Column {
68            column: dim.column.clone(),
69            alias: None,
70        })
71        .collect();
72
73    // When only metrics are requested with no dimension fields, keep selects empty
74    // so GROUP BY is also empty → produces a single aggregated row (e.g. total count).
75    // Only fall back to all dimensions when there are NO metrics either (pure wildcard).
76    if selects.is_empty() && !flat.is_empty() && metrics.is_empty() {
77        selects = flat
78            .iter()
79            .map(|(_, dim)| SelectExpr::Column {
80                column: dim.column.clone(),
81                alias: None,
82            })
83            .collect();
84    }
85
86    let mut group_by = Vec::new();
87    let mut having = FilterNode::Empty;
88
89    if !metrics.is_empty() {
90        group_by = selects
91            .iter()
92            .filter_map(|s| match s {
93                SelectExpr::Column { column, .. } => Some(column.clone()),
94                _ => None,
95            })
96            .collect();
97
98        for m in metrics {
99            let dim_col = flat
100                .iter()
101                .find(|(path, _)| path == &m.of_dimension)
102                .map(|(_, dim)| dim.column.clone())
103                .unwrap_or_else(|| "*".to_string());
104
105            let func = m.function.to_uppercase();
106            let alias = format!("__{}", m.function);
107
108            let condition = m.condition_filter.as_ref().and_then(|f| {
109                let sql = compile_filter_inline(f);
110                if sql.is_empty() { None } else { Some(sql) }
111            });
112
113            selects.push(SelectExpr::Aggregate {
114                function: func.clone(),
115                column: dim_col.clone(),
116                alias,
117                condition,
118            });
119
120            if let Some(async_graphql::Value::Object(ref obj)) = m.select_where_value {
121                let agg_expr = if func == "COUNT" && dim_col == "*" {
122                    "COUNT(*)".to_string()
123                } else if func == "UNIQ" {
124                    format!("COUNT(DISTINCT `{dim_col}`)")
125                } else {
126                    format!("{func}(`{dim_col}`)")
127                };
128
129                let h = parse_select_where_from_value(obj, &agg_expr)?;
130                if !h.is_empty() {
131                    having = if having.is_empty() {
132                        h
133                    } else {
134                        FilterNode::And(vec![having, h])
135                    };
136                }
137            }
138        }
139    }
140
141    let limit_by = parse_limit_by(args, cube)?;
142
143    Ok(QueryIR {
144        cube: cube.name.clone(),
145        schema: cube.schema.clone(),
146        table,
147        selects,
148        filters,
149        having,
150        group_by,
151        order_by,
152        limit,
153        offset,
154        limit_by,
155        use_final: cube.use_final,
156    })
157}
158
159/// Parse a selectWhere value object (from GraphQL Value, not ObjectAccessor)
160/// into a HAVING FilterNode.
161fn parse_select_where_from_value(
162    obj: &indexmap::IndexMap<async_graphql::Name, async_graphql::Value>,
163    aggregate_expr: &str,
164) -> Result<FilterNode, async_graphql::Error> {
165    let mut conditions = Vec::new();
166
167    for (key, op) in &[
168        ("eq", CompareOp::Eq),
169        ("gt", CompareOp::Gt),
170        ("ge", CompareOp::Ge),
171        ("lt", CompareOp::Lt),
172        ("le", CompareOp::Le),
173    ] {
174        if let Some(val) = obj.get(*key) {
175            let sql_val = match val {
176                async_graphql::Value::String(s) => {
177                    if let Ok(f) = s.parse::<f64>() {
178                        SqlValue::Float(f)
179                    } else {
180                        SqlValue::String(s.clone())
181                    }
182                }
183                async_graphql::Value::Number(n) => {
184                    if let Some(f) = n.as_f64() {
185                        SqlValue::Float(f)
186                    } else {
187                        SqlValue::Int(n.as_i64().unwrap_or(0))
188                    }
189                }
190                _ => continue,
191            };
192            conditions.push(FilterNode::Condition {
193                column: aggregate_expr.to_string(),
194                op: op.clone(),
195                value: sql_val,
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 merge_selector_filters(
208    base: FilterNode,
209    args: &ObjectAccessor,
210    selectors: &[SelectorDef],
211) -> Result<FilterNode, async_graphql::Error> {
212    let mut extra = Vec::new();
213
214    for sel in selectors {
215        if let Ok(val) = args.try_get(&sel.graphql_name) {
216            if let Ok(obj) = val.object() {
217                let leaf_filters =
218                    filter::parse_leaf_filter_for_selector(&obj, &sel.column, &sel.dim_type)?;
219                extra.extend(leaf_filters);
220            }
221        }
222    }
223
224    if extra.is_empty() {
225        return Ok(base);
226    }
227    if base.is_empty() {
228        return Ok(if extra.len() == 1 {
229            extra.remove(0)
230        } else {
231            FilterNode::And(extra)
232        });
233    }
234    extra.push(base);
235    Ok(FilterNode::And(extra))
236}
237
238fn apply_default_filters(user_filters: FilterNode, defaults: &[(String, String)]) -> FilterNode {
239    if defaults.is_empty() {
240        return user_filters;
241    }
242
243    let mut default_nodes: Vec<FilterNode> = defaults
244        .iter()
245        .map(|(col, val)| {
246            let sql_val = if val == "true" || val == "false" {
247                SqlValue::Bool(val == "true")
248            } else if let Ok(n) = val.parse::<i64>() {
249                SqlValue::Int(n)
250            } else {
251                SqlValue::String(val.clone())
252            };
253            FilterNode::Condition {
254                column: col.clone(),
255                op: CompareOp::Eq,
256                value: sql_val,
257            }
258        })
259        .collect();
260
261    if user_filters.is_empty() {
262        if default_nodes.len() == 1 {
263            return default_nodes.remove(0);
264        }
265        return FilterNode::And(default_nodes);
266    }
267
268    default_nodes.push(user_filters);
269    FilterNode::And(default_nodes)
270}
271
272fn parse_limit(
273    args: &ObjectAccessor,
274    default: u32,
275    max: u32,
276) -> Result<(u32, u32), async_graphql::Error> {
277    let mut limit = default;
278    let mut offset = 0u32;
279
280    if let Ok(limit_val) = args.try_get("limit") {
281        if let Ok(limit_obj) = limit_val.object() {
282            if let Ok(count) = limit_obj.try_get("count") {
283                limit = (count.i64()? as u32).min(max);
284            }
285            if let Ok(off) = limit_obj.try_get("offset") {
286                offset = off.i64()? as u32;
287            }
288        }
289    }
290
291    Ok((limit, offset))
292}
293
294fn parse_order_by(
295    args: &ObjectAccessor,
296    cube: &CubeDefinition,
297) -> Result<Vec<OrderExpr>, async_graphql::Error> {
298    let flat = cube.flat_dimensions();
299
300    if let Ok(list_val) = args.try_get("orderByList") {
301        if let Ok(list) = list_val.list() {
302            let mut orders = Vec::new();
303            for item in list.iter() {
304                let obj = item.object()
305                    .map_err(|_| async_graphql::Error::new("orderByList items must be objects"))?;
306                let field_accessor = obj.try_get("field")
307                    .map_err(|_| async_graphql::Error::new("orderByList item requires 'field'"))?;
308                let field_str = field_accessor.enum_name()
309                    .map_err(|_| async_graphql::Error::new("orderByList 'field' must be an enum value"))?;
310                let descending = if let Ok(dir_accessor) = obj.try_get("direction") {
311                    dir_accessor.enum_name() == Ok("DESC")
312                } else {
313                    false
314                };
315                let column = flat.iter()
316                    .find(|(p, _)| p == field_str)
317                    .map(|(_, dim)| dim.column.clone())
318                    .ok_or_else(|| async_graphql::Error::new(format!("Unknown orderBy field: {field_str}")))?;
319                orders.push(OrderExpr { column, descending });
320            }
321            if !orders.is_empty() {
322                return Ok(orders);
323            }
324        }
325    }
326
327    let order_val = match args.try_get("orderBy") {
328        Ok(v) => v,
329        Err(_) => return Ok(Vec::new()),
330    };
331
332    let enum_str = order_val
333        .enum_name()
334        .map_err(|_| async_graphql::Error::new("orderBy must be an enum value"))?;
335
336    let (descending, field_path) = if let Some(path) = enum_str.strip_suffix("_DESC") {
337        (true, path)
338    } else if let Some(path) = enum_str.strip_suffix("_ASC") {
339        (false, path)
340    } else {
341        return Err(async_graphql::Error::new(format!(
342            "Invalid orderBy value: {enum_str}"
343        )));
344    };
345
346    let column = flat
347        .iter()
348        .find(|(p, _)| p == field_path)
349        .map(|(_, dim)| dim.column.clone())
350        .ok_or_else(|| {
351            async_graphql::Error::new(format!("Unknown orderBy field: {field_path}"))
352        })?;
353
354    Ok(vec![OrderExpr { column, descending }])
355}
356
357/// Compile a FilterNode into an inline SQL fragment (no parameterized bindings).
358/// Used for embedding conditions inside aggregate functions (countIf, sumIf).
359fn compile_filter_inline(node: &FilterNode) -> String {
360    match node {
361        FilterNode::Empty => String::new(),
362        FilterNode::Condition { column, op, value } => {
363            let col = if column.contains('(') { column.clone() } else { format!("`{column}`") };
364            if op.is_unary() {
365                return format!("{col} {}", op.sql_op());
366            }
367            let val_str = match value {
368                SqlValue::String(s) => format!("'{}'", s.replace('\'', "\\'")),
369                SqlValue::Int(i) => i.to_string(),
370                SqlValue::Float(f) => f.to_string(),
371                SqlValue::Bool(b) => if *b { "1".to_string() } else { "0".to_string() },
372            };
373            match op {
374                CompareOp::In | CompareOp::NotIn => {
375                    if let SqlValue::String(csv) = value {
376                        let items: Vec<String> = csv.split(',')
377                            .map(|s| format!("'{}'", s.trim().replace('\'', "\\'")))
378                            .collect();
379                        format!("{col} {} ({})", op.sql_op(), items.join(", "))
380                    } else {
381                        format!("{col} {} ({val_str})", op.sql_op())
382                    }
383                }
384                CompareOp::Includes => {
385                    let like_val = match value {
386                        SqlValue::String(s) => format!("'%{}%'", s.replace('\'', "\\'")),
387                        _ => val_str,
388                    };
389                    format!("{col} LIKE {like_val}")
390                }
391                _ => format!("{col} {} {val_str}", op.sql_op()),
392            }
393        }
394        FilterNode::And(children) => {
395            let parts: Vec<String> = children.iter()
396                .map(compile_filter_inline)
397                .filter(|s| !s.is_empty())
398                .collect();
399            match parts.len() {
400                0 => String::new(),
401                1 => parts.into_iter().next().unwrap(),
402                _ => format!("({})", parts.join(" AND ")),
403            }
404        }
405        FilterNode::Or(children) => {
406            let parts: Vec<String> = children.iter()
407                .map(compile_filter_inline)
408                .filter(|s| !s.is_empty())
409                .collect();
410            match parts.len() {
411                0 => String::new(),
412                1 => parts.into_iter().next().unwrap(),
413                _ => format!("({})", parts.join(" OR ")),
414            }
415        }
416    }
417}
418
419fn parse_limit_by(
420    args: &ObjectAccessor,
421    cube: &CubeDefinition,
422) -> Result<Option<LimitByExpr>, async_graphql::Error> {
423    let lb_val = match args.try_get("limitBy") {
424        Ok(v) => v,
425        Err(_) => return Ok(None),
426    };
427    let lb_obj = lb_val.object()?;
428    let count = lb_obj.try_get("count")?.i64()? as u32;
429    let offset = lb_obj
430        .try_get("offset")
431        .ok()
432        .and_then(|v| v.i64().ok())
433        .unwrap_or(0) as u32;
434    let by_str = lb_obj.try_get("by")?.string()?;
435
436    let flat = cube.flat_dimensions();
437    let columns: Vec<String> = by_str
438        .split(',')
439        .map(|s| {
440            let trimmed = s.trim();
441            flat.iter()
442                .find(|(path, _)| path == trimmed)
443                .map(|(_, dim)| dim.column.clone())
444                .unwrap_or_else(|| trimmed.to_string())
445        })
446        .collect();
447
448    if columns.is_empty() {
449        return Err(async_graphql::Error::new("limitBy.by must specify at least one field"));
450    }
451
452    Ok(Some(LimitByExpr { count, offset, columns }))
453}