Skip to main content

activecube_rs/compiler/
parser.rs

1use async_graphql::dynamic::ObjectAccessor;
2
3use crate::compiler::filter;
4use crate::compiler::ir::*;
5use crate::cube::definition::{CubeDefinition, SelectorDef};
6
7/// Describes a metric requested in the GraphQL selection set.
8pub struct MetricRequest {
9    pub function: String,
10    pub of_dimension: String,
11    /// The raw selectWhere value extracted from GraphQL arguments.
12    pub select_where_value: Option<async_graphql::Value>,
13}
14
15pub fn parse_cube_query(
16    cube: &CubeDefinition,
17    network: &str,
18    args: &ObjectAccessor,
19    metrics: &[MetricRequest],
20) -> Result<QueryIR, async_graphql::Error> {
21    let table = cube.table_for_chain(network);
22
23    let filters = if let Ok(where_val) = args.try_get("where") {
24        if let Ok(where_obj) = where_val.object() {
25            filter::parse_where(&where_obj, &cube.dimensions)?
26        } else {
27            FilterNode::Empty
28        }
29    } else {
30        FilterNode::Empty
31    };
32
33    let filters = merge_selector_filters(filters, args, &cube.selectors)?;
34    // For tables that use a chain column instead of chain-prefixed table names,
35    // inject a WHERE chain = ? filter automatically.
36    let filters = if let Some(ref chain_col) = cube.chain_column {
37        let chain_filter = FilterNode::Condition {
38            column: chain_col.clone(),
39            op: CompareOp::Eq,
40            value: SqlValue::String(network.to_string()),
41        };
42        if filters.is_empty() {
43            chain_filter
44        } else {
45            FilterNode::And(vec![chain_filter, filters])
46        }
47    } else {
48        filters
49    };
50    let filters = apply_default_filters(filters, &cube.default_filters);
51    let (limit, offset) = parse_limit(args, cube.default_limit, cube.max_limit)?;
52    let order_by = parse_order_by(args, cube)?;
53
54    let flat = cube.flat_dimensions();
55    let mut selects: Vec<SelectExpr> = flat
56        .iter()
57        .map(|(_path, dim)| SelectExpr::Column {
58            column: dim.column.clone(),
59            alias: None,
60        })
61        .collect();
62
63    let mut group_by = Vec::new();
64    let mut having = FilterNode::Empty;
65
66    if !metrics.is_empty() {
67        group_by = flat.iter().map(|(_, dim)| dim.column.clone()).collect();
68
69        for m in metrics {
70            let dim_col = flat
71                .iter()
72                .find(|(path, _)| path == &m.of_dimension)
73                .map(|(_, dim)| dim.column.clone())
74                .unwrap_or_else(|| "*".to_string());
75
76            let func = m.function.to_uppercase();
77            let alias = format!("__{}", m.function);
78
79            selects.push(SelectExpr::Aggregate {
80                function: func.clone(),
81                column: dim_col.clone(),
82                alias,
83            });
84
85            if let Some(async_graphql::Value::Object(ref obj)) = m.select_where_value {
86                let agg_expr = if func == "COUNT" && dim_col == "*" {
87                    "COUNT(*)".to_string()
88                } else if func == "UNIQ" {
89                    format!("COUNT(DISTINCT `{dim_col}`)")
90                } else {
91                    format!("{func}(`{dim_col}`)")
92                };
93
94                let h = parse_select_where_from_value(obj, &agg_expr)?;
95                if !h.is_empty() {
96                    having = if having.is_empty() {
97                        h
98                    } else {
99                        FilterNode::And(vec![having, h])
100                    };
101                }
102            }
103        }
104    }
105
106    Ok(QueryIR {
107        cube: cube.name.clone(),
108        schema: cube.schema.clone(),
109        table,
110        selects,
111        filters,
112        having,
113        group_by,
114        order_by,
115        limit,
116        offset,
117    })
118}
119
120/// Parse a selectWhere value object (from GraphQL Value, not ObjectAccessor)
121/// into a HAVING FilterNode.
122fn parse_select_where_from_value(
123    obj: &indexmap::IndexMap<async_graphql::Name, async_graphql::Value>,
124    aggregate_expr: &str,
125) -> Result<FilterNode, async_graphql::Error> {
126    let mut conditions = Vec::new();
127
128    for (key, op) in &[
129        ("eq", CompareOp::Eq),
130        ("gt", CompareOp::Gt),
131        ("ge", CompareOp::Ge),
132        ("lt", CompareOp::Lt),
133        ("le", CompareOp::Le),
134    ] {
135        if let Some(val) = obj.get(*key) {
136            let sql_val = match val {
137                async_graphql::Value::String(s) => {
138                    if let Ok(f) = s.parse::<f64>() {
139                        SqlValue::Float(f)
140                    } else {
141                        SqlValue::String(s.clone())
142                    }
143                }
144                async_graphql::Value::Number(n) => {
145                    if let Some(f) = n.as_f64() {
146                        SqlValue::Float(f)
147                    } else {
148                        SqlValue::Int(n.as_i64().unwrap_or(0))
149                    }
150                }
151                _ => continue,
152            };
153            conditions.push(FilterNode::Condition {
154                column: aggregate_expr.to_string(),
155                op: op.clone(),
156                value: sql_val,
157            });
158        }
159    }
160
161    Ok(match conditions.len() {
162        0 => FilterNode::Empty,
163        1 => conditions.into_iter().next().unwrap(),
164        _ => FilterNode::And(conditions),
165    })
166}
167
168fn merge_selector_filters(
169    base: FilterNode,
170    args: &ObjectAccessor,
171    selectors: &[SelectorDef],
172) -> Result<FilterNode, async_graphql::Error> {
173    let mut extra = Vec::new();
174
175    for sel in selectors {
176        if let Ok(val) = args.try_get(&sel.graphql_name) {
177            if let Ok(obj) = val.object() {
178                let leaf_filters =
179                    filter::parse_leaf_filter_for_selector(&obj, &sel.column, &sel.dim_type)?;
180                extra.extend(leaf_filters);
181            }
182        }
183    }
184
185    if extra.is_empty() {
186        return Ok(base);
187    }
188    if base.is_empty() {
189        return Ok(if extra.len() == 1 {
190            extra.remove(0)
191        } else {
192            FilterNode::And(extra)
193        });
194    }
195    extra.push(base);
196    Ok(FilterNode::And(extra))
197}
198
199fn apply_default_filters(user_filters: FilterNode, defaults: &[(String, String)]) -> FilterNode {
200    if defaults.is_empty() {
201        return user_filters;
202    }
203
204    let mut default_nodes: Vec<FilterNode> = defaults
205        .iter()
206        .map(|(col, val)| {
207            let sql_val = if val == "true" || val == "false" {
208                SqlValue::Bool(val == "true")
209            } else if let Ok(n) = val.parse::<i64>() {
210                SqlValue::Int(n)
211            } else {
212                SqlValue::String(val.clone())
213            };
214            FilterNode::Condition {
215                column: col.clone(),
216                op: CompareOp::Eq,
217                value: sql_val,
218            }
219        })
220        .collect();
221
222    if user_filters.is_empty() {
223        if default_nodes.len() == 1 {
224            return default_nodes.remove(0);
225        }
226        return FilterNode::And(default_nodes);
227    }
228
229    default_nodes.push(user_filters);
230    FilterNode::And(default_nodes)
231}
232
233fn parse_limit(
234    args: &ObjectAccessor,
235    default: u32,
236    max: u32,
237) -> Result<(u32, u32), async_graphql::Error> {
238    let mut limit = default;
239    let mut offset = 0u32;
240
241    if let Ok(limit_val) = args.try_get("limit") {
242        if let Ok(limit_obj) = limit_val.object() {
243            if let Ok(count) = limit_obj.try_get("count") {
244                limit = (count.i64()? as u32).min(max);
245            }
246            if let Ok(off) = limit_obj.try_get("offset") {
247                offset = off.i64()? as u32;
248            }
249        }
250    }
251
252    Ok((limit, offset))
253}
254
255fn parse_order_by(
256    args: &ObjectAccessor,
257    cube: &CubeDefinition,
258) -> Result<Vec<OrderExpr>, async_graphql::Error> {
259    let order_val = match args.try_get("orderBy") {
260        Ok(v) => v,
261        Err(_) => return Ok(Vec::new()),
262    };
263
264    let enum_str = order_val
265        .enum_name()
266        .map_err(|_| async_graphql::Error::new("orderBy must be an enum value"))?;
267
268    let flat = cube.flat_dimensions();
269    let (descending, field_path) = if let Some(path) = enum_str.strip_suffix("_DESC") {
270        (true, path)
271    } else if let Some(path) = enum_str.strip_suffix("_ASC") {
272        (false, path)
273    } else {
274        return Err(async_graphql::Error::new(format!(
275            "Invalid orderBy value: {enum_str}"
276        )));
277    };
278
279    let column = flat
280        .iter()
281        .find(|(p, _)| p == field_path)
282        .map(|(_, dim)| dim.column.clone())
283        .ok_or_else(|| {
284            async_graphql::Error::new(format!("Unknown orderBy field: {field_path}"))
285        })?;
286
287    Ok(vec![OrderExpr { column, descending }])
288}