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)]
105pub struct AggregateResult {
106 pub count: Option<i64>,
108 pub sum: std::collections::HashMap<String, f64>,
110 pub avg: std::collections::HashMap<String, f64>,
112 pub min: std::collections::HashMap<String, serde_json::Value>,
114 pub max: std::collections::HashMap<String, serde_json::Value>,
116}
117
118impl AggregateResult {
119 pub fn from_row(row: std::collections::HashMap<String, crate::filter::FilterValue>) -> Self {
130 use crate::filter::FilterValue;
131 let mut out = Self::default();
132 for (k, v) in row {
133 if k == "_count" {
134 if let FilterValue::Int(n) = v {
135 out.count = Some(n);
136 }
137 } else if let Some(col) = k.strip_prefix("_sum_") {
138 if let Some(f) = value_to_f64(&v) {
139 out.sum.insert(col.to_string(), f);
140 }
141 } else if let Some(col) = k.strip_prefix("_avg_") {
142 if let Some(f) = value_to_f64(&v) {
143 out.avg.insert(col.to_string(), f);
144 }
145 } else if let Some(col) = k.strip_prefix("_min_") {
146 out.min.insert(col.to_string(), filter_value_to_json(&v));
147 } else if let Some(col) = k.strip_prefix("_max_") {
148 out.max.insert(col.to_string(), filter_value_to_json(&v));
149 }
150 }
151 out
152 }
153
154 pub fn sum_as_f64(&self, column: &str) -> Option<f64> {
156 self.sum.get(column).copied()
157 }
158
159 pub fn avg_as_f64(&self, column: &str) -> Option<f64> {
161 self.avg.get(column).copied()
162 }
163
164 pub fn min_as_f64(&self, column: &str) -> Option<f64> {
167 self.min.get(column).and_then(|v| v.as_f64())
168 }
169
170 pub fn max_as_f64(&self, column: &str) -> Option<f64> {
173 self.max.get(column).and_then(|v| v.as_f64())
174 }
175}
176
177fn value_to_f64(v: &crate::filter::FilterValue) -> Option<f64> {
178 use crate::filter::FilterValue;
179 match v {
180 FilterValue::Int(n) => Some(*n as f64),
181 FilterValue::Float(f) => Some(*f),
182 FilterValue::String(s) => s.parse::<f64>().ok(),
183 _ => None,
184 }
185}
186
187fn filter_value_to_json(v: &crate::filter::FilterValue) -> serde_json::Value {
188 use crate::filter::FilterValue;
189 match v {
190 FilterValue::Null => serde_json::Value::Null,
191 FilterValue::Bool(b) => serde_json::Value::Bool(*b),
192 FilterValue::Int(n) => serde_json::Value::from(*n),
193 FilterValue::Float(f) => serde_json::Number::from_f64(*f)
194 .map(serde_json::Value::Number)
195 .unwrap_or(serde_json::Value::Null),
196 FilterValue::String(s) => serde_json::Value::String(s.clone()),
197 FilterValue::Json(j) => j.clone(),
198 FilterValue::List(_) => serde_json::Value::Null,
199 }
200}
201
202#[derive(Debug)]
214pub struct AggregateOperation<M: Model, E: QueryEngine> {
215 _model: PhantomData<M>,
217 engine: Option<E>,
220 fields: Vec<AggregateField>,
222 filter: Option<Filter>,
224}
225
226impl<M: Model, E: QueryEngine> AggregateOperation<M, E> {
227 pub fn new() -> Self {
232 Self {
233 _model: PhantomData,
234 engine: None,
235 fields: Vec::new(),
236 filter: None,
237 }
238 }
239
240 pub fn with_engine(engine: E) -> Self {
245 Self {
246 _model: PhantomData,
247 engine: Some(engine),
248 fields: Vec::new(),
249 filter: None,
250 }
251 }
252
253 pub fn count(mut self) -> Self {
255 self.fields.push(AggregateField::CountAll);
256 self
257 }
258
259 pub fn count_column(mut self, column: impl Into<String>) -> Self {
261 self.fields.push(AggregateField::CountColumn(column.into()));
262 self
263 }
264
265 pub fn count_distinct(mut self, column: impl Into<String>) -> Self {
267 self.fields
268 .push(AggregateField::CountDistinct(column.into()));
269 self
270 }
271
272 pub fn sum(mut self, column: impl Into<String>) -> Self {
274 self.fields.push(AggregateField::Sum(column.into()));
275 self
276 }
277
278 pub fn avg(mut self, column: impl Into<String>) -> Self {
280 self.fields.push(AggregateField::Avg(column.into()));
281 self
282 }
283
284 pub fn min(mut self, column: impl Into<String>) -> Self {
286 self.fields.push(AggregateField::Min(column.into()));
287 self
288 }
289
290 pub fn max(mut self, column: impl Into<String>) -> Self {
292 self.fields.push(AggregateField::Max(column.into()));
293 self
294 }
295
296 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
298 self.filter = Some(filter.into());
299 self
300 }
301
302 pub fn build_sql(
304 &self,
305 dialect: &dyn crate::dialect::SqlDialect,
306 ) -> (String, Vec<crate::filter::FilterValue>) {
307 let mut params = Vec::new();
308
309 let fields = if self.fields.is_empty() {
311 vec![AggregateField::CountAll]
312 } else {
313 self.fields.clone()
314 };
315
316 let select_parts: Vec<String> = fields
317 .iter()
318 .map(|f| format!("{} AS {}", f.to_sql(), quote_identifier(&f.alias())))
319 .collect();
320
321 let mut sql = format!(
322 "SELECT {} FROM {}",
323 select_parts.join(", "),
324 quote_identifier(M::TABLE_NAME)
325 );
326
327 if let Some(filter) = &self.filter {
329 let (where_sql, where_params) = filter.to_sql(params.len() + 1, dialect);
330 sql.push_str(&format!(" WHERE {}", where_sql));
331 params.extend(where_params);
332 }
333
334 (sql, params)
335 }
336
337 pub async fn exec(self) -> QueryResult<AggregateResult> {
348 let engine = self.engine.as_ref().ok_or_else(|| {
349 crate::error::QueryError::internal(
350 "AggregateOperation::exec called on a builder without an engine; \
351 use Client<E>::aggregate() (which calls with_engine) instead of \
352 AggregateOperation::new()",
353 )
354 })?;
355 let dialect = engine.dialect();
356 let (sql, params) = self.build_sql(dialect);
357 let mut rows = engine.aggregate_query(&sql, params).await?;
358 Ok(AggregateResult::from_row(rows.pop().unwrap_or_default()))
359 }
360}
361
362impl<M: Model, E: QueryEngine> Default for AggregateOperation<M, E> {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368#[derive(Debug)]
377pub struct GroupByOperation<M: Model, E: QueryEngine> {
378 _model: PhantomData<M>,
380 engine: Option<E>,
383 group_columns: Vec<String>,
385 agg_fields: Vec<AggregateField>,
387 filter: Option<Filter>,
389 having: Option<HavingCondition>,
391 order_by: Vec<OrderByField>,
393 skip: Option<usize>,
395 take: Option<usize>,
397}
398
399#[derive(Debug, Clone)]
401pub struct HavingCondition {
402 pub field: AggregateField,
404 pub op: HavingOp,
406 pub value: f64,
408}
409
410#[derive(Debug, Clone, Copy)]
412pub enum HavingOp {
413 Gt,
415 Gte,
417 Lt,
419 Lte,
421 Eq,
423 Ne,
425}
426
427impl HavingOp {
428 pub fn as_str(&self) -> &'static str {
430 match self {
431 Self::Gt => ">",
432 Self::Gte => ">=",
433 Self::Lt => "<",
434 Self::Lte => "<=",
435 Self::Eq => "=",
436 Self::Ne => "<>",
437 }
438 }
439}
440
441impl<M: Model, E: QueryEngine> GroupByOperation<M, E> {
442 pub fn new(columns: Vec<String>) -> Self {
447 Self {
448 _model: PhantomData,
449 engine: None,
450 group_columns: columns,
451 agg_fields: Vec::new(),
452 filter: None,
453 having: None,
454 order_by: Vec::new(),
455 skip: None,
456 take: None,
457 }
458 }
459
460 pub fn with_engine(engine: E, columns: Vec<String>) -> Self {
465 Self {
466 _model: PhantomData,
467 engine: Some(engine),
468 group_columns: columns,
469 agg_fields: Vec::new(),
470 filter: None,
471 having: None,
472 order_by: Vec::new(),
473 skip: None,
474 take: None,
475 }
476 }
477
478 pub fn count(mut self) -> Self {
480 self.agg_fields.push(AggregateField::CountAll);
481 self
482 }
483
484 pub fn sum(mut self, column: impl Into<String>) -> Self {
486 self.agg_fields.push(AggregateField::Sum(column.into()));
487 self
488 }
489
490 pub fn avg(mut self, column: impl Into<String>) -> Self {
492 self.agg_fields.push(AggregateField::Avg(column.into()));
493 self
494 }
495
496 pub fn min(mut self, column: impl Into<String>) -> Self {
498 self.agg_fields.push(AggregateField::Min(column.into()));
499 self
500 }
501
502 pub fn max(mut self, column: impl Into<String>) -> Self {
504 self.agg_fields.push(AggregateField::Max(column.into()));
505 self
506 }
507
508 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
510 self.filter = Some(filter.into());
511 self
512 }
513
514 pub fn having(mut self, condition: HavingCondition) -> Self {
516 self.having = Some(condition);
517 self
518 }
519
520 pub fn order_by(mut self, order: impl Into<OrderByField>) -> Self {
522 self.order_by.push(order.into());
523 self
524 }
525
526 pub fn skip(mut self, count: usize) -> Self {
528 self.skip = Some(count);
529 self
530 }
531
532 pub fn take(mut self, count: usize) -> Self {
534 self.take = Some(count);
535 self
536 }
537
538 pub fn build_sql(
540 &self,
541 dialect: &dyn crate::dialect::SqlDialect,
542 ) -> (String, Vec<crate::filter::FilterValue>) {
543 let mut params = Vec::new();
544
545 let mut select_parts: Vec<String> = self
547 .group_columns
548 .iter()
549 .map(|c| quote_identifier(c))
550 .collect();
551
552 for field in &self.agg_fields {
553 select_parts.push(format!(
554 "{} AS {}",
555 field.to_sql(),
556 quote_identifier(&field.alias())
557 ));
558 }
559
560 let mut sql = format!(
561 "SELECT {} FROM {}",
562 select_parts.join(", "),
563 quote_identifier(M::TABLE_NAME)
564 );
565
566 if let Some(filter) = &self.filter {
568 let (where_sql, where_params) = filter.to_sql(params.len() + 1, dialect);
569 sql.push_str(&format!(" WHERE {}", where_sql));
570 params.extend(where_params);
571 }
572
573 if !self.group_columns.is_empty() {
575 let group_cols: Vec<String> = self
576 .group_columns
577 .iter()
578 .map(|c| quote_identifier(c))
579 .collect();
580 sql.push_str(&format!(" GROUP BY {}", group_cols.join(", ")));
581 }
582
583 if let Some(having) = &self.having {
585 sql.push_str(&format!(
586 " HAVING {} {} {}",
587 having.field.to_sql(),
588 having.op.as_str(),
589 having.value
590 ));
591 }
592
593 if !self.order_by.is_empty() {
595 let order_parts: Vec<String> = self
596 .order_by
597 .iter()
598 .map(|o| {
599 let mut part = format!("{} {}", quote_identifier(&o.column), o.order.as_sql());
600 if let Some(nulls) = o.nulls {
601 part.push(' ');
602 part.push_str(nulls.as_sql());
603 }
604 part
605 })
606 .collect();
607 sql.push_str(&format!(" ORDER BY {}", order_parts.join(", ")));
608 }
609
610 if let Some(take) = self.take {
612 sql.push_str(&format!(" LIMIT {}", take));
613 }
614 if let Some(skip) = self.skip {
615 sql.push_str(&format!(" OFFSET {}", skip));
616 }
617
618 (sql, params)
619 }
620
621 pub async fn exec(self) -> QueryResult<Vec<GroupByResult>> {
633 let engine = self.engine.as_ref().ok_or_else(|| {
634 crate::error::QueryError::internal(
635 "GroupByOperation::exec called on a builder without an engine; \
636 use Client<E>::group_by() (which calls with_engine) instead of \
637 GroupByOperation::new()",
638 )
639 })?;
640 let dialect = engine.dialect();
641 let group_columns = self.group_columns.clone();
642 let (sql, params) = self.build_sql(dialect);
643 let rows = engine.aggregate_query(&sql, params).await?;
644 Ok(rows
645 .into_iter()
646 .map(|row| {
647 let mut group_values = std::collections::HashMap::new();
648 let mut agg_map = std::collections::HashMap::new();
649 for (k, v) in row {
650 if group_columns.iter().any(|c| c == &k) {
651 group_values.insert(k, filter_value_to_json(&v));
652 } else {
653 agg_map.insert(k, v);
654 }
655 }
656 GroupByResult {
657 group_values,
658 aggregates: AggregateResult::from_row(agg_map),
659 }
660 })
661 .collect())
662 }
663}
664
665#[derive(Debug, Clone)]
667pub struct GroupByResult {
668 pub group_values: std::collections::HashMap<String, serde_json::Value>,
670 pub aggregates: AggregateResult,
672}
673
674pub mod having {
676 use super::*;
677
678 pub fn count_gt(value: f64) -> HavingCondition {
680 HavingCondition {
681 field: AggregateField::CountAll,
682 op: HavingOp::Gt,
683 value,
684 }
685 }
686
687 pub fn count_gte(value: f64) -> HavingCondition {
689 HavingCondition {
690 field: AggregateField::CountAll,
691 op: HavingOp::Gte,
692 value,
693 }
694 }
695
696 pub fn count_lt(value: f64) -> HavingCondition {
698 HavingCondition {
699 field: AggregateField::CountAll,
700 op: HavingOp::Lt,
701 value,
702 }
703 }
704
705 pub fn sum_gt(column: impl Into<String>, value: f64) -> HavingCondition {
707 HavingCondition {
708 field: AggregateField::Sum(column.into()),
709 op: HavingOp::Gt,
710 value,
711 }
712 }
713
714 pub fn avg_gt(column: impl Into<String>, value: f64) -> HavingCondition {
716 HavingCondition {
717 field: AggregateField::Avg(column.into()),
718 op: HavingOp::Gt,
719 value,
720 }
721 }
722}
723
724#[cfg(test)]
725mod tests {
726 use super::*;
727 use crate::filter::{Filter, FilterValue};
728 use crate::types::NullsOrder;
729
730 struct TestModel;
732
733 impl Model for TestModel {
734 const MODEL_NAME: &'static str = "TestModel";
735 const TABLE_NAME: &'static str = "test_models";
736 const PRIMARY_KEY: &'static [&'static str] = &["id"];
737 const COLUMNS: &'static [&'static str] = &["id", "name", "age", "score"];
738 }
739
740 impl crate::row::FromRow for TestModel {
741 fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
742 Ok(TestModel)
743 }
744 }
745
746 #[derive(Clone)]
748 struct MockEngine;
749
750 impl QueryEngine for MockEngine {
751 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
752 &crate::dialect::Postgres
753 }
754
755 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
756 &self,
757 _sql: &str,
758 _params: Vec<crate::filter::FilterValue>,
759 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
760 Box::pin(async { Ok(Vec::new()) })
761 }
762
763 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
764 &self,
765 _sql: &str,
766 _params: Vec<crate::filter::FilterValue>,
767 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
768 Box::pin(async { Err(crate::error::QueryError::not_found("Not implemented")) })
769 }
770
771 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
772 &self,
773 _sql: &str,
774 _params: Vec<crate::filter::FilterValue>,
775 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
776 Box::pin(async { Ok(None) })
777 }
778
779 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
780 &self,
781 _sql: &str,
782 _params: Vec<crate::filter::FilterValue>,
783 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
784 Box::pin(async { Err(crate::error::QueryError::not_found("Not implemented")) })
785 }
786
787 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
788 &self,
789 _sql: &str,
790 _params: Vec<crate::filter::FilterValue>,
791 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
792 Box::pin(async { Ok(Vec::new()) })
793 }
794
795 fn execute_delete(
796 &self,
797 _sql: &str,
798 _params: Vec<crate::filter::FilterValue>,
799 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
800 Box::pin(async { Ok(0) })
801 }
802
803 fn execute_raw(
804 &self,
805 _sql: &str,
806 _params: Vec<crate::filter::FilterValue>,
807 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
808 Box::pin(async { Ok(0) })
809 }
810
811 fn count(
812 &self,
813 _sql: &str,
814 _params: Vec<crate::filter::FilterValue>,
815 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
816 Box::pin(async { Ok(0) })
817 }
818 }
819
820 #[test]
823 fn test_aggregate_field_sql() {
824 assert_eq!(AggregateField::CountAll.to_sql(), "COUNT(*)");
826 assert_eq!(
827 AggregateField::CountColumn("id".into()).to_sql(),
828 "COUNT(id)"
829 );
830 assert_eq!(
831 AggregateField::CountDistinct("email".into()).to_sql(),
832 "COUNT(DISTINCT email)"
833 );
834 assert_eq!(AggregateField::Sum("amount".into()).to_sql(), "SUM(amount)");
835 assert_eq!(
836 AggregateField::Avg("score".to_string()).to_sql(),
837 "AVG(score)"
838 );
839 assert_eq!(AggregateField::Min("age".into()).to_sql(), "MIN(age)");
840 assert_eq!(AggregateField::Max("age".into()).to_sql(), "MAX(age)");
841 assert_eq!(
843 AggregateField::CountColumn("user".to_string()).to_sql(),
844 "COUNT(\"user\")"
845 );
846 }
847
848 #[test]
849 fn test_aggregate_field_alias() {
850 assert_eq!(AggregateField::CountAll.alias(), "_count");
851 assert_eq!(
852 AggregateField::CountColumn("id".into()).alias(),
853 "_count_id"
854 );
855 assert_eq!(
856 AggregateField::CountDistinct("email".into()).alias(),
857 "_count_distinct_email"
858 );
859 assert_eq!(AggregateField::Sum("amount".into()).alias(), "_sum_amount");
860 assert_eq!(
861 AggregateField::Avg("score".to_string()).alias(),
862 "_avg_score"
863 );
864 assert_eq!(AggregateField::Min("age".into()).alias(), "_min_age");
865 assert_eq!(
866 AggregateField::Max("salary".to_string()).alias(),
867 "_max_salary"
868 );
869 }
870
871 #[test]
874 fn test_aggregate_result_default() {
875 let result = AggregateResult::default();
876 assert!(result.count.is_none());
877 assert!(result.sum.is_empty());
878 assert!(result.avg.is_empty());
879 assert!(result.min.is_empty());
880 assert!(result.max.is_empty());
881 }
882
883 #[test]
884 fn test_aggregate_result_debug() {
885 let result = AggregateResult::default();
886 let debug_str = format!("{:?}", result);
887 assert!(debug_str.contains("AggregateResult"));
888 }
889
890 #[test]
891 fn test_aggregate_result_clone() {
892 let mut result = AggregateResult {
893 count: Some(42),
894 ..AggregateResult::default()
895 };
896 result.sum.insert("amount".into(), 1000.0);
897
898 let cloned = result.clone();
899 assert_eq!(cloned.count, Some(42));
900 assert_eq!(cloned.sum.get("amount"), Some(&1000.0));
901 }
902
903 #[test]
906 fn test_aggregate_operation_new() {
907 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new();
908 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
909
910 assert!(sql.contains("COUNT(*)"));
912 assert!(params.is_empty());
913 }
914
915 #[test]
916 fn test_aggregate_operation_default() {
917 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::default();
918 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
919
920 assert!(sql.contains("COUNT(*)"));
921 assert!(params.is_empty());
922 }
923
924 #[test]
925 fn test_aggregate_operation_build_sql() {
926 let op: AggregateOperation<TestModel, MockEngine> =
927 AggregateOperation::new().count().sum("score").avg("age");
928
929 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
930
931 assert!(sql.contains("SELECT"));
932 assert!(sql.contains("COUNT(*)"));
933 assert!(sql.contains("SUM(score)"));
934 assert!(sql.contains("AVG(age)"));
935 assert!(sql.contains("FROM test_models"));
936 assert!(params.is_empty());
937 }
938
939 #[test]
940 fn test_aggregate_operation_count_column() {
941 let op: AggregateOperation<TestModel, MockEngine> =
942 AggregateOperation::new().count_column("email");
943
944 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
945
946 assert!(sql.contains("COUNT(email)"));
947 }
948
949 #[test]
950 fn test_aggregate_operation_count_distinct() {
951 let op: AggregateOperation<TestModel, MockEngine> =
952 AggregateOperation::new().count_distinct("email");
953
954 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
955
956 assert!(sql.contains("COUNT(DISTINCT email)"));
957 }
958
959 #[test]
960 fn test_aggregate_operation_min_max() {
961 let op: AggregateOperation<TestModel, MockEngine> =
962 AggregateOperation::new().min("age").max("age");
963
964 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
965
966 assert!(sql.contains("MIN(age)"));
967 assert!(sql.contains("MAX(age)"));
968 }
969
970 #[test]
971 fn test_aggregate_with_where() {
972 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
973 .count()
974 .r#where(Filter::Gt("age".into(), FilterValue::Int(18)));
975
976 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
977
978 assert!(sql.contains("WHERE"));
979 assert!(sql.contains("age")); assert!(sql.contains(">"));
981 assert!(!params.is_empty());
982 }
983
984 #[test]
985 fn test_aggregate_with_complex_filter() {
986 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
987 .sum("score")
988 .avg("age")
989 .r#where(Filter::and([
990 Filter::Gte("age".into(), FilterValue::Int(18)),
991 Filter::Equals("active".into(), FilterValue::Bool(true)),
992 ]));
993
994 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
995
996 assert!(sql.contains("WHERE"));
997 assert!(sql.contains("AND"));
998 assert_eq!(params.len(), 2);
999 }
1000
1001 #[test]
1002 fn test_aggregate_all_methods() {
1003 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1004 .count()
1005 .count_column("name")
1006 .count_distinct("email")
1007 .sum("score")
1008 .avg("score")
1009 .min("age")
1010 .max("age");
1011
1012 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1013
1014 assert!(sql.contains("COUNT(*)"));
1015 assert!(sql.contains("COUNT(name)"));
1016 assert!(sql.contains("COUNT(DISTINCT email)"));
1017 assert!(sql.contains("SUM(score)"));
1018 assert!(sql.contains("AVG(score)"));
1019 assert!(sql.contains("MIN(age)"));
1020 assert!(sql.contains("MAX(age)"));
1021 }
1022
1023 #[tokio::test]
1024 async fn test_aggregate_exec_without_engine_errors() {
1025 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new().count();
1028 let err = op.exec().await.unwrap_err();
1029 assert!(err.to_string().contains("without an engine"));
1030 }
1031
1032 #[tokio::test]
1033 async fn test_aggregate_exec_with_engine_ok() {
1034 let op: AggregateOperation<TestModel, MockEngine> =
1038 AggregateOperation::with_engine(MockEngine).count();
1039 let err = op.exec().await.unwrap_err();
1040 assert!(err.to_string().contains("aggregate_query"));
1041 }
1042
1043 #[test]
1046 fn test_group_by_new() {
1047 let op: GroupByOperation<TestModel, MockEngine> =
1048 GroupByOperation::new(vec!["department".into()]);
1049
1050 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1051
1052 assert!(sql.contains("GROUP BY department"));
1053 }
1054
1055 #[test]
1056 fn test_group_by_build_sql() {
1057 let op: GroupByOperation<TestModel, MockEngine> =
1058 GroupByOperation::new(vec!["name".to_string()])
1059 .count()
1060 .avg("score");
1061
1062 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1063
1064 assert!(sql.contains("SELECT"));
1065 assert!(sql.contains("name")); assert!(sql.contains("COUNT(*)"));
1067 assert!(sql.contains("AVG(score)"));
1068 assert!(sql.contains("GROUP BY name"));
1069 assert!(params.is_empty());
1070 }
1071
1072 #[test]
1073 fn test_group_by_multiple_columns() {
1074 let op: GroupByOperation<TestModel, MockEngine> =
1075 GroupByOperation::new(vec!["department".into(), "role".into()]).count();
1076
1077 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1078
1079 assert!(sql.contains("GROUP BY department, role"));
1080 }
1081
1082 #[test]
1083 fn test_group_by_with_sum() {
1084 let op: GroupByOperation<TestModel, MockEngine> =
1085 GroupByOperation::new(vec!["category".into()]).sum("amount");
1086
1087 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1088
1089 assert!(sql.contains("SUM(amount)"));
1090 }
1091
1092 #[test]
1093 fn test_group_by_with_min_max() {
1094 let op: GroupByOperation<TestModel, MockEngine> =
1095 GroupByOperation::new(vec!["category".into()])
1096 .min("price")
1097 .max("price");
1098
1099 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1100
1101 assert!(sql.contains("MIN(price)"));
1102 assert!(sql.contains("MAX(price)"));
1103 }
1104
1105 #[test]
1106 fn test_group_by_with_where() {
1107 let op: GroupByOperation<TestModel, MockEngine> =
1108 GroupByOperation::new(vec!["department".into()])
1109 .count()
1110 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)));
1111
1112 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1113
1114 assert!(sql.contains("WHERE"));
1115 assert!(sql.contains("GROUP BY"));
1116 assert_eq!(params.len(), 1);
1117 }
1118
1119 #[test]
1120 fn test_group_by_with_having() {
1121 let op: GroupByOperation<TestModel, MockEngine> =
1122 GroupByOperation::new(vec!["name".to_string()])
1123 .count()
1124 .having(having::count_gt(5.0));
1125
1126 let (sql, _params) = op.build_sql(&crate::dialect::Postgres);
1127
1128 assert!(sql.contains("HAVING COUNT(*) > 5"));
1129 }
1130
1131 #[test]
1132 fn test_group_by_with_order_and_limit() {
1133 let op: GroupByOperation<TestModel, MockEngine> =
1134 GroupByOperation::new(vec!["name".to_string()])
1135 .count()
1136 .order_by(OrderByField::desc("_count"))
1137 .take(10)
1138 .skip(5);
1139
1140 let (sql, _params) = op.build_sql(&crate::dialect::Postgres);
1141
1142 assert!(sql.contains("ORDER BY _count DESC")); assert!(sql.contains("LIMIT 10"));
1144 assert!(sql.contains("OFFSET 5"));
1145 }
1146
1147 #[test]
1148 fn test_group_by_order_with_nulls() {
1149 let op: GroupByOperation<TestModel, MockEngine> =
1150 GroupByOperation::new(vec!["department".into()])
1151 .count()
1152 .order_by(OrderByField::asc("name").nulls(NullsOrder::First));
1153
1154 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1155
1156 assert!(sql.contains("ORDER BY"));
1157 assert!(sql.contains("NULLS FIRST"));
1158 }
1159
1160 #[test]
1161 fn test_group_by_skip_only() {
1162 let op: GroupByOperation<TestModel, MockEngine> =
1163 GroupByOperation::new(vec!["department".into()])
1164 .count()
1165 .skip(20);
1166
1167 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1168
1169 assert!(sql.contains("OFFSET 20"));
1170 assert!(!sql.contains("LIMIT"));
1171 }
1172
1173 #[test]
1174 fn test_group_by_take_only() {
1175 let op: GroupByOperation<TestModel, MockEngine> =
1176 GroupByOperation::new(vec!["department".into()])
1177 .count()
1178 .take(50);
1179
1180 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1181
1182 assert!(sql.contains("LIMIT 50"));
1183 assert!(!sql.contains("OFFSET"));
1184 }
1185
1186 #[tokio::test]
1187 async fn test_group_by_exec_without_engine_errors() {
1188 let op: GroupByOperation<TestModel, MockEngine> =
1189 GroupByOperation::new(vec!["department".into()]).count();
1190 let err = op.exec().await.unwrap_err();
1191 assert!(err.to_string().contains("without an engine"));
1192 }
1193
1194 #[tokio::test]
1195 async fn test_group_by_exec_with_engine_ok() {
1196 let op: GroupByOperation<TestModel, MockEngine> =
1197 GroupByOperation::with_engine(MockEngine, vec!["department".into()]).count();
1198 let err = op.exec().await.unwrap_err();
1199 assert!(err.to_string().contains("aggregate_query"));
1200 }
1201
1202 #[test]
1205 fn test_having_op_as_str() {
1206 assert_eq!(HavingOp::Gt.as_str(), ">");
1207 assert_eq!(HavingOp::Gte.as_str(), ">=");
1208 assert_eq!(HavingOp::Lt.as_str(), "<");
1209 assert_eq!(HavingOp::Lte.as_str(), "<=");
1210 assert_eq!(HavingOp::Eq.as_str(), "=");
1211 assert_eq!(HavingOp::Ne.as_str(), "<>");
1212 }
1213
1214 #[test]
1217 fn test_having_condition_debug() {
1218 let cond = HavingCondition {
1219 field: AggregateField::CountAll,
1220 op: HavingOp::Gt,
1221 value: 10.0,
1222 };
1223 let debug_str = format!("{:?}", cond);
1224 assert!(debug_str.contains("HavingCondition"));
1225 }
1226
1227 #[test]
1228 fn test_having_condition_clone() {
1229 let cond = HavingCondition {
1230 field: AggregateField::Sum("amount".into()),
1231 op: HavingOp::Gte,
1232 value: 1000.0,
1233 };
1234 let cloned = cond.clone();
1235 assert!((cloned.value - 1000.0).abs() < f64::EPSILON);
1236 }
1237
1238 #[test]
1241 fn test_having_helpers() {
1242 let cond = having::count_gt(10.0);
1243 assert!(matches!(cond.field, AggregateField::CountAll));
1244 assert!(matches!(cond.op, HavingOp::Gt));
1245 assert!((cond.value - 10.0).abs() < f64::EPSILON);
1246
1247 let cond = having::sum_gt("amount", 1000.0);
1248 if let AggregateField::Sum(col) = cond.field {
1249 assert_eq!(col, "amount");
1250 } else {
1251 panic!("Expected Sum");
1252 }
1253 }
1254
1255 #[test]
1256 fn test_having_count_gte() {
1257 let cond = having::count_gte(5.0);
1258 assert!(matches!(cond.field, AggregateField::CountAll));
1259 assert!(matches!(cond.op, HavingOp::Gte));
1260 assert!((cond.value - 5.0).abs() < f64::EPSILON);
1261 }
1262
1263 #[test]
1264 fn test_having_count_lt() {
1265 let cond = having::count_lt(100.0);
1266 assert!(matches!(cond.field, AggregateField::CountAll));
1267 assert!(matches!(cond.op, HavingOp::Lt));
1268 assert!((cond.value - 100.0).abs() < f64::EPSILON);
1269 }
1270
1271 #[test]
1272 fn test_having_avg_gt() {
1273 let cond = having::avg_gt("score", 75.5);
1274 assert!(matches!(cond.op, HavingOp::Gt));
1275 assert!((cond.value - 75.5).abs() < f64::EPSILON);
1276 if let AggregateField::Avg(col) = cond.field {
1277 assert_eq!(col, "score");
1278 } else {
1279 panic!("Expected Avg");
1280 }
1281 }
1282
1283 #[test]
1284 fn test_having_sum_gt_with_different_columns() {
1285 let cond1 = having::sum_gt("revenue", 50000.0);
1286 let cond2 = having::sum_gt("cost", 10000.0);
1287
1288 if let AggregateField::Sum(col) = &cond1.field {
1289 assert_eq!(col, "revenue");
1290 }
1291 if let AggregateField::Sum(col) = &cond2.field {
1292 assert_eq!(col, "cost");
1293 }
1294 }
1295
1296 #[test]
1299 fn test_group_by_result_debug() {
1300 let result = GroupByResult {
1301 group_values: std::collections::HashMap::new(),
1302 aggregates: AggregateResult::default(),
1303 };
1304 let debug_str = format!("{:?}", result);
1305 assert!(debug_str.contains("GroupByResult"));
1306 }
1307
1308 #[test]
1309 fn test_group_by_result_clone() {
1310 let mut result = GroupByResult {
1311 group_values: std::collections::HashMap::new(),
1312 aggregates: AggregateResult::default(),
1313 };
1314 result
1315 .group_values
1316 .insert("category".into(), serde_json::json!("electronics"));
1317 result.aggregates.count = Some(50);
1318
1319 let cloned = result.clone();
1320 assert_eq!(cloned.aggregates.count, Some(50));
1321 assert!(cloned.group_values.contains_key("category"));
1322 }
1323
1324 #[test]
1327 fn test_group_by_sql_structure() {
1328 let op: GroupByOperation<TestModel, MockEngine> =
1329 GroupByOperation::new(vec!["department".into()])
1330 .count()
1331 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)))
1332 .having(having::count_gt(5.0))
1333 .order_by(OrderByField::desc("_count"))
1334 .take(10)
1335 .skip(5);
1336
1337 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1338
1339 let select_pos = sql.find("SELECT").unwrap();
1341 let from_pos = sql.find("FROM").unwrap();
1342 let where_pos = sql.find("WHERE").unwrap();
1343 let group_pos = sql.find("GROUP BY").unwrap();
1344 let having_pos = sql.find("HAVING").unwrap();
1345 let order_pos = sql.find("ORDER BY").unwrap();
1346 let limit_pos = sql.find("LIMIT").unwrap();
1347 let offset_pos = sql.find("OFFSET").unwrap();
1348
1349 assert!(select_pos < from_pos);
1350 assert!(from_pos < where_pos);
1351 assert!(where_pos < group_pos);
1352 assert!(group_pos < having_pos);
1353 assert!(having_pos < order_pos);
1354 assert!(order_pos < limit_pos);
1355 assert!(limit_pos < offset_pos);
1356 }
1357
1358 #[test]
1359 fn test_aggregate_no_group_by() {
1360 let op: AggregateOperation<TestModel, MockEngine> =
1361 AggregateOperation::new().count().sum("score");
1362
1363 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1364
1365 assert!(!sql.contains("GROUP BY"));
1366 }
1367
1368 #[test]
1369 fn test_group_by_empty_columns() {
1370 let op: GroupByOperation<TestModel, MockEngine> = GroupByOperation::new(vec![]).count();
1371
1372 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1373
1374 assert!(!sql.contains("GROUP BY"));
1376 }
1377}