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)]
25pub struct AggregateExpr {
26 kind: AggregateKind,
27 input_expr: Option<Box<Expr>>,
28 filter_expr: Option<Box<Expr>>,
29 distinct: bool,
30}
31
32impl AggregateExpr {
33 const fn terminal(kind: AggregateKind) -> Self {
35 Self {
36 kind,
37 input_expr: None,
38 filter_expr: None,
39 distinct: false,
40 }
41 }
42
43 fn field_target(kind: AggregateKind, field: impl Into<String>) -> Self {
45 Self {
46 kind,
47 input_expr: Some(Box::new(Expr::Field(FieldId::new(field.into())))),
48 filter_expr: None,
49 distinct: false,
50 }
51 }
52
53 pub(in crate::db) fn from_expression_input(kind: AggregateKind, input_expr: Expr) -> Self {
55 Self {
56 kind,
57 input_expr: Some(Box::new(canonicalize_aggregate_input_expr(
58 kind, input_expr,
59 ))),
60 filter_expr: None,
61 distinct: false,
62 }
63 }
64
65 #[must_use]
67 pub(in crate::db) fn with_filter_expr(mut self, filter_expr: Expr) -> Self {
68 self.filter_expr = Some(Box::new(filter_expr));
69 self
70 }
71
72 #[must_use]
74 pub const fn distinct(mut self) -> Self {
75 self.distinct = true;
76 self
77 }
78
79 #[must_use]
81 pub(crate) const fn kind(&self) -> AggregateKind {
82 self.kind
83 }
84
85 #[must_use]
87 pub(crate) fn input_expr(&self) -> Option<&Expr> {
88 self.input_expr.as_deref()
89 }
90
91 #[must_use]
93 pub(crate) fn filter_expr(&self) -> Option<&Expr> {
94 self.filter_expr.as_deref()
95 }
96
97 #[must_use]
99 pub(crate) fn target_field(&self) -> Option<&str> {
100 match self.input_expr() {
101 Some(Expr::Field(field)) => Some(field.as_str()),
102 _ => None,
103 }
104 }
105
106 #[must_use]
108 pub(crate) const fn is_distinct(&self) -> bool {
109 self.distinct
110 }
111
112 pub(in crate::db::query) fn from_semantic_parts(
114 kind: AggregateKind,
115 target_field: Option<String>,
116 distinct: bool,
117 ) -> Self {
118 Self {
119 kind,
120 input_expr: target_field.map(|field| Box::new(Expr::Field(FieldId::new(field)))),
121 filter_expr: None,
122 distinct,
123 }
124 }
125
126 #[cfg(test)]
128 #[must_use]
129 pub(in crate::db) fn terminal_for_kind(kind: AggregateKind) -> Self {
130 match kind {
131 AggregateKind::Count => count(),
132 AggregateKind::Exists => exists(),
133 AggregateKind::Min => min(),
134 AggregateKind::Max => max(),
135 AggregateKind::First => first(),
136 AggregateKind::Last => last(),
137 AggregateKind::Sum | AggregateKind::Avg => unreachable!(
138 "AggregateExpr::terminal_for_kind does not support SUM/AVG field-target kinds"
139 ),
140 }
141 }
142}
143
144pub(in crate::db) fn canonicalize_aggregate_input_expr(kind: AggregateKind, expr: Expr) -> Expr {
148 let folded =
149 normalize_aggregate_input_numeric_literals(fold_aggregate_input_constant_expr(expr));
150
151 match kind {
152 AggregateKind::Sum | AggregateKind::Avg => match folded {
153 Expr::Literal(value) => value
154 .to_numeric_decimal()
155 .map_or(Expr::Literal(value), |decimal| {
156 Expr::Literal(Value::Decimal(decimal.normalize()))
157 }),
158 other => other,
159 },
160 AggregateKind::Count
161 | AggregateKind::Min
162 | AggregateKind::Max
163 | AggregateKind::Exists
164 | AggregateKind::First
165 | AggregateKind::Last => folded,
166 }
167}
168
169fn fold_aggregate_input_constant_expr(expr: Expr) -> Expr {
172 match expr {
173 Expr::Field(_) | Expr::Literal(_) | Expr::Aggregate(_) => expr,
174 Expr::FunctionCall { function, args } => {
175 let args = args
176 .into_iter()
177 .map(fold_aggregate_input_constant_expr)
178 .collect::<Vec<_>>();
179
180 fold_aggregate_input_constant_function(function, args.as_slice())
181 .unwrap_or(Expr::FunctionCall { function, args })
182 }
183 Expr::Case {
184 when_then_arms,
185 else_expr,
186 } => Expr::Case {
187 when_then_arms: when_then_arms
188 .into_iter()
189 .map(|arm| {
190 crate::db::query::plan::expr::CaseWhenArm::new(
191 fold_aggregate_input_constant_expr(arm.condition().clone()),
192 fold_aggregate_input_constant_expr(arm.result().clone()),
193 )
194 })
195 .collect(),
196 else_expr: Box::new(fold_aggregate_input_constant_expr(*else_expr)),
197 },
198 Expr::Binary { op, left, right } => {
199 let left = fold_aggregate_input_constant_expr(*left);
200 let right = fold_aggregate_input_constant_expr(*right);
201
202 fold_aggregate_input_constant_binary(op, &left, &right).unwrap_or_else(|| {
203 Expr::Binary {
204 op,
205 left: Box::new(left),
206 right: Box::new(right),
207 }
208 })
209 }
210 #[cfg(test)]
211 Expr::Alias { expr, name } => Expr::Alias {
212 expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
213 name,
214 },
215 Expr::Unary { op, expr } => Expr::Unary {
216 op,
217 expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
218 },
219 }
220}
221
222fn fold_aggregate_input_constant_binary(op: BinaryOp, left: &Expr, right: &Expr) -> Option<Expr> {
225 let (Expr::Literal(left), Expr::Literal(right)) = (left, right) else {
226 return None;
227 };
228 if matches!(left, Value::Null) || matches!(right, Value::Null) {
229 return Some(Expr::Literal(Value::Null));
230 }
231
232 let arithmetic_op = match op {
233 BinaryOp::Or
234 | BinaryOp::And
235 | BinaryOp::Eq
236 | BinaryOp::Ne
237 | BinaryOp::Lt
238 | BinaryOp::Lte
239 | BinaryOp::Gt
240 | BinaryOp::Gte => return None,
241 BinaryOp::Add => NumericArithmeticOp::Add,
242 BinaryOp::Sub => NumericArithmeticOp::Sub,
243 BinaryOp::Mul => NumericArithmeticOp::Mul,
244 BinaryOp::Div => NumericArithmeticOp::Div,
245 };
246 let result = apply_numeric_arithmetic(arithmetic_op, left, right)?;
247
248 Some(Expr::Literal(Value::Decimal(result)))
249}
250
251fn fold_aggregate_input_constant_function(function: Function, args: &[Expr]) -> Option<Expr> {
254 match function {
255 Function::Round => fold_aggregate_input_constant_round(args),
256 Function::IsNull
257 | Function::IsNotNull
258 | Function::IsMissing
259 | Function::IsEmpty
260 | Function::IsNotEmpty
261 | Function::Trim
262 | Function::Ltrim
263 | Function::Rtrim
264 | Function::Lower
265 | Function::Upper
266 | Function::Length
267 | Function::Left
268 | Function::Right
269 | Function::StartsWith
270 | Function::EndsWith
271 | Function::Contains
272 | Function::CollectionContains
273 | Function::Position
274 | Function::Replace
275 | Function::Substring => None,
276 }
277}
278
279fn fold_aggregate_input_constant_round(args: &[Expr]) -> Option<Expr> {
282 let [Expr::Literal(input), Expr::Literal(scale)] = args else {
283 return None;
284 };
285 if matches!(input, Value::Null) || matches!(scale, Value::Null) {
286 return Some(Expr::Literal(Value::Null));
287 }
288
289 let scale = match scale {
290 Value::Int(value) => u32::try_from(*value).ok()?,
291 Value::Uint(value) => u32::try_from(*value).ok()?,
292 _ => return None,
293 };
294 let decimal = input.to_numeric_decimal()?;
295
296 Some(Expr::Literal(Value::Decimal(decimal.round_dp(scale))))
297}
298
299fn normalize_aggregate_input_numeric_literals(expr: Expr) -> Expr {
303 match expr {
304 Expr::Literal(value) => value
305 .to_numeric_decimal()
306 .map_or(Expr::Literal(value), |decimal| {
307 Expr::Literal(Value::Decimal(decimal.normalize()))
308 }),
309 Expr::Field(_) | Expr::Aggregate(_) => expr,
310 Expr::FunctionCall { function, args } => Expr::FunctionCall {
311 function,
312 args: args
313 .into_iter()
314 .map(normalize_aggregate_input_numeric_literals)
315 .collect(),
316 },
317 Expr::Case {
318 when_then_arms,
319 else_expr,
320 } => Expr::Case {
321 when_then_arms: when_then_arms
322 .into_iter()
323 .map(|arm| {
324 crate::db::query::plan::expr::CaseWhenArm::new(
325 normalize_aggregate_input_numeric_literals(arm.condition().clone()),
326 normalize_aggregate_input_numeric_literals(arm.result().clone()),
327 )
328 })
329 .collect(),
330 else_expr: Box::new(normalize_aggregate_input_numeric_literals(*else_expr)),
331 },
332 Expr::Binary { op, left, right } => Expr::Binary {
333 op,
334 left: Box::new(normalize_aggregate_input_numeric_literals(*left)),
335 right: Box::new(normalize_aggregate_input_numeric_literals(*right)),
336 },
337 #[cfg(test)]
338 Expr::Alias { expr, name } => Expr::Alias {
339 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
340 name,
341 },
342 Expr::Unary { op, expr } => Expr::Unary {
343 op,
344 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
345 },
346 }
347}
348
349pub(crate) trait PreparedFluentAggregateExplainStrategy {
360 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
363
364 fn explain_projected_field(&self) -> Option<&str> {
366 None
367 }
368}
369
370#[derive(Clone, Debug, Eq, PartialEq)]
377pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
378 CountRows,
379 ExistsRows,
380}
381
382#[derive(Clone, Debug, Eq, PartialEq)]
395pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
396 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
397}
398
399impl PreparedFluentExistingRowsTerminalStrategy {
400 #[must_use]
402 pub(crate) const fn count_rows() -> Self {
403 Self {
404 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
405 }
406 }
407
408 #[must_use]
410 pub(crate) const fn exists_rows() -> Self {
411 Self {
412 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
413 }
414 }
415
416 #[cfg(test)]
419 #[must_use]
420 pub(crate) const fn aggregate(&self) -> AggregateExpr {
421 match self.runtime_request {
422 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
423 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
424 }
425 }
426
427 #[cfg(test)]
430 #[must_use]
431 pub(crate) const fn runtime_request(
432 &self,
433 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
434 &self.runtime_request
435 }
436
437 #[must_use]
440 pub(crate) const fn into_runtime_request(
441 self,
442 ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
443 self.runtime_request
444 }
445}
446
447impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
448 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
449 Some(match self.runtime_request {
450 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
451 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
452 })
453 }
454}
455
456#[derive(Clone, Debug, Eq, PartialEq)]
463pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
464 IdTerminal {
465 kind: AggregateKind,
466 },
467 IdBySlot {
468 kind: AggregateKind,
469 target_field: FieldSlot,
470 },
471}
472
473#[derive(Clone, Debug, Eq, PartialEq)]
485pub(crate) struct PreparedFluentScalarTerminalStrategy {
486 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
487}
488
489impl PreparedFluentScalarTerminalStrategy {
490 #[must_use]
492 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
493 Self {
494 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
495 }
496 }
497
498 #[must_use]
501 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
502 Self {
503 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
504 kind,
505 target_field,
506 },
507 }
508 }
509
510 #[must_use]
513 pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
514 self.runtime_request
515 }
516}
517
518impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
519 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
520 Some(match self.runtime_request {
521 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
522 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
523 })
524 }
525
526 fn explain_projected_field(&self) -> Option<&str> {
527 match &self.runtime_request {
528 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
529 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
530 Some(target_field.field())
531 }
532 }
533 }
534}
535
536#[derive(Clone, Copy, Debug, Eq, PartialEq)]
546pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
547 Sum,
548 SumDistinct,
549 Avg,
550 AvgDistinct,
551}
552
553#[derive(Clone, Debug, Eq, PartialEq)]
566pub(crate) struct PreparedFluentNumericFieldStrategy {
567 target_field: FieldSlot,
568 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
569}
570
571impl PreparedFluentNumericFieldStrategy {
572 #[must_use]
574 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
575 Self {
576 target_field,
577 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
578 }
579 }
580
581 #[must_use]
583 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
584 Self {
585 target_field,
586 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
587 }
588 }
589
590 #[must_use]
592 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
593 Self {
594 target_field,
595 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
596 }
597 }
598
599 #[must_use]
601 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
602 Self {
603 target_field,
604 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
605 }
606 }
607
608 #[cfg(test)]
611 #[must_use]
612 pub(crate) fn aggregate(&self) -> AggregateExpr {
613 let field = self.target_field.field();
614
615 match self.runtime_request {
616 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
617 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
618 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
619 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
620 }
621 }
622
623 #[cfg(test)]
626 #[must_use]
627 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
628 match self.runtime_request {
629 PreparedFluentNumericFieldRuntimeRequest::Sum
630 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
631 PreparedFluentNumericFieldRuntimeRequest::Avg
632 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
633 }
634 }
635
636 #[cfg(test)]
639 #[must_use]
640 pub(crate) fn projected_field(&self) -> &str {
641 self.target_field.field()
642 }
643
644 #[cfg(test)]
647 #[must_use]
648 pub(crate) const fn target_field(&self) -> &FieldSlot {
649 &self.target_field
650 }
651
652 #[cfg(test)]
655 #[must_use]
656 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
657 self.runtime_request
658 }
659
660 #[must_use]
663 pub(crate) fn into_runtime_parts(
664 self,
665 ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
666 (self.target_field, self.runtime_request)
667 }
668}
669
670impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
671 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
672 Some(match self.runtime_request {
673 PreparedFluentNumericFieldRuntimeRequest::Sum
674 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
675 PreparedFluentNumericFieldRuntimeRequest::Avg
676 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
677 })
678 }
679
680 fn explain_projected_field(&self) -> Option<&str> {
681 Some(self.target_field.field())
682 }
683}
684
685#[derive(Clone, Debug, Eq, PartialEq)]
696pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
697 ResponseOrder { kind: AggregateKind },
698 NthBySlot { target_field: FieldSlot, nth: usize },
699 MedianBySlot { target_field: FieldSlot },
700 MinMaxBySlot { target_field: FieldSlot },
701}
702
703#[derive(Clone, Debug, Eq, PartialEq)]
714pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
715 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
716}
717
718impl PreparedFluentOrderSensitiveTerminalStrategy {
719 #[must_use]
721 pub(crate) const fn first() -> Self {
722 Self {
723 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
724 kind: AggregateKind::First,
725 },
726 }
727 }
728
729 #[must_use]
731 pub(crate) const fn last() -> Self {
732 Self {
733 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
734 kind: AggregateKind::Last,
735 },
736 }
737 }
738
739 #[must_use]
741 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
742 Self {
743 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
744 target_field,
745 nth,
746 },
747 }
748 }
749
750 #[must_use]
752 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
753 Self {
754 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
755 target_field,
756 },
757 }
758 }
759
760 #[must_use]
762 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
763 Self {
764 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
765 target_field,
766 },
767 }
768 }
769
770 #[cfg(test)]
773 #[must_use]
774 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
775 match self.runtime_request {
776 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
777 Some(AggregateExpr::terminal_for_kind(kind))
778 }
779 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
780 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
781 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
782 }
783 }
784
785 #[cfg(test)]
788 #[must_use]
789 pub(crate) const fn runtime_request(
790 &self,
791 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
792 &self.runtime_request
793 }
794
795 #[must_use]
798 pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
799 self.runtime_request
800 }
801}
802
803impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
804 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
805 match self.runtime_request {
806 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
807 Some(kind)
808 }
809 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
810 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
811 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
812 }
813 }
814}
815
816#[derive(Clone, Copy, Debug, Eq, PartialEq)]
826pub(crate) enum PreparedFluentProjectionRuntimeRequest {
827 Values,
828 DistinctValues,
829 CountDistinct,
830 ValuesWithIds,
831 TerminalValue { terminal_kind: AggregateKind },
832}
833
834#[derive(Clone, Copy, Debug, Eq, PartialEq)]
844pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
845 terminal: &'static str,
846 field: &'a str,
847 output: &'static str,
848}
849
850impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
851 #[must_use]
853 pub(crate) const fn terminal_label(self) -> &'static str {
854 self.terminal
855 }
856
857 #[must_use]
859 pub(crate) const fn field_label(self) -> &'a str {
860 self.field
861 }
862
863 #[must_use]
865 pub(crate) const fn output_label(self) -> &'static str {
866 self.output
867 }
868}
869
870#[derive(Clone, Debug, Eq, PartialEq)]
881pub(crate) struct PreparedFluentProjectionStrategy {
882 target_field: FieldSlot,
883 runtime_request: PreparedFluentProjectionRuntimeRequest,
884}
885
886impl PreparedFluentProjectionStrategy {
887 #[must_use]
889 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
890 Self {
891 target_field,
892 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
893 }
894 }
895
896 #[must_use]
898 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
899 Self {
900 target_field,
901 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
902 }
903 }
904
905 #[must_use]
907 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
908 Self {
909 target_field,
910 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
911 }
912 }
913
914 #[must_use]
916 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
917 Self {
918 target_field,
919 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
920 }
921 }
922
923 #[must_use]
925 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
926 Self {
927 target_field,
928 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
929 terminal_kind: AggregateKind::First,
930 },
931 }
932 }
933
934 #[must_use]
936 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
937 Self {
938 target_field,
939 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
940 terminal_kind: AggregateKind::Last,
941 },
942 }
943 }
944
945 #[cfg(test)]
948 #[must_use]
949 pub(crate) const fn target_field(&self) -> &FieldSlot {
950 &self.target_field
951 }
952
953 #[cfg(test)]
956 #[must_use]
957 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
958 self.runtime_request
959 }
960
961 #[must_use]
965 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
966 (self.target_field, self.runtime_request)
967 }
968
969 #[must_use]
972 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
973 let terminal_label = match self.runtime_request {
974 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
975 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
976 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
977 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
978 PreparedFluentProjectionRuntimeRequest::TerminalValue {
979 terminal_kind: AggregateKind::First,
980 } => "first_value_by",
981 PreparedFluentProjectionRuntimeRequest::TerminalValue {
982 terminal_kind: AggregateKind::Last,
983 } => "last_value_by",
984 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
985 unreachable!("projection terminal value explain requires FIRST/LAST kind")
986 }
987 };
988 let output_label = match self.runtime_request {
989 PreparedFluentProjectionRuntimeRequest::Values
990 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
991 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
992 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
993 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
994 };
995
996 PreparedFluentProjectionExplainDescriptor {
997 terminal: terminal_label,
998 field: self.target_field.field(),
999 output: output_label,
1000 }
1001 }
1002}
1003
1004#[must_use]
1006pub const fn count() -> AggregateExpr {
1007 AggregateExpr::terminal(AggregateKind::Count)
1008}
1009
1010#[must_use]
1012pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
1013 AggregateExpr::field_target(AggregateKind::Count, field.as_ref().to_string())
1014}
1015
1016#[must_use]
1018pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
1019 AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
1020}
1021
1022#[must_use]
1024pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
1025 AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
1026}
1027
1028#[must_use]
1030pub const fn exists() -> AggregateExpr {
1031 AggregateExpr::terminal(AggregateKind::Exists)
1032}
1033
1034#[must_use]
1036pub const fn first() -> AggregateExpr {
1037 AggregateExpr::terminal(AggregateKind::First)
1038}
1039
1040#[must_use]
1042pub const fn last() -> AggregateExpr {
1043 AggregateExpr::terminal(AggregateKind::Last)
1044}
1045
1046#[must_use]
1048pub const fn min() -> AggregateExpr {
1049 AggregateExpr::terminal(AggregateKind::Min)
1050}
1051
1052#[must_use]
1054pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
1055 AggregateExpr::field_target(AggregateKind::Min, field.as_ref().to_string())
1056}
1057
1058#[must_use]
1060pub const fn max() -> AggregateExpr {
1061 AggregateExpr::terminal(AggregateKind::Max)
1062}
1063
1064#[must_use]
1066pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
1067 AggregateExpr::field_target(AggregateKind::Max, field.as_ref().to_string())
1068}
1069
1070#[cfg(test)]
1075mod tests {
1076 use crate::db::query::{
1077 builder::{
1078 PreparedFluentExistingRowsTerminalRuntimeRequest,
1079 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
1080 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
1081 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
1082 PreparedFluentProjectionStrategy,
1083 },
1084 plan::{AggregateKind, FieldSlot},
1085 };
1086
1087 #[test]
1088 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
1089 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1090 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
1091
1092 assert_eq!(
1093 strategy.aggregate_kind(),
1094 AggregateKind::Sum,
1095 "sum(distinct field) should preserve SUM aggregate kind",
1096 );
1097 assert_eq!(
1098 strategy.projected_field(),
1099 "rank",
1100 "sum(distinct field) should preserve projected field labels",
1101 );
1102 assert!(
1103 strategy.aggregate().is_distinct(),
1104 "sum(distinct field) should preserve DISTINCT aggregate shape",
1105 );
1106 assert_eq!(
1107 strategy.target_field(),
1108 &rank_slot,
1109 "sum(distinct field) should preserve the resolved planner field slot",
1110 );
1111 assert_eq!(
1112 strategy.runtime_request(),
1113 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
1114 "sum(distinct field) should project the numeric DISTINCT runtime request",
1115 );
1116 }
1117
1118 #[test]
1119 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
1120 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
1121
1122 assert_eq!(
1123 strategy.aggregate().kind(),
1124 AggregateKind::Count,
1125 "count() should preserve the explain-visible aggregate kind",
1126 );
1127 assert_eq!(
1128 strategy.runtime_request(),
1129 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
1130 "count() should project the existing-rows count runtime request",
1131 );
1132 }
1133
1134 #[test]
1135 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
1136 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
1137
1138 assert_eq!(
1139 strategy.aggregate().kind(),
1140 AggregateKind::Exists,
1141 "exists() should preserve the explain-visible aggregate kind",
1142 );
1143 assert_eq!(
1144 strategy.runtime_request(),
1145 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
1146 "exists() should project the existing-rows exists runtime request",
1147 );
1148 }
1149
1150 #[test]
1151 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
1152 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1153 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
1154
1155 assert_eq!(
1156 strategy.aggregate_kind(),
1157 AggregateKind::Avg,
1158 "avg(field) should preserve AVG aggregate kind",
1159 );
1160 assert_eq!(
1161 strategy.projected_field(),
1162 "rank",
1163 "avg(field) should preserve projected field labels",
1164 );
1165 assert!(
1166 !strategy.aggregate().is_distinct(),
1167 "avg(field) should stay non-distinct unless requested explicitly",
1168 );
1169 assert_eq!(
1170 strategy.target_field(),
1171 &rank_slot,
1172 "avg(field) should preserve the resolved planner field slot",
1173 );
1174 assert_eq!(
1175 strategy.runtime_request(),
1176 PreparedFluentNumericFieldRuntimeRequest::Avg,
1177 "avg(field) should project the numeric AVG runtime request",
1178 );
1179 }
1180
1181 #[test]
1182 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
1183 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
1184
1185 assert_eq!(
1186 strategy
1187 .explain_aggregate()
1188 .map(|aggregate| aggregate.kind()),
1189 Some(AggregateKind::First),
1190 "first() should preserve the explain-visible aggregate kind",
1191 );
1192 assert_eq!(
1193 strategy.runtime_request(),
1194 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
1195 kind: AggregateKind::First,
1196 },
1197 "first() should project the response-order runtime request",
1198 );
1199 }
1200
1201 #[test]
1202 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
1203 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1204 let strategy =
1205 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
1206
1207 assert_eq!(
1208 strategy.explain_aggregate(),
1209 None,
1210 "nth_by(field, nth) should stay off the current explain aggregate surface",
1211 );
1212 assert_eq!(
1213 strategy.runtime_request(),
1214 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
1215 target_field: rank_slot,
1216 nth: 2,
1217 },
1218 "nth_by(field, nth) should preserve the resolved field-order runtime request",
1219 );
1220 }
1221
1222 #[test]
1223 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
1224 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1225 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
1226 let explain = strategy.explain_descriptor();
1227
1228 assert_eq!(
1229 strategy.target_field(),
1230 &rank_slot,
1231 "count_distinct_by(field) should preserve the resolved planner field slot",
1232 );
1233 assert_eq!(
1234 strategy.runtime_request(),
1235 PreparedFluentProjectionRuntimeRequest::CountDistinct,
1236 "count_distinct_by(field) should project the distinct-count runtime request",
1237 );
1238 assert_eq!(
1239 explain.terminal_label(),
1240 "count_distinct_by",
1241 "count_distinct_by(field) should project the stable explain terminal label",
1242 );
1243 assert_eq!(
1244 explain.field_label(),
1245 "rank",
1246 "count_distinct_by(field) should project the stable explain field label",
1247 );
1248 assert_eq!(
1249 explain.output_label(),
1250 "count",
1251 "count_distinct_by(field) should project the stable explain output label",
1252 );
1253 }
1254
1255 #[test]
1256 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1257 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1258 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1259 let explain = strategy.explain_descriptor();
1260
1261 assert_eq!(
1262 strategy.target_field(),
1263 &rank_slot,
1264 "last_value_by(field) should preserve the resolved planner field slot",
1265 );
1266 assert_eq!(
1267 strategy.runtime_request(),
1268 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1269 terminal_kind: AggregateKind::Last,
1270 },
1271 "last_value_by(field) should project the terminal-value runtime request",
1272 );
1273 assert_eq!(
1274 explain.terminal_label(),
1275 "last_value_by",
1276 "last_value_by(field) should project the stable explain terminal label",
1277 );
1278 assert_eq!(
1279 explain.field_label(),
1280 "rank",
1281 "last_value_by(field) should project the stable explain field label",
1282 );
1283 assert_eq!(
1284 explain.output_label(),
1285 "terminal_value",
1286 "last_value_by(field) should project the stable explain output label",
1287 );
1288 }
1289}