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    // If no columns matched (e.g., only metrics requested), include all dimensions
74    if selects.is_empty() && !flat.is_empty() {
75        selects = flat
76            .iter()
77            .map(|(_, dim)| SelectExpr::Column {
78                column: dim.column.clone(),
79                alias: None,
80            })
81            .collect();
82    }
83
84    let mut group_by = Vec::new();
85    let mut having = FilterNode::Empty;
86
87    if !metrics.is_empty() {
88        group_by = selects
89            .iter()
90            .filter_map(|s| match s {
91                SelectExpr::Column { column, .. } => Some(column.clone()),
92                _ => None,
93            })
94            .collect();
95
96        for m in metrics {
97            let dim_col = flat
98                .iter()
99                .find(|(path, _)| path == &m.of_dimension)
100                .map(|(_, dim)| dim.column.clone())
101                .unwrap_or_else(|| "*".to_string());
102
103            let func = m.function.to_uppercase();
104            let alias = format!("__{}", m.function);
105
106            let condition = m.condition_filter.as_ref().and_then(|f| {
107                let sql = compile_filter_inline(f);
108                if sql.is_empty() { None } else { Some(sql) }
109            });
110
111            selects.push(SelectExpr::Aggregate {
112                function: func.clone(),
113                column: dim_col.clone(),
114                alias,
115                condition,
116            });
117
118            if let Some(async_graphql::Value::Object(ref obj)) = m.select_where_value {
119                let agg_expr = if func == "COUNT" && dim_col == "*" {
120                    "COUNT(*)".to_string()
121                } else if func == "UNIQ" {
122                    format!("COUNT(DISTINCT `{dim_col}`)")
123                } else {
124                    format!("{func}(`{dim_col}`)")
125                };
126
127                let h = parse_select_where_from_value(obj, &agg_expr)?;
128                if !h.is_empty() {
129                    having = if having.is_empty() {
130                        h
131                    } else {
132                        FilterNode::And(vec![having, h])
133                    };
134                }
135            }
136        }
137    }
138
139    Ok(QueryIR {
140        cube: cube.name.clone(),
141        schema: cube.schema.clone(),
142        table,
143        selects,
144        filters,
145        having,
146        group_by,
147        order_by,
148        limit,
149        offset,
150        use_final: cube.use_final,
151    })
152}
153
154/// Parse a selectWhere value object (from GraphQL Value, not ObjectAccessor)
155/// into a HAVING FilterNode.
156fn parse_select_where_from_value(
157    obj: &indexmap::IndexMap<async_graphql::Name, async_graphql::Value>,
158    aggregate_expr: &str,
159) -> Result<FilterNode, async_graphql::Error> {
160    let mut conditions = Vec::new();
161
162    for (key, op) in &[
163        ("eq", CompareOp::Eq),
164        ("gt", CompareOp::Gt),
165        ("ge", CompareOp::Ge),
166        ("lt", CompareOp::Lt),
167        ("le", CompareOp::Le),
168    ] {
169        if let Some(val) = obj.get(*key) {
170            let sql_val = match val {
171                async_graphql::Value::String(s) => {
172                    if let Ok(f) = s.parse::<f64>() {
173                        SqlValue::Float(f)
174                    } else {
175                        SqlValue::String(s.clone())
176                    }
177                }
178                async_graphql::Value::Number(n) => {
179                    if let Some(f) = n.as_f64() {
180                        SqlValue::Float(f)
181                    } else {
182                        SqlValue::Int(n.as_i64().unwrap_or(0))
183                    }
184                }
185                _ => continue,
186            };
187            conditions.push(FilterNode::Condition {
188                column: aggregate_expr.to_string(),
189                op: op.clone(),
190                value: sql_val,
191            });
192        }
193    }
194
195    Ok(match conditions.len() {
196        0 => FilterNode::Empty,
197        1 => conditions.into_iter().next().unwrap(),
198        _ => FilterNode::And(conditions),
199    })
200}
201
202fn merge_selector_filters(
203    base: FilterNode,
204    args: &ObjectAccessor,
205    selectors: &[SelectorDef],
206) -> Result<FilterNode, async_graphql::Error> {
207    let mut extra = Vec::new();
208
209    for sel in selectors {
210        if let Ok(val) = args.try_get(&sel.graphql_name) {
211            if let Ok(obj) = val.object() {
212                let leaf_filters =
213                    filter::parse_leaf_filter_for_selector(&obj, &sel.column, &sel.dim_type)?;
214                extra.extend(leaf_filters);
215            }
216        }
217    }
218
219    if extra.is_empty() {
220        return Ok(base);
221    }
222    if base.is_empty() {
223        return Ok(if extra.len() == 1 {
224            extra.remove(0)
225        } else {
226            FilterNode::And(extra)
227        });
228    }
229    extra.push(base);
230    Ok(FilterNode::And(extra))
231}
232
233fn apply_default_filters(user_filters: FilterNode, defaults: &[(String, String)]) -> FilterNode {
234    if defaults.is_empty() {
235        return user_filters;
236    }
237
238    let mut default_nodes: Vec<FilterNode> = defaults
239        .iter()
240        .map(|(col, val)| {
241            let sql_val = if val == "true" || val == "false" {
242                SqlValue::Bool(val == "true")
243            } else if let Ok(n) = val.parse::<i64>() {
244                SqlValue::Int(n)
245            } else {
246                SqlValue::String(val.clone())
247            };
248            FilterNode::Condition {
249                column: col.clone(),
250                op: CompareOp::Eq,
251                value: sql_val,
252            }
253        })
254        .collect();
255
256    if user_filters.is_empty() {
257        if default_nodes.len() == 1 {
258            return default_nodes.remove(0);
259        }
260        return FilterNode::And(default_nodes);
261    }
262
263    default_nodes.push(user_filters);
264    FilterNode::And(default_nodes)
265}
266
267fn parse_limit(
268    args: &ObjectAccessor,
269    default: u32,
270    max: u32,
271) -> Result<(u32, u32), async_graphql::Error> {
272    let mut limit = default;
273    let mut offset = 0u32;
274
275    if let Ok(limit_val) = args.try_get("limit") {
276        if let Ok(limit_obj) = limit_val.object() {
277            if let Ok(count) = limit_obj.try_get("count") {
278                limit = (count.i64()? as u32).min(max);
279            }
280            if let Ok(off) = limit_obj.try_get("offset") {
281                offset = off.i64()? as u32;
282            }
283        }
284    }
285
286    Ok((limit, offset))
287}
288
289fn parse_order_by(
290    args: &ObjectAccessor,
291    cube: &CubeDefinition,
292) -> Result<Vec<OrderExpr>, async_graphql::Error> {
293    let flat = cube.flat_dimensions();
294
295    if let Ok(list_val) = args.try_get("orderByList") {
296        if let Ok(list) = list_val.list() {
297            let mut orders = Vec::new();
298            for item in list.iter() {
299                let obj = item.object()
300                    .map_err(|_| async_graphql::Error::new("orderByList items must be objects"))?;
301                let field_accessor = obj.try_get("field")
302                    .map_err(|_| async_graphql::Error::new("orderByList item requires 'field'"))?;
303                let field_str = field_accessor.enum_name()
304                    .map_err(|_| async_graphql::Error::new("orderByList 'field' must be an enum value"))?;
305                let descending = if let Ok(dir_accessor) = obj.try_get("direction") {
306                    dir_accessor.enum_name() == Ok("DESC")
307                } else {
308                    false
309                };
310                let column = flat.iter()
311                    .find(|(p, _)| p == field_str)
312                    .map(|(_, dim)| dim.column.clone())
313                    .ok_or_else(|| async_graphql::Error::new(format!("Unknown orderBy field: {field_str}")))?;
314                orders.push(OrderExpr { column, descending });
315            }
316            if !orders.is_empty() {
317                return Ok(orders);
318            }
319        }
320    }
321
322    let order_val = match args.try_get("orderBy") {
323        Ok(v) => v,
324        Err(_) => return Ok(Vec::new()),
325    };
326
327    let enum_str = order_val
328        .enum_name()
329        .map_err(|_| async_graphql::Error::new("orderBy must be an enum value"))?;
330
331    let (descending, field_path) = if let Some(path) = enum_str.strip_suffix("_DESC") {
332        (true, path)
333    } else if let Some(path) = enum_str.strip_suffix("_ASC") {
334        (false, path)
335    } else {
336        return Err(async_graphql::Error::new(format!(
337            "Invalid orderBy value: {enum_str}"
338        )));
339    };
340
341    let column = flat
342        .iter()
343        .find(|(p, _)| p == field_path)
344        .map(|(_, dim)| dim.column.clone())
345        .ok_or_else(|| {
346            async_graphql::Error::new(format!("Unknown orderBy field: {field_path}"))
347        })?;
348
349    Ok(vec![OrderExpr { column, descending }])
350}
351
352/// Compile a FilterNode into an inline SQL fragment (no parameterized bindings).
353/// Used for embedding conditions inside aggregate functions (countIf, sumIf).
354fn compile_filter_inline(node: &FilterNode) -> String {
355    match node {
356        FilterNode::Empty => String::new(),
357        FilterNode::Condition { column, op, value } => {
358            let col = if column.contains('(') { column.clone() } else { format!("`{column}`") };
359            if op.is_unary() {
360                return format!("{col} {}", op.sql_op());
361            }
362            let val_str = match value {
363                SqlValue::String(s) => format!("'{}'", s.replace('\'', "\\'")),
364                SqlValue::Int(i) => i.to_string(),
365                SqlValue::Float(f) => f.to_string(),
366                SqlValue::Bool(b) => if *b { "1".to_string() } else { "0".to_string() },
367            };
368            match op {
369                CompareOp::In | CompareOp::NotIn => {
370                    if let SqlValue::String(csv) = value {
371                        let items: Vec<String> = csv.split(',')
372                            .map(|s| format!("'{}'", s.trim().replace('\'', "\\'")))
373                            .collect();
374                        format!("{col} {} ({})", op.sql_op(), items.join(", "))
375                    } else {
376                        format!("{col} {} ({val_str})", op.sql_op())
377                    }
378                }
379                CompareOp::Includes => {
380                    let like_val = match value {
381                        SqlValue::String(s) => format!("'%{}%'", s.replace('\'', "\\'")),
382                        _ => val_str,
383                    };
384                    format!("{col} LIKE {like_val}")
385                }
386                _ => format!("{col} {} {val_str}", op.sql_op()),
387            }
388        }
389        FilterNode::And(children) => {
390            let parts: Vec<String> = children.iter()
391                .map(compile_filter_inline)
392                .filter(|s| !s.is_empty())
393                .collect();
394            match parts.len() {
395                0 => String::new(),
396                1 => parts.into_iter().next().unwrap(),
397                _ => format!("({})", parts.join(" AND ")),
398            }
399        }
400        FilterNode::Or(children) => {
401            let parts: Vec<String> = children.iter()
402                .map(compile_filter_inline)
403                .filter(|s| !s.is_empty())
404                .collect();
405            match parts.len() {
406                0 => String::new(),
407                1 => parts.into_iter().next().unwrap(),
408                _ => format!("({})", parts.join(" OR ")),
409            }
410        }
411    }
412}