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