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 #[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 #[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
101pub(crate) trait PreparedFluentAggregateExplainStrategy {
112 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
115
116 fn explain_projected_field(&self) -> Option<&str> {
118 None
119 }
120}
121
122#[derive(Clone, Debug, Eq, PartialEq)]
129pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
130 CountRows,
131 ExistsRows,
132}
133
134#[derive(Clone, Debug, Eq, PartialEq)]
147pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
148 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
149}
150
151impl PreparedFluentExistingRowsTerminalStrategy {
152 #[must_use]
154 pub(crate) const fn count_rows() -> Self {
155 Self {
156 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
157 }
158 }
159
160 #[must_use]
162 pub(crate) const fn exists_rows() -> Self {
163 Self {
164 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
165 }
166 }
167
168 #[cfg(test)]
171 #[must_use]
172 pub(crate) const fn aggregate(&self) -> AggregateExpr {
173 match self.runtime_request {
174 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
175 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
176 }
177 }
178
179 #[cfg(test)]
182 #[must_use]
183 pub(crate) const fn runtime_request(
184 &self,
185 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
186 &self.runtime_request
187 }
188
189 #[must_use]
192 pub(crate) const fn into_runtime_request(
193 self,
194 ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
195 self.runtime_request
196 }
197}
198
199impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
200 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
201 Some(match self.runtime_request {
202 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
203 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
204 })
205 }
206}
207
208#[derive(Clone, Debug, Eq, PartialEq)]
215pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
216 IdTerminal {
217 kind: AggregateKind,
218 },
219 IdBySlot {
220 kind: AggregateKind,
221 target_field: FieldSlot,
222 },
223}
224
225#[derive(Clone, Debug, Eq, PartialEq)]
237pub(crate) struct PreparedFluentScalarTerminalStrategy {
238 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
239}
240
241impl PreparedFluentScalarTerminalStrategy {
242 #[must_use]
244 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
245 Self {
246 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
247 }
248 }
249
250 #[must_use]
253 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
254 Self {
255 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
256 kind,
257 target_field,
258 },
259 }
260 }
261
262 #[must_use]
265 pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
266 self.runtime_request
267 }
268}
269
270impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
271 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
272 Some(match self.runtime_request {
273 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
274 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
275 })
276 }
277
278 fn explain_projected_field(&self) -> Option<&str> {
279 match &self.runtime_request {
280 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
281 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
282 Some(target_field.field())
283 }
284 }
285 }
286}
287
288#[derive(Clone, Copy, Debug, Eq, PartialEq)]
298pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
299 Sum,
300 SumDistinct,
301 Avg,
302 AvgDistinct,
303}
304
305#[derive(Clone, Debug, Eq, PartialEq)]
318pub(crate) struct PreparedFluentNumericFieldStrategy {
319 target_field: FieldSlot,
320 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
321}
322
323impl PreparedFluentNumericFieldStrategy {
324 #[must_use]
326 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
327 Self {
328 target_field,
329 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
330 }
331 }
332
333 #[must_use]
335 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
336 Self {
337 target_field,
338 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
339 }
340 }
341
342 #[must_use]
344 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
345 Self {
346 target_field,
347 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
348 }
349 }
350
351 #[must_use]
353 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
354 Self {
355 target_field,
356 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
357 }
358 }
359
360 #[cfg(test)]
363 #[must_use]
364 pub(crate) fn aggregate(&self) -> AggregateExpr {
365 let field = self.target_field.field();
366
367 match self.runtime_request {
368 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
369 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
370 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
371 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
372 }
373 }
374
375 #[cfg(test)]
378 #[must_use]
379 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
380 match self.runtime_request {
381 PreparedFluentNumericFieldRuntimeRequest::Sum
382 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
383 PreparedFluentNumericFieldRuntimeRequest::Avg
384 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
385 }
386 }
387
388 #[cfg(test)]
391 #[must_use]
392 pub(crate) fn projected_field(&self) -> &str {
393 self.target_field.field()
394 }
395
396 #[cfg(test)]
399 #[must_use]
400 pub(crate) const fn target_field(&self) -> &FieldSlot {
401 &self.target_field
402 }
403
404 #[cfg(test)]
407 #[must_use]
408 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
409 self.runtime_request
410 }
411
412 #[must_use]
415 pub(crate) fn into_runtime_parts(
416 self,
417 ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
418 (self.target_field, self.runtime_request)
419 }
420}
421
422impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
423 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
424 Some(match self.runtime_request {
425 PreparedFluentNumericFieldRuntimeRequest::Sum
426 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
427 PreparedFluentNumericFieldRuntimeRequest::Avg
428 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
429 })
430 }
431
432 fn explain_projected_field(&self) -> Option<&str> {
433 Some(self.target_field.field())
434 }
435}
436
437#[derive(Clone, Debug, Eq, PartialEq)]
448pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
449 ResponseOrder { kind: AggregateKind },
450 NthBySlot { target_field: FieldSlot, nth: usize },
451 MedianBySlot { target_field: FieldSlot },
452 MinMaxBySlot { target_field: FieldSlot },
453}
454
455#[derive(Clone, Debug, Eq, PartialEq)]
466pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
467 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
468}
469
470impl PreparedFluentOrderSensitiveTerminalStrategy {
471 #[must_use]
473 pub(crate) const fn first() -> Self {
474 Self {
475 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
476 kind: AggregateKind::First,
477 },
478 }
479 }
480
481 #[must_use]
483 pub(crate) const fn last() -> Self {
484 Self {
485 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
486 kind: AggregateKind::Last,
487 },
488 }
489 }
490
491 #[must_use]
493 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
494 Self {
495 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
496 target_field,
497 nth,
498 },
499 }
500 }
501
502 #[must_use]
504 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
505 Self {
506 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
507 target_field,
508 },
509 }
510 }
511
512 #[must_use]
514 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
515 Self {
516 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
517 target_field,
518 },
519 }
520 }
521
522 #[cfg(test)]
525 #[must_use]
526 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
527 match self.runtime_request {
528 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
529 Some(AggregateExpr::terminal_for_kind(kind))
530 }
531 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
532 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
533 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
534 }
535 }
536
537 #[cfg(test)]
540 #[must_use]
541 pub(crate) const fn runtime_request(
542 &self,
543 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
544 &self.runtime_request
545 }
546
547 #[must_use]
550 pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
551 self.runtime_request
552 }
553}
554
555impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
556 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
557 match self.runtime_request {
558 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
559 Some(kind)
560 }
561 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
562 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
563 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
564 }
565 }
566}
567
568#[derive(Clone, Copy, Debug, Eq, PartialEq)]
578pub(crate) enum PreparedFluentProjectionRuntimeRequest {
579 Values,
580 DistinctValues,
581 CountDistinct,
582 ValuesWithIds,
583 TerminalValue { terminal_kind: AggregateKind },
584}
585
586#[derive(Clone, Copy, Debug, Eq, PartialEq)]
596pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
597 terminal: &'static str,
598 field: &'a str,
599 output: &'static str,
600}
601
602impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
603 #[must_use]
605 pub(crate) const fn terminal_label(self) -> &'static str {
606 self.terminal
607 }
608
609 #[must_use]
611 pub(crate) const fn field_label(self) -> &'a str {
612 self.field
613 }
614
615 #[must_use]
617 pub(crate) const fn output_label(self) -> &'static str {
618 self.output
619 }
620}
621
622#[derive(Clone, Debug, Eq, PartialEq)]
633pub(crate) struct PreparedFluentProjectionStrategy {
634 target_field: FieldSlot,
635 runtime_request: PreparedFluentProjectionRuntimeRequest,
636}
637
638impl PreparedFluentProjectionStrategy {
639 #[must_use]
641 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
642 Self {
643 target_field,
644 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
645 }
646 }
647
648 #[must_use]
650 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
651 Self {
652 target_field,
653 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
654 }
655 }
656
657 #[must_use]
659 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
660 Self {
661 target_field,
662 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
663 }
664 }
665
666 #[must_use]
668 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
669 Self {
670 target_field,
671 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
672 }
673 }
674
675 #[must_use]
677 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
678 Self {
679 target_field,
680 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
681 terminal_kind: AggregateKind::First,
682 },
683 }
684 }
685
686 #[must_use]
688 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
689 Self {
690 target_field,
691 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
692 terminal_kind: AggregateKind::Last,
693 },
694 }
695 }
696
697 #[cfg(test)]
700 #[must_use]
701 pub(crate) const fn target_field(&self) -> &FieldSlot {
702 &self.target_field
703 }
704
705 #[cfg(test)]
708 #[must_use]
709 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
710 self.runtime_request
711 }
712
713 #[must_use]
717 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
718 (self.target_field, self.runtime_request)
719 }
720
721 #[must_use]
724 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
725 let terminal_label = match self.runtime_request {
726 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
727 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
728 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
729 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
730 PreparedFluentProjectionRuntimeRequest::TerminalValue {
731 terminal_kind: AggregateKind::First,
732 } => "first_value_by",
733 PreparedFluentProjectionRuntimeRequest::TerminalValue {
734 terminal_kind: AggregateKind::Last,
735 } => "last_value_by",
736 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
737 unreachable!("projection terminal value explain requires FIRST/LAST kind")
738 }
739 };
740 let output_label = match self.runtime_request {
741 PreparedFluentProjectionRuntimeRequest::Values
742 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
743 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
744 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
745 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
746 };
747
748 PreparedFluentProjectionExplainDescriptor {
749 terminal: terminal_label,
750 field: self.target_field.field(),
751 output: output_label,
752 }
753 }
754}
755
756#[must_use]
758pub const fn count() -> AggregateExpr {
759 AggregateExpr::new(AggregateKind::Count, None)
760}
761
762#[must_use]
764pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
765 AggregateExpr::new(AggregateKind::Count, Some(field.as_ref().to_string()))
766}
767
768#[must_use]
770pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
771 AggregateExpr::new(AggregateKind::Sum, Some(field.as_ref().to_string()))
772}
773
774#[must_use]
776pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
777 AggregateExpr::new(AggregateKind::Avg, Some(field.as_ref().to_string()))
778}
779
780#[must_use]
782pub const fn exists() -> AggregateExpr {
783 AggregateExpr::new(AggregateKind::Exists, None)
784}
785
786#[must_use]
788pub const fn first() -> AggregateExpr {
789 AggregateExpr::new(AggregateKind::First, None)
790}
791
792#[must_use]
794pub const fn last() -> AggregateExpr {
795 AggregateExpr::new(AggregateKind::Last, None)
796}
797
798#[must_use]
800pub const fn min() -> AggregateExpr {
801 AggregateExpr::new(AggregateKind::Min, None)
802}
803
804#[must_use]
806pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
807 AggregateExpr::new(AggregateKind::Min, Some(field.as_ref().to_string()))
808}
809
810#[must_use]
812pub const fn max() -> AggregateExpr {
813 AggregateExpr::new(AggregateKind::Max, None)
814}
815
816#[must_use]
818pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
819 AggregateExpr::new(AggregateKind::Max, Some(field.as_ref().to_string()))
820}
821
822#[cfg(test)]
827mod tests {
828 use crate::db::query::{
829 builder::{
830 PreparedFluentExistingRowsTerminalRuntimeRequest,
831 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
832 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
833 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
834 PreparedFluentProjectionStrategy,
835 },
836 plan::{AggregateKind, FieldSlot},
837 };
838
839 #[test]
840 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
841 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
842 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
843
844 assert_eq!(
845 strategy.aggregate_kind(),
846 AggregateKind::Sum,
847 "sum(distinct field) should preserve SUM aggregate kind",
848 );
849 assert_eq!(
850 strategy.projected_field(),
851 "rank",
852 "sum(distinct field) should preserve projected field labels",
853 );
854 assert!(
855 strategy.aggregate().is_distinct(),
856 "sum(distinct field) should preserve DISTINCT aggregate shape",
857 );
858 assert_eq!(
859 strategy.target_field(),
860 &rank_slot,
861 "sum(distinct field) should preserve the resolved planner field slot",
862 );
863 assert_eq!(
864 strategy.runtime_request(),
865 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
866 "sum(distinct field) should project the numeric DISTINCT runtime request",
867 );
868 }
869
870 #[test]
871 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
872 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
873
874 assert_eq!(
875 strategy.aggregate().kind(),
876 AggregateKind::Count,
877 "count() should preserve the explain-visible aggregate kind",
878 );
879 assert_eq!(
880 strategy.runtime_request(),
881 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
882 "count() should project the existing-rows count runtime request",
883 );
884 }
885
886 #[test]
887 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
888 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
889
890 assert_eq!(
891 strategy.aggregate().kind(),
892 AggregateKind::Exists,
893 "exists() should preserve the explain-visible aggregate kind",
894 );
895 assert_eq!(
896 strategy.runtime_request(),
897 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
898 "exists() should project the existing-rows exists runtime request",
899 );
900 }
901
902 #[test]
903 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
904 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
905 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
906
907 assert_eq!(
908 strategy.aggregate_kind(),
909 AggregateKind::Avg,
910 "avg(field) should preserve AVG aggregate kind",
911 );
912 assert_eq!(
913 strategy.projected_field(),
914 "rank",
915 "avg(field) should preserve projected field labels",
916 );
917 assert!(
918 !strategy.aggregate().is_distinct(),
919 "avg(field) should stay non-distinct unless requested explicitly",
920 );
921 assert_eq!(
922 strategy.target_field(),
923 &rank_slot,
924 "avg(field) should preserve the resolved planner field slot",
925 );
926 assert_eq!(
927 strategy.runtime_request(),
928 PreparedFluentNumericFieldRuntimeRequest::Avg,
929 "avg(field) should project the numeric AVG runtime request",
930 );
931 }
932
933 #[test]
934 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
935 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
936
937 assert_eq!(
938 strategy
939 .explain_aggregate()
940 .map(|aggregate| aggregate.kind()),
941 Some(AggregateKind::First),
942 "first() should preserve the explain-visible aggregate kind",
943 );
944 assert_eq!(
945 strategy.runtime_request(),
946 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
947 kind: AggregateKind::First,
948 },
949 "first() should project the response-order runtime request",
950 );
951 }
952
953 #[test]
954 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
955 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
956 let strategy =
957 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
958
959 assert_eq!(
960 strategy.explain_aggregate(),
961 None,
962 "nth_by(field, nth) should stay off the current explain aggregate surface",
963 );
964 assert_eq!(
965 strategy.runtime_request(),
966 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
967 target_field: rank_slot,
968 nth: 2,
969 },
970 "nth_by(field, nth) should preserve the resolved field-order runtime request",
971 );
972 }
973
974 #[test]
975 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
976 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
977 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
978 let explain = strategy.explain_descriptor();
979
980 assert_eq!(
981 strategy.target_field(),
982 &rank_slot,
983 "count_distinct_by(field) should preserve the resolved planner field slot",
984 );
985 assert_eq!(
986 strategy.runtime_request(),
987 PreparedFluentProjectionRuntimeRequest::CountDistinct,
988 "count_distinct_by(field) should project the distinct-count runtime request",
989 );
990 assert_eq!(
991 explain.terminal_label(),
992 "count_distinct_by",
993 "count_distinct_by(field) should project the stable explain terminal label",
994 );
995 assert_eq!(
996 explain.field_label(),
997 "rank",
998 "count_distinct_by(field) should project the stable explain field label",
999 );
1000 assert_eq!(
1001 explain.output_label(),
1002 "count",
1003 "count_distinct_by(field) should project the stable explain output label",
1004 );
1005 }
1006
1007 #[test]
1008 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1009 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1010 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1011 let explain = strategy.explain_descriptor();
1012
1013 assert_eq!(
1014 strategy.target_field(),
1015 &rank_slot,
1016 "last_value_by(field) should preserve the resolved planner field slot",
1017 );
1018 assert_eq!(
1019 strategy.runtime_request(),
1020 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1021 terminal_kind: AggregateKind::Last,
1022 },
1023 "last_value_by(field) should project the terminal-value runtime request",
1024 );
1025 assert_eq!(
1026 explain.terminal_label(),
1027 "last_value_by",
1028 "last_value_by(field) should project the stable explain terminal label",
1029 );
1030 assert_eq!(
1031 explain.field_label(),
1032 "rank",
1033 "last_value_by(field) should project the stable explain field label",
1034 );
1035 assert_eq!(
1036 explain.output_label(),
1037 "terminal_value",
1038 "last_value_by(field) should project the stable explain output label",
1039 );
1040 }
1041}