1use 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#[derive(Debug, Clone)]
52pub enum AggregateField {
53 CountAll,
55 CountColumn(String),
57 CountDistinct(String),
59 Sum(String),
61 Avg(String),
63 Min(String),
65 Max(String),
67}
68
69impl AggregateField {
70 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 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#[derive(Debug, Clone, Default)]
99pub struct AggregateResult {
100 pub count: Option<i64>,
102 pub sum: std::collections::HashMap<String, f64>,
104 pub avg: std::collections::HashMap<String, f64>,
106 pub min: std::collections::HashMap<String, serde_json::Value>,
108 pub max: std::collections::HashMap<String, serde_json::Value>,
110}
111
112#[derive(Debug)]
114pub struct AggregateOperation<M: Model, E: QueryEngine> {
115 _model: PhantomData<M>,
117 _engine: PhantomData<E>,
119 fields: Vec<AggregateField>,
121 filter: Option<Filter>,
123}
124
125impl<M: Model, E: QueryEngine> AggregateOperation<M, E> {
126 pub fn new() -> Self {
128 Self {
129 _model: PhantomData,
130 _engine: PhantomData,
131 fields: Vec::new(),
132 filter: None,
133 }
134 }
135
136 pub fn count(mut self) -> Self {
138 self.fields.push(AggregateField::CountAll);
139 self
140 }
141
142 pub fn count_column(mut self, column: impl Into<String>) -> Self {
144 self.fields.push(AggregateField::CountColumn(column.into()));
145 self
146 }
147
148 pub fn count_distinct(mut self, column: impl Into<String>) -> Self {
150 self.fields.push(AggregateField::CountDistinct(column.into()));
151 self
152 }
153
154 pub fn sum(mut self, column: impl Into<String>) -> Self {
156 self.fields.push(AggregateField::Sum(column.into()));
157 self
158 }
159
160 pub fn avg(mut self, column: impl Into<String>) -> Self {
162 self.fields.push(AggregateField::Avg(column.into()));
163 self
164 }
165
166 pub fn min(mut self, column: impl Into<String>) -> Self {
168 self.fields.push(AggregateField::Min(column.into()));
169 self
170 }
171
172 pub fn max(mut self, column: impl Into<String>) -> Self {
174 self.fields.push(AggregateField::Max(column.into()));
175 self
176 }
177
178 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
180 self.filter = Some(filter.into());
181 self
182 }
183
184 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
186 let mut params = Vec::new();
187
188 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 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 pub async fn exec(self, _engine: &E) -> QueryResult<AggregateResult> {
218 let (_sql, _params) = self.build_sql();
219 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#[derive(Debug)]
233pub struct GroupByOperation<M: Model, E: QueryEngine> {
234 _model: PhantomData<M>,
236 _engine: PhantomData<E>,
238 group_columns: Vec<String>,
240 agg_fields: Vec<AggregateField>,
242 filter: Option<Filter>,
244 having: Option<HavingCondition>,
246 order_by: Vec<OrderByField>,
248 skip: Option<usize>,
250 take: Option<usize>,
252}
253
254#[derive(Debug, Clone)]
256pub struct HavingCondition {
257 pub field: AggregateField,
259 pub op: HavingOp,
261 pub value: f64,
263}
264
265#[derive(Debug, Clone, Copy)]
267pub enum HavingOp {
268 Gt,
270 Gte,
272 Lt,
274 Lte,
276 Eq,
278 Ne,
280}
281
282impl HavingOp {
283 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 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 pub fn count(mut self) -> Self {
314 self.agg_fields.push(AggregateField::CountAll);
315 self
316 }
317
318 pub fn sum(mut self, column: impl Into<String>) -> Self {
320 self.agg_fields.push(AggregateField::Sum(column.into()));
321 self
322 }
323
324 pub fn avg(mut self, column: impl Into<String>) -> Self {
326 self.agg_fields.push(AggregateField::Avg(column.into()));
327 self
328 }
329
330 pub fn min(mut self, column: impl Into<String>) -> Self {
332 self.agg_fields.push(AggregateField::Min(column.into()));
333 self
334 }
335
336 pub fn max(mut self, column: impl Into<String>) -> Self {
338 self.agg_fields.push(AggregateField::Max(column.into()));
339 self
340 }
341
342 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
344 self.filter = Some(filter.into());
345 self
346 }
347
348 pub fn having(mut self, condition: HavingCondition) -> Self {
350 self.having = Some(condition);
351 self
352 }
353
354 pub fn order_by(mut self, order: impl Into<OrderByField>) -> Self {
356 self.order_by.push(order.into());
357 self
358 }
359
360 pub fn skip(mut self, count: usize) -> Self {
362 self.skip = Some(count);
363 self
364 }
365
366 pub fn take(mut self, count: usize) -> Self {
368 self.take = Some(count);
369 self
370 }
371
372 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
374 let mut params = Vec::new();
375
376 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 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 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 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 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 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 pub async fn exec(self, _engine: &E) -> QueryResult<Vec<GroupByResult>> {
446 let (_sql, _params) = self.build_sql();
447 Ok(Vec::new())
449 }
450}
451
452#[derive(Debug, Clone)]
454pub struct GroupByResult {
455 pub group_values: std::collections::HashMap<String, serde_json::Value>,
457 pub aggregates: AggregateResult,
459}
460
461pub mod having {
463 use super::*;
464
465 pub fn count_gt(value: f64) -> HavingCondition {
467 HavingCondition {
468 field: AggregateField::CountAll,
469 op: HavingOp::Gt,
470 value,
471 }
472 }
473
474 pub fn count_gte(value: f64) -> HavingCondition {
476 HavingCondition {
477 field: AggregateField::CountAll,
478 op: HavingOp::Gte,
479 value,
480 }
481 }
482
483 pub fn count_lt(value: f64) -> HavingCondition {
485 HavingCondition {
486 field: AggregateField::CountAll,
487 op: HavingOp::Lt,
488 value,
489 }
490 }
491
492 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 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 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 #[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 #[test]
600 fn test_aggregate_field_sql() {
601 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 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 #[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 #[test]
678 fn test_aggregate_operation_new() {
679 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new();
680 let (sql, params) = op.build_sql();
681
682 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")); 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 #[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")); 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")); 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 #[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 #[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 #[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 #[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 #[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 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 assert!(!sql.contains("GROUP BY"));
1143 }
1144}
1145