Skip to main content

icydb_core/db/query/builder/
aggregate.rs

1//! Module: query::builder::aggregate
2//! Responsibility: composable grouped/global aggregate expression builders.
3//! Does not own: aggregate validation policy or executor fold semantics.
4//! Boundary: fluent aggregate intent construction lowered into grouped specs.
5
6use 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///
16/// AggregateExpr
17///
18/// Composable aggregate expression used by query/fluent aggregate entrypoints.
19/// This builder only carries declarative shape (`kind`, aggregate input
20/// expression, `distinct`) and does not perform semantic validation.
21///
22
23#[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    /// Construct one terminal aggregate expression with no input expression.
32    const fn terminal(kind: AggregateKind) -> Self {
33        Self {
34            kind,
35            input_expr: None,
36            distinct: false,
37        }
38    }
39
40    /// Construct one aggregate expression over one canonical field leaf.
41    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    /// Construct one aggregate expression from one planner-owned input expression.
50    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    /// Enable DISTINCT modifier for this aggregate expression.
61    #[must_use]
62    pub const fn distinct(mut self) -> Self {
63        self.distinct = true;
64        self
65    }
66
67    /// Borrow aggregate kind.
68    #[must_use]
69    pub(crate) const fn kind(&self) -> AggregateKind {
70        self.kind
71    }
72
73    /// Borrow the aggregate input expression, if any.
74    #[must_use]
75    pub(crate) fn input_expr(&self) -> Option<&Expr> {
76        self.input_expr.as_deref()
77    }
78
79    /// Borrow the optional target field when this aggregate input stays a plain field leaf.
80    #[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    /// Return true when DISTINCT is enabled.
89    #[must_use]
90    pub(crate) const fn is_distinct(&self) -> bool {
91        self.distinct
92    }
93
94    /// Build one aggregate expression directly from planner semantic parts.
95    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    /// Build one non-field-target terminal aggregate expression from one kind.
108    #[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
125// Keep aggregate input identity canonical anywhere planner-owned aggregate
126// expressions are constructed so grouped/global paths do not drift on
127// semantically equivalent constant subexpressions.
128pub(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
150// Fold literal-only aggregate-input subexpressions so semantic aggregate
151// matching can treat `AVG(age + 1 * 2)` and `AVG(age + 2)` as the same input.
152fn 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::Binary { op, left, right } => {
165            let left = fold_aggregate_input_constant_expr(*left);
166            let right = fold_aggregate_input_constant_expr(*right);
167
168            fold_aggregate_input_constant_binary(op, &left, &right).unwrap_or_else(|| {
169                Expr::Binary {
170                    op,
171                    left: Box::new(left),
172                    right: Box::new(right),
173                }
174            })
175        }
176        #[cfg(test)]
177        Expr::Alias { expr, name } => Expr::Alias {
178            expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
179            name,
180        },
181        #[cfg(test)]
182        Expr::Unary { op, expr } => Expr::Unary {
183            op,
184            expr: Box::new(fold_aggregate_input_constant_expr(*expr)),
185        },
186    }
187}
188
189// Fold one literal-only binary aggregate-input fragment onto one decimal
190// literal so aggregate identity stays stable across equivalent SQL spellings.
191fn fold_aggregate_input_constant_binary(op: BinaryOp, left: &Expr, right: &Expr) -> Option<Expr> {
192    let (Expr::Literal(left), Expr::Literal(right)) = (left, right) else {
193        return None;
194    };
195    if matches!(left, Value::Null) || matches!(right, Value::Null) {
196        return Some(Expr::Literal(Value::Null));
197    }
198
199    let arithmetic_op = match op {
200        BinaryOp::Add => NumericArithmeticOp::Add,
201        BinaryOp::Sub => NumericArithmeticOp::Sub,
202        BinaryOp::Mul => NumericArithmeticOp::Mul,
203        BinaryOp::Div => NumericArithmeticOp::Div,
204        #[cfg(test)]
205        BinaryOp::And | BinaryOp::Eq => return None,
206    };
207    let result = apply_numeric_arithmetic(arithmetic_op, left, right)?;
208
209    Some(Expr::Literal(Value::Decimal(result)))
210}
211
212// Fold one admitted literal-only aggregate-input function call when the
213// reduced aggregate-input family has one deterministic literal result.
214fn fold_aggregate_input_constant_function(function: Function, args: &[Expr]) -> Option<Expr> {
215    match function {
216        Function::Round => fold_aggregate_input_constant_round(args),
217        Function::Trim
218        | Function::Ltrim
219        | Function::Rtrim
220        | Function::Lower
221        | Function::Upper
222        | Function::Length
223        | Function::Left
224        | Function::Right
225        | Function::StartsWith
226        | Function::EndsWith
227        | Function::Contains
228        | Function::Position
229        | Function::Replace
230        | Function::Substring => None,
231    }
232}
233
234// Fold one literal-only ROUND(...) aggregate-input fragment so parenthesized
235// constant arithmetic keeps the same aggregate identity as its literal result.
236fn fold_aggregate_input_constant_round(args: &[Expr]) -> Option<Expr> {
237    let [Expr::Literal(input), Expr::Literal(scale)] = args else {
238        return None;
239    };
240    if matches!(input, Value::Null) || matches!(scale, Value::Null) {
241        return Some(Expr::Literal(Value::Null));
242    }
243
244    let scale = match scale {
245        Value::Int(value) => u32::try_from(*value).ok()?,
246        Value::Uint(value) => u32::try_from(*value).ok()?,
247        _ => return None,
248    };
249    let decimal = input.to_numeric_decimal()?;
250
251    Some(Expr::Literal(Value::Decimal(decimal.round_dp(scale))))
252}
253
254// Normalize numeric literal leaves recursively so semantically equivalent
255// aggregate inputs like `age + 2` and `age + 1 * 2` share one canonical
256// planner identity after literal-only subtree folding.
257fn normalize_aggregate_input_numeric_literals(expr: Expr) -> Expr {
258    match expr {
259        Expr::Literal(value) => value
260            .to_numeric_decimal()
261            .map_or(Expr::Literal(value), |decimal| {
262                Expr::Literal(Value::Decimal(decimal.normalize()))
263            }),
264        Expr::Field(_) | Expr::Aggregate(_) => expr,
265        Expr::FunctionCall { function, args } => Expr::FunctionCall {
266            function,
267            args: args
268                .into_iter()
269                .map(normalize_aggregate_input_numeric_literals)
270                .collect(),
271        },
272        Expr::Binary { op, left, right } => Expr::Binary {
273            op,
274            left: Box::new(normalize_aggregate_input_numeric_literals(*left)),
275            right: Box::new(normalize_aggregate_input_numeric_literals(*right)),
276        },
277        #[cfg(test)]
278        Expr::Alias { expr, name } => Expr::Alias {
279            expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
280            name,
281        },
282        #[cfg(test)]
283        Expr::Unary { op, expr } => Expr::Unary {
284            op,
285            expr: Box::new(normalize_aggregate_input_numeric_literals(*expr)),
286        },
287    }
288}
289
290///
291/// PreparedFluentAggregateExplainStrategy
292///
293/// PreparedFluentAggregateExplainStrategy is the shared explain-only
294/// projection contract for fluent aggregate domains that can render one
295/// `AggregateExpr`.
296/// It keeps session/query explain projection generic without collapsing the
297/// runtime domain boundaries that still stay family-specific.
298///
299
300pub(crate) trait PreparedFluentAggregateExplainStrategy {
301    /// Return the explain-visible aggregate kind when this runtime family can
302    /// project one aggregate terminal plan shape.
303    fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
304
305    /// Return the explain-visible projected field label, if any.
306    fn explain_projected_field(&self) -> Option<&str> {
307        None
308    }
309}
310
311/// PreparedFluentExistingRowsTerminalRuntimeRequest
312///
313/// Stable fluent existing-rows terminal runtime request projection derived
314/// once at the fluent aggregate entrypoint boundary.
315/// This keeps count/exists request choice aligned with the aggregate
316/// expression used for explain projection.
317#[derive(Clone, Debug, Eq, PartialEq)]
318pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
319    CountRows,
320    ExistsRows,
321}
322
323///
324/// PreparedFluentExistingRowsTerminalStrategy
325///
326/// PreparedFluentExistingRowsTerminalStrategy is the single fluent
327/// existing-rows behavior source for the next `0.71` slice.
328/// It resolves runtime terminal request shape once and projects explain
329/// aggregate metadata from that same prepared state on demand.
330/// This keeps `count()` and `exists()` off the mixed id/extrema scalar
331/// strategy without carrying owned explain-only aggregate expressions through
332/// execution.
333///
334
335#[derive(Clone, Debug, Eq, PartialEq)]
336pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
337    runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
338}
339
340impl PreparedFluentExistingRowsTerminalStrategy {
341    /// Prepare one fluent `count(*)` terminal strategy.
342    #[must_use]
343    pub(crate) const fn count_rows() -> Self {
344        Self {
345            runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
346        }
347    }
348
349    /// Prepare one fluent `exists()` terminal strategy.
350    #[must_use]
351    pub(crate) const fn exists_rows() -> Self {
352        Self {
353            runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
354        }
355    }
356
357    /// Build the explain-visible aggregate expression projected by this
358    /// prepared fluent existing-rows strategy.
359    #[cfg(test)]
360    #[must_use]
361    pub(crate) const fn aggregate(&self) -> AggregateExpr {
362        match self.runtime_request {
363            PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
364            PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
365        }
366    }
367
368    /// Borrow the prepared runtime request projected by this fluent
369    /// existing-rows strategy.
370    #[cfg(test)]
371    #[must_use]
372    pub(crate) const fn runtime_request(
373        &self,
374    ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
375        &self.runtime_request
376    }
377
378    /// Move the prepared runtime request out of this fluent existing-rows
379    /// strategy so execution can consume it without cloning.
380    #[must_use]
381    pub(crate) const fn into_runtime_request(
382        self,
383    ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
384        self.runtime_request
385    }
386}
387
388impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
389    fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
390        Some(match self.runtime_request {
391            PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
392            PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
393        })
394    }
395}
396
397/// PreparedFluentScalarTerminalRuntimeRequest
398///
399/// Stable fluent scalar terminal runtime request projection derived once at
400/// the fluent aggregate entrypoint boundary.
401/// This keeps id/extrema execution-side request choice aligned with the
402/// same prepared metadata that explain projects on demand.
403#[derive(Clone, Debug, Eq, PartialEq)]
404pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
405    IdTerminal {
406        kind: AggregateKind,
407    },
408    IdBySlot {
409        kind: AggregateKind,
410        target_field: FieldSlot,
411    },
412}
413
414///
415/// PreparedFluentScalarTerminalStrategy
416///
417/// PreparedFluentScalarTerminalStrategy is the fluent scalar id/extrema
418/// behavior source for the current `0.71` slice.
419/// It resolves runtime terminal request shape once so the id/extrema family
420/// does not rebuild those decisions through parallel branch trees.
421/// Explain-visible aggregate shape is projected from that same prepared
422/// metadata only when explain needs it.
423///
424
425#[derive(Clone, Debug, Eq, PartialEq)]
426pub(crate) struct PreparedFluentScalarTerminalStrategy {
427    runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
428}
429
430impl PreparedFluentScalarTerminalStrategy {
431    /// Prepare one fluent id-returning scalar terminal without a field target.
432    #[must_use]
433    pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
434        Self {
435            runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
436        }
437    }
438
439    /// Prepare one fluent field-targeted extrema terminal with a resolved
440    /// planner slot.
441    #[must_use]
442    pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
443        Self {
444            runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
445                kind,
446                target_field,
447            },
448        }
449    }
450
451    /// Move the prepared runtime request out of this fluent scalar strategy
452    /// so execution can consume it without cloning.
453    #[must_use]
454    pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
455        self.runtime_request
456    }
457}
458
459impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
460    fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
461        Some(match self.runtime_request {
462            PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
463            | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
464        })
465    }
466
467    fn explain_projected_field(&self) -> Option<&str> {
468        match &self.runtime_request {
469            PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
470            PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
471                Some(target_field.field())
472            }
473        }
474    }
475}
476
477///
478/// PreparedFluentNumericFieldRuntimeRequest
479///
480/// Stable fluent numeric-field runtime request projection derived once at the
481/// fluent aggregate entrypoint boundary.
482/// This keeps numeric boundary selection aligned with the same prepared
483/// metadata that runtime and explain projections share.
484///
485
486#[derive(Clone, Copy, Debug, Eq, PartialEq)]
487pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
488    Sum,
489    SumDistinct,
490    Avg,
491    AvgDistinct,
492}
493
494///
495/// PreparedFluentNumericFieldStrategy
496///
497/// PreparedFluentNumericFieldStrategy is the single fluent numeric-field
498/// behavior source for the next `0.71` slice.
499/// It resolves target-slot ownership and runtime boundary request once so
500/// `SUM/AVG` callers do not rebuild those decisions through parallel branch
501/// trees.
502/// Explain-visible aggregate shape is projected on demand from that prepared
503/// state instead of being carried as owned execution metadata.
504///
505
506#[derive(Clone, Debug, Eq, PartialEq)]
507pub(crate) struct PreparedFluentNumericFieldStrategy {
508    target_field: FieldSlot,
509    runtime_request: PreparedFluentNumericFieldRuntimeRequest,
510}
511
512impl PreparedFluentNumericFieldStrategy {
513    /// Prepare one fluent `sum(field)` terminal strategy.
514    #[must_use]
515    pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
516        Self {
517            target_field,
518            runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
519        }
520    }
521
522    /// Prepare one fluent `sum(distinct field)` terminal strategy.
523    #[must_use]
524    pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
525        Self {
526            target_field,
527            runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
528        }
529    }
530
531    /// Prepare one fluent `avg(field)` terminal strategy.
532    #[must_use]
533    pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
534        Self {
535            target_field,
536            runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
537        }
538    }
539
540    /// Prepare one fluent `avg(distinct field)` terminal strategy.
541    #[must_use]
542    pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
543        Self {
544            target_field,
545            runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
546        }
547    }
548
549    /// Build the explain-visible aggregate expression projected by this
550    /// prepared fluent numeric strategy.
551    #[cfg(test)]
552    #[must_use]
553    pub(crate) fn aggregate(&self) -> AggregateExpr {
554        let field = self.target_field.field();
555
556        match self.runtime_request {
557            PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
558            PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
559            PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
560            PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
561        }
562    }
563
564    /// Return the aggregate kind projected by this prepared fluent numeric
565    /// strategy.
566    #[cfg(test)]
567    #[must_use]
568    pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
569        match self.runtime_request {
570            PreparedFluentNumericFieldRuntimeRequest::Sum
571            | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
572            PreparedFluentNumericFieldRuntimeRequest::Avg
573            | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
574        }
575    }
576
577    /// Borrow the projected field label for this prepared fluent numeric
578    /// strategy.
579    #[cfg(test)]
580    #[must_use]
581    pub(crate) fn projected_field(&self) -> &str {
582        self.target_field.field()
583    }
584
585    /// Borrow the resolved planner target slot owned by this prepared fluent
586    /// numeric strategy.
587    #[cfg(test)]
588    #[must_use]
589    pub(crate) const fn target_field(&self) -> &FieldSlot {
590        &self.target_field
591    }
592
593    /// Return the prepared runtime request projected by this fluent numeric
594    /// strategy.
595    #[cfg(test)]
596    #[must_use]
597    pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
598        self.runtime_request
599    }
600
601    /// Move the resolved field slot and numeric runtime request out of this
602    /// strategy so execution can consume them without cloning the field slot.
603    #[must_use]
604    pub(crate) fn into_runtime_parts(
605        self,
606    ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
607        (self.target_field, self.runtime_request)
608    }
609}
610
611impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
612    fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
613        Some(match self.runtime_request {
614            PreparedFluentNumericFieldRuntimeRequest::Sum
615            | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
616            PreparedFluentNumericFieldRuntimeRequest::Avg
617            | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
618        })
619    }
620
621    fn explain_projected_field(&self) -> Option<&str> {
622        Some(self.target_field.field())
623    }
624}
625
626///
627/// PreparedFluentOrderSensitiveTerminalRuntimeRequest
628///
629/// Stable fluent order-sensitive runtime request projection derived once at
630/// the fluent aggregate entrypoint boundary.
631/// This keeps response-order and field-order terminal request shape aligned
632/// with the prepared strategy that fluent execution consumes and explain
633/// projects on demand.
634///
635
636#[derive(Clone, Debug, Eq, PartialEq)]
637pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
638    ResponseOrder { kind: AggregateKind },
639    NthBySlot { target_field: FieldSlot, nth: usize },
640    MedianBySlot { target_field: FieldSlot },
641    MinMaxBySlot { target_field: FieldSlot },
642}
643
644///
645/// PreparedFluentOrderSensitiveTerminalStrategy
646///
647/// PreparedFluentOrderSensitiveTerminalStrategy is the single fluent
648/// order-sensitive behavior source for the next `0.71` slice.
649/// It resolves EXPLAIN-visible aggregate shape where applicable and the
650/// runtime terminal request once so `first/last/nth_by/median_by/min_max_by`
651/// do not rebuild those decisions through parallel branch trees.
652///
653
654#[derive(Clone, Debug, Eq, PartialEq)]
655pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
656    runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
657}
658
659impl PreparedFluentOrderSensitiveTerminalStrategy {
660    /// Prepare one fluent `first()` terminal strategy.
661    #[must_use]
662    pub(crate) const fn first() -> Self {
663        Self {
664            runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
665                kind: AggregateKind::First,
666            },
667        }
668    }
669
670    /// Prepare one fluent `last()` terminal strategy.
671    #[must_use]
672    pub(crate) const fn last() -> Self {
673        Self {
674            runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
675                kind: AggregateKind::Last,
676            },
677        }
678    }
679
680    /// Prepare one fluent `nth_by(field, nth)` terminal strategy.
681    #[must_use]
682    pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
683        Self {
684            runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
685                target_field,
686                nth,
687            },
688        }
689    }
690
691    /// Prepare one fluent `median_by(field)` terminal strategy.
692    #[must_use]
693    pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
694        Self {
695            runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
696                target_field,
697            },
698        }
699    }
700
701    /// Prepare one fluent `min_max_by(field)` terminal strategy.
702    #[must_use]
703    pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
704        Self {
705            runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
706                target_field,
707            },
708        }
709    }
710
711    /// Build the explain-visible aggregate expression projected by this
712    /// prepared order-sensitive strategy when one exists.
713    #[cfg(test)]
714    #[must_use]
715    pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
716        match self.runtime_request {
717            PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
718                Some(AggregateExpr::terminal_for_kind(kind))
719            }
720            PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
721            | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
722            | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
723        }
724    }
725
726    /// Borrow the prepared runtime request projected by this fluent
727    /// order-sensitive strategy.
728    #[cfg(test)]
729    #[must_use]
730    pub(crate) const fn runtime_request(
731        &self,
732    ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
733        &self.runtime_request
734    }
735
736    /// Move the prepared runtime request out of this order-sensitive strategy
737    /// so execution can consume it without cloning.
738    #[must_use]
739    pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
740        self.runtime_request
741    }
742}
743
744impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
745    fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
746        match self.runtime_request {
747            PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
748                Some(kind)
749            }
750            PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
751            | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
752            | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
753        }
754    }
755}
756
757///
758/// PreparedFluentProjectionRuntimeRequest
759///
760/// Stable fluent projection/distinct runtime request projection derived once
761/// at the fluent aggregate entrypoint boundary.
762/// This keeps field-target projection terminal request shape aligned with the
763/// prepared strategy that fluent execution consumes.
764///
765
766#[derive(Clone, Copy, Debug, Eq, PartialEq)]
767pub(crate) enum PreparedFluentProjectionRuntimeRequest {
768    Values,
769    DistinctValues,
770    CountDistinct,
771    ValuesWithIds,
772    TerminalValue { terminal_kind: AggregateKind },
773}
774
775///
776/// PreparedFluentProjectionExplainDescriptor
777///
778/// PreparedFluentProjectionExplainDescriptor is the stable explain projection
779/// surface for fluent projection/distinct terminals.
780/// It carries the already-decided descriptor labels explain needs so query
781/// intent does not rebuild projection terminal shape from runtime requests.
782///
783
784#[derive(Clone, Copy, Debug, Eq, PartialEq)]
785pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
786    terminal: &'static str,
787    field: &'a str,
788    output: &'static str,
789}
790
791impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
792    /// Return the stable explain terminal label.
793    #[must_use]
794    pub(crate) const fn terminal_label(self) -> &'static str {
795        self.terminal
796    }
797
798    /// Return the stable explain field label.
799    #[must_use]
800    pub(crate) const fn field_label(self) -> &'a str {
801        self.field
802    }
803
804    /// Return the stable explain output-shape label.
805    #[must_use]
806    pub(crate) const fn output_label(self) -> &'static str {
807        self.output
808    }
809}
810
811///
812/// PreparedFluentProjectionStrategy
813///
814/// PreparedFluentProjectionStrategy is the single fluent projection/distinct
815/// behavior source for the next `0.71` slice.
816/// It resolves target-slot ownership plus runtime request shape once so
817/// `values_by`/`distinct_values_by`/`count_distinct_by`/`values_by_with_ids`/
818/// `first_value_by`/`last_value_by` do not rebuild those decisions inline.
819///
820
821#[derive(Clone, Debug, Eq, PartialEq)]
822pub(crate) struct PreparedFluentProjectionStrategy {
823    target_field: FieldSlot,
824    runtime_request: PreparedFluentProjectionRuntimeRequest,
825}
826
827impl PreparedFluentProjectionStrategy {
828    /// Prepare one fluent `values_by(field)` terminal strategy.
829    #[must_use]
830    pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
831        Self {
832            target_field,
833            runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
834        }
835    }
836
837    /// Prepare one fluent `distinct_values_by(field)` terminal strategy.
838    #[must_use]
839    pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
840        Self {
841            target_field,
842            runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
843        }
844    }
845
846    /// Prepare one fluent `count_distinct_by(field)` terminal strategy.
847    #[must_use]
848    pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
849        Self {
850            target_field,
851            runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
852        }
853    }
854
855    /// Prepare one fluent `values_by_with_ids(field)` terminal strategy.
856    #[must_use]
857    pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
858        Self {
859            target_field,
860            runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
861        }
862    }
863
864    /// Prepare one fluent `first_value_by(field)` terminal strategy.
865    #[must_use]
866    pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
867        Self {
868            target_field,
869            runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
870                terminal_kind: AggregateKind::First,
871            },
872        }
873    }
874
875    /// Prepare one fluent `last_value_by(field)` terminal strategy.
876    #[must_use]
877    pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
878        Self {
879            target_field,
880            runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
881                terminal_kind: AggregateKind::Last,
882            },
883        }
884    }
885
886    /// Borrow the resolved planner target slot owned by this prepared fluent
887    /// projection strategy.
888    #[cfg(test)]
889    #[must_use]
890    pub(crate) const fn target_field(&self) -> &FieldSlot {
891        &self.target_field
892    }
893
894    /// Return the prepared runtime request projected by this fluent
895    /// projection strategy.
896    #[cfg(test)]
897    #[must_use]
898    pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
899        self.runtime_request
900    }
901
902    /// Move the resolved field slot and projection runtime request out of
903    /// this strategy so execution can consume them without cloning the field
904    /// slot.
905    #[must_use]
906    pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
907        (self.target_field, self.runtime_request)
908    }
909
910    /// Return the stable projection explain descriptor for this prepared
911    /// strategy.
912    #[must_use]
913    pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
914        let terminal_label = match self.runtime_request {
915            PreparedFluentProjectionRuntimeRequest::Values => "values_by",
916            PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
917            PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
918            PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
919            PreparedFluentProjectionRuntimeRequest::TerminalValue {
920                terminal_kind: AggregateKind::First,
921            } => "first_value_by",
922            PreparedFluentProjectionRuntimeRequest::TerminalValue {
923                terminal_kind: AggregateKind::Last,
924            } => "last_value_by",
925            PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
926                unreachable!("projection terminal value explain requires FIRST/LAST kind")
927            }
928        };
929        let output_label = match self.runtime_request {
930            PreparedFluentProjectionRuntimeRequest::Values
931            | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
932            PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
933            PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
934            PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
935        };
936
937        PreparedFluentProjectionExplainDescriptor {
938            terminal: terminal_label,
939            field: self.target_field.field(),
940            output: output_label,
941        }
942    }
943}
944
945/// Build `count(*)`.
946#[must_use]
947pub const fn count() -> AggregateExpr {
948    AggregateExpr::terminal(AggregateKind::Count)
949}
950
951/// Build `count(field)`.
952#[must_use]
953pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
954    AggregateExpr::field_target(AggregateKind::Count, field.as_ref().to_string())
955}
956
957/// Build `sum(field)`.
958#[must_use]
959pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
960    AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
961}
962
963/// Build `avg(field)`.
964#[must_use]
965pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
966    AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
967}
968
969/// Build `exists`.
970#[must_use]
971pub const fn exists() -> AggregateExpr {
972    AggregateExpr::terminal(AggregateKind::Exists)
973}
974
975/// Build `first`.
976#[must_use]
977pub const fn first() -> AggregateExpr {
978    AggregateExpr::terminal(AggregateKind::First)
979}
980
981/// Build `last`.
982#[must_use]
983pub const fn last() -> AggregateExpr {
984    AggregateExpr::terminal(AggregateKind::Last)
985}
986
987/// Build `min`.
988#[must_use]
989pub const fn min() -> AggregateExpr {
990    AggregateExpr::terminal(AggregateKind::Min)
991}
992
993/// Build `min(field)`.
994#[must_use]
995pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
996    AggregateExpr::field_target(AggregateKind::Min, field.as_ref().to_string())
997}
998
999/// Build `max`.
1000#[must_use]
1001pub const fn max() -> AggregateExpr {
1002    AggregateExpr::terminal(AggregateKind::Max)
1003}
1004
1005/// Build `max(field)`.
1006#[must_use]
1007pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
1008    AggregateExpr::field_target(AggregateKind::Max, field.as_ref().to_string())
1009}
1010
1011///
1012/// TESTS
1013///
1014
1015#[cfg(test)]
1016mod tests {
1017    use crate::db::query::{
1018        builder::{
1019            PreparedFluentExistingRowsTerminalRuntimeRequest,
1020            PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
1021            PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
1022            PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
1023            PreparedFluentProjectionStrategy,
1024        },
1025        plan::{AggregateKind, FieldSlot},
1026    };
1027
1028    #[test]
1029    fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
1030        let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1031        let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
1032
1033        assert_eq!(
1034            strategy.aggregate_kind(),
1035            AggregateKind::Sum,
1036            "sum(distinct field) should preserve SUM aggregate kind",
1037        );
1038        assert_eq!(
1039            strategy.projected_field(),
1040            "rank",
1041            "sum(distinct field) should preserve projected field labels",
1042        );
1043        assert!(
1044            strategy.aggregate().is_distinct(),
1045            "sum(distinct field) should preserve DISTINCT aggregate shape",
1046        );
1047        assert_eq!(
1048            strategy.target_field(),
1049            &rank_slot,
1050            "sum(distinct field) should preserve the resolved planner field slot",
1051        );
1052        assert_eq!(
1053            strategy.runtime_request(),
1054            PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
1055            "sum(distinct field) should project the numeric DISTINCT runtime request",
1056        );
1057    }
1058
1059    #[test]
1060    fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
1061        let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
1062
1063        assert_eq!(
1064            strategy.aggregate().kind(),
1065            AggregateKind::Count,
1066            "count() should preserve the explain-visible aggregate kind",
1067        );
1068        assert_eq!(
1069            strategy.runtime_request(),
1070            &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
1071            "count() should project the existing-rows count runtime request",
1072        );
1073    }
1074
1075    #[test]
1076    fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
1077        let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
1078
1079        assert_eq!(
1080            strategy.aggregate().kind(),
1081            AggregateKind::Exists,
1082            "exists() should preserve the explain-visible aggregate kind",
1083        );
1084        assert_eq!(
1085            strategy.runtime_request(),
1086            &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
1087            "exists() should project the existing-rows exists runtime request",
1088        );
1089    }
1090
1091    #[test]
1092    fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
1093        let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1094        let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
1095
1096        assert_eq!(
1097            strategy.aggregate_kind(),
1098            AggregateKind::Avg,
1099            "avg(field) should preserve AVG aggregate kind",
1100        );
1101        assert_eq!(
1102            strategy.projected_field(),
1103            "rank",
1104            "avg(field) should preserve projected field labels",
1105        );
1106        assert!(
1107            !strategy.aggregate().is_distinct(),
1108            "avg(field) should stay non-distinct unless requested explicitly",
1109        );
1110        assert_eq!(
1111            strategy.target_field(),
1112            &rank_slot,
1113            "avg(field) should preserve the resolved planner field slot",
1114        );
1115        assert_eq!(
1116            strategy.runtime_request(),
1117            PreparedFluentNumericFieldRuntimeRequest::Avg,
1118            "avg(field) should project the numeric AVG runtime request",
1119        );
1120    }
1121
1122    #[test]
1123    fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
1124        let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
1125
1126        assert_eq!(
1127            strategy
1128                .explain_aggregate()
1129                .map(|aggregate| aggregate.kind()),
1130            Some(AggregateKind::First),
1131            "first() should preserve the explain-visible aggregate kind",
1132        );
1133        assert_eq!(
1134            strategy.runtime_request(),
1135            &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
1136                kind: AggregateKind::First,
1137            },
1138            "first() should project the response-order runtime request",
1139        );
1140    }
1141
1142    #[test]
1143    fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
1144        let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1145        let strategy =
1146            PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
1147
1148        assert_eq!(
1149            strategy.explain_aggregate(),
1150            None,
1151            "nth_by(field, nth) should stay off the current explain aggregate surface",
1152        );
1153        assert_eq!(
1154            strategy.runtime_request(),
1155            &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
1156                target_field: rank_slot,
1157                nth: 2,
1158            },
1159            "nth_by(field, nth) should preserve the resolved field-order runtime request",
1160        );
1161    }
1162
1163    #[test]
1164    fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
1165        let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1166        let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
1167        let explain = strategy.explain_descriptor();
1168
1169        assert_eq!(
1170            strategy.target_field(),
1171            &rank_slot,
1172            "count_distinct_by(field) should preserve the resolved planner field slot",
1173        );
1174        assert_eq!(
1175            strategy.runtime_request(),
1176            PreparedFluentProjectionRuntimeRequest::CountDistinct,
1177            "count_distinct_by(field) should project the distinct-count runtime request",
1178        );
1179        assert_eq!(
1180            explain.terminal_label(),
1181            "count_distinct_by",
1182            "count_distinct_by(field) should project the stable explain terminal label",
1183        );
1184        assert_eq!(
1185            explain.field_label(),
1186            "rank",
1187            "count_distinct_by(field) should project the stable explain field label",
1188        );
1189        assert_eq!(
1190            explain.output_label(),
1191            "count",
1192            "count_distinct_by(field) should project the stable explain output label",
1193        );
1194    }
1195
1196    #[test]
1197    fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1198        let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1199        let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1200        let explain = strategy.explain_descriptor();
1201
1202        assert_eq!(
1203            strategy.target_field(),
1204            &rank_slot,
1205            "last_value_by(field) should preserve the resolved planner field slot",
1206        );
1207        assert_eq!(
1208            strategy.runtime_request(),
1209            PreparedFluentProjectionRuntimeRequest::TerminalValue {
1210                terminal_kind: AggregateKind::Last,
1211            },
1212            "last_value_by(field) should project the terminal-value runtime request",
1213        );
1214        assert_eq!(
1215            explain.terminal_label(),
1216            "last_value_by",
1217            "last_value_by(field) should project the stable explain terminal label",
1218        );
1219        assert_eq!(
1220            explain.field_label(),
1221            "rank",
1222            "last_value_by(field) should project the stable explain field label",
1223        );
1224        assert_eq!(
1225            explain.output_label(),
1226            "terminal_value",
1227            "last_value_by(field) should project the stable explain output label",
1228        );
1229    }
1230}