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
420impl GlobalDistinctAggregateKind {}
421
422///
423/// GroupedPlanAggregateFamily
424///
425/// Planner-owned grouped aggregate-family profile.
426/// This is intentionally coarse and execution-oriented: it captures which
427/// grouped aggregate family the planner admitted so runtime can select grouped
428/// execution paths without rebuilding family policy from raw aggregate
429/// expressions again.
430///
431
432#[derive(Clone, Copy, Debug, Eq, PartialEq)]
433pub(in crate::db) enum GroupedPlanAggregateFamily {
434    CountRowsOnly,
435    FieldTargetRows,
436    GenericRows,
437}
438
439impl GroupedPlanAggregateFamily {
440    /// Return the stable planner-owned aggregate-family code.
441    #[must_use]
442    pub(in crate::db) const fn code(self) -> &'static str {
443        match self {
444            Self::CountRowsOnly => "count_rows_only",
445            Self::FieldTargetRows => "field_target_rows",
446            Self::GenericRows => "generic_rows",
447        }
448    }
449}
450
451impl AggregateKind {
452    /// Return the canonical uppercase render label for this aggregate kind.
453    #[must_use]
454    pub(in crate::db) const fn canonical_label(self) -> &'static str {
455        match self {
456            Self::Count => "COUNT",
457            Self::Sum => "SUM",
458            Self::Avg => "AVG",
459            Self::Exists => "EXISTS",
460            Self::First => "FIRST",
461            Self::Last => "LAST",
462            Self::Min => "MIN",
463            Self::Max => "MAX",
464        }
465    }
466
467    /// Return whether this terminal kind is `COUNT`.
468    #[must_use]
469    pub(in crate::db) const fn is_count(self) -> bool {
470        matches!(self, Self::Count)
471    }
472
473    /// Return whether this terminal kind belongs to the SUM/AVG numeric fold family.
474    #[must_use]
475    pub(in crate::db) const fn is_sum(self) -> bool {
476        matches!(self, Self::Sum | Self::Avg)
477    }
478
479    /// Return whether this terminal kind belongs to the extrema family.
480    #[must_use]
481    pub(in crate::db) const fn is_extrema(self) -> bool {
482        matches!(self, Self::Min | Self::Max)
483    }
484
485    /// Return whether this kind supports one grouped or global field target.
486    #[must_use]
487    pub(in crate::db) const fn supports_field_target_v1(self) -> bool {
488        matches!(
489            self,
490            Self::Count | Self::Sum | Self::Avg | Self::Min | Self::Max
491        )
492    }
493
494    /// Return whether reducer updates for this kind require a decoded id payload.
495    #[must_use]
496    pub(in crate::db) const fn requires_decoded_id(self) -> bool {
497        !matches!(self, Self::Count | Self::Sum | Self::Avg | Self::Exists)
498    }
499
500    /// Return whether grouped aggregate DISTINCT is supported for this kind.
501    #[must_use]
502    pub(in crate::db) const fn supports_grouped_distinct_v1(self) -> bool {
503        matches!(self, Self::Count | Self::Sum | Self::Avg)
504    }
505
506    /// Return the stable aggregate discriminant used by projection and
507    /// aggregate fingerprint hashing.
508    #[must_use]
509    pub(in crate::db::query) const fn fingerprint_tag(self) -> u8 {
510        match self {
511            Self::Count => 0x01,
512            Self::Sum => 0x02,
513            Self::Exists => 0x03,
514            Self::Min => 0x04,
515            Self::Max => 0x05,
516            Self::First => 0x06,
517            Self::Last => 0x07,
518            Self::Avg => 0x08,
519        }
520    }
521
522    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
523    #[must_use]
524    pub(in crate::db) const fn global_distinct_kind(self) -> Option<GlobalDistinctAggregateKind> {
525        match self {
526            Self::Count => Some(GlobalDistinctAggregateKind::Count),
527            Self::Sum => Some(GlobalDistinctAggregateKind::Sum),
528            Self::Avg => Some(GlobalDistinctAggregateKind::Avg),
529            Self::Exists | Self::Min | Self::Max | Self::First | Self::Last => None,
530        }
531    }
532
533    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
534    #[must_use]
535    pub(in crate::db) const fn supports_global_distinct_without_group_keys(self) -> bool {
536        self.global_distinct_kind().is_some()
537    }
538
539    /// Return the planner-owned grouped aggregate-family profile for one aggregate shape.
540    #[must_use]
541    pub(in crate::db) const fn grouped_plan_family(
542        self,
543        has_target_field: bool,
544    ) -> GroupedPlanAggregateFamily {
545        if has_target_field && self.supports_field_target_v1() {
546            GroupedPlanAggregateFamily::FieldTargetRows
547        } else {
548            GroupedPlanAggregateFamily::GenericRows
549        }
550    }
551
552    /// Return whether this grouped aggregate shape supports ordered grouped streaming.
553    #[must_use]
554    pub(in crate::db) const fn supports_grouped_streaming_v1(
555        self,
556        has_target_field: bool,
557        distinct: bool,
558    ) -> bool {
559        if self.supports_field_target_v1() {
560            return !distinct && (self.is_count() || has_target_field);
561        }
562
563        !has_target_field && (!distinct || self.supports_grouped_distinct_v1())
564    }
565
566    /// Return the canonical extrema traversal direction for this kind.
567    #[must_use]
568    pub(in crate::db) const fn extrema_direction(self) -> Option<Direction> {
569        match self {
570            Self::Min => Some(Direction::Asc),
571            Self::Max => Some(Direction::Desc),
572            Self::Count | Self::Sum | Self::Avg | Self::Exists | Self::First | Self::Last => None,
573        }
574    }
575
576    /// Return the canonical materialized fold direction for this kind.
577    #[must_use]
578    pub(in crate::db) const fn materialized_fold_direction(self) -> Direction {
579        match self {
580            Self::Min => Direction::Desc,
581            Self::Count
582            | Self::Sum
583            | Self::Avg
584            | Self::Exists
585            | Self::Max
586            | Self::First
587            | Self::Last => Direction::Asc,
588        }
589    }
590
591    /// Return true when this kind can use bounded aggregate probe hints.
592    #[must_use]
593    pub(in crate::db) const fn supports_bounded_probe_hint(self) -> bool {
594        !self.is_count() && !self.is_sum()
595    }
596
597    /// Derive a bounded aggregate probe fetch hint for this kind.
598    #[must_use]
599    pub(in crate::db) fn bounded_probe_fetch_hint(
600        self,
601        direction: Direction,
602        offset: usize,
603        page_limit: Option<usize>,
604    ) -> Option<usize> {
605        match self {
606            Self::Exists | Self::First => Some(offset.saturating_add(1)),
607            Self::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
608            Self::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
609            Self::Last => page_limit.map(|limit| offset.saturating_add(limit)),
610            Self::Count | Self::Sum | Self::Avg | Self::Min | Self::Max => None,
611        }
612    }
613
614    /// Return the explain projection mode label for this kind and projection surface.
615    #[must_use]
616    pub(in crate::db) const fn explain_projection_mode_label(
617        self,
618        has_projected_field: bool,
619        covering_projection: bool,
620    ) -> &'static str {
621        if has_projected_field {
622            if covering_projection {
623                "field_idx"
624            } else {
625                "field_mat"
626            }
627        } else if matches!(self, Self::Min | Self::Max | Self::First | Self::Last) {
628            "entity_term"
629        } else {
630            "scalar_agg"
631        }
632    }
633
634    /// Return whether this terminal kind can remain covering on existing-row plans.
635    #[must_use]
636    pub(in crate::db) const fn supports_covering_existing_rows_terminal(self) -> bool {
637        matches!(self, Self::Count | Self::Exists)
638    }
639}
640
641///
642/// GroupAggregateSpec
643///
644/// One grouped aggregate terminal specification declared at query-plan time.
645/// `input_expr` is the single expression source for grouped aggregate identity.
646/// Field-target behavior is derived from plain `Expr::Field` leaves so grouped
647/// semantics, explain, fingerprinting, and runtime do not carry a second
648/// compatibility shape beside the canonical aggregate input expression.
649///
650
651#[derive(Clone, Debug)]
652pub(in crate::db) struct GroupAggregateSpec {
653    pub(in crate::db) kind: AggregateKind,
654    pub(in crate::db) input_expr: Option<Box<Expr>>,
655    pub(in crate::db) filter_expr: Option<Box<Expr>>,
656    pub(in crate::db) distinct: bool,
657}
658
659impl PartialEq for GroupAggregateSpec {
660    fn eq(&self, other: &Self) -> bool {
661        self.semantic_key() == other.semantic_key()
662    }
663}
664
665impl Eq for GroupAggregateSpec {}
666
667impl GroupedPlanAggregateFamily {
668    /// Derive the grouped aggregate-family profile from one planner aggregate list.
669    #[must_use]
670    pub(in crate::db) fn from_grouped_aggregates(aggregates: &[GroupAggregateSpec]) -> Self {
671        if matches!(aggregates, [aggregate] if aggregate.identity().is_count_rows_only()) {
672            return Self::CountRowsOnly;
673        }
674
675        if aggregates.iter().all(|aggregate| {
676            aggregate
677                .kind()
678                .grouped_plan_family(aggregate.target_field().is_some())
679                == Self::FieldTargetRows
680        }) {
681            return Self::FieldTargetRows;
682        }
683
684        Self::GenericRows
685    }
686}
687
688///
689/// FieldSlot
690///
691/// Canonical resolved field reference used by logical planning.
692/// `index` is the stable slot in `EntityModel::fields`; `field` is retained
693/// for diagnostics and explain surfaces.
694/// `authority` freezes exactly one planner metadata source so accepted runtime
695/// slots cannot also carry generated fallback metadata.
696///
697
698#[derive(Clone, Debug)]
699pub(in crate::db::query::plan) enum FieldSlotAuthority {
700    Unresolved,
701    ModelOnly(FieldKind),
702    Accepted(AcceptedFieldKind),
703}
704
705#[derive(Clone, Debug)]
706pub(crate) struct FieldSlot {
707    pub(in crate::db) index: usize,
708    pub(in crate::db) field: String,
709    pub(in crate::db::query::plan) authority: FieldSlotAuthority,
710}
711
712impl PartialEq for FieldSlot {
713    fn eq(&self, other: &Self) -> bool {
714        self.index == other.index && self.field == other.field
715    }
716}
717
718impl Eq for FieldSlot {}
719
720///
721/// GroupedExecutionConfig
722///
723/// Declarative grouped-execution budget policy selected by query planning.
724/// This remains planner-owned input; executor policy bridges may still apply
725/// defaults and enforcement strategy at runtime boundaries.
726///
727
728#[derive(Clone, Copy, Debug, Eq, PartialEq)]
729pub(in crate::db) struct GroupedExecutionConfig {
730    pub(in crate::db) max_groups: u64,
731    pub(in crate::db) max_group_bytes: u64,
732}
733
734///
735/// GroupSpec
736///
737/// Declarative GROUP BY stage contract attached to a validated base plan.
738/// This wrapper is intentionally semantic-only; field-slot resolution and
739/// execution-mode derivation remain executor-owned boundaries.
740///
741
742#[derive(Clone, Debug, Eq, PartialEq)]
743pub(in crate::db) struct GroupSpec {
744    pub(in crate::db) group_fields: Vec<FieldSlot>,
745    pub(in crate::db) aggregates: Vec<GroupAggregateSpec>,
746    pub(in crate::db) execution: GroupedExecutionConfig,
747}
748
749///
750/// ScalarPlan
751///
752/// Pure scalar logical query intent produced by the planner.
753///
754/// A `ScalarPlan` represents the access-independent query semantics:
755/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
756/// and read-consistency mode.
757///
758/// Design notes:
759/// - Predicates are applied *after* data access
760/// - Ordering is applied after filtering
761/// - Pagination is applied after ordering (load only)
762/// - Delete limits are applied after ordering (delete only)
763/// - Missing-row policy is explicit and must not depend on access strategy
764///
765/// This struct is the logical compiler stage output and intentionally excludes
766/// access-path details.
767///
768
769#[derive(Clone, Debug, Eq, PartialEq)]
770pub(in crate::db) struct ScalarPlan {
771    /// Load vs delete intent.
772    pub(in crate::db) mode: QueryMode,
773
774    /// Optional planner-owned scalar filter expression.
775    pub(in crate::db) filter_expr: Option<Expr>,
776
777    /// Whether the predicate fully covers the scalar filter expression.
778    pub(in crate::db) predicate_covers_filter_expr: bool,
779
780    /// Optional residual predicate applied after access.
781    pub(in crate::db) predicate: Option<Predicate>,
782
783    /// Optional ordering specification.
784    pub(in crate::db) order: Option<OrderSpec>,
785
786    /// Optional distinct semantics over ordered rows.
787    pub(in crate::db) distinct: bool,
788
789    /// Optional ordered delete window (delete intents only).
790    pub(in crate::db) delete_limit: Option<DeleteLimitSpec>,
791
792    /// Optional pagination specification.
793    pub(in crate::db) page: Option<PageSpec>,
794
795    /// Missing-row policy for execution.
796    pub(in crate::db) consistency: MissingRowPolicy,
797}
798
799///
800/// GroupPlan
801///
802/// Pure grouped logical intent emitted by grouped planning.
803/// Group metadata is carried through one canonical `GroupSpec` contract.
804///
805
806#[derive(Clone, Debug, Eq, PartialEq)]
807pub(in crate::db) struct GroupPlan {
808    pub(in crate::db) scalar: ScalarPlan,
809    pub(in crate::db) group: GroupSpec,
810    pub(in crate::db) having_expr: Option<Expr>,
811}
812
813///
814/// LogicalPlan
815///
816/// Exclusive logical query intent emitted by planning.
817/// Scalar and grouped semantics are distinct variants by construction.
818///
819
820// Logical plans keep scalar and grouped shapes inline because planner/executor handoff
821// passes these variants by ownership and boxing would widen that boundary for little benefit.
822#[derive(Clone, Debug, Eq, PartialEq)]
823pub(in crate::db) enum LogicalPlan {
824    Scalar(ScalarPlan),
825    Grouped(GroupPlan),
826}