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_plan_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(in crate::db) limit: Option<u32>,
46    pub(in crate::db) 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(in crate::db) limit: Option<u32>,
73    pub(in crate::db) 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(in crate::db) struct OrderTerm {
111    pub(in crate::db) expr: Expr,
112    pub(in crate::db) direction: OrderDirection,
113}
114
115impl OrderTerm {
116    /// Construct one planner-owned ORDER BY term from one semantic expression.
117    #[must_use]
118    pub(in crate::db) 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(in crate::db) 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(in crate::db) 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(in crate::db) 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(in crate::db) fn rendered_label(&self) -> String {
147        render_scalar_projection_expr_plan_label(&self.expr)
148    }
149
150    /// Return the executor-facing direction for this ORDER BY term.
151    #[must_use]
152    pub(in crate::db) 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_plan_label(expr: &Expr) -> String {
183    render_scalar_projection_expr_plan_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(in crate::db) struct OrderSpec {
194    pub(in crate::db) fields: Vec<OrderTerm>,
195}
196
197///
198/// DeleteLimitSpec
199/// Executor-facing ordered delete window.
200///
201
202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub(in crate::db) struct DeleteLimitSpec {
204    pub(in crate::db) limit: Option<u32>,
205    pub(in crate::db) 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(in crate::db) 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(in crate::db) 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(in crate::db) struct PageSpec {
378    pub(in crate::db) limit: Option<u32>,
379    pub(in crate::db) 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(in crate::db) 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(in crate::db) 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(in crate::db) 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 render label for this aggregate kind.
452    #[must_use]
453    pub(in crate::db) const fn canonical_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(in crate::db) 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!(self, Self::Count | Self::Sum | Self::Avg)
503    }
504
505    /// Return the stable aggregate discriminant used by projection and
506    /// aggregate fingerprint hashing.
507    #[must_use]
508    pub(in crate::db::query) const fn fingerprint_tag(self) -> u8 {
509        match self {
510            Self::Count => 0x01,
511            Self::Sum => 0x02,
512            Self::Exists => 0x03,
513            Self::Min => 0x04,
514            Self::Max => 0x05,
515            Self::First => 0x06,
516            Self::Last => 0x07,
517            Self::Avg => 0x08,
518        }
519    }
520
521    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
522    #[must_use]
523    pub(in crate::db) const fn global_distinct_kind(self) -> Option<GlobalDistinctAggregateKind> {
524        match self {
525            Self::Count => Some(GlobalDistinctAggregateKind::Count),
526            Self::Sum => Some(GlobalDistinctAggregateKind::Sum),
527            Self::Avg => Some(GlobalDistinctAggregateKind::Avg),
528            Self::Exists | Self::Min | Self::Max | Self::First | Self::Last => None,
529        }
530    }
531
532    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
533    #[must_use]
534    pub(in crate::db) const fn supports_global_distinct_without_group_keys(self) -> bool {
535        self.global_distinct_kind().is_some()
536    }
537
538    /// Return the planner-owned grouped aggregate-family profile for one aggregate shape.
539    #[must_use]
540    pub(in crate::db) const fn grouped_plan_family(
541        self,
542        has_target_field: bool,
543    ) -> GroupedPlanAggregateFamily {
544        if has_target_field && self.supports_field_target_v1() {
545            GroupedPlanAggregateFamily::FieldTargetRows
546        } else {
547            GroupedPlanAggregateFamily::GenericRows
548        }
549    }
550
551    /// Return whether this grouped aggregate shape supports ordered grouped streaming.
552    #[must_use]
553    pub(in crate::db) const fn supports_grouped_streaming_v1(
554        self,
555        has_target_field: bool,
556        distinct: bool,
557    ) -> bool {
558        if self.supports_field_target_v1() {
559            return !distinct && (self.is_count() || has_target_field);
560        }
561
562        !has_target_field && (!distinct || self.supports_grouped_distinct_v1())
563    }
564
565    /// Return the canonical extrema traversal direction for this kind.
566    #[must_use]
567    pub(in crate::db) const fn extrema_direction(self) -> Option<Direction> {
568        match self {
569            Self::Min => Some(Direction::Asc),
570            Self::Max => Some(Direction::Desc),
571            Self::Count | Self::Sum | Self::Avg | Self::Exists | Self::First | Self::Last => None,
572        }
573    }
574
575    /// Return the canonical materialized fold direction for this kind.
576    #[must_use]
577    pub(in crate::db) const fn materialized_fold_direction(self) -> Direction {
578        match self {
579            Self::Min => Direction::Desc,
580            Self::Count
581            | Self::Sum
582            | Self::Avg
583            | Self::Exists
584            | Self::Max
585            | Self::First
586            | Self::Last => Direction::Asc,
587        }
588    }
589
590    /// Return true when this kind can use bounded aggregate probe hints.
591    #[must_use]
592    pub(in crate::db) const fn supports_bounded_probe_hint(self) -> bool {
593        !self.is_count() && !self.is_sum()
594    }
595
596    /// Derive a bounded aggregate probe fetch hint for this kind.
597    #[must_use]
598    pub(in crate::db) fn bounded_probe_fetch_hint(
599        self,
600        direction: Direction,
601        offset: usize,
602        page_limit: Option<usize>,
603    ) -> Option<usize> {
604        match self {
605            Self::Exists | Self::First => Some(offset.saturating_add(1)),
606            Self::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
607            Self::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
608            Self::Last => page_limit.map(|limit| offset.saturating_add(limit)),
609            Self::Count | Self::Sum | Self::Avg | Self::Min | Self::Max => None,
610        }
611    }
612
613    /// Return the explain projection mode label for this kind and projection surface.
614    #[must_use]
615    pub(in crate::db) const fn explain_projection_mode_label(
616        self,
617        has_projected_field: bool,
618        covering_projection: bool,
619    ) -> &'static str {
620        if has_projected_field {
621            if covering_projection {
622                "field_idx"
623            } else {
624                "field_mat"
625            }
626        } else if matches!(self, Self::Min | Self::Max | Self::First | Self::Last) {
627            "entity_term"
628        } else {
629            "scalar_agg"
630        }
631    }
632
633    /// Return whether this terminal kind can remain covering on existing-row plans.
634    #[must_use]
635    pub(in crate::db) const fn supports_covering_existing_rows_terminal(self) -> bool {
636        matches!(self, Self::Count | Self::Exists)
637    }
638}
639
640///
641/// GroupAggregateSpec
642///
643/// One grouped aggregate terminal specification declared at query-plan time.
644/// `input_expr` is the single expression source for grouped aggregate identity.
645/// Field-target behavior is derived from plain `Expr::Field` leaves so grouped
646/// semantics, explain, fingerprinting, and runtime do not carry a second
647/// compatibility shape beside the canonical aggregate input expression.
648///
649
650#[derive(Clone, Debug)]
651pub(in crate::db) struct GroupAggregateSpec {
652    pub(in crate::db) kind: AggregateKind,
653    pub(in crate::db) input_expr: Option<Box<Expr>>,
654    pub(in crate::db) filter_expr: Option<Box<Expr>>,
655    pub(in crate::db) distinct: bool,
656}
657
658impl PartialEq for GroupAggregateSpec {
659    fn eq(&self, other: &Self) -> bool {
660        self.semantic_key() == other.semantic_key()
661    }
662}
663
664impl Eq for GroupAggregateSpec {}
665
666impl GroupedPlanAggregateFamily {
667    /// Derive the grouped aggregate-family profile from one planner aggregate list.
668    #[must_use]
669    pub(in crate::db) fn from_grouped_aggregates(aggregates: &[GroupAggregateSpec]) -> Self {
670        if matches!(aggregates, [aggregate] if aggregate.identity().is_count_rows_only()) {
671            return Self::CountRowsOnly;
672        }
673
674        if aggregates.iter().all(|aggregate| {
675            aggregate
676                .kind()
677                .grouped_plan_family(aggregate.target_field().is_some())
678                == Self::FieldTargetRows
679        }) {
680            return Self::FieldTargetRows;
681        }
682
683        Self::GenericRows
684    }
685}
686
687///
688/// FieldSlot
689///
690/// Canonical resolved field reference used by logical planning.
691/// `index` is the stable slot in `EntityModel::fields`; `field` is retained
692/// for diagnostics and explain surfaces.
693/// `kind` freezes planner-resolved field metadata so executor boundaries do
694/// not need to reopen `EntityModel` just to recover type/capability shape.
695///
696
697#[derive(Clone, Debug)]
698pub(crate) struct FieldSlot {
699    pub(in crate::db) index: usize,
700    pub(in crate::db) field: String,
701    pub(in crate::db) kind: Option<FieldKind>,
702}
703
704impl PartialEq for FieldSlot {
705    fn eq(&self, other: &Self) -> bool {
706        self.index == other.index && self.field == other.field
707    }
708}
709
710impl Eq for FieldSlot {}
711
712///
713/// GroupedExecutionConfig
714///
715/// Declarative grouped-execution budget policy selected by query planning.
716/// This remains planner-owned input; executor policy bridges may still apply
717/// defaults and enforcement strategy at runtime boundaries.
718///
719
720#[derive(Clone, Copy, Debug, Eq, PartialEq)]
721pub(in crate::db) struct GroupedExecutionConfig {
722    pub(in crate::db) max_groups: u64,
723    pub(in crate::db) max_group_bytes: u64,
724}
725
726///
727/// GroupSpec
728///
729/// Declarative GROUP BY stage contract attached to a validated base plan.
730/// This wrapper is intentionally semantic-only; field-slot resolution and
731/// execution-mode derivation remain executor-owned boundaries.
732///
733
734#[derive(Clone, Debug, Eq, PartialEq)]
735pub(in crate::db) struct GroupSpec {
736    pub(in crate::db) group_fields: Vec<FieldSlot>,
737    pub(in crate::db) aggregates: Vec<GroupAggregateSpec>,
738    pub(in crate::db) execution: GroupedExecutionConfig,
739}
740
741///
742/// ScalarPlan
743///
744/// Pure scalar logical query intent produced by the planner.
745///
746/// A `ScalarPlan` represents the access-independent query semantics:
747/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
748/// and read-consistency mode.
749///
750/// Design notes:
751/// - Predicates are applied *after* data access
752/// - Ordering is applied after filtering
753/// - Pagination is applied after ordering (load only)
754/// - Delete limits are applied after ordering (delete only)
755/// - Missing-row policy is explicit and must not depend on access strategy
756///
757/// This struct is the logical compiler stage output and intentionally excludes
758/// access-path details.
759///
760
761#[derive(Clone, Debug, Eq, PartialEq)]
762pub(in crate::db) struct ScalarPlan {
763    /// Load vs delete intent.
764    pub(in crate::db) mode: QueryMode,
765
766    /// Optional planner-owned scalar filter expression.
767    pub(in crate::db) filter_expr: Option<Expr>,
768
769    /// Whether the predicate fully covers the scalar filter expression.
770    pub(in crate::db) predicate_covers_filter_expr: bool,
771
772    /// Optional residual predicate applied after access.
773    pub(in crate::db) predicate: Option<Predicate>,
774
775    /// Optional ordering specification.
776    pub(in crate::db) order: Option<OrderSpec>,
777
778    /// Optional distinct semantics over ordered rows.
779    pub(in crate::db) distinct: bool,
780
781    /// Optional ordered delete window (delete intents only).
782    pub(in crate::db) delete_limit: Option<DeleteLimitSpec>,
783
784    /// Optional pagination specification.
785    pub(in crate::db) page: Option<PageSpec>,
786
787    /// Missing-row policy for execution.
788    pub(in crate::db) consistency: MissingRowPolicy,
789}
790
791///
792/// GroupPlan
793///
794/// Pure grouped logical intent emitted by grouped planning.
795/// Group metadata is carried through one canonical `GroupSpec` contract.
796///
797
798#[derive(Clone, Debug, Eq, PartialEq)]
799pub(in crate::db) struct GroupPlan {
800    pub(in crate::db) scalar: ScalarPlan,
801    pub(in crate::db) group: GroupSpec,
802    pub(in crate::db) having_expr: Option<Expr>,
803}
804
805///
806/// LogicalPlan
807///
808/// Exclusive logical query intent emitted by planning.
809/// Scalar and grouped semantics are distinct variants by construction.
810///
811
812// Logical plans keep scalar and grouped shapes inline because planner/executor handoff
813// passes these variants by ownership and boxing would widen that boundary for little benefit.
814#[derive(Clone, Debug, Eq, PartialEq)]
815pub(in crate::db) enum LogicalPlan {
816    Scalar(ScalarPlan),
817    Grouped(GroupPlan),
818}