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