1use crate::db::{
7 executor::{
8 ScalarNumericFieldBoundaryRequest, ScalarProjectionBoundaryRequest,
9 ScalarTerminalBoundaryRequest,
10 },
11 query::plan::{
12 AggregateKind, FieldSlot,
13 expr::{Expr, FieldId, canonicalize_aggregate_input_expr},
14 },
15};
16
17#[derive(Clone, Debug, Eq, PartialEq)]
27pub struct AggregateExpr {
28 kind: AggregateKind,
29 input_expr: Option<Box<Expr>>,
30 filter_expr: Option<Box<Expr>>,
31 distinct: bool,
32}
33
34impl AggregateExpr {
35 const fn terminal(kind: AggregateKind) -> Self {
37 Self {
38 kind,
39 input_expr: None,
40 filter_expr: None,
41 distinct: false,
42 }
43 }
44
45 fn field_target(kind: AggregateKind, field: impl Into<String>) -> Self {
47 Self {
48 kind,
49 input_expr: Some(Box::new(Expr::Field(FieldId::new(field.into())))),
50 filter_expr: None,
51 distinct: false,
52 }
53 }
54
55 pub(in crate::db) fn from_expression_input(kind: AggregateKind, input_expr: Expr) -> Self {
57 Self {
58 kind,
59 input_expr: Some(Box::new(canonicalize_aggregate_input_expr(
60 kind, input_expr,
61 ))),
62 filter_expr: None,
63 distinct: false,
64 }
65 }
66
67 #[must_use]
69 pub(in crate::db) fn with_filter_expr(mut self, filter_expr: Expr) -> Self {
70 self.filter_expr = Some(Box::new(filter_expr));
71 self
72 }
73
74 #[must_use]
76 pub const fn distinct(mut self) -> Self {
77 self.distinct = true;
78 self
79 }
80
81 #[must_use]
83 pub(crate) const fn kind(&self) -> AggregateKind {
84 self.kind
85 }
86
87 #[must_use]
89 pub(crate) fn input_expr(&self) -> Option<&Expr> {
90 self.input_expr.as_deref()
91 }
92
93 #[must_use]
95 pub(crate) fn filter_expr(&self) -> Option<&Expr> {
96 self.filter_expr.as_deref()
97 }
98
99 #[must_use]
101 pub(crate) fn target_field(&self) -> Option<&str> {
102 match self.input_expr() {
103 Some(Expr::Field(field)) => Some(field.as_str()),
104 _ => None,
105 }
106 }
107
108 #[must_use]
110 pub(crate) const fn is_distinct(&self) -> bool {
111 self.distinct
112 }
113
114 pub(in crate::db::query) fn from_semantic_parts(
116 kind: AggregateKind,
117 target_field: Option<String>,
118 distinct: bool,
119 ) -> Self {
120 Self {
121 kind,
122 input_expr: target_field.map(|field| Box::new(Expr::Field(FieldId::new(field)))),
123 filter_expr: None,
124 distinct,
125 }
126 }
127
128 #[cfg(test)]
130 #[must_use]
131 pub(in crate::db) fn terminal_for_kind(kind: AggregateKind) -> Self {
132 match kind {
133 AggregateKind::Count => count(),
134 AggregateKind::Exists => exists(),
135 AggregateKind::Min => min(),
136 AggregateKind::Max => max(),
137 AggregateKind::First => first(),
138 AggregateKind::Last => last(),
139 AggregateKind::Sum | AggregateKind::Avg => unreachable!(
140 "AggregateExpr::terminal_for_kind does not support SUM/AVG field-target kinds"
141 ),
142 }
143 }
144}
145
146pub(crate) trait PreparedFluentAggregateExplainStrategy {
157 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
160
161 fn explain_projected_field(&self) -> Option<&str> {
163 None
164 }
165}
166
167#[derive(Clone, Debug, Eq, PartialEq)]
174pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
175 CountRows,
176 ExistsRows,
177}
178
179#[derive(Clone, Debug, Eq, PartialEq)]
192pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
193 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
194}
195
196impl PreparedFluentExistingRowsTerminalStrategy {
197 #[must_use]
199 pub(crate) const fn count_rows() -> Self {
200 Self {
201 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
202 }
203 }
204
205 #[must_use]
207 pub(crate) const fn exists_rows() -> Self {
208 Self {
209 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
210 }
211 }
212
213 #[cfg(test)]
216 #[must_use]
217 pub(crate) const fn aggregate(&self) -> AggregateExpr {
218 match self.runtime_request {
219 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
220 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
221 }
222 }
223
224 #[cfg(test)]
227 #[must_use]
228 pub(crate) const fn runtime_request(
229 &self,
230 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
231 &self.runtime_request
232 }
233
234 #[must_use]
239 pub(in crate::db) const fn into_executor_request(
240 self,
241 ) -> (
242 ScalarTerminalBoundaryRequest,
243 PreparedFluentExistingRowsTerminalRuntimeRequest,
244 ) {
245 match self.runtime_request {
246 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => (
247 ScalarTerminalBoundaryRequest::Count,
248 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
249 ),
250 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => (
251 ScalarTerminalBoundaryRequest::Exists,
252 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
253 ),
254 }
255 }
256}
257
258impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
259 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
260 Some(match self.runtime_request {
261 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
262 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
263 })
264 }
265}
266
267#[derive(Clone, Debug, Eq, PartialEq)]
274pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
275 IdTerminal {
276 kind: AggregateKind,
277 },
278 IdBySlot {
279 kind: AggregateKind,
280 target_field: FieldSlot,
281 },
282}
283
284#[derive(Clone, Debug, Eq, PartialEq)]
296pub(crate) struct PreparedFluentScalarTerminalStrategy {
297 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
298}
299
300impl PreparedFluentScalarTerminalStrategy {
301 #[must_use]
303 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
304 Self {
305 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
306 }
307 }
308
309 #[must_use]
312 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
313 Self {
314 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
315 kind,
316 target_field,
317 },
318 }
319 }
320
321 #[must_use]
323 pub(in crate::db) fn into_executor_request(self) -> ScalarTerminalBoundaryRequest {
324 match self.runtime_request {
325 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind } => {
326 ScalarTerminalBoundaryRequest::IdTerminal { kind }
327 }
328 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, target_field } => {
329 ScalarTerminalBoundaryRequest::IdBySlot { kind, target_field }
330 }
331 }
332 }
333}
334
335impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
336 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
337 Some(match self.runtime_request {
338 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
339 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
340 })
341 }
342
343 fn explain_projected_field(&self) -> Option<&str> {
344 match &self.runtime_request {
345 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
346 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
347 Some(target_field.field())
348 }
349 }
350 }
351}
352
353#[derive(Clone, Copy, Debug, Eq, PartialEq)]
363pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
364 Sum,
365 SumDistinct,
366 Avg,
367 AvgDistinct,
368}
369
370#[derive(Clone, Debug, Eq, PartialEq)]
383pub(crate) struct PreparedFluentNumericFieldStrategy {
384 target_field: FieldSlot,
385 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
386}
387
388impl PreparedFluentNumericFieldStrategy {
389 #[must_use]
391 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
392 Self {
393 target_field,
394 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
395 }
396 }
397
398 #[must_use]
400 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
401 Self {
402 target_field,
403 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
404 }
405 }
406
407 #[must_use]
409 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
410 Self {
411 target_field,
412 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
413 }
414 }
415
416 #[must_use]
418 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
419 Self {
420 target_field,
421 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
422 }
423 }
424
425 #[cfg(test)]
428 #[must_use]
429 pub(crate) fn aggregate(&self) -> AggregateExpr {
430 let field = self.target_field.field();
431
432 match self.runtime_request {
433 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
434 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
435 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
436 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
437 }
438 }
439
440 #[cfg(test)]
443 #[must_use]
444 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
445 match self.runtime_request {
446 PreparedFluentNumericFieldRuntimeRequest::Sum
447 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
448 PreparedFluentNumericFieldRuntimeRequest::Avg
449 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
450 }
451 }
452
453 #[cfg(test)]
456 #[must_use]
457 pub(crate) fn projected_field(&self) -> &str {
458 self.target_field.field()
459 }
460
461 #[cfg(test)]
464 #[must_use]
465 pub(crate) const fn target_field(&self) -> &FieldSlot {
466 &self.target_field
467 }
468
469 #[cfg(test)]
472 #[must_use]
473 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
474 self.runtime_request
475 }
476
477 #[must_use]
479 pub(in crate::db) fn into_executor_request(
480 self,
481 ) -> (FieldSlot, ScalarNumericFieldBoundaryRequest) {
482 let request = match self.runtime_request {
483 PreparedFluentNumericFieldRuntimeRequest::Sum => ScalarNumericFieldBoundaryRequest::Sum,
484 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => {
485 ScalarNumericFieldBoundaryRequest::SumDistinct
486 }
487 PreparedFluentNumericFieldRuntimeRequest::Avg => ScalarNumericFieldBoundaryRequest::Avg,
488 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => {
489 ScalarNumericFieldBoundaryRequest::AvgDistinct
490 }
491 };
492
493 (self.target_field, request)
494 }
495}
496
497impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
498 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
499 Some(match self.runtime_request {
500 PreparedFluentNumericFieldRuntimeRequest::Sum
501 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
502 PreparedFluentNumericFieldRuntimeRequest::Avg
503 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
504 })
505 }
506
507 fn explain_projected_field(&self) -> Option<&str> {
508 Some(self.target_field.field())
509 }
510}
511
512#[derive(Clone, Debug, Eq, PartialEq)]
523pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
524 ResponseOrder { kind: AggregateKind },
525 NthBySlot { target_field: FieldSlot, nth: usize },
526 MedianBySlot { target_field: FieldSlot },
527 MinMaxBySlot { target_field: FieldSlot },
528}
529
530#[derive(Clone, Debug, Eq, PartialEq)]
541pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
542 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
543}
544
545impl PreparedFluentOrderSensitiveTerminalStrategy {
546 #[must_use]
548 pub(crate) const fn first() -> Self {
549 Self {
550 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
551 kind: AggregateKind::First,
552 },
553 }
554 }
555
556 #[must_use]
558 pub(crate) const fn last() -> Self {
559 Self {
560 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
561 kind: AggregateKind::Last,
562 },
563 }
564 }
565
566 #[must_use]
568 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
569 Self {
570 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
571 target_field,
572 nth,
573 },
574 }
575 }
576
577 #[must_use]
579 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
580 Self {
581 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
582 target_field,
583 },
584 }
585 }
586
587 #[must_use]
589 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
590 Self {
591 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
592 target_field,
593 },
594 }
595 }
596
597 #[cfg(test)]
600 #[must_use]
601 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
602 match self.runtime_request {
603 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
604 Some(AggregateExpr::terminal_for_kind(kind))
605 }
606 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
607 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
608 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
609 }
610 }
611
612 #[cfg(test)]
615 #[must_use]
616 pub(crate) const fn runtime_request(
617 &self,
618 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
619 &self.runtime_request
620 }
621
622 #[must_use]
624 pub(in crate::db) fn into_executor_request(self) -> (ScalarTerminalBoundaryRequest, bool) {
625 match self.runtime_request {
626 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
627 (ScalarTerminalBoundaryRequest::IdTerminal { kind }, false)
628 }
629 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { target_field, nth } => {
630 (
631 ScalarTerminalBoundaryRequest::NthBySlot { target_field, nth },
632 false,
633 )
634 }
635 PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { target_field } => (
636 ScalarTerminalBoundaryRequest::MedianBySlot { target_field },
637 false,
638 ),
639 PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { target_field } => (
640 ScalarTerminalBoundaryRequest::MinMaxBySlot { target_field },
641 true,
642 ),
643 }
644 }
645}
646
647impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
648 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
649 match self.runtime_request {
650 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
651 Some(kind)
652 }
653 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
654 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
655 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
656 }
657 }
658}
659
660#[derive(Clone, Copy, Debug, Eq, PartialEq)]
670pub(crate) enum PreparedFluentProjectionRuntimeRequest {
671 Values,
672 DistinctValues,
673 CountDistinct,
674 ValuesWithIds,
675 TerminalValue { terminal_kind: AggregateKind },
676}
677
678#[derive(Clone, Copy, Debug, Eq, PartialEq)]
688pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
689 terminal: &'static str,
690 field: &'a str,
691 output: &'static str,
692}
693
694impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
695 #[must_use]
697 pub(crate) const fn terminal_label(self) -> &'static str {
698 self.terminal
699 }
700
701 #[must_use]
703 pub(crate) const fn field_label(self) -> &'a str {
704 self.field
705 }
706
707 #[must_use]
709 pub(crate) const fn output_label(self) -> &'static str {
710 self.output
711 }
712}
713
714#[derive(Clone, Debug, Eq, PartialEq)]
725pub(crate) struct PreparedFluentProjectionStrategy {
726 target_field: FieldSlot,
727 runtime_request: PreparedFluentProjectionRuntimeRequest,
728}
729
730impl PreparedFluentProjectionStrategy {
731 #[must_use]
733 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
734 Self {
735 target_field,
736 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
737 }
738 }
739
740 #[must_use]
742 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
743 Self {
744 target_field,
745 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
746 }
747 }
748
749 #[must_use]
751 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
752 Self {
753 target_field,
754 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
755 }
756 }
757
758 #[must_use]
760 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
761 Self {
762 target_field,
763 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
764 }
765 }
766
767 #[must_use]
769 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
770 Self {
771 target_field,
772 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
773 terminal_kind: AggregateKind::First,
774 },
775 }
776 }
777
778 #[must_use]
780 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
781 Self {
782 target_field,
783 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
784 terminal_kind: AggregateKind::Last,
785 },
786 }
787 }
788
789 #[cfg(test)]
792 #[must_use]
793 pub(crate) const fn target_field(&self) -> &FieldSlot {
794 &self.target_field
795 }
796
797 #[cfg(test)]
800 #[must_use]
801 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
802 self.runtime_request
803 }
804
805 #[must_use]
807 pub(in crate::db) fn into_executor_request(
808 self,
809 ) -> (
810 FieldSlot,
811 ScalarProjectionBoundaryRequest,
812 PreparedFluentProjectionRuntimeRequest,
813 ) {
814 let request = match self.runtime_request {
815 PreparedFluentProjectionRuntimeRequest::Values => {
816 ScalarProjectionBoundaryRequest::Values
817 }
818 PreparedFluentProjectionRuntimeRequest::DistinctValues => {
819 ScalarProjectionBoundaryRequest::DistinctValues
820 }
821 PreparedFluentProjectionRuntimeRequest::CountDistinct => {
822 ScalarProjectionBoundaryRequest::CountDistinct
823 }
824 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => {
825 ScalarProjectionBoundaryRequest::ValuesWithIds
826 }
827 PreparedFluentProjectionRuntimeRequest::TerminalValue { terminal_kind } => {
828 ScalarProjectionBoundaryRequest::TerminalValue { terminal_kind }
829 }
830 };
831
832 (self.target_field, request, self.runtime_request)
833 }
834
835 #[must_use]
838 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
839 let terminal_label = match self.runtime_request {
840 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
841 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
842 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
843 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
844 PreparedFluentProjectionRuntimeRequest::TerminalValue {
845 terminal_kind: AggregateKind::First,
846 } => "first_value_by",
847 PreparedFluentProjectionRuntimeRequest::TerminalValue {
848 terminal_kind: AggregateKind::Last,
849 } => "last_value_by",
850 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
851 unreachable!("projection terminal value explain requires FIRST/LAST kind")
852 }
853 };
854 let output_label = match self.runtime_request {
855 PreparedFluentProjectionRuntimeRequest::Values
856 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
857 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
858 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
859 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
860 };
861
862 PreparedFluentProjectionExplainDescriptor {
863 terminal: terminal_label,
864 field: self.target_field.field(),
865 output: output_label,
866 }
867 }
868}
869
870#[must_use]
872pub const fn count() -> AggregateExpr {
873 AggregateExpr::terminal(AggregateKind::Count)
874}
875
876#[must_use]
878pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
879 AggregateExpr::field_target(AggregateKind::Count, field.as_ref().to_string())
880}
881
882#[must_use]
884pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
885 AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
886}
887
888#[must_use]
890pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
891 AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
892}
893
894#[must_use]
896pub const fn exists() -> AggregateExpr {
897 AggregateExpr::terminal(AggregateKind::Exists)
898}
899
900#[must_use]
902pub const fn first() -> AggregateExpr {
903 AggregateExpr::terminal(AggregateKind::First)
904}
905
906#[must_use]
908pub const fn last() -> AggregateExpr {
909 AggregateExpr::terminal(AggregateKind::Last)
910}
911
912#[must_use]
914pub const fn min() -> AggregateExpr {
915 AggregateExpr::terminal(AggregateKind::Min)
916}
917
918#[must_use]
920pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
921 AggregateExpr::field_target(AggregateKind::Min, field.as_ref().to_string())
922}
923
924#[must_use]
926pub const fn max() -> AggregateExpr {
927 AggregateExpr::terminal(AggregateKind::Max)
928}
929
930#[must_use]
932pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
933 AggregateExpr::field_target(AggregateKind::Max, field.as_ref().to_string())
934}
935
936#[cfg(test)]
941mod tests {
942 use crate::db::query::{
943 builder::{
944 PreparedFluentExistingRowsTerminalRuntimeRequest,
945 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
946 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
947 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
948 PreparedFluentProjectionStrategy,
949 },
950 plan::{AggregateKind, FieldSlot},
951 };
952
953 #[test]
954 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
955 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
956 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
957
958 assert_eq!(
959 strategy.aggregate_kind(),
960 AggregateKind::Sum,
961 "sum(distinct field) should preserve SUM aggregate kind",
962 );
963 assert_eq!(
964 strategy.projected_field(),
965 "rank",
966 "sum(distinct field) should preserve projected field labels",
967 );
968 assert!(
969 strategy.aggregate().is_distinct(),
970 "sum(distinct field) should preserve DISTINCT aggregate shape",
971 );
972 assert_eq!(
973 strategy.target_field(),
974 &rank_slot,
975 "sum(distinct field) should preserve the resolved planner field slot",
976 );
977 assert_eq!(
978 strategy.runtime_request(),
979 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
980 "sum(distinct field) should project the numeric DISTINCT runtime request",
981 );
982 }
983
984 #[test]
985 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
986 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
987
988 assert_eq!(
989 strategy.aggregate().kind(),
990 AggregateKind::Count,
991 "count() should preserve the explain-visible aggregate kind",
992 );
993 assert_eq!(
994 strategy.runtime_request(),
995 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
996 "count() should project the existing-rows count runtime request",
997 );
998 }
999
1000 #[test]
1001 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
1002 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
1003
1004 assert_eq!(
1005 strategy.aggregate().kind(),
1006 AggregateKind::Exists,
1007 "exists() should preserve the explain-visible aggregate kind",
1008 );
1009 assert_eq!(
1010 strategy.runtime_request(),
1011 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
1012 "exists() should project the existing-rows exists runtime request",
1013 );
1014 }
1015
1016 #[test]
1017 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
1018 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1019 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
1020
1021 assert_eq!(
1022 strategy.aggregate_kind(),
1023 AggregateKind::Avg,
1024 "avg(field) should preserve AVG aggregate kind",
1025 );
1026 assert_eq!(
1027 strategy.projected_field(),
1028 "rank",
1029 "avg(field) should preserve projected field labels",
1030 );
1031 assert!(
1032 !strategy.aggregate().is_distinct(),
1033 "avg(field) should stay non-distinct unless requested explicitly",
1034 );
1035 assert_eq!(
1036 strategy.target_field(),
1037 &rank_slot,
1038 "avg(field) should preserve the resolved planner field slot",
1039 );
1040 assert_eq!(
1041 strategy.runtime_request(),
1042 PreparedFluentNumericFieldRuntimeRequest::Avg,
1043 "avg(field) should project the numeric AVG runtime request",
1044 );
1045 }
1046
1047 #[test]
1048 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
1049 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
1050
1051 assert_eq!(
1052 strategy
1053 .explain_aggregate()
1054 .map(|aggregate| aggregate.kind()),
1055 Some(AggregateKind::First),
1056 "first() should preserve the explain-visible aggregate kind",
1057 );
1058 assert_eq!(
1059 strategy.runtime_request(),
1060 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
1061 kind: AggregateKind::First,
1062 },
1063 "first() should project the response-order runtime request",
1064 );
1065 }
1066
1067 #[test]
1068 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
1069 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1070 let strategy =
1071 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
1072
1073 assert_eq!(
1074 strategy.explain_aggregate(),
1075 None,
1076 "nth_by(field, nth) should stay off the current explain aggregate surface",
1077 );
1078 assert_eq!(
1079 strategy.runtime_request(),
1080 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
1081 target_field: rank_slot,
1082 nth: 2,
1083 },
1084 "nth_by(field, nth) should preserve the resolved field-order runtime request",
1085 );
1086 }
1087
1088 #[test]
1089 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
1090 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1091 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
1092 let explain = strategy.explain_descriptor();
1093
1094 assert_eq!(
1095 strategy.target_field(),
1096 &rank_slot,
1097 "count_distinct_by(field) should preserve the resolved planner field slot",
1098 );
1099 assert_eq!(
1100 strategy.runtime_request(),
1101 PreparedFluentProjectionRuntimeRequest::CountDistinct,
1102 "count_distinct_by(field) should project the distinct-count runtime request",
1103 );
1104 assert_eq!(
1105 explain.terminal_label(),
1106 "count_distinct_by",
1107 "count_distinct_by(field) should project the stable explain terminal label",
1108 );
1109 assert_eq!(
1110 explain.field_label(),
1111 "rank",
1112 "count_distinct_by(field) should project the stable explain field label",
1113 );
1114 assert_eq!(
1115 explain.output_label(),
1116 "count",
1117 "count_distinct_by(field) should project the stable explain output label",
1118 );
1119 }
1120
1121 #[test]
1122 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1123 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1124 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1125 let explain = strategy.explain_descriptor();
1126
1127 assert_eq!(
1128 strategy.target_field(),
1129 &rank_slot,
1130 "last_value_by(field) should preserve the resolved planner field slot",
1131 );
1132 assert_eq!(
1133 strategy.runtime_request(),
1134 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1135 terminal_kind: AggregateKind::Last,
1136 },
1137 "last_value_by(field) should project the terminal-value runtime request",
1138 );
1139 assert_eq!(
1140 explain.terminal_label(),
1141 "last_value_by",
1142 "last_value_by(field) should project the stable explain terminal label",
1143 );
1144 assert_eq!(
1145 explain.field_label(),
1146 "rank",
1147 "last_value_by(field) should project the stable explain field label",
1148 );
1149 assert_eq!(
1150 explain.output_label(),
1151 "terminal_value",
1152 "last_value_by(field) should project the stable explain output label",
1153 );
1154 }
1155}