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 to_sql_dialect(&self, dialect: &dyn crate::dialect::SqlDialect) -> String {
96 match self {
97 Self::CountAll => "COUNT(*)".to_string(),
98 Self::CountColumn(col) => format!("COUNT({})", dialect.quote_ident(col)),
99 Self::CountDistinct(col) => {
100 format!("COUNT(DISTINCT {})", dialect.quote_ident(col))
101 }
102 Self::Sum(col) => format!("SUM({})", dialect.quote_ident(col)),
103 Self::Avg(col) => format!("AVG({})", dialect.quote_ident(col)),
104 Self::Min(col) => format!("MIN({})", dialect.quote_ident(col)),
105 Self::Max(col) => format!("MAX({})", dialect.quote_ident(col)),
106 }
107 }
108
109 pub fn alias(&self) -> String {
111 match self {
112 Self::CountAll => "_count".to_string(),
113 Self::CountColumn(col) => format!("_count_{}", col),
114 Self::CountDistinct(col) => format!("_count_distinct_{}", col),
115 Self::Sum(col) => format!("_sum_{}", col),
116 Self::Avg(col) => format!("_avg_{}", col),
117 Self::Min(col) => format!("_min_{}", col),
118 Self::Max(col) => format!("_max_{}", col),
119 }
120 }
121}
122
123#[derive(Debug, Clone, Default)]
131pub struct AggregateResult {
132 pub count: Option<i64>,
134 pub count_columns: std::collections::HashMap<String, i64>,
136 pub count_distinct: std::collections::HashMap<String, i64>,
138 pub sum: std::collections::HashMap<String, f64>,
140 pub avg: std::collections::HashMap<String, f64>,
142 pub min: std::collections::HashMap<String, serde_json::Value>,
144 pub max: std::collections::HashMap<String, serde_json::Value>,
146}
147
148impl AggregateResult {
149 pub fn from_row(row: std::collections::HashMap<String, crate::filter::FilterValue>) -> Self {
160 use crate::filter::FilterValue;
161 let mut out = Self::default();
162 for (k, v) in row {
163 if k == "_count" {
164 if let FilterValue::Int(n) = v {
165 out.count = Some(n);
166 }
167 } else if let Some(col) = k.strip_prefix("_count_distinct_") {
168 if let Some(n) = value_to_i64(&v) {
169 out.count_distinct.insert(col.to_string(), n);
170 }
171 } else if let Some(col) = k.strip_prefix("_count_") {
172 if let Some(n) = value_to_i64(&v) {
173 out.count_columns.insert(col.to_string(), n);
174 }
175 } else if let Some(col) = k.strip_prefix("_sum_") {
176 if let Some(f) = value_to_f64(&v) {
177 out.sum.insert(col.to_string(), f);
178 }
179 } else if let Some(col) = k.strip_prefix("_avg_") {
180 if let Some(f) = value_to_f64(&v) {
181 out.avg.insert(col.to_string(), f);
182 }
183 } else if let Some(col) = k.strip_prefix("_min_") {
184 out.min.insert(col.to_string(), filter_value_to_json(&v));
185 } else if let Some(col) = k.strip_prefix("_max_") {
186 out.max.insert(col.to_string(), filter_value_to_json(&v));
187 }
188 }
189 out
190 }
191
192 pub fn count_of(&self, column: &str) -> Option<i64> {
194 self.count_columns.get(column).copied()
195 }
196
197 pub fn count_distinct_of(&self, column: &str) -> Option<i64> {
199 self.count_distinct.get(column).copied()
200 }
201
202 pub fn sum_as_f64(&self, column: &str) -> Option<f64> {
204 self.sum.get(column).copied()
205 }
206
207 pub fn avg_as_f64(&self, column: &str) -> Option<f64> {
209 self.avg.get(column).copied()
210 }
211
212 pub fn min_as_f64(&self, column: &str) -> Option<f64> {
215 self.min.get(column).and_then(|v| v.as_f64())
216 }
217
218 pub fn max_as_f64(&self, column: &str) -> Option<f64> {
221 self.max.get(column).and_then(|v| v.as_f64())
222 }
223}
224
225fn value_to_i64(v: &crate::filter::FilterValue) -> Option<i64> {
226 use crate::filter::FilterValue;
227 match v {
228 FilterValue::Int(n) => Some(*n),
229 FilterValue::String(s) => s.parse::<i64>().ok(),
230 _ => None,
231 }
232}
233
234fn value_to_f64(v: &crate::filter::FilterValue) -> Option<f64> {
235 use crate::filter::FilterValue;
236 match v {
237 FilterValue::Int(n) => Some(*n as f64),
238 FilterValue::Float(f) => Some(*f),
239 FilterValue::String(s) => s.parse::<f64>().ok(),
240 _ => None,
241 }
242}
243
244fn filter_value_to_json(v: &crate::filter::FilterValue) -> serde_json::Value {
245 use crate::filter::FilterValue;
246 match v {
247 FilterValue::Null => serde_json::Value::Null,
248 FilterValue::Bool(b) => serde_json::Value::Bool(*b),
249 FilterValue::Int(n) => serde_json::Value::from(*n),
250 FilterValue::Float(f) => serde_json::Number::from_f64(*f)
251 .map(serde_json::Value::Number)
252 .unwrap_or(serde_json::Value::Null),
253 FilterValue::String(s) => serde_json::Value::String(s.clone()),
254 FilterValue::Json(j) => j.clone(),
255 FilterValue::List(_) => serde_json::Value::Null,
256 }
257}
258
259#[derive(Debug)]
271pub struct AggregateOperation<M: Model, E: QueryEngine> {
272 _model: PhantomData<M>,
274 engine: Option<E>,
277 fields: Vec<AggregateField>,
279 filter: Option<Filter>,
281}
282
283impl<M: Model, E: QueryEngine> AggregateOperation<M, E> {
284 pub fn new() -> Self {
289 Self {
290 _model: PhantomData,
291 engine: None,
292 fields: Vec::new(),
293 filter: None,
294 }
295 }
296
297 pub fn with_engine(engine: E) -> Self {
302 Self {
303 _model: PhantomData,
304 engine: Some(engine),
305 fields: Vec::new(),
306 filter: None,
307 }
308 }
309
310 pub fn count(mut self) -> Self {
312 self.fields.push(AggregateField::CountAll);
313 self
314 }
315
316 pub fn count_column(mut self, column: impl Into<String>) -> Self {
318 self.fields.push(AggregateField::CountColumn(column.into()));
319 self
320 }
321
322 pub fn count_distinct(mut self, column: impl Into<String>) -> Self {
324 self.fields
325 .push(AggregateField::CountDistinct(column.into()));
326 self
327 }
328
329 pub fn sum(mut self, column: impl Into<String>) -> Self {
331 self.fields.push(AggregateField::Sum(column.into()));
332 self
333 }
334
335 pub fn avg(mut self, column: impl Into<String>) -> Self {
337 self.fields.push(AggregateField::Avg(column.into()));
338 self
339 }
340
341 pub fn min(mut self, column: impl Into<String>) -> Self {
343 self.fields.push(AggregateField::Min(column.into()));
344 self
345 }
346
347 pub fn max(mut self, column: impl Into<String>) -> Self {
349 self.fields.push(AggregateField::Max(column.into()));
350 self
351 }
352
353 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
355 let new_filter = filter.into();
356 self.filter = Some(match self.filter.take() {
357 Some(existing) => existing.and_then(new_filter),
358 None => new_filter,
359 });
360 self
361 }
362
363 pub fn with_where_input<W: crate::inputs::WhereInput<Model = M>>(mut self, w: W) -> Self {
365 let f = w.into_ir();
366 self.filter = Some(match self.filter.take() {
367 Some(existing) => existing.and_then(f),
368 None => f,
369 });
370 self
371 }
372
373 pub fn build_sql(
375 &self,
376 dialect: &dyn crate::dialect::SqlDialect,
377 ) -> (String, Vec<crate::filter::FilterValue>) {
378 let mut params = Vec::new();
379
380 let fields = if self.fields.is_empty() {
382 vec![AggregateField::CountAll]
383 } else {
384 self.fields.clone()
385 };
386
387 let select_parts: Vec<String> = fields
388 .iter()
389 .map(|f| {
390 format!(
391 "{} AS {}",
392 f.to_sql_dialect(dialect),
393 dialect.quote_ident(&f.alias())
394 )
395 })
396 .collect();
397
398 let mut sql = format!(
399 "SELECT {} FROM {}",
400 select_parts.join(", "),
401 dialect.quote_ident(M::TABLE_NAME)
402 );
403
404 if let Some(filter) = &self.filter {
406 let (where_sql, where_params) = filter.to_sql(params.len(), dialect);
407 sql.push_str(&format!(" WHERE {}", where_sql));
408 params.extend(where_params);
409 }
410
411 (sql, params)
412 }
413
414 pub async fn exec(self) -> QueryResult<AggregateResult> {
425 let engine = self.engine.as_ref().ok_or_else(|| {
426 crate::error::QueryError::internal(
427 "AggregateOperation::exec called on a builder without an engine; \
428 use Client<E>::aggregate() (which calls with_engine) instead of \
429 AggregateOperation::new()",
430 )
431 })?;
432 let dialect = engine.dialect();
433 let (sql, params) = self.build_sql(dialect);
434 let mut rows = engine.aggregate_query(&sql, params).await?;
435 Ok(AggregateResult::from_row(rows.pop().unwrap_or_default()))
436 }
437}
438
439impl<M: Model, E: QueryEngine> Default for AggregateOperation<M, E> {
440 fn default() -> Self {
441 Self::new()
442 }
443}
444
445#[derive(Debug)]
454pub struct GroupByOperation<M: Model, E: QueryEngine> {
455 _model: PhantomData<M>,
457 engine: Option<E>,
460 group_columns: Vec<String>,
462 agg_fields: Vec<AggregateField>,
464 filter: Option<Filter>,
466 having: Option<HavingCondition>,
468 order_by: Vec<OrderByField>,
470 skip: Option<usize>,
472 take: Option<usize>,
474}
475
476#[derive(Debug, Clone)]
478pub struct HavingCondition {
479 pub field: AggregateField,
481 pub op: HavingOp,
483 pub value: f64,
485}
486
487#[derive(Debug, Clone, Copy)]
489pub enum HavingOp {
490 Gt,
492 Gte,
494 Lt,
496 Lte,
498 Eq,
500 Ne,
502}
503
504impl HavingOp {
505 pub fn as_str(&self) -> &'static str {
507 match self {
508 Self::Gt => ">",
509 Self::Gte => ">=",
510 Self::Lt => "<",
511 Self::Lte => "<=",
512 Self::Eq => "=",
513 Self::Ne => "<>",
514 }
515 }
516}
517
518impl<M: Model, E: QueryEngine> GroupByOperation<M, E> {
519 pub fn new(columns: Vec<String>) -> Self {
524 Self {
525 _model: PhantomData,
526 engine: None,
527 group_columns: columns,
528 agg_fields: Vec::new(),
529 filter: None,
530 having: None,
531 order_by: Vec::new(),
532 skip: None,
533 take: None,
534 }
535 }
536
537 pub fn with_engine(engine: E, columns: Vec<String>) -> Self {
542 Self {
543 _model: PhantomData,
544 engine: Some(engine),
545 group_columns: columns,
546 agg_fields: Vec::new(),
547 filter: None,
548 having: None,
549 order_by: Vec::new(),
550 skip: None,
551 take: None,
552 }
553 }
554
555 pub fn count(mut self) -> Self {
557 self.agg_fields.push(AggregateField::CountAll);
558 self
559 }
560
561 pub fn count_column(mut self, column: impl Into<String>) -> Self {
563 self.agg_fields
564 .push(AggregateField::CountColumn(column.into()));
565 self
566 }
567
568 pub fn count_distinct(mut self, column: impl Into<String>) -> Self {
570 self.agg_fields
571 .push(AggregateField::CountDistinct(column.into()));
572 self
573 }
574
575 pub fn sum(mut self, column: impl Into<String>) -> Self {
577 self.agg_fields.push(AggregateField::Sum(column.into()));
578 self
579 }
580
581 pub fn avg(mut self, column: impl Into<String>) -> Self {
583 self.agg_fields.push(AggregateField::Avg(column.into()));
584 self
585 }
586
587 pub fn min(mut self, column: impl Into<String>) -> Self {
589 self.agg_fields.push(AggregateField::Min(column.into()));
590 self
591 }
592
593 pub fn max(mut self, column: impl Into<String>) -> Self {
595 self.agg_fields.push(AggregateField::Max(column.into()));
596 self
597 }
598
599 pub fn r#where(mut self, filter: impl Into<Filter>) -> Self {
601 let new_filter = filter.into();
602 self.filter = Some(match self.filter.take() {
603 Some(existing) => existing.and_then(new_filter),
604 None => new_filter,
605 });
606 self
607 }
608
609 pub fn having(mut self, condition: HavingCondition) -> Self {
616 assert!(
617 condition.value.is_finite(),
618 "HAVING condition value must be finite, got {}",
619 condition.value
620 );
621 self.having = Some(condition);
622 self
623 }
624
625 pub fn order_by(mut self, order: impl Into<OrderByField>) -> Self {
627 self.order_by.push(order.into());
628 self
629 }
630
631 pub fn skip(mut self, count: usize) -> Self {
633 self.skip = Some(count);
634 self
635 }
636
637 pub fn take(mut self, count: usize) -> Self {
639 self.take = Some(count);
640 self
641 }
642
643 pub fn build_sql(
645 &self,
646 dialect: &dyn crate::dialect::SqlDialect,
647 ) -> (String, Vec<crate::filter::FilterValue>) {
648 let mut params = Vec::new();
649
650 let mut select_parts: Vec<String> = self
652 .group_columns
653 .iter()
654 .map(|c| dialect.quote_ident(c))
655 .collect();
656
657 for field in &self.agg_fields {
658 select_parts.push(format!(
659 "{} AS {}",
660 field.to_sql_dialect(dialect),
661 dialect.quote_ident(&field.alias())
662 ));
663 }
664
665 let mut sql = format!(
666 "SELECT {} FROM {}",
667 select_parts.join(", "),
668 dialect.quote_ident(M::TABLE_NAME)
669 );
670
671 if let Some(filter) = &self.filter {
673 let (where_sql, where_params) = filter.to_sql(params.len(), dialect);
674 sql.push_str(&format!(" WHERE {}", where_sql));
675 params.extend(where_params);
676 }
677
678 if !self.group_columns.is_empty() {
680 let group_cols: Vec<String> = self
681 .group_columns
682 .iter()
683 .map(|c| dialect.quote_ident(c))
684 .collect();
685 sql.push_str(&format!(" GROUP BY {}", group_cols.join(", ")));
686 }
687
688 if let Some(having) = &self.having {
691 params.push(crate::filter::FilterValue::Float(having.value));
692 sql.push_str(&format!(
693 " HAVING {} {} {}",
694 having.field.to_sql_dialect(dialect),
695 having.op.as_str(),
696 dialect.placeholder(params.len())
697 ));
698 }
699
700 if !self.order_by.is_empty() {
702 let order_parts: Vec<String> = self
703 .order_by
704 .iter()
705 .map(|o| {
706 let mut part =
707 format!("{} {}", dialect.quote_ident(&o.column), o.order.as_sql());
708 if let Some(nulls) = o.nulls {
709 part.push(' ');
710 part.push_str(nulls.as_sql());
711 }
712 part
713 })
714 .collect();
715 sql.push_str(&format!(" ORDER BY {}", order_parts.join(", ")));
716 }
717
718 if let Some(take) = self.take {
720 sql.push_str(&format!(" LIMIT {}", take));
721 }
722 if let Some(skip) = self.skip {
723 sql.push_str(&format!(" OFFSET {}", skip));
724 }
725
726 (sql, params)
727 }
728
729 pub async fn exec(self) -> QueryResult<Vec<GroupByResult>> {
741 let engine = self.engine.as_ref().ok_or_else(|| {
742 crate::error::QueryError::internal(
743 "GroupByOperation::exec called on a builder without an engine; \
744 use Client<E>::group_by() (which calls with_engine) instead of \
745 GroupByOperation::new()",
746 )
747 })?;
748 let dialect = engine.dialect();
749 let group_columns = self.group_columns.clone();
750 let (sql, params) = self.build_sql(dialect);
751 let rows = engine.aggregate_query(&sql, params).await?;
752 Ok(rows
753 .into_iter()
754 .map(|row| {
755 let mut group_values = std::collections::HashMap::new();
756 let mut agg_map = std::collections::HashMap::new();
757 for (k, v) in row {
758 if group_columns.iter().any(|c| c == &k) {
759 group_values.insert(k, filter_value_to_json(&v));
760 } else {
761 agg_map.insert(k, v);
762 }
763 }
764 GroupByResult {
765 group_values,
766 aggregates: AggregateResult::from_row(agg_map),
767 }
768 })
769 .collect())
770 }
771}
772
773#[derive(Debug, Clone)]
775pub struct GroupByResult {
776 pub group_values: std::collections::HashMap<String, serde_json::Value>,
778 pub aggregates: AggregateResult,
780}
781
782pub mod having {
784 use super::*;
785
786 pub fn count_gt(value: f64) -> HavingCondition {
788 HavingCondition {
789 field: AggregateField::CountAll,
790 op: HavingOp::Gt,
791 value,
792 }
793 }
794
795 pub fn count_gte(value: f64) -> HavingCondition {
797 HavingCondition {
798 field: AggregateField::CountAll,
799 op: HavingOp::Gte,
800 value,
801 }
802 }
803
804 pub fn count_lt(value: f64) -> HavingCondition {
806 HavingCondition {
807 field: AggregateField::CountAll,
808 op: HavingOp::Lt,
809 value,
810 }
811 }
812
813 pub fn count_lte(value: f64) -> HavingCondition {
814 HavingCondition {
815 field: AggregateField::CountAll,
816 op: HavingOp::Lte,
817 value,
818 }
819 }
820
821 pub fn count_eq(value: f64) -> HavingCondition {
822 HavingCondition {
823 field: AggregateField::CountAll,
824 op: HavingOp::Eq,
825 value,
826 }
827 }
828
829 pub fn count_ne(value: f64) -> HavingCondition {
830 HavingCondition {
831 field: AggregateField::CountAll,
832 op: HavingOp::Ne,
833 value,
834 }
835 }
836
837 pub fn sum_gt(column: impl Into<String>, value: f64) -> HavingCondition {
838 HavingCondition {
839 field: AggregateField::Sum(column.into()),
840 op: HavingOp::Gt,
841 value,
842 }
843 }
844
845 pub fn sum_gte(column: impl Into<String>, value: f64) -> HavingCondition {
846 HavingCondition {
847 field: AggregateField::Sum(column.into()),
848 op: HavingOp::Gte,
849 value,
850 }
851 }
852
853 pub fn sum_lt(column: impl Into<String>, value: f64) -> HavingCondition {
854 HavingCondition {
855 field: AggregateField::Sum(column.into()),
856 op: HavingOp::Lt,
857 value,
858 }
859 }
860
861 pub fn sum_lte(column: impl Into<String>, value: f64) -> HavingCondition {
862 HavingCondition {
863 field: AggregateField::Sum(column.into()),
864 op: HavingOp::Lte,
865 value,
866 }
867 }
868
869 pub fn sum_eq(column: impl Into<String>, value: f64) -> HavingCondition {
870 HavingCondition {
871 field: AggregateField::Sum(column.into()),
872 op: HavingOp::Eq,
873 value,
874 }
875 }
876
877 pub fn sum_ne(column: impl Into<String>, value: f64) -> HavingCondition {
878 HavingCondition {
879 field: AggregateField::Sum(column.into()),
880 op: HavingOp::Ne,
881 value,
882 }
883 }
884
885 pub fn avg_gt(column: impl Into<String>, value: f64) -> HavingCondition {
886 HavingCondition {
887 field: AggregateField::Avg(column.into()),
888 op: HavingOp::Gt,
889 value,
890 }
891 }
892
893 pub fn avg_gte(column: impl Into<String>, value: f64) -> HavingCondition {
894 HavingCondition {
895 field: AggregateField::Avg(column.into()),
896 op: HavingOp::Gte,
897 value,
898 }
899 }
900
901 pub fn avg_lt(column: impl Into<String>, value: f64) -> HavingCondition {
902 HavingCondition {
903 field: AggregateField::Avg(column.into()),
904 op: HavingOp::Lt,
905 value,
906 }
907 }
908
909 pub fn avg_lte(column: impl Into<String>, value: f64) -> HavingCondition {
910 HavingCondition {
911 field: AggregateField::Avg(column.into()),
912 op: HavingOp::Lte,
913 value,
914 }
915 }
916
917 pub fn avg_eq(column: impl Into<String>, value: f64) -> HavingCondition {
918 HavingCondition {
919 field: AggregateField::Avg(column.into()),
920 op: HavingOp::Eq,
921 value,
922 }
923 }
924
925 pub fn avg_ne(column: impl Into<String>, value: f64) -> HavingCondition {
926 HavingCondition {
927 field: AggregateField::Avg(column.into()),
928 op: HavingOp::Ne,
929 value,
930 }
931 }
932
933 pub fn min_gt(column: impl Into<String>, value: f64) -> HavingCondition {
934 HavingCondition {
935 field: AggregateField::Min(column.into()),
936 op: HavingOp::Gt,
937 value,
938 }
939 }
940
941 pub fn min_gte(column: impl Into<String>, value: f64) -> HavingCondition {
942 HavingCondition {
943 field: AggregateField::Min(column.into()),
944 op: HavingOp::Gte,
945 value,
946 }
947 }
948
949 pub fn min_lt(column: impl Into<String>, value: f64) -> HavingCondition {
950 HavingCondition {
951 field: AggregateField::Min(column.into()),
952 op: HavingOp::Lt,
953 value,
954 }
955 }
956
957 pub fn min_lte(column: impl Into<String>, value: f64) -> HavingCondition {
958 HavingCondition {
959 field: AggregateField::Min(column.into()),
960 op: HavingOp::Lte,
961 value,
962 }
963 }
964
965 pub fn min_eq(column: impl Into<String>, value: f64) -> HavingCondition {
966 HavingCondition {
967 field: AggregateField::Min(column.into()),
968 op: HavingOp::Eq,
969 value,
970 }
971 }
972
973 pub fn min_ne(column: impl Into<String>, value: f64) -> HavingCondition {
974 HavingCondition {
975 field: AggregateField::Min(column.into()),
976 op: HavingOp::Ne,
977 value,
978 }
979 }
980
981 pub fn max_gt(column: impl Into<String>, value: f64) -> HavingCondition {
982 HavingCondition {
983 field: AggregateField::Max(column.into()),
984 op: HavingOp::Gt,
985 value,
986 }
987 }
988
989 pub fn max_gte(column: impl Into<String>, value: f64) -> HavingCondition {
990 HavingCondition {
991 field: AggregateField::Max(column.into()),
992 op: HavingOp::Gte,
993 value,
994 }
995 }
996
997 pub fn max_lt(column: impl Into<String>, value: f64) -> HavingCondition {
998 HavingCondition {
999 field: AggregateField::Max(column.into()),
1000 op: HavingOp::Lt,
1001 value,
1002 }
1003 }
1004
1005 pub fn max_lte(column: impl Into<String>, value: f64) -> HavingCondition {
1006 HavingCondition {
1007 field: AggregateField::Max(column.into()),
1008 op: HavingOp::Lte,
1009 value,
1010 }
1011 }
1012
1013 pub fn max_eq(column: impl Into<String>, value: f64) -> HavingCondition {
1014 HavingCondition {
1015 field: AggregateField::Max(column.into()),
1016 op: HavingOp::Eq,
1017 value,
1018 }
1019 }
1020
1021 pub fn max_ne(column: impl Into<String>, value: f64) -> HavingCondition {
1022 HavingCondition {
1023 field: AggregateField::Max(column.into()),
1024 op: HavingOp::Ne,
1025 value,
1026 }
1027 }
1028}
1029
1030#[cfg(test)]
1031mod tests {
1032 use super::*;
1033 use crate::filter::{Filter, FilterValue};
1034 use crate::types::NullsOrder;
1035
1036 struct TestModel;
1038
1039 impl Model for TestModel {
1040 const MODEL_NAME: &'static str = "TestModel";
1041 const TABLE_NAME: &'static str = "test_models";
1042 const PRIMARY_KEY: &'static [&'static str] = &["id"];
1043 const COLUMNS: &'static [&'static str] = &["id", "name", "age", "score"];
1044 }
1045
1046 impl crate::row::FromRow for TestModel {
1047 fn from_row(_row: &impl crate::row::RowRef) -> Result<Self, crate::row::RowError> {
1048 Ok(TestModel)
1049 }
1050 }
1051
1052 #[derive(Clone)]
1054 struct MockEngine;
1055
1056 impl QueryEngine for MockEngine {
1057 fn dialect(&self) -> &dyn crate::dialect::SqlDialect {
1058 &crate::dialect::Postgres
1059 }
1060
1061 fn query_many<T: Model + crate::row::FromRow + Send + 'static>(
1062 &self,
1063 _sql: &str,
1064 _params: Vec<crate::filter::FilterValue>,
1065 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
1066 Box::pin(async { Ok(Vec::new()) })
1067 }
1068
1069 fn query_one<T: Model + crate::row::FromRow + Send + 'static>(
1070 &self,
1071 _sql: &str,
1072 _params: Vec<crate::filter::FilterValue>,
1073 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
1074 Box::pin(async { Err(crate::error::QueryError::not_found("Not implemented")) })
1075 }
1076
1077 fn query_optional<T: Model + crate::row::FromRow + Send + 'static>(
1078 &self,
1079 _sql: &str,
1080 _params: Vec<crate::filter::FilterValue>,
1081 ) -> crate::traits::BoxFuture<'_, QueryResult<Option<T>>> {
1082 Box::pin(async { Ok(None) })
1083 }
1084
1085 fn execute_insert<T: Model + crate::row::FromRow + Send + 'static>(
1086 &self,
1087 _sql: &str,
1088 _params: Vec<crate::filter::FilterValue>,
1089 ) -> crate::traits::BoxFuture<'_, QueryResult<T>> {
1090 Box::pin(async { Err(crate::error::QueryError::not_found("Not implemented")) })
1091 }
1092
1093 fn execute_update<T: Model + crate::row::FromRow + Send + 'static>(
1094 &self,
1095 _sql: &str,
1096 _params: Vec<crate::filter::FilterValue>,
1097 ) -> crate::traits::BoxFuture<'_, QueryResult<Vec<T>>> {
1098 Box::pin(async { Ok(Vec::new()) })
1099 }
1100
1101 fn execute_delete(
1102 &self,
1103 _sql: &str,
1104 _params: Vec<crate::filter::FilterValue>,
1105 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
1106 Box::pin(async { Ok(0) })
1107 }
1108
1109 fn execute_raw(
1110 &self,
1111 _sql: &str,
1112 _params: Vec<crate::filter::FilterValue>,
1113 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
1114 Box::pin(async { Ok(0) })
1115 }
1116
1117 fn count(
1118 &self,
1119 _sql: &str,
1120 _params: Vec<crate::filter::FilterValue>,
1121 ) -> crate::traits::BoxFuture<'_, QueryResult<u64>> {
1122 Box::pin(async { Ok(0) })
1123 }
1124 }
1125
1126 #[test]
1129 fn test_aggregate_field_sql() {
1130 assert_eq!(AggregateField::CountAll.to_sql(), "COUNT(*)");
1132 assert_eq!(
1133 AggregateField::CountColumn("id".into()).to_sql(),
1134 "COUNT(id)"
1135 );
1136 assert_eq!(
1137 AggregateField::CountDistinct("email".into()).to_sql(),
1138 "COUNT(DISTINCT email)"
1139 );
1140 assert_eq!(AggregateField::Sum("amount".into()).to_sql(), "SUM(amount)");
1141 assert_eq!(
1142 AggregateField::Avg("score".to_string()).to_sql(),
1143 "AVG(score)"
1144 );
1145 assert_eq!(AggregateField::Min("age".into()).to_sql(), "MIN(age)");
1146 assert_eq!(AggregateField::Max("age".into()).to_sql(), "MAX(age)");
1147 assert_eq!(
1149 AggregateField::CountColumn("user".to_string()).to_sql(),
1150 "COUNT(\"user\")"
1151 );
1152 }
1153
1154 #[test]
1155 fn test_aggregate_field_alias() {
1156 assert_eq!(AggregateField::CountAll.alias(), "_count");
1157 assert_eq!(
1158 AggregateField::CountColumn("id".into()).alias(),
1159 "_count_id"
1160 );
1161 assert_eq!(
1162 AggregateField::CountDistinct("email".into()).alias(),
1163 "_count_distinct_email"
1164 );
1165 assert_eq!(AggregateField::Sum("amount".into()).alias(), "_sum_amount");
1166 assert_eq!(
1167 AggregateField::Avg("score".to_string()).alias(),
1168 "_avg_score"
1169 );
1170 assert_eq!(AggregateField::Min("age".into()).alias(), "_min_age");
1171 assert_eq!(
1172 AggregateField::Max("salary".to_string()).alias(),
1173 "_max_salary"
1174 );
1175 }
1176
1177 #[test]
1180 fn test_aggregate_result_default() {
1181 let result = AggregateResult::default();
1182 assert!(result.count.is_none());
1183 assert!(result.sum.is_empty());
1184 assert!(result.avg.is_empty());
1185 assert!(result.min.is_empty());
1186 assert!(result.max.is_empty());
1187 }
1188
1189 #[test]
1190 fn test_aggregate_result_debug() {
1191 let result = AggregateResult::default();
1192 let debug_str = format!("{:?}", result);
1193 assert!(debug_str.contains("AggregateResult"));
1194 }
1195
1196 #[test]
1197 fn test_aggregate_result_clone() {
1198 let mut result = AggregateResult {
1199 count: Some(42),
1200 ..AggregateResult::default()
1201 };
1202 result.sum.insert("amount".into(), 1000.0);
1203
1204 let cloned = result.clone();
1205 assert_eq!(cloned.count, Some(42));
1206 assert_eq!(cloned.sum.get("amount"), Some(&1000.0));
1207 }
1208
1209 #[test]
1212 fn test_aggregate_operation_new() {
1213 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new();
1214 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1215
1216 assert!(sql.contains("COUNT(*)"));
1218 assert!(params.is_empty());
1219 }
1220
1221 #[test]
1222 fn test_aggregate_operation_default() {
1223 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::default();
1224 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1225
1226 assert!(sql.contains("COUNT(*)"));
1227 assert!(params.is_empty());
1228 }
1229
1230 #[test]
1231 fn test_aggregate_operation_build_sql() {
1232 let op: AggregateOperation<TestModel, MockEngine> =
1233 AggregateOperation::new().count().sum("score").avg("age");
1234
1235 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1236
1237 assert!(sql.contains("SELECT"));
1238 assert!(sql.contains("COUNT(*)"));
1239 assert!(sql.contains(r#"SUM("score")"#));
1240 assert!(sql.contains(r#"AVG("age")"#));
1241 assert!(sql.contains(r#"FROM "test_models""#));
1242 assert!(params.is_empty());
1243 }
1244
1245 #[test]
1246 fn test_aggregate_operation_count_column() {
1247 let op: AggregateOperation<TestModel, MockEngine> =
1248 AggregateOperation::new().count_column("email");
1249
1250 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1251
1252 assert!(sql.contains(r#"COUNT("email")"#));
1253 }
1254
1255 #[test]
1256 fn test_aggregate_operation_count_distinct() {
1257 let op: AggregateOperation<TestModel, MockEngine> =
1258 AggregateOperation::new().count_distinct("email");
1259
1260 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1261
1262 assert!(sql.contains(r#"COUNT(DISTINCT "email")"#));
1263 }
1264
1265 #[test]
1266 fn test_aggregate_operation_min_max() {
1267 let op: AggregateOperation<TestModel, MockEngine> =
1268 AggregateOperation::new().min("age").max("age");
1269
1270 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1271
1272 assert!(sql.contains(r#"MIN("age")"#));
1273 assert!(sql.contains(r#"MAX("age")"#));
1274 }
1275
1276 #[test]
1277 fn test_aggregate_with_where() {
1278 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1279 .count()
1280 .r#where(Filter::Gt("age".into(), FilterValue::Int(18)));
1281
1282 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1283
1284 assert_eq!(
1286 sql,
1287 r#"SELECT COUNT(*) AS "_count" FROM "test_models" WHERE "age" > $1"#
1288 );
1289 assert_eq!(params, vec![FilterValue::Int(18)]);
1290 }
1291
1292 #[test]
1293 fn test_aggregate_with_complex_filter() {
1294 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1295 .sum("score")
1296 .avg("age")
1297 .r#where(Filter::and([
1298 Filter::Gte("age".into(), FilterValue::Int(18)),
1299 Filter::Equals("active".into(), FilterValue::Bool(true)),
1300 ]));
1301
1302 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1303
1304 assert!(
1305 sql.contains(r#"WHERE ("age" >= $1 AND "active" = $2)"#),
1306 "got: {sql}"
1307 );
1308 assert_eq!(params.len(), 2);
1309 }
1310
1311 #[test]
1312 fn test_aggregate_where_and_composes() {
1313 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1315 .count()
1316 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)))
1317 .r#where(Filter::Gt("age".into(), FilterValue::Int(18)));
1318
1319 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1320
1321 assert!(
1322 sql.contains(r#"WHERE ("active" = $1 AND "age" > $2)"#),
1323 "got: {sql}"
1324 );
1325 assert_eq!(params, vec![FilterValue::Bool(true), FilterValue::Int(18)]);
1326 }
1327
1328 #[test]
1329 fn test_aggregate_mysql_dialect() {
1330 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1331 .count()
1332 .sum("score")
1333 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)));
1334
1335 let (sql, params) = op.build_sql(&crate::dialect::Mysql);
1336
1337 assert_eq!(
1338 sql,
1339 "SELECT COUNT(*) AS `_count`, SUM(`score`) AS `_sum_score` \
1340 FROM `test_models` WHERE `active` = ?"
1341 );
1342 assert_eq!(params, vec![FilterValue::Bool(true)]);
1343 }
1344
1345 #[test]
1346 fn test_aggregate_all_methods() {
1347 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new()
1348 .count()
1349 .count_column("name")
1350 .count_distinct("email")
1351 .sum("score")
1352 .avg("score")
1353 .min("age")
1354 .max("age");
1355
1356 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1357
1358 assert!(sql.contains("COUNT(*)"));
1359 assert!(sql.contains(r#"COUNT("name")"#));
1360 assert!(sql.contains(r#"COUNT(DISTINCT "email")"#));
1361 assert!(sql.contains(r#"SUM("score")"#));
1362 assert!(sql.contains(r#"AVG("score")"#));
1363 assert!(sql.contains(r#"MIN("age")"#));
1364 assert!(sql.contains(r#"MAX("age")"#));
1365 }
1366
1367 #[tokio::test]
1368 async fn test_aggregate_exec_without_engine_errors() {
1369 let op: AggregateOperation<TestModel, MockEngine> = AggregateOperation::new().count();
1372 let err = op.exec().await.unwrap_err();
1373 assert!(err.to_string().contains("without an engine"));
1374 }
1375
1376 #[tokio::test]
1377 async fn test_aggregate_exec_with_engine_ok() {
1378 let op: AggregateOperation<TestModel, MockEngine> =
1382 AggregateOperation::with_engine(MockEngine).count();
1383 let err = op.exec().await.unwrap_err();
1384 assert!(err.to_string().contains("aggregate_query"));
1385 }
1386
1387 #[test]
1390 fn test_group_by_new() {
1391 let op: GroupByOperation<TestModel, MockEngine> =
1392 GroupByOperation::new(vec!["department".into()]);
1393
1394 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1395
1396 assert!(sql.contains(r#"GROUP BY "department""#));
1397 }
1398
1399 #[test]
1400 fn test_group_by_build_sql() {
1401 let op: GroupByOperation<TestModel, MockEngine> =
1402 GroupByOperation::new(vec!["name".to_string()])
1403 .count()
1404 .avg("score");
1405
1406 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1407
1408 assert!(sql.contains("SELECT"));
1409 assert!(sql.contains(r#""name""#)); assert!(sql.contains("COUNT(*)"));
1411 assert!(sql.contains(r#"AVG("score")"#));
1412 assert!(sql.contains(r#"GROUP BY "name""#));
1413 assert!(params.is_empty());
1414 }
1415
1416 #[test]
1417 fn test_group_by_multiple_columns() {
1418 let op: GroupByOperation<TestModel, MockEngine> =
1419 GroupByOperation::new(vec!["department".into(), "role".into()]).count();
1420
1421 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1422
1423 assert!(sql.contains(r#"GROUP BY "department", "role""#));
1424 }
1425
1426 #[test]
1427 fn test_group_by_with_sum() {
1428 let op: GroupByOperation<TestModel, MockEngine> =
1429 GroupByOperation::new(vec!["category".into()]).sum("amount");
1430
1431 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1432
1433 assert!(sql.contains(r#"SUM("amount")"#));
1434 }
1435
1436 #[test]
1437 fn test_group_by_with_min_max() {
1438 let op: GroupByOperation<TestModel, MockEngine> =
1439 GroupByOperation::new(vec!["category".into()])
1440 .min("price")
1441 .max("price");
1442
1443 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1444
1445 assert!(sql.contains(r#"MIN("price")"#));
1446 assert!(sql.contains(r#"MAX("price")"#));
1447 }
1448
1449 #[test]
1450 fn test_group_by_with_where() {
1451 let op: GroupByOperation<TestModel, MockEngine> =
1452 GroupByOperation::new(vec!["department".into()])
1453 .count()
1454 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)));
1455
1456 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1457
1458 assert!(sql.contains(r#"WHERE "active" = $1"#), "got: {sql}");
1460 assert!(sql.contains("GROUP BY"));
1461 assert_eq!(params, vec![FilterValue::Bool(true)]);
1462 }
1463
1464 #[test]
1465 fn test_group_by_where_and_composes() {
1466 let op: GroupByOperation<TestModel, MockEngine> =
1468 GroupByOperation::new(vec!["department".into()])
1469 .count()
1470 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)))
1471 .r#where(Filter::Gt("age".into(), FilterValue::Int(18)));
1472
1473 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1474
1475 assert!(
1476 sql.contains(r#"WHERE ("active" = $1 AND "age" > $2)"#),
1477 "got: {sql}"
1478 );
1479 assert_eq!(params, vec![FilterValue::Bool(true), FilterValue::Int(18)]);
1480 }
1481
1482 #[test]
1483 fn test_group_by_with_having() {
1484 let op: GroupByOperation<TestModel, MockEngine> =
1485 GroupByOperation::new(vec!["name".to_string()])
1486 .count()
1487 .having(having::count_gt(5.0));
1488
1489 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1490
1491 assert!(sql.contains("HAVING COUNT(*) > $1"), "got: {sql}");
1493 assert_eq!(params, vec![FilterValue::Float(5.0)]);
1494 }
1495
1496 #[test]
1497 fn test_group_by_having_placeholder_follows_where_params() {
1498 let op: GroupByOperation<TestModel, MockEngine> =
1499 GroupByOperation::new(vec!["department".into()])
1500 .count()
1501 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)))
1502 .having(having::count_gt(5.0));
1503
1504 let (sql, params) = op.build_sql(&crate::dialect::Postgres);
1505
1506 assert!(sql.contains(r#"WHERE "active" = $1"#), "got: {sql}");
1507 assert!(sql.contains("HAVING COUNT(*) > $2"), "got: {sql}");
1508 assert_eq!(
1509 params,
1510 vec![FilterValue::Bool(true), FilterValue::Float(5.0)]
1511 );
1512 }
1513
1514 #[test]
1515 fn test_group_by_mysql_dialect() {
1516 let op: GroupByOperation<TestModel, MockEngine> =
1517 GroupByOperation::new(vec!["department".into()])
1518 .count()
1519 .having(having::count_gt(5.0));
1520
1521 let (sql, params) = op.build_sql(&crate::dialect::Mysql);
1522
1523 assert_eq!(
1524 sql,
1525 "SELECT `department`, COUNT(*) AS `_count` FROM `test_models` \
1526 GROUP BY `department` HAVING COUNT(*) > ?"
1527 );
1528 assert_eq!(params, vec![FilterValue::Float(5.0)]);
1529 }
1530
1531 #[test]
1532 #[should_panic(expected = "must be finite")]
1533 fn test_group_by_having_rejects_nan() {
1534 let _ = GroupByOperation::<TestModel, MockEngine>::new(vec!["department".into()])
1535 .count()
1536 .having(having::count_gt(f64::NAN));
1537 }
1538
1539 #[test]
1540 #[should_panic(expected = "must be finite")]
1541 fn test_group_by_having_rejects_infinity() {
1542 let _ = GroupByOperation::<TestModel, MockEngine>::new(vec!["department".into()])
1543 .count()
1544 .having(having::avg_gt("score", f64::INFINITY));
1545 }
1546
1547 #[test]
1548 fn test_group_by_with_order_and_limit() {
1549 let op: GroupByOperation<TestModel, MockEngine> =
1550 GroupByOperation::new(vec!["name".to_string()])
1551 .count()
1552 .order_by(OrderByField::desc("_count"))
1553 .take(10)
1554 .skip(5);
1555
1556 let (sql, _params) = op.build_sql(&crate::dialect::Postgres);
1557
1558 assert!(sql.contains(r#"ORDER BY "_count" DESC"#)); assert!(sql.contains("LIMIT 10"));
1560 assert!(sql.contains("OFFSET 5"));
1561 }
1562
1563 #[test]
1564 fn test_group_by_order_with_nulls() {
1565 let op: GroupByOperation<TestModel, MockEngine> =
1566 GroupByOperation::new(vec!["department".into()])
1567 .count()
1568 .order_by(OrderByField::asc("name").nulls(NullsOrder::First));
1569
1570 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1571
1572 assert!(sql.contains("ORDER BY"));
1573 assert!(sql.contains("NULLS FIRST"));
1574 }
1575
1576 #[test]
1577 fn test_group_by_skip_only() {
1578 let op: GroupByOperation<TestModel, MockEngine> =
1579 GroupByOperation::new(vec!["department".into()])
1580 .count()
1581 .skip(20);
1582
1583 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1584
1585 assert!(sql.contains("OFFSET 20"));
1586 assert!(!sql.contains("LIMIT"));
1587 }
1588
1589 #[test]
1590 fn test_group_by_take_only() {
1591 let op: GroupByOperation<TestModel, MockEngine> =
1592 GroupByOperation::new(vec!["department".into()])
1593 .count()
1594 .take(50);
1595
1596 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1597
1598 assert!(sql.contains("LIMIT 50"));
1599 assert!(!sql.contains("OFFSET"));
1600 }
1601
1602 #[tokio::test]
1603 async fn test_group_by_exec_without_engine_errors() {
1604 let op: GroupByOperation<TestModel, MockEngine> =
1605 GroupByOperation::new(vec!["department".into()]).count();
1606 let err = op.exec().await.unwrap_err();
1607 assert!(err.to_string().contains("without an engine"));
1608 }
1609
1610 #[tokio::test]
1611 async fn test_group_by_exec_with_engine_ok() {
1612 let op: GroupByOperation<TestModel, MockEngine> =
1613 GroupByOperation::with_engine(MockEngine, vec!["department".into()]).count();
1614 let err = op.exec().await.unwrap_err();
1615 assert!(err.to_string().contains("aggregate_query"));
1616 }
1617
1618 #[test]
1621 fn test_having_op_as_str() {
1622 assert_eq!(HavingOp::Gt.as_str(), ">");
1623 assert_eq!(HavingOp::Gte.as_str(), ">=");
1624 assert_eq!(HavingOp::Lt.as_str(), "<");
1625 assert_eq!(HavingOp::Lte.as_str(), "<=");
1626 assert_eq!(HavingOp::Eq.as_str(), "=");
1627 assert_eq!(HavingOp::Ne.as_str(), "<>");
1628 }
1629
1630 #[test]
1633 fn test_having_condition_debug() {
1634 let cond = HavingCondition {
1635 field: AggregateField::CountAll,
1636 op: HavingOp::Gt,
1637 value: 10.0,
1638 };
1639 let debug_str = format!("{:?}", cond);
1640 assert!(debug_str.contains("HavingCondition"));
1641 }
1642
1643 #[test]
1644 fn test_having_condition_clone() {
1645 let cond = HavingCondition {
1646 field: AggregateField::Sum("amount".into()),
1647 op: HavingOp::Gte,
1648 value: 1000.0,
1649 };
1650 let cloned = cond.clone();
1651 assert!((cloned.value - 1000.0).abs() < f64::EPSILON);
1652 }
1653
1654 #[test]
1657 fn test_having_helpers() {
1658 let cond = having::count_gt(10.0);
1659 assert!(matches!(cond.field, AggregateField::CountAll));
1660 assert!(matches!(cond.op, HavingOp::Gt));
1661 assert!((cond.value - 10.0).abs() < f64::EPSILON);
1662
1663 let cond = having::sum_gt("amount", 1000.0);
1664 if let AggregateField::Sum(col) = cond.field {
1665 assert_eq!(col, "amount");
1666 } else {
1667 panic!("Expected Sum");
1668 }
1669 }
1670
1671 #[test]
1672 fn test_having_count_gte() {
1673 let cond = having::count_gte(5.0);
1674 assert!(matches!(cond.field, AggregateField::CountAll));
1675 assert!(matches!(cond.op, HavingOp::Gte));
1676 assert!((cond.value - 5.0).abs() < f64::EPSILON);
1677 }
1678
1679 #[test]
1680 fn test_having_count_lt() {
1681 let cond = having::count_lt(100.0);
1682 assert!(matches!(cond.field, AggregateField::CountAll));
1683 assert!(matches!(cond.op, HavingOp::Lt));
1684 assert!((cond.value - 100.0).abs() < f64::EPSILON);
1685 }
1686
1687 #[test]
1688 fn test_having_avg_gt() {
1689 let cond = having::avg_gt("score", 75.5);
1690 assert!(matches!(cond.op, HavingOp::Gt));
1691 assert!((cond.value - 75.5).abs() < f64::EPSILON);
1692 if let AggregateField::Avg(col) = cond.field {
1693 assert_eq!(col, "score");
1694 } else {
1695 panic!("Expected Avg");
1696 }
1697 }
1698
1699 #[test]
1700 fn test_having_sum_gt_with_different_columns() {
1701 let cond1 = having::sum_gt("revenue", 50000.0);
1702 let cond2 = having::sum_gt("cost", 10000.0);
1703
1704 if let AggregateField::Sum(col) = &cond1.field {
1705 assert_eq!(col, "revenue");
1706 }
1707 if let AggregateField::Sum(col) = &cond2.field {
1708 assert_eq!(col, "cost");
1709 }
1710 }
1711
1712 #[test]
1715 fn test_group_by_result_debug() {
1716 let result = GroupByResult {
1717 group_values: std::collections::HashMap::new(),
1718 aggregates: AggregateResult::default(),
1719 };
1720 let debug_str = format!("{:?}", result);
1721 assert!(debug_str.contains("GroupByResult"));
1722 }
1723
1724 #[test]
1725 fn test_group_by_result_clone() {
1726 let mut result = GroupByResult {
1727 group_values: std::collections::HashMap::new(),
1728 aggregates: AggregateResult::default(),
1729 };
1730 result
1731 .group_values
1732 .insert("category".into(), serde_json::json!("electronics"));
1733 result.aggregates.count = Some(50);
1734
1735 let cloned = result.clone();
1736 assert_eq!(cloned.aggregates.count, Some(50));
1737 assert!(cloned.group_values.contains_key("category"));
1738 }
1739
1740 #[test]
1743 fn test_group_by_sql_structure() {
1744 let op: GroupByOperation<TestModel, MockEngine> =
1745 GroupByOperation::new(vec!["department".into()])
1746 .count()
1747 .r#where(Filter::Equals("active".into(), FilterValue::Bool(true)))
1748 .having(having::count_gt(5.0))
1749 .order_by(OrderByField::desc("_count"))
1750 .take(10)
1751 .skip(5);
1752
1753 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1754
1755 let select_pos = sql.find("SELECT").unwrap();
1757 let from_pos = sql.find("FROM").unwrap();
1758 let where_pos = sql.find("WHERE").unwrap();
1759 let group_pos = sql.find("GROUP BY").unwrap();
1760 let having_pos = sql.find("HAVING").unwrap();
1761 let order_pos = sql.find("ORDER BY").unwrap();
1762 let limit_pos = sql.find("LIMIT").unwrap();
1763 let offset_pos = sql.find("OFFSET").unwrap();
1764
1765 assert!(select_pos < from_pos);
1766 assert!(from_pos < where_pos);
1767 assert!(where_pos < group_pos);
1768 assert!(group_pos < having_pos);
1769 assert!(having_pos < order_pos);
1770 assert!(order_pos < limit_pos);
1771 assert!(limit_pos < offset_pos);
1772 }
1773
1774 #[test]
1775 fn test_aggregate_no_group_by() {
1776 let op: AggregateOperation<TestModel, MockEngine> =
1777 AggregateOperation::new().count().sum("score");
1778
1779 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1780
1781 assert!(!sql.contains("GROUP BY"));
1782 }
1783
1784 #[test]
1785 fn test_group_by_empty_columns() {
1786 let op: GroupByOperation<TestModel, MockEngine> = GroupByOperation::new(vec![]).count();
1787
1788 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1789
1790 assert!(!sql.contains("GROUP BY"));
1792 }
1793
1794 #[test]
1795 fn group_by_build_sql_emits_count_column_and_distinct() {
1796 let op: GroupByOperation<TestModel, MockEngine> =
1797 GroupByOperation::new(vec!["team_id".to_string()])
1798 .count_column("email")
1799 .count_distinct("region");
1800 let (sql, _) = op.build_sql(&crate::dialect::Postgres);
1801 assert!(
1802 sql.contains(r#"COUNT("email") AS "_count_email""#),
1803 "got: {sql}"
1804 );
1805 assert!(
1806 sql.contains(r#"COUNT(DISTINCT "region") AS "_count_distinct_region""#),
1807 "got: {sql}"
1808 );
1809 }
1810
1811 #[test]
1812 fn test_having_count_lte_eq_ne() {
1813 let c = having::count_lte(10.0);
1814 assert!(matches!(c.field, AggregateField::CountAll));
1815 assert!(matches!(c.op, HavingOp::Lte));
1816 assert_eq!(c.value, 10.0);
1817
1818 let c = having::count_eq(5.0);
1819 assert!(matches!(c.op, HavingOp::Eq));
1820
1821 let c = having::count_ne(0.0);
1822 assert!(matches!(c.op, HavingOp::Ne));
1823 }
1824
1825 #[test]
1826 fn test_having_sum_variants() {
1827 let c = having::sum_gte("views", 100.0);
1828 assert!(matches!(&c.field, AggregateField::Sum(col) if col == "views"));
1829 assert!(matches!(c.op, HavingOp::Gte));
1830
1831 let c = having::sum_lt("views", 50.0);
1832 assert!(matches!(c.op, HavingOp::Lt));
1833
1834 let c = having::sum_lte("views", 50.0);
1835 assert!(matches!(c.op, HavingOp::Lte));
1836
1837 let c = having::sum_eq("views", 0.0);
1838 assert!(matches!(c.op, HavingOp::Eq));
1839
1840 let c = having::sum_ne("views", 0.0);
1841 assert!(matches!(c.op, HavingOp::Ne));
1842 }
1843
1844 #[test]
1845 fn test_having_avg_variants() {
1846 let c = having::avg_gte("score", 3.5);
1847 assert!(matches!(&c.field, AggregateField::Avg(col) if col == "score"));
1848 assert!(matches!(c.op, HavingOp::Gte));
1849
1850 let c = having::avg_lt("score", 2.0);
1851 assert!(matches!(c.op, HavingOp::Lt));
1852
1853 let c = having::avg_lte("score", 2.0);
1854 assert!(matches!(c.op, HavingOp::Lte));
1855
1856 let c = having::avg_eq("score", 5.0);
1857 assert!(matches!(c.op, HavingOp::Eq));
1858
1859 let c = having::avg_ne("score", 0.0);
1860 assert!(matches!(c.op, HavingOp::Ne));
1861 }
1862
1863 #[test]
1864 fn test_having_min_variants() {
1865 let c = having::min_gt("age", 18.0);
1866 assert!(matches!(&c.field, AggregateField::Min(col) if col == "age"));
1867 assert!(matches!(c.op, HavingOp::Gt));
1868
1869 let c = having::min_gte("age", 18.0);
1870 assert!(matches!(c.op, HavingOp::Gte));
1871
1872 let c = having::min_lt("age", 65.0);
1873 assert!(matches!(c.op, HavingOp::Lt));
1874
1875 let c = having::min_lte("age", 65.0);
1876 assert!(matches!(c.op, HavingOp::Lte));
1877
1878 let c = having::min_eq("age", 21.0);
1879 assert!(matches!(c.op, HavingOp::Eq));
1880
1881 let c = having::min_ne("age", 0.0);
1882 assert!(matches!(c.op, HavingOp::Ne));
1883 }
1884
1885 #[test]
1886 fn test_having_max_variants() {
1887 let c = having::max_gt("salary", 50000.0);
1888 assert!(matches!(&c.field, AggregateField::Max(col) if col == "salary"));
1889 assert!(matches!(c.op, HavingOp::Gt));
1890
1891 let c = having::max_gte("salary", 50000.0);
1892 assert!(matches!(c.op, HavingOp::Gte));
1893
1894 let c = having::max_lt("salary", 200000.0);
1895 assert!(matches!(c.op, HavingOp::Lt));
1896
1897 let c = having::max_lte("salary", 200000.0);
1898 assert!(matches!(c.op, HavingOp::Lte));
1899
1900 let c = having::max_eq("salary", 100000.0);
1901 assert!(matches!(c.op, HavingOp::Eq));
1902
1903 let c = having::max_ne("salary", 0.0);
1904 assert!(matches!(c.op, HavingOp::Ne));
1905 }
1906
1907 #[test]
1908 fn from_row_hydrates_per_column_and_distinct_counts() {
1909 use crate::filter::FilterValue;
1910 use std::collections::HashMap;
1911 let mut row = HashMap::new();
1912 row.insert("_count".to_string(), FilterValue::Int(5));
1913 row.insert("_count_email".to_string(), FilterValue::Int(3));
1914 row.insert("_count_distinct_email".to_string(), FilterValue::Int(2));
1915 let r = AggregateResult::from_row(row);
1916 assert_eq!(r.count, Some(5));
1917 assert_eq!(r.count_of("email"), Some(3));
1918 assert_eq!(r.count_distinct_of("email"), Some(2));
1919 assert_eq!(r.count_columns.get("distinct_email"), None);
1922 }
1923}