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::{CompareOp, 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    value::Value,
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(crate) limit: Option<u32>,
47    pub(crate) 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(crate) limit: Option<u32>,
74    pub(crate) 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(crate) struct OrderTerm {
112    pub(crate) expr: Expr,
113    pub(crate) direction: OrderDirection,
114}
115
116impl OrderTerm {
117    /// Construct one planner-owned ORDER BY term from one semantic expression.
118    #[must_use]
119    pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) fn rendered_label(&self) -> String {
148        render_scalar_projection_expr_sql_label(&self.expr)
149    }
150
151    /// Return the executor-facing direction for this ORDER BY term.
152    #[must_use]
153    pub(crate) 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///
181/// OrderSpec
182///
183/// Executor-facing ordering specification.
184/// Carries the canonical ordered term list after planner expression lowering.
185///
186#[derive(Clone, Debug, Eq, PartialEq)]
187pub(crate) struct OrderSpec {
188    pub(crate) fields: Vec<OrderTerm>,
189}
190
191///
192/// DeleteLimitSpec
193/// Executor-facing ordered delete window.
194///
195
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
197pub(crate) struct DeleteLimitSpec {
198    pub(crate) limit: Option<u32>,
199    pub(crate) offset: u32,
200}
201
202///
203/// DistinctExecutionStrategy
204///
205/// Planner-owned scalar DISTINCT execution strategy.
206/// This is execution-mechanics only and must not be used for semantic
207/// admissibility decisions.
208///
209
210#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub(crate) enum DistinctExecutionStrategy {
212    None,
213    PreOrdered,
214    HashMaterialize,
215}
216
217impl DistinctExecutionStrategy {
218    /// Return true when scalar DISTINCT execution is enabled.
219    #[must_use]
220    pub(crate) const fn is_enabled(self) -> bool {
221        !matches!(self, Self::None)
222    }
223}
224
225///
226/// PlannerRouteProfile
227///
228/// Planner-projected route profile consumed by executor route planning.
229/// Carries planner-owned continuation policy plus deterministic order/pushdown
230/// contracts that route/load layers must honor without recomputing order shape.
231///
232
233#[derive(Clone, Debug, Eq, PartialEq)]
234pub(in crate::db) struct PlannerRouteProfile {
235    continuation_policy: ContinuationPolicy,
236    logical_pushdown_eligibility: LogicalPushdownEligibility,
237    secondary_order_contract: Option<DeterministicSecondaryOrderContract>,
238}
239
240impl PlannerRouteProfile {
241    /// Construct one planner-projected route profile.
242    #[must_use]
243    pub(in crate::db) const fn new(
244        continuation_policy: ContinuationPolicy,
245        logical_pushdown_eligibility: LogicalPushdownEligibility,
246        secondary_order_contract: Option<DeterministicSecondaryOrderContract>,
247    ) -> Self {
248        Self {
249            continuation_policy,
250            logical_pushdown_eligibility,
251            secondary_order_contract,
252        }
253    }
254
255    /// Construct one fail-closed route profile for manually assembled plans
256    /// that have not yet been finalized against model authority.
257    #[must_use]
258    pub(in crate::db) const fn seeded_unfinalized(is_grouped: bool) -> Self {
259        Self {
260            continuation_policy: ContinuationPolicy::new(true, true, !is_grouped),
261            logical_pushdown_eligibility: LogicalPushdownEligibility::new(false, is_grouped, false),
262            secondary_order_contract: None,
263        }
264    }
265
266    /// Borrow planner-projected continuation policy contract.
267    #[must_use]
268    pub(in crate::db) const fn continuation_policy(&self) -> &ContinuationPolicy {
269        &self.continuation_policy
270    }
271
272    /// Borrow planner-owned logical pushdown eligibility contract.
273    #[must_use]
274    pub(in crate::db) const fn logical_pushdown_eligibility(&self) -> LogicalPushdownEligibility {
275        self.logical_pushdown_eligibility
276    }
277
278    /// Borrow the planner-owned deterministic secondary-order contract, if one exists.
279    #[must_use]
280    pub(in crate::db) const fn secondary_order_contract(
281        &self,
282    ) -> Option<&DeterministicSecondaryOrderContract> {
283        self.secondary_order_contract.as_ref()
284    }
285}
286
287///
288/// ContinuationPolicy
289///
290/// Planner-projected continuation contract carried into route/executor layers.
291/// This contract captures static continuation invariants and must not be
292/// rederived by route/load orchestration code.
293///
294
295#[derive(Clone, Copy, Debug, Eq, PartialEq)]
296pub(in crate::db) struct ContinuationPolicy {
297    requires_anchor: bool,
298    requires_strict_advance: bool,
299    is_grouped_safe: bool,
300}
301
302impl ContinuationPolicy {
303    /// Construct one planner-projected continuation policy contract.
304    #[must_use]
305    pub(in crate::db) const fn new(
306        requires_anchor: bool,
307        requires_strict_advance: bool,
308        is_grouped_safe: bool,
309    ) -> Self {
310        Self {
311            requires_anchor,
312            requires_strict_advance,
313            is_grouped_safe,
314        }
315    }
316
317    /// Return true when continuation resume paths require an anchor boundary.
318    #[must_use]
319    pub(in crate::db) const fn requires_anchor(self) -> bool {
320        self.requires_anchor
321    }
322
323    /// Return true when continuation resume paths require strict advancement.
324    #[must_use]
325    pub(in crate::db) const fn requires_strict_advance(self) -> bool {
326        self.requires_strict_advance
327    }
328
329    /// Return true when grouped continuation usage is semantically safe.
330    #[must_use]
331    pub(in crate::db) const fn is_grouped_safe(self) -> bool {
332        self.is_grouped_safe
333    }
334}
335
336///
337/// ExecutionShapeSignature
338///
339/// Immutable planner-projected semantic shape signature contract.
340/// Continuation transport encodes this contract; route/load consume it as a
341/// read-only execution identity boundary without re-deriving semantics.
342///
343
344#[derive(Clone, Copy, Debug, Eq, PartialEq)]
345pub(in crate::db) struct ExecutionShapeSignature {
346    continuation_signature: ContinuationSignature,
347}
348
349impl ExecutionShapeSignature {
350    /// Construct one immutable execution-shape signature contract.
351    #[must_use]
352    pub(in crate::db) const fn new(continuation_signature: ContinuationSignature) -> Self {
353        Self {
354            continuation_signature,
355        }
356    }
357
358    /// Borrow the canonical continuation signature for this execution shape.
359    #[must_use]
360    pub(in crate::db) const fn continuation_signature(self) -> ContinuationSignature {
361        self.continuation_signature
362    }
363}
364
365///
366/// PageSpec
367/// Executor-facing pagination specification.
368///
369
370#[derive(Clone, Debug, Eq, PartialEq)]
371pub(crate) struct PageSpec {
372    pub(crate) limit: Option<u32>,
373    pub(crate) offset: u32,
374}
375
376///
377/// AggregateKind
378///
379/// Canonical aggregate terminal taxonomy owned by query planning.
380/// All layers (query, explain, fingerprint, executor) must interpret aggregate
381/// terminal semantics through this single enum authority.
382/// Executor must derive traversal and fold direction exclusively from this enum.
383///
384
385#[derive(Clone, Copy, Debug, Eq, PartialEq)]
386pub enum AggregateKind {
387    Count,
388    Sum,
389    Avg,
390    Exists,
391    Min,
392    Max,
393    First,
394    Last,
395}
396
397impl AggregateKind {
398    /// Return the canonical uppercase SQL/render label for this aggregate kind.
399    #[must_use]
400    pub(in crate::db) const fn sql_label(self) -> &'static str {
401        match self {
402            Self::Count => "COUNT",
403            Self::Sum => "SUM",
404            Self::Avg => "AVG",
405            Self::Exists => "EXISTS",
406            Self::First => "FIRST",
407            Self::Last => "LAST",
408            Self::Min => "MIN",
409            Self::Max => "MAX",
410        }
411    }
412
413    /// Return whether this terminal kind is `COUNT`.
414    #[must_use]
415    pub(crate) const fn is_count(self) -> bool {
416        matches!(self, Self::Count)
417    }
418
419    /// Return whether this terminal kind belongs to the SUM/AVG numeric fold family.
420    #[must_use]
421    pub(in crate::db) const fn is_sum(self) -> bool {
422        matches!(self, Self::Sum | Self::Avg)
423    }
424
425    /// Return whether this terminal kind belongs to the extrema family.
426    #[must_use]
427    pub(in crate::db) const fn is_extrema(self) -> bool {
428        matches!(self, Self::Min | Self::Max)
429    }
430
431    /// Return whether reducer updates for this kind require a decoded id payload.
432    #[must_use]
433    pub(in crate::db) const fn requires_decoded_id(self) -> bool {
434        !matches!(self, Self::Count | Self::Sum | Self::Avg | Self::Exists)
435    }
436
437    /// Return whether grouped aggregate DISTINCT is supported for this kind.
438    #[must_use]
439    pub(in crate::db) const fn supports_grouped_distinct_v1(self) -> bool {
440        matches!(
441            self,
442            Self::Count | Self::Min | Self::Max | Self::Sum | Self::Avg
443        )
444    }
445
446    /// Return whether global DISTINCT aggregate shape is supported without GROUP BY keys.
447    #[must_use]
448    pub(in crate::db) const fn supports_global_distinct_without_group_keys(self) -> bool {
449        matches!(self, Self::Count | Self::Sum | Self::Avg)
450    }
451
452    /// Return the canonical extrema traversal direction for this kind.
453    #[must_use]
454    pub(crate) const fn extrema_direction(self) -> Option<Direction> {
455        match self {
456            Self::Min => Some(Direction::Asc),
457            Self::Max => Some(Direction::Desc),
458            Self::Count | Self::Sum | Self::Avg | Self::Exists | Self::First | Self::Last => None,
459        }
460    }
461
462    /// Return the canonical materialized fold direction for this kind.
463    #[must_use]
464    pub(crate) const fn materialized_fold_direction(self) -> Direction {
465        match self {
466            Self::Min => Direction::Desc,
467            Self::Count
468            | Self::Sum
469            | Self::Avg
470            | Self::Exists
471            | Self::Max
472            | Self::First
473            | Self::Last => Direction::Asc,
474        }
475    }
476
477    /// Return true when this kind can use bounded aggregate probe hints.
478    #[must_use]
479    pub(crate) const fn supports_bounded_probe_hint(self) -> bool {
480        !self.is_count() && !self.is_sum()
481    }
482
483    /// Derive a bounded aggregate probe fetch hint for this kind.
484    #[must_use]
485    pub(crate) fn bounded_probe_fetch_hint(
486        self,
487        direction: Direction,
488        offset: usize,
489        page_limit: Option<usize>,
490    ) -> Option<usize> {
491        match self {
492            Self::Exists | Self::First => Some(offset.saturating_add(1)),
493            Self::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
494            Self::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
495            Self::Last => page_limit.map(|limit| offset.saturating_add(limit)),
496            Self::Count | Self::Sum | Self::Avg | Self::Min | Self::Max => None,
497        }
498    }
499
500    /// Return the explain projection mode label for this kind and projection surface.
501    #[must_use]
502    pub(in crate::db) const fn explain_projection_mode_label(
503        self,
504        has_projected_field: bool,
505        covering_projection: bool,
506    ) -> &'static str {
507        if has_projected_field {
508            if covering_projection {
509                "field_idx"
510            } else {
511                "field_mat"
512            }
513        } else if matches!(self, Self::Min | Self::Max | Self::First | Self::Last) {
514            "entity_term"
515        } else {
516            "scalar_agg"
517        }
518    }
519
520    /// Return whether this terminal kind can remain covering on existing-row plans.
521    #[must_use]
522    pub(in crate::db) const fn supports_covering_existing_rows_terminal(self) -> bool {
523        matches!(self, Self::Count | Self::Exists)
524    }
525}
526
527///
528/// GroupAggregateSpec
529///
530/// One grouped aggregate terminal specification declared at query-plan time.
531/// `input_expr` is the single semantic source for grouped aggregate identity.
532/// Field-target behavior is derived from plain `Expr::Field` leaves so grouped
533/// semantics, explain, fingerprinting, and runtime do not carry a second
534/// compatibility shape beside the canonical aggregate input expression.
535///
536
537#[derive(Clone, Debug)]
538pub(crate) struct GroupAggregateSpec {
539    pub(crate) kind: AggregateKind,
540    #[cfg(test)]
541    #[allow(dead_code)]
542    #[cfg(test)]
543    pub(crate) target_field: Option<String>,
544    pub(crate) input_expr: Option<Box<Expr>>,
545    pub(crate) filter_expr: Option<Box<Expr>>,
546    pub(crate) distinct: bool,
547}
548
549impl PartialEq for GroupAggregateSpec {
550    fn eq(&self, other: &Self) -> bool {
551        self.kind == other.kind
552            && self.input_expr == other.input_expr
553            && self.filter_expr == other.filter_expr
554            && self.distinct == other.distinct
555    }
556}
557
558impl Eq for GroupAggregateSpec {}
559
560///
561/// FieldSlot
562///
563/// Canonical resolved field reference used by logical planning.
564/// `index` is the stable slot in `EntityModel::fields`; `field` is retained
565/// for diagnostics and explain surfaces.
566/// `kind` freezes planner-resolved field metadata so executor boundaries do
567/// not need to reopen `EntityModel` just to recover type/capability shape.
568///
569
570#[derive(Clone, Debug)]
571pub(crate) struct FieldSlot {
572    pub(crate) index: usize,
573    pub(crate) field: String,
574    pub(crate) kind: Option<FieldKind>,
575}
576
577impl PartialEq for FieldSlot {
578    fn eq(&self, other: &Self) -> bool {
579        self.index == other.index && self.field == other.field
580    }
581}
582
583impl Eq for FieldSlot {}
584
585///
586/// GroupedExecutionConfig
587///
588/// Declarative grouped-execution budget policy selected by query planning.
589/// This remains planner-owned input; executor policy bridges may still apply
590/// defaults and enforcement strategy at runtime boundaries.
591///
592
593#[derive(Clone, Copy, Debug, Eq, PartialEq)]
594pub(crate) struct GroupedExecutionConfig {
595    pub(crate) max_groups: u64,
596    pub(crate) max_group_bytes: u64,
597}
598
599///
600/// GroupSpec
601///
602/// Declarative GROUP BY stage contract attached to a validated base plan.
603/// This wrapper is intentionally semantic-only; field-slot resolution and
604/// execution-mode derivation remain executor-owned boundaries.
605///
606
607#[derive(Clone, Debug, Eq, PartialEq)]
608pub(crate) struct GroupSpec {
609    pub(crate) group_fields: Vec<FieldSlot>,
610    pub(crate) aggregates: Vec<GroupAggregateSpec>,
611    pub(crate) execution: GroupedExecutionConfig,
612}
613
614///
615/// GroupHavingSymbol
616///
617/// Reference to one grouped HAVING input symbol.
618/// Group-field symbols reference resolved grouped key slots.
619/// Aggregate symbols reference grouped aggregate outputs by declaration index.
620///
621
622#[derive(Clone, Debug, Eq, PartialEq)]
623pub(crate) enum GroupHavingSymbol {
624    GroupField(FieldSlot),
625    AggregateIndex(usize),
626}
627
628///
629/// GroupHavingClause
630///
631/// One conservative grouped HAVING clause.
632/// This clause model intentionally supports one symbol-to-literal comparison
633/// and excludes arbitrary expression trees in grouped v1.
634///
635
636#[derive(Clone, Debug, Eq, PartialEq)]
637pub(crate) struct GroupHavingClause {
638    pub(crate) symbol: GroupHavingSymbol,
639    pub(crate) op: CompareOp,
640    pub(crate) value: Value,
641}
642
643///
644/// ScalarPlan
645///
646/// Pure scalar logical query intent produced by the planner.
647///
648/// A `ScalarPlan` represents the access-independent query semantics:
649/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
650/// and read-consistency mode.
651///
652/// Design notes:
653/// - Predicates are applied *after* data access
654/// - Ordering is applied after filtering
655/// - Pagination is applied after ordering (load only)
656/// - Delete limits are applied after ordering (delete only)
657/// - Missing-row policy is explicit and must not depend on access strategy
658///
659/// This struct is the logical compiler stage output and intentionally excludes
660/// access-path details.
661///
662
663#[derive(Clone, Debug, Eq, PartialEq)]
664pub(crate) struct ScalarPlan {
665    /// Load vs delete intent.
666    pub(crate) mode: QueryMode,
667
668    /// Optional residual predicate applied after access.
669    pub(crate) predicate: Option<PredicateExecutionModel>,
670
671    /// Optional ordering specification.
672    pub(crate) order: Option<OrderSpec>,
673
674    /// Optional distinct semantics over ordered rows.
675    pub(crate) distinct: bool,
676
677    /// Optional ordered delete window (delete intents only).
678    pub(crate) delete_limit: Option<DeleteLimitSpec>,
679
680    /// Optional pagination specification.
681    pub(crate) page: Option<PageSpec>,
682
683    /// Missing-row policy for execution.
684    pub(crate) consistency: MissingRowPolicy,
685}
686
687///
688/// GroupPlan
689///
690/// Pure grouped logical intent emitted by grouped planning.
691/// Group metadata is carried through one canonical `GroupSpec` contract.
692///
693
694#[derive(Clone, Debug, Eq, PartialEq)]
695pub(crate) struct GroupPlan {
696    pub(crate) scalar: ScalarPlan,
697    pub(crate) group: GroupSpec,
698    pub(crate) having_expr: Option<Expr>,
699}
700
701///
702/// LogicalPlan
703///
704/// Exclusive logical query intent emitted by planning.
705/// Scalar and grouped semantics are distinct variants by construction.
706///
707
708// Logical plans keep scalar and grouped shapes inline because planner/executor handoff
709// passes these variants by ownership and boxing would widen that boundary for little benefit.
710#[derive(Clone, Debug, Eq, PartialEq)]
711pub(crate) enum LogicalPlan {
712    Scalar(ScalarPlan),
713    Grouped(GroupPlan),
714}