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 reducer updates for this kind require a decoded id payload.
487    #[must_use]
488    pub(in crate::db) const fn requires_decoded_id(self) -> bool {
489        !matches!(self, Self::Count | Self::Sum | Self::Avg | Self::Exists)
490    }
491
492    /// Return whether grouped aggregate DISTINCT is supported for this kind.
493    #[must_use]
494    pub(in crate::db) const fn supports_grouped_distinct(self) -> bool {
495        matches!(self, Self::Count | Self::Sum | Self::Avg)
496    }
497
498    /// Return the stable aggregate discriminant used by projection and
499    /// aggregate fingerprint hashing.
500    #[must_use]
501    pub(in crate::db::query) const fn fingerprint_tag(self) -> u8 {
502        match self {
503            Self::Count => 0x01,
504            Self::Sum => 0x02,
505            Self::Exists => 0x03,
506            Self::Min => 0x04,
507            Self::Max => 0x05,
508            Self::First => 0x06,
509            Self::Last => 0x07,
510            Self::Avg => 0x08,
511        }
512    }
513
514    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
515    #[must_use]
516    pub(in crate::db) const fn global_distinct_kind(self) -> Option<GlobalDistinctAggregateKind> {
517        match self {
518            Self::Count => Some(GlobalDistinctAggregateKind::Count),
519            Self::Sum => Some(GlobalDistinctAggregateKind::Sum),
520            Self::Avg => Some(GlobalDistinctAggregateKind::Avg),
521            Self::Exists | Self::Min | Self::Max | Self::First | Self::Last => None,
522        }
523    }
524
525    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
526    #[must_use]
527    pub(in crate::db) const fn supports_global_distinct_without_group_keys(self) -> bool {
528        self.global_distinct_kind().is_some()
529    }
530
531    /// Return the planner-owned grouped aggregate-family profile for one aggregate shape.
532    #[must_use]
533    pub(in crate::db) const fn grouped_plan_family(
534        self,
535        has_target_field: bool,
536    ) -> GroupedPlanAggregateFamily {
537        if has_target_field && self.supports_field_target() {
538            GroupedPlanAggregateFamily::FieldTargetRows
539        } else {
540            GroupedPlanAggregateFamily::GenericRows
541        }
542    }
543
544    /// Return whether this grouped aggregate shape supports ordered grouped streaming.
545    #[must_use]
546    pub(in crate::db) const fn supports_grouped_streaming(
547        self,
548        has_target_field: bool,
549        distinct: bool,
550    ) -> bool {
551        if self.supports_field_target() {
552            return !distinct && (self.is_count() || has_target_field);
553        }
554
555        !has_target_field && (!distinct || self.supports_grouped_distinct())
556    }
557
558    /// Return the canonical extrema traversal direction for this kind.
559    #[must_use]
560    pub(in crate::db) const fn extrema_direction(self) -> Option<Direction> {
561        match self {
562            Self::Min => Some(Direction::Asc),
563            Self::Max => Some(Direction::Desc),
564            Self::Count | Self::Sum | Self::Avg | Self::Exists | Self::First | Self::Last => None,
565        }
566    }
567
568    /// Return the canonical materialized fold direction for this kind.
569    #[must_use]
570    pub(in crate::db) const fn materialized_fold_direction(self) -> Direction {
571        match self {
572            Self::Min => Direction::Desc,
573            Self::Count
574            | Self::Sum
575            | Self::Avg
576            | Self::Exists
577            | Self::Max
578            | Self::First
579            | Self::Last => Direction::Asc,
580        }
581    }
582
583    /// Return true when this kind can use bounded aggregate probe hints.
584    #[must_use]
585    pub(in crate::db) const fn supports_bounded_probe_hint(self) -> bool {
586        !self.is_count() && !self.is_sum()
587    }
588
589    /// Derive a bounded aggregate probe fetch hint for this kind.
590    #[must_use]
591    pub(in crate::db) fn bounded_probe_fetch_hint(
592        self,
593        direction: Direction,
594        offset: usize,
595        page_limit: Option<usize>,
596    ) -> Option<usize> {
597        match self {
598            Self::Exists | Self::First => Some(offset.saturating_add(1)),
599            Self::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
600            Self::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
601            Self::Last => page_limit.map(|limit| offset.saturating_add(limit)),
602            Self::Count | Self::Sum | Self::Avg | Self::Min | Self::Max => None,
603        }
604    }
605
606    /// Return the explain projection mode label for this kind and projection surface.
607    #[must_use]
608    #[cfg(feature = "sql")]
609    pub(in crate::db) const fn explain_projection_mode_label(
610        self,
611        has_projected_field: bool,
612        covering_projection: bool,
613    ) -> &'static str {
614        if has_projected_field {
615            if covering_projection {
616                "field_idx"
617            } else {
618                "field_mat"
619            }
620        } else if matches!(self, Self::Min | Self::Max | Self::First | Self::Last) {
621            "entity_term"
622        } else {
623            "scalar_agg"
624        }
625    }
626
627    /// Return whether this terminal kind can remain covering on existing-row plans.
628    #[must_use]
629    #[cfg(feature = "sql")]
630    pub(in crate::db) const fn supports_covering_existing_rows_terminal(self) -> bool {
631        matches!(self, Self::Count | Self::Exists)
632    }
633}
634
635///
636/// GroupAggregateSpec
637///
638/// One grouped aggregate terminal specification declared at query-plan time.
639/// `input_expr` is the single expression source for grouped aggregate identity.
640/// Field-target behavior is derived from plain `Expr::Field` leaves so grouped
641/// semantics, explain, fingerprinting, and runtime do not carry a second
642/// compatibility shape beside the canonical aggregate input expression.
643///
644
645#[derive(Clone, Debug)]
646pub(in crate::db) struct GroupAggregateSpec {
647    shape: AggregateShape,
648}
649
650impl GroupAggregateSpec {
651    /// Wrap one canonical raw aggregate shape for grouped planning.
652    #[must_use]
653    pub(in crate::db) const fn from_shape(shape: AggregateShape) -> Self {
654        Self { shape }
655    }
656
657    /// Borrow the canonical raw aggregate shape.
658    #[must_use]
659    pub(in crate::db) const fn shape(&self) -> &AggregateShape {
660        &self.shape
661    }
662}
663
664impl PartialEq for GroupAggregateSpec {
665    fn eq(&self, other: &Self) -> bool {
666        self.semantic_key() == other.semantic_key()
667    }
668}
669
670impl Eq for GroupAggregateSpec {}
671
672impl GroupedPlanAggregateFamily {
673    /// Inspect aggregate facts with observation before each borrowed visit.
674    pub(in crate::db) fn try_from_grouped_aggregates<E>(
675        aggregates: &[GroupAggregateSpec],
676        observe: &mut impl FnMut(u64) -> Result<(), E>,
677    ) -> Result<Self, E> {
678        observe(1)?;
679        if matches!(aggregates, [aggregate] if aggregate.is_count_rows_only()) {
680            return Ok(Self::CountRowsOnly);
681        }
682
683        for aggregate in aggregates {
684            observe(1)?;
685            if aggregate
686                .kind()
687                .grouped_plan_family(aggregate.target_field().is_some())
688                != Self::FieldTargetRows
689            {
690                return Ok(Self::GenericRows);
691            }
692        }
693
694        Ok(Self::FieldTargetRows)
695    }
696}
697
698///
699/// FieldSlot
700///
701/// Canonical resolved field reference used by logical planning.
702/// `index` is the stable accepted field slot; `field` is retained
703/// for diagnostics and explain surfaces. Resolved labels share the accepted
704/// schema allocation; detached plan clones do not copy their text.
705/// `authority` freezes exactly one planner metadata source.
706///
707
708#[derive(Clone, Debug)]
709pub(in crate::db::query::plan) enum FieldSlotAuthority {
710    Unresolved,
711    Accepted(Arc<AcceptedFieldKind>),
712}
713
714#[derive(Clone, Debug)]
715pub(crate) struct FieldSlot {
716    pub(in crate::db) index: usize,
717    pub(in crate::db) field: Arc<str>,
718    pub(in crate::db::query::plan) authority: FieldSlotAuthority,
719}
720
721impl PartialEq for FieldSlot {
722    fn eq(&self, other: &Self) -> bool {
723        self.index == other.index && self.field == other.field
724    }
725}
726
727impl Eq for FieldSlot {}
728
729///
730/// GroupedExecutionConfig
731///
732/// Declarative grouped-execution budget policy selected by query planning.
733/// This remains planner-owned input; executor policy bridges may still apply
734/// defaults and enforcement strategy at runtime boundaries.
735///
736
737#[derive(Clone, Copy, Debug, Eq, PartialEq)]
738pub(in crate::db) struct GroupedExecutionConfig {
739    pub(in crate::db) max_groups: u64,
740    pub(in crate::db) max_group_bytes: u64,
741}
742
743///
744/// GroupSpec
745///
746/// Declarative GROUP BY stage contract attached to a validated base plan.
747/// This wrapper is intentionally semantic-only; field-slot resolution and
748/// execution-mode derivation remain executor-owned boundaries.
749///
750
751#[derive(Clone, Debug, Eq, PartialEq)]
752pub(in crate::db) struct GroupSpec {
753    pub(in crate::db) group_fields: crate::db::query::plan::GroupFieldSet,
754    pub(in crate::db) aggregates: Vec<GroupAggregateSpec>,
755    pub(in crate::db) execution: GroupedExecutionConfig,
756}
757
758///
759/// ScalarPlan
760///
761/// Pure scalar logical query intent produced by the planner.
762///
763/// A `ScalarPlan` represents the access-independent query semantics:
764/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
765/// and read-consistency mode.
766///
767/// Design notes:
768/// - Predicates are applied *after* data access
769/// - Ordering is applied after filtering
770/// - Pagination is applied after ordering (load only)
771/// - Delete limits are applied after ordering (delete only)
772/// - Missing-row policy is explicit and must not depend on access strategy
773///
774/// This struct is the logical compiler stage output and intentionally excludes
775/// access-path details.
776///
777
778#[derive(Clone, Debug, Eq, PartialEq)]
779pub(in crate::db) struct ScalarPlan {
780    /// Load vs delete intent.
781    pub(in crate::db) mode: QueryMode,
782
783    /// Optional planner-owned scalar filter expression.
784    pub(in crate::db) filter_expr: Option<Expr>,
785
786    /// Whether the predicate fully covers the scalar filter expression.
787    pub(in crate::db) predicate_covers_filter_expr: bool,
788
789    /// Optional residual predicate applied after access.
790    pub(in crate::db) predicate: Option<Predicate>,
791
792    /// Optional ordering specification.
793    pub(in crate::db) order: Option<OrderSpec>,
794
795    /// Optional distinct semantics over ordered rows.
796    pub(in crate::db) distinct: bool,
797
798    /// Optional ordered delete window (delete intents only).
799    pub(in crate::db) delete_limit: Option<DeleteLimitSpec>,
800
801    /// Optional pagination specification.
802    pub(in crate::db) page: Option<PageSpec>,
803
804    /// Missing-row policy for execution.
805    pub(in crate::db) consistency: MissingRowPolicy,
806}
807
808///
809/// GroupPlan
810///
811/// Pure grouped logical intent emitted by grouped planning.
812/// Group metadata is carried through one canonical `GroupSpec` contract.
813///
814
815#[derive(Clone, Debug, Eq, PartialEq)]
816pub(in crate::db) struct GroupPlan {
817    pub(in crate::db) scalar: ScalarPlan,
818    pub(in crate::db) group: GroupSpec,
819    pub(in crate::db) having_expr: Option<Expr>,
820}
821
822///
823/// LogicalPlan
824///
825/// Exclusive logical query intent emitted by planning.
826/// Scalar and grouped semantics are distinct variants by construction.
827///
828
829// Logical plans keep scalar and grouped shapes inline because planner/executor handoff
830// passes these variants by ownership and boxing would widen that boundary for little benefit.
831#[derive(Clone, Debug, Eq, PartialEq)]
832pub(in crate::db) enum LogicalPlan {
833    Scalar(ScalarPlan),
834    Grouped(GroupPlan),
835}
836
837// Exhaustive cache-retention coverage; new owned fields require accounting.
838crate::retained::retained_copy!(AggregateKind);
839crate::retained::retained_copy!(ContinuationPolicy);
840crate::retained::retained_copy!(DeleteLimitSpec);
841crate::retained::retained_copy!(ExecutionShapeSignature);
842crate::retained::retained_fields!(FieldSlot {
843Self{index,field,authority} => [index,field,authority],
844});
845crate::retained::retained_fields!(FieldSlotAuthority {
846Self::Unresolved => [],
847Self::Accepted(field_0) => [field_0],
848});
849crate::retained::retained_fields!(GroupAggregateSpec {
850Self{shape} => [shape],
851});
852crate::retained::retained_fields!(GroupPlan {
853Self{scalar,group,having_expr} => [scalar,group,having_expr],
854});
855crate::retained::retained_fields!(GroupSpec {
856Self{group_fields,aggregates,execution} => [group_fields,aggregates,execution],
857});
858crate::retained::retained_copy!(GroupedExecutionConfig);
859crate::retained::retained_fields!(LogicalPlan {
860Self::Scalar(field_0) => [field_0],
861Self::Grouped(field_0) => [field_0],
862});
863crate::retained::retained_copy!(OrderDirection);
864crate::retained::retained_fields!(OrderSpec {
865Self{fields} => [fields],
866});
867crate::retained::retained_fields!(OrderTerm {
868Self{expr,direction} => [expr,direction],
869});
870crate::retained::retained_fields!(PageSpec {
871Self{limit,offset} => [limit,offset],
872});
873crate::retained::retained_fields!(PlannerRouteProfile {
874Self{continuation_policy,logical_pushdown_eligibility,secondary_order_contract} => [continuation_policy,logical_pushdown_eligibility,secondary_order_contract],
875});
876crate::retained::retained_copy!(QueryMode);
877crate::retained::retained_fields!(ScalarPlan {
878Self{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],
879});