1use crate::db::query::plan::{
7 AggregateKind, FieldSlot,
8 expr::{Expr, FieldId},
9};
10
11#[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 const fn terminal(kind: AggregateKind) -> Self {
29 Self {
30 kind,
31 input_expr: None,
32 distinct: false,
33 }
34 }
35
36 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 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 #[must_use]
56 pub const fn distinct(mut self) -> Self {
57 self.distinct = true;
58 self
59 }
60
61 #[must_use]
63 pub(crate) const fn kind(&self) -> AggregateKind {
64 self.kind
65 }
66
67 #[must_use]
69 pub(crate) fn input_expr(&self) -> Option<&Expr> {
70 self.input_expr.as_deref()
71 }
72
73 #[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 #[must_use]
84 pub(crate) const fn is_distinct(&self) -> bool {
85 self.distinct
86 }
87
88 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 #[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
119pub(crate) trait PreparedFluentAggregateExplainStrategy {
130 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
133
134 fn explain_projected_field(&self) -> Option<&str> {
136 None
137 }
138}
139
140#[derive(Clone, Debug, Eq, PartialEq)]
147pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
148 CountRows,
149 ExistsRows,
150}
151
152#[derive(Clone, Debug, Eq, PartialEq)]
165pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
166 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
167}
168
169impl PreparedFluentExistingRowsTerminalStrategy {
170 #[must_use]
172 pub(crate) const fn count_rows() -> Self {
173 Self {
174 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
175 }
176 }
177
178 #[must_use]
180 pub(crate) const fn exists_rows() -> Self {
181 Self {
182 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
183 }
184 }
185
186 #[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 #[cfg(test)]
200 #[must_use]
201 pub(crate) const fn runtime_request(
202 &self,
203 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
204 &self.runtime_request
205 }
206
207 #[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#[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#[derive(Clone, Debug, Eq, PartialEq)]
255pub(crate) struct PreparedFluentScalarTerminalStrategy {
256 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
257}
258
259impl PreparedFluentScalarTerminalStrategy {
260 #[must_use]
262 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
263 Self {
264 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
265 }
266 }
267
268 #[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 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
316pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
317 Sum,
318 SumDistinct,
319 Avg,
320 AvgDistinct,
321}
322
323#[derive(Clone, Debug, Eq, PartialEq)]
336pub(crate) struct PreparedFluentNumericFieldStrategy {
337 target_field: FieldSlot,
338 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
339}
340
341impl PreparedFluentNumericFieldStrategy {
342 #[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 #[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 #[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 #[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 #[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 #[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 #[cfg(test)]
409 #[must_use]
410 pub(crate) fn projected_field(&self) -> &str {
411 self.target_field.field()
412 }
413
414 #[cfg(test)]
417 #[must_use]
418 pub(crate) const fn target_field(&self) -> &FieldSlot {
419 &self.target_field
420 }
421
422 #[cfg(test)]
425 #[must_use]
426 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
427 self.runtime_request
428 }
429
430 #[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#[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#[derive(Clone, Debug, Eq, PartialEq)]
484pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
485 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
486}
487
488impl PreparedFluentOrderSensitiveTerminalStrategy {
489 #[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 #[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 #[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 #[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 #[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 #[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 #[cfg(test)]
558 #[must_use]
559 pub(crate) const fn runtime_request(
560 &self,
561 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
562 &self.runtime_request
563 }
564
565 #[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#[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#[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 #[must_use]
623 pub(crate) const fn terminal_label(self) -> &'static str {
624 self.terminal
625 }
626
627 #[must_use]
629 pub(crate) const fn field_label(self) -> &'a str {
630 self.field
631 }
632
633 #[must_use]
635 pub(crate) const fn output_label(self) -> &'static str {
636 self.output
637 }
638}
639
640#[derive(Clone, Debug, Eq, PartialEq)]
651pub(crate) struct PreparedFluentProjectionStrategy {
652 target_field: FieldSlot,
653 runtime_request: PreparedFluentProjectionRuntimeRequest,
654}
655
656impl PreparedFluentProjectionStrategy {
657 #[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 #[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 #[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 #[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 #[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 #[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 #[cfg(test)]
718 #[must_use]
719 pub(crate) const fn target_field(&self) -> &FieldSlot {
720 &self.target_field
721 }
722
723 #[cfg(test)]
726 #[must_use]
727 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
728 self.runtime_request
729 }
730
731 #[must_use]
735 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
736 (self.target_field, self.runtime_request)
737 }
738
739 #[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#[must_use]
776pub const fn count() -> AggregateExpr {
777 AggregateExpr::terminal(AggregateKind::Count)
778}
779
780#[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#[must_use]
788pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
789 AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
790}
791
792#[must_use]
794pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
795 AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
796}
797
798#[must_use]
800pub const fn exists() -> AggregateExpr {
801 AggregateExpr::terminal(AggregateKind::Exists)
802}
803
804#[must_use]
806pub const fn first() -> AggregateExpr {
807 AggregateExpr::terminal(AggregateKind::First)
808}
809
810#[must_use]
812pub const fn last() -> AggregateExpr {
813 AggregateExpr::terminal(AggregateKind::Last)
814}
815
816#[must_use]
818pub const fn min() -> AggregateExpr {
819 AggregateExpr::terminal(AggregateKind::Min)
820}
821
822#[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#[must_use]
830pub const fn max() -> AggregateExpr {
831 AggregateExpr::terminal(AggregateKind::Max)
832}
833
834#[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#[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}