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