Skip to main content

icydb_core/db/query/builder/
aggregate.rs

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