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