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