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