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