1use crate::db::query::plan::{
7 AggregateKind, FieldSlot,
8 expr::{BinaryOp, Expr, FieldId, Function},
9};
10use crate::{
11 db::numeric::{NumericArithmeticOp, apply_numeric_arithmetic},
12 value::Value,
13};
14
15#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct AggregateExpr {
25 kind: AggregateKind,
26 input_expr: Option<Box<Expr>>,
27 distinct: bool,
28}
29
30impl AggregateExpr {
31 const fn terminal(kind: AggregateKind) -> Self {
33 Self {
34 kind,
35 input_expr: None,
36 distinct: false,
37 }
38 }
39
40 fn field_target(kind: AggregateKind, field: impl Into<String>) -> Self {
42 Self {
43 kind,
44 input_expr: Some(Box::new(Expr::Field(FieldId::new(field.into())))),
45 distinct: false,
46 }
47 }
48
49 pub(in crate::db) fn from_expression_input(kind: AggregateKind, input_expr: Expr) -> Self {
51 Self {
52 kind,
53 input_expr: Some(Box::new(canonicalize_aggregate_input_expr(
54 kind, input_expr,
55 ))),
56 distinct: false,
57 }
58 }
59
60 #[must_use]
62 pub const fn distinct(mut self) -> Self {
63 self.distinct = true;
64 self
65 }
66
67 #[must_use]
69 pub(crate) const fn kind(&self) -> AggregateKind {
70 self.kind
71 }
72
73 #[must_use]
75 pub(crate) fn input_expr(&self) -> Option<&Expr> {
76 self.input_expr.as_deref()
77 }
78
79 #[must_use]
81 pub(crate) fn target_field(&self) -> Option<&str> {
82 match self.input_expr() {
83 Some(Expr::Field(field)) => Some(field.as_str()),
84 _ => None,
85 }
86 }
87
88 #[must_use]
90 pub(crate) const fn is_distinct(&self) -> bool {
91 self.distinct
92 }
93
94 pub(in crate::db::query) fn from_semantic_parts(
96 kind: AggregateKind,
97 target_field: Option<String>,
98 distinct: bool,
99 ) -> Self {
100 Self {
101 kind,
102 input_expr: target_field.map(|field| Box::new(Expr::Field(FieldId::new(field)))),
103 distinct,
104 }
105 }
106
107 #[cfg(test)]
109 #[must_use]
110 pub(in crate::db) fn terminal_for_kind(kind: AggregateKind) -> Self {
111 match kind {
112 AggregateKind::Count => count(),
113 AggregateKind::Exists => exists(),
114 AggregateKind::Min => min(),
115 AggregateKind::Max => max(),
116 AggregateKind::First => first(),
117 AggregateKind::Last => last(),
118 AggregateKind::Sum | AggregateKind::Avg => unreachable!(
119 "AggregateExpr::terminal_for_kind does not support SUM/AVG field-target kinds"
120 ),
121 }
122 }
123}
124
125pub(in crate::db) fn canonicalize_aggregate_input_expr(kind: AggregateKind, expr: Expr) -> Expr {
129 let folded =
130 normalize_aggregate_input_numeric_literals(fold_aggregate_input_constant_expr(expr));
131
132 match kind {
133 AggregateKind::Sum | AggregateKind::Avg => match folded {
134 Expr::Literal(value) => value
135 .to_numeric_decimal()
136 .map_or(Expr::Literal(value), |decimal| {
137 Expr::Literal(Value::Decimal(decimal.normalize()))
138 }),
139 other => other,
140 },
141 AggregateKind::Count
142 | AggregateKind::Min
143 | AggregateKind::Max
144 | AggregateKind::Exists
145 | AggregateKind::First
146 | AggregateKind::Last => folded,
147 }
148}
149
150fn fold_aggregate_input_constant_expr(expr: Expr) -> Expr {
153 match expr {
154 Expr::Field(_) | Expr::Literal(_) | Expr::Aggregate(_) => expr,
155 Expr::FunctionCall { function, args } => {
156 let args = args
157 .into_iter()
158 .map(fold_aggregate_input_constant_expr)
159 .collect::<Vec<_>>();
160
161 fold_aggregate_input_constant_function(function, args.as_slice())
162 .unwrap_or(Expr::FunctionCall { function, args })
163 }
164 Expr::Binary { op, left, right } => {
165 let left = fold_aggregate_input_constant_expr(*left);
166 let right = fold_aggregate_input_constant_expr(*right);
167
168 fold_aggregate_input_constant_binary(op, &left, &right).unwrap_or_else(|| {
169 Expr::Binary {
170 op,
171 left: Box::new(left),
172 right: Box::new(right),
173 }
174 })
175 }
176 #[cfg(test)]
177 Expr::Alias { expr, name } => Expr::Alias {
178 expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
179 name,
180 },
181 #[cfg(test)]
182 Expr::Unary { op, expr } => Expr::Unary {
183 op,
184 expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
185 },
186 }
187}
188
189fn fold_aggregate_input_constant_binary(op: BinaryOp, left: &Expr, right: &Expr) -> Option<Expr> {
192 let (Expr::Literal(left), Expr::Literal(right)) = (left, right) else {
193 return None;
194 };
195 if matches!(left, Value::Null) || matches!(right, Value::Null) {
196 return Some(Expr::Literal(Value::Null));
197 }
198
199 let arithmetic_op = match op {
200 BinaryOp::Add => NumericArithmeticOp::Add,
201 BinaryOp::Sub => NumericArithmeticOp::Sub,
202 BinaryOp::Mul => NumericArithmeticOp::Mul,
203 BinaryOp::Div => NumericArithmeticOp::Div,
204 #[cfg(test)]
205 BinaryOp::And | BinaryOp::Eq => return None,
206 };
207 let result = apply_numeric_arithmetic(arithmetic_op, left, right)?;
208
209 Some(Expr::Literal(Value::Decimal(result)))
210}
211
212fn fold_aggregate_input_constant_function(function: Function, args: &[Expr]) -> Option<Expr> {
215 match function {
216 Function::Round => fold_aggregate_input_constant_round(args),
217 Function::Trim
218 | Function::Ltrim
219 | Function::Rtrim
220 | Function::Lower
221 | Function::Upper
222 | Function::Length
223 | Function::Left
224 | Function::Right
225 | Function::StartsWith
226 | Function::EndsWith
227 | Function::Contains
228 | Function::Position
229 | Function::Replace
230 | Function::Substring => None,
231 }
232}
233
234fn fold_aggregate_input_constant_round(args: &[Expr]) -> Option<Expr> {
237 let [Expr::Literal(input), Expr::Literal(scale)] = args else {
238 return None;
239 };
240 if matches!(input, Value::Null) || matches!(scale, Value::Null) {
241 return Some(Expr::Literal(Value::Null));
242 }
243
244 let scale = match scale {
245 Value::Int(value) => u32::try_from(*value).ok()?,
246 Value::Uint(value) => u32::try_from(*value).ok()?,
247 _ => return None,
248 };
249 let decimal = input.to_numeric_decimal()?;
250
251 Some(Expr::Literal(Value::Decimal(decimal.round_dp(scale))))
252}
253
254fn normalize_aggregate_input_numeric_literals(expr: Expr) -> Expr {
258 match expr {
259 Expr::Literal(value) => value
260 .to_numeric_decimal()
261 .map_or(Expr::Literal(value), |decimal| {
262 Expr::Literal(Value::Decimal(decimal.normalize()))
263 }),
264 Expr::Field(_) | Expr::Aggregate(_) => expr,
265 Expr::FunctionCall { function, args } => Expr::FunctionCall {
266 function,
267 args: args
268 .into_iter()
269 .map(normalize_aggregate_input_numeric_literals)
270 .collect(),
271 },
272 Expr::Binary { op, left, right } => Expr::Binary {
273 op,
274 left: Box::new(normalize_aggregate_input_numeric_literals(*left)),
275 right: Box::new(normalize_aggregate_input_numeric_literals(*right)),
276 },
277 #[cfg(test)]
278 Expr::Alias { expr, name } => Expr::Alias {
279 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
280 name,
281 },
282 #[cfg(test)]
283 Expr::Unary { op, expr } => Expr::Unary {
284 op,
285 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
286 },
287 }
288}
289
290pub(crate) trait PreparedFluentAggregateExplainStrategy {
301 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
304
305 fn explain_projected_field(&self) -> Option<&str> {
307 None
308 }
309}
310
311#[derive(Clone, Debug, Eq, PartialEq)]
318pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
319 CountRows,
320 ExistsRows,
321}
322
323#[derive(Clone, Debug, Eq, PartialEq)]
336pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
337 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
338}
339
340impl PreparedFluentExistingRowsTerminalStrategy {
341 #[must_use]
343 pub(crate) const fn count_rows() -> Self {
344 Self {
345 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
346 }
347 }
348
349 #[must_use]
351 pub(crate) const fn exists_rows() -> Self {
352 Self {
353 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
354 }
355 }
356
357 #[cfg(test)]
360 #[must_use]
361 pub(crate) const fn aggregate(&self) -> AggregateExpr {
362 match self.runtime_request {
363 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
364 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
365 }
366 }
367
368 #[cfg(test)]
371 #[must_use]
372 pub(crate) const fn runtime_request(
373 &self,
374 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
375 &self.runtime_request
376 }
377
378 #[must_use]
381 pub(crate) const fn into_runtime_request(
382 self,
383 ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
384 self.runtime_request
385 }
386}
387
388impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
389 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
390 Some(match self.runtime_request {
391 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
392 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
393 })
394 }
395}
396
397#[derive(Clone, Debug, Eq, PartialEq)]
404pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
405 IdTerminal {
406 kind: AggregateKind,
407 },
408 IdBySlot {
409 kind: AggregateKind,
410 target_field: FieldSlot,
411 },
412}
413
414#[derive(Clone, Debug, Eq, PartialEq)]
426pub(crate) struct PreparedFluentScalarTerminalStrategy {
427 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
428}
429
430impl PreparedFluentScalarTerminalStrategy {
431 #[must_use]
433 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
434 Self {
435 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
436 }
437 }
438
439 #[must_use]
442 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
443 Self {
444 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
445 kind,
446 target_field,
447 },
448 }
449 }
450
451 #[must_use]
454 pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
455 self.runtime_request
456 }
457}
458
459impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
460 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
461 Some(match self.runtime_request {
462 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
463 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
464 })
465 }
466
467 fn explain_projected_field(&self) -> Option<&str> {
468 match &self.runtime_request {
469 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
470 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
471 Some(target_field.field())
472 }
473 }
474 }
475}
476
477#[derive(Clone, Copy, Debug, Eq, PartialEq)]
487pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
488 Sum,
489 SumDistinct,
490 Avg,
491 AvgDistinct,
492}
493
494#[derive(Clone, Debug, Eq, PartialEq)]
507pub(crate) struct PreparedFluentNumericFieldStrategy {
508 target_field: FieldSlot,
509 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
510}
511
512impl PreparedFluentNumericFieldStrategy {
513 #[must_use]
515 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
516 Self {
517 target_field,
518 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
519 }
520 }
521
522 #[must_use]
524 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
525 Self {
526 target_field,
527 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
528 }
529 }
530
531 #[must_use]
533 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
534 Self {
535 target_field,
536 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
537 }
538 }
539
540 #[must_use]
542 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
543 Self {
544 target_field,
545 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
546 }
547 }
548
549 #[cfg(test)]
552 #[must_use]
553 pub(crate) fn aggregate(&self) -> AggregateExpr {
554 let field = self.target_field.field();
555
556 match self.runtime_request {
557 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
558 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
559 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
560 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
561 }
562 }
563
564 #[cfg(test)]
567 #[must_use]
568 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
569 match self.runtime_request {
570 PreparedFluentNumericFieldRuntimeRequest::Sum
571 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
572 PreparedFluentNumericFieldRuntimeRequest::Avg
573 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
574 }
575 }
576
577 #[cfg(test)]
580 #[must_use]
581 pub(crate) fn projected_field(&self) -> &str {
582 self.target_field.field()
583 }
584
585 #[cfg(test)]
588 #[must_use]
589 pub(crate) const fn target_field(&self) -> &FieldSlot {
590 &self.target_field
591 }
592
593 #[cfg(test)]
596 #[must_use]
597 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
598 self.runtime_request
599 }
600
601 #[must_use]
604 pub(crate) fn into_runtime_parts(
605 self,
606 ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
607 (self.target_field, self.runtime_request)
608 }
609}
610
611impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
612 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
613 Some(match self.runtime_request {
614 PreparedFluentNumericFieldRuntimeRequest::Sum
615 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
616 PreparedFluentNumericFieldRuntimeRequest::Avg
617 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
618 })
619 }
620
621 fn explain_projected_field(&self) -> Option<&str> {
622 Some(self.target_field.field())
623 }
624}
625
626#[derive(Clone, Debug, Eq, PartialEq)]
637pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
638 ResponseOrder { kind: AggregateKind },
639 NthBySlot { target_field: FieldSlot, nth: usize },
640 MedianBySlot { target_field: FieldSlot },
641 MinMaxBySlot { target_field: FieldSlot },
642}
643
644#[derive(Clone, Debug, Eq, PartialEq)]
655pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
656 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
657}
658
659impl PreparedFluentOrderSensitiveTerminalStrategy {
660 #[must_use]
662 pub(crate) const fn first() -> Self {
663 Self {
664 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
665 kind: AggregateKind::First,
666 },
667 }
668 }
669
670 #[must_use]
672 pub(crate) const fn last() -> Self {
673 Self {
674 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
675 kind: AggregateKind::Last,
676 },
677 }
678 }
679
680 #[must_use]
682 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
683 Self {
684 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
685 target_field,
686 nth,
687 },
688 }
689 }
690
691 #[must_use]
693 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
694 Self {
695 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
696 target_field,
697 },
698 }
699 }
700
701 #[must_use]
703 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
704 Self {
705 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
706 target_field,
707 },
708 }
709 }
710
711 #[cfg(test)]
714 #[must_use]
715 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
716 match self.runtime_request {
717 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
718 Some(AggregateExpr::terminal_for_kind(kind))
719 }
720 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
721 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
722 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
723 }
724 }
725
726 #[cfg(test)]
729 #[must_use]
730 pub(crate) const fn runtime_request(
731 &self,
732 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
733 &self.runtime_request
734 }
735
736 #[must_use]
739 pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
740 self.runtime_request
741 }
742}
743
744impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
745 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
746 match self.runtime_request {
747 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
748 Some(kind)
749 }
750 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
751 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
752 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
753 }
754 }
755}
756
757#[derive(Clone, Copy, Debug, Eq, PartialEq)]
767pub(crate) enum PreparedFluentProjectionRuntimeRequest {
768 Values,
769 DistinctValues,
770 CountDistinct,
771 ValuesWithIds,
772 TerminalValue { terminal_kind: AggregateKind },
773}
774
775#[derive(Clone, Copy, Debug, Eq, PartialEq)]
785pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
786 terminal: &'static str,
787 field: &'a str,
788 output: &'static str,
789}
790
791impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
792 #[must_use]
794 pub(crate) const fn terminal_label(self) -> &'static str {
795 self.terminal
796 }
797
798 #[must_use]
800 pub(crate) const fn field_label(self) -> &'a str {
801 self.field
802 }
803
804 #[must_use]
806 pub(crate) const fn output_label(self) -> &'static str {
807 self.output
808 }
809}
810
811#[derive(Clone, Debug, Eq, PartialEq)]
822pub(crate) struct PreparedFluentProjectionStrategy {
823 target_field: FieldSlot,
824 runtime_request: PreparedFluentProjectionRuntimeRequest,
825}
826
827impl PreparedFluentProjectionStrategy {
828 #[must_use]
830 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
831 Self {
832 target_field,
833 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
834 }
835 }
836
837 #[must_use]
839 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
840 Self {
841 target_field,
842 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
843 }
844 }
845
846 #[must_use]
848 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
849 Self {
850 target_field,
851 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
852 }
853 }
854
855 #[must_use]
857 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
858 Self {
859 target_field,
860 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
861 }
862 }
863
864 #[must_use]
866 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
867 Self {
868 target_field,
869 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
870 terminal_kind: AggregateKind::First,
871 },
872 }
873 }
874
875 #[must_use]
877 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
878 Self {
879 target_field,
880 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
881 terminal_kind: AggregateKind::Last,
882 },
883 }
884 }
885
886 #[cfg(test)]
889 #[must_use]
890 pub(crate) const fn target_field(&self) -> &FieldSlot {
891 &self.target_field
892 }
893
894 #[cfg(test)]
897 #[must_use]
898 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
899 self.runtime_request
900 }
901
902 #[must_use]
906 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
907 (self.target_field, self.runtime_request)
908 }
909
910 #[must_use]
913 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
914 let terminal_label = match self.runtime_request {
915 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
916 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
917 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
918 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
919 PreparedFluentProjectionRuntimeRequest::TerminalValue {
920 terminal_kind: AggregateKind::First,
921 } => "first_value_by",
922 PreparedFluentProjectionRuntimeRequest::TerminalValue {
923 terminal_kind: AggregateKind::Last,
924 } => "last_value_by",
925 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
926 unreachable!("projection terminal value explain requires FIRST/LAST kind")
927 }
928 };
929 let output_label = match self.runtime_request {
930 PreparedFluentProjectionRuntimeRequest::Values
931 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
932 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
933 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
934 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
935 };
936
937 PreparedFluentProjectionExplainDescriptor {
938 terminal: terminal_label,
939 field: self.target_field.field(),
940 output: output_label,
941 }
942 }
943}
944
945#[must_use]
947pub const fn count() -> AggregateExpr {
948 AggregateExpr::terminal(AggregateKind::Count)
949}
950
951#[must_use]
953pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
954 AggregateExpr::field_target(AggregateKind::Count, field.as_ref().to_string())
955}
956
957#[must_use]
959pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
960 AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
961}
962
963#[must_use]
965pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
966 AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
967}
968
969#[must_use]
971pub const fn exists() -> AggregateExpr {
972 AggregateExpr::terminal(AggregateKind::Exists)
973}
974
975#[must_use]
977pub const fn first() -> AggregateExpr {
978 AggregateExpr::terminal(AggregateKind::First)
979}
980
981#[must_use]
983pub const fn last() -> AggregateExpr {
984 AggregateExpr::terminal(AggregateKind::Last)
985}
986
987#[must_use]
989pub const fn min() -> AggregateExpr {
990 AggregateExpr::terminal(AggregateKind::Min)
991}
992
993#[must_use]
995pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
996 AggregateExpr::field_target(AggregateKind::Min, field.as_ref().to_string())
997}
998
999#[must_use]
1001pub const fn max() -> AggregateExpr {
1002 AggregateExpr::terminal(AggregateKind::Max)
1003}
1004
1005#[must_use]
1007pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
1008 AggregateExpr::field_target(AggregateKind::Max, field.as_ref().to_string())
1009}
1010
1011#[cfg(test)]
1016mod tests {
1017 use crate::db::query::{
1018 builder::{
1019 PreparedFluentExistingRowsTerminalRuntimeRequest,
1020 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
1021 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
1022 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
1023 PreparedFluentProjectionStrategy,
1024 },
1025 plan::{AggregateKind, FieldSlot},
1026 };
1027
1028 #[test]
1029 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
1030 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1031 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
1032
1033 assert_eq!(
1034 strategy.aggregate_kind(),
1035 AggregateKind::Sum,
1036 "sum(distinct field) should preserve SUM aggregate kind",
1037 );
1038 assert_eq!(
1039 strategy.projected_field(),
1040 "rank",
1041 "sum(distinct field) should preserve projected field labels",
1042 );
1043 assert!(
1044 strategy.aggregate().is_distinct(),
1045 "sum(distinct field) should preserve DISTINCT aggregate shape",
1046 );
1047 assert_eq!(
1048 strategy.target_field(),
1049 &rank_slot,
1050 "sum(distinct field) should preserve the resolved planner field slot",
1051 );
1052 assert_eq!(
1053 strategy.runtime_request(),
1054 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
1055 "sum(distinct field) should project the numeric DISTINCT runtime request",
1056 );
1057 }
1058
1059 #[test]
1060 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
1061 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
1062
1063 assert_eq!(
1064 strategy.aggregate().kind(),
1065 AggregateKind::Count,
1066 "count() should preserve the explain-visible aggregate kind",
1067 );
1068 assert_eq!(
1069 strategy.runtime_request(),
1070 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
1071 "count() should project the existing-rows count runtime request",
1072 );
1073 }
1074
1075 #[test]
1076 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
1077 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
1078
1079 assert_eq!(
1080 strategy.aggregate().kind(),
1081 AggregateKind::Exists,
1082 "exists() should preserve the explain-visible aggregate kind",
1083 );
1084 assert_eq!(
1085 strategy.runtime_request(),
1086 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
1087 "exists() should project the existing-rows exists runtime request",
1088 );
1089 }
1090
1091 #[test]
1092 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
1093 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1094 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
1095
1096 assert_eq!(
1097 strategy.aggregate_kind(),
1098 AggregateKind::Avg,
1099 "avg(field) should preserve AVG aggregate kind",
1100 );
1101 assert_eq!(
1102 strategy.projected_field(),
1103 "rank",
1104 "avg(field) should preserve projected field labels",
1105 );
1106 assert!(
1107 !strategy.aggregate().is_distinct(),
1108 "avg(field) should stay non-distinct unless requested explicitly",
1109 );
1110 assert_eq!(
1111 strategy.target_field(),
1112 &rank_slot,
1113 "avg(field) should preserve the resolved planner field slot",
1114 );
1115 assert_eq!(
1116 strategy.runtime_request(),
1117 PreparedFluentNumericFieldRuntimeRequest::Avg,
1118 "avg(field) should project the numeric AVG runtime request",
1119 );
1120 }
1121
1122 #[test]
1123 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
1124 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
1125
1126 assert_eq!(
1127 strategy
1128 .explain_aggregate()
1129 .map(|aggregate| aggregate.kind()),
1130 Some(AggregateKind::First),
1131 "first() should preserve the explain-visible aggregate kind",
1132 );
1133 assert_eq!(
1134 strategy.runtime_request(),
1135 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
1136 kind: AggregateKind::First,
1137 },
1138 "first() should project the response-order runtime request",
1139 );
1140 }
1141
1142 #[test]
1143 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
1144 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1145 let strategy =
1146 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
1147
1148 assert_eq!(
1149 strategy.explain_aggregate(),
1150 None,
1151 "nth_by(field, nth) should stay off the current explain aggregate surface",
1152 );
1153 assert_eq!(
1154 strategy.runtime_request(),
1155 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
1156 target_field: rank_slot,
1157 nth: 2,
1158 },
1159 "nth_by(field, nth) should preserve the resolved field-order runtime request",
1160 );
1161 }
1162
1163 #[test]
1164 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
1165 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1166 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
1167 let explain = strategy.explain_descriptor();
1168
1169 assert_eq!(
1170 strategy.target_field(),
1171 &rank_slot,
1172 "count_distinct_by(field) should preserve the resolved planner field slot",
1173 );
1174 assert_eq!(
1175 strategy.runtime_request(),
1176 PreparedFluentProjectionRuntimeRequest::CountDistinct,
1177 "count_distinct_by(field) should project the distinct-count runtime request",
1178 );
1179 assert_eq!(
1180 explain.terminal_label(),
1181 "count_distinct_by",
1182 "count_distinct_by(field) should project the stable explain terminal label",
1183 );
1184 assert_eq!(
1185 explain.field_label(),
1186 "rank",
1187 "count_distinct_by(field) should project the stable explain field label",
1188 );
1189 assert_eq!(
1190 explain.output_label(),
1191 "count",
1192 "count_distinct_by(field) should project the stable explain output label",
1193 );
1194 }
1195
1196 #[test]
1197 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1198 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1199 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1200 let explain = strategy.explain_descriptor();
1201
1202 assert_eq!(
1203 strategy.target_field(),
1204 &rank_slot,
1205 "last_value_by(field) should preserve the resolved planner field slot",
1206 );
1207 assert_eq!(
1208 strategy.runtime_request(),
1209 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1210 terminal_kind: AggregateKind::Last,
1211 },
1212 "last_value_by(field) should project the terminal-value runtime request",
1213 );
1214 assert_eq!(
1215 explain.terminal_label(),
1216 "last_value_by",
1217 "last_value_by(field) should project the stable explain terminal label",
1218 );
1219 assert_eq!(
1220 explain.field_label(),
1221 "rank",
1222 "last_value_by(field) should project the stable explain field label",
1223 );
1224 assert_eq!(
1225 explain.output_label(),
1226 "terminal_value",
1227 "last_value_by(field) should project the stable explain output label",
1228 );
1229 }
1230}