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