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::db::{
7    cursor::ContinuationSignature,
8    direction::Direction,
9    predicate::{MissingRowPolicy, Predicate},
10    query::{
11        builder::scalar_projection::render_scalar_projection_expr_plan_label,
12        plan::{
13            aggregate_shape::AggregateShape,
14            expr::{Expr, FieldId},
15            order_contract::DeterministicSecondaryOrderContract,
16            semantics::LogicalPushdownEligibility,
17        },
18    },
19    schema::AcceptedFieldKind,
20};
21use std::sync::Arc;
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#[cfg(any(feature = "sql", test))]
182#[must_use]
183pub(in crate::db) fn render_scalar_filter_expr_plan_label(expr: &Expr) -> String {
184    // Intent owns canonicalization. Residual filters retain that same tree;
185    // rendering/fingerprinting must neither re-prepare it nor acquire a budget.
186    render_scalar_projection_expr_plan_label(expr)
187}
188
189///
190/// OrderSpec
191///
192/// Executor-facing ordering specification.
193/// Carries the canonical ordered term list after planner expression lowering.
194///
195#[derive(Clone, Debug, Eq, PartialEq)]
196pub(in crate::db) struct OrderSpec {
197    pub(in crate::db) fields: Vec<OrderTerm>,
198}
199
200///
201/// DeleteLimitSpec
202/// Executor-facing ordered delete window.
203///
204
205#[derive(Clone, Copy, Debug, Eq, PartialEq)]
206pub(in crate::db) struct DeleteLimitSpec {
207    pub(in crate::db) limit: Option<u32>,
208    pub(in crate::db) offset: u32,
209}
210
211///
212/// DistinctExecutionStrategy
213///
214/// Planner-owned scalar DISTINCT execution strategy.
215/// This is execution-mechanics only and must not be used for semantic
216/// admissibility decisions.
217///
218
219#[derive(Clone, Copy, Debug, Eq, PartialEq)]
220pub(in crate::db) enum DistinctExecutionStrategy {
221    None,
222    PreOrdered,
223    HashMaterialize,
224}
225
226///
227/// PlannerRouteProfile
228///
229/// Planner-projected route profile consumed by executor route planning.
230/// Carries planner-owned continuation policy plus deterministic order/pushdown
231/// contracts that route/load layers must honor without recomputing order shape.
232///
233
234#[derive(Clone, Debug, Eq, PartialEq)]
235pub(in crate::db) struct PlannerRouteProfile {
236    continuation_policy: ContinuationPolicy,
237    logical_pushdown_eligibility: LogicalPushdownEligibility,
238    secondary_order_contract: Option<DeterministicSecondaryOrderContract>,
239}
240
241impl PlannerRouteProfile {
242    /// Construct one planner-projected route profile.
243    #[must_use]
244    pub(in crate::db) const fn new(
245        continuation_policy: ContinuationPolicy,
246        logical_pushdown_eligibility: LogicalPushdownEligibility,
247        secondary_order_contract: Option<DeterministicSecondaryOrderContract>,
248    ) -> Self {
249        Self {
250            continuation_policy,
251            logical_pushdown_eligibility,
252            secondary_order_contract,
253        }
254    }
255
256    /// Construct one fail-closed route profile for manually assembled plans
257    /// that have not yet been finalized against model authority.
258    #[must_use]
259    pub(in crate::db) const fn seeded_unfinalized(is_grouped: bool) -> Self {
260        Self {
261            continuation_policy: ContinuationPolicy::new(true, true, !is_grouped),
262            logical_pushdown_eligibility: LogicalPushdownEligibility::new(false, is_grouped, false),
263            secondary_order_contract: None,
264        }
265    }
266
267    /// Borrow planner-projected continuation policy contract.
268    #[must_use]
269    pub(in crate::db) const fn continuation_policy(&self) -> &ContinuationPolicy {
270        &self.continuation_policy
271    }
272
273    /// Borrow planner-owned logical pushdown eligibility contract.
274    #[must_use]
275    pub(in crate::db) const fn logical_pushdown_eligibility(&self) -> LogicalPushdownEligibility {
276        self.logical_pushdown_eligibility
277    }
278
279    /// Borrow the planner-owned deterministic secondary-order contract, if one exists.
280    #[must_use]
281    pub(in crate::db) const fn secondary_order_contract(
282        &self,
283    ) -> Option<&DeterministicSecondaryOrderContract> {
284        self.secondary_order_contract.as_ref()
285    }
286}
287
288///
289/// ContinuationPolicy
290///
291/// Planner-projected continuation contract carried into route/executor layers.
292/// This contract captures static continuation invariants and must not be
293/// rederived by route/load orchestration code.
294///
295
296#[derive(Clone, Copy, Debug, Eq, PartialEq)]
297pub(in crate::db) struct ContinuationPolicy {
298    requires_anchor: bool,
299    requires_strict_advance: bool,
300    is_grouped_safe: bool,
301}
302
303impl ContinuationPolicy {
304    /// Construct one planner-projected continuation policy contract.
305    #[must_use]
306    pub(in crate::db) const fn new(
307        requires_anchor: bool,
308        requires_strict_advance: bool,
309        is_grouped_safe: bool,
310    ) -> Self {
311        Self {
312            requires_anchor,
313            requires_strict_advance,
314            is_grouped_safe,
315        }
316    }
317
318    /// Return true when continuation resume paths require an anchor boundary.
319    #[must_use]
320    pub(in crate::db) const fn requires_anchor(self) -> bool {
321        self.requires_anchor
322    }
323
324    /// Return true when continuation resume paths require strict advancement.
325    #[must_use]
326    pub(in crate::db) const fn requires_strict_advance(self) -> bool {
327        self.requires_strict_advance
328    }
329
330    /// Return true when grouped continuation usage is semantically safe.
331    #[must_use]
332    pub(in crate::db) const fn is_grouped_safe(self) -> bool {
333        self.is_grouped_safe
334    }
335}
336
337///
338/// ExecutionShapeSignature
339///
340/// Immutable planner-projected semantic shape signature contract.
341/// Continuation transport encodes this contract; route/load consume it as a
342/// read-only execution identity boundary without re-deriving semantics.
343///
344
345#[derive(Clone, Copy, Debug, Eq, PartialEq)]
346pub(in crate::db) struct ExecutionShapeSignature {
347    continuation_signature: ContinuationSignature,
348}
349
350impl ExecutionShapeSignature {
351    /// Construct one immutable execution-shape signature contract.
352    #[must_use]
353    pub(in crate::db) const fn new(continuation_signature: ContinuationSignature) -> Self {
354        Self {
355            continuation_signature,
356        }
357    }
358
359    /// Borrow the canonical continuation signature for this execution shape.
360    #[must_use]
361    pub(in crate::db) const fn continuation_signature(self) -> ContinuationSignature {
362        self.continuation_signature
363    }
364}
365
366///
367/// PageSpec
368/// Executor-facing pagination specification.
369///
370
371#[derive(Clone, Debug, Eq, PartialEq)]
372pub(in crate::db) struct PageSpec {
373    pub(in crate::db) limit: Option<u32>,
374    pub(in crate::db) offset: u32,
375}
376
377///
378/// AggregateKind
379///
380/// Canonical aggregate terminal taxonomy owned by query planning.
381/// All layers (query, explain, fingerprint, executor) must interpret aggregate
382/// terminal semantics through this single enum authority.
383/// Executor must derive traversal and fold direction exclusively from this enum.
384///
385
386#[derive(Clone, Copy, Debug, Eq, PartialEq)]
387pub enum AggregateKind {
388    Count,
389    Sum,
390    Avg,
391    Exists,
392    Min,
393    Max,
394    First,
395    Last,
396}
397
398///
399/// GlobalDistinctAggregateKind
400///
401/// Canonical support-family for grouped global-DISTINCT field aggregates.
402/// This keeps the admitted `COUNT | SUM | AVG` family on one planner-owned
403/// support surface instead of repeating that support set across grouped
404/// semantics and grouped executor handoff.
405///
406
407#[derive(Clone, Copy, Debug, Eq, PartialEq)]
408pub(in crate::db) enum GlobalDistinctAggregateKind {
409    Count,
410    Sum,
411    Avg,
412}
413
414///
415/// GroupedPlanAggregateFamily
416///
417/// Planner-owned grouped aggregate-family profile.
418/// This is intentionally coarse and execution-oriented: it captures which
419/// grouped aggregate family the planner admitted so runtime can select grouped
420/// execution paths without rebuilding family policy from raw aggregate
421/// expressions again.
422///
423
424#[derive(Clone, Copy, Debug, Eq, PartialEq)]
425pub(in crate::db) enum GroupedPlanAggregateFamily {
426    CountRowsOnly,
427    FieldTargetRows,
428    GenericRows,
429}
430
431impl GroupedPlanAggregateFamily {
432    /// Return the stable planner-owned aggregate-family code.
433    #[must_use]
434    pub(in crate::db) const fn code(self) -> &'static str {
435        match self {
436            Self::CountRowsOnly => "count_rows_only",
437            Self::FieldTargetRows => "field_target_rows",
438            Self::GenericRows => "generic_rows",
439        }
440    }
441}
442
443impl AggregateKind {
444    /// Return the canonical uppercase render label for this aggregate kind.
445    #[must_use]
446    pub(in crate::db) const fn canonical_label(self) -> &'static str {
447        match self {
448            Self::Count => "COUNT",
449            Self::Sum => "SUM",
450            Self::Avg => "AVG",
451            Self::Exists => "EXISTS",
452            Self::First => "FIRST",
453            Self::Last => "LAST",
454            Self::Min => "MIN",
455            Self::Max => "MAX",
456        }
457    }
458
459    /// Return whether this terminal kind is `COUNT`.
460    #[must_use]
461    pub(in crate::db) const fn is_count(self) -> bool {
462        matches!(self, Self::Count)
463    }
464
465    /// Return whether this terminal kind belongs to the SUM/AVG numeric fold family.
466    #[must_use]
467    pub(in crate::db) const fn is_sum(self) -> bool {
468        matches!(self, Self::Sum | Self::Avg)
469    }
470
471    /// Return whether this terminal kind belongs to the extrema family.
472    #[must_use]
473    pub(in crate::db) const fn is_extrema(self) -> bool {
474        matches!(self, Self::Min | Self::Max)
475    }
476
477    /// Return whether this kind supports one grouped or global field target.
478    #[must_use]
479    pub(in crate::db) const fn supports_field_target(self) -> bool {
480        matches!(
481            self,
482            Self::Count | Self::Sum | Self::Avg | Self::Min | Self::Max
483        )
484    }
485
486    /// Return whether grouped aggregate DISTINCT is supported for this kind.
487    #[must_use]
488    pub(in crate::db) const fn supports_grouped_distinct(self) -> bool {
489        matches!(self, Self::Count | Self::Sum | Self::Avg)
490    }
491
492    /// Return the stable aggregate discriminant used by projection and
493    /// aggregate fingerprint hashing.
494    #[must_use]
495    pub(in crate::db::query) const fn fingerprint_tag(self) -> u8 {
496        match self {
497            Self::Count => 0x01,
498            Self::Sum => 0x02,
499            Self::Exists => 0x03,
500            Self::Min => 0x04,
501            Self::Max => 0x05,
502            Self::First => 0x06,
503            Self::Last => 0x07,
504            Self::Avg => 0x08,
505        }
506    }
507
508    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
509    #[must_use]
510    pub(in crate::db) const fn global_distinct_kind(self) -> Option<GlobalDistinctAggregateKind> {
511        match self {
512            Self::Count => Some(GlobalDistinctAggregateKind::Count),
513            Self::Sum => Some(GlobalDistinctAggregateKind::Sum),
514            Self::Avg => Some(GlobalDistinctAggregateKind::Avg),
515            Self::Exists | Self::Min | Self::Max | Self::First | Self::Last => None,
516        }
517    }
518
519    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
520    #[must_use]
521    pub(in crate::db) const fn supports_global_distinct_without_group_keys(self) -> bool {
522        self.global_distinct_kind().is_some()
523    }
524
525    /// Return the planner-owned grouped aggregate-family profile for one aggregate shape.
526    #[must_use]
527    pub(in crate::db) const fn grouped_plan_family(
528        self,
529        has_target_field: bool,
530    ) -> GroupedPlanAggregateFamily {
531        if has_target_field && self.supports_field_target() {
532            GroupedPlanAggregateFamily::FieldTargetRows
533        } else {
534            GroupedPlanAggregateFamily::GenericRows
535        }
536    }
537
538    /// Return whether this grouped aggregate shape supports ordered grouped streaming.
539    #[must_use]
540    pub(in crate::db) const fn supports_grouped_streaming(
541        self,
542        has_target_field: bool,
543        distinct: bool,
544    ) -> bool {
545        if self.supports_field_target() {
546            return !distinct && (self.is_count() || has_target_field);
547        }
548
549        !has_target_field && (!distinct || self.supports_grouped_distinct())
550    }
551
552    /// Return the canonical extrema traversal direction for this kind.
553    #[must_use]
554    pub(in crate::db) const fn extrema_direction(self) -> Option<Direction> {
555        match self {
556            Self::Min => Some(Direction::Asc),
557            Self::Max => Some(Direction::Desc),
558            Self::Count | Self::Sum | Self::Avg | Self::Exists | Self::First | Self::Last => None,
559        }
560    }
561
562    /// Return true when this kind can use bounded aggregate probe hints.
563    #[must_use]
564    pub(in crate::db) const fn supports_bounded_probe_hint(self) -> bool {
565        !self.is_count() && !self.is_sum()
566    }
567
568    /// Derive a bounded aggregate probe fetch hint for this kind.
569    #[must_use]
570    pub(in crate::db) fn bounded_probe_fetch_hint(
571        self,
572        direction: Direction,
573        offset: usize,
574        page_limit: Option<usize>,
575    ) -> Option<usize> {
576        match self {
577            Self::Exists | Self::First => Some(offset.saturating_add(1)),
578            Self::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
579            Self::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
580            Self::Last => page_limit.map(|limit| offset.saturating_add(limit)),
581            Self::Count | Self::Sum | Self::Avg | Self::Min | Self::Max => None,
582        }
583    }
584
585    /// Return the explain projection mode label for this kind and projection surface.
586    #[must_use]
587    #[cfg(feature = "sql")]
588    pub(in crate::db) const fn explain_projection_mode_label(
589        self,
590        has_projected_field: bool,
591        covering_projection: bool,
592    ) -> &'static str {
593        if has_projected_field {
594            if covering_projection {
595                "field_idx"
596            } else {
597                "field_mat"
598            }
599        } else if matches!(self, Self::Min | Self::Max | Self::First | Self::Last) {
600            "entity_term"
601        } else {
602            "scalar_agg"
603        }
604    }
605
606    /// Return whether this terminal kind can remain covering on existing-row plans.
607    #[must_use]
608    #[cfg(feature = "sql")]
609    pub(in crate::db) const fn supports_covering_existing_rows_terminal(self) -> bool {
610        matches!(self, Self::Count | Self::Exists)
611    }
612}
613
614///
615/// GroupAggregateSpec
616///
617/// One grouped aggregate terminal specification declared at query-plan time.
618/// `input_expr` is the single expression source for grouped aggregate identity.
619/// Field-target behavior is derived from plain `Expr::Field` leaves so grouped
620/// semantics, explain, fingerprinting, and runtime do not carry a second
621/// compatibility shape beside the canonical aggregate input expression.
622///
623
624#[derive(Clone, Debug)]
625pub(in crate::db) struct GroupAggregateSpec {
626    shape: AggregateShape,
627}
628
629impl GroupAggregateSpec {
630    /// Wrap one canonical raw aggregate shape for grouped planning.
631    #[must_use]
632    pub(in crate::db) const fn from_shape(shape: AggregateShape) -> Self {
633        Self { shape }
634    }
635
636    /// Borrow the canonical raw aggregate shape.
637    #[must_use]
638    pub(in crate::db) const fn shape(&self) -> &AggregateShape {
639        &self.shape
640    }
641}
642
643impl PartialEq for GroupAggregateSpec {
644    fn eq(&self, other: &Self) -> bool {
645        self.semantic_key() == other.semantic_key()
646    }
647}
648
649impl Eq for GroupAggregateSpec {}
650
651impl GroupedPlanAggregateFamily {
652    /// Inspect aggregate facts with observation before each borrowed visit.
653    pub(in crate::db) fn try_from_grouped_aggregates<E>(
654        aggregates: &[GroupAggregateSpec],
655        observe: &mut impl FnMut(u64) -> Result<(), E>,
656    ) -> Result<Self, E> {
657        observe(1)?;
658        if matches!(aggregates, [aggregate] if aggregate.is_count_rows_only()) {
659            return Ok(Self::CountRowsOnly);
660        }
661
662        for aggregate in aggregates {
663            observe(1)?;
664            if aggregate
665                .kind()
666                .grouped_plan_family(aggregate.target_field().is_some())
667                != Self::FieldTargetRows
668            {
669                return Ok(Self::GenericRows);
670            }
671        }
672
673        Ok(Self::FieldTargetRows)
674    }
675}
676
677///
678/// FieldSlot
679///
680/// Canonical resolved field reference used by logical planning.
681/// `index` is the stable accepted field slot; `field` is retained
682/// for diagnostics and explain surfaces. Resolved labels share the accepted
683/// schema allocation; detached plan clones do not copy their text.
684/// `authority` freezes exactly one planner metadata source.
685///
686
687#[derive(Clone, Debug)]
688pub(in crate::db::query::plan) enum FieldSlotAuthority {
689    Unresolved,
690    Accepted(Arc<AcceptedFieldKind>),
691}
692
693#[derive(Clone, Debug)]
694pub(crate) struct FieldSlot {
695    pub(in crate::db) index: usize,
696    pub(in crate::db) field: Arc<str>,
697    pub(in crate::db::query::plan) authority: FieldSlotAuthority,
698}
699
700impl PartialEq for FieldSlot {
701    fn eq(&self, other: &Self) -> bool {
702        self.index == other.index && self.field == other.field
703    }
704}
705
706impl Eq for FieldSlot {}
707
708///
709/// GroupedExecutionConfig
710///
711/// Declarative grouped-execution budget policy selected by query planning.
712/// This remains planner-owned input; executor policy bridges may still apply
713/// defaults and enforcement strategy at runtime boundaries.
714///
715
716#[derive(Clone, Copy, Debug, Eq, PartialEq)]
717pub(in crate::db) struct GroupedExecutionConfig {
718    pub(in crate::db) max_groups: u64,
719    pub(in crate::db) max_group_bytes: u64,
720}
721
722///
723/// GroupSpec
724///
725/// Declarative GROUP BY stage contract attached to a validated base plan.
726/// This wrapper is intentionally semantic-only; field-slot resolution and
727/// execution-mode derivation remain executor-owned boundaries.
728///
729
730#[derive(Clone, Debug, Eq, PartialEq)]
731pub(in crate::db) struct GroupSpec {
732    pub(in crate::db) group_fields: crate::db::query::plan::GroupFieldSet,
733    pub(in crate::db) aggregates: Vec<GroupAggregateSpec>,
734    pub(in crate::db) execution: GroupedExecutionConfig,
735}
736
737///
738/// ScalarPlan
739///
740/// Pure scalar logical query intent produced by the planner.
741///
742/// A `ScalarPlan` represents the access-independent query semantics:
743/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
744/// and read-consistency mode.
745///
746/// Design notes:
747/// - Predicates are applied *after* data access
748/// - Ordering is applied after filtering
749/// - Pagination is applied after ordering (load only)
750/// - Delete limits are applied after ordering (delete only)
751/// - Missing-row policy is explicit and must not depend on access strategy
752///
753/// This struct is the logical compiler stage output and intentionally excludes
754/// access-path details.
755///
756
757#[derive(Clone, Debug, Eq, PartialEq)]
758pub(in crate::db) struct ScalarPlan {
759    /// Load vs delete intent.
760    pub(in crate::db) mode: QueryMode,
761
762    /// Optional planner-owned scalar filter expression.
763    pub(in crate::db) filter_expr: Option<Expr>,
764
765    /// Whether the predicate fully covers the scalar filter expression.
766    pub(in crate::db) predicate_covers_filter_expr: bool,
767
768    /// Optional residual predicate applied after access.
769    pub(in crate::db) predicate: Option<Predicate>,
770
771    /// Optional ordering specification.
772    pub(in crate::db) order: Option<OrderSpec>,
773
774    /// Optional distinct semantics over ordered rows.
775    pub(in crate::db) distinct: bool,
776
777    /// Optional ordered delete window (delete intents only).
778    pub(in crate::db) delete_limit: Option<DeleteLimitSpec>,
779
780    /// Optional pagination specification.
781    pub(in crate::db) page: Option<PageSpec>,
782
783    /// Missing-row policy for execution.
784    pub(in crate::db) consistency: MissingRowPolicy,
785}
786
787///
788/// GroupPlan
789///
790/// Pure grouped logical intent emitted by grouped planning.
791/// Group metadata is carried through one canonical `GroupSpec` contract.
792///
793
794#[derive(Clone, Debug, Eq, PartialEq)]
795pub(in crate::db) struct GroupPlan {
796    pub(in crate::db) scalar: ScalarPlan,
797    pub(in crate::db) group: GroupSpec,
798    pub(in crate::db) having_expr: Option<Expr>,
799}
800
801///
802/// LogicalPlan
803///
804/// Exclusive logical query intent emitted by planning.
805/// Scalar and grouped semantics are distinct variants by construction.
806///
807
808// Logical plans keep scalar and grouped shapes inline because planner/executor handoff
809// passes these variants by ownership and boxing would widen that boundary for little benefit.
810#[derive(Clone, Debug, Eq, PartialEq)]
811pub(in crate::db) enum LogicalPlan {
812    Scalar(ScalarPlan),
813    Grouped(GroupPlan),
814}
815
816// Exhaustive cache-retention coverage; new owned fields require accounting.
817crate::retained::retained_copy!(AggregateKind);
818crate::retained::retained_copy!(ContinuationPolicy);
819crate::retained::retained_copy!(DeleteLimitSpec);
820crate::retained::retained_copy!(ExecutionShapeSignature);
821crate::retained::retained_fields!(FieldSlot {
822Self{index,field,authority} => [index,field,authority],
823});
824crate::retained::retained_fields!(FieldSlotAuthority {
825Self::Unresolved => [],
826Self::Accepted(field_0) => [field_0],
827});
828crate::retained::retained_fields!(GroupAggregateSpec {
829Self{shape} => [shape],
830});
831crate::retained::retained_fields!(GroupPlan {
832Self{scalar,group,having_expr} => [scalar,group,having_expr],
833});
834crate::retained::retained_fields!(GroupSpec {
835Self{group_fields,aggregates,execution} => [group_fields,aggregates,execution],
836});
837crate::retained::retained_copy!(GroupedExecutionConfig);
838crate::retained::retained_fields!(LogicalPlan {
839Self::Scalar(field_0) => [field_0],
840Self::Grouped(field_0) => [field_0],
841});
842crate::retained::retained_copy!(OrderDirection);
843crate::retained::retained_fields!(OrderSpec {
844Self{fields} => [fields],
845});
846crate::retained::retained_fields!(OrderTerm {
847Self{expr,direction} => [expr,direction],
848});
849crate::retained::retained_fields!(PageSpec {
850Self{limit,offset} => [limit,offset],
851});
852crate::retained::retained_fields!(PlannerRouteProfile {
853Self{continuation_policy,logical_pushdown_eligibility,secondary_order_contract} => [continuation_policy,logical_pushdown_eligibility,secondary_order_contract],
854});
855crate::retained::retained_copy!(QueryMode);
856crate::retained::retained_fields!(ScalarPlan {
857Self{mode,filter_expr,predicate_covers_filter_expr,predicate,order,distinct,delete_limit,page,consistency} => [mode,filter_expr,predicate_covers_filter_expr,predicate,order,distinct,delete_limit,page,consistency],
858});