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