prax_query/operations/
aggregate.rs

1//! Aggregation query operations.
2//!
3//! This module provides aggregate operations like `count`, `sum`, `avg`, `min`, `max`,
4//! and `groupBy` for performing statistical queries on the database.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! // Count users
10//! let count = client
11//!     .user()
12//!     .aggregate()
13//!     .count()
14//!     .r#where(user::active::equals(true))
15//!     .exec()
16//!     .await?;
17//!
18//! // Get aggregated statistics
19//! let stats = client
20//!     .user()
21//!     .aggregate()
22//!     .count()
23//!     .avg(user::age())
24//!     .min(user::age())
25//!     .max(user::age())
26//!     .sum(user::age())
27//!     .r#where(user::active::equals(true))
28//!     .exec()
29//!     .await?;
30//!
31//! // Group by with aggregation
32//! let by_country = client
33//!     .user()
34//!     .group_by(user::country())
35//!     .count()
36//!     .avg(user::age())
37//!     .having(aggregate::count::gt(10))
38//!     .exec()
39//!     .await?;
40//! ```
41
42use std::marker::PhantomData;
43
44use crate::error::QueryResult;
45use crate::filter::Filter;
46use crate::sql::quote_identifier;
47use crate::traits::{Model, QueryEngine};
48use crate::types::OrderByField;
49
50/// An aggregation field specifier.
51#[derive(Debug, Clone)]
52pub enum AggregateField {
53    /// Count all rows.
54    CountAll,
55    /// Count non-null values in a column.
56    CountColumn(String),
57    /// Count distinct values in a column.
58    CountDistinct(String),
59    /// Sum of a numeric column.
60    Sum(String),
61    /// Average of a numeric column.
62    Avg(String),
63    /// Minimum value in a column.
64    Min(String),
65    /// Maximum value in a column.
66    Max(String),
67}
68
69impl AggregateField {
70    /// Build the SQL expression for this aggregate.
71    pub fn to_sql(&self) -> String {
72        match self {
73            Self::CountAll => "COUNT(*)".to_string(),
74            Self::CountColumn(col) => format!("COUNT({})", quote_identifier(col)),
75            Self::CountDistinct(col) => format!("COUNT(DISTINCT {})", quote_identifier(col)),
76            Self::Sum(col) => format!("SUM({})", quote_identifier(col)),
77            Self::Avg(col) => format!("AVG({})", quote_identifier(col)),
78            Self::Min(col) => format!("MIN({})", quote_identifier(col)),
79            Self::Max(col) => format!("MAX({})", quote_identifier(col)),
80        }
81    }
82
83    /// Get the alias for this aggregate.
84    pub fn alias(&self) -> String {
85        match self {
86            Self::CountAll => "_count".to_string(),
87            Self::CountColumn(col) => format!("_count_{}", col),
88            Self::CountDistinct(col) => format!("_count_distinct_{}", col),
89            Self::Sum(col) => format!("_sum_{}", col),
90            Self::Avg(col) => format!("_avg_{}", col),
91            Self::Min(col) => format!("_min_{}", col),
92            Self::Max(col) => format!("_max_{}", col),
93        }
94    }
95}
96
97/// Result of an aggregation query.
98#[derive(Debug, Clone, Default)]
99pub struct AggregateResult {
100    /// Total count (if requested).
101    pub count: Option<i64>,
102    /// Sum results keyed by column name.
103    pub sum: std::collections::HashMap<String, f64>,
104    /// Average results keyed by column name.
105    pub avg: std::collections::HashMap<String, f64>,
106    /// Minimum results keyed by column name.
107    pub min: std::collections::HashMap<String, serde_json::Value>,
108    /// Maximum results keyed by column name.
109    pub max: std::collections::HashMap<String, serde_json::Value>,
110}
111
112/// Aggregate operation builder.
113#[derive(Debug)]
114pub struct AggregateOperation<M: Model, E: QueryEngine> {
115    /// Phantom data for model type.
116    _model: PhantomData<M>,
117    /// Phantom data for engine type.
118    _engine: PhantomData<E>,
119    /// Aggregate fields to compute.
120    fields: Vec<AggregateField>,
121    /// Filter conditions.
122    filter: Option<Filter>,
123}
124
125impl<M: Model, E: QueryEngine> AggregateOperation<M, E> {
126    /// Create a new aggregate operation.
127    pub fn new() -> Self {
128        Self {
129            _model: PhantomData,
130            _engine: PhantomData,
131            fields: Vec::new(),
132            filter: None,
133        }
134    }
135
136    /// Add a count of all rows.
137    pub fn count(mut self) -> Self {
138        self.fields.push(AggregateField::CountAll);
139        self
140    }
141
142    /// Add a count of non-null values in a column.
143    pub fn count_column(mut self, column: impl Into<String>) -> Self {
144        self.fields.push(AggregateField::CountColumn(column.into()));
145        self
146    }
147
148    /// Add a count of distinct values in a column.
149    pub fn count_distinct(mut self, column: impl Into<String>) -> Self {
150        self.fields.push(AggregateField::CountDistinct(column.into()));
151        self
152    }
153
154    /// Add sum of a numeric column.
155    pub fn sum(mut self, column: impl Into<String>) -> Self {
156        self.fields.push(AggregateField::Sum(column.into()));
157        self
158    }
159
160    /// Add average of a numeric column.
161    pub fn avg(mut self, column: impl Into<String>) -> Self {
162        self.fields.push(AggregateField::Avg(column.into()));
163        self
164    }
165
166    /// Add minimum of a column.
167    pub fn min(mut self, column: impl Into<String>) -> Self {
168        self.fields.push(AggregateField::Min(column.into()));
169        self
170    }
171
172    /// Add maximum of a column.
173    pub fn max(mut self, column: impl Into<String>) -> Self {
174        self.fields.push(AggregateField::Max(column.into()));
175        self
176    }
177
178    /// Add a filter condition.
179    pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
180        self.filter = Some(filter.into());
181        self
182    }
183
184    /// Build the SQL for this operation.
185    pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
186        let mut params = Vec::new();
187
188        // If no fields specified, default to count
189        let fields = if self.fields.is_empty() {
190            vec![AggregateField::CountAll]
191        } else {
192            self.fields.clone()
193        };
194
195        let select_parts: Vec<String> = fields
196            .iter()
197            .map(|f| format!("{} AS {}", f.to_sql(), quote_identifier(&f.alias())))
198            .collect();
199
200        let mut sql = format!(
201            "SELECT {} FROM {}",
202            select_parts.join(", "),
203            quote_identifier(M::TABLE_NAME)
204        );
205
206        // Add WHERE clause
207        if let Some(filter) = &self.filter {
208            let (where_sql, where_params) = filter.to_sql(params.len() + 1);
209            sql.push_str(&format!(" WHERE {}", where_sql));
210            params.extend(where_params);
211        }
212
213        (sql, params)
214    }
215
216    /// Execute the aggregate operation.
217    pub async fn exec(self, _engine: &E) -> QueryResult<AggregateResult> {
218        let (_sql, _params) = self.build_sql();
219        // In a real implementation, this would execute the query
220        // For now, return a placeholder
221        Ok(AggregateResult::default())
222    }
223}
224
225impl<M: Model, E: QueryEngine> Default for AggregateOperation<M, E> {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231/// Group by operation builder.
232#[derive(Debug)]
233pub struct GroupByOperation<M: Model, E: QueryEngine> {
234    /// Phantom data for model type.
235    _model: PhantomData<M>,
236    /// Phantom data for engine type.
237    _engine: PhantomData<E>,
238    /// Columns to group by.
239    group_columns: Vec<String>,
240    /// Aggregate fields to compute.
241    agg_fields: Vec<AggregateField>,
242    /// Filter conditions (WHERE).
243    filter: Option<Filter>,
244    /// Having conditions.
245    having: Option<HavingCondition>,
246    /// Order by clauses.
247    order_by: Vec<OrderByField>,
248    /// Skip count.
249    skip: Option<usize>,
250    /// Take count.
251    take: Option<usize>,
252}
253
254/// A condition for the HAVING clause.
255#[derive(Debug, Clone)]
256pub struct HavingCondition {
257    /// The aggregate field to check.
258    pub field: AggregateField,
259    /// The comparison operator.
260    pub op: HavingOp,
261    /// The value to compare against.
262    pub value: f64,
263}
264
265/// Operators for HAVING conditions.
266#[derive(Debug, Clone, Copy)]
267pub enum HavingOp {
268    /// Greater than.
269    Gt,
270    /// Greater than or equal.
271    Gte,
272    /// Less than.
273    Lt,
274    /// Less than or equal.
275    Lte,
276    /// Equal.
277    Eq,
278    /// Not equal.
279    Ne,
280}
281
282impl HavingOp {
283    /// Get the SQL operator string.
284    pub fn as_str(&self) -> &'static str {
285        match self {
286            Self::Gt => ">",
287            Self::Gte => ">=",
288            Self::Lt => "<",
289            Self::Lte => "<=",
290            Self::Eq => "=",
291            Self::Ne => "<>",
292        }
293    }
294}
295
296impl<M: Model, E: QueryEngine> GroupByOperation<M, E> {
297    /// Create a new group by operation.
298    pub fn new(columns: Vec<String>) -> Self {
299        Self {
300            _model: PhantomData,
301            _engine: PhantomData,
302            group_columns: columns,
303            agg_fields: Vec::new(),
304            filter: None,
305            having: None,
306            order_by: Vec::new(),
307            skip: None,
308            take: None,
309        }
310    }
311
312    /// Add a count aggregate.
313    pub fn count(mut self) -> Self {
314        self.agg_fields.push(AggregateField::CountAll);
315        self
316    }
317
318    /// Add sum of a column.
319    pub fn sum(mut self, column: impl Into<String>) -> Self {
320        self.agg_fields.push(AggregateField::Sum(column.into()));
321        self
322    }
323
324    /// Add average of a column.
325    pub fn avg(mut self, column: impl Into<String>) -> Self {
326        self.agg_fields.push(AggregateField::Avg(column.into()));
327        self
328    }
329
330    /// Add minimum of a column.
331    pub fn min(mut self, column: impl Into<String>) -> Self {
332        self.agg_fields.push(AggregateField::Min(column.into()));
333        self
334    }
335
336    /// Add maximum of a column.
337    pub fn max(mut self, column: impl Into<String>) -> Self {
338        self.agg_fields.push(AggregateField::Max(column.into()));
339        self
340    }
341
342    /// Add a filter condition.
343    pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
344        self.filter = Some(filter.into());
345        self
346    }
347
348    /// Add a having condition.
349    pub fn having(mut self, condition: HavingCondition) -> Self {
350        self.having = Some(condition);
351        self
352    }
353
354    /// Add ordering.
355    pub fn order_by(mut self, order: impl Into<OrderByField>) -> Self {
356        self.order_by.push(order.into());
357        self
358    }
359
360    /// Set skip count.
361    pub fn skip(mut self, count: usize) -> Self {
362        self.skip = Some(count);
363        self
364    }
365
366    /// Set take count.
367    pub fn take(mut self, count: usize) -> Self {
368        self.take = Some(count);
369        self
370    }
371
372    /// Build the SQL for this operation.
373    pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
374        let mut params = Vec::new();
375
376        // Build SELECT clause
377        let mut select_parts: Vec<String> = self
378            .group_columns
379            .iter()
380            .map(|c| quote_identifier(c))
381            .collect();
382
383        for field in &self.agg_fields {
384            select_parts.push(format!("{} AS {}", field.to_sql(), quote_identifier(&field.alias())));
385        }
386
387        let mut sql = format!(
388            "SELECT {} FROM {}",
389            select_parts.join(", "),
390            quote_identifier(M::TABLE_NAME)
391        );
392
393        // Add WHERE clause
394        if let Some(filter) = &self.filter {
395            let (where_sql, where_params) = filter.to_sql(params.len() + 1);
396            sql.push_str(&format!(" WHERE {}", where_sql));
397            params.extend(where_params);
398        }
399
400        // Add GROUP BY clause
401        if !self.group_columns.is_empty() {
402            let group_cols: Vec<String> = self.group_columns.iter().map(|c| quote_identifier(c)).collect();
403            sql.push_str(&format!(" GROUP BY {}", group_cols.join(", ")));
404        }
405
406        // Add HAVING clause
407        if let Some(having) = &self.having {
408            sql.push_str(&format!(
409                " HAVING {} {} {}",
410                having.field.to_sql(),
411                having.op.as_str(),
412                having.value
413            ));
414        }
415
416        // Add ORDER BY clause
417        if !self.order_by.is_empty() {
418            let order_parts: Vec<String> = self
419                .order_by
420                .iter()
421                .map(|o| {
422                    let mut part = format!("{} {}", quote_identifier(&o.column), o.order.as_sql());
423                    if let Some(nulls) = o.nulls {
424                        part.push(' ');
425                        part.push_str(nulls.as_sql());
426                    }
427                    part
428                })
429                .collect();
430            sql.push_str(&format!(" ORDER BY {}", order_parts.join(", ")));
431        }
432
433        // Add LIMIT/OFFSET
434        if let Some(take) = self.take {
435            sql.push_str(&format!(" LIMIT {}", take));
436        }
437        if let Some(skip) = self.skip {
438            sql.push_str(&format!(" OFFSET {}", skip));
439        }
440
441        (sql, params)
442    }
443
444    /// Execute the group by operation.
445    pub async fn exec(self, _engine: &E) -> QueryResult<Vec<GroupByResult>> {
446        let (_sql, _params) = self.build_sql();
447        // In a real implementation, this would execute the query
448        Ok(Vec::new())
449    }
450}
451
452/// Result of a group by query.
453#[derive(Debug, Clone)]
454pub struct GroupByResult {
455    /// The grouped column values.
456    pub group_values: std::collections::HashMap<String, serde_json::Value>,
457    /// The aggregate results.
458    pub aggregates: AggregateResult,
459}
460
461/// Helper for creating having conditions.
462pub mod having {
463    use super::*;
464
465    /// Create a having condition for count > value.
466    pub fn count_gt(value: f64) -> HavingCondition {
467        HavingCondition {
468            field: AggregateField::CountAll,
469            op: HavingOp::Gt,
470            value,
471        }
472    }
473
474    /// Create a having condition for count >= value.
475    pub fn count_gte(value: f64) -> HavingCondition {
476        HavingCondition {
477            field: AggregateField::CountAll,
478            op: HavingOp::Gte,
479            value,
480        }
481    }
482
483    /// Create a having condition for count < value.
484    pub fn count_lt(value: f64) -> HavingCondition {
485        HavingCondition {
486            field: AggregateField::CountAll,
487            op: HavingOp::Lt,
488            value,
489        }
490    }
491
492    /// Create a having condition for sum > value.
493    pub fn sum_gt(column: impl Into<String>, value: f64) -> HavingCondition {
494        HavingCondition {
495            field: AggregateField::Sum(column.into()),
496            op: HavingOp::Gt,
497            value,
498        }
499    }
500
501    /// Create a having condition for avg > value.
502    pub fn avg_gt(column: impl Into<String>, value: f64) -> HavingCondition {
503        HavingCondition {
504            field: AggregateField::Avg(column.into()),
505            op: HavingOp::Gt,
506            value,
507        }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514    use crate::filter::{Filter, FilterValue};
515    use crate::types::NullsOrder;
516
517    // A simple test model
518    struct TestModel;
519
520    impl Model for TestModel {
521        const MODEL_NAME: &'static str = "TestModel";
522        const TABLE_NAME: &'static str = "test_models";
523        const PRIMARY_KEY: &'static [&'static str] = &["id"];
524        const COLUMNS: &'static [&'static str] = &["id", "name", "age", "score"];
525    }
526
527    // A mock query engine
528    #[derive(Clone)]
529    struct MockEngine;
530
531    impl QueryEngine for MockEngine {
532        fn query_many<T: Model + Send + 'static>(
533            &self,
534            _sql: &str,
535            _params: Vec<crate::filter::FilterValue>,
536        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
537            Box::pin(async { Ok(Vec::new()) })
538        }
539
540        fn query_one<T: Model + Send + 'static>(
541            &self,
542            _sql: &str,
543            _params: Vec<crate::filter::FilterValue>,
544        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
545            Box::pin(async { Err(crate::error::QueryError::not_found("Not implemented")) })
546        }
547
548        fn query_optional<T: Model + Send + 'static>(
549            &self,
550            _sql: &str,
551            _params: Vec<crate::filter::FilterValue>,
552        ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
553            Box::pin(async { Ok(None) })
554        }
555
556        fn execute_insert<T: Model + Send + 'static>(
557            &self,
558            _sql: &str,
559            _params: Vec<crate::filter::FilterValue>,
560        ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
561            Box::pin(async { Err(crate::error::QueryError::not_found("Not implemented")) })
562        }
563
564        fn execute_update<T: Model + Send + 'static>(
565            &self,
566            _sql: &str,
567            _params: Vec<crate::filter::FilterValue>,
568        ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
569            Box::pin(async { Ok(Vec::new()) })
570        }
571
572        fn execute_delete(
573            &self,
574            _sql: &str,
575            _params: Vec<crate::filter::FilterValue>,
576        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
577            Box::pin(async { Ok(0) })
578        }
579
580        fn execute_raw(
581            &self,
582            _sql: &str,
583            _params: Vec<crate::filter::FilterValue>,
584        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
585            Box::pin(async { Ok(0) })
586        }
587
588        fn count(
589            &self,
590            _sql: &str,
591            _params: Vec<crate::filter::FilterValue>,
592        ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
593            Box::pin(async { Ok(0) })
594        }
595    }
596
597    // ========== AggregateField Tests ==========
598
599    #[test]
600    fn test_aggregate_field_sql() {
601        // Note: quote_identifier only quotes when needed (reserved words, special chars)
602        assert_eq!(AggregateField::CountAll.to_sql(), "COUNT(*)");
603        assert_eq!(
604            AggregateField::CountColumn("id".into()).to_sql(),
605            "COUNT(id)"
606        );
607        assert_eq!(
608            AggregateField::CountDistinct("email".into()).to_sql(),
609            "COUNT(DISTINCT email)"
610        );
611        assert_eq!(
612            AggregateField::Sum("amount".into()).to_sql(),
613            "SUM(amount)"
614        );
615        assert_eq!(
616            AggregateField::Avg("score".to_string()).to_sql(),
617            "AVG(score)"
618        );
619        assert_eq!(
620            AggregateField::Min("age".into()).to_sql(),
621            "MIN(age)"
622        );
623        assert_eq!(
624            AggregateField::Max("age".into()).to_sql(),
625            "MAX(age)"
626        );
627        // Test with reserved word - should be quoted
628        assert_eq!(
629            AggregateField::CountColumn("user".to_string()).to_sql(),
630            "COUNT(\"user\")"
631        );
632    }
633
634    #[test]
635    fn test_aggregate_field_alias() {
636        assert_eq!(AggregateField::CountAll.alias(), "_count");
637        assert_eq!(AggregateField::CountColumn("id".into()).alias(), "_count_id");
638        assert_eq!(AggregateField::CountDistinct("email".into()).alias(), "_count_distinct_email");
639        assert_eq!(AggregateField::Sum("amount".into()).alias(), "_sum_amount");
640        assert_eq!(AggregateField::Avg("score".to_string()).alias(), "_avg_score");
641        assert_eq!(AggregateField::Min("age".into()).alias(), "_min_age");
642        assert_eq!(AggregateField::Max("salary".to_string()).alias(), "_max_salary");
643    }
644
645    // ========== AggregateResult Tests ==========
646
647    #[test]
648    fn test_aggregate_result_default() {
649        let result = AggregateResult::default();
650        assert!(result.count.is_none());
651        assert!(result.sum.is_empty());
652        assert!(result.avg.is_empty());
653        assert!(result.min.is_empty());
654        assert!(result.max.is_empty());
655    }
656
657    #[test]
658    fn test_aggregate_result_debug() {
659        let result = AggregateResult::default();
660        let debug_str = format!("{:?}", result);
661        assert!(debug_str.contains("AggregateResult"));
662    }
663
664    #[test]
665    fn test_aggregate_result_clone() {
666        let mut result = AggregateResult::default();
667        result.count = Some(42);
668        result.sum.insert("amount".into(), 1000.0);
669
670        let cloned = result.clone();
671        assert_eq!(cloned.count, Some(42));
672        assert_eq!(cloned.sum.get("amount"), Some(&1000.0));
673    }
674
675    // ========== AggregateOperation Tests ==========
676
677    #[test]
678    fn test_aggregate_operation_new() {
679        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new();
680        let (sql, params) = op.build_sql();
681
682        // Default should be count all
683        assert!(sql.contains("COUNT(*)"));
684        assert!(params.is_empty());
685    }
686
687    #[test]
688    fn test_aggregate_operation_default() {
689        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::default();
690        let (sql, params) = op.build_sql();
691
692        assert!(sql.contains("COUNT(*)"));
693        assert!(params.is_empty());
694    }
695
696    #[test]
697    fn test_aggregate_operation_build_sql() {
698        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
699            .count()
700            .sum("score")
701            .avg("age");
702
703        let (sql, params) = op.build_sql();
704
705        assert!(sql.contains("SELECT"));
706        assert!(sql.contains("COUNT(*)"));
707        assert!(sql.contains("SUM(score)"));
708        assert!(sql.contains("AVG(age)"));
709        assert!(sql.contains("FROM test_models"));
710        assert!(params.is_empty());
711    }
712
713    #[test]
714    fn test_aggregate_operation_count_column() {
715        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
716            .count_column("email");
717
718        let (sql, _) = op.build_sql();
719
720        assert!(sql.contains("COUNT(email)"));
721    }
722
723    #[test]
724    fn test_aggregate_operation_count_distinct() {
725        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
726            .count_distinct("email");
727
728        let (sql, _) = op.build_sql();
729
730        assert!(sql.contains("COUNT(DISTINCT email)"));
731    }
732
733    #[test]
734    fn test_aggregate_operation_min_max() {
735        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
736            .min("age")
737            .max("age");
738
739        let (sql, _) = op.build_sql();
740
741        assert!(sql.contains("MIN(age)"));
742        assert!(sql.contains("MAX(age)"));
743    }
744
745    #[test]
746    fn test_aggregate_with_where() {
747        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
748            .count()
749            .r#where(Filter::Gt(
750                "age".into(),
751                FilterValue::Int(18),
752            ));
753
754        let (sql, params) = op.build_sql();
755
756        assert!(sql.contains("WHERE"));
757        assert!(sql.contains("age")); // Not quoted since "age" is not a reserved word
758        assert!(sql.contains(">"));
759        assert!(!params.is_empty());
760    }
761
762    #[test]
763    fn test_aggregate_with_complex_filter() {
764        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
765            .sum("score")
766            .avg("age")
767            .r#where(Filter::and([
768                Filter::Gte("age".into(), FilterValue::Int(18)),
769                Filter::Equals("active".into(), FilterValue::Bool(true)),
770            ]));
771
772        let (sql, params) = op.build_sql();
773
774        assert!(sql.contains("WHERE"));
775        assert!(sql.contains("AND"));
776        assert_eq!(params.len(), 2);
777    }
778
779    #[test]
780    fn test_aggregate_all_methods() {
781        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
782            .count()
783            .count_column("name")
784            .count_distinct("email")
785            .sum("score")
786            .avg("score")
787            .min("age")
788            .max("age");
789
790        let (sql, _) = op.build_sql();
791
792        assert!(sql.contains("COUNT(*)"));
793        assert!(sql.contains("COUNT(name)"));
794        assert!(sql.contains("COUNT(DISTINCT email)"));
795        assert!(sql.contains("SUM(score)"));
796        assert!(sql.contains("AVG(score)"));
797        assert!(sql.contains("MIN(age)"));
798        assert!(sql.contains("MAX(age)"));
799    }
800
801    #[tokio::test]
802    async fn test_aggregate_exec() {
803        let engine = MockEngine;
804        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
805            .count();
806
807        let result = op.exec(&engine).await;
808        assert!(result.is_ok());
809    }
810
811    // ========== GroupByOperation Tests ==========
812
813    #[test]
814    fn test_group_by_new() {
815        let op: GroupByOperation<TestModel, MockEngine> =
816            GroupByOperation::new(vec!["department".into()]);
817
818        let (sql, _) = op.build_sql();
819
820        assert!(sql.contains("GROUP BY department"));
821    }
822
823    #[test]
824    fn test_group_by_build_sql() {
825        let op: GroupByOperation<TestModel, MockEngine> =
826            GroupByOperation::new(vec!["name".to_string()])
827                .count()
828                .avg("score");
829
830        let (sql, params) = op.build_sql();
831
832        assert!(sql.contains("SELECT"));
833        assert!(sql.contains("name")); // Not quoted since "name" is not a reserved word
834        assert!(sql.contains("COUNT(*)"));
835        assert!(sql.contains("AVG(score)"));
836        assert!(sql.contains("GROUP BY name"));
837        assert!(params.is_empty());
838    }
839
840    #[test]
841    fn test_group_by_multiple_columns() {
842        let op: GroupByOperation<TestModel, MockEngine> =
843            GroupByOperation::new(vec!["department".into(), "role".into()])
844                .count();
845
846        let (sql, _) = op.build_sql();
847
848        assert!(sql.contains("GROUP BY department, role"));
849    }
850
851    #[test]
852    fn test_group_by_with_sum() {
853        let op: GroupByOperation<TestModel, MockEngine> =
854            GroupByOperation::new(vec!["category".into()])
855                .sum("amount");
856
857        let (sql, _) = op.build_sql();
858
859        assert!(sql.contains("SUM(amount)"));
860    }
861
862    #[test]
863    fn test_group_by_with_min_max() {
864        let op: GroupByOperation<TestModel, MockEngine> =
865            GroupByOperation::new(vec!["category".into()])
866                .min("price")
867                .max("price");
868
869        let (sql, _) = op.build_sql();
870
871        assert!(sql.contains("MIN(price)"));
872        assert!(sql.contains("MAX(price)"));
873    }
874
875    #[test]
876    fn test_group_by_with_where() {
877        let op: GroupByOperation<TestModel, MockEngine> =
878            GroupByOperation::new(vec!["department".into()])
879                .count()
880                .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)));
881
882        let (sql, params) = op.build_sql();
883
884        assert!(sql.contains("WHERE"));
885        assert!(sql.contains("GROUP BY"));
886        assert_eq!(params.len(), 1);
887    }
888
889    #[test]
890    fn test_group_by_with_having() {
891        let op: GroupByOperation<TestModel, MockEngine> =
892            GroupByOperation::new(vec!["name".to_string()])
893                .count()
894                .having(having::count_gt(5.0));
895
896        let (sql, _params) = op.build_sql();
897
898        assert!(sql.contains("HAVING COUNT(*) > 5"));
899    }
900
901    #[test]
902    fn test_group_by_with_order_and_limit() {
903        let op: GroupByOperation<TestModel, MockEngine> =
904            GroupByOperation::new(vec!["name".to_string()])
905                .count()
906                .order_by(OrderByField::desc("_count"))
907                .take(10)
908                .skip(5);
909
910        let (sql, _params) = op.build_sql();
911
912        assert!(sql.contains("ORDER BY _count DESC")); // Not quoted since "_count" is not a reserved word
913        assert!(sql.contains("LIMIT 10"));
914        assert!(sql.contains("OFFSET 5"));
915    }
916
917    #[test]
918    fn test_group_by_order_with_nulls() {
919        let op: GroupByOperation<TestModel, MockEngine> =
920            GroupByOperation::new(vec!["department".into()])
921                .count()
922                .order_by(OrderByField::asc("name").nulls(NullsOrder::First));
923
924        let (sql, _) = op.build_sql();
925
926        assert!(sql.contains("ORDER BY"));
927        assert!(sql.contains("NULLS FIRST"));
928    }
929
930    #[test]
931    fn test_group_by_skip_only() {
932        let op: GroupByOperation<TestModel, MockEngine> =
933            GroupByOperation::new(vec!["department".into()])
934                .count()
935                .skip(20);
936
937        let (sql, _) = op.build_sql();
938
939        assert!(sql.contains("OFFSET 20"));
940        assert!(!sql.contains("LIMIT"));
941    }
942
943    #[test]
944    fn test_group_by_take_only() {
945        let op: GroupByOperation<TestModel, MockEngine> =
946            GroupByOperation::new(vec!["department".into()])
947                .count()
948                .take(50);
949
950        let (sql, _) = op.build_sql();
951
952        assert!(sql.contains("LIMIT 50"));
953        assert!(!sql.contains("OFFSET"));
954    }
955
956    #[tokio::test]
957    async fn test_group_by_exec() {
958        let engine = MockEngine;
959        let op: GroupByOperation<TestModel, MockEngine> =
960            GroupByOperation::new(vec!["department".into()])
961                .count();
962
963        let result = op.exec(&engine).await;
964        assert!(result.is_ok());
965        assert!(result.unwrap().is_empty());
966    }
967
968    // ========== HavingOp Tests ==========
969
970    #[test]
971    fn test_having_op_as_str() {
972        assert_eq!(HavingOp::Gt.as_str(), ">");
973        assert_eq!(HavingOp::Gte.as_str(), ">=");
974        assert_eq!(HavingOp::Lt.as_str(), "<");
975        assert_eq!(HavingOp::Lte.as_str(), "<=");
976        assert_eq!(HavingOp::Eq.as_str(), "=");
977        assert_eq!(HavingOp::Ne.as_str(), "<>");
978    }
979
980    // ========== HavingCondition Tests ==========
981
982    #[test]
983    fn test_having_condition_debug() {
984        let cond = HavingCondition {
985            field: AggregateField::CountAll,
986            op: HavingOp::Gt,
987            value: 10.0,
988        };
989        let debug_str = format!("{:?}", cond);
990        assert!(debug_str.contains("HavingCondition"));
991    }
992
993    #[test]
994    fn test_having_condition_clone() {
995        let cond = HavingCondition {
996            field: AggregateField::Sum("amount".into()),
997            op: HavingOp::Gte,
998            value: 1000.0,
999        };
1000        let cloned = cond.clone();
1001        assert!((cloned.value - 1000.0).abs() < f64::EPSILON);
1002    }
1003
1004    // ========== Having Helper Tests ==========
1005
1006    #[test]
1007    fn test_having_helpers() {
1008        let cond = having::count_gt(10.0);
1009        assert!(matches!(cond.field, AggregateField::CountAll));
1010        assert!(matches!(cond.op, HavingOp::Gt));
1011        assert!((cond.value - 10.0).abs() < f64::EPSILON);
1012
1013        let cond = having::sum_gt("amount", 1000.0);
1014        if let AggregateField::Sum(col) = cond.field {
1015            assert_eq!(col, "amount");
1016        } else {
1017            panic!("Expected Sum");
1018        }
1019    }
1020
1021    #[test]
1022    fn test_having_count_gte() {
1023        let cond = having::count_gte(5.0);
1024        assert!(matches!(cond.field, AggregateField::CountAll));
1025        assert!(matches!(cond.op, HavingOp::Gte));
1026        assert!((cond.value - 5.0).abs() < f64::EPSILON);
1027    }
1028
1029    #[test]
1030    fn test_having_count_lt() {
1031        let cond = having::count_lt(100.0);
1032        assert!(matches!(cond.field, AggregateField::CountAll));
1033        assert!(matches!(cond.op, HavingOp::Lt));
1034        assert!((cond.value - 100.0).abs() < f64::EPSILON);
1035    }
1036
1037    #[test]
1038    fn test_having_avg_gt() {
1039        let cond = having::avg_gt("score", 75.5);
1040        assert!(matches!(cond.op, HavingOp::Gt));
1041        assert!((cond.value - 75.5).abs() < f64::EPSILON);
1042        if let AggregateField::Avg(col) = cond.field {
1043            assert_eq!(col, "score");
1044        } else {
1045            panic!("Expected Avg");
1046        }
1047    }
1048
1049    #[test]
1050    fn test_having_sum_gt_with_different_columns() {
1051        let cond1 = having::sum_gt("revenue", 50000.0);
1052        let cond2 = having::sum_gt("cost", 10000.0);
1053
1054        if let AggregateField::Sum(col) = &cond1.field {
1055            assert_eq!(col, "revenue");
1056        }
1057        if let AggregateField::Sum(col) = &cond2.field {
1058            assert_eq!(col, "cost");
1059        }
1060    }
1061
1062    // ========== GroupByResult Tests ==========
1063
1064    #[test]
1065    fn test_group_by_result_debug() {
1066        let result = GroupByResult {
1067            group_values: std::collections::HashMap::new(),
1068            aggregates: AggregateResult::default(),
1069        };
1070        let debug_str = format!("{:?}", result);
1071        assert!(debug_str.contains("GroupByResult"));
1072    }
1073
1074    #[test]
1075    fn test_group_by_result_clone() {
1076        let mut result = GroupByResult {
1077            group_values: std::collections::HashMap::new(),
1078            aggregates: AggregateResult::default(),
1079        };
1080        result.group_values.insert("category".into(), serde_json::json!("electronics"));
1081        result.aggregates.count = Some(50);
1082
1083        let cloned = result.clone();
1084        assert_eq!(cloned.aggregates.count, Some(50));
1085        assert!(cloned.group_values.contains_key("category"));
1086    }
1087
1088    // ========== SQL Structure Tests ==========
1089
1090    #[test]
1091    fn test_group_by_sql_structure() {
1092        let op: GroupByOperation<TestModel, MockEngine> =
1093            GroupByOperation::new(vec!["department".into()])
1094                .count()
1095                .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)))
1096                .having(having::count_gt(5.0))
1097                .order_by(OrderByField::desc("_count"))
1098                .take(10)
1099                .skip(5);
1100
1101        let (sql, _) = op.build_sql();
1102
1103        // Check SQL clause ordering: SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET
1104        let select_pos = sql.find("SELECT").unwrap();
1105        let from_pos = sql.find("FROM").unwrap();
1106        let where_pos = sql.find("WHERE").unwrap();
1107        let group_pos = sql.find("GROUP BY").unwrap();
1108        let having_pos = sql.find("HAVING").unwrap();
1109        let order_pos = sql.find("ORDER BY").unwrap();
1110        let limit_pos = sql.find("LIMIT").unwrap();
1111        let offset_pos = sql.find("OFFSET").unwrap();
1112
1113        assert!(select_pos < from_pos);
1114        assert!(from_pos < where_pos);
1115        assert!(where_pos < group_pos);
1116        assert!(group_pos < having_pos);
1117        assert!(having_pos < order_pos);
1118        assert!(order_pos < limit_pos);
1119        assert!(limit_pos < offset_pos);
1120    }
1121
1122    #[test]
1123    fn test_aggregate_no_group_by() {
1124        let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1125            .count()
1126            .sum("score");
1127
1128        let (sql, _) = op.build_sql();
1129
1130        assert!(!sql.contains("GROUP BY"));
1131    }
1132
1133    #[test]
1134    fn test_group_by_empty_columns() {
1135        let op: GroupByOperation<TestModel, MockEngine> =
1136            GroupByOperation::new(vec![])
1137                .count();
1138
1139        let (sql, _) = op.build_sql();
1140
1141        // Empty group columns should not produce GROUP BY
1142        assert!(!sql.contains("GROUP BY"));
1143    }
1144}
1145