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 #[must_use]
115 pub(in crate::db) fn bind_template_values(self, replacements: &[(Value, Value)]) -> Self {
116 let kind = self.kind();
117 let input_expr = self
118 .input_expr()
119 .cloned()
120 .map(|expr| expr.bind_template_values(replacements));
121 let filter_expr = self
122 .filter_expr()
123 .cloned()
124 .map(|expr| expr.bind_template_values(replacements));
125 let mut rebound = input_expr.map_or_else(
126 || match kind {
127 AggregateKind::Count
128 | AggregateKind::Exists
129 | AggregateKind::Min
130 | AggregateKind::Max
131 | AggregateKind::First
132 | AggregateKind::Last => Self::terminal(kind),
133 AggregateKind::Sum | AggregateKind::Avg => {
134 unreachable!("SUM/AVG aggregate templates must preserve one input expression")
135 }
136 },
137 |expr| Self::from_expression_input(kind, expr),
138 );
139
140 if let Some(filter_expr) = filter_expr {
141 rebound = rebound.with_filter_expr(filter_expr);
142 }
143 if self.is_distinct() {
144 rebound = rebound.distinct();
145 }
146
147 rebound
148 }
149
150 pub(in crate::db::query) fn from_semantic_parts(
152 kind: AggregateKind,
153 target_field: Option<String>,
154 distinct: bool,
155 ) -> Self {
156 Self {
157 kind,
158 input_expr: target_field.map(|field| Box::new(Expr::Field(FieldId::new(field)))),
159 filter_expr: None,
160 distinct,
161 }
162 }
163
164 #[cfg(test)]
166 #[must_use]
167 pub(in crate::db) fn terminal_for_kind(kind: AggregateKind) -> Self {
168 match kind {
169 AggregateKind::Count => count(),
170 AggregateKind::Exists => exists(),
171 AggregateKind::Min => min(),
172 AggregateKind::Max => max(),
173 AggregateKind::First => first(),
174 AggregateKind::Last => last(),
175 AggregateKind::Sum | AggregateKind::Avg => unreachable!(
176 "AggregateExpr::terminal_for_kind does not support SUM/AVG field-target kinds"
177 ),
178 }
179 }
180}
181
182pub(in crate::db) fn canonicalize_aggregate_input_expr(kind: AggregateKind, expr: Expr) -> Expr {
186 let folded =
187 normalize_aggregate_input_numeric_literals(fold_aggregate_input_constant_expr(expr));
188
189 match kind {
190 AggregateKind::Sum | AggregateKind::Avg => match folded {
191 Expr::Literal(value) => value
192 .to_numeric_decimal()
193 .map_or(Expr::Literal(value), |decimal| {
194 Expr::Literal(Value::Decimal(decimal.normalize()))
195 }),
196 other => other,
197 },
198 AggregateKind::Count
199 | AggregateKind::Min
200 | AggregateKind::Max
201 | AggregateKind::Exists
202 | AggregateKind::First
203 | AggregateKind::Last => folded,
204 }
205}
206
207fn fold_aggregate_input_constant_expr(expr: Expr) -> Expr {
210 match expr {
211 Expr::Field(_) | Expr::Literal(_) | Expr::Aggregate(_) => expr,
212 Expr::FunctionCall { function, args } => {
213 let args = args
214 .into_iter()
215 .map(fold_aggregate_input_constant_expr)
216 .collect::<Vec<_>>();
217
218 fold_aggregate_input_constant_function(function, args.as_slice())
219 .unwrap_or(Expr::FunctionCall { function, args })
220 }
221 Expr::Case {
222 when_then_arms,
223 else_expr,
224 } => Expr::Case {
225 when_then_arms: when_then_arms
226 .into_iter()
227 .map(|arm| {
228 crate::db::query::plan::expr::CaseWhenArm::new(
229 fold_aggregate_input_constant_expr(arm.condition().clone()),
230 fold_aggregate_input_constant_expr(arm.result().clone()),
231 )
232 })
233 .collect(),
234 else_expr: Box::new(fold_aggregate_input_constant_expr(*else_expr)),
235 },
236 Expr::Binary { op, left, right } => {
237 let left = fold_aggregate_input_constant_expr(*left);
238 let right = fold_aggregate_input_constant_expr(*right);
239
240 fold_aggregate_input_constant_binary(op, &left, &right).unwrap_or_else(|| {
241 Expr::Binary {
242 op,
243 left: Box::new(left),
244 right: Box::new(right),
245 }
246 })
247 }
248 #[cfg(test)]
249 Expr::Alias { expr, name } => Expr::Alias {
250 expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
251 name,
252 },
253 Expr::Unary { op, expr } => Expr::Unary {
254 op,
255 expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
256 },
257 }
258}
259
260fn fold_aggregate_input_constant_binary(op: BinaryOp, left: &Expr, right: &Expr) -> Option<Expr> {
263 let (Expr::Literal(left), Expr::Literal(right)) = (left, right) else {
264 return None;
265 };
266 if matches!(left, Value::Null) || matches!(right, Value::Null) {
267 return Some(Expr::Literal(Value::Null));
268 }
269
270 let arithmetic_op = match op {
271 BinaryOp::Or
272 | BinaryOp::And
273 | BinaryOp::Eq
274 | BinaryOp::Ne
275 | BinaryOp::Lt
276 | BinaryOp::Lte
277 | BinaryOp::Gt
278 | BinaryOp::Gte => return None,
279 BinaryOp::Add => NumericArithmeticOp::Add,
280 BinaryOp::Sub => NumericArithmeticOp::Sub,
281 BinaryOp::Mul => NumericArithmeticOp::Mul,
282 BinaryOp::Div => NumericArithmeticOp::Div,
283 };
284 let result = apply_numeric_arithmetic(arithmetic_op, left, right)?;
285
286 Some(Expr::Literal(Value::Decimal(result)))
287}
288
289fn fold_aggregate_input_constant_function(function: Function, args: &[Expr]) -> Option<Expr> {
292 match function {
293 Function::Round => fold_aggregate_input_constant_round(args),
294 Function::Coalesce => fold_aggregate_input_constant_coalesce(args),
295 Function::NullIf => fold_aggregate_input_constant_nullif(args),
296 Function::Abs | Function::Ceil | Function::Ceiling | Function::Floor => {
297 fold_aggregate_input_constant_unary_numeric(function, args)
298 }
299 Function::IsNull
300 | Function::IsNotNull
301 | Function::IsMissing
302 | Function::IsEmpty
303 | Function::IsNotEmpty
304 | Function::Trim
305 | Function::Ltrim
306 | Function::Rtrim
307 | Function::Left
308 | Function::Right
309 | Function::StartsWith
310 | Function::EndsWith
311 | Function::Contains
312 | Function::CollectionContains
313 | Function::Position
314 | Function::Replace
315 | Function::Substring
316 | Function::Lower
317 | Function::Upper
318 | Function::Length => None,
319 }
320}
321
322fn fold_aggregate_input_constant_unary_numeric(function: Function, args: &[Expr]) -> Option<Expr> {
323 let [Expr::Literal(input)] = args else {
324 return None;
325 };
326 if matches!(input, Value::Null) {
327 return Some(Expr::Literal(Value::Null));
328 }
329
330 let decimal = input.to_numeric_decimal()?;
331 let result = match function {
332 Function::Abs => decimal.abs(),
333 Function::Ceil | Function::Ceiling => decimal.ceil_dp0(),
334 Function::Floor => decimal.floor_dp0(),
335 _ => return None,
336 };
337
338 Some(Expr::Literal(Value::Decimal(result)))
339}
340
341fn fold_aggregate_input_constant_round(args: &[Expr]) -> Option<Expr> {
344 let [Expr::Literal(input), Expr::Literal(scale)] = args else {
345 return None;
346 };
347 if matches!(input, Value::Null) || matches!(scale, Value::Null) {
348 return Some(Expr::Literal(Value::Null));
349 }
350
351 let scale = match scale {
352 Value::Int(value) => u32::try_from(*value).ok()?,
353 Value::Uint(value) => u32::try_from(*value).ok()?,
354 _ => return None,
355 };
356 let decimal = input.to_numeric_decimal()?;
357
358 Some(Expr::Literal(Value::Decimal(decimal.round_dp(scale))))
359}
360
361fn fold_aggregate_input_constant_coalesce(args: &[Expr]) -> Option<Expr> {
362 let mut literal_values = Vec::with_capacity(args.len());
363 for arg in args {
364 let Expr::Literal(value) = arg else {
365 return None;
366 };
367 literal_values.push(value.clone());
368 }
369
370 Some(Expr::Literal(
371 literal_values
372 .into_iter()
373 .find(|value| !matches!(value, Value::Null))
374 .unwrap_or(Value::Null),
375 ))
376}
377
378fn fold_aggregate_input_constant_nullif(args: &[Expr]) -> Option<Expr> {
379 let [Expr::Literal(left), Expr::Literal(right)] = args else {
380 return None;
381 };
382 if matches!(left, Value::Null) || matches!(right, Value::Null) {
383 return Some(Expr::Literal(left.clone()));
384 }
385
386 Some(Expr::Literal(if left == right {
387 Value::Null
388 } else {
389 left.clone()
390 }))
391}
392
393fn normalize_aggregate_input_numeric_literals(expr: Expr) -> Expr {
397 match expr {
398 Expr::Literal(value) => value
399 .to_numeric_decimal()
400 .map_or(Expr::Literal(value), |decimal| {
401 Expr::Literal(Value::Decimal(decimal.normalize()))
402 }),
403 Expr::Field(_) | Expr::Aggregate(_) => expr,
404 Expr::FunctionCall { function, args } => Expr::FunctionCall {
405 function,
406 args: args
407 .into_iter()
408 .map(normalize_aggregate_input_numeric_literals)
409 .collect(),
410 },
411 Expr::Case {
412 when_then_arms,
413 else_expr,
414 } => Expr::Case {
415 when_then_arms: when_then_arms
416 .into_iter()
417 .map(|arm| {
418 crate::db::query::plan::expr::CaseWhenArm::new(
419 normalize_aggregate_input_numeric_literals(arm.condition().clone()),
420 normalize_aggregate_input_numeric_literals(arm.result().clone()),
421 )
422 })
423 .collect(),
424 else_expr: Box::new(normalize_aggregate_input_numeric_literals(*else_expr)),
425 },
426 Expr::Binary { op, left, right } => Expr::Binary {
427 op,
428 left: Box::new(normalize_aggregate_input_numeric_literals(*left)),
429 right: Box::new(normalize_aggregate_input_numeric_literals(*right)),
430 },
431 #[cfg(test)]
432 Expr::Alias { expr, name } => Expr::Alias {
433 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
434 name,
435 },
436 Expr::Unary { op, expr } => Expr::Unary {
437 op,
438 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
439 },
440 }
441}
442
443pub(crate) trait PreparedFluentAggregateExplainStrategy {
454 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
457
458 fn explain_projected_field(&self) -> Option<&str> {
460 None
461 }
462}
463
464#[derive(Clone, Debug, Eq, PartialEq)]
471pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
472 CountRows,
473 ExistsRows,
474}
475
476#[derive(Clone, Debug, Eq, PartialEq)]
489pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
490 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
491}
492
493impl PreparedFluentExistingRowsTerminalStrategy {
494 #[must_use]
496 pub(crate) const fn count_rows() -> Self {
497 Self {
498 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
499 }
500 }
501
502 #[must_use]
504 pub(crate) const fn exists_rows() -> Self {
505 Self {
506 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
507 }
508 }
509
510 #[cfg(test)]
513 #[must_use]
514 pub(crate) const fn aggregate(&self) -> AggregateExpr {
515 match self.runtime_request {
516 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
517 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
518 }
519 }
520
521 #[cfg(test)]
524 #[must_use]
525 pub(crate) const fn runtime_request(
526 &self,
527 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
528 &self.runtime_request
529 }
530
531 #[must_use]
534 pub(crate) const fn into_runtime_request(
535 self,
536 ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
537 self.runtime_request
538 }
539}
540
541impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
542 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
543 Some(match self.runtime_request {
544 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
545 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
546 })
547 }
548}
549
550#[derive(Clone, Debug, Eq, PartialEq)]
557pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
558 IdTerminal {
559 kind: AggregateKind,
560 },
561 IdBySlot {
562 kind: AggregateKind,
563 target_field: FieldSlot,
564 },
565}
566
567#[derive(Clone, Debug, Eq, PartialEq)]
579pub(crate) struct PreparedFluentScalarTerminalStrategy {
580 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
581}
582
583impl PreparedFluentScalarTerminalStrategy {
584 #[must_use]
586 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
587 Self {
588 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
589 }
590 }
591
592 #[must_use]
595 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
596 Self {
597 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
598 kind,
599 target_field,
600 },
601 }
602 }
603
604 #[must_use]
607 pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
608 self.runtime_request
609 }
610}
611
612impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
613 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
614 Some(match self.runtime_request {
615 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
616 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
617 })
618 }
619
620 fn explain_projected_field(&self) -> Option<&str> {
621 match &self.runtime_request {
622 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
623 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
624 Some(target_field.field())
625 }
626 }
627 }
628}
629
630#[derive(Clone, Copy, Debug, Eq, PartialEq)]
640pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
641 Sum,
642 SumDistinct,
643 Avg,
644 AvgDistinct,
645}
646
647#[derive(Clone, Debug, Eq, PartialEq)]
660pub(crate) struct PreparedFluentNumericFieldStrategy {
661 target_field: FieldSlot,
662 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
663}
664
665impl PreparedFluentNumericFieldStrategy {
666 #[must_use]
668 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
669 Self {
670 target_field,
671 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
672 }
673 }
674
675 #[must_use]
677 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
678 Self {
679 target_field,
680 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
681 }
682 }
683
684 #[must_use]
686 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
687 Self {
688 target_field,
689 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
690 }
691 }
692
693 #[must_use]
695 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
696 Self {
697 target_field,
698 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
699 }
700 }
701
702 #[cfg(test)]
705 #[must_use]
706 pub(crate) fn aggregate(&self) -> AggregateExpr {
707 let field = self.target_field.field();
708
709 match self.runtime_request {
710 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
711 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
712 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
713 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
714 }
715 }
716
717 #[cfg(test)]
720 #[must_use]
721 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
722 match self.runtime_request {
723 PreparedFluentNumericFieldRuntimeRequest::Sum
724 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
725 PreparedFluentNumericFieldRuntimeRequest::Avg
726 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
727 }
728 }
729
730 #[cfg(test)]
733 #[must_use]
734 pub(crate) fn projected_field(&self) -> &str {
735 self.target_field.field()
736 }
737
738 #[cfg(test)]
741 #[must_use]
742 pub(crate) const fn target_field(&self) -> &FieldSlot {
743 &self.target_field
744 }
745
746 #[cfg(test)]
749 #[must_use]
750 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
751 self.runtime_request
752 }
753
754 #[must_use]
757 pub(crate) fn into_runtime_parts(
758 self,
759 ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
760 (self.target_field, self.runtime_request)
761 }
762}
763
764impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
765 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
766 Some(match self.runtime_request {
767 PreparedFluentNumericFieldRuntimeRequest::Sum
768 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
769 PreparedFluentNumericFieldRuntimeRequest::Avg
770 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
771 })
772 }
773
774 fn explain_projected_field(&self) -> Option<&str> {
775 Some(self.target_field.field())
776 }
777}
778
779#[derive(Clone, Debug, Eq, PartialEq)]
790pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
791 ResponseOrder { kind: AggregateKind },
792 NthBySlot { target_field: FieldSlot, nth: usize },
793 MedianBySlot { target_field: FieldSlot },
794 MinMaxBySlot { target_field: FieldSlot },
795}
796
797#[derive(Clone, Debug, Eq, PartialEq)]
808pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
809 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
810}
811
812impl PreparedFluentOrderSensitiveTerminalStrategy {
813 #[must_use]
815 pub(crate) const fn first() -> Self {
816 Self {
817 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
818 kind: AggregateKind::First,
819 },
820 }
821 }
822
823 #[must_use]
825 pub(crate) const fn last() -> Self {
826 Self {
827 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
828 kind: AggregateKind::Last,
829 },
830 }
831 }
832
833 #[must_use]
835 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
836 Self {
837 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
838 target_field,
839 nth,
840 },
841 }
842 }
843
844 #[must_use]
846 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
847 Self {
848 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
849 target_field,
850 },
851 }
852 }
853
854 #[must_use]
856 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
857 Self {
858 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
859 target_field,
860 },
861 }
862 }
863
864 #[cfg(test)]
867 #[must_use]
868 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
869 match self.runtime_request {
870 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
871 Some(AggregateExpr::terminal_for_kind(kind))
872 }
873 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
874 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
875 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
876 }
877 }
878
879 #[cfg(test)]
882 #[must_use]
883 pub(crate) const fn runtime_request(
884 &self,
885 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
886 &self.runtime_request
887 }
888
889 #[must_use]
892 pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
893 self.runtime_request
894 }
895}
896
897impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
898 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
899 match self.runtime_request {
900 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
901 Some(kind)
902 }
903 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
904 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
905 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
906 }
907 }
908}
909
910#[derive(Clone, Copy, Debug, Eq, PartialEq)]
920pub(crate) enum PreparedFluentProjectionRuntimeRequest {
921 Values,
922 DistinctValues,
923 CountDistinct,
924 ValuesWithIds,
925 TerminalValue { terminal_kind: AggregateKind },
926}
927
928#[derive(Clone, Copy, Debug, Eq, PartialEq)]
938pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
939 terminal: &'static str,
940 field: &'a str,
941 output: &'static str,
942}
943
944impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
945 #[must_use]
947 pub(crate) const fn terminal_label(self) -> &'static str {
948 self.terminal
949 }
950
951 #[must_use]
953 pub(crate) const fn field_label(self) -> &'a str {
954 self.field
955 }
956
957 #[must_use]
959 pub(crate) const fn output_label(self) -> &'static str {
960 self.output
961 }
962}
963
964#[derive(Clone, Debug, Eq, PartialEq)]
975pub(crate) struct PreparedFluentProjectionStrategy {
976 target_field: FieldSlot,
977 runtime_request: PreparedFluentProjectionRuntimeRequest,
978}
979
980impl PreparedFluentProjectionStrategy {
981 #[must_use]
983 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
984 Self {
985 target_field,
986 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
987 }
988 }
989
990 #[must_use]
992 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
993 Self {
994 target_field,
995 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
996 }
997 }
998
999 #[must_use]
1001 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
1002 Self {
1003 target_field,
1004 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
1005 }
1006 }
1007
1008 #[must_use]
1010 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
1011 Self {
1012 target_field,
1013 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
1014 }
1015 }
1016
1017 #[must_use]
1019 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
1020 Self {
1021 target_field,
1022 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
1023 terminal_kind: AggregateKind::First,
1024 },
1025 }
1026 }
1027
1028 #[must_use]
1030 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
1031 Self {
1032 target_field,
1033 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
1034 terminal_kind: AggregateKind::Last,
1035 },
1036 }
1037 }
1038
1039 #[cfg(test)]
1042 #[must_use]
1043 pub(crate) const fn target_field(&self) -> &FieldSlot {
1044 &self.target_field
1045 }
1046
1047 #[cfg(test)]
1050 #[must_use]
1051 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
1052 self.runtime_request
1053 }
1054
1055 #[must_use]
1059 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
1060 (self.target_field, self.runtime_request)
1061 }
1062
1063 #[must_use]
1066 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
1067 let terminal_label = match self.runtime_request {
1068 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
1069 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
1070 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
1071 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
1072 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1073 terminal_kind: AggregateKind::First,
1074 } => "first_value_by",
1075 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1076 terminal_kind: AggregateKind::Last,
1077 } => "last_value_by",
1078 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
1079 unreachable!("projection terminal value explain requires FIRST/LAST kind")
1080 }
1081 };
1082 let output_label = match self.runtime_request {
1083 PreparedFluentProjectionRuntimeRequest::Values
1084 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
1085 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
1086 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
1087 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
1088 };
1089
1090 PreparedFluentProjectionExplainDescriptor {
1091 terminal: terminal_label,
1092 field: self.target_field.field(),
1093 output: output_label,
1094 }
1095 }
1096}
1097
1098#[must_use]
1100pub const fn count() -> AggregateExpr {
1101 AggregateExpr::terminal(AggregateKind::Count)
1102}
1103
1104#[must_use]
1106pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
1107 AggregateExpr::field_target(AggregateKind::Count, field.as_ref().to_string())
1108}
1109
1110#[must_use]
1112pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
1113 AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
1114}
1115
1116#[must_use]
1118pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
1119 AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
1120}
1121
1122#[must_use]
1124pub const fn exists() -> AggregateExpr {
1125 AggregateExpr::terminal(AggregateKind::Exists)
1126}
1127
1128#[must_use]
1130pub const fn first() -> AggregateExpr {
1131 AggregateExpr::terminal(AggregateKind::First)
1132}
1133
1134#[must_use]
1136pub const fn last() -> AggregateExpr {
1137 AggregateExpr::terminal(AggregateKind::Last)
1138}
1139
1140#[must_use]
1142pub const fn min() -> AggregateExpr {
1143 AggregateExpr::terminal(AggregateKind::Min)
1144}
1145
1146#[must_use]
1148pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
1149 AggregateExpr::field_target(AggregateKind::Min, field.as_ref().to_string())
1150}
1151
1152#[must_use]
1154pub const fn max() -> AggregateExpr {
1155 AggregateExpr::terminal(AggregateKind::Max)
1156}
1157
1158#[must_use]
1160pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
1161 AggregateExpr::field_target(AggregateKind::Max, field.as_ref().to_string())
1162}
1163
1164#[cfg(test)]
1169mod tests {
1170 use crate::db::query::{
1171 builder::{
1172 PreparedFluentExistingRowsTerminalRuntimeRequest,
1173 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
1174 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
1175 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
1176 PreparedFluentProjectionStrategy,
1177 },
1178 plan::{AggregateKind, FieldSlot},
1179 };
1180
1181 #[test]
1182 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
1183 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1184 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
1185
1186 assert_eq!(
1187 strategy.aggregate_kind(),
1188 AggregateKind::Sum,
1189 "sum(distinct field) should preserve SUM aggregate kind",
1190 );
1191 assert_eq!(
1192 strategy.projected_field(),
1193 "rank",
1194 "sum(distinct field) should preserve projected field labels",
1195 );
1196 assert!(
1197 strategy.aggregate().is_distinct(),
1198 "sum(distinct field) should preserve DISTINCT aggregate shape",
1199 );
1200 assert_eq!(
1201 strategy.target_field(),
1202 &rank_slot,
1203 "sum(distinct field) should preserve the resolved planner field slot",
1204 );
1205 assert_eq!(
1206 strategy.runtime_request(),
1207 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
1208 "sum(distinct field) should project the numeric DISTINCT runtime request",
1209 );
1210 }
1211
1212 #[test]
1213 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
1214 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
1215
1216 assert_eq!(
1217 strategy.aggregate().kind(),
1218 AggregateKind::Count,
1219 "count() should preserve the explain-visible aggregate kind",
1220 );
1221 assert_eq!(
1222 strategy.runtime_request(),
1223 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
1224 "count() should project the existing-rows count runtime request",
1225 );
1226 }
1227
1228 #[test]
1229 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
1230 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
1231
1232 assert_eq!(
1233 strategy.aggregate().kind(),
1234 AggregateKind::Exists,
1235 "exists() should preserve the explain-visible aggregate kind",
1236 );
1237 assert_eq!(
1238 strategy.runtime_request(),
1239 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
1240 "exists() should project the existing-rows exists runtime request",
1241 );
1242 }
1243
1244 #[test]
1245 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
1246 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1247 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
1248
1249 assert_eq!(
1250 strategy.aggregate_kind(),
1251 AggregateKind::Avg,
1252 "avg(field) should preserve AVG aggregate kind",
1253 );
1254 assert_eq!(
1255 strategy.projected_field(),
1256 "rank",
1257 "avg(field) should preserve projected field labels",
1258 );
1259 assert!(
1260 !strategy.aggregate().is_distinct(),
1261 "avg(field) should stay non-distinct unless requested explicitly",
1262 );
1263 assert_eq!(
1264 strategy.target_field(),
1265 &rank_slot,
1266 "avg(field) should preserve the resolved planner field slot",
1267 );
1268 assert_eq!(
1269 strategy.runtime_request(),
1270 PreparedFluentNumericFieldRuntimeRequest::Avg,
1271 "avg(field) should project the numeric AVG runtime request",
1272 );
1273 }
1274
1275 #[test]
1276 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
1277 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
1278
1279 assert_eq!(
1280 strategy
1281 .explain_aggregate()
1282 .map(|aggregate| aggregate.kind()),
1283 Some(AggregateKind::First),
1284 "first() should preserve the explain-visible aggregate kind",
1285 );
1286 assert_eq!(
1287 strategy.runtime_request(),
1288 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
1289 kind: AggregateKind::First,
1290 },
1291 "first() should project the response-order runtime request",
1292 );
1293 }
1294
1295 #[test]
1296 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
1297 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1298 let strategy =
1299 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
1300
1301 assert_eq!(
1302 strategy.explain_aggregate(),
1303 None,
1304 "nth_by(field, nth) should stay off the current explain aggregate surface",
1305 );
1306 assert_eq!(
1307 strategy.runtime_request(),
1308 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
1309 target_field: rank_slot,
1310 nth: 2,
1311 },
1312 "nth_by(field, nth) should preserve the resolved field-order runtime request",
1313 );
1314 }
1315
1316 #[test]
1317 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
1318 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1319 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
1320 let explain = strategy.explain_descriptor();
1321
1322 assert_eq!(
1323 strategy.target_field(),
1324 &rank_slot,
1325 "count_distinct_by(field) should preserve the resolved planner field slot",
1326 );
1327 assert_eq!(
1328 strategy.runtime_request(),
1329 PreparedFluentProjectionRuntimeRequest::CountDistinct,
1330 "count_distinct_by(field) should project the distinct-count runtime request",
1331 );
1332 assert_eq!(
1333 explain.terminal_label(),
1334 "count_distinct_by",
1335 "count_distinct_by(field) should project the stable explain terminal label",
1336 );
1337 assert_eq!(
1338 explain.field_label(),
1339 "rank",
1340 "count_distinct_by(field) should project the stable explain field label",
1341 );
1342 assert_eq!(
1343 explain.output_label(),
1344 "count",
1345 "count_distinct_by(field) should project the stable explain output label",
1346 );
1347 }
1348
1349 #[test]
1350 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1351 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1352 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1353 let explain = strategy.explain_descriptor();
1354
1355 assert_eq!(
1356 strategy.target_field(),
1357 &rank_slot,
1358 "last_value_by(field) should preserve the resolved planner field slot",
1359 );
1360 assert_eq!(
1361 strategy.runtime_request(),
1362 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1363 terminal_kind: AggregateKind::Last,
1364 },
1365 "last_value_by(field) should project the terminal-value runtime request",
1366 );
1367 assert_eq!(
1368 explain.terminal_label(),
1369 "last_value_by",
1370 "last_value_by(field) should project the stable explain terminal label",
1371 );
1372 assert_eq!(
1373 explain.field_label(),
1374 "rank",
1375 "last_value_by(field) should project the stable explain field label",
1376 );
1377 assert_eq!(
1378 explain.output_label(),
1379 "terminal_value",
1380 "last_value_by(field) should project the stable explain output label",
1381 );
1382 }
1383}