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