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::Case {
165 when_then_arms,
166 else_expr,
167 } => Expr::Case {
168 when_then_arms: when_then_arms
169 .into_iter()
170 .map(|arm| {
171 crate::db::query::plan::expr::CaseWhenArm::new(
172 fold_aggregate_input_constant_expr(arm.condition().clone()),
173 fold_aggregate_input_constant_expr(arm.result().clone()),
174 )
175 })
176 .collect(),
177 else_expr: Box::new(fold_aggregate_input_constant_expr(*else_expr)),
178 },
179 Expr::Binary { op, left, right } => {
180 let left = fold_aggregate_input_constant_expr(*left);
181 let right = fold_aggregate_input_constant_expr(*right);
182
183 fold_aggregate_input_constant_binary(op, &left, &right).unwrap_or_else(|| {
184 Expr::Binary {
185 op,
186 left: Box::new(left),
187 right: Box::new(right),
188 }
189 })
190 }
191 #[cfg(test)]
192 Expr::Alias { expr, name } => Expr::Alias {
193 expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
194 name,
195 },
196 Expr::Unary { op, expr } => Expr::Unary {
197 op,
198 expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
199 },
200 }
201}
202
203fn fold_aggregate_input_constant_binary(op: BinaryOp, left: &Expr, right: &Expr) -> Option<Expr> {
206 let (Expr::Literal(left), Expr::Literal(right)) = (left, right) else {
207 return None;
208 };
209 if matches!(left, Value::Null) || matches!(right, Value::Null) {
210 return Some(Expr::Literal(Value::Null));
211 }
212
213 let arithmetic_op = match op {
214 BinaryOp::Or
215 | BinaryOp::And
216 | BinaryOp::Eq
217 | BinaryOp::Ne
218 | BinaryOp::Lt
219 | BinaryOp::Lte
220 | BinaryOp::Gt
221 | BinaryOp::Gte => return None,
222 BinaryOp::Add => NumericArithmeticOp::Add,
223 BinaryOp::Sub => NumericArithmeticOp::Sub,
224 BinaryOp::Mul => NumericArithmeticOp::Mul,
225 BinaryOp::Div => NumericArithmeticOp::Div,
226 };
227 let result = apply_numeric_arithmetic(arithmetic_op, left, right)?;
228
229 Some(Expr::Literal(Value::Decimal(result)))
230}
231
232fn fold_aggregate_input_constant_function(function: Function, args: &[Expr]) -> Option<Expr> {
235 match function {
236 Function::Round => fold_aggregate_input_constant_round(args),
237 Function::IsNull
238 | Function::IsNotNull
239 | Function::Trim
240 | Function::Ltrim
241 | Function::Rtrim
242 | Function::Lower
243 | Function::Upper
244 | Function::Length
245 | Function::Left
246 | Function::Right
247 | Function::StartsWith
248 | Function::EndsWith
249 | Function::Contains
250 | Function::Position
251 | Function::Replace
252 | Function::Substring => None,
253 }
254}
255
256fn fold_aggregate_input_constant_round(args: &[Expr]) -> Option<Expr> {
259 let [Expr::Literal(input), Expr::Literal(scale)] = args else {
260 return None;
261 };
262 if matches!(input, Value::Null) || matches!(scale, Value::Null) {
263 return Some(Expr::Literal(Value::Null));
264 }
265
266 let scale = match scale {
267 Value::Int(value) => u32::try_from(*value).ok()?,
268 Value::Uint(value) => u32::try_from(*value).ok()?,
269 _ => return None,
270 };
271 let decimal = input.to_numeric_decimal()?;
272
273 Some(Expr::Literal(Value::Decimal(decimal.round_dp(scale))))
274}
275
276fn normalize_aggregate_input_numeric_literals(expr: Expr) -> Expr {
280 match expr {
281 Expr::Literal(value) => value
282 .to_numeric_decimal()
283 .map_or(Expr::Literal(value), |decimal| {
284 Expr::Literal(Value::Decimal(decimal.normalize()))
285 }),
286 Expr::Field(_) | Expr::Aggregate(_) => expr,
287 Expr::FunctionCall { function, args } => Expr::FunctionCall {
288 function,
289 args: args
290 .into_iter()
291 .map(normalize_aggregate_input_numeric_literals)
292 .collect(),
293 },
294 Expr::Case {
295 when_then_arms,
296 else_expr,
297 } => Expr::Case {
298 when_then_arms: when_then_arms
299 .into_iter()
300 .map(|arm| {
301 crate::db::query::plan::expr::CaseWhenArm::new(
302 normalize_aggregate_input_numeric_literals(arm.condition().clone()),
303 normalize_aggregate_input_numeric_literals(arm.result().clone()),
304 )
305 })
306 .collect(),
307 else_expr: Box::new(normalize_aggregate_input_numeric_literals(*else_expr)),
308 },
309 Expr::Binary { op, left, right } => Expr::Binary {
310 op,
311 left: Box::new(normalize_aggregate_input_numeric_literals(*left)),
312 right: Box::new(normalize_aggregate_input_numeric_literals(*right)),
313 },
314 #[cfg(test)]
315 Expr::Alias { expr, name } => Expr::Alias {
316 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
317 name,
318 },
319 Expr::Unary { op, expr } => Expr::Unary {
320 op,
321 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
322 },
323 }
324}
325
326pub(crate) trait PreparedFluentAggregateExplainStrategy {
337 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
340
341 fn explain_projected_field(&self) -> Option<&str> {
343 None
344 }
345}
346
347#[derive(Clone, Debug, Eq, PartialEq)]
354pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
355 CountRows,
356 ExistsRows,
357}
358
359#[derive(Clone, Debug, Eq, PartialEq)]
372pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
373 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
374}
375
376impl PreparedFluentExistingRowsTerminalStrategy {
377 #[must_use]
379 pub(crate) const fn count_rows() -> Self {
380 Self {
381 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
382 }
383 }
384
385 #[must_use]
387 pub(crate) const fn exists_rows() -> Self {
388 Self {
389 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
390 }
391 }
392
393 #[cfg(test)]
396 #[must_use]
397 pub(crate) const fn aggregate(&self) -> AggregateExpr {
398 match self.runtime_request {
399 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
400 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
401 }
402 }
403
404 #[cfg(test)]
407 #[must_use]
408 pub(crate) const fn runtime_request(
409 &self,
410 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
411 &self.runtime_request
412 }
413
414 #[must_use]
417 pub(crate) const fn into_runtime_request(
418 self,
419 ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
420 self.runtime_request
421 }
422}
423
424impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
425 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
426 Some(match self.runtime_request {
427 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
428 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
429 })
430 }
431}
432
433#[derive(Clone, Debug, Eq, PartialEq)]
440pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
441 IdTerminal {
442 kind: AggregateKind,
443 },
444 IdBySlot {
445 kind: AggregateKind,
446 target_field: FieldSlot,
447 },
448}
449
450#[derive(Clone, Debug, Eq, PartialEq)]
462pub(crate) struct PreparedFluentScalarTerminalStrategy {
463 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
464}
465
466impl PreparedFluentScalarTerminalStrategy {
467 #[must_use]
469 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
470 Self {
471 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
472 }
473 }
474
475 #[must_use]
478 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
479 Self {
480 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
481 kind,
482 target_field,
483 },
484 }
485 }
486
487 #[must_use]
490 pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
491 self.runtime_request
492 }
493}
494
495impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
496 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
497 Some(match self.runtime_request {
498 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
499 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
500 })
501 }
502
503 fn explain_projected_field(&self) -> Option<&str> {
504 match &self.runtime_request {
505 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
506 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
507 Some(target_field.field())
508 }
509 }
510 }
511}
512
513#[derive(Clone, Copy, Debug, Eq, PartialEq)]
523pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
524 Sum,
525 SumDistinct,
526 Avg,
527 AvgDistinct,
528}
529
530#[derive(Clone, Debug, Eq, PartialEq)]
543pub(crate) struct PreparedFluentNumericFieldStrategy {
544 target_field: FieldSlot,
545 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
546}
547
548impl PreparedFluentNumericFieldStrategy {
549 #[must_use]
551 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
552 Self {
553 target_field,
554 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
555 }
556 }
557
558 #[must_use]
560 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
561 Self {
562 target_field,
563 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
564 }
565 }
566
567 #[must_use]
569 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
570 Self {
571 target_field,
572 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
573 }
574 }
575
576 #[must_use]
578 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
579 Self {
580 target_field,
581 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
582 }
583 }
584
585 #[cfg(test)]
588 #[must_use]
589 pub(crate) fn aggregate(&self) -> AggregateExpr {
590 let field = self.target_field.field();
591
592 match self.runtime_request {
593 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
594 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
595 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
596 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
597 }
598 }
599
600 #[cfg(test)]
603 #[must_use]
604 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
605 match self.runtime_request {
606 PreparedFluentNumericFieldRuntimeRequest::Sum
607 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
608 PreparedFluentNumericFieldRuntimeRequest::Avg
609 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
610 }
611 }
612
613 #[cfg(test)]
616 #[must_use]
617 pub(crate) fn projected_field(&self) -> &str {
618 self.target_field.field()
619 }
620
621 #[cfg(test)]
624 #[must_use]
625 pub(crate) const fn target_field(&self) -> &FieldSlot {
626 &self.target_field
627 }
628
629 #[cfg(test)]
632 #[must_use]
633 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
634 self.runtime_request
635 }
636
637 #[must_use]
640 pub(crate) fn into_runtime_parts(
641 self,
642 ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
643 (self.target_field, self.runtime_request)
644 }
645}
646
647impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
648 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
649 Some(match self.runtime_request {
650 PreparedFluentNumericFieldRuntimeRequest::Sum
651 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
652 PreparedFluentNumericFieldRuntimeRequest::Avg
653 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
654 })
655 }
656
657 fn explain_projected_field(&self) -> Option<&str> {
658 Some(self.target_field.field())
659 }
660}
661
662#[derive(Clone, Debug, Eq, PartialEq)]
673pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
674 ResponseOrder { kind: AggregateKind },
675 NthBySlot { target_field: FieldSlot, nth: usize },
676 MedianBySlot { target_field: FieldSlot },
677 MinMaxBySlot { target_field: FieldSlot },
678}
679
680#[derive(Clone, Debug, Eq, PartialEq)]
691pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
692 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
693}
694
695impl PreparedFluentOrderSensitiveTerminalStrategy {
696 #[must_use]
698 pub(crate) const fn first() -> Self {
699 Self {
700 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
701 kind: AggregateKind::First,
702 },
703 }
704 }
705
706 #[must_use]
708 pub(crate) const fn last() -> Self {
709 Self {
710 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
711 kind: AggregateKind::Last,
712 },
713 }
714 }
715
716 #[must_use]
718 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
719 Self {
720 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
721 target_field,
722 nth,
723 },
724 }
725 }
726
727 #[must_use]
729 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
730 Self {
731 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
732 target_field,
733 },
734 }
735 }
736
737 #[must_use]
739 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
740 Self {
741 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
742 target_field,
743 },
744 }
745 }
746
747 #[cfg(test)]
750 #[must_use]
751 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
752 match self.runtime_request {
753 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
754 Some(AggregateExpr::terminal_for_kind(kind))
755 }
756 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
757 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
758 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
759 }
760 }
761
762 #[cfg(test)]
765 #[must_use]
766 pub(crate) const fn runtime_request(
767 &self,
768 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
769 &self.runtime_request
770 }
771
772 #[must_use]
775 pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
776 self.runtime_request
777 }
778}
779
780impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
781 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
782 match self.runtime_request {
783 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
784 Some(kind)
785 }
786 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
787 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
788 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
789 }
790 }
791}
792
793#[derive(Clone, Copy, Debug, Eq, PartialEq)]
803pub(crate) enum PreparedFluentProjectionRuntimeRequest {
804 Values,
805 DistinctValues,
806 CountDistinct,
807 ValuesWithIds,
808 TerminalValue { terminal_kind: AggregateKind },
809}
810
811#[derive(Clone, Copy, Debug, Eq, PartialEq)]
821pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
822 terminal: &'static str,
823 field: &'a str,
824 output: &'static str,
825}
826
827impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
828 #[must_use]
830 pub(crate) const fn terminal_label(self) -> &'static str {
831 self.terminal
832 }
833
834 #[must_use]
836 pub(crate) const fn field_label(self) -> &'a str {
837 self.field
838 }
839
840 #[must_use]
842 pub(crate) const fn output_label(self) -> &'static str {
843 self.output
844 }
845}
846
847#[derive(Clone, Debug, Eq, PartialEq)]
858pub(crate) struct PreparedFluentProjectionStrategy {
859 target_field: FieldSlot,
860 runtime_request: PreparedFluentProjectionRuntimeRequest,
861}
862
863impl PreparedFluentProjectionStrategy {
864 #[must_use]
866 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
867 Self {
868 target_field,
869 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
870 }
871 }
872
873 #[must_use]
875 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
876 Self {
877 target_field,
878 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
879 }
880 }
881
882 #[must_use]
884 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
885 Self {
886 target_field,
887 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
888 }
889 }
890
891 #[must_use]
893 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
894 Self {
895 target_field,
896 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
897 }
898 }
899
900 #[must_use]
902 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
903 Self {
904 target_field,
905 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
906 terminal_kind: AggregateKind::First,
907 },
908 }
909 }
910
911 #[must_use]
913 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
914 Self {
915 target_field,
916 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
917 terminal_kind: AggregateKind::Last,
918 },
919 }
920 }
921
922 #[cfg(test)]
925 #[must_use]
926 pub(crate) const fn target_field(&self) -> &FieldSlot {
927 &self.target_field
928 }
929
930 #[cfg(test)]
933 #[must_use]
934 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
935 self.runtime_request
936 }
937
938 #[must_use]
942 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
943 (self.target_field, self.runtime_request)
944 }
945
946 #[must_use]
949 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
950 let terminal_label = match self.runtime_request {
951 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
952 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
953 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
954 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
955 PreparedFluentProjectionRuntimeRequest::TerminalValue {
956 terminal_kind: AggregateKind::First,
957 } => "first_value_by",
958 PreparedFluentProjectionRuntimeRequest::TerminalValue {
959 terminal_kind: AggregateKind::Last,
960 } => "last_value_by",
961 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
962 unreachable!("projection terminal value explain requires FIRST/LAST kind")
963 }
964 };
965 let output_label = match self.runtime_request {
966 PreparedFluentProjectionRuntimeRequest::Values
967 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
968 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
969 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
970 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
971 };
972
973 PreparedFluentProjectionExplainDescriptor {
974 terminal: terminal_label,
975 field: self.target_field.field(),
976 output: output_label,
977 }
978 }
979}
980
981#[must_use]
983pub const fn count() -> AggregateExpr {
984 AggregateExpr::terminal(AggregateKind::Count)
985}
986
987#[must_use]
989pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
990 AggregateExpr::field_target(AggregateKind::Count, field.as_ref().to_string())
991}
992
993#[must_use]
995pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
996 AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
997}
998
999#[must_use]
1001pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
1002 AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
1003}
1004
1005#[must_use]
1007pub const fn exists() -> AggregateExpr {
1008 AggregateExpr::terminal(AggregateKind::Exists)
1009}
1010
1011#[must_use]
1013pub const fn first() -> AggregateExpr {
1014 AggregateExpr::terminal(AggregateKind::First)
1015}
1016
1017#[must_use]
1019pub const fn last() -> AggregateExpr {
1020 AggregateExpr::terminal(AggregateKind::Last)
1021}
1022
1023#[must_use]
1025pub const fn min() -> AggregateExpr {
1026 AggregateExpr::terminal(AggregateKind::Min)
1027}
1028
1029#[must_use]
1031pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
1032 AggregateExpr::field_target(AggregateKind::Min, field.as_ref().to_string())
1033}
1034
1035#[must_use]
1037pub const fn max() -> AggregateExpr {
1038 AggregateExpr::terminal(AggregateKind::Max)
1039}
1040
1041#[must_use]
1043pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
1044 AggregateExpr::field_target(AggregateKind::Max, field.as_ref().to_string())
1045}
1046
1047#[cfg(test)]
1052mod tests {
1053 use crate::db::query::{
1054 builder::{
1055 PreparedFluentExistingRowsTerminalRuntimeRequest,
1056 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
1057 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
1058 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
1059 PreparedFluentProjectionStrategy,
1060 },
1061 plan::{AggregateKind, FieldSlot},
1062 };
1063
1064 #[test]
1065 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
1066 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1067 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
1068
1069 assert_eq!(
1070 strategy.aggregate_kind(),
1071 AggregateKind::Sum,
1072 "sum(distinct field) should preserve SUM aggregate kind",
1073 );
1074 assert_eq!(
1075 strategy.projected_field(),
1076 "rank",
1077 "sum(distinct field) should preserve projected field labels",
1078 );
1079 assert!(
1080 strategy.aggregate().is_distinct(),
1081 "sum(distinct field) should preserve DISTINCT aggregate shape",
1082 );
1083 assert_eq!(
1084 strategy.target_field(),
1085 &rank_slot,
1086 "sum(distinct field) should preserve the resolved planner field slot",
1087 );
1088 assert_eq!(
1089 strategy.runtime_request(),
1090 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
1091 "sum(distinct field) should project the numeric DISTINCT runtime request",
1092 );
1093 }
1094
1095 #[test]
1096 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
1097 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
1098
1099 assert_eq!(
1100 strategy.aggregate().kind(),
1101 AggregateKind::Count,
1102 "count() should preserve the explain-visible aggregate kind",
1103 );
1104 assert_eq!(
1105 strategy.runtime_request(),
1106 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
1107 "count() should project the existing-rows count runtime request",
1108 );
1109 }
1110
1111 #[test]
1112 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
1113 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
1114
1115 assert_eq!(
1116 strategy.aggregate().kind(),
1117 AggregateKind::Exists,
1118 "exists() should preserve the explain-visible aggregate kind",
1119 );
1120 assert_eq!(
1121 strategy.runtime_request(),
1122 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
1123 "exists() should project the existing-rows exists runtime request",
1124 );
1125 }
1126
1127 #[test]
1128 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
1129 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1130 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
1131
1132 assert_eq!(
1133 strategy.aggregate_kind(),
1134 AggregateKind::Avg,
1135 "avg(field) should preserve AVG aggregate kind",
1136 );
1137 assert_eq!(
1138 strategy.projected_field(),
1139 "rank",
1140 "avg(field) should preserve projected field labels",
1141 );
1142 assert!(
1143 !strategy.aggregate().is_distinct(),
1144 "avg(field) should stay non-distinct unless requested explicitly",
1145 );
1146 assert_eq!(
1147 strategy.target_field(),
1148 &rank_slot,
1149 "avg(field) should preserve the resolved planner field slot",
1150 );
1151 assert_eq!(
1152 strategy.runtime_request(),
1153 PreparedFluentNumericFieldRuntimeRequest::Avg,
1154 "avg(field) should project the numeric AVG runtime request",
1155 );
1156 }
1157
1158 #[test]
1159 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
1160 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
1161
1162 assert_eq!(
1163 strategy
1164 .explain_aggregate()
1165 .map(|aggregate| aggregate.kind()),
1166 Some(AggregateKind::First),
1167 "first() should preserve the explain-visible aggregate kind",
1168 );
1169 assert_eq!(
1170 strategy.runtime_request(),
1171 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
1172 kind: AggregateKind::First,
1173 },
1174 "first() should project the response-order runtime request",
1175 );
1176 }
1177
1178 #[test]
1179 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
1180 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1181 let strategy =
1182 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
1183
1184 assert_eq!(
1185 strategy.explain_aggregate(),
1186 None,
1187 "nth_by(field, nth) should stay off the current explain aggregate surface",
1188 );
1189 assert_eq!(
1190 strategy.runtime_request(),
1191 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
1192 target_field: rank_slot,
1193 nth: 2,
1194 },
1195 "nth_by(field, nth) should preserve the resolved field-order runtime request",
1196 );
1197 }
1198
1199 #[test]
1200 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
1201 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1202 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
1203 let explain = strategy.explain_descriptor();
1204
1205 assert_eq!(
1206 strategy.target_field(),
1207 &rank_slot,
1208 "count_distinct_by(field) should preserve the resolved planner field slot",
1209 );
1210 assert_eq!(
1211 strategy.runtime_request(),
1212 PreparedFluentProjectionRuntimeRequest::CountDistinct,
1213 "count_distinct_by(field) should project the distinct-count runtime request",
1214 );
1215 assert_eq!(
1216 explain.terminal_label(),
1217 "count_distinct_by",
1218 "count_distinct_by(field) should project the stable explain terminal label",
1219 );
1220 assert_eq!(
1221 explain.field_label(),
1222 "rank",
1223 "count_distinct_by(field) should project the stable explain field label",
1224 );
1225 assert_eq!(
1226 explain.output_label(),
1227 "count",
1228 "count_distinct_by(field) should project the stable explain output label",
1229 );
1230 }
1231
1232 #[test]
1233 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1234 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1235 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1236 let explain = strategy.explain_descriptor();
1237
1238 assert_eq!(
1239 strategy.target_field(),
1240 &rank_slot,
1241 "last_value_by(field) should preserve the resolved planner field slot",
1242 );
1243 assert_eq!(
1244 strategy.runtime_request(),
1245 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1246 terminal_kind: AggregateKind::Last,
1247 },
1248 "last_value_by(field) should project the terminal-value runtime request",
1249 );
1250 assert_eq!(
1251 explain.terminal_label(),
1252 "last_value_by",
1253 "last_value_by(field) should project the stable explain terminal label",
1254 );
1255 assert_eq!(
1256 explain.field_label(),
1257 "rank",
1258 "last_value_by(field) should project the stable explain field label",
1259 );
1260 assert_eq!(
1261 explain.output_label(),
1262 "terminal_value",
1263 "last_value_by(field) should project the stable explain output label",
1264 );
1265 }
1266}