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
101#[derive(Clone, Debug, Eq, PartialEq)]
108pub(crate) enum PreparedFluentScalarTerminalRuntimeRequest {
109 CountRows,
110 ExistsRows,
111 IdTerminal {
112 kind: AggregateKind,
113 },
114 IdBySlot {
115 kind: AggregateKind,
116 target_field: FieldSlot,
117 },
118}
119
120#[derive(Clone, Debug, Eq, PartialEq)]
131pub(crate) struct PreparedFluentScalarTerminalStrategy {
132 aggregate: AggregateExpr,
133 runtime_request: PreparedFluentScalarTerminalRuntimeRequest,
134}
135
136impl PreparedFluentScalarTerminalStrategy {
137 #[must_use]
139 pub(crate) const fn count_rows() -> Self {
140 Self {
141 aggregate: count(),
142 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::CountRows,
143 }
144 }
145
146 #[must_use]
148 pub(crate) const fn exists_rows() -> Self {
149 Self {
150 aggregate: exists(),
151 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::ExistsRows,
152 }
153 }
154
155 #[must_use]
157 pub(crate) fn id_terminal(kind: AggregateKind) -> Self {
158 Self {
159 aggregate: AggregateExpr::terminal_for_kind(kind),
160 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdTerminal { kind },
161 }
162 }
163
164 #[must_use]
167 pub(crate) fn id_by_slot(kind: AggregateKind, target_field: FieldSlot) -> Self {
168 Self {
169 aggregate: AggregateExpr::field_target_extrema_for_kind(kind, target_field.field()),
170 runtime_request: PreparedFluentScalarTerminalRuntimeRequest::IdBySlot {
171 kind,
172 target_field,
173 },
174 }
175 }
176
177 #[must_use]
180 pub(crate) const fn aggregate(&self) -> &AggregateExpr {
181 &self.aggregate
182 }
183
184 #[must_use]
187 pub(crate) const fn runtime_request(&self) -> &PreparedFluentScalarTerminalRuntimeRequest {
188 &self.runtime_request
189 }
190}
191
192#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub(crate) enum PreparedFluentNumericFieldRuntimeRequest {
203 Sum,
204 SumDistinct,
205 Avg,
206 AvgDistinct,
207}
208
209#[derive(Clone, Debug, Eq, PartialEq)]
220pub(crate) struct PreparedFluentNumericFieldStrategy {
221 aggregate: AggregateExpr,
222 target_field: FieldSlot,
223 runtime_request: PreparedFluentNumericFieldRuntimeRequest,
224}
225
226impl PreparedFluentNumericFieldStrategy {
227 #[must_use]
229 pub(crate) fn sum_by_slot(target_field: FieldSlot) -> Self {
230 Self {
231 aggregate: sum(target_field.field()),
232 target_field,
233 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Sum,
234 }
235 }
236
237 #[must_use]
239 pub(crate) fn sum_distinct_by_slot(target_field: FieldSlot) -> Self {
240 Self {
241 aggregate: sum(target_field.field()).distinct(),
242 target_field,
243 runtime_request: PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
244 }
245 }
246
247 #[must_use]
249 pub(crate) fn avg_by_slot(target_field: FieldSlot) -> Self {
250 Self {
251 aggregate: avg(target_field.field()),
252 target_field,
253 runtime_request: PreparedFluentNumericFieldRuntimeRequest::Avg,
254 }
255 }
256
257 #[must_use]
259 pub(crate) fn avg_distinct_by_slot(target_field: FieldSlot) -> Self {
260 Self {
261 aggregate: avg(target_field.field()).distinct(),
262 target_field,
263 runtime_request: PreparedFluentNumericFieldRuntimeRequest::AvgDistinct,
264 }
265 }
266
267 #[cfg(test)]
270 #[must_use]
271 pub(crate) const fn aggregate(&self) -> &AggregateExpr {
272 &self.aggregate
273 }
274
275 #[cfg(test)]
278 #[must_use]
279 pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
280 self.aggregate.kind()
281 }
282
283 #[cfg(test)]
286 #[must_use]
287 pub(crate) fn projected_field(&self) -> Option<&str> {
288 self.aggregate.target_field()
289 }
290
291 #[must_use]
294 pub(crate) const fn target_field(&self) -> &FieldSlot {
295 &self.target_field
296 }
297
298 #[must_use]
301 pub(crate) const fn runtime_request(&self) -> PreparedFluentNumericFieldRuntimeRequest {
302 self.runtime_request
303 }
304}
305
306#[derive(Clone, Debug, Eq, PartialEq)]
316pub(crate) enum PreparedFluentOrderSensitiveTerminalRuntimeRequest {
317 ResponseOrder { kind: AggregateKind },
318 NthBySlot { target_field: FieldSlot, nth: usize },
319 MedianBySlot { target_field: FieldSlot },
320 MinMaxBySlot { target_field: FieldSlot },
321}
322
323#[derive(Clone, Debug, Eq, PartialEq)]
334pub(crate) struct PreparedFluentOrderSensitiveTerminalStrategy {
335 explain_aggregate: Option<AggregateExpr>,
336 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest,
337}
338
339impl PreparedFluentOrderSensitiveTerminalStrategy {
340 #[must_use]
342 pub(crate) const fn first() -> Self {
343 Self {
344 explain_aggregate: Some(first()),
345 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
346 kind: AggregateKind::First,
347 },
348 }
349 }
350
351 #[must_use]
353 pub(crate) const fn last() -> Self {
354 Self {
355 explain_aggregate: Some(last()),
356 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
357 kind: AggregateKind::Last,
358 },
359 }
360 }
361
362 #[must_use]
364 pub(crate) const fn nth_by_slot(target_field: FieldSlot, nth: usize) -> Self {
365 Self {
366 explain_aggregate: None,
367 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
368 target_field,
369 nth,
370 },
371 }
372 }
373
374 #[must_use]
376 pub(crate) const fn median_by_slot(target_field: FieldSlot) -> Self {
377 Self {
378 explain_aggregate: None,
379 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MedianBySlot {
380 target_field,
381 },
382 }
383 }
384
385 #[must_use]
387 pub(crate) const fn min_max_by_slot(target_field: FieldSlot) -> Self {
388 Self {
389 explain_aggregate: None,
390 runtime_request: PreparedFluentOrderSensitiveTerminalRuntimeRequest::MinMaxBySlot {
391 target_field,
392 },
393 }
394 }
395
396 #[must_use]
399 pub(crate) const fn explain_aggregate(&self) -> Option<&AggregateExpr> {
400 self.explain_aggregate.as_ref()
401 }
402
403 #[must_use]
406 pub(crate) const fn runtime_request(
407 &self,
408 ) -> &PreparedFluentOrderSensitiveTerminalRuntimeRequest {
409 &self.runtime_request
410 }
411}
412
413#[derive(Clone, Copy, Debug, Eq, PartialEq)]
423pub(crate) enum PreparedFluentProjectionRuntimeRequest {
424 Values,
425 DistinctValues,
426 CountDistinct,
427 ValuesWithIds,
428 TerminalValue { terminal_kind: AggregateKind },
429}
430
431#[derive(Clone, Debug, Eq, PartialEq)]
442pub(crate) struct PreparedFluentProjectionStrategy {
443 target_field: FieldSlot,
444 runtime_request: PreparedFluentProjectionRuntimeRequest,
445}
446
447impl PreparedFluentProjectionStrategy {
448 #[must_use]
450 pub(crate) const fn values_by_slot(target_field: FieldSlot) -> Self {
451 Self {
452 target_field,
453 runtime_request: PreparedFluentProjectionRuntimeRequest::Values,
454 }
455 }
456
457 #[must_use]
459 pub(crate) const fn distinct_values_by_slot(target_field: FieldSlot) -> Self {
460 Self {
461 target_field,
462 runtime_request: PreparedFluentProjectionRuntimeRequest::DistinctValues,
463 }
464 }
465
466 #[must_use]
468 pub(crate) const fn count_distinct_by_slot(target_field: FieldSlot) -> Self {
469 Self {
470 target_field,
471 runtime_request: PreparedFluentProjectionRuntimeRequest::CountDistinct,
472 }
473 }
474
475 #[must_use]
477 pub(crate) const fn values_by_with_ids_slot(target_field: FieldSlot) -> Self {
478 Self {
479 target_field,
480 runtime_request: PreparedFluentProjectionRuntimeRequest::ValuesWithIds,
481 }
482 }
483
484 #[must_use]
486 pub(crate) const fn first_value_by_slot(target_field: FieldSlot) -> Self {
487 Self {
488 target_field,
489 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
490 terminal_kind: AggregateKind::First,
491 },
492 }
493 }
494
495 #[must_use]
497 pub(crate) const fn last_value_by_slot(target_field: FieldSlot) -> Self {
498 Self {
499 target_field,
500 runtime_request: PreparedFluentProjectionRuntimeRequest::TerminalValue {
501 terminal_kind: AggregateKind::Last,
502 },
503 }
504 }
505
506 #[must_use]
509 pub(crate) const fn target_field(&self) -> &FieldSlot {
510 &self.target_field
511 }
512
513 #[must_use]
516 pub(crate) const fn runtime_request(&self) -> PreparedFluentProjectionRuntimeRequest {
517 self.runtime_request
518 }
519}
520
521#[must_use]
523pub const fn count() -> AggregateExpr {
524 AggregateExpr::new(AggregateKind::Count, None)
525}
526
527#[must_use]
529pub fn count_by(field: impl AsRef<str>) -> AggregateExpr {
530 AggregateExpr::new(AggregateKind::Count, Some(field.as_ref().to_string()))
531}
532
533#[must_use]
535pub fn sum(field: impl AsRef<str>) -> AggregateExpr {
536 AggregateExpr::new(AggregateKind::Sum, Some(field.as_ref().to_string()))
537}
538
539#[must_use]
541pub fn avg(field: impl AsRef<str>) -> AggregateExpr {
542 AggregateExpr::new(AggregateKind::Avg, Some(field.as_ref().to_string()))
543}
544
545#[must_use]
547pub const fn exists() -> AggregateExpr {
548 AggregateExpr::new(AggregateKind::Exists, None)
549}
550
551#[must_use]
553pub const fn first() -> AggregateExpr {
554 AggregateExpr::new(AggregateKind::First, None)
555}
556
557#[must_use]
559pub const fn last() -> AggregateExpr {
560 AggregateExpr::new(AggregateKind::Last, None)
561}
562
563#[must_use]
565pub const fn min() -> AggregateExpr {
566 AggregateExpr::new(AggregateKind::Min, None)
567}
568
569#[must_use]
571pub fn min_by(field: impl AsRef<str>) -> AggregateExpr {
572 AggregateExpr::new(AggregateKind::Min, Some(field.as_ref().to_string()))
573}
574
575#[must_use]
577pub const fn max() -> AggregateExpr {
578 AggregateExpr::new(AggregateKind::Max, None)
579}
580
581#[must_use]
583pub fn max_by(field: impl AsRef<str>) -> AggregateExpr {
584 AggregateExpr::new(AggregateKind::Max, Some(field.as_ref().to_string()))
585}
586
587#[cfg(test)]
592mod tests {
593 use crate::db::query::{
594 builder::{
595 PreparedFluentNumericFieldRuntimeRequest, PreparedFluentNumericFieldStrategy,
596 PreparedFluentOrderSensitiveTerminalRuntimeRequest,
597 PreparedFluentOrderSensitiveTerminalStrategy, PreparedFluentProjectionRuntimeRequest,
598 PreparedFluentProjectionStrategy,
599 },
600 plan::{AggregateKind, FieldSlot},
601 };
602
603 #[test]
604 fn prepared_fluent_numeric_field_strategy_sum_distinct_preserves_runtime_shape() {
605 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
606 let strategy = PreparedFluentNumericFieldStrategy::sum_distinct_by_slot(rank_slot.clone());
607
608 assert_eq!(
609 strategy.aggregate_kind(),
610 AggregateKind::Sum,
611 "sum(distinct field) should preserve SUM aggregate kind",
612 );
613 assert_eq!(
614 strategy.projected_field(),
615 Some("rank"),
616 "sum(distinct field) should preserve projected field labels",
617 );
618 assert!(
619 strategy.aggregate().is_distinct(),
620 "sum(distinct field) should preserve DISTINCT aggregate shape",
621 );
622 assert_eq!(
623 strategy.target_field(),
624 &rank_slot,
625 "sum(distinct field) should preserve the resolved planner field slot",
626 );
627 assert_eq!(
628 strategy.runtime_request(),
629 PreparedFluentNumericFieldRuntimeRequest::SumDistinct,
630 "sum(distinct field) should project the numeric DISTINCT runtime request",
631 );
632 }
633
634 #[test]
635 fn prepared_fluent_numeric_field_strategy_avg_preserves_runtime_shape() {
636 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
637 let strategy = PreparedFluentNumericFieldStrategy::avg_by_slot(rank_slot.clone());
638
639 assert_eq!(
640 strategy.aggregate_kind(),
641 AggregateKind::Avg,
642 "avg(field) should preserve AVG aggregate kind",
643 );
644 assert_eq!(
645 strategy.projected_field(),
646 Some("rank"),
647 "avg(field) should preserve projected field labels",
648 );
649 assert!(
650 !strategy.aggregate().is_distinct(),
651 "avg(field) should stay non-distinct unless requested explicitly",
652 );
653 assert_eq!(
654 strategy.target_field(),
655 &rank_slot,
656 "avg(field) should preserve the resolved planner field slot",
657 );
658 assert_eq!(
659 strategy.runtime_request(),
660 PreparedFluentNumericFieldRuntimeRequest::Avg,
661 "avg(field) should project the numeric AVG runtime request",
662 );
663 }
664
665 #[test]
666 fn prepared_fluent_order_sensitive_strategy_first_preserves_explain_and_runtime_shape() {
667 let strategy = PreparedFluentOrderSensitiveTerminalStrategy::first();
668
669 assert_eq!(
670 strategy.explain_aggregate().map(super::AggregateExpr::kind),
671 Some(AggregateKind::First),
672 "first() should preserve the explain-visible aggregate kind",
673 );
674 assert_eq!(
675 strategy.runtime_request(),
676 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::ResponseOrder {
677 kind: AggregateKind::First,
678 },
679 "first() should project the response-order runtime request",
680 );
681 }
682
683 #[test]
684 fn prepared_fluent_order_sensitive_strategy_nth_preserves_field_order_runtime_shape() {
685 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
686 let strategy =
687 PreparedFluentOrderSensitiveTerminalStrategy::nth_by_slot(rank_slot.clone(), 2);
688
689 assert_eq!(
690 strategy.explain_aggregate(),
691 None,
692 "nth_by(field, nth) should stay off the current explain aggregate surface",
693 );
694 assert_eq!(
695 strategy.runtime_request(),
696 &PreparedFluentOrderSensitiveTerminalRuntimeRequest::NthBySlot {
697 target_field: rank_slot,
698 nth: 2,
699 },
700 "nth_by(field, nth) should preserve the resolved field-order runtime request",
701 );
702 }
703
704 #[test]
705 fn prepared_fluent_projection_strategy_count_distinct_preserves_runtime_shape() {
706 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
707 let strategy = PreparedFluentProjectionStrategy::count_distinct_by_slot(rank_slot.clone());
708
709 assert_eq!(
710 strategy.target_field(),
711 &rank_slot,
712 "count_distinct_by(field) should preserve the resolved planner field slot",
713 );
714 assert_eq!(
715 strategy.runtime_request(),
716 PreparedFluentProjectionRuntimeRequest::CountDistinct,
717 "count_distinct_by(field) should project the distinct-count runtime request",
718 );
719 }
720
721 #[test]
722 fn prepared_fluent_projection_strategy_terminal_value_preserves_runtime_shape() {
723 let rank_slot = FieldSlot::from_parts_for_test(7, "rank");
724 let strategy = PreparedFluentProjectionStrategy::last_value_by_slot(rank_slot.clone());
725
726 assert_eq!(
727 strategy.target_field(),
728 &rank_slot,
729 "last_value_by(field) should preserve the resolved planner field slot",
730 );
731 assert_eq!(
732 strategy.runtime_request(),
733 PreparedFluentProjectionRuntimeRequest::TerminalValue {
734 terminal_kind: AggregateKind::Last,
735 },
736 "last_value_by(field) should project the terminal-value runtime request",
737 );
738 }
739}