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