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
151 .push(AggregateField::CountDistinct(column.into()));
152 self
153 }
154
155 pub fn sum(mut self, column: impl Into<String>) -> Self {
157 self.fields.push(AggregateField::Sum(column.into()));
158 self
159 }
160
161 pub fn avg(mut self, column: impl Into<String>) -> Self {
163 self.fields.push(AggregateField::Avg(column.into()));
164 self
165 }
166
167 pub fn min(mut self, column: impl Into<String>) -> Self {
169 self.fields.push(AggregateField::Min(column.into()));
170 self
171 }
172
173 pub fn max(mut self, column: impl Into<String>) -> Self {
175 self.fields.push(AggregateField::Max(column.into()));
176 self
177 }
178
179 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
181 self.filter = Some(filter.into());
182 self
183 }
184
185 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
187 let mut params = Vec::new();
188
189 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 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 pub async fn exec(self, _engine: &E) -> QueryResult<AggregateResult> {
219 let (_sql, _params) = self.build_sql();
220 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#[derive(Debug)]
234pub struct GroupByOperation<M: Model, E: QueryEngine> {
235 _model: PhantomData<M>,
237 _engine: PhantomData<E>,
239 group_columns: Vec<String>,
241 agg_fields: Vec<AggregateField>,
243 filter: Option<Filter>,
245 having: Option<HavingCondition>,
247 order_by: Vec<OrderByField>,
249 skip: Option<usize>,
251 take: Option<usize>,
253}
254
255#[derive(Debug, Clone)]
257pub struct HavingCondition {
258 pub field: AggregateField,
260 pub op: HavingOp,
262 pub value: f64,
264}
265
266#[derive(Debug, Clone, Copy)]
268pub enum HavingOp {
269 Gt,
271 Gte,
273 Lt,
275 Lte,
277 Eq,
279 Ne,
281}
282
283impl HavingOp {
284 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 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 pub fn count(mut self) -> Self {
315 self.agg_fields.push(AggregateField::CountAll);
316 self
317 }
318
319 pub fn sum(mut self, column: impl Into<String>) -> Self {
321 self.agg_fields.push(AggregateField::Sum(column.into()));
322 self
323 }
324
325 pub fn avg(mut self, column: impl Into<String>) -> Self {
327 self.agg_fields.push(AggregateField::Avg(column.into()));
328 self
329 }
330
331 pub fn min(mut self, column: impl Into<String>) -> Self {
333 self.agg_fields.push(AggregateField::Min(column.into()));
334 self
335 }
336
337 pub fn max(mut self, column: impl Into<String>) -> Self {
339 self.agg_fields.push(AggregateField::Max(column.into()));
340 self
341 }
342
343 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
345 self.filter = Some(filter.into());
346 self
347 }
348
349 pub fn having(mut self, condition: HavingCondition) -> Self {
351 self.having = Some(condition);
352 self
353 }
354
355 pub fn order_by(mut self, order: impl Into<OrderByField>) -> Self {
357 self.order_by.push(order.into());
358 self
359 }
360
361 pub fn skip(mut self, count: usize) -> Self {
363 self.skip = Some(count);
364 self
365 }
366
367 pub fn take(mut self, count: usize) -> Self {
369 self.take = Some(count);
370 self
371 }
372
373 pub fn build_sql(&self) -> (String, Vec<crate::filter::FilterValue>) {
375 let mut params = Vec::new();
376
377 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 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 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 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 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 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 pub async fn exec(self, _engine: &E) -> QueryResult<Vec<GroupByResult>> {
455 let (_sql, _params) = self.build_sql();
456 Ok(Vec::new())
458 }
459}
460
461#[derive(Debug, Clone)]
463pub struct GroupByResult {
464 pub group_values: std::collections::HashMap<String, serde_json::Value>,
466 pub aggregates: AggregateResult,
468}
469
470pub mod having {
472 use super::*;
473
474 pub fn count_gt(value: f64) -> HavingCondition {
476 HavingCondition {
477 field: AggregateField::CountAll,
478 op: HavingOp::Gt,
479 value,
480 }
481 }
482
483 pub fn count_gte(value: f64) -> HavingCondition {
485 HavingCondition {
486 field: AggregateField::CountAll,
487 op: HavingOp::Gte,
488 value,
489 }
490 }
491
492 pub fn count_lt(value: f64) -> HavingCondition {
494 HavingCondition {
495 field: AggregateField::CountAll,
496 op: HavingOp::Lt,
497 value,
498 }
499 }
500
501 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 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 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 #[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 #[test]
609 fn test_aggregate_field_sql() {
610 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 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 #[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 #[test]
690 fn test_aggregate_operation_new() {
691 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new();
692 let (sql, params) = op.build_sql();
693
694 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")); 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 #[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")); 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")); 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 #[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 #[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 #[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 #[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 #[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 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 assert!(!sql.contains("GROUP BY"));
1144 }
1145}