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