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