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 project_explain_aggregate(&self) -> Option<&AggregateExpr>;
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
125pub(crate) enum PreparedFluentExistingRowsTerminalRuntimeRequest {
126 CountRows,
127 ExistsRows,
128}
129
130#[derive(Clone, Debug, Eq, PartialEq)]
141pub(crate) struct PreparedFluentExistingRowsTerminalStrategy {
142 aggregate: AggregateExpr,
143 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest,
144}
145
146impl PreparedFluentExistingRowsTerminalStrategy {
147 #[must_use]
149 pub(crate) const fn count_rows() -> Self {
150 Self {
151 aggregate: count(),
152 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
153 }
154 }
155
156 #[must_use]
158 pub(crate) const fn exists_rows() -> Self {
159 Self {
160 aggregate: exists(),
161 runtime_request: PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
162 }
163 }
164
165 #[must_use]
168 pub(crate) const fn aggregate(&self) -> &AggregateExpr {
169 &self.aggregate
170 }
171
172 #[must_use]
175 pub(crate) const fn runtime_request(
176 &self,
177 ) -> &PreparedFluentExistingRowsTerminalRuntimeRequest {
178 &self.runtime_request
179 }
180}
181
182impl PreparedFluentAggregateExplainStrategy for PreparedFluentExistingRowsTerminalStrategy {
183 fn project_explain_aggregate(&self) -> Option<&AggregateExpr> {
184 Some(self.aggregate())
185 }
186}
187
188#[derive(Clone, Debug, Eq, PartialEq)]
195pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
196 IdTerminal {
197 kind: AggregateKind,
198 },
199 IdBySlot {
200 kind: AggregateKind,
201 target_field: FieldSlot,
202 },
203}
204
205#[derive(Clone, Debug, Eq, PartialEq)]
216pub(crate) struct PreparedFluentScalarTerminalStrategy {
217 aggregate: AggregateExpr,
218 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
219}
220
221impl PreparedFluentScalarTerminalStrategy {
222 #[must_use]
224 pub(crate) fn id_terminal(kind: AggregateKind) -> Self {
225 Self {
226 aggregate: AggregateExpr::terminal_for_kind(kind),
227 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
228 }
229 }
230
231 #[must_use]
234 pub(crate) fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
235 Self {
236 aggregate: AggregateExpr::field_target_extrema_for_kind(kind, target_field.field()),
237 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
238 kind,
239 target_field,
240 },
241 }
242 }
243
244 #[must_use]
247 pub(crate) const fn aggregate(&self) -> &AggregateExpr {
248 &self.aggregate
249 }
250
251 #[must_use]
254 pub(crate) const fn runtime_request(&self) -> &PreparedFluentScalarTerminalRuntimeRequest {
255 &self.runtime_request
256 }
257}
258
259impl PreparedFluentAggregateExplainStrategy for PreparedFluentScalarTerminalStrategy {
260 fn project_explain_aggregate(&self) -> Option<&AggregateExpr> {
261 Some(self.aggregate())
262 }
263}
264
265#[derive(Clone, Copy, Debug, Eq, PartialEq)]
275pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
276 Sum,
277 SumDistinct,
278 Avg,
279 AvgDistinct,
280}
281
282#[derive(Clone, Debug, Eq, PartialEq)]
293pub(crate) struct PreparedFluentNumericFieldStrategy {
294 aggregate: AggregateExpr,
295 target_field: FieldSlot,
296 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
297}
298
299impl PreparedFluentNumericFieldStrategy {
300 #[must_use]
302 pub(crate) fn sum_by_slot(target_field: FieldSlot) -> Self {
303 Self {
304 aggregate: sum(target_field.field()),
305 target_field,
306 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
307 }
308 }
309
310 #[must_use]
312 pub(crate) fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
313 Self {
314 aggregate: sum(target_field.field()).distinct(),
315 target_field,
316 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
317 }
318 }
319
320 #[must_use]
322 pub(crate) fn avg_by_slot(target_field: FieldSlot) -> Self {
323 Self {
324 aggregate: avg(target_field.field()),
325 target_field,
326 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
327 }
328 }
329
330 #[must_use]
332 pub(crate) fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
333 Self {
334 aggregate: avg(target_field.field()).distinct(),
335 target_field,
336 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
337 }
338 }
339
340 #[must_use]
343 pub(crate) const fn aggregate(&self) -> &AggregateExpr {
344 &self.aggregate
345 }
346
347 #[cfg(test)]
350 #[must_use]
351 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
352 self.aggregate.kind()
353 }
354
355 #[cfg(test)]
358 #[must_use]
359 pub(crate) fn projected_field(&self) -> Option<&str> {
360 self.aggregate.target_field()
361 }
362
363 #[must_use]
366 pub(crate) const fn target_field(&self) -> &FieldSlot {
367 &self.target_field
368 }
369
370 #[must_use]
373 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
374 self.runtime_request
375 }
376}
377
378impl PreparedFluentAggregateExplainStrategy for PreparedFluentNumericFieldStrategy {
379 fn project_explain_aggregate(&self) -> Option<&AggregateExpr> {
380 Some(self.aggregate())
381 }
382}
383
384#[derive(Clone, Debug, Eq, PartialEq)]
394pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
395 ResponseOrder { kind: AggregateKind },
396 NthBySlot { target_field: FieldSlot, nth: usize },
397 MedianBySlot { target_field: FieldSlot },
398 MinMaxBySlot { target_field: FieldSlot },
399}
400
401#[derive(Clone, Debug, Eq, PartialEq)]
412pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
413 explain_aggregate: Option<AggregateExpr>,
414 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
415}
416
417impl PreparedFluentOrderSensitiveTerminalStrategy {
418 #[must_use]
420 pub(crate) const fn first() -> Self {
421 Self {
422 explain_aggregate: Some(first()),
423 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
424 kind: AggregateKind::First,
425 },
426 }
427 }
428
429 #[must_use]
431 pub(crate) const fn last() -> Self {
432 Self {
433 explain_aggregate: Some(last()),
434 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
435 kind: AggregateKind::Last,
436 },
437 }
438 }
439
440 #[must_use]
442 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
443 Self {
444 explain_aggregate: None,
445 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
446 target_field,
447 nth,
448 },
449 }
450 }
451
452 #[must_use]
454 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
455 Self {
456 explain_aggregate: None,
457 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
458 target_field,
459 },
460 }
461 }
462
463 #[must_use]
465 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
466 Self {
467 explain_aggregate: None,
468 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
469 target_field,
470 },
471 }
472 }
473
474 #[must_use]
477 pub(crate) const fn explain_aggregate(&self) -> Option<&AggregateExpr> {
478 self.explain_aggregate.as_ref()
479 }
480
481 #[must_use]
484 pub(crate) const fn runtime_request(
485 &self,
486 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
487 &self.runtime_request
488 }
489}
490
491impl PreparedFluentAggregateExplainStrategy for PreparedFluentOrderSensitiveTerminalStrategy {
492 fn project_explain_aggregate(&self) -> Option<&AggregateExpr> {
493 self.explain_aggregate()
494 }
495}
496
497#[derive(Clone, Copy, Debug, Eq, PartialEq)]
507pub(crate) enum PreparedFluentProjectionRuntimeRequest {
508 Values,
509 DistinctValues,
510 CountDistinct,
511 ValuesWithIds,
512 TerminalValue { terminal_kind: AggregateKind },
513}
514
515#[derive(Clone, Debug, Eq, PartialEq)]
526pub(crate) struct PreparedFluentProjectionStrategy {
527 target_field: FieldSlot,
528 runtime_request: PreparedFluentProjectionRuntimeRequest,
529}
530
531impl PreparedFluentProjectionStrategy {
532 #[must_use]
534 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
535 Self {
536 target_field,
537 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
538 }
539 }
540
541 #[must_use]
543 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
544 Self {
545 target_field,
546 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
547 }
548 }
549
550 #[must_use]
552 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
553 Self {
554 target_field,
555 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
556 }
557 }
558
559 #[must_use]
561 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
562 Self {
563 target_field,
564 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
565 }
566 }
567
568 #[must_use]
570 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
571 Self {
572 target_field,
573 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
574 terminal_kind: AggregateKind::First,
575 },
576 }
577 }
578
579 #[must_use]
581 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
582 Self {
583 target_field,
584 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
585 terminal_kind: AggregateKind::Last,
586 },
587 }
588 }
589
590 #[must_use]
593 pub(crate) const fn target_field(&self) -> &FieldSlot {
594 &self.target_field
595 }
596
597 #[must_use]
600 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
601 self.runtime_request
602 }
603
604 #[must_use]
607 pub(crate) fn explain_terminal_label(&self) -> &'static str {
608 match self.runtime_request {
609 PreparedFluentProjectionRuntimeRequest::Values => "values_by",
610 PreparedFluentProjectionRuntimeRequest::DistinctValues => "distinct_values_by",
611 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count_distinct_by",
612 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_by_with_ids",
613 PreparedFluentProjectionRuntimeRequest::TerminalValue {
614 terminal_kind: AggregateKind::First,
615 } => "first_value_by",
616 PreparedFluentProjectionRuntimeRequest::TerminalValue {
617 terminal_kind: AggregateKind::Last,
618 } => "last_value_by",
619 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => {
620 unreachable!("projection terminal value explain requires FIRST/LAST kind")
621 }
622 }
623 }
624
625 #[must_use]
628 pub(crate) const fn explain_output_label(&self) -> &'static str {
629 match self.runtime_request {
630 PreparedFluentProjectionRuntimeRequest::Values
631 | PreparedFluentProjectionRuntimeRequest::DistinctValues => "values",
632 PreparedFluentProjectionRuntimeRequest::CountDistinct => "count",
633 PreparedFluentProjectionRuntimeRequest::ValuesWithIds => "values_with_ids",
634 PreparedFluentProjectionRuntimeRequest::TerminalValue { .. } => "terminal_value",
635 }
636 }
637}
638
639#[must_use]
641pub const fn count() -> AggregateExpr {
642 AggregateExpr::new(AggregateKind::Count, None)
643}
644
645#[must_use]
647pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
648 AggregateExpr::new(AggregateKind::Count, Some(field.as_ref().to_string()))
649}
650
651#[must_use]
653pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
654 AggregateExpr::new(AggregateKind::Sum, Some(field.as_ref().to_string()))
655}
656
657#[must_use]
659pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
660 AggregateExpr::new(AggregateKind::Avg, Some(field.as_ref().to_string()))
661}
662
663#[must_use]
665pub const fn exists() -> AggregateExpr {
666 AggregateExpr::new(AggregateKind::Exists, None)
667}
668
669#[must_use]
671pub const fn first() -> AggregateExpr {
672 AggregateExpr::new(AggregateKind::First, None)
673}
674
675#[must_use]
677pub const fn last() -> AggregateExpr {
678 AggregateExpr::new(AggregateKind::Last, None)
679}
680
681#[must_use]
683pub const fn min() -> AggregateExpr {
684 AggregateExpr::new(AggregateKind::Min, None)
685}
686
687#[must_use]
689pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
690 AggregateExpr::new(AggregateKind::Min, Some(field.as_ref().to_string()))
691}
692
693#[must_use]
695pub const fn max() -> AggregateExpr {
696 AggregateExpr::new(AggregateKind::Max, None)
697}
698
699#[must_use]
701pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
702 AggregateExpr::new(AggregateKind::Max, Some(field.as_ref().to_string()))
703}
704
705#[cfg(test)]
710mod tests {
711 use crate::db::query::{
712 builder::{
713 PreparedFluentExistingRowsTerminalRuntimeRequest,
714 PreparedFluentExistingRowsTerminalStrategy, PreparedFluentNumericFieldRuntimeRequest,
715 PreparedFluentNumericFieldStrategy, PreparedFluentOrderSensitiveTerminalRuntimeRequest,
716 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
717 PreparedFluentProjectionStrategy,
718 },
719 plan::{AggregateKind, FieldSlot},
720 };
721
722 #[test]
723 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
724 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
725 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
726
727 assert_eq!(
728 strategy.aggregate_kind(),
729 AggregateKind::Sum,
730 "sum(distinct field) should preserve SUM aggregate kind",
731 );
732 assert_eq!(
733 strategy.projected_field(),
734 Some("rank"),
735 "sum(distinct field) should preserve projected field labels",
736 );
737 assert!(
738 strategy.aggregate().is_distinct(),
739 "sum(distinct field) should preserve DISTINCT aggregate shape",
740 );
741 assert_eq!(
742 strategy.target_field(),
743 &rank_slot,
744 "sum(distinct field) should preserve the resolved planner field slot",
745 );
746 assert_eq!(
747 strategy.runtime_request(),
748 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
749 "sum(distinct field) should project the numeric DISTINCT runtime request",
750 );
751 }
752
753 #[test]
754 fn prepared_fluent_existing_rows_strategy_count_preserves_runtime_shape() {
755 let strategy = PreparedFluentExistingRowsTerminalStrategy::count_rows();
756
757 assert_eq!(
758 strategy.aggregate().kind(),
759 AggregateKind::Count,
760 "count() should preserve the explain-visible aggregate kind",
761 );
762 assert_eq!(
763 strategy.runtime_request(),
764 &PreparedFluentExistingRowsTerminalRuntimeRequest::CountRows,
765 "count() should project the existing-rows count runtime request",
766 );
767 }
768
769 #[test]
770 fn prepared_fluent_existing_rows_strategy_exists_preserves_runtime_shape() {
771 let strategy = PreparedFluentExistingRowsTerminalStrategy::exists_rows();
772
773 assert_eq!(
774 strategy.aggregate().kind(),
775 AggregateKind::Exists,
776 "exists() should preserve the explain-visible aggregate kind",
777 );
778 assert_eq!(
779 strategy.runtime_request(),
780 &PreparedFluentExistingRowsTerminalRuntimeRequest::ExistsRows,
781 "exists() should project the existing-rows exists runtime request",
782 );
783 }
784
785 #[test]
786 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
787 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
788 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
789
790 assert_eq!(
791 strategy.aggregate_kind(),
792 AggregateKind::Avg,
793 "avg(field) should preserve AVG aggregate kind",
794 );
795 assert_eq!(
796 strategy.projected_field(),
797 Some("rank"),
798 "avg(field) should preserve projected field labels",
799 );
800 assert!(
801 !strategy.aggregate().is_distinct(),
802 "avg(field) should stay non-distinct unless requested explicitly",
803 );
804 assert_eq!(
805 strategy.target_field(),
806 &rank_slot,
807 "avg(field) should preserve the resolved planner field slot",
808 );
809 assert_eq!(
810 strategy.runtime_request(),
811 PreparedFluentNumericFieldRuntimeRequest::Avg,
812 "avg(field) should project the numeric AVG runtime request",
813 );
814 }
815
816 #[test]
817 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
818 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
819
820 assert_eq!(
821 strategy.explain_aggregate().map(super::AggregateExpr::kind),
822 Some(AggregateKind::First),
823 "first() should preserve the explain-visible aggregate kind",
824 );
825 assert_eq!(
826 strategy.runtime_request(),
827 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
828 kind: AggregateKind::First,
829 },
830 "first() should project the response-order runtime request",
831 );
832 }
833
834 #[test]
835 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
836 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
837 let strategy =
838 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
839
840 assert_eq!(
841 strategy.explain_aggregate(),
842 None,
843 "nth_by(field, nth) should stay off the current explain aggregate surface",
844 );
845 assert_eq!(
846 strategy.runtime_request(),
847 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
848 target_field: rank_slot,
849 nth: 2,
850 },
851 "nth_by(field, nth) should preserve the resolved field-order runtime request",
852 );
853 }
854
855 #[test]
856 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
857 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
858 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
859
860 assert_eq!(
861 strategy.target_field(),
862 &rank_slot,
863 "count_distinct_by(field) should preserve the resolved planner field slot",
864 );
865 assert_eq!(
866 strategy.runtime_request(),
867 PreparedFluentProjectionRuntimeRequest::CountDistinct,
868 "count_distinct_by(field) should project the distinct-count runtime request",
869 );
870 }
871
872 #[test]
873 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
874 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
875 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
876
877 assert_eq!(
878 strategy.target_field(),
879 &rank_slot,
880 "last_value_by(field) should preserve the resolved planner field slot",
881 );
882 assert_eq!(
883 strategy.runtime_request(),
884 PreparedFluentProjectionRuntimeRequest::TerminalValue {
885 terminal_kind: AggregateKind::Last,
886 },
887 "last_value_by(field) should project the terminal-value runtime request",
888 );
889 }
890}