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