1use serde_json::Value;
49
50use crate::{
51 compiler::{
52 aggregate_types::{AggregateFunction, HavingOperator, TemporalBucket},
53 aggregation::{
54 AggregateSelection, AggregationRequest, GroupBySelection, HavingCondition,
55 OrderByClause, OrderDirection,
56 },
57 fact_table::FactTableMetadata,
58 },
59 db::where_clause::{WhereClause, WhereOperator},
60 error::{FraiseQLError, Result},
61};
62
63pub struct AggregateQueryParser;
65
66impl AggregateQueryParser {
67 pub fn parse(
100 query_json: &Value,
101 metadata: &FactTableMetadata,
102 native_columns: &std::collections::HashMap<String, String>,
103 ) -> Result<AggregationRequest> {
104 let table_name = query_json
106 .get("table")
107 .and_then(|v| v.as_str())
108 .ok_or_else(|| FraiseQLError::Validation {
109 message: "Missing 'table' field in aggregate query".to_string(),
110 path: None,
111 })?
112 .to_string();
113
114 let where_clause = if let Some(where_obj) = query_json.get("where") {
116 Some(Self::parse_where_clause(where_obj, native_columns)?)
117 } else {
118 None
119 };
120
121 let group_by = if let Some(group_by_obj) = query_json.get("groupBy") {
123 Self::parse_group_by(group_by_obj, metadata, native_columns)?
124 } else {
125 vec![]
126 };
127
128 let aggregates = if let Some(agg_array) = query_json.get("aggregates") {
130 Self::parse_aggregates(agg_array, metadata)?
131 } else {
132 vec![]
133 };
134
135 let having = if let Some(having_obj) = query_json.get("having") {
137 Self::parse_having(having_obj, &aggregates, metadata)?
138 } else {
139 vec![]
140 };
141
142 let order_by = if let Some(order_obj) = query_json.get("orderBy") {
144 Self::parse_order_by(order_obj)?
145 } else {
146 vec![]
147 };
148
149 let limit = query_json
151 .get("limit")
152 .and_then(|v| v.as_u64())
153 .map(|n| u32::try_from(n).unwrap_or(u32::MAX));
154
155 let offset = query_json
157 .get("offset")
158 .and_then(|v| v.as_u64())
159 .map(|n| u32::try_from(n).unwrap_or(u32::MAX));
160
161 Ok(AggregationRequest {
162 table_name,
163 where_clause,
164 group_by,
165 aggregates,
166 having,
167 order_by,
168 limit,
169 offset,
170 })
171 }
172
173 fn parse_where_clause(
183 where_obj: &Value,
184 native_columns: &std::collections::HashMap<String, String>,
185 ) -> Result<WhereClause> {
186 let Some(obj) = where_obj.as_object() else {
187 return Ok(WhereClause::And(vec![]));
188 };
189
190 let mut conditions = Vec::new();
191
192 for (key, value) in obj {
193 if let Some((field, operator_str)) = Self::parse_where_field_and_operator(key)? {
196 let operator = WhereOperator::from_str(operator_str)?;
197
198 let clause = if let Some(pg_cast) = native_columns.get(field) {
199 WhereClause::NativeField {
200 column: field.to_string(),
201 pg_cast: pg_cast.clone(),
202 operator,
203 value: value.clone(),
204 }
205 } else {
206 WhereClause::Field {
207 path: vec![field.to_string()],
208 operator,
209 value: value.clone(),
210 }
211 };
212 conditions.push(clause);
213 }
214 }
215
216 Ok(WhereClause::And(conditions))
217 }
218
219 fn parse_where_field_and_operator(key: &str) -> Result<Option<(&str, &str)>> {
222 if let Some(last_underscore) = key.rfind('_') {
224 let field = &key[..last_underscore];
225 let operator = &key[last_underscore + 1..];
226
227 match WhereOperator::from_str(operator) {
229 Ok(_) => Ok(Some((field, operator))),
230 Err(_) => {
231 Ok(None)
234 },
235 }
236 } else {
237 Ok(None)
239 }
240 }
241
242 fn parse_group_by(
252 group_by_obj: &Value,
253 metadata: &FactTableMetadata,
254 native_columns: &std::collections::HashMap<String, String>,
255 ) -> Result<Vec<GroupBySelection>> {
256 let mut selections = Vec::new();
257
258 if let Some(obj) = group_by_obj.as_object() {
259 for (key, value) in obj {
260 if value.as_bool() == Some(true) {
261 if let Some(calendar_sel) = Self::try_parse_calendar_bucket(key, metadata)? {
264 selections.push(calendar_sel);
265 } else if let Some(bucket_sel) = Self::parse_temporal_bucket(key, metadata)? {
266 selections.push(bucket_sel);
268 } else if let Some(mapped_col) =
269 metadata.native_dimension_mapping.get(key.as_str())
270 {
271 selections.push(GroupBySelection::NativeDimension {
273 column: mapped_col.clone(),
274 pg_cast: String::new(),
275 });
276 } else if let Some(pg_cast) = native_columns.get(key.as_str()) {
277 selections.push(GroupBySelection::NativeDimension {
279 column: key.clone(),
280 pg_cast: pg_cast.clone(),
281 });
282 } else {
283 selections.push(GroupBySelection::Dimension {
285 path: key.clone(),
286 alias: key.clone(),
287 });
288 }
289 } else if let Some(bucket_str) = value.as_str() {
290 let bucket = TemporalBucket::from_str(bucket_str)?;
292
293 if let Some(calendar_sel) =
295 Self::try_find_calendar_bucket(key, bucket, metadata)
296 {
297 selections.push(calendar_sel);
298 } else {
299 let column_exists =
302 metadata.denormalized_filters.iter().any(|f| f.name == *key);
303
304 if !column_exists {
305 return Err(FraiseQLError::Validation {
306 message: format!(
307 "Temporal bucketing column '{}' not found in denormalized filters",
308 key
309 ),
310 path: None,
311 });
312 }
313
314 selections.push(GroupBySelection::TemporalBucket {
315 column: key.clone(),
316 bucket,
317 alias: key.clone(),
318 });
319 }
320 }
321 }
322 }
323
324 Ok(selections)
325 }
326
327 fn parse_temporal_bucket(
329 key: &str,
330 metadata: &FactTableMetadata,
331 ) -> Result<Option<GroupBySelection>> {
332 for filter_col in &metadata.denormalized_filters {
334 for bucket in &[
335 ("_second", TemporalBucket::Second),
336 ("_minute", TemporalBucket::Minute),
337 ("_hour", TemporalBucket::Hour),
338 ("_day", TemporalBucket::Day),
339 ("_week", TemporalBucket::Week),
340 ("_month", TemporalBucket::Month),
341 ("_quarter", TemporalBucket::Quarter),
342 ("_year", TemporalBucket::Year),
343 ] {
344 let expected_key = format!("{}{}", filter_col.name, bucket.0);
345 if key == expected_key {
346 return Ok(Some(GroupBySelection::TemporalBucket {
347 column: filter_col.name.clone(),
348 bucket: bucket.1,
349 alias: key.to_string(),
350 }));
351 }
352 }
353 }
354
355 Ok(None)
356 }
357
358 fn try_parse_calendar_bucket(
363 key: &str,
364 metadata: &FactTableMetadata,
365 ) -> Result<Option<GroupBySelection>> {
366 for calendar_dim in &metadata.calendar_dimensions {
367 for (suffix, bucket_type) in &[
369 ("_second", TemporalBucket::Second),
370 ("_minute", TemporalBucket::Minute),
371 ("_hour", TemporalBucket::Hour),
372 ("_day", TemporalBucket::Day),
373 ("_week", TemporalBucket::Week),
374 ("_month", TemporalBucket::Month),
375 ("_quarter", TemporalBucket::Quarter),
376 ("_year", TemporalBucket::Year),
377 ] {
378 let expected_key = format!("{}{}", calendar_dim.source_column, suffix);
379 if key == expected_key {
380 if let Some((gran, bucket)) =
382 Self::find_calendar_bucket(calendar_dim, *bucket_type)
383 {
384 return Ok(Some(GroupBySelection::CalendarDimension {
385 source_column: calendar_dim.source_column.clone(),
386 calendar_column: gran.column_name.clone(),
387 json_key: bucket.json_key.clone(),
388 bucket: bucket.bucket_type,
389 alias: key.to_string(),
390 }));
391 }
392 }
393 }
394 }
395 Ok(None)
396 }
397
398 fn try_find_calendar_bucket(
402 column: &str,
403 bucket: TemporalBucket,
404 metadata: &FactTableMetadata,
405 ) -> Option<GroupBySelection> {
406 for calendar_dim in &metadata.calendar_dimensions {
407 if calendar_dim.source_column == column {
408 if let Some((gran, cal_bucket)) = Self::find_calendar_bucket(calendar_dim, bucket) {
409 return Some(GroupBySelection::CalendarDimension {
410 source_column: calendar_dim.source_column.clone(),
411 calendar_column: gran.column_name.clone(),
412 json_key: cal_bucket.json_key.clone(),
413 bucket: cal_bucket.bucket_type,
414 alias: column.to_string(),
415 });
416 }
417 }
418 }
419 None
420 }
421
422 fn find_calendar_bucket(
427 calendar_dim: &crate::compiler::fact_table::CalendarDimension,
428 bucket: TemporalBucket,
429 ) -> Option<(
430 &crate::compiler::fact_table::CalendarGranularity,
431 &crate::compiler::fact_table::CalendarBucket,
432 )> {
433 for granularity in &calendar_dim.granularities {
434 for cal_bucket in &granularity.buckets {
435 if cal_bucket.bucket_type == bucket {
436 return Some((granularity, cal_bucket));
437 }
438 }
439 }
440 None
441 }
442
443 fn parse_aggregates(
445 agg_array: &Value,
446 metadata: &FactTableMetadata,
447 ) -> Result<Vec<AggregateSelection>> {
448 let mut aggregates = Vec::new();
449
450 if let Some(arr) = agg_array.as_array() {
451 for item in arr {
452 if let Some(obj) = item.as_object() {
453 for (agg_name, _value) in obj {
455 aggregates.push(Self::parse_aggregate_selection(agg_name, metadata)?);
456 }
457 }
458 }
459 }
460
461 Ok(aggregates)
462 }
463
464 fn parse_aggregate_selection(
466 agg_name: &str,
467 metadata: &FactTableMetadata,
468 ) -> Result<AggregateSelection> {
469 if agg_name == "count" {
471 return Ok(AggregateSelection::Count {
472 alias: "count".to_string(),
473 });
474 }
475
476 if agg_name == "count_distinct" {
479 let default_field = Self::extract_dimension_paths(metadata)
481 .first()
482 .cloned()
483 .unwrap_or_else(|| "id".to_string());
484 return Ok(AggregateSelection::CountDistinct {
485 field: default_field,
486 alias: "count_distinct".to_string(),
487 });
488 }
489
490 if let Some(stripped) = agg_name.strip_suffix("_count_distinct") {
492 let dimension_paths = Self::extract_dimension_paths(metadata);
494 if dimension_paths.iter().any(|p| p == stripped) {
495 return Ok(AggregateSelection::CountDistinct {
496 field: stripped.to_string(),
497 alias: agg_name.to_string(),
498 });
499 }
500 if metadata.measures.iter().any(|m| m.name == stripped) {
502 return Ok(AggregateSelection::CountDistinct {
503 field: stripped.to_string(),
504 alias: agg_name.to_string(),
505 });
506 }
507 return Err(FraiseQLError::Validation {
509 message: format!(
510 "COUNT DISTINCT field '{}' not found in dimensions or measures. Available: {:?}",
511 stripped, dimension_paths
512 ),
513 path: None,
514 });
515 }
516
517 for dimension_path in Self::extract_dimension_paths(metadata) {
520 if let Some(stripped) = agg_name.strip_suffix("_bool_and") {
521 if stripped == dimension_path {
522 return Ok(AggregateSelection::BoolAggregate {
523 field: dimension_path,
524 function: crate::compiler::aggregate_types::BoolAggregateFunction::And,
525 alias: agg_name.to_string(),
526 });
527 }
528 }
529 if let Some(stripped) = agg_name.strip_suffix("_bool_or") {
530 if stripped == dimension_path {
531 return Ok(AggregateSelection::BoolAggregate {
532 field: dimension_path,
533 function: crate::compiler::aggregate_types::BoolAggregateFunction::Or,
534 alias: agg_name.to_string(),
535 });
536 }
537 }
538 }
539
540 for measure in &metadata.measures {
542 for func in &[
543 ("_sum", AggregateFunction::Sum),
544 ("_avg", AggregateFunction::Avg),
545 ("_min", AggregateFunction::Min),
546 ("_max", AggregateFunction::Max),
547 ("_stddev", AggregateFunction::Stddev),
548 ("_variance", AggregateFunction::Variance),
549 ("_array_agg", AggregateFunction::ArrayAgg),
551 ("_json_agg", AggregateFunction::JsonAgg),
552 ("_jsonb_agg", AggregateFunction::JsonbAgg),
553 ("_string_agg", AggregateFunction::StringAgg),
554 ] {
555 let expected_name = format!("{}{}", measure.name, func.0);
556 if agg_name == expected_name {
557 return Ok(AggregateSelection::MeasureAggregate {
558 measure: measure.name.clone(),
559 function: func.1,
560 alias: agg_name.to_string(),
561 });
562 }
563 }
564 }
565
566 for jsonb_path in metadata.native_measures.keys() {
569 for func in &[
570 ("_sum", AggregateFunction::Sum),
571 ("_avg", AggregateFunction::Avg),
572 ("_min", AggregateFunction::Min),
573 ("_max", AggregateFunction::Max),
574 ("_stddev", AggregateFunction::Stddev),
575 ("_variance", AggregateFunction::Variance),
576 ("_array_agg", AggregateFunction::ArrayAgg),
577 ("_json_agg", AggregateFunction::JsonAgg),
578 ("_jsonb_agg", AggregateFunction::JsonbAgg),
579 ("_string_agg", AggregateFunction::StringAgg),
580 ] {
581 let expected_name = format!("{}{}", jsonb_path, func.0);
582 if agg_name == expected_name {
583 return Ok(AggregateSelection::MeasureAggregate {
586 measure: jsonb_path.clone(),
587 function: func.1,
588 alias: agg_name.to_string(),
589 });
590 }
591 }
592 }
593
594 for dimension_path in Self::extract_dimension_paths(metadata) {
597 for func in &[
598 ("_array_agg", AggregateFunction::ArrayAgg),
599 ("_json_agg", AggregateFunction::JsonAgg),
600 ("_jsonb_agg", AggregateFunction::JsonbAgg),
601 ("_string_agg", AggregateFunction::StringAgg),
602 ] {
603 let expected_name = format!("{}{}", dimension_path, func.0);
604 if agg_name == expected_name {
605 return Ok(AggregateSelection::MeasureAggregate {
607 measure: dimension_path,
608 function: func.1,
609 alias: agg_name.to_string(),
610 });
611 }
612 }
613 }
614
615 Err(FraiseQLError::Validation {
616 message: format!("Unknown aggregate selection: {agg_name}"),
617 path: None,
618 })
619 }
620
621 fn extract_dimension_paths(metadata: &FactTableMetadata) -> Vec<String> {
623 let mut paths = Vec::new();
624
625 for dim_path in &metadata.dimensions.paths {
627 paths.push(dim_path.name.clone());
628 }
629
630 for filter in &metadata.denormalized_filters {
632 paths.push(filter.name.clone());
633 }
634
635 paths
636 }
637
638 fn parse_having(
640 having_obj: &Value,
641 aggregates: &[AggregateSelection],
642 _metadata: &FactTableMetadata,
643 ) -> Result<Vec<HavingCondition>> {
644 let mut conditions = Vec::new();
645
646 if let Some(obj) = having_obj.as_object() {
647 for (key, value) in obj {
648 if let Some((agg_name, operator)) = Self::parse_having_key(key) {
650 let aggregate = aggregates
652 .iter()
653 .find(|a| a.alias() == agg_name)
654 .ok_or_else(|| FraiseQLError::Validation {
655 message: format!(
656 "HAVING condition references non-selected aggregate: {agg_name}"
657 ),
658 path: None,
659 })?
660 .clone();
661
662 conditions.push(HavingCondition {
663 aggregate,
664 operator,
665 value: value.clone(),
666 });
667 }
668 }
669 }
670
671 Ok(conditions)
672 }
673
674 fn parse_having_key(key: &str) -> Option<(&str, HavingOperator)> {
676 for (suffix, op) in &[
677 ("_gt", HavingOperator::Gt),
678 ("_gte", HavingOperator::Gte),
679 ("_lt", HavingOperator::Lt),
680 ("_lte", HavingOperator::Lte),
681 ("_eq", HavingOperator::Eq),
682 ("_neq", HavingOperator::Neq),
683 ] {
684 if let Some(agg_name) = key.strip_suffix(suffix) {
685 return Some((agg_name, *op));
686 }
687 }
688 None
689 }
690
691 fn parse_order_by(order_obj: &Value) -> Result<Vec<OrderByClause>> {
693 let mut clauses = Vec::new();
694
695 if let Some(obj) = order_obj.as_object() {
696 for (field, value) in obj {
697 #[allow(clippy::match_same_arms)]
698 let direction = match value.as_str() {
701 Some("ASC" | "asc") => OrderDirection::Asc,
702 Some("DESC" | "desc") => OrderDirection::Desc,
703 _ => OrderDirection::Asc, };
705
706 clauses.push(OrderByClause::new(field.clone(), direction));
707 }
708 }
709
710 Ok(clauses)
711 }
712}