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