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