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, Copy, PartialEq, Eq)]
52pub enum CountSelectMode {
53 NonNull,
55 Distinct,
57}
58
59#[derive(Debug, Clone)]
61pub enum AggregateField {
62 CountAll,
64 CountColumn(String),
66 CountDistinct(String),
68 Sum(String),
70 Avg(String),
72 Min(String),
74 Max(String),
76}
77
78impl AggregateField {
79 pub fn to_sql(&self) -> String {
81 match self {
82 Self::CountAll => "COUNT(*)".to_string(),
83 Self::CountColumn(col) => format!("COUNT({})", quote_identifier(col)),
84 Self::CountDistinct(col) => format!("COUNT(DISTINCT {})", quote_identifier(col)),
85 Self::Sum(col) => format!("SUM({})", quote_identifier(col)),
86 Self::Avg(col) => format!("AVG({})", quote_identifier(col)),
87 Self::Min(col) => format!("MIN({})", quote_identifier(col)),
88 Self::Max(col) => format!("MAX({})", quote_identifier(col)),
89 }
90 }
91
92 pub fn alias(&self) -> String {
94 match self {
95 Self::CountAll => "_count".to_string(),
96 Self::CountColumn(col) => format!("_count_{}", col),
97 Self::CountDistinct(col) => format!("_count_distinct_{}", col),
98 Self::Sum(col) => format!("_sum_{}", col),
99 Self::Avg(col) => format!("_avg_{}", col),
100 Self::Min(col) => format!("_min_{}", col),
101 Self::Max(col) => format!("_max_{}", col),
102 }
103 }
104}
105
106#[derive(Debug, Clone, Default)]
114pub struct AggregateResult {
115 pub count: Option<i64>,
117 pub count_columns: std::collections::HashMap<String, i64>,
119 pub count_distinct: std::collections::HashMap<String, i64>,
121 pub sum: std::collections::HashMap<String, f64>,
123 pub avg: std::collections::HashMap<String, f64>,
125 pub min: std::collections::HashMap<String, serde_json::Value>,
127 pub max: std::collections::HashMap<String, serde_json::Value>,
129}
130
131impl AggregateResult {
132 pub fn from_row(row: std::collections::HashMap<String, crate::filter::FilterValue>) -> Self {
143 use crate::filter::FilterValue;
144 let mut out = Self::default();
145 for (k, v) in row {
146 if k == "_count" {
147 if let FilterValue::Int(n) = v {
148 out.count = Some(n);
149 }
150 } else if let Some(col) = k.strip_prefix("_count_distinct_") {
151 if let Some(n) = value_to_i64(&v) {
152 out.count_distinct.insert(col.to_string(), n);
153 }
154 } else if let Some(col) = k.strip_prefix("_count_") {
155 if let Some(n) = value_to_i64(&v) {
156 out.count_columns.insert(col.to_string(), n);
157 }
158 } else if let Some(col) = k.strip_prefix("_sum_") {
159 if let Some(f) = value_to_f64(&v) {
160 out.sum.insert(col.to_string(), f);
161 }
162 } else if let Some(col) = k.strip_prefix("_avg_") {
163 if let Some(f) = value_to_f64(&v) {
164 out.avg.insert(col.to_string(), f);
165 }
166 } else if let Some(col) = k.strip_prefix("_min_") {
167 out.min.insert(col.to_string(), filter_value_to_json(&v));
168 } else if let Some(col) = k.strip_prefix("_max_") {
169 out.max.insert(col.to_string(), filter_value_to_json(&v));
170 }
171 }
172 out
173 }
174
175 pub fn count_of(&self, column: &str) -> Option<i64> {
177 self.count_columns.get(column).copied()
178 }
179
180 pub fn count_distinct_of(&self, column: &str) -> Option<i64> {
182 self.count_distinct.get(column).copied()
183 }
184
185 pub fn sum_as_f64(&self, column: &str) -> Option<f64> {
187 self.sum.get(column).copied()
188 }
189
190 pub fn avg_as_f64(&self, column: &str) -> Option<f64> {
192 self.avg.get(column).copied()
193 }
194
195 pub fn min_as_f64(&self, column: &str) -> Option<f64> {
198 self.min.get(column).and_then(|v| v.as_f64())
199 }
200
201 pub fn max_as_f64(&self, column: &str) -> Option<f64> {
204 self.max.get(column).and_then(|v| v.as_f64())
205 }
206}
207
208fn value_to_i64(v: &crate::filter::FilterValue) -> Option<i64> {
209 use crate::filter::FilterValue;
210 match v {
211 FilterValue::Int(n) => Some(*n),
212 FilterValue::String(s) => s.parse::<i64>().ok(),
213 _ => None,
214 }
215}
216
217fn value_to_f64(v: &crate::filter::FilterValue) -> Option<f64> {
218 use crate::filter::FilterValue;
219 match v {
220 FilterValue::Int(n) => Some(*n as f64),
221 FilterValue::Float(f) => Some(*f),
222 FilterValue::String(s) => s.parse::<f64>().ok(),
223 _ => None,
224 }
225}
226
227fn filter_value_to_json(v: &crate::filter::FilterValue) -> serde_json::Value {
228 use crate::filter::FilterValue;
229 match v {
230 FilterValue::Null => serde_json::Value::Null,
231 FilterValue::Bool(b) => serde_json::Value::Bool(*b),
232 FilterValue::Int(n) => serde_json::Value::from(*n),
233 FilterValue::Float(f) => serde_json::Number::from_f64(*f)
234 .map(serde_json::Value::Number)
235 .unwrap_or(serde_json::Value::Null),
236 FilterValue::String(s) => serde_json::Value::String(s.clone()),
237 FilterValue::Json(j) => j.clone(),
238 FilterValue::List(_) => serde_json::Value::Null,
239 }
240}
241
242#[derive(Debug)]
254pub struct AggregateOperation<M: Model, E: QueryEngine> {
255 _model: PhantomData<M>,
257 engine: Option<E>,
260 fields: Vec<AggregateField>,
262 filter: Option<Filter>,
264}
265
266impl<M: Model, E: QueryEngine> AggregateOperation<M, E> {
267 pub fn new() -> Self {
272 Self {
273 _model: PhantomData,
274 engine: None,
275 fields: Vec::new(),
276 filter: None,
277 }
278 }
279
280 pub fn with_engine(engine: E) -> Self {
285 Self {
286 _model: PhantomData,
287 engine: Some(engine),
288 fields: Vec::new(),
289 filter: None,
290 }
291 }
292
293 pub fn count(mut self) -> Self {
295 self.fields.push(AggregateField::CountAll);
296 self
297 }
298
299 pub fn count_column(mut self, column: impl Into<String>) -> Self {
301 self.fields.push(AggregateField::CountColumn(column.into()));
302 self
303 }
304
305 pub fn count_distinct(mut self, column: impl Into<String>) -> Self {
307 self.fields
308 .push(AggregateField::CountDistinct(column.into()));
309 self
310 }
311
312 pub fn sum(mut self, column: impl Into<String>) -> Self {
314 self.fields.push(AggregateField::Sum(column.into()));
315 self
316 }
317
318 pub fn avg(mut self, column: impl Into<String>) -> Self {
320 self.fields.push(AggregateField::Avg(column.into()));
321 self
322 }
323
324 pub fn min(mut self, column: impl Into<String>) -> Self {
326 self.fields.push(AggregateField::Min(column.into()));
327 self
328 }
329
330 pub fn max(mut self, column: impl Into<String>) -> Self {
332 self.fields.push(AggregateField::Max(column.into()));
333 self
334 }
335
336 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
338 self.filter = Some(filter.into());
339 self
340 }
341
342 pub fn with_where_input<W: crate::inputs::WhereInput<Model = M>>(mut self, w: W) -> Self {
344 let f = w.into_ir();
345 self.filter = Some(match self.filter.take() {
346 Some(existing) => existing.and_then(f),
347 None => f,
348 });
349 self
350 }
351
352 pub fn build_sql(
354 &self,
355 dialect: &dyn crate::dialect::SqlDialect,
356 ) -> (String, Vec<crate::filter::FilterValue>) {
357 let mut params = Vec::new();
358
359 let fields = if self.fields.is_empty() {
361 vec![AggregateField::CountAll]
362 } else {
363 self.fields.clone()
364 };
365
366 let select_parts: Vec<String> = fields
367 .iter()
368 .map(|f| format!("{} AS {}", f.to_sql(), quote_identifier(&f.alias())))
369 .collect();
370
371 let mut sql = format!(
372 "SELECT {} FROM {}",
373 select_parts.join(", "),
374 quote_identifier(M::TABLE_NAME)
375 );
376
377 if let Some(filter) = &self.filter {
379 let (where_sql, where_params) = filter.to_sql(params.len() + 1, dialect);
380 sql.push_str(&format!(" WHERE {}", where_sql));
381 params.extend(where_params);
382 }
383
384 (sql, params)
385 }
386
387 pub async fn exec(self) -> QueryResult<AggregateResult> {
398 let engine = self.engine.as_ref().ok_or_else(|| {
399 crate::error::QueryError::internal(
400 "AggregateOperation::exec called on a builder without an engine; \
401 use Client<E>::aggregate() (which calls with_engine) instead of \
402 AggregateOperation::new()",
403 )
404 })?;
405 let dialect = engine.dialect();
406 let (sql, params) = self.build_sql(dialect);
407 let mut rows = engine.aggregate_query(&sql, params).await?;
408 Ok(AggregateResult::from_row(rows.pop().unwrap_or_default()))
409 }
410}
411
412impl<M: Model, E: QueryEngine> Default for AggregateOperation<M, E> {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418#[derive(Debug)]
427pub struct GroupByOperation<M: Model, E: QueryEngine> {
428 _model: PhantomData<M>,
430 engine: Option<E>,
433 group_columns: Vec<String>,
435 agg_fields: Vec<AggregateField>,
437 filter: Option<Filter>,
439 having: Option<HavingCondition>,
441 order_by: Vec<OrderByField>,
443 skip: Option<usize>,
445 take: Option<usize>,
447}
448
449#[derive(Debug, Clone)]
451pub struct HavingCondition {
452 pub field: AggregateField,
454 pub op: HavingOp,
456 pub value: f64,
458}
459
460#[derive(Debug, Clone, Copy)]
462pub enum HavingOp {
463 Gt,
465 Gte,
467 Lt,
469 Lte,
471 Eq,
473 Ne,
475}
476
477impl HavingOp {
478 pub fn as_str(&self) -> &'static str {
480 match self {
481 Self::Gt => ">",
482 Self::Gte => ">=",
483 Self::Lt => "<",
484 Self::Lte => "<=",
485 Self::Eq => "=",
486 Self::Ne => "<>",
487 }
488 }
489}
490
491impl<M: Model, E: QueryEngine> GroupByOperation<M, E> {
492 pub fn new(columns: Vec<String>) -> Self {
497 Self {
498 _model: PhantomData,
499 engine: None,
500 group_columns: columns,
501 agg_fields: Vec::new(),
502 filter: None,
503 having: None,
504 order_by: Vec::new(),
505 skip: None,
506 take: None,
507 }
508 }
509
510 pub fn with_engine(engine: E, columns: Vec<String>) -> Self {
515 Self {
516 _model: PhantomData,
517 engine: Some(engine),
518 group_columns: columns,
519 agg_fields: Vec::new(),
520 filter: None,
521 having: None,
522 order_by: Vec::new(),
523 skip: None,
524 take: None,
525 }
526 }
527
528 pub fn count(mut self) -> Self {
530 self.agg_fields.push(AggregateField::CountAll);
531 self
532 }
533
534 pub fn count_column(mut self, column: impl Into<String>) -> Self {
536 self.agg_fields
537 .push(AggregateField::CountColumn(column.into()));
538 self
539 }
540
541 pub fn count_distinct(mut self, column: impl Into<String>) -> Self {
543 self.agg_fields
544 .push(AggregateField::CountDistinct(column.into()));
545 self
546 }
547
548 pub fn sum(mut self, column: impl Into<String>) -> Self {
550 self.agg_fields.push(AggregateField::Sum(column.into()));
551 self
552 }
553
554 pub fn avg(mut self, column: impl Into<String>) -> Self {
556 self.agg_fields.push(AggregateField::Avg(column.into()));
557 self
558 }
559
560 pub fn min(mut self, column: impl Into<String>) -> Self {
562 self.agg_fields.push(AggregateField::Min(column.into()));
563 self
564 }
565
566 pub fn max(mut self, column: impl Into<String>) -> Self {
568 self.agg_fields.push(AggregateField::Max(column.into()));
569 self
570 }
571
572 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
574 self.filter = Some(filter.into());
575 self
576 }
577
578 pub fn having(mut self, condition: HavingCondition) -> Self {
580 self.having = Some(condition);
581 self
582 }
583
584 pub fn order_by(mut self, order: impl Into<OrderByField>) -> Self {
586 self.order_by.push(order.into());
587 self
588 }
589
590 pub fn skip(mut self, count: usize) -> Self {
592 self.skip = Some(count);
593 self
594 }
595
596 pub fn take(mut self, count: usize) -> Self {
598 self.take = Some(count);
599 self
600 }
601
602 pub fn build_sql(
604 &self,
605 dialect: &dyn crate::dialect::SqlDialect,
606 ) -> (String, Vec<crate::filter::FilterValue>) {
607 let mut params = Vec::new();
608
609 let mut select_parts: Vec<String> = self
611 .group_columns
612 .iter()
613 .map(|c| quote_identifier(c))
614 .collect();
615
616 for field in &self.agg_fields {
617 select_parts.push(format!(
618 "{} AS {}",
619 field.to_sql(),
620 quote_identifier(&field.alias())
621 ));
622 }
623
624 let mut sql = format!(
625 "SELECT {} FROM {}",
626 select_parts.join(", "),
627 quote_identifier(M::TABLE_NAME)
628 );
629
630 if let Some(filter) = &self.filter {
632 let (where_sql, where_params) = filter.to_sql(params.len() + 1, dialect);
633 sql.push_str(&format!(" WHERE {}", where_sql));
634 params.extend(where_params);
635 }
636
637 if !self.group_columns.is_empty() {
639 let group_cols: Vec<String> = self
640 .group_columns
641 .iter()
642 .map(|c| quote_identifier(c))
643 .collect();
644 sql.push_str(&format!(" GROUP BY {}", group_cols.join(", ")));
645 }
646
647 if let Some(having) = &self.having {
649 sql.push_str(&format!(
650 " HAVING {} {} {}",
651 having.field.to_sql(),
652 having.op.as_str(),
653 having.value
654 ));
655 }
656
657 if !self.order_by.is_empty() {
659 let order_parts: Vec<String> = self
660 .order_by
661 .iter()
662 .map(|o| {
663 let mut part = format!("{} {}", quote_identifier(&o.column), o.order.as_sql());
664 if let Some(nulls) = o.nulls {
665 part.push(' ');
666 part.push_str(nulls.as_sql());
667 }
668 part
669 })
670 .collect();
671 sql.push_str(&format!(" ORDER BY {}", order_parts.join(", ")));
672 }
673
674 if let Some(take) = self.take {
676 sql.push_str(&format!(" LIMIT {}", take));
677 }
678 if let Some(skip) = self.skip {
679 sql.push_str(&format!(" OFFSET {}", skip));
680 }
681
682 (sql, params)
683 }
684
685 pub async fn exec(self) -> QueryResult<Vec<GroupByResult>> {
697 let engine = self.engine.as_ref().ok_or_else(|| {
698 crate::error::QueryError::internal(
699 "GroupByOperation::exec called on a builder without an engine; \
700 use Client<E>::group_by() (which calls with_engine) instead of \
701 GroupByOperation::new()",
702 )
703 })?;
704 let dialect = engine.dialect();
705 let group_columns = self.group_columns.clone();
706 let (sql, params) = self.build_sql(dialect);
707 let rows = engine.aggregate_query(&sql, params).await?;
708 Ok(rows
709 .into_iter()
710 .map(|row| {
711 let mut group_values = std::collections::HashMap::new();
712 let mut agg_map = std::collections::HashMap::new();
713 for (k, v) in row {
714 if group_columns.iter().any(|c| c == &k) {
715 group_values.insert(k, filter_value_to_json(&v));
716 } else {
717 agg_map.insert(k, v);
718 }
719 }
720 GroupByResult {
721 group_values,
722 aggregates: AggregateResult::from_row(agg_map),
723 }
724 })
725 .collect())
726 }
727}
728
729#[derive(Debug, Clone)]
731pub struct GroupByResult {
732 pub group_values: std::collections::HashMap<String, serde_json::Value>,
734 pub aggregates: AggregateResult,
736}
737
738pub mod having {
740 use super::*;
741
742 pub fn count_gt(value: f64) -> HavingCondition {
744 HavingCondition {
745 field: AggregateField::CountAll,
746 op: HavingOp::Gt,
747 value,
748 }
749 }
750
751 pub fn count_gte(value: f64) -> HavingCondition {
753 HavingCondition {
754 field: AggregateField::CountAll,
755 op: HavingOp::Gte,
756 value,
757 }
758 }
759
760 pub fn count_lt(value: f64) -> HavingCondition {
762 HavingCondition {
763 field: AggregateField::CountAll,
764 op: HavingOp::Lt,
765 value,
766 }
767 }
768
769 pub fn count_lte(value: f64) -> HavingCondition {
770 HavingCondition {
771 field: AggregateField::CountAll,
772 op: HavingOp::Lte,
773 value,
774 }
775 }
776
777 pub fn count_eq(value: f64) -> HavingCondition {
778 HavingCondition {
779 field: AggregateField::CountAll,
780 op: HavingOp::Eq,
781 value,
782 }
783 }
784
785 pub fn count_ne(value: f64) -> HavingCondition {
786 HavingCondition {
787 field: AggregateField::CountAll,
788 op: HavingOp::Ne,
789 value,
790 }
791 }
792
793 pub fn sum_gt(column: impl Into<String>, value: f64) -> HavingCondition {
794 HavingCondition {
795 field: AggregateField::Sum(column.into()),
796 op: HavingOp::Gt,
797 value,
798 }
799 }
800
801 pub fn sum_gte(column: impl Into<String>, value: f64) -> HavingCondition {
802 HavingCondition {
803 field: AggregateField::Sum(column.into()),
804 op: HavingOp::Gte,
805 value,
806 }
807 }
808
809 pub fn sum_lt(column: impl Into<String>, value: f64) -> HavingCondition {
810 HavingCondition {
811 field: AggregateField::Sum(column.into()),
812 op: HavingOp::Lt,
813 value,
814 }
815 }
816
817 pub fn sum_lte(column: impl Into<String>, value: f64) -> HavingCondition {
818 HavingCondition {
819 field: AggregateField::Sum(column.into()),
820 op: HavingOp::Lte,
821 value,
822 }
823 }
824
825 pub fn sum_eq(column: impl Into<String>, value: f64) -> HavingCondition {
826 HavingCondition {
827 field: AggregateField::Sum(column.into()),
828 op: HavingOp::Eq,
829 value,
830 }
831 }
832
833 pub fn sum_ne(column: impl Into<String>, value: f64) -> HavingCondition {
834 HavingCondition {
835 field: AggregateField::Sum(column.into()),
836 op: HavingOp::Ne,
837 value,
838 }
839 }
840
841 pub fn avg_gt(column: impl Into<String>, value: f64) -> HavingCondition {
842 HavingCondition {
843 field: AggregateField::Avg(column.into()),
844 op: HavingOp::Gt,
845 value,
846 }
847 }
848
849 pub fn avg_gte(column: impl Into<String>, value: f64) -> HavingCondition {
850 HavingCondition {
851 field: AggregateField::Avg(column.into()),
852 op: HavingOp::Gte,
853 value,
854 }
855 }
856
857 pub fn avg_lt(column: impl Into<String>, value: f64) -> HavingCondition {
858 HavingCondition {
859 field: AggregateField::Avg(column.into()),
860 op: HavingOp::Lt,
861 value,
862 }
863 }
864
865 pub fn avg_lte(column: impl Into<String>, value: f64) -> HavingCondition {
866 HavingCondition {
867 field: AggregateField::Avg(column.into()),
868 op: HavingOp::Lte,
869 value,
870 }
871 }
872
873 pub fn avg_eq(column: impl Into<String>, value: f64) -> HavingCondition {
874 HavingCondition {
875 field: AggregateField::Avg(column.into()),
876 op: HavingOp::Eq,
877 value,
878 }
879 }
880
881 pub fn avg_ne(column: impl Into<String>, value: f64) -> HavingCondition {
882 HavingCondition {
883 field: AggregateField::Avg(column.into()),
884 op: HavingOp::Ne,
885 value,
886 }
887 }
888
889 pub fn min_gt(column: impl Into<String>, value: f64) -> HavingCondition {
890 HavingCondition {
891 field: AggregateField::Min(column.into()),
892 op: HavingOp::Gt,
893 value,
894 }
895 }
896
897 pub fn min_gte(column: impl Into<String>, value: f64) -> HavingCondition {
898 HavingCondition {
899 field: AggregateField::Min(column.into()),
900 op: HavingOp::Gte,
901 value,
902 }
903 }
904
905 pub fn min_lt(column: impl Into<String>, value: f64) -> HavingCondition {
906 HavingCondition {
907 field: AggregateField::Min(column.into()),
908 op: HavingOp::Lt,
909 value,
910 }
911 }
912
913 pub fn min_lte(column: impl Into<String>, value: f64) -> HavingCondition {
914 HavingCondition {
915 field: AggregateField::Min(column.into()),
916 op: HavingOp::Lte,
917 value,
918 }
919 }
920
921 pub fn min_eq(column: impl Into<String>, value: f64) -> HavingCondition {
922 HavingCondition {
923 field: AggregateField::Min(column.into()),
924 op: HavingOp::Eq,
925 value,
926 }
927 }
928
929 pub fn min_ne(column: impl Into<String>, value: f64) -> HavingCondition {
930 HavingCondition {
931 field: AggregateField::Min(column.into()),
932 op: HavingOp::Ne,
933 value,
934 }
935 }
936
937 pub fn max_gt(column: impl Into<String>, value: f64) -> HavingCondition {
938 HavingCondition {
939 field: AggregateField::Max(column.into()),
940 op: HavingOp::Gt,
941 value,
942 }
943 }
944
945 pub fn max_gte(column: impl Into<String>, value: f64) -> HavingCondition {
946 HavingCondition {
947 field: AggregateField::Max(column.into()),
948 op: HavingOp::Gte,
949 value,
950 }
951 }
952
953 pub fn max_lt(column: impl Into<String>, value: f64) -> HavingCondition {
954 HavingCondition {
955 field: AggregateField::Max(column.into()),
956 op: HavingOp::Lt,
957 value,
958 }
959 }
960
961 pub fn max_lte(column: impl Into<String>, value: f64) -> HavingCondition {
962 HavingCondition {
963 field: AggregateField::Max(column.into()),
964 op: HavingOp::Lte,
965 value,
966 }
967 }
968
969 pub fn max_eq(column: impl Into<String>, value: f64) -> HavingCondition {
970 HavingCondition {
971 field: AggregateField::Max(column.into()),
972 op: HavingOp::Eq,
973 value,
974 }
975 }
976
977 pub fn max_ne(column: impl Into<String>, value: f64) -> HavingCondition {
978 HavingCondition {
979 field: AggregateField::Max(column.into()),
980 op: HavingOp::Ne,
981 value,
982 }
983 }
984}
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989 use crate::filter::{Filter, FilterValue};
990 use crate::types::NullsOrder;
991
992 struct TestModel;
994
995 impl Model for TestModel {
996 const MODEL_NAME: &'static str = "TestModel";
997 const TABLE_NAME: &'static str = "test_models";
998 const PRIMARY_KEY: &'static [&'static str] = &["id"];
999 const COLUMNS: &'static [&'static str] = &["id", "name", "age", "score"];
1000 }
1001
1002 impl crate::row::FromRow for TestModel {
1003 fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
1004 Ok(TestModel)
1005 }
1006 }
1007
1008 #[derive(Clone)]
1010 struct MockEngine;
1011
1012 impl QueryEngine for MockEngine {
1013 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
1014 &crate::dialect::Postgres
1015 }
1016
1017 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
1018 &self,
1019 _sql: &str,
1020 _params: Vec<crate::filter::FilterValue>,
1021 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
1022 Box::pin(async { Ok(Vec::new()) })
1023 }
1024
1025 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
1026 &self,
1027 _sql: &str,
1028 _params: Vec<crate::filter::FilterValue>,
1029 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
1030 Box::pin(async { Err(crate::error::QueryError::not_found("Not implemented")) })
1031 }
1032
1033 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
1034 &self,
1035 _sql: &str,
1036 _params: Vec<crate::filter::FilterValue>,
1037 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
1038 Box::pin(async { Ok(None) })
1039 }
1040
1041 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
1042 &self,
1043 _sql: &str,
1044 _params: Vec<crate::filter::FilterValue>,
1045 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
1046 Box::pin(async { Err(crate::error::QueryError::not_found("Not implemented")) })
1047 }
1048
1049 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
1050 &self,
1051 _sql: &str,
1052 _params: Vec<crate::filter::FilterValue>,
1053 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
1054 Box::pin(async { Ok(Vec::new()) })
1055 }
1056
1057 fn execute_delete(
1058 &self,
1059 _sql: &str,
1060 _params: Vec<crate::filter::FilterValue>,
1061 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
1062 Box::pin(async { Ok(0) })
1063 }
1064
1065 fn execute_raw(
1066 &self,
1067 _sql: &str,
1068 _params: Vec<crate::filter::FilterValue>,
1069 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
1070 Box::pin(async { Ok(0) })
1071 }
1072
1073 fn count(
1074 &self,
1075 _sql: &str,
1076 _params: Vec<crate::filter::FilterValue>,
1077 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
1078 Box::pin(async { Ok(0) })
1079 }
1080 }
1081
1082 #[test]
1085 fn test_aggregate_field_sql() {
1086 assert_eq!(AggregateField::CountAll.to_sql(), "COUNT(*)");
1088 assert_eq!(
1089 AggregateField::CountColumn("id".into()).to_sql(),
1090 "COUNT(id)"
1091 );
1092 assert_eq!(
1093 AggregateField::CountDistinct("email".into()).to_sql(),
1094 "COUNT(DISTINCT email)"
1095 );
1096 assert_eq!(AggregateField::Sum("amount".into()).to_sql(), "SUM(amount)");
1097 assert_eq!(
1098 AggregateField::Avg("score".to_string()).to_sql(),
1099 "AVG(score)"
1100 );
1101 assert_eq!(AggregateField::Min("age".into()).to_sql(), "MIN(age)");
1102 assert_eq!(AggregateField::Max("age".into()).to_sql(), "MAX(age)");
1103 assert_eq!(
1105 AggregateField::CountColumn("user".to_string()).to_sql(),
1106 "COUNT(\"user\")"
1107 );
1108 }
1109
1110 #[test]
1111 fn test_aggregate_field_alias() {
1112 assert_eq!(AggregateField::CountAll.alias(), "_count");
1113 assert_eq!(
1114 AggregateField::CountColumn("id".into()).alias(),
1115 "_count_id"
1116 );
1117 assert_eq!(
1118 AggregateField::CountDistinct("email".into()).alias(),
1119 "_count_distinct_email"
1120 );
1121 assert_eq!(AggregateField::Sum("amount".into()).alias(), "_sum_amount");
1122 assert_eq!(
1123 AggregateField::Avg("score".to_string()).alias(),
1124 "_avg_score"
1125 );
1126 assert_eq!(AggregateField::Min("age".into()).alias(), "_min_age");
1127 assert_eq!(
1128 AggregateField::Max("salary".to_string()).alias(),
1129 "_max_salary"
1130 );
1131 }
1132
1133 #[test]
1136 fn test_aggregate_result_default() {
1137 let result = AggregateResult::default();
1138 assert!(result.count.is_none());
1139 assert!(result.sum.is_empty());
1140 assert!(result.avg.is_empty());
1141 assert!(result.min.is_empty());
1142 assert!(result.max.is_empty());
1143 }
1144
1145 #[test]
1146 fn test_aggregate_result_debug() {
1147 let result = AggregateResult::default();
1148 let debug_str = format!("{:?}", result);
1149 assert!(debug_str.contains("AggregateResult"));
1150 }
1151
1152 #[test]
1153 fn test_aggregate_result_clone() {
1154 let mut result = AggregateResult {
1155 count: Some(42),
1156 ..AggregateResult::default()
1157 };
1158 result.sum.insert("amount".into(), 1000.0);
1159
1160 let cloned = result.clone();
1161 assert_eq!(cloned.count, Some(42));
1162 assert_eq!(cloned.sum.get("amount"), Some(&1000.0));
1163 }
1164
1165 #[test]
1168 fn test_aggregate_operation_new() {
1169 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new();
1170 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1171
1172 assert!(sql.contains("COUNT(*)"));
1174 assert!(params.is_empty());
1175 }
1176
1177 #[test]
1178 fn test_aggregate_operation_default() {
1179 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::default();
1180 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1181
1182 assert!(sql.contains("COUNT(*)"));
1183 assert!(params.is_empty());
1184 }
1185
1186 #[test]
1187 fn test_aggregate_operation_build_sql() {
1188 let op: AggregateOperation<TestModel, MockEngine> =
1189 AggregateOperation::new().count().sum("score").avg("age");
1190
1191 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1192
1193 assert!(sql.contains("SELECT"));
1194 assert!(sql.contains("COUNT(*)"));
1195 assert!(sql.contains("SUM(score)"));
1196 assert!(sql.contains("AVG(age)"));
1197 assert!(sql.contains("FROM test_models"));
1198 assert!(params.is_empty());
1199 }
1200
1201 #[test]
1202 fn test_aggregate_operation_count_column() {
1203 let op: AggregateOperation<TestModel, MockEngine> =
1204 AggregateOperation::new().count_column("email");
1205
1206 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1207
1208 assert!(sql.contains("COUNT(email)"));
1209 }
1210
1211 #[test]
1212 fn test_aggregate_operation_count_distinct() {
1213 let op: AggregateOperation<TestModel, MockEngine> =
1214 AggregateOperation::new().count_distinct("email");
1215
1216 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1217
1218 assert!(sql.contains("COUNT(DISTINCT email)"));
1219 }
1220
1221 #[test]
1222 fn test_aggregate_operation_min_max() {
1223 let op: AggregateOperation<TestModel, MockEngine> =
1224 AggregateOperation::new().min("age").max("age");
1225
1226 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1227
1228 assert!(sql.contains("MIN(age)"));
1229 assert!(sql.contains("MAX(age)"));
1230 }
1231
1232 #[test]
1233 fn test_aggregate_with_where() {
1234 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1235 .count()
1236 .r#where(Filter::Gt("age".into(), FilterValue::Int(18)));
1237
1238 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1239
1240 assert!(sql.contains("WHERE"));
1241 assert!(sql.contains("age")); assert!(sql.contains(">"));
1243 assert!(!params.is_empty());
1244 }
1245
1246 #[test]
1247 fn test_aggregate_with_complex_filter() {
1248 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1249 .sum("score")
1250 .avg("age")
1251 .r#where(Filter::and([
1252 Filter::Gte("age".into(), FilterValue::Int(18)),
1253 Filter::Equals("active".into(), FilterValue::Bool(true)),
1254 ]));
1255
1256 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1257
1258 assert!(sql.contains("WHERE"));
1259 assert!(sql.contains("AND"));
1260 assert_eq!(params.len(), 2);
1261 }
1262
1263 #[test]
1264 fn test_aggregate_all_methods() {
1265 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1266 .count()
1267 .count_column("name")
1268 .count_distinct("email")
1269 .sum("score")
1270 .avg("score")
1271 .min("age")
1272 .max("age");
1273
1274 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1275
1276 assert!(sql.contains("COUNT(*)"));
1277 assert!(sql.contains("COUNT(name)"));
1278 assert!(sql.contains("COUNT(DISTINCT email)"));
1279 assert!(sql.contains("SUM(score)"));
1280 assert!(sql.contains("AVG(score)"));
1281 assert!(sql.contains("MIN(age)"));
1282 assert!(sql.contains("MAX(age)"));
1283 }
1284
1285 #[tokio::test]
1286 async fn test_aggregate_exec_without_engine_errors() {
1287 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new().count();
1290 let err = op.exec().await.unwrap_err();
1291 assert!(err.to_string().contains("without an engine"));
1292 }
1293
1294 #[tokio::test]
1295 async fn test_aggregate_exec_with_engine_ok() {
1296 let op: AggregateOperation<TestModel, MockEngine> =
1300 AggregateOperation::with_engine(MockEngine).count();
1301 let err = op.exec().await.unwrap_err();
1302 assert!(err.to_string().contains("aggregate_query"));
1303 }
1304
1305 #[test]
1308 fn test_group_by_new() {
1309 let op: GroupByOperation<TestModel, MockEngine> =
1310 GroupByOperation::new(vec!["department".into()]);
1311
1312 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1313
1314 assert!(sql.contains("GROUP BY department"));
1315 }
1316
1317 #[test]
1318 fn test_group_by_build_sql() {
1319 let op: GroupByOperation<TestModel, MockEngine> =
1320 GroupByOperation::new(vec!["name".to_string()])
1321 .count()
1322 .avg("score");
1323
1324 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1325
1326 assert!(sql.contains("SELECT"));
1327 assert!(sql.contains("name")); assert!(sql.contains("COUNT(*)"));
1329 assert!(sql.contains("AVG(score)"));
1330 assert!(sql.contains("GROUP BY name"));
1331 assert!(params.is_empty());
1332 }
1333
1334 #[test]
1335 fn test_group_by_multiple_columns() {
1336 let op: GroupByOperation<TestModel, MockEngine> =
1337 GroupByOperation::new(vec!["department".into(), "role".into()]).count();
1338
1339 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1340
1341 assert!(sql.contains("GROUP BY department, role"));
1342 }
1343
1344 #[test]
1345 fn test_group_by_with_sum() {
1346 let op: GroupByOperation<TestModel, MockEngine> =
1347 GroupByOperation::new(vec!["category".into()]).sum("amount");
1348
1349 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1350
1351 assert!(sql.contains("SUM(amount)"));
1352 }
1353
1354 #[test]
1355 fn test_group_by_with_min_max() {
1356 let op: GroupByOperation<TestModel, MockEngine> =
1357 GroupByOperation::new(vec!["category".into()])
1358 .min("price")
1359 .max("price");
1360
1361 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1362
1363 assert!(sql.contains("MIN(price)"));
1364 assert!(sql.contains("MAX(price)"));
1365 }
1366
1367 #[test]
1368 fn test_group_by_with_where() {
1369 let op: GroupByOperation<TestModel, MockEngine> =
1370 GroupByOperation::new(vec!["department".into()])
1371 .count()
1372 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)));
1373
1374 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1375
1376 assert!(sql.contains("WHERE"));
1377 assert!(sql.contains("GROUP BY"));
1378 assert_eq!(params.len(), 1);
1379 }
1380
1381 #[test]
1382 fn test_group_by_with_having() {
1383 let op: GroupByOperation<TestModel, MockEngine> =
1384 GroupByOperation::new(vec!["name".to_string()])
1385 .count()
1386 .having(having::count_gt(5.0));
1387
1388 let (sql, _params) = op.build_sql(&crate::dialect::Postgres);
1389
1390 assert!(sql.contains("HAVING COUNT(*) > 5"));
1391 }
1392
1393 #[test]
1394 fn test_group_by_with_order_and_limit() {
1395 let op: GroupByOperation<TestModel, MockEngine> =
1396 GroupByOperation::new(vec!["name".to_string()])
1397 .count()
1398 .order_by(OrderByField::desc("_count"))
1399 .take(10)
1400 .skip(5);
1401
1402 let (sql, _params) = op.build_sql(&crate::dialect::Postgres);
1403
1404 assert!(sql.contains("ORDER BY _count DESC")); assert!(sql.contains("LIMIT 10"));
1406 assert!(sql.contains("OFFSET 5"));
1407 }
1408
1409 #[test]
1410 fn test_group_by_order_with_nulls() {
1411 let op: GroupByOperation<TestModel, MockEngine> =
1412 GroupByOperation::new(vec!["department".into()])
1413 .count()
1414 .order_by(OrderByField::asc("name").nulls(NullsOrder::First));
1415
1416 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1417
1418 assert!(sql.contains("ORDER BY"));
1419 assert!(sql.contains("NULLS FIRST"));
1420 }
1421
1422 #[test]
1423 fn test_group_by_skip_only() {
1424 let op: GroupByOperation<TestModel, MockEngine> =
1425 GroupByOperation::new(vec!["department".into()])
1426 .count()
1427 .skip(20);
1428
1429 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1430
1431 assert!(sql.contains("OFFSET 20"));
1432 assert!(!sql.contains("LIMIT"));
1433 }
1434
1435 #[test]
1436 fn test_group_by_take_only() {
1437 let op: GroupByOperation<TestModel, MockEngine> =
1438 GroupByOperation::new(vec!["department".into()])
1439 .count()
1440 .take(50);
1441
1442 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1443
1444 assert!(sql.contains("LIMIT 50"));
1445 assert!(!sql.contains("OFFSET"));
1446 }
1447
1448 #[tokio::test]
1449 async fn test_group_by_exec_without_engine_errors() {
1450 let op: GroupByOperation<TestModel, MockEngine> =
1451 GroupByOperation::new(vec!["department".into()]).count();
1452 let err = op.exec().await.unwrap_err();
1453 assert!(err.to_string().contains("without an engine"));
1454 }
1455
1456 #[tokio::test]
1457 async fn test_group_by_exec_with_engine_ok() {
1458 let op: GroupByOperation<TestModel, MockEngine> =
1459 GroupByOperation::with_engine(MockEngine, vec!["department".into()]).count();
1460 let err = op.exec().await.unwrap_err();
1461 assert!(err.to_string().contains("aggregate_query"));
1462 }
1463
1464 #[test]
1467 fn test_having_op_as_str() {
1468 assert_eq!(HavingOp::Gt.as_str(), ">");
1469 assert_eq!(HavingOp::Gte.as_str(), ">=");
1470 assert_eq!(HavingOp::Lt.as_str(), "<");
1471 assert_eq!(HavingOp::Lte.as_str(), "<=");
1472 assert_eq!(HavingOp::Eq.as_str(), "=");
1473 assert_eq!(HavingOp::Ne.as_str(), "<>");
1474 }
1475
1476 #[test]
1479 fn test_having_condition_debug() {
1480 let cond = HavingCondition {
1481 field: AggregateField::CountAll,
1482 op: HavingOp::Gt,
1483 value: 10.0,
1484 };
1485 let debug_str = format!("{:?}", cond);
1486 assert!(debug_str.contains("HavingCondition"));
1487 }
1488
1489 #[test]
1490 fn test_having_condition_clone() {
1491 let cond = HavingCondition {
1492 field: AggregateField::Sum("amount".into()),
1493 op: HavingOp::Gte,
1494 value: 1000.0,
1495 };
1496 let cloned = cond.clone();
1497 assert!((cloned.value - 1000.0).abs() < f64::EPSILON);
1498 }
1499
1500 #[test]
1503 fn test_having_helpers() {
1504 let cond = having::count_gt(10.0);
1505 assert!(matches!(cond.field, AggregateField::CountAll));
1506 assert!(matches!(cond.op, HavingOp::Gt));
1507 assert!((cond.value - 10.0).abs() < f64::EPSILON);
1508
1509 let cond = having::sum_gt("amount", 1000.0);
1510 if let AggregateField::Sum(col) = cond.field {
1511 assert_eq!(col, "amount");
1512 } else {
1513 panic!("Expected Sum");
1514 }
1515 }
1516
1517 #[test]
1518 fn test_having_count_gte() {
1519 let cond = having::count_gte(5.0);
1520 assert!(matches!(cond.field, AggregateField::CountAll));
1521 assert!(matches!(cond.op, HavingOp::Gte));
1522 assert!((cond.value - 5.0).abs() < f64::EPSILON);
1523 }
1524
1525 #[test]
1526 fn test_having_count_lt() {
1527 let cond = having::count_lt(100.0);
1528 assert!(matches!(cond.field, AggregateField::CountAll));
1529 assert!(matches!(cond.op, HavingOp::Lt));
1530 assert!((cond.value - 100.0).abs() < f64::EPSILON);
1531 }
1532
1533 #[test]
1534 fn test_having_avg_gt() {
1535 let cond = having::avg_gt("score", 75.5);
1536 assert!(matches!(cond.op, HavingOp::Gt));
1537 assert!((cond.value - 75.5).abs() < f64::EPSILON);
1538 if let AggregateField::Avg(col) = cond.field {
1539 assert_eq!(col, "score");
1540 } else {
1541 panic!("Expected Avg");
1542 }
1543 }
1544
1545 #[test]
1546 fn test_having_sum_gt_with_different_columns() {
1547 let cond1 = having::sum_gt("revenue", 50000.0);
1548 let cond2 = having::sum_gt("cost", 10000.0);
1549
1550 if let AggregateField::Sum(col) = &cond1.field {
1551 assert_eq!(col, "revenue");
1552 }
1553 if let AggregateField::Sum(col) = &cond2.field {
1554 assert_eq!(col, "cost");
1555 }
1556 }
1557
1558 #[test]
1561 fn test_group_by_result_debug() {
1562 let result = GroupByResult {
1563 group_values: std::collections::HashMap::new(),
1564 aggregates: AggregateResult::default(),
1565 };
1566 let debug_str = format!("{:?}", result);
1567 assert!(debug_str.contains("GroupByResult"));
1568 }
1569
1570 #[test]
1571 fn test_group_by_result_clone() {
1572 let mut result = GroupByResult {
1573 group_values: std::collections::HashMap::new(),
1574 aggregates: AggregateResult::default(),
1575 };
1576 result
1577 .group_values
1578 .insert("category".into(), serde_json::json!("electronics"));
1579 result.aggregates.count = Some(50);
1580
1581 let cloned = result.clone();
1582 assert_eq!(cloned.aggregates.count, Some(50));
1583 assert!(cloned.group_values.contains_key("category"));
1584 }
1585
1586 #[test]
1589 fn test_group_by_sql_structure() {
1590 let op: GroupByOperation<TestModel, MockEngine> =
1591 GroupByOperation::new(vec!["department".into()])
1592 .count()
1593 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)))
1594 .having(having::count_gt(5.0))
1595 .order_by(OrderByField::desc("_count"))
1596 .take(10)
1597 .skip(5);
1598
1599 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1600
1601 let select_pos = sql.find("SELECT").unwrap();
1603 let from_pos = sql.find("FROM").unwrap();
1604 let where_pos = sql.find("WHERE").unwrap();
1605 let group_pos = sql.find("GROUP BY").unwrap();
1606 let having_pos = sql.find("HAVING").unwrap();
1607 let order_pos = sql.find("ORDER BY").unwrap();
1608 let limit_pos = sql.find("LIMIT").unwrap();
1609 let offset_pos = sql.find("OFFSET").unwrap();
1610
1611 assert!(select_pos < from_pos);
1612 assert!(from_pos < where_pos);
1613 assert!(where_pos < group_pos);
1614 assert!(group_pos < having_pos);
1615 assert!(having_pos < order_pos);
1616 assert!(order_pos < limit_pos);
1617 assert!(limit_pos < offset_pos);
1618 }
1619
1620 #[test]
1621 fn test_aggregate_no_group_by() {
1622 let op: AggregateOperation<TestModel, MockEngine> =
1623 AggregateOperation::new().count().sum("score");
1624
1625 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1626
1627 assert!(!sql.contains("GROUP BY"));
1628 }
1629
1630 #[test]
1631 fn test_group_by_empty_columns() {
1632 let op: GroupByOperation<TestModel, MockEngine> = GroupByOperation::new(vec![]).count();
1633
1634 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1635
1636 assert!(!sql.contains("GROUP BY"));
1638 }
1639
1640 #[test]
1641 fn group_by_build_sql_emits_count_column_and_distinct() {
1642 let op: GroupByOperation<TestModel, MockEngine> =
1643 GroupByOperation::new(vec!["team_id".to_string()])
1644 .count_column("email")
1645 .count_distinct("region");
1646 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1647 assert!(sql.contains("COUNT(email) AS _count_email"), "got: {sql}");
1648 assert!(
1649 sql.contains("COUNT(DISTINCT region) AS _count_distinct_region"),
1650 "got: {sql}"
1651 );
1652 }
1653
1654 #[test]
1655 fn test_having_count_lte_eq_ne() {
1656 let c = having::count_lte(10.0);
1657 assert!(matches!(c.field, AggregateField::CountAll));
1658 assert!(matches!(c.op, HavingOp::Lte));
1659 assert_eq!(c.value, 10.0);
1660
1661 let c = having::count_eq(5.0);
1662 assert!(matches!(c.op, HavingOp::Eq));
1663
1664 let c = having::count_ne(0.0);
1665 assert!(matches!(c.op, HavingOp::Ne));
1666 }
1667
1668 #[test]
1669 fn test_having_sum_variants() {
1670 let c = having::sum_gte("views", 100.0);
1671 assert!(matches!(&c.field, AggregateField::Sum(col) if col == "views"));
1672 assert!(matches!(c.op, HavingOp::Gte));
1673
1674 let c = having::sum_lt("views", 50.0);
1675 assert!(matches!(c.op, HavingOp::Lt));
1676
1677 let c = having::sum_lte("views", 50.0);
1678 assert!(matches!(c.op, HavingOp::Lte));
1679
1680 let c = having::sum_eq("views", 0.0);
1681 assert!(matches!(c.op, HavingOp::Eq));
1682
1683 let c = having::sum_ne("views", 0.0);
1684 assert!(matches!(c.op, HavingOp::Ne));
1685 }
1686
1687 #[test]
1688 fn test_having_avg_variants() {
1689 let c = having::avg_gte("score", 3.5);
1690 assert!(matches!(&c.field, AggregateField::Avg(col) if col == "score"));
1691 assert!(matches!(c.op, HavingOp::Gte));
1692
1693 let c = having::avg_lt("score", 2.0);
1694 assert!(matches!(c.op, HavingOp::Lt));
1695
1696 let c = having::avg_lte("score", 2.0);
1697 assert!(matches!(c.op, HavingOp::Lte));
1698
1699 let c = having::avg_eq("score", 5.0);
1700 assert!(matches!(c.op, HavingOp::Eq));
1701
1702 let c = having::avg_ne("score", 0.0);
1703 assert!(matches!(c.op, HavingOp::Ne));
1704 }
1705
1706 #[test]
1707 fn test_having_min_variants() {
1708 let c = having::min_gt("age", 18.0);
1709 assert!(matches!(&c.field, AggregateField::Min(col) if col == "age"));
1710 assert!(matches!(c.op, HavingOp::Gt));
1711
1712 let c = having::min_gte("age", 18.0);
1713 assert!(matches!(c.op, HavingOp::Gte));
1714
1715 let c = having::min_lt("age", 65.0);
1716 assert!(matches!(c.op, HavingOp::Lt));
1717
1718 let c = having::min_lte("age", 65.0);
1719 assert!(matches!(c.op, HavingOp::Lte));
1720
1721 let c = having::min_eq("age", 21.0);
1722 assert!(matches!(c.op, HavingOp::Eq));
1723
1724 let c = having::min_ne("age", 0.0);
1725 assert!(matches!(c.op, HavingOp::Ne));
1726 }
1727
1728 #[test]
1729 fn test_having_max_variants() {
1730 let c = having::max_gt("salary", 50000.0);
1731 assert!(matches!(&c.field, AggregateField::Max(col) if col == "salary"));
1732 assert!(matches!(c.op, HavingOp::Gt));
1733
1734 let c = having::max_gte("salary", 50000.0);
1735 assert!(matches!(c.op, HavingOp::Gte));
1736
1737 let c = having::max_lt("salary", 200000.0);
1738 assert!(matches!(c.op, HavingOp::Lt));
1739
1740 let c = having::max_lte("salary", 200000.0);
1741 assert!(matches!(c.op, HavingOp::Lte));
1742
1743 let c = having::max_eq("salary", 100000.0);
1744 assert!(matches!(c.op, HavingOp::Eq));
1745
1746 let c = having::max_ne("salary", 0.0);
1747 assert!(matches!(c.op, HavingOp::Ne));
1748 }
1749
1750 #[test]
1751 fn from_row_hydrates_per_column_and_distinct_counts() {
1752 use crate::filter::FilterValue;
1753 use std::collections::HashMap;
1754 let mut row = HashMap::new();
1755 row.insert("_count".to_string(), FilterValue::Int(5));
1756 row.insert("_count_email".to_string(), FilterValue::Int(3));
1757 row.insert("_count_distinct_email".to_string(), FilterValue::Int(2));
1758 let r = AggregateResult::from_row(row);
1759 assert_eq!(r.count, Some(5));
1760 assert_eq!(r.count_of("email"), Some(3));
1761 assert_eq!(r.count_distinct_of("email"), Some(2));
1762 assert_eq!(r.count_columns.get("distinct_email"), None);
1765 }
1766}