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