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
9pub struct MetricRequest {
11 pub function: String,
12 pub of_dimension: String,
13 pub select_where_value: Option<async_graphql::Value>,
15 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 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 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
159fn 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
357fn 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}