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::Coalesce => fold_aggregate_input_constant_coalesce(args),
257 Function::NullIf => fold_aggregate_input_constant_nullif(args),
258 Function::Abs | Function::Ceil | Function::Ceiling | Function::Floor => {
259 fold_aggregate_input_constant_unary_numeric(function, args)
260 }
261 Function::IsNull
262 | Function::IsNotNull
263 | Function::IsMissing
264 | Function::IsEmpty
265 | Function::IsNotEmpty
266 | Function::Trim
267 | Function::Ltrim
268 | Function::Rtrim
269 | Function::Left
270 | Function::Right
271 | Function::StartsWith
272 | Function::EndsWith
273 | Function::Contains
274 | Function::CollectionContains
275 | Function::Position
276 | Function::Replace
277 | Function::Substring
278 | Function::Lower
279 | Function::Upper
280 | Function::Length => None,
281 }
282}
283
284fn fold_aggregate_input_constant_unary_numeric(function: Function, args: &[Expr]) -> Option<Expr> {
285 let [Expr::Literal(input)] = args else {
286 return None;
287 };
288 if matches!(input, Value::Null) {
289 return Some(Expr::Literal(Value::Null));
290 }
291
292 let decimal = input.to_numeric_decimal()?;
293 let result = match function {
294 Function::Abs => decimal.abs(),
295 Function::Ceil | Function::Ceiling => decimal.ceil_dp0(),
296 Function::Floor => decimal.floor_dp0(),
297 _ => return None,
298 };
299
300 Some(Expr::Literal(Value::Decimal(result)))
301}
302
303fn fold_aggregate_input_constant_round(args: &[Expr]) -> Option<Expr> {
306 let [Expr::Literal(input), Expr::Literal(scale)] = args else {
307 return None;
308 };
309 if matches!(input, Value::Null) || matches!(scale, Value::Null) {
310 return Some(Expr::Literal(Value::Null));
311 }
312
313 let scale = match scale {
314 Value::Int(value) => u32::try_from(*value).ok()?,
315 Value::Uint(value) => u32::try_from(*value).ok()?,
316 _ => return None,
317 };
318 let decimal = input.to_numeric_decimal()?;
319
320 Some(Expr::Literal(Value::Decimal(decimal.round_dp(scale))))
321}
322
323fn fold_aggregate_input_constant_coalesce(args: &[Expr]) -> Option<Expr> {
324 let mut literal_values = Vec::with_capacity(args.len());
325 for arg in args {
326 let Expr::Literal(value) = arg else {
327 return None;
328 };
329 literal_values.push(value.clone());
330 }
331
332 Some(Expr::Literal(
333 literal_values
334 .into_iter()
335 .find(|value| !matches!(value, Value::Null))
336 .unwrap_or(Value::Null),
337 ))
338}
339
340fn fold_aggregate_input_constant_nullif(args: &[Expr]) -> Option<Expr> {
341 let [Expr::Literal(left), Expr::Literal(right)] = args else {
342 return None;
343 };
344 if matches!(left, Value::Null) || matches!(right, Value::Null) {
345 return Some(Expr::Literal(left.clone()));
346 }
347
348 Some(Expr::Literal(if left == right {
349 Value::Null
350 } else {
351 left.clone()
352 }))
353}
354
355fn normalize_aggregate_input_numeric_literals(expr: Expr) -> Expr {
359 match expr {
360 Expr::Literal(value) => value
361 .to_numeric_decimal()
362 .map_or(Expr::Literal(value), |decimal| {
363 Expr::Literal(Value::Decimal(decimal.normalize()))
364 }),
365 Expr::Field(_) | Expr::Aggregate(_) => expr,
366 Expr::FunctionCall { function, args } => Expr::FunctionCall {
367 function,
368 args: args
369 .into_iter()
370 .map(normalize_aggregate_input_numeric_literals)
371 .collect(),
372 },
373 Expr::Case {
374 when_then_arms,
375 else_expr,
376 } => Expr::Case {
377 when_then_arms: when_then_arms
378 .into_iter()
379 .map(|arm| {
380 crate::db::query::plan::expr::CaseWhenArm::new(
381 normalize_aggregate_input_numeric_literals(arm.condition().clone()),
382 normalize_aggregate_input_numeric_literals(arm.result().clone()),
383 )
384 })
385 .collect(),
386 else_expr: Box::new(normalize_aggregate_input_numeric_literals(*else_expr)),
387 },
388 Expr::Binary { op, left, right } => Expr::Binary {
389 op,
390 left: Box::new(normalize_aggregate_input_numeric_literals(*left)),
391 right: Box::new(normalize_aggregate_input_numeric_literals(*right)),
392 },
393 #[cfg(test)]
394 Expr::Alias { expr, name } => Expr::Alias {
395 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
396 name,
397 },
398 Expr::Unary { op, expr } => Expr::Unary {
399 op,
400 expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
401 },
402 }
403}
404
405pub(crate) trait PreparedFluentAggregateExplainStrategy {
416 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
419
420 fn explain_projected_field(&self) -> Option<&str> {
422 None
423 }
424}
425
426#[derive(Clone, Debug, Eq, PartialEq)]
433pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
434 CountRows,
435 ExistsRows,
436}
437
438#[derive(Clone, Debug, Eq, PartialEq)]
451pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
452 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
453}
454
455impl PreparedFluentExistingRowsTerminalStrategy {
456 #[must_use]
458 pub(crate) const fn count_rows() -> Self {
459 Self {
460 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
461 }
462 }
463
464 #[must_use]
466 pub(crate) const fn exists_rows() -> Self {
467 Self {
468 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
469 }
470 }
471
472 #[cfg(test)]
475 #[must_use]
476 pub(crate) const fn aggregate(&self) -> AggregateExpr {
477 match self.runtime_request {
478 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
479 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
480 }
481 }
482
483 #[cfg(test)]
486 #[must_use]
487 pub(crate) const fn runtime_request(
488 &self,
489 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
490 &self.runtime_request
491 }
492
493 #[must_use]
496 pub(crate) const fn into_runtime_request(
497 self,
498 ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
499 self.runtime_request
500 }
501}
502
503impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
504 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
505 Some(match self.runtime_request {
506 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
507 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
508 })
509 }
510}
511
512#[derive(Clone, Debug, Eq, PartialEq)]
519pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
520 IdTerminal {
521 kind: AggregateKind,
522 },
523 IdBySlot {
524 kind: AggregateKind,
525 target_field: FieldSlot,
526 },
527}
528
529#[derive(Clone, Debug, Eq, PartialEq)]
541pub(crate) struct PreparedFluentScalarTerminalStrategy {
542 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
543}
544
545impl PreparedFluentScalarTerminalStrategy {
546 #[must_use]
548 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
549 Self {
550 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
551 }
552 }
553
554 #[must_use]
557 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
558 Self {
559 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
560 kind,
561 target_field,
562 },
563 }
564 }
565
566 #[must_use]
569 pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
570 self.runtime_request
571 }
572}
573
574impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
575 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
576 Some(match self.runtime_request {
577 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
578 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
579 })
580 }
581
582 fn explain_projected_field(&self) -> Option<&str> {
583 match &self.runtime_request {
584 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
585 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
586 Some(target_field.field())
587 }
588 }
589 }
590}
591
592#[derive(Clone, Copy, Debug, Eq, PartialEq)]
602pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
603 Sum,
604 SumDistinct,
605 Avg,
606 AvgDistinct,
607}
608
609#[derive(Clone, Debug, Eq, PartialEq)]
622pub(crate) struct PreparedFluentNumericFieldStrategy {
623 target_field: FieldSlot,
624 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
625}
626
627impl PreparedFluentNumericFieldStrategy {
628 #[must_use]
630 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
631 Self {
632 target_field,
633 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
634 }
635 }
636
637 #[must_use]
639 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
640 Self {
641 target_field,
642 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
643 }
644 }
645
646 #[must_use]
648 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
649 Self {
650 target_field,
651 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
652 }
653 }
654
655 #[must_use]
657 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
658 Self {
659 target_field,
660 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
661 }
662 }
663
664 #[cfg(test)]
667 #[must_use]
668 pub(crate) fn aggregate(&self) -> AggregateExpr {
669 let field = self.target_field.field();
670
671 match self.runtime_request {
672 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
673 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
674 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
675 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
676 }
677 }
678
679 #[cfg(test)]
682 #[must_use]
683 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
684 match self.runtime_request {
685 PreparedFluentNumericFieldRuntimeRequest::Sum
686 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
687 PreparedFluentNumericFieldRuntimeRequest::Avg
688 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
689 }
690 }
691
692 #[cfg(test)]
695 #[must_use]
696 pub(crate) fn projected_field(&self) -> &str {
697 self.target_field.field()
698 }
699
700 #[cfg(test)]
703 #[must_use]
704 pub(crate) const fn target_field(&self) -> &FieldSlot {
705 &self.target_field
706 }
707
708 #[cfg(test)]
711 #[must_use]
712 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
713 self.runtime_request
714 }
715
716 #[must_use]
719 pub(crate) fn into_runtime_parts(
720 self,
721 ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
722 (self.target_field, self.runtime_request)
723 }
724}
725
726impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
727 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
728 Some(match self.runtime_request {
729 PreparedFluentNumericFieldRuntimeRequest::Sum
730 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
731 PreparedFluentNumericFieldRuntimeRequest::Avg
732 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
733 })
734 }
735
736 fn explain_projected_field(&self) -> Option<&str> {
737 Some(self.target_field.field())
738 }
739}
740
741#[derive(Clone, Debug, Eq, PartialEq)]
752pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
753 ResponseOrder { kind: AggregateKind },
754 NthBySlot { target_field: FieldSlot, nth: usize },
755 MedianBySlot { target_field: FieldSlot },
756 MinMaxBySlot { target_field: FieldSlot },
757}
758
759#[derive(Clone, Debug, Eq, PartialEq)]
770pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
771 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
772}
773
774impl PreparedFluentOrderSensitiveTerminalStrategy {
775 #[must_use]
777 pub(crate) const fn first() -> Self {
778 Self {
779 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
780 kind: AggregateKind::First,
781 },
782 }
783 }
784
785 #[must_use]
787 pub(crate) const fn last() -> Self {
788 Self {
789 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
790 kind: AggregateKind::Last,
791 },
792 }
793 }
794
795 #[must_use]
797 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
798 Self {
799 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
800 target_field,
801 nth,
802 },
803 }
804 }
805
806 #[must_use]
808 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
809 Self {
810 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
811 target_field,
812 },
813 }
814 }
815
816 #[must_use]
818 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
819 Self {
820 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
821 target_field,
822 },
823 }
824 }
825
826 #[cfg(test)]
829 #[must_use]
830 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
831 match self.runtime_request {
832 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
833 Some(AggregateExpr::terminal_for_kind(kind))
834 }
835 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
836 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
837 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
838 }
839 }
840
841 #[cfg(test)]
844 #[must_use]
845 pub(crate) const fn runtime_request(
846 &self,
847 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
848 &self.runtime_request
849 }
850
851 #[must_use]
854 pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
855 self.runtime_request
856 }
857}
858
859impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
860 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
861 match self.runtime_request {
862 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
863 Some(kind)
864 }
865 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
866 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
867 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
868 }
869 }
870}
871
872#[derive(Clone, Copy, Debug, Eq, PartialEq)]
882pub(crate) enum PreparedFluentProjectionRuntimeRequest {
883 Values,
884 DistinctValues,
885 CountDistinct,
886 ValuesWithIds,
887 TerminalValue { terminal_kind: AggregateKind },
888}
889
890#[derive(Clone, Copy, Debug, Eq, PartialEq)]
900pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
901 terminal: &'static str,
902 field: &'a str,
903 output: &'static str,
904}
905
906impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
907 #[must_use]
909 pub(crate) const fn terminal_label(self) -> &'static str {
910 self.terminal
911 }
912
913 #[must_use]
915 pub(crate) const fn field_label(self) -> &'a str {
916 self.field
917 }
918
919 #[must_use]
921 pub(crate) const fn output_label(self) -> &'static str {
922 self.output
923 }
924}
925
926#[derive(Clone, Debug, Eq, PartialEq)]
937pub(crate) struct PreparedFluentProjectionStrategy {
938 target_field: FieldSlot,
939 runtime_request: PreparedFluentProjectionRuntimeRequest,
940}
941
942impl PreparedFluentProjectionStrategy {
943 #[must_use]
945 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
946 Self {
947 target_field,
948 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
949 }
950 }
951
952 #[must_use]
954 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
955 Self {
956 target_field,
957 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
958 }
959 }
960
961 #[must_use]
963 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
964 Self {
965 target_field,
966 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
967 }
968 }
969
970 #[must_use]
972 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
973 Self {
974 target_field,
975 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
976 }
977 }
978
979 #[must_use]
981 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
982 Self {
983 target_field,
984 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
985 terminal_kind: AggregateKind::First,
986 },
987 }
988 }
989
990 #[must_use]
992 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
993 Self {
994 target_field,
995 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
996 terminal_kind: AggregateKind::Last,
997 },
998 }
999 }
1000
1001 #[cfg(test)]
1004 #[must_use]
1005 pub(crate) const fn target_field(&self) -> &FieldSlot {
1006 &self.target_field
1007 }
1008
1009 #[cfg(test)]
1012 #[must_use]
1013 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
1014 self.runtime_request
1015 }
1016
1017 #[must_use]
1021 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
1022 (self.target_field, self.runtime_request)
1023 }
1024
1025 #[must_use]
1028 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
1029 let terminal_label = match self.runtime_request {
1030 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
1031 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
1032 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
1033 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
1034 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1035 terminal_kind: AggregateKind::First,
1036 } => "first_value_by",
1037 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1038 terminal_kind: AggregateKind::Last,
1039 } => "last_value_by",
1040 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
1041 unreachable!("projection terminal value explain requires FIRST/LAST kind")
1042 }
1043 };
1044 let output_label = match self.runtime_request {
1045 PreparedFluentProjectionRuntimeRequest::Values
1046 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
1047 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
1048 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
1049 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
1050 };
1051
1052 PreparedFluentProjectionExplainDescriptor {
1053 terminal: terminal_label,
1054 field: self.target_field.field(),
1055 output: output_label,
1056 }
1057 }
1058}
1059
1060#[must_use]
1062pub const fn count() -> AggregateExpr {
1063 AggregateExpr::terminal(AggregateKind::Count)
1064}
1065
1066#[must_use]
1068pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
1069 AggregateExpr::field_target(AggregateKind::Count, field.as_ref().to_string())
1070}
1071
1072#[must_use]
1074pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
1075 AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
1076}
1077
1078#[must_use]
1080pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
1081 AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
1082}
1083
1084#[must_use]
1086pub const fn exists() -> AggregateExpr {
1087 AggregateExpr::terminal(AggregateKind::Exists)
1088}
1089
1090#[must_use]
1092pub const fn first() -> AggregateExpr {
1093 AggregateExpr::terminal(AggregateKind::First)
1094}
1095
1096#[must_use]
1098pub const fn last() -> AggregateExpr {
1099 AggregateExpr::terminal(AggregateKind::Last)
1100}
1101
1102#[must_use]
1104pub const fn min() -> AggregateExpr {
1105 AggregateExpr::terminal(AggregateKind::Min)
1106}
1107
1108#[must_use]
1110pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
1111 AggregateExpr::field_target(AggregateKind::Min, field.as_ref().to_string())
1112}
1113
1114#[must_use]
1116pub const fn max() -> AggregateExpr {
1117 AggregateExpr::terminal(AggregateKind::Max)
1118}
1119
1120#[must_use]
1122pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
1123 AggregateExpr::field_target(AggregateKind::Max, field.as_ref().to_string())
1124}
1125
1126#[cfg(test)]
1131mod tests {
1132 use crate::db::query::{
1133 builder::{
1134 PreparedFluentExistingRowsTerminalRuntimeRequest,
1135 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
1136 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
1137 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
1138 PreparedFluentProjectionStrategy,
1139 },
1140 plan::{AggregateKind, FieldSlot},
1141 };
1142
1143 #[test]
1144 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
1145 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1146 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
1147
1148 assert_eq!(
1149 strategy.aggregate_kind(),
1150 AggregateKind::Sum,
1151 "sum(distinct field) should preserve SUM aggregate kind",
1152 );
1153 assert_eq!(
1154 strategy.projected_field(),
1155 "rank",
1156 "sum(distinct field) should preserve projected field labels",
1157 );
1158 assert!(
1159 strategy.aggregate().is_distinct(),
1160 "sum(distinct field) should preserve DISTINCT aggregate shape",
1161 );
1162 assert_eq!(
1163 strategy.target_field(),
1164 &rank_slot,
1165 "sum(distinct field) should preserve the resolved planner field slot",
1166 );
1167 assert_eq!(
1168 strategy.runtime_request(),
1169 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
1170 "sum(distinct field) should project the numeric DISTINCT runtime request",
1171 );
1172 }
1173
1174 #[test]
1175 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
1176 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
1177
1178 assert_eq!(
1179 strategy.aggregate().kind(),
1180 AggregateKind::Count,
1181 "count() should preserve the explain-visible aggregate kind",
1182 );
1183 assert_eq!(
1184 strategy.runtime_request(),
1185 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
1186 "count() should project the existing-rows count runtime request",
1187 );
1188 }
1189
1190 #[test]
1191 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
1192 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
1193
1194 assert_eq!(
1195 strategy.aggregate().kind(),
1196 AggregateKind::Exists,
1197 "exists() should preserve the explain-visible aggregate kind",
1198 );
1199 assert_eq!(
1200 strategy.runtime_request(),
1201 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
1202 "exists() should project the existing-rows exists runtime request",
1203 );
1204 }
1205
1206 #[test]
1207 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
1208 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1209 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
1210
1211 assert_eq!(
1212 strategy.aggregate_kind(),
1213 AggregateKind::Avg,
1214 "avg(field) should preserve AVG aggregate kind",
1215 );
1216 assert_eq!(
1217 strategy.projected_field(),
1218 "rank",
1219 "avg(field) should preserve projected field labels",
1220 );
1221 assert!(
1222 !strategy.aggregate().is_distinct(),
1223 "avg(field) should stay non-distinct unless requested explicitly",
1224 );
1225 assert_eq!(
1226 strategy.target_field(),
1227 &rank_slot,
1228 "avg(field) should preserve the resolved planner field slot",
1229 );
1230 assert_eq!(
1231 strategy.runtime_request(),
1232 PreparedFluentNumericFieldRuntimeRequest::Avg,
1233 "avg(field) should project the numeric AVG runtime request",
1234 );
1235 }
1236
1237 #[test]
1238 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
1239 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
1240
1241 assert_eq!(
1242 strategy
1243 .explain_aggregate()
1244 .map(|aggregate| aggregate.kind()),
1245 Some(AggregateKind::First),
1246 "first() should preserve the explain-visible aggregate kind",
1247 );
1248 assert_eq!(
1249 strategy.runtime_request(),
1250 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
1251 kind: AggregateKind::First,
1252 },
1253 "first() should project the response-order runtime request",
1254 );
1255 }
1256
1257 #[test]
1258 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
1259 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1260 let strategy =
1261 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
1262
1263 assert_eq!(
1264 strategy.explain_aggregate(),
1265 None,
1266 "nth_by(field, nth) should stay off the current explain aggregate surface",
1267 );
1268 assert_eq!(
1269 strategy.runtime_request(),
1270 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
1271 target_field: rank_slot,
1272 nth: 2,
1273 },
1274 "nth_by(field, nth) should preserve the resolved field-order runtime request",
1275 );
1276 }
1277
1278 #[test]
1279 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
1280 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1281 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
1282 let explain = strategy.explain_descriptor();
1283
1284 assert_eq!(
1285 strategy.target_field(),
1286 &rank_slot,
1287 "count_distinct_by(field) should preserve the resolved planner field slot",
1288 );
1289 assert_eq!(
1290 strategy.runtime_request(),
1291 PreparedFluentProjectionRuntimeRequest::CountDistinct,
1292 "count_distinct_by(field) should project the distinct-count runtime request",
1293 );
1294 assert_eq!(
1295 explain.terminal_label(),
1296 "count_distinct_by",
1297 "count_distinct_by(field) should project the stable explain terminal label",
1298 );
1299 assert_eq!(
1300 explain.field_label(),
1301 "rank",
1302 "count_distinct_by(field) should project the stable explain field label",
1303 );
1304 assert_eq!(
1305 explain.output_label(),
1306 "count",
1307 "count_distinct_by(field) should project the stable explain output label",
1308 );
1309 }
1310
1311 #[test]
1312 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1313 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1314 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1315 let explain = strategy.explain_descriptor();
1316
1317 assert_eq!(
1318 strategy.target_field(),
1319 &rank_slot,
1320 "last_value_by(field) should preserve the resolved planner field slot",
1321 );
1322 assert_eq!(
1323 strategy.runtime_request(),
1324 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1325 terminal_kind: AggregateKind::Last,
1326 },
1327 "last_value_by(field) should project the terminal-value runtime request",
1328 );
1329 assert_eq!(
1330 explain.terminal_label(),
1331 "last_value_by",
1332 "last_value_by(field) should project the stable explain terminal label",
1333 );
1334 assert_eq!(
1335 explain.field_label(),
1336 "rank",
1337 "last_value_by(field) should project the stable explain field label",
1338 );
1339 assert_eq!(
1340 explain.output_label(),
1341 "terminal_value",
1342 "last_value_by(field) should project the stable explain output label",
1343 );
1344 }
1345}