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