1use crate::db::query::plan::{AggregateKind, FieldSlot};
7
8#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct AggregateExpr {
18 kind: AggregateKind,
19 target_field: Option<String>,
20 distinct: bool,
21}
22
23impl AggregateExpr {
24 const fn new(kind: AggregateKind, target_field: Option<String>) -> Self {
26 Self {
27 kind,
28 target_field,
29 distinct: false,
30 }
31 }
32
33 #[must_use]
35 pub const fn distinct(mut self) -> Self {
36 self.distinct = true;
37 self
38 }
39
40 #[must_use]
42 pub(crate) const fn kind(&self) -> AggregateKind {
43 self.kind
44 }
45
46 #[must_use]
48 pub(crate) fn target_field(&self) -> Option<&str> {
49 self.target_field.as_deref()
50 }
51
52 #[must_use]
54 pub(crate) const fn is_distinct(&self) -> bool {
55 self.distinct
56 }
57
58 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 #[cfg(test)]
73 #[must_use]
74 pub(in crate::db) fn terminal_for_kind(kind: AggregateKind) -> Self {
75 match kind {
76 AggregateKind::Count => count(),
77 AggregateKind::Exists => exists(),
78 AggregateKind::Min => min(),
79 AggregateKind::Max => max(),
80 AggregateKind::First => first(),
81 AggregateKind::Last => last(),
82 AggregateKind::Sum | AggregateKind::Avg => unreachable!(
83 "AggregateExpr::terminal_for_kind does not support SUM/AVG field-target kinds"
84 ),
85 }
86 }
87}
88
89pub(crate) trait PreparedFluentAggregateExplainStrategy {
100 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
103
104 fn explain_projected_field(&self) -> Option<&str> {
106 None
107 }
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
117pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
118 CountRows,
119 ExistsRows,
120}
121
122#[derive(Clone, Debug, Eq, PartialEq)]
135pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
136 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
137}
138
139impl PreparedFluentExistingRowsTerminalStrategy {
140 #[must_use]
142 pub(crate) const fn count_rows() -> Self {
143 Self {
144 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
145 }
146 }
147
148 #[must_use]
150 pub(crate) const fn exists_rows() -> Self {
151 Self {
152 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
153 }
154 }
155
156 #[cfg(test)]
159 #[must_use]
160 pub(crate) const fn aggregate(&self) -> AggregateExpr {
161 match self.runtime_request {
162 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
163 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
164 }
165 }
166
167 #[cfg(test)]
170 #[must_use]
171 pub(crate) const fn runtime_request(
172 &self,
173 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
174 &self.runtime_request
175 }
176
177 #[must_use]
180 pub(crate) const fn into_runtime_request(
181 self,
182 ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
183 self.runtime_request
184 }
185}
186
187impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
188 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
189 Some(match self.runtime_request {
190 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
191 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
192 })
193 }
194}
195
196#[derive(Clone, Debug, Eq, PartialEq)]
203pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
204 IdTerminal {
205 kind: AggregateKind,
206 },
207 IdBySlot {
208 kind: AggregateKind,
209 target_field: FieldSlot,
210 },
211}
212
213#[derive(Clone, Debug, Eq, PartialEq)]
225pub(crate) struct PreparedFluentScalarTerminalStrategy {
226 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
227}
228
229impl PreparedFluentScalarTerminalStrategy {
230 #[must_use]
232 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
233 Self {
234 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
235 }
236 }
237
238 #[must_use]
241 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
242 Self {
243 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
244 kind,
245 target_field,
246 },
247 }
248 }
249
250 #[must_use]
253 pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
254 self.runtime_request
255 }
256}
257
258impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
259 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
260 Some(match self.runtime_request {
261 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
262 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
263 })
264 }
265
266 fn explain_projected_field(&self) -> Option<&str> {
267 match &self.runtime_request {
268 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
269 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
270 Some(target_field.field())
271 }
272 }
273 }
274}
275
276#[derive(Clone, Copy, Debug, Eq, PartialEq)]
286pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
287 Sum,
288 SumDistinct,
289 Avg,
290 AvgDistinct,
291}
292
293#[derive(Clone, Debug, Eq, PartialEq)]
306pub(crate) struct PreparedFluentNumericFieldStrategy {
307 target_field: FieldSlot,
308 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
309}
310
311impl PreparedFluentNumericFieldStrategy {
312 #[must_use]
314 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
315 Self {
316 target_field,
317 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
318 }
319 }
320
321 #[must_use]
323 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
324 Self {
325 target_field,
326 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
327 }
328 }
329
330 #[must_use]
332 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
333 Self {
334 target_field,
335 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
336 }
337 }
338
339 #[must_use]
341 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
342 Self {
343 target_field,
344 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
345 }
346 }
347
348 #[cfg(test)]
351 #[must_use]
352 pub(crate) fn aggregate(&self) -> AggregateExpr {
353 let field = self.target_field.field();
354
355 match self.runtime_request {
356 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
357 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
358 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
359 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
360 }
361 }
362
363 #[cfg(test)]
366 #[must_use]
367 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
368 match self.runtime_request {
369 PreparedFluentNumericFieldRuntimeRequest::Sum
370 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
371 PreparedFluentNumericFieldRuntimeRequest::Avg
372 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
373 }
374 }
375
376 #[cfg(test)]
379 #[must_use]
380 pub(crate) fn projected_field(&self) -> &str {
381 self.target_field.field()
382 }
383
384 #[cfg(test)]
387 #[must_use]
388 pub(crate) const fn target_field(&self) -> &FieldSlot {
389 &self.target_field
390 }
391
392 #[cfg(test)]
395 #[must_use]
396 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
397 self.runtime_request
398 }
399
400 #[must_use]
403 pub(crate) fn into_runtime_parts(
404 self,
405 ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
406 (self.target_field, self.runtime_request)
407 }
408}
409
410impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
411 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
412 Some(match self.runtime_request {
413 PreparedFluentNumericFieldRuntimeRequest::Sum
414 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
415 PreparedFluentNumericFieldRuntimeRequest::Avg
416 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
417 })
418 }
419
420 fn explain_projected_field(&self) -> Option<&str> {
421 Some(self.target_field.field())
422 }
423}
424
425#[derive(Clone, Debug, Eq, PartialEq)]
436pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
437 ResponseOrder { kind: AggregateKind },
438 NthBySlot { target_field: FieldSlot, nth: usize },
439 MedianBySlot { target_field: FieldSlot },
440 MinMaxBySlot { target_field: FieldSlot },
441}
442
443#[derive(Clone, Debug, Eq, PartialEq)]
454pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
455 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
456}
457
458impl PreparedFluentOrderSensitiveTerminalStrategy {
459 #[must_use]
461 pub(crate) const fn first() -> Self {
462 Self {
463 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
464 kind: AggregateKind::First,
465 },
466 }
467 }
468
469 #[must_use]
471 pub(crate) const fn last() -> Self {
472 Self {
473 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
474 kind: AggregateKind::Last,
475 },
476 }
477 }
478
479 #[must_use]
481 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
482 Self {
483 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
484 target_field,
485 nth,
486 },
487 }
488 }
489
490 #[must_use]
492 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
493 Self {
494 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
495 target_field,
496 },
497 }
498 }
499
500 #[must_use]
502 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
503 Self {
504 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
505 target_field,
506 },
507 }
508 }
509
510 #[cfg(test)]
513 #[must_use]
514 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
515 match self.runtime_request {
516 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
517 Some(AggregateExpr::terminal_for_kind(kind))
518 }
519 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
520 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
521 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
522 }
523 }
524
525 #[cfg(test)]
528 #[must_use]
529 pub(crate) const fn runtime_request(
530 &self,
531 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
532 &self.runtime_request
533 }
534
535 #[must_use]
538 pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
539 self.runtime_request
540 }
541}
542
543impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
544 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
545 match self.runtime_request {
546 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
547 Some(kind)
548 }
549 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
550 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
551 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
552 }
553 }
554}
555
556#[derive(Clone, Copy, Debug, Eq, PartialEq)]
566pub(crate) enum PreparedFluentProjectionRuntimeRequest {
567 Values,
568 DistinctValues,
569 CountDistinct,
570 ValuesWithIds,
571 TerminalValue { terminal_kind: AggregateKind },
572}
573
574#[derive(Clone, Copy, Debug, Eq, PartialEq)]
584pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
585 terminal: &'static str,
586 field: &'a str,
587 output: &'static str,
588}
589
590impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
591 #[must_use]
593 pub(crate) const fn terminal_label(self) -> &'static str {
594 self.terminal
595 }
596
597 #[must_use]
599 pub(crate) const fn field_label(self) -> &'a str {
600 self.field
601 }
602
603 #[must_use]
605 pub(crate) const fn output_label(self) -> &'static str {
606 self.output
607 }
608}
609
610#[derive(Clone, Debug, Eq, PartialEq)]
621pub(crate) struct PreparedFluentProjectionStrategy {
622 target_field: FieldSlot,
623 runtime_request: PreparedFluentProjectionRuntimeRequest,
624}
625
626impl PreparedFluentProjectionStrategy {
627 #[must_use]
629 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
630 Self {
631 target_field,
632 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
633 }
634 }
635
636 #[must_use]
638 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
639 Self {
640 target_field,
641 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
642 }
643 }
644
645 #[must_use]
647 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
648 Self {
649 target_field,
650 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
651 }
652 }
653
654 #[must_use]
656 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
657 Self {
658 target_field,
659 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
660 }
661 }
662
663 #[must_use]
665 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
666 Self {
667 target_field,
668 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
669 terminal_kind: AggregateKind::First,
670 },
671 }
672 }
673
674 #[must_use]
676 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
677 Self {
678 target_field,
679 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
680 terminal_kind: AggregateKind::Last,
681 },
682 }
683 }
684
685 #[cfg(test)]
688 #[must_use]
689 pub(crate) const fn target_field(&self) -> &FieldSlot {
690 &self.target_field
691 }
692
693 #[cfg(test)]
696 #[must_use]
697 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
698 self.runtime_request
699 }
700
701 #[must_use]
705 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
706 (self.target_field, self.runtime_request)
707 }
708
709 #[must_use]
712 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
713 let terminal_label = match self.runtime_request {
714 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
715 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
716 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
717 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
718 PreparedFluentProjectionRuntimeRequest::TerminalValue {
719 terminal_kind: AggregateKind::First,
720 } => "first_value_by",
721 PreparedFluentProjectionRuntimeRequest::TerminalValue {
722 terminal_kind: AggregateKind::Last,
723 } => "last_value_by",
724 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
725 unreachable!("projection terminal value explain requires FIRST/LAST kind")
726 }
727 };
728 let output_label = match self.runtime_request {
729 PreparedFluentProjectionRuntimeRequest::Values
730 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
731 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
732 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
733 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
734 };
735
736 PreparedFluentProjectionExplainDescriptor {
737 terminal: terminal_label,
738 field: self.target_field.field(),
739 output: output_label,
740 }
741 }
742}
743
744#[must_use]
746pub const fn count() -> AggregateExpr {
747 AggregateExpr::new(AggregateKind::Count, None)
748}
749
750#[must_use]
752pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
753 AggregateExpr::new(AggregateKind::Count, Some(field.as_ref().to_string()))
754}
755
756#[must_use]
758pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
759 AggregateExpr::new(AggregateKind::Sum, Some(field.as_ref().to_string()))
760}
761
762#[must_use]
764pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
765 AggregateExpr::new(AggregateKind::Avg, Some(field.as_ref().to_string()))
766}
767
768#[must_use]
770pub const fn exists() -> AggregateExpr {
771 AggregateExpr::new(AggregateKind::Exists, None)
772}
773
774#[must_use]
776pub const fn first() -> AggregateExpr {
777 AggregateExpr::new(AggregateKind::First, None)
778}
779
780#[must_use]
782pub const fn last() -> AggregateExpr {
783 AggregateExpr::new(AggregateKind::Last, None)
784}
785
786#[must_use]
788pub const fn min() -> AggregateExpr {
789 AggregateExpr::new(AggregateKind::Min, None)
790}
791
792#[must_use]
794pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
795 AggregateExpr::new(AggregateKind::Min, Some(field.as_ref().to_string()))
796}
797
798#[must_use]
800pub const fn max() -> AggregateExpr {
801 AggregateExpr::new(AggregateKind::Max, None)
802}
803
804#[must_use]
806pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
807 AggregateExpr::new(AggregateKind::Max, Some(field.as_ref().to_string()))
808}
809
810#[cfg(test)]
815mod tests {
816 use crate::db::query::{
817 builder::{
818 PreparedFluentExistingRowsTerminalRuntimeRequest,
819 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
820 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
821 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
822 PreparedFluentProjectionStrategy,
823 },
824 plan::{AggregateKind, FieldSlot},
825 };
826
827 #[test]
828 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
829 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
830 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
831
832 assert_eq!(
833 strategy.aggregate_kind(),
834 AggregateKind::Sum,
835 "sum(distinct field) should preserve SUM aggregate kind",
836 );
837 assert_eq!(
838 strategy.projected_field(),
839 "rank",
840 "sum(distinct field) should preserve projected field labels",
841 );
842 assert!(
843 strategy.aggregate().is_distinct(),
844 "sum(distinct field) should preserve DISTINCT aggregate shape",
845 );
846 assert_eq!(
847 strategy.target_field(),
848 &rank_slot,
849 "sum(distinct field) should preserve the resolved planner field slot",
850 );
851 assert_eq!(
852 strategy.runtime_request(),
853 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
854 "sum(distinct field) should project the numeric DISTINCT runtime request",
855 );
856 }
857
858 #[test]
859 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
860 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
861
862 assert_eq!(
863 strategy.aggregate().kind(),
864 AggregateKind::Count,
865 "count() should preserve the explain-visible aggregate kind",
866 );
867 assert_eq!(
868 strategy.runtime_request(),
869 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
870 "count() should project the existing-rows count runtime request",
871 );
872 }
873
874 #[test]
875 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
876 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
877
878 assert_eq!(
879 strategy.aggregate().kind(),
880 AggregateKind::Exists,
881 "exists() should preserve the explain-visible aggregate kind",
882 );
883 assert_eq!(
884 strategy.runtime_request(),
885 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
886 "exists() should project the existing-rows exists runtime request",
887 );
888 }
889
890 #[test]
891 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
892 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
893 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
894
895 assert_eq!(
896 strategy.aggregate_kind(),
897 AggregateKind::Avg,
898 "avg(field) should preserve AVG aggregate kind",
899 );
900 assert_eq!(
901 strategy.projected_field(),
902 "rank",
903 "avg(field) should preserve projected field labels",
904 );
905 assert!(
906 !strategy.aggregate().is_distinct(),
907 "avg(field) should stay non-distinct unless requested explicitly",
908 );
909 assert_eq!(
910 strategy.target_field(),
911 &rank_slot,
912 "avg(field) should preserve the resolved planner field slot",
913 );
914 assert_eq!(
915 strategy.runtime_request(),
916 PreparedFluentNumericFieldRuntimeRequest::Avg,
917 "avg(field) should project the numeric AVG runtime request",
918 );
919 }
920
921 #[test]
922 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
923 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
924
925 assert_eq!(
926 strategy
927 .explain_aggregate()
928 .map(|aggregate| aggregate.kind()),
929 Some(AggregateKind::First),
930 "first() should preserve the explain-visible aggregate kind",
931 );
932 assert_eq!(
933 strategy.runtime_request(),
934 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
935 kind: AggregateKind::First,
936 },
937 "first() should project the response-order runtime request",
938 );
939 }
940
941 #[test]
942 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
943 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
944 let strategy =
945 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
946
947 assert_eq!(
948 strategy.explain_aggregate(),
949 None,
950 "nth_by(field, nth) should stay off the current explain aggregate surface",
951 );
952 assert_eq!(
953 strategy.runtime_request(),
954 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
955 target_field: rank_slot,
956 nth: 2,
957 },
958 "nth_by(field, nth) should preserve the resolved field-order runtime request",
959 );
960 }
961
962 #[test]
963 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
964 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
965 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
966 let explain = strategy.explain_descriptor();
967
968 assert_eq!(
969 strategy.target_field(),
970 &rank_slot,
971 "count_distinct_by(field) should preserve the resolved planner field slot",
972 );
973 assert_eq!(
974 strategy.runtime_request(),
975 PreparedFluentProjectionRuntimeRequest::CountDistinct,
976 "count_distinct_by(field) should project the distinct-count runtime request",
977 );
978 assert_eq!(
979 explain.terminal_label(),
980 "count_distinct_by",
981 "count_distinct_by(field) should project the stable explain terminal label",
982 );
983 assert_eq!(
984 explain.field_label(),
985 "rank",
986 "count_distinct_by(field) should project the stable explain field label",
987 );
988 assert_eq!(
989 explain.output_label(),
990 "count",
991 "count_distinct_by(field) should project the stable explain output label",
992 );
993 }
994
995 #[test]
996 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
997 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
998 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
999 let explain = strategy.explain_descriptor();
1000
1001 assert_eq!(
1002 strategy.target_field(),
1003 &rank_slot,
1004 "last_value_by(field) should preserve the resolved planner field slot",
1005 );
1006 assert_eq!(
1007 strategy.runtime_request(),
1008 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1009 terminal_kind: AggregateKind::Last,
1010 },
1011 "last_value_by(field) should project the terminal-value runtime request",
1012 );
1013 assert_eq!(
1014 explain.terminal_label(),
1015 "last_value_by",
1016 "last_value_by(field) should project the stable explain terminal label",
1017 );
1018 assert_eq!(
1019 explain.field_label(),
1020 "rank",
1021 "last_value_by(field) should project the stable explain field label",
1022 );
1023 assert_eq!(
1024 explain.output_label(),
1025 "terminal_value",
1026 "last_value_by(field) should project the stable explain output label",
1027 );
1028 }
1029}