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::Trim
259 | Function::Ltrim
260 | Function::Rtrim
261 | Function::Lower
262 | Function::Upper
263 | Function::Length
264 | Function::Left
265 | Function::Right
266 | Function::StartsWith
267 | Function::EndsWith
268 | Function::Contains
269 | Function::Position
270 | Function::Replace
271 | Function::Substring => None,
272 }
273}
274
275fn fold_aggregate_input_constant_round(args: &[Expr]) -> Option<Expr> {
278 let [Expr::Literal(input), Expr::Literal(scale)] = args else {
279 return None;
280 };
281 if matches!(input, Value::Null) || matches!(scale, Value::Null) {
282 return Some(Expr::Literal(Value::Null));
283 }
284
285 let scale = match scale {
286 Value::Int(value) => u32::try_from(*value).ok()?,
287 Value::Uint(value) => u32::try_from(*value).ok()?,
288 _ => return None,
289 };
290 let decimal = input.to_numeric_decimal()?;
291
292 Some(Expr::Literal(Value::Decimal(decimal.round_dp(scale))))
293}
294
295fn normalize_aggregate_input_numeric_literals(expr: Expr) -> Expr {
299 match expr {
300 Expr::Literal(value) => value
301 .to_numeric_decimal()
302 .map_or(Expr::Literal(value), |decimal| {
303 Expr::Literal(Value::Decimal(decimal.normalize()))
304 }),
305 Expr::Field(_) | Expr::Aggregate(_) => expr,
306 Expr::FunctionCall { function, args } => Expr::FunctionCall {
307 function,
308 args: args
309 .into_iter()
310 .map(normalize_aggregate_input_numeric_literals)
311 .collect(),
312 },
313 Expr::Case {
314 when_then_arms,
315 else_expr,
316 } => Expr::Case {
317 when_then_arms: when_then_arms
318 .into_iter()
319 .map(|arm| {
320 crate::db::query::plan::expr::CaseWhenArm::new(
321 normalize_aggregate_input_numeric_literals(arm.condition().clone()),
322 normalize_aggregate_input_numeric_literals(arm.result().clone()),
323 )
324 })
325 .collect(),
326 else_expr: Box::new(normalize_aggregate_input_numeric_literals(*else_expr)),
327 },
328 Expr::Binary { op, left, right } => Expr::Binary {
329 op,
330 left: Box::new(normalize_aggregate_input_numeric_literals(*left)),
331 right: Box::new(normalize_aggregate_input_numeric_literals(*right)),
332 },
333 #[cfg(test)]
334 Expr::Alias { expr, name } => Expr::Alias {
335 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
336 name,
337 },
338 Expr::Unary { op, expr } => Expr::Unary {
339 op,
340 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
341 },
342 }
343}
344
345pub(crate) trait PreparedFluentAggregateExplainStrategy {
356 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
359
360 fn explain_projected_field(&self) -> Option<&str> {
362 None
363 }
364}
365
366#[derive(Clone, Debug, Eq, PartialEq)]
373pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
374 CountRows,
375 ExistsRows,
376}
377
378#[derive(Clone, Debug, Eq, PartialEq)]
391pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
392 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
393}
394
395impl PreparedFluentExistingRowsTerminalStrategy {
396 #[must_use]
398 pub(crate) const fn count_rows() -> Self {
399 Self {
400 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
401 }
402 }
403
404 #[must_use]
406 pub(crate) const fn exists_rows() -> Self {
407 Self {
408 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
409 }
410 }
411
412 #[cfg(test)]
415 #[must_use]
416 pub(crate) const fn aggregate(&self) -> AggregateExpr {
417 match self.runtime_request {
418 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
419 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
420 }
421 }
422
423 #[cfg(test)]
426 #[must_use]
427 pub(crate) const fn runtime_request(
428 &self,
429 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
430 &self.runtime_request
431 }
432
433 #[must_use]
436 pub(crate) const fn into_runtime_request(
437 self,
438 ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
439 self.runtime_request
440 }
441}
442
443impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
444 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
445 Some(match self.runtime_request {
446 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
447 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
448 })
449 }
450}
451
452#[derive(Clone, Debug, Eq, PartialEq)]
459pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
460 IdTerminal {
461 kind: AggregateKind,
462 },
463 IdBySlot {
464 kind: AggregateKind,
465 target_field: FieldSlot,
466 },
467}
468
469#[derive(Clone, Debug, Eq, PartialEq)]
481pub(crate) struct PreparedFluentScalarTerminalStrategy {
482 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
483}
484
485impl PreparedFluentScalarTerminalStrategy {
486 #[must_use]
488 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
489 Self {
490 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
491 }
492 }
493
494 #[must_use]
497 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
498 Self {
499 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
500 kind,
501 target_field,
502 },
503 }
504 }
505
506 #[must_use]
509 pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
510 self.runtime_request
511 }
512}
513
514impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
515 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
516 Some(match self.runtime_request {
517 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
518 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
519 })
520 }
521
522 fn explain_projected_field(&self) -> Option<&str> {
523 match &self.runtime_request {
524 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
525 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
526 Some(target_field.field())
527 }
528 }
529 }
530}
531
532#[derive(Clone, Copy, Debug, Eq, PartialEq)]
542pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
543 Sum,
544 SumDistinct,
545 Avg,
546 AvgDistinct,
547}
548
549#[derive(Clone, Debug, Eq, PartialEq)]
562pub(crate) struct PreparedFluentNumericFieldStrategy {
563 target_field: FieldSlot,
564 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
565}
566
567impl PreparedFluentNumericFieldStrategy {
568 #[must_use]
570 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
571 Self {
572 target_field,
573 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
574 }
575 }
576
577 #[must_use]
579 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
580 Self {
581 target_field,
582 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
583 }
584 }
585
586 #[must_use]
588 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
589 Self {
590 target_field,
591 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
592 }
593 }
594
595 #[must_use]
597 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
598 Self {
599 target_field,
600 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
601 }
602 }
603
604 #[cfg(test)]
607 #[must_use]
608 pub(crate) fn aggregate(&self) -> AggregateExpr {
609 let field = self.target_field.field();
610
611 match self.runtime_request {
612 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
613 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
614 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
615 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
616 }
617 }
618
619 #[cfg(test)]
622 #[must_use]
623 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
624 match self.runtime_request {
625 PreparedFluentNumericFieldRuntimeRequest::Sum
626 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
627 PreparedFluentNumericFieldRuntimeRequest::Avg
628 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
629 }
630 }
631
632 #[cfg(test)]
635 #[must_use]
636 pub(crate) fn projected_field(&self) -> &str {
637 self.target_field.field()
638 }
639
640 #[cfg(test)]
643 #[must_use]
644 pub(crate) const fn target_field(&self) -> &FieldSlot {
645 &self.target_field
646 }
647
648 #[cfg(test)]
651 #[must_use]
652 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
653 self.runtime_request
654 }
655
656 #[must_use]
659 pub(crate) fn into_runtime_parts(
660 self,
661 ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
662 (self.target_field, self.runtime_request)
663 }
664}
665
666impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
667 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
668 Some(match self.runtime_request {
669 PreparedFluentNumericFieldRuntimeRequest::Sum
670 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
671 PreparedFluentNumericFieldRuntimeRequest::Avg
672 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
673 })
674 }
675
676 fn explain_projected_field(&self) -> Option<&str> {
677 Some(self.target_field.field())
678 }
679}
680
681#[derive(Clone, Debug, Eq, PartialEq)]
692pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
693 ResponseOrder { kind: AggregateKind },
694 NthBySlot { target_field: FieldSlot, nth: usize },
695 MedianBySlot { target_field: FieldSlot },
696 MinMaxBySlot { target_field: FieldSlot },
697}
698
699#[derive(Clone, Debug, Eq, PartialEq)]
710pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
711 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
712}
713
714impl PreparedFluentOrderSensitiveTerminalStrategy {
715 #[must_use]
717 pub(crate) const fn first() -> Self {
718 Self {
719 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
720 kind: AggregateKind::First,
721 },
722 }
723 }
724
725 #[must_use]
727 pub(crate) const fn last() -> Self {
728 Self {
729 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
730 kind: AggregateKind::Last,
731 },
732 }
733 }
734
735 #[must_use]
737 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
738 Self {
739 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
740 target_field,
741 nth,
742 },
743 }
744 }
745
746 #[must_use]
748 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
749 Self {
750 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
751 target_field,
752 },
753 }
754 }
755
756 #[must_use]
758 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
759 Self {
760 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
761 target_field,
762 },
763 }
764 }
765
766 #[cfg(test)]
769 #[must_use]
770 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
771 match self.runtime_request {
772 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
773 Some(AggregateExpr::terminal_for_kind(kind))
774 }
775 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
776 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
777 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
778 }
779 }
780
781 #[cfg(test)]
784 #[must_use]
785 pub(crate) const fn runtime_request(
786 &self,
787 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
788 &self.runtime_request
789 }
790
791 #[must_use]
794 pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
795 self.runtime_request
796 }
797}
798
799impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
800 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
801 match self.runtime_request {
802 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
803 Some(kind)
804 }
805 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
806 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
807 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
808 }
809 }
810}
811
812#[derive(Clone, Copy, Debug, Eq, PartialEq)]
822pub(crate) enum PreparedFluentProjectionRuntimeRequest {
823 Values,
824 DistinctValues,
825 CountDistinct,
826 ValuesWithIds,
827 TerminalValue { terminal_kind: AggregateKind },
828}
829
830#[derive(Clone, Copy, Debug, Eq, PartialEq)]
840pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
841 terminal: &'static str,
842 field: &'a str,
843 output: &'static str,
844}
845
846impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
847 #[must_use]
849 pub(crate) const fn terminal_label(self) -> &'static str {
850 self.terminal
851 }
852
853 #[must_use]
855 pub(crate) const fn field_label(self) -> &'a str {
856 self.field
857 }
858
859 #[must_use]
861 pub(crate) const fn output_label(self) -> &'static str {
862 self.output
863 }
864}
865
866#[derive(Clone, Debug, Eq, PartialEq)]
877pub(crate) struct PreparedFluentProjectionStrategy {
878 target_field: FieldSlot,
879 runtime_request: PreparedFluentProjectionRuntimeRequest,
880}
881
882impl PreparedFluentProjectionStrategy {
883 #[must_use]
885 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
886 Self {
887 target_field,
888 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
889 }
890 }
891
892 #[must_use]
894 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
895 Self {
896 target_field,
897 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
898 }
899 }
900
901 #[must_use]
903 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
904 Self {
905 target_field,
906 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
907 }
908 }
909
910 #[must_use]
912 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
913 Self {
914 target_field,
915 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
916 }
917 }
918
919 #[must_use]
921 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
922 Self {
923 target_field,
924 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
925 terminal_kind: AggregateKind::First,
926 },
927 }
928 }
929
930 #[must_use]
932 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
933 Self {
934 target_field,
935 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
936 terminal_kind: AggregateKind::Last,
937 },
938 }
939 }
940
941 #[cfg(test)]
944 #[must_use]
945 pub(crate) const fn target_field(&self) -> &FieldSlot {
946 &self.target_field
947 }
948
949 #[cfg(test)]
952 #[must_use]
953 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
954 self.runtime_request
955 }
956
957 #[must_use]
961 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
962 (self.target_field, self.runtime_request)
963 }
964
965 #[must_use]
968 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
969 let terminal_label = match self.runtime_request {
970 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
971 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
972 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
973 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
974 PreparedFluentProjectionRuntimeRequest::TerminalValue {
975 terminal_kind: AggregateKind::First,
976 } => "first_value_by",
977 PreparedFluentProjectionRuntimeRequest::TerminalValue {
978 terminal_kind: AggregateKind::Last,
979 } => "last_value_by",
980 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
981 unreachable!("projection terminal value explain requires FIRST/LAST kind")
982 }
983 };
984 let output_label = match self.runtime_request {
985 PreparedFluentProjectionRuntimeRequest::Values
986 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
987 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
988 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
989 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
990 };
991
992 PreparedFluentProjectionExplainDescriptor {
993 terminal: terminal_label,
994 field: self.target_field.field(),
995 output: output_label,
996 }
997 }
998}
999
1000#[must_use]
1002pub const fn count() -> AggregateExpr {
1003 AggregateExpr::terminal(AggregateKind::Count)
1004}
1005
1006#[must_use]
1008pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
1009 AggregateExpr::field_target(AggregateKind::Count, field.as_ref().to_string())
1010}
1011
1012#[must_use]
1014pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
1015 AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
1016}
1017
1018#[must_use]
1020pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
1021 AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
1022}
1023
1024#[must_use]
1026pub const fn exists() -> AggregateExpr {
1027 AggregateExpr::terminal(AggregateKind::Exists)
1028}
1029
1030#[must_use]
1032pub const fn first() -> AggregateExpr {
1033 AggregateExpr::terminal(AggregateKind::First)
1034}
1035
1036#[must_use]
1038pub const fn last() -> AggregateExpr {
1039 AggregateExpr::terminal(AggregateKind::Last)
1040}
1041
1042#[must_use]
1044pub const fn min() -> AggregateExpr {
1045 AggregateExpr::terminal(AggregateKind::Min)
1046}
1047
1048#[must_use]
1050pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
1051 AggregateExpr::field_target(AggregateKind::Min, field.as_ref().to_string())
1052}
1053
1054#[must_use]
1056pub const fn max() -> AggregateExpr {
1057 AggregateExpr::terminal(AggregateKind::Max)
1058}
1059
1060#[must_use]
1062pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
1063 AggregateExpr::field_target(AggregateKind::Max, field.as_ref().to_string())
1064}
1065
1066#[cfg(test)]
1071mod tests {
1072 use crate::db::query::{
1073 builder::{
1074 PreparedFluentExistingRowsTerminalRuntimeRequest,
1075 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
1076 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
1077 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
1078 PreparedFluentProjectionStrategy,
1079 },
1080 plan::{AggregateKind, FieldSlot},
1081 };
1082
1083 #[test]
1084 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
1085 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1086 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
1087
1088 assert_eq!(
1089 strategy.aggregate_kind(),
1090 AggregateKind::Sum,
1091 "sum(distinct field) should preserve SUM aggregate kind",
1092 );
1093 assert_eq!(
1094 strategy.projected_field(),
1095 "rank",
1096 "sum(distinct field) should preserve projected field labels",
1097 );
1098 assert!(
1099 strategy.aggregate().is_distinct(),
1100 "sum(distinct field) should preserve DISTINCT aggregate shape",
1101 );
1102 assert_eq!(
1103 strategy.target_field(),
1104 &rank_slot,
1105 "sum(distinct field) should preserve the resolved planner field slot",
1106 );
1107 assert_eq!(
1108 strategy.runtime_request(),
1109 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
1110 "sum(distinct field) should project the numeric DISTINCT runtime request",
1111 );
1112 }
1113
1114 #[test]
1115 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
1116 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
1117
1118 assert_eq!(
1119 strategy.aggregate().kind(),
1120 AggregateKind::Count,
1121 "count() should preserve the explain-visible aggregate kind",
1122 );
1123 assert_eq!(
1124 strategy.runtime_request(),
1125 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
1126 "count() should project the existing-rows count runtime request",
1127 );
1128 }
1129
1130 #[test]
1131 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
1132 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
1133
1134 assert_eq!(
1135 strategy.aggregate().kind(),
1136 AggregateKind::Exists,
1137 "exists() should preserve the explain-visible aggregate kind",
1138 );
1139 assert_eq!(
1140 strategy.runtime_request(),
1141 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
1142 "exists() should project the existing-rows exists runtime request",
1143 );
1144 }
1145
1146 #[test]
1147 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
1148 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1149 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
1150
1151 assert_eq!(
1152 strategy.aggregate_kind(),
1153 AggregateKind::Avg,
1154 "avg(field) should preserve AVG aggregate kind",
1155 );
1156 assert_eq!(
1157 strategy.projected_field(),
1158 "rank",
1159 "avg(field) should preserve projected field labels",
1160 );
1161 assert!(
1162 !strategy.aggregate().is_distinct(),
1163 "avg(field) should stay non-distinct unless requested explicitly",
1164 );
1165 assert_eq!(
1166 strategy.target_field(),
1167 &rank_slot,
1168 "avg(field) should preserve the resolved planner field slot",
1169 );
1170 assert_eq!(
1171 strategy.runtime_request(),
1172 PreparedFluentNumericFieldRuntimeRequest::Avg,
1173 "avg(field) should project the numeric AVG runtime request",
1174 );
1175 }
1176
1177 #[test]
1178 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
1179 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
1180
1181 assert_eq!(
1182 strategy
1183 .explain_aggregate()
1184 .map(|aggregate| aggregate.kind()),
1185 Some(AggregateKind::First),
1186 "first() should preserve the explain-visible aggregate kind",
1187 );
1188 assert_eq!(
1189 strategy.runtime_request(),
1190 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
1191 kind: AggregateKind::First,
1192 },
1193 "first() should project the response-order runtime request",
1194 );
1195 }
1196
1197 #[test]
1198 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
1199 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1200 let strategy =
1201 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
1202
1203 assert_eq!(
1204 strategy.explain_aggregate(),
1205 None,
1206 "nth_by(field, nth) should stay off the current explain aggregate surface",
1207 );
1208 assert_eq!(
1209 strategy.runtime_request(),
1210 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
1211 target_field: rank_slot,
1212 nth: 2,
1213 },
1214 "nth_by(field, nth) should preserve the resolved field-order runtime request",
1215 );
1216 }
1217
1218 #[test]
1219 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
1220 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1221 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
1222 let explain = strategy.explain_descriptor();
1223
1224 assert_eq!(
1225 strategy.target_field(),
1226 &rank_slot,
1227 "count_distinct_by(field) should preserve the resolved planner field slot",
1228 );
1229 assert_eq!(
1230 strategy.runtime_request(),
1231 PreparedFluentProjectionRuntimeRequest::CountDistinct,
1232 "count_distinct_by(field) should project the distinct-count runtime request",
1233 );
1234 assert_eq!(
1235 explain.terminal_label(),
1236 "count_distinct_by",
1237 "count_distinct_by(field) should project the stable explain terminal label",
1238 );
1239 assert_eq!(
1240 explain.field_label(),
1241 "rank",
1242 "count_distinct_by(field) should project the stable explain field label",
1243 );
1244 assert_eq!(
1245 explain.output_label(),
1246 "count",
1247 "count_distinct_by(field) should project the stable explain output label",
1248 );
1249 }
1250
1251 #[test]
1252 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1253 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1254 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1255 let explain = strategy.explain_descriptor();
1256
1257 assert_eq!(
1258 strategy.target_field(),
1259 &rank_slot,
1260 "last_value_by(field) should preserve the resolved planner field slot",
1261 );
1262 assert_eq!(
1263 strategy.runtime_request(),
1264 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1265 terminal_kind: AggregateKind::Last,
1266 },
1267 "last_value_by(field) should project the terminal-value runtime request",
1268 );
1269 assert_eq!(
1270 explain.terminal_label(),
1271 "last_value_by",
1272 "last_value_by(field) should project the stable explain terminal label",
1273 );
1274 assert_eq!(
1275 explain.field_label(),
1276 "rank",
1277 "last_value_by(field) should project the stable explain field label",
1278 );
1279 assert_eq!(
1280 explain.output_label(),
1281 "terminal_value",
1282 "last_value_by(field) should project the stable explain output label",
1283 );
1284 }
1285}