1use crate::db::query::plan::{
7 AggregateKind, FieldSlot,
8 expr::{Expr, FieldId, canonicalize_aggregate_input_expr},
9};
10
11#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct AggregateExpr {
22 kind: AggregateKind,
23 input_expr: Option<Box<Expr>>,
24 filter_expr: Option<Box<Expr>>,
25 distinct: bool,
26}
27
28impl AggregateExpr {
29 const fn terminal(kind: AggregateKind) -> Self {
31 Self {
32 kind,
33 input_expr: None,
34 filter_expr: None,
35 distinct: false,
36 }
37 }
38
39 fn field_target(kind: AggregateKind, field: impl Into<String>) -> Self {
41 Self {
42 kind,
43 input_expr: Some(Box::new(Expr::Field(FieldId::new(field.into())))),
44 filter_expr: None,
45 distinct: false,
46 }
47 }
48
49 pub(in crate::db) fn from_expression_input(kind: AggregateKind, input_expr: Expr) -> Self {
51 Self {
52 kind,
53 input_expr: Some(Box::new(canonicalize_aggregate_input_expr(
54 kind, input_expr,
55 ))),
56 filter_expr: None,
57 distinct: false,
58 }
59 }
60
61 #[must_use]
63 pub(in crate::db) fn with_filter_expr(mut self, filter_expr: Expr) -> Self {
64 self.filter_expr = Some(Box::new(filter_expr));
65 self
66 }
67
68 #[must_use]
70 pub const fn distinct(mut self) -> Self {
71 self.distinct = true;
72 self
73 }
74
75 #[must_use]
77 pub(crate) const fn kind(&self) -> AggregateKind {
78 self.kind
79 }
80
81 #[must_use]
83 pub(crate) fn input_expr(&self) -> Option<&Expr> {
84 self.input_expr.as_deref()
85 }
86
87 #[must_use]
89 pub(crate) fn filter_expr(&self) -> Option<&Expr> {
90 self.filter_expr.as_deref()
91 }
92
93 #[must_use]
95 pub(crate) fn target_field(&self) -> Option<&str> {
96 match self.input_expr() {
97 Some(Expr::Field(field)) => Some(field.as_str()),
98 _ => None,
99 }
100 }
101
102 #[must_use]
104 pub(crate) const fn is_distinct(&self) -> bool {
105 self.distinct
106 }
107
108 pub(in crate::db::query) fn from_semantic_parts(
110 kind: AggregateKind,
111 target_field: Option<String>,
112 distinct: bool,
113 ) -> Self {
114 Self {
115 kind,
116 input_expr: target_field.map(|field| Box::new(Expr::Field(FieldId::new(field)))),
117 filter_expr: None,
118 distinct,
119 }
120 }
121
122 #[cfg(test)]
124 #[must_use]
125 pub(in crate::db) fn terminal_for_kind(kind: AggregateKind) -> Self {
126 match kind {
127 AggregateKind::Count => count(),
128 AggregateKind::Exists => exists(),
129 AggregateKind::Min => min(),
130 AggregateKind::Max => max(),
131 AggregateKind::First => first(),
132 AggregateKind::Last => last(),
133 AggregateKind::Sum | AggregateKind::Avg => unreachable!(
134 "AggregateExpr::terminal_for_kind does not support SUM/AVG field-target kinds"
135 ),
136 }
137 }
138}
139
140pub(crate) trait PreparedFluentAggregateExplainStrategy {
151 fn explain_aggregate_kind(&self) -> Option<AggregateKind>;
154
155 fn explain_projected_field(&self) -> Option<&str> {
157 None
158 }
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
168pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
169 CountRows,
170 ExistsRows,
171}
172
173#[derive(Clone, Debug, Eq, PartialEq)]
186pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
187 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
188}
189
190impl PreparedFluentExistingRowsTerminalStrategy {
191 #[must_use]
193 pub(crate) const fn count_rows() -> Self {
194 Self {
195 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
196 }
197 }
198
199 #[must_use]
201 pub(crate) const fn exists_rows() -> Self {
202 Self {
203 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
204 }
205 }
206
207 #[cfg(test)]
210 #[must_use]
211 pub(crate) const fn aggregate(&self) -> AggregateExpr {
212 match self.runtime_request {
213 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => count(),
214 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => exists(),
215 }
216 }
217
218 #[cfg(test)]
221 #[must_use]
222 pub(crate) const fn runtime_request(
223 &self,
224 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
225 &self.runtime_request
226 }
227
228 #[must_use]
231 pub(crate) const fn into_runtime_request(
232 self,
233 ) -> PreparedFluentExistingRowsTerminalRuntimeRequest {
234 self.runtime_request
235 }
236}
237
238impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
239 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
240 Some(match self.runtime_request {
241 PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows => AggregateKind::Count,
242 PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows => AggregateKind::Exists,
243 })
244 }
245}
246
247#[derive(Clone, Debug, Eq, PartialEq)]
254pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
255 IdTerminal {
256 kind: AggregateKind,
257 },
258 IdBySlot {
259 kind: AggregateKind,
260 target_field: FieldSlot,
261 },
262}
263
264#[derive(Clone, Debug, Eq, PartialEq)]
276pub(crate) struct PreparedFluentScalarTerminalStrategy {
277 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
278}
279
280impl PreparedFluentScalarTerminalStrategy {
281 #[must_use]
283 pub(crate) const fn id_terminal(kind: AggregateKind) -> Self {
284 Self {
285 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
286 }
287 }
288
289 #[must_use]
292 pub(crate) const fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
293 Self {
294 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
295 kind,
296 target_field,
297 },
298 }
299 }
300
301 #[must_use]
304 pub(crate) fn into_runtime_request(self) -> PreparedFluentScalarTerminalRuntimeRequest {
305 self.runtime_request
306 }
307}
308
309impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
310 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
311 Some(match self.runtime_request {
312 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind }
313 | PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { kind, .. } => kind,
314 })
315 }
316
317 fn explain_projected_field(&self) -> Option<&str> {
318 match &self.runtime_request {
319 PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { .. } => None,
320 PreparedFluentScalarTerminalRuntimeRequest::IdBySlot { target_field, .. } => {
321 Some(target_field.field())
322 }
323 }
324 }
325}
326
327#[derive(Clone, Copy, Debug, Eq, PartialEq)]
337pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
338 Sum,
339 SumDistinct,
340 Avg,
341 AvgDistinct,
342}
343
344#[derive(Clone, Debug, Eq, PartialEq)]
357pub(crate) struct PreparedFluentNumericFieldStrategy {
358 target_field: FieldSlot,
359 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
360}
361
362impl PreparedFluentNumericFieldStrategy {
363 #[must_use]
365 pub(crate) const fn sum_by_slot(target_field: FieldSlot) -> Self {
366 Self {
367 target_field,
368 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
369 }
370 }
371
372 #[must_use]
374 pub(crate) const fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
375 Self {
376 target_field,
377 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
378 }
379 }
380
381 #[must_use]
383 pub(crate) const fn avg_by_slot(target_field: FieldSlot) -> Self {
384 Self {
385 target_field,
386 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
387 }
388 }
389
390 #[must_use]
392 pub(crate) const fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
393 Self {
394 target_field,
395 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
396 }
397 }
398
399 #[cfg(test)]
402 #[must_use]
403 pub(crate) fn aggregate(&self) -> AggregateExpr {
404 let field = self.target_field.field();
405
406 match self.runtime_request {
407 PreparedFluentNumericFieldRuntimeRequest::Sum => sum(field),
408 PreparedFluentNumericFieldRuntimeRequest::SumDistinct => sum(field).distinct(),
409 PreparedFluentNumericFieldRuntimeRequest::Avg => avg(field),
410 PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => avg(field).distinct(),
411 }
412 }
413
414 #[cfg(test)]
417 #[must_use]
418 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
419 match self.runtime_request {
420 PreparedFluentNumericFieldRuntimeRequest::Sum
421 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
422 PreparedFluentNumericFieldRuntimeRequest::Avg
423 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
424 }
425 }
426
427 #[cfg(test)]
430 #[must_use]
431 pub(crate) fn projected_field(&self) -> &str {
432 self.target_field.field()
433 }
434
435 #[cfg(test)]
438 #[must_use]
439 pub(crate) const fn target_field(&self) -> &FieldSlot {
440 &self.target_field
441 }
442
443 #[cfg(test)]
446 #[must_use]
447 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
448 self.runtime_request
449 }
450
451 #[must_use]
454 pub(crate) fn into_runtime_parts(
455 self,
456 ) -> (FieldSlot, PreparedFluentNumericFieldRuntimeRequest) {
457 (self.target_field, self.runtime_request)
458 }
459}
460
461impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
462 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
463 Some(match self.runtime_request {
464 PreparedFluentNumericFieldRuntimeRequest::Sum
465 | PreparedFluentNumericFieldRuntimeRequest::SumDistinct => AggregateKind::Sum,
466 PreparedFluentNumericFieldRuntimeRequest::Avg
467 | PreparedFluentNumericFieldRuntimeRequest::AvgDistinct => AggregateKind::Avg,
468 })
469 }
470
471 fn explain_projected_field(&self) -> Option<&str> {
472 Some(self.target_field.field())
473 }
474}
475
476#[derive(Clone, Debug, Eq, PartialEq)]
487pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
488 ResponseOrder { kind: AggregateKind },
489 NthBySlot { target_field: FieldSlot, nth: usize },
490 MedianBySlot { target_field: FieldSlot },
491 MinMaxBySlot { target_field: FieldSlot },
492}
493
494#[derive(Clone, Debug, Eq, PartialEq)]
505pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
506 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
507}
508
509impl PreparedFluentOrderSensitiveTerminalStrategy {
510 #[must_use]
512 pub(crate) const fn first() -> Self {
513 Self {
514 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
515 kind: AggregateKind::First,
516 },
517 }
518 }
519
520 #[must_use]
522 pub(crate) const fn last() -> Self {
523 Self {
524 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
525 kind: AggregateKind::Last,
526 },
527 }
528 }
529
530 #[must_use]
532 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
533 Self {
534 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
535 target_field,
536 nth,
537 },
538 }
539 }
540
541 #[must_use]
543 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
544 Self {
545 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
546 target_field,
547 },
548 }
549 }
550
551 #[must_use]
553 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
554 Self {
555 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
556 target_field,
557 },
558 }
559 }
560
561 #[cfg(test)]
564 #[must_use]
565 pub(crate) fn explain_aggregate(&self) -> Option<AggregateExpr> {
566 match self.runtime_request {
567 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
568 Some(AggregateExpr::terminal_for_kind(kind))
569 }
570 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
571 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
572 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
573 }
574 }
575
576 #[cfg(test)]
579 #[must_use]
580 pub(crate) const fn runtime_request(
581 &self,
582 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
583 &self.runtime_request
584 }
585
586 #[must_use]
589 pub(crate) fn into_runtime_request(self) -> PreparedFluentOrderSensitiveTerminalRuntimeRequest {
590 self.runtime_request
591 }
592}
593
594impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
595 fn explain_aggregate_kind(&self) -> Option<AggregateKind> {
596 match self.runtime_request {
597 PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder { kind } => {
598 Some(kind)
599 }
600 PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot { .. }
601 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot { .. }
602 | PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot { .. } => None,
603 }
604 }
605}
606
607#[derive(Clone, Copy, Debug, Eq, PartialEq)]
617pub(crate) enum PreparedFluentProjectionRuntimeRequest {
618 Values,
619 DistinctValues,
620 CountDistinct,
621 ValuesWithIds,
622 TerminalValue { terminal_kind: AggregateKind },
623}
624
625#[derive(Clone, Copy, Debug, Eq, PartialEq)]
635pub(crate) struct PreparedFluentProjectionExplainDescriptor<'a> {
636 terminal: &'static str,
637 field: &'a str,
638 output: &'static str,
639}
640
641impl<'a> PreparedFluentProjectionExplainDescriptor<'a> {
642 #[must_use]
644 pub(crate) const fn terminal_label(self) -> &'static str {
645 self.terminal
646 }
647
648 #[must_use]
650 pub(crate) const fn field_label(self) -> &'a str {
651 self.field
652 }
653
654 #[must_use]
656 pub(crate) const fn output_label(self) -> &'static str {
657 self.output
658 }
659}
660
661#[derive(Clone, Debug, Eq, PartialEq)]
672pub(crate) struct PreparedFluentProjectionStrategy {
673 target_field: FieldSlot,
674 runtime_request: PreparedFluentProjectionRuntimeRequest,
675}
676
677impl PreparedFluentProjectionStrategy {
678 #[must_use]
680 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
681 Self {
682 target_field,
683 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
684 }
685 }
686
687 #[must_use]
689 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
690 Self {
691 target_field,
692 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
693 }
694 }
695
696 #[must_use]
698 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
699 Self {
700 target_field,
701 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
702 }
703 }
704
705 #[must_use]
707 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
708 Self {
709 target_field,
710 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
711 }
712 }
713
714 #[must_use]
716 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
717 Self {
718 target_field,
719 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
720 terminal_kind: AggregateKind::First,
721 },
722 }
723 }
724
725 #[must_use]
727 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
728 Self {
729 target_field,
730 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
731 terminal_kind: AggregateKind::Last,
732 },
733 }
734 }
735
736 #[cfg(test)]
739 #[must_use]
740 pub(crate) const fn target_field(&self) -> &FieldSlot {
741 &self.target_field
742 }
743
744 #[cfg(test)]
747 #[must_use]
748 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
749 self.runtime_request
750 }
751
752 #[must_use]
756 pub(crate) fn into_runtime_parts(self) -> (FieldSlot, PreparedFluentProjectionRuntimeRequest) {
757 (self.target_field, self.runtime_request)
758 }
759
760 #[must_use]
763 pub(crate) fn explain_descriptor(&self) -> PreparedFluentProjectionExplainDescriptor<'_> {
764 let terminal_label = match self.runtime_request {
765 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
766 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
767 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
768 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
769 PreparedFluentProjectionRuntimeRequest::TerminalValue {
770 terminal_kind: AggregateKind::First,
771 } => "first_value_by",
772 PreparedFluentProjectionRuntimeRequest::TerminalValue {
773 terminal_kind: AggregateKind::Last,
774 } => "last_value_by",
775 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
776 unreachable!("projection terminal value explain requires FIRST/LAST kind")
777 }
778 };
779 let output_label = match self.runtime_request {
780 PreparedFluentProjectionRuntimeRequest::Values
781 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
782 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
783 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
784 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
785 };
786
787 PreparedFluentProjectionExplainDescriptor {
788 terminal: terminal_label,
789 field: self.target_field.field(),
790 output: output_label,
791 }
792 }
793}
794
795#[must_use]
797pub const fn count() -> AggregateExpr {
798 AggregateExpr::terminal(AggregateKind::Count)
799}
800
801#[must_use]
803pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
804 AggregateExpr::field_target(AggregateKind::Count, field.as_ref().to_string())
805}
806
807#[must_use]
809pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
810 AggregateExpr::field_target(AggregateKind::Sum, field.as_ref().to_string())
811}
812
813#[must_use]
815pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
816 AggregateExpr::field_target(AggregateKind::Avg, field.as_ref().to_string())
817}
818
819#[must_use]
821pub const fn exists() -> AggregateExpr {
822 AggregateExpr::terminal(AggregateKind::Exists)
823}
824
825#[must_use]
827pub const fn first() -> AggregateExpr {
828 AggregateExpr::terminal(AggregateKind::First)
829}
830
831#[must_use]
833pub const fn last() -> AggregateExpr {
834 AggregateExpr::terminal(AggregateKind::Last)
835}
836
837#[must_use]
839pub const fn min() -> AggregateExpr {
840 AggregateExpr::terminal(AggregateKind::Min)
841}
842
843#[must_use]
845pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
846 AggregateExpr::field_target(AggregateKind::Min, field.as_ref().to_string())
847}
848
849#[must_use]
851pub const fn max() -> AggregateExpr {
852 AggregateExpr::terminal(AggregateKind::Max)
853}
854
855#[must_use]
857pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
858 AggregateExpr::field_target(AggregateKind::Max, field.as_ref().to_string())
859}
860
861#[cfg(test)]
866mod tests {
867 use crate::db::query::{
868 builder::{
869 PreparedFluentExistingRowsTerminalRuntimeRequest,
870 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
871 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
872 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
873 PreparedFluentProjectionStrategy,
874 },
875 plan::{AggregateKind, FieldSlot},
876 };
877
878 #[test]
879 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
880 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
881 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
882
883 assert_eq!(
884 strategy.aggregate_kind(),
885 AggregateKind::Sum,
886 "sum(distinct field) should preserve SUM aggregate kind",
887 );
888 assert_eq!(
889 strategy.projected_field(),
890 "rank",
891 "sum(distinct field) should preserve projected field labels",
892 );
893 assert!(
894 strategy.aggregate().is_distinct(),
895 "sum(distinct field) should preserve DISTINCT aggregate shape",
896 );
897 assert_eq!(
898 strategy.target_field(),
899 &rank_slot,
900 "sum(distinct field) should preserve the resolved planner field slot",
901 );
902 assert_eq!(
903 strategy.runtime_request(),
904 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
905 "sum(distinct field) should project the numeric DISTINCT runtime request",
906 );
907 }
908
909 #[test]
910 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
911 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
912
913 assert_eq!(
914 strategy.aggregate().kind(),
915 AggregateKind::Count,
916 "count() should preserve the explain-visible aggregate kind",
917 );
918 assert_eq!(
919 strategy.runtime_request(),
920 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
921 "count() should project the existing-rows count runtime request",
922 );
923 }
924
925 #[test]
926 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
927 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
928
929 assert_eq!(
930 strategy.aggregate().kind(),
931 AggregateKind::Exists,
932 "exists() should preserve the explain-visible aggregate kind",
933 );
934 assert_eq!(
935 strategy.runtime_request(),
936 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
937 "exists() should project the existing-rows exists runtime request",
938 );
939 }
940
941 #[test]
942 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
943 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
944 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
945
946 assert_eq!(
947 strategy.aggregate_kind(),
948 AggregateKind::Avg,
949 "avg(field) should preserve AVG aggregate kind",
950 );
951 assert_eq!(
952 strategy.projected_field(),
953 "rank",
954 "avg(field) should preserve projected field labels",
955 );
956 assert!(
957 !strategy.aggregate().is_distinct(),
958 "avg(field) should stay non-distinct unless requested explicitly",
959 );
960 assert_eq!(
961 strategy.target_field(),
962 &rank_slot,
963 "avg(field) should preserve the resolved planner field slot",
964 );
965 assert_eq!(
966 strategy.runtime_request(),
967 PreparedFluentNumericFieldRuntimeRequest::Avg,
968 "avg(field) should project the numeric AVG runtime request",
969 );
970 }
971
972 #[test]
973 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
974 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
975
976 assert_eq!(
977 strategy
978 .explain_aggregate()
979 .map(|aggregate| aggregate.kind()),
980 Some(AggregateKind::First),
981 "first() should preserve the explain-visible aggregate kind",
982 );
983 assert_eq!(
984 strategy.runtime_request(),
985 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
986 kind: AggregateKind::First,
987 },
988 "first() should project the response-order runtime request",
989 );
990 }
991
992 #[test]
993 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
994 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
995 let strategy =
996 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
997
998 assert_eq!(
999 strategy.explain_aggregate(),
1000 None,
1001 "nth_by(field, nth) should stay off the current explain aggregate surface",
1002 );
1003 assert_eq!(
1004 strategy.runtime_request(),
1005 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
1006 target_field: rank_slot,
1007 nth: 2,
1008 },
1009 "nth_by(field, nth) should preserve the resolved field-order runtime request",
1010 );
1011 }
1012
1013 #[test]
1014 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
1015 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1016 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
1017 let explain = strategy.explain_descriptor();
1018
1019 assert_eq!(
1020 strategy.target_field(),
1021 &rank_slot,
1022 "count_distinct_by(field) should preserve the resolved planner field slot",
1023 );
1024 assert_eq!(
1025 strategy.runtime_request(),
1026 PreparedFluentProjectionRuntimeRequest::CountDistinct,
1027 "count_distinct_by(field) should project the distinct-count runtime request",
1028 );
1029 assert_eq!(
1030 explain.terminal_label(),
1031 "count_distinct_by",
1032 "count_distinct_by(field) should project the stable explain terminal label",
1033 );
1034 assert_eq!(
1035 explain.field_label(),
1036 "rank",
1037 "count_distinct_by(field) should project the stable explain field label",
1038 );
1039 assert_eq!(
1040 explain.output_label(),
1041 "count",
1042 "count_distinct_by(field) should project the stable explain output label",
1043 );
1044 }
1045
1046 #[test]
1047 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
1048 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
1049 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
1050 let explain = strategy.explain_descriptor();
1051
1052 assert_eq!(
1053 strategy.target_field(),
1054 &rank_slot,
1055 "last_value_by(field) should preserve the resolved planner field slot",
1056 );
1057 assert_eq!(
1058 strategy.runtime_request(),
1059 PreparedFluentProjectionRuntimeRequest::TerminalValue {
1060 terminal_kind: AggregateKind::Last,
1061 },
1062 "last_value_by(field) should project the terminal-value runtime request",
1063 );
1064 assert_eq!(
1065 explain.terminal_label(),
1066 "last_value_by",
1067 "last_value_by(field) should project the stable explain terminal label",
1068 );
1069 assert_eq!(
1070 explain.field_label(),
1071 "rank",
1072 "last_value_by(field) should project the stable explain field label",
1073 );
1074 assert_eq!(
1075 explain.output_label(),
1076 "terminal_value",
1077 "last_value_by(field) should project the stable explain output label",
1078 );
1079 }
1080}