Skip to main content

icydb_core/db/query/plan/
model.rs

1//! Module: query::plan::model
2//! Responsibility: pure logical query-plan data contracts.
3//! Does not own: constructors, plan assembly, or semantic interpretation.
4//! Boundary: data-only types shared by plan builder/semantics/validation layers.
5
6use crate::{
7    db::{
8        cursor::ContinuationSignature,
9        direction::Direction,
10        predicate::{MissingRowPolicy, Predicate},
11        query::{
12            builder::scalar_projection::render_scalar_projection_expr_sql_label,
13            plan::{
14                expr::{Expr, FieldId, normalize_bool_expr},
15                order_contract::DeterministicSecondaryOrderContract,
16                semantics::LogicalPushdownEligibility,
17            },
18        },
19    },
20    model::field::FieldKind,
21};
22
23///
24/// QueryMode
25///
26/// Discriminates load vs delete intent at planning time.
27/// Encodes mode-specific fields so invalid states are unrepresentable.
28/// Mode checks are explicit and stable at execution time.
29///
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum QueryMode {
33    Load(LoadSpec),
34    Delete(DeleteSpec),
35}
36
37///
38/// LoadSpec
39///
40/// Mode-specific fields for load intents.
41/// Encodes pagination without leaking into delete intents.
42///
43#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
44pub struct LoadSpec {
45    pub(crate) limit: Option<u32>,
46    pub(crate) offset: u32,
47}
48
49impl LoadSpec {
50    /// Return optional row-limit bound for this load-mode spec.
51    #[must_use]
52    pub const fn limit(&self) -> Option<u32> {
53        self.limit
54    }
55
56    /// Return zero-based pagination offset for this load-mode spec.
57    #[must_use]
58    pub const fn offset(&self) -> u32 {
59        self.offset
60    }
61}
62
63///
64/// DeleteSpec
65///
66/// Mode-specific fields for delete intents.
67/// Encodes delete limits without leaking into load intents.
68///
69
70#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
71pub struct DeleteSpec {
72    pub(crate) limit: Option<u32>,
73    pub(crate) offset: u32,
74}
75
76impl DeleteSpec {
77    /// Return optional row-limit bound for this delete-mode spec.
78    #[must_use]
79    pub const fn limit(&self) -> Option<u32> {
80        self.limit
81    }
82
83    /// Return zero-based ordered delete offset for this delete-mode spec.
84    #[must_use]
85    pub const fn offset(&self) -> u32 {
86        self.offset
87    }
88}
89
90///
91/// OrderDirection
92/// Executor-facing ordering direction (applied after filtering).
93///
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum OrderDirection {
96    Asc,
97    Desc,
98}
99
100///
101/// OrderTerm
102///
103/// Planner-owned canonical ORDER BY term contract.
104/// Carries one semantic expression plus direction so downstream validation and
105/// execution stay expression-first, with rendered labels derived only at
106/// diagnostic, explain, and hashing edges.
107///
108
109#[derive(Clone, Eq, PartialEq)]
110pub(crate) struct OrderTerm {
111    pub(crate) expr: Expr,
112    pub(crate) direction: OrderDirection,
113}
114
115impl OrderTerm {
116    /// Construct one planner-owned ORDER BY term from one semantic expression.
117    #[must_use]
118    pub(crate) const fn new(expr: Expr, direction: OrderDirection) -> Self {
119        Self { expr, direction }
120    }
121
122    /// Construct one direct field ORDER BY term.
123    #[must_use]
124    pub(crate) fn field(field: impl Into<String>, direction: OrderDirection) -> Self {
125        Self::new(Expr::Field(FieldId::new(field.into())), direction)
126    }
127
128    /// Borrow the semantic ORDER BY expression.
129    #[must_use]
130    pub(crate) const fn expr(&self) -> &Expr {
131        &self.expr
132    }
133
134    /// Return the direct field name when this ORDER BY term is field-backed.
135    #[must_use]
136    pub(crate) const fn direct_field(&self) -> Option<&str> {
137        let Expr::Field(field) = &self.expr else {
138            return None;
139        };
140
141        Some(field.as_str())
142    }
143
144    /// Render the stable ORDER BY display label for diagnostics and hashing.
145    #[must_use]
146    pub(crate) fn rendered_label(&self) -> String {
147        render_scalar_projection_expr_sql_label(&self.expr)
148    }
149
150    /// Return the executor-facing direction for this ORDER BY term.
151    #[must_use]
152    pub(crate) const fn direction(&self) -> OrderDirection {
153        self.direction
154    }
155}
156
157impl std::fmt::Debug for OrderTerm {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("OrderTerm")
160            .field("label", &self.rendered_label())
161            .field("expr", &self.expr)
162            .field("direction", &self.direction)
163            .finish()
164    }
165}
166
167impl PartialEq<(String, OrderDirection)> for OrderTerm {
168    fn eq(&self, other: &(String, OrderDirection)) -> bool {
169        self.rendered_label() == other.0 && self.direction == other.1
170    }
171}
172
173impl PartialEq<OrderTerm> for (String, OrderDirection) {
174    fn eq(&self, other: &OrderTerm) -> bool {
175        self.0 == other.rendered_label() && self.1 == other.direction
176    }
177}
178
179/// Render one planner-owned scalar filter expression label for explain and
180/// diagnostics surfaces.
181#[must_use]
182pub(in crate::db) fn render_scalar_filter_expr_sql_label(expr: &Expr) -> String {
183    render_scalar_projection_expr_sql_label(&normalize_bool_expr(expr.clone()))
184}
185
186///
187/// OrderSpec
188///
189/// Executor-facing ordering specification.
190/// Carries the canonical ordered term list after planner expression lowering.
191///
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub(crate) struct OrderSpec {
194    pub(crate) fields: Vec<OrderTerm>,
195}
196
197///
198/// DeleteLimitSpec
199/// Executor-facing ordered delete window.
200///
201
202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub(crate) struct DeleteLimitSpec {
204    pub(crate) limit: Option<u32>,
205    pub(crate) offset: u32,
206}
207
208///
209/// DistinctExecutionStrategy
210///
211/// Planner-owned scalar DISTINCT execution strategy.
212/// This is execution-mechanics only and must not be used for semantic
213/// admissibility decisions.
214///
215
216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
217pub(crate) enum DistinctExecutionStrategy {
218    None,
219    PreOrdered,
220    HashMaterialize,
221}
222
223impl DistinctExecutionStrategy {
224    /// Return true when scalar DISTINCT execution is enabled.
225    #[must_use]
226    pub(crate) const fn is_enabled(self) -> bool {
227        !matches!(self, Self::None)
228    }
229}
230
231///
232/// PlannerRouteProfile
233///
234/// Planner-projected route profile consumed by executor route planning.
235/// Carries planner-owned continuation policy plus deterministic order/pushdown
236/// contracts that route/load layers must honor without recomputing order shape.
237///
238
239#[derive(Clone, Debug, Eq, PartialEq)]
240pub(in crate::db) struct PlannerRouteProfile {
241    continuation_policy: ContinuationPolicy,
242    logical_pushdown_eligibility: LogicalPushdownEligibility,
243    secondary_order_contract: Option<DeterministicSecondaryOrderContract>,
244}
245
246impl PlannerRouteProfile {
247    /// Construct one planner-projected route profile.
248    #[must_use]
249    pub(in crate::db) const fn new(
250        continuation_policy: ContinuationPolicy,
251        logical_pushdown_eligibility: LogicalPushdownEligibility,
252        secondary_order_contract: Option<DeterministicSecondaryOrderContract>,
253    ) -> Self {
254        Self {
255            continuation_policy,
256            logical_pushdown_eligibility,
257            secondary_order_contract,
258        }
259    }
260
261    /// Construct one fail-closed route profile for manually assembled plans
262    /// that have not yet been finalized against model authority.
263    #[must_use]
264    pub(in crate::db) const fn seeded_unfinalized(is_grouped: bool) -> Self {
265        Self {
266            continuation_policy: ContinuationPolicy::new(true, true, !is_grouped),
267            logical_pushdown_eligibility: LogicalPushdownEligibility::new(false, is_grouped, false),
268            secondary_order_contract: None,
269        }
270    }
271
272    /// Borrow planner-projected continuation policy contract.
273    #[must_use]
274    pub(in crate::db) const fn continuation_policy(&self) -> &ContinuationPolicy {
275        &self.continuation_policy
276    }
277
278    /// Borrow planner-owned logical pushdown eligibility contract.
279    #[must_use]
280    pub(in crate::db) const fn logical_pushdown_eligibility(&self) -> LogicalPushdownEligibility {
281        self.logical_pushdown_eligibility
282    }
283
284    /// Borrow the planner-owned deterministic secondary-order contract, if one exists.
285    #[must_use]
286    pub(in crate::db) const fn secondary_order_contract(
287        &self,
288    ) -> Option<&DeterministicSecondaryOrderContract> {
289        self.secondary_order_contract.as_ref()
290    }
291}
292
293///
294/// ContinuationPolicy
295///
296/// Planner-projected continuation contract carried into route/executor layers.
297/// This contract captures static continuation invariants and must not be
298/// rederived by route/load orchestration code.
299///
300
301#[derive(Clone, Copy, Debug, Eq, PartialEq)]
302pub(in crate::db) struct ContinuationPolicy {
303    requires_anchor: bool,
304    requires_strict_advance: bool,
305    is_grouped_safe: bool,
306}
307
308impl ContinuationPolicy {
309    /// Construct one planner-projected continuation policy contract.
310    #[must_use]
311    pub(in crate::db) const fn new(
312        requires_anchor: bool,
313        requires_strict_advance: bool,
314        is_grouped_safe: bool,
315    ) -> Self {
316        Self {
317            requires_anchor,
318            requires_strict_advance,
319            is_grouped_safe,
320        }
321    }
322
323    /// Return true when continuation resume paths require an anchor boundary.
324    #[must_use]
325    pub(in crate::db) const fn requires_anchor(self) -> bool {
326        self.requires_anchor
327    }
328
329    /// Return true when continuation resume paths require strict advancement.
330    #[must_use]
331    pub(in crate::db) const fn requires_strict_advance(self) -> bool {
332        self.requires_strict_advance
333    }
334
335    /// Return true when grouped continuation usage is semantically safe.
336    #[must_use]
337    pub(in crate::db) const fn is_grouped_safe(self) -> bool {
338        self.is_grouped_safe
339    }
340}
341
342///
343/// ExecutionShapeSignature
344///
345/// Immutable planner-projected semantic shape signature contract.
346/// Continuation transport encodes this contract; route/load consume it as a
347/// read-only execution identity boundary without re-deriving semantics.
348///
349
350#[derive(Clone, Copy, Debug, Eq, PartialEq)]
351pub(in crate::db) struct ExecutionShapeSignature {
352    continuation_signature: ContinuationSignature,
353}
354
355impl ExecutionShapeSignature {
356    /// Construct one immutable execution-shape signature contract.
357    #[must_use]
358    pub(in crate::db) const fn new(continuation_signature: ContinuationSignature) -> Self {
359        Self {
360            continuation_signature,
361        }
362    }
363
364    /// Borrow the canonical continuation signature for this execution shape.
365    #[must_use]
366    pub(in crate::db) const fn continuation_signature(self) -> ContinuationSignature {
367        self.continuation_signature
368    }
369}
370
371///
372/// PageSpec
373/// Executor-facing pagination specification.
374///
375
376#[derive(Clone, Debug, Eq, PartialEq)]
377pub(crate) struct PageSpec {
378    pub(crate) limit: Option<u32>,
379    pub(crate) offset: u32,
380}
381
382///
383/// AggregateKind
384///
385/// Canonical aggregate terminal taxonomy owned by query planning.
386/// All layers (query, explain, fingerprint, executor) must interpret aggregate
387/// terminal semantics through this single enum authority.
388/// Executor must derive traversal and fold direction exclusively from this enum.
389///
390
391#[derive(Clone, Copy, Debug, Eq, PartialEq)]
392pub enum AggregateKind {
393    Count,
394    Sum,
395    Avg,
396    Exists,
397    Min,
398    Max,
399    First,
400    Last,
401}
402
403///
404/// GlobalDistinctAggregateKind
405///
406/// Canonical support-family for grouped global-DISTINCT field aggregates.
407/// This keeps the admitted `COUNT | SUM | AVG` family on one planner-owned
408/// support surface instead of repeating that support set across grouped
409/// semantics and grouped executor handoff.
410///
411
412#[derive(Clone, Copy, Debug, Eq, PartialEq)]
413pub(crate) enum GlobalDistinctAggregateKind {
414    Count,
415    Sum,
416    Avg,
417}
418
419impl GlobalDistinctAggregateKind {}
420
421///
422/// GroupedPlanAggregateFamily
423///
424/// Planner-owned grouped aggregate-family profile.
425/// This is intentionally coarse and execution-oriented: it captures which
426/// grouped aggregate family the planner admitted so runtime can select grouped
427/// execution paths without rebuilding family policy from raw aggregate
428/// expressions again.
429///
430
431#[derive(Clone, Copy, Debug, Eq, PartialEq)]
432pub(crate) enum GroupedPlanAggregateFamily {
433    CountRowsOnly,
434    FieldTargetRows,
435    GenericRows,
436}
437
438impl GroupedPlanAggregateFamily {
439    /// Return the stable planner-owned aggregate-family code.
440    #[must_use]
441    pub(crate) const fn code(self) -> &'static str {
442        match self {
443            Self::CountRowsOnly => "count_rows_only",
444            Self::FieldTargetRows => "field_target_rows",
445            Self::GenericRows => "generic_rows",
446        }
447    }
448}
449
450impl AggregateKind {
451    /// Return the canonical uppercase SQL/render label for this aggregate kind.
452    #[must_use]
453    pub(in crate::db) const fn sql_label(self) -> &'static str {
454        match self {
455            Self::Count => "COUNT",
456            Self::Sum => "SUM",
457            Self::Avg => "AVG",
458            Self::Exists => "EXISTS",
459            Self::First => "FIRST",
460            Self::Last => "LAST",
461            Self::Min => "MIN",
462            Self::Max => "MAX",
463        }
464    }
465
466    /// Return whether this terminal kind is `COUNT`.
467    #[must_use]
468    pub(crate) const fn is_count(self) -> bool {
469        matches!(self, Self::Count)
470    }
471
472    /// Return whether this terminal kind belongs to the SUM/AVG numeric fold family.
473    #[must_use]
474    pub(in crate::db) const fn is_sum(self) -> bool {
475        matches!(self, Self::Sum | Self::Avg)
476    }
477
478    /// Return whether this terminal kind belongs to the extrema family.
479    #[must_use]
480    pub(in crate::db) const fn is_extrema(self) -> bool {
481        matches!(self, Self::Min | Self::Max)
482    }
483
484    /// Return whether this kind supports one grouped or global field target.
485    #[must_use]
486    pub(in crate::db) const fn supports_field_target_v1(self) -> bool {
487        matches!(
488            self,
489            Self::Count | Self::Sum | Self::Avg | Self::Min | Self::Max
490        )
491    }
492
493    /// Return whether reducer updates for this kind require a decoded id payload.
494    #[must_use]
495    pub(in crate::db) const fn requires_decoded_id(self) -> bool {
496        !matches!(self, Self::Count | Self::Sum | Self::Avg | Self::Exists)
497    }
498
499    /// Return whether grouped aggregate DISTINCT is supported for this kind.
500    #[must_use]
501    pub(in crate::db) const fn supports_grouped_distinct_v1(self) -> bool {
502        matches!(
503            self,
504            Self::Count | Self::Min | Self::Max | Self::Sum | Self::Avg
505        )
506    }
507
508    /// Return whether grouped DISTINCT uses value-based deduplication for
509    /// this kind instead of key-only tracking.
510    #[must_use]
511    pub(in crate::db) const fn uses_grouped_distinct_value_dedup_v1(self) -> bool {
512        matches!(self, Self::Count | Self::Sum | Self::Avg)
513    }
514
515    /// Return the stable aggregate discriminant used by projection and
516    /// aggregate fingerprint hashing.
517    #[must_use]
518    pub(in crate::db::query) const fn fingerprint_tag(self) -> u8 {
519        match self {
520            Self::Count => 0x01,
521            Self::Sum => 0x02,
522            Self::Exists => 0x03,
523            Self::Min => 0x04,
524            Self::Max => 0x05,
525            Self::First => 0x06,
526            Self::Last => 0x07,
527            Self::Avg => 0x08,
528        }
529    }
530
531    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
532    #[must_use]
533    pub(in crate::db) const fn global_distinct_kind(self) -> Option<GlobalDistinctAggregateKind> {
534        match self {
535            Self::Count => Some(GlobalDistinctAggregateKind::Count),
536            Self::Sum => Some(GlobalDistinctAggregateKind::Sum),
537            Self::Avg => Some(GlobalDistinctAggregateKind::Avg),
538            Self::Exists | Self::Min | Self::Max | Self::First | Self::Last => None,
539        }
540    }
541
542    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
543    #[must_use]
544    pub(in crate::db) const fn supports_global_distinct_without_group_keys(self) -> bool {
545        self.global_distinct_kind().is_some()
546    }
547
548    /// Return whether this kind is the dedicated grouped `COUNT(*)` terminal family.
549    #[must_use]
550    pub(in crate::db) const fn is_count_rows_only_group_terminal(
551        self,
552        has_target_field: bool,
553        distinct: bool,
554    ) -> bool {
555        matches!(self, Self::Count) && !has_target_field && !distinct
556    }
557
558    /// Return the planner-owned grouped aggregate-family profile for one aggregate shape.
559    #[must_use]
560    pub(in crate::db) const fn grouped_plan_family(
561        self,
562        has_target_field: bool,
563    ) -> GroupedPlanAggregateFamily {
564        if has_target_field && self.supports_field_target_v1() {
565            GroupedPlanAggregateFamily::FieldTargetRows
566        } else {
567            GroupedPlanAggregateFamily::GenericRows
568        }
569    }
570
571    /// Return whether this grouped aggregate shape supports ordered grouped streaming.
572    #[must_use]
573    pub(in crate::db) const fn supports_grouped_streaming_v1(
574        self,
575        has_target_field: bool,
576        distinct: bool,
577    ) -> bool {
578        if self.supports_field_target_v1() {
579            return !distinct && (self.is_count() || has_target_field);
580        }
581
582        !has_target_field && (!distinct || self.supports_grouped_distinct_v1())
583    }
584
585    /// Return the canonical extrema traversal direction for this kind.
586    #[must_use]
587    pub(crate) const fn extrema_direction(self) -> Option<Direction> {
588        match self {
589            Self::Min => Some(Direction::Asc),
590            Self::Max => Some(Direction::Desc),
591            Self::Count | Self::Sum | Self::Avg | Self::Exists | Self::First | Self::Last => None,
592        }
593    }
594
595    /// Return the canonical materialized fold direction for this kind.
596    #[must_use]
597    pub(crate) const fn materialized_fold_direction(self) -> Direction {
598        match self {
599            Self::Min => Direction::Desc,
600            Self::Count
601            | Self::Sum
602            | Self::Avg
603            | Self::Exists
604            | Self::Max
605            | Self::First
606            | Self::Last => Direction::Asc,
607        }
608    }
609
610    /// Return true when this kind can use bounded aggregate probe hints.
611    #[must_use]
612    pub(crate) const fn supports_bounded_probe_hint(self) -> bool {
613        !self.is_count() && !self.is_sum()
614    }
615
616    /// Derive a bounded aggregate probe fetch hint for this kind.
617    #[must_use]
618    pub(crate) fn bounded_probe_fetch_hint(
619        self,
620        direction: Direction,
621        offset: usize,
622        page_limit: Option<usize>,
623    ) -> Option<usize> {
624        match self {
625            Self::Exists | Self::First => Some(offset.saturating_add(1)),
626            Self::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
627            Self::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
628            Self::Last => page_limit.map(|limit| offset.saturating_add(limit)),
629            Self::Count | Self::Sum | Self::Avg | Self::Min | Self::Max => None,
630        }
631    }
632
633    /// Return the explain projection mode label for this kind and projection surface.
634    #[must_use]
635    pub(in crate::db) const fn explain_projection_mode_label(
636        self,
637        has_projected_field: bool,
638        covering_projection: bool,
639    ) -> &'static str {
640        if has_projected_field {
641            if covering_projection {
642                "field_idx"
643            } else {
644                "field_mat"
645            }
646        } else if matches!(self, Self::Min | Self::Max | Self::First | Self::Last) {
647            "entity_term"
648        } else {
649            "scalar_agg"
650        }
651    }
652
653    /// Return whether this terminal kind can remain covering on existing-row plans.
654    #[must_use]
655    pub(in crate::db) const fn supports_covering_existing_rows_terminal(self) -> bool {
656        matches!(self, Self::Count | Self::Exists)
657    }
658}
659
660///
661/// GroupAggregateSpec
662///
663/// One grouped aggregate terminal specification declared at query-plan time.
664/// `input_expr` is the single semantic source for grouped aggregate identity.
665/// Field-target behavior is derived from plain `Expr::Field` leaves so grouped
666/// semantics, explain, fingerprinting, and runtime do not carry a second
667/// compatibility shape beside the canonical aggregate input expression.
668///
669
670#[derive(Clone, Debug)]
671pub(crate) struct GroupAggregateSpec {
672    pub(crate) kind: AggregateKind,
673    #[cfg(test)]
674    #[cfg(test)]
675    pub(crate) target_field: Option<String>,
676    pub(crate) input_expr: Option<Box<Expr>>,
677    pub(crate) filter_expr: Option<Box<Expr>>,
678    pub(crate) distinct: bool,
679}
680
681impl PartialEq for GroupAggregateSpec {
682    fn eq(&self, other: &Self) -> bool {
683        self.kind == other.kind
684            && self.input_expr == other.input_expr
685            && self.filter_expr == other.filter_expr
686            && self.distinct == other.distinct
687    }
688}
689
690impl Eq for GroupAggregateSpec {}
691
692impl GroupedPlanAggregateFamily {
693    /// Derive the grouped aggregate-family profile from one planner aggregate list.
694    #[must_use]
695    pub(in crate::db) fn from_grouped_aggregates(aggregates: &[GroupAggregateSpec]) -> Self {
696        if matches!(aggregates, [aggregate] if aggregate.kind().is_count_rows_only_group_terminal(
697            aggregate.target_field().is_some(),
698            aggregate.distinct(),
699        )) {
700            return Self::CountRowsOnly;
701        }
702
703        if aggregates.iter().all(|aggregate| {
704            aggregate
705                .kind()
706                .grouped_plan_family(aggregate.target_field().is_some())
707                == Self::FieldTargetRows
708        }) {
709            return Self::FieldTargetRows;
710        }
711
712        Self::GenericRows
713    }
714}
715
716///
717/// FieldSlot
718///
719/// Canonical resolved field reference used by logical planning.
720/// `index` is the stable slot in `EntityModel::fields`; `field` is retained
721/// for diagnostics and explain surfaces.
722/// `kind` freezes planner-resolved field metadata so executor boundaries do
723/// not need to reopen `EntityModel` just to recover type/capability shape.
724///
725
726#[derive(Clone, Debug)]
727pub(crate) struct FieldSlot {
728    pub(crate) index: usize,
729    pub(crate) field: String,
730    pub(crate) kind: Option<FieldKind>,
731}
732
733impl PartialEq for FieldSlot {
734    fn eq(&self, other: &Self) -> bool {
735        self.index == other.index && self.field == other.field
736    }
737}
738
739impl Eq for FieldSlot {}
740
741///
742/// GroupedExecutionConfig
743///
744/// Declarative grouped-execution budget policy selected by query planning.
745/// This remains planner-owned input; executor policy bridges may still apply
746/// defaults and enforcement strategy at runtime boundaries.
747///
748
749#[derive(Clone, Copy, Debug, Eq, PartialEq)]
750pub(crate) struct GroupedExecutionConfig {
751    pub(crate) max_groups: u64,
752    pub(crate) max_group_bytes: u64,
753}
754
755///
756/// GroupSpec
757///
758/// Declarative GROUP BY stage contract attached to a validated base plan.
759/// This wrapper is intentionally semantic-only; field-slot resolution and
760/// execution-mode derivation remain executor-owned boundaries.
761///
762
763#[derive(Clone, Debug, Eq, PartialEq)]
764pub(crate) struct GroupSpec {
765    pub(crate) group_fields: Vec<FieldSlot>,
766    pub(crate) aggregates: Vec<GroupAggregateSpec>,
767    pub(crate) execution: GroupedExecutionConfig,
768}
769
770///
771/// ScalarPlan
772///
773/// Pure scalar logical query intent produced by the planner.
774///
775/// A `ScalarPlan` represents the access-independent query semantics:
776/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
777/// and read-consistency mode.
778///
779/// Design notes:
780/// - Predicates are applied *after* data access
781/// - Ordering is applied after filtering
782/// - Pagination is applied after ordering (load only)
783/// - Delete limits are applied after ordering (delete only)
784/// - Missing-row policy is explicit and must not depend on access strategy
785///
786/// This struct is the logical compiler stage output and intentionally excludes
787/// access-path details.
788///
789
790#[derive(Clone, Debug, Eq, PartialEq)]
791pub(crate) struct ScalarPlan {
792    /// Load vs delete intent.
793    pub(crate) mode: QueryMode,
794
795    /// Optional planner-owned scalar filter expression.
796    pub(crate) filter_expr: Option<Expr>,
797
798    /// Optional residual predicate applied after access.
799    pub(crate) predicate: Option<Predicate>,
800
801    /// Optional ordering specification.
802    pub(crate) order: Option<OrderSpec>,
803
804    /// Optional distinct semantics over ordered rows.
805    pub(crate) distinct: bool,
806
807    /// Optional ordered delete window (delete intents only).
808    pub(crate) delete_limit: Option<DeleteLimitSpec>,
809
810    /// Optional pagination specification.
811    pub(crate) page: Option<PageSpec>,
812
813    /// Missing-row policy for execution.
814    pub(crate) consistency: MissingRowPolicy,
815}
816
817///
818/// GroupPlan
819///
820/// Pure grouped logical intent emitted by grouped planning.
821/// Group metadata is carried through one canonical `GroupSpec` contract.
822///
823
824#[derive(Clone, Debug, Eq, PartialEq)]
825pub(crate) struct GroupPlan {
826    pub(crate) scalar: ScalarPlan,
827    pub(crate) group: GroupSpec,
828    pub(crate) having_expr: Option<Expr>,
829}
830
831///
832/// LogicalPlan
833///
834/// Exclusive logical query intent emitted by planning.
835/// Scalar and grouped semantics are distinct variants by construction.
836///
837
838// Logical plans keep scalar and grouped shapes inline because planner/executor handoff
839// passes these variants by ownership and boxing would widen that boundary for little benefit.
840#[derive(Clone, Debug, Eq, PartialEq)]
841pub(crate) enum LogicalPlan {
842    Scalar(ScalarPlan),
843    Grouped(GroupPlan),
844}