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, Expr, 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` keeps the direct field-target fast path explicit for grouped
449/// streaming/distinct policy and route hints.
450/// `input_expr` carries the canonical aggregate input shape so grouped
451/// semantics, explain, fingerprinting, and runtime do not split again on
452/// field-only versus expression-backed aggregate inputs.
453///
454
455#[derive(Clone, Debug, Eq, PartialEq)]
456pub(crate) struct GroupAggregateSpec {
457    pub(crate) kind: AggregateKind,
458    pub(crate) target_field: Option<String>,
459    pub(crate) input_expr: Option<Box<Expr>>,
460    pub(crate) distinct: bool,
461}
462
463///
464/// FieldSlot
465///
466/// Canonical resolved field reference used by logical planning.
467/// `index` is the stable slot in `EntityModel::fields`; `field` is retained
468/// for diagnostics and explain surfaces.
469/// `kind` freezes planner-resolved field metadata so executor boundaries do
470/// not need to reopen `EntityModel` just to recover type/capability shape.
471///
472
473#[derive(Clone, Debug)]
474pub(crate) struct FieldSlot {
475    pub(crate) index: usize,
476    pub(crate) field: String,
477    pub(crate) kind: Option<FieldKind>,
478}
479
480impl PartialEq for FieldSlot {
481    fn eq(&self, other: &Self) -> bool {
482        self.index == other.index && self.field == other.field
483    }
484}
485
486impl Eq for FieldSlot {}
487
488///
489/// GroupedExecutionConfig
490///
491/// Declarative grouped-execution budget policy selected by query planning.
492/// This remains planner-owned input; executor policy bridges may still apply
493/// defaults and enforcement strategy at runtime boundaries.
494///
495
496#[derive(Clone, Copy, Debug, Eq, PartialEq)]
497pub(crate) struct GroupedExecutionConfig {
498    pub(crate) max_groups: u64,
499    pub(crate) max_group_bytes: u64,
500}
501
502///
503/// GroupSpec
504///
505/// Declarative GROUP BY stage contract attached to a validated base plan.
506/// This wrapper is intentionally semantic-only; field-slot resolution and
507/// execution-mode derivation remain executor-owned boundaries.
508///
509
510#[derive(Clone, Debug, Eq, PartialEq)]
511pub(crate) struct GroupSpec {
512    pub(crate) group_fields: Vec<FieldSlot>,
513    pub(crate) aggregates: Vec<GroupAggregateSpec>,
514    pub(crate) execution: GroupedExecutionConfig,
515}
516
517///
518/// GroupHavingSymbol
519///
520/// Reference to one grouped HAVING input symbol.
521/// Group-field symbols reference resolved grouped key slots.
522/// Aggregate symbols reference grouped aggregate outputs by declaration index.
523///
524
525#[derive(Clone, Debug, Eq, PartialEq)]
526pub(crate) enum GroupHavingSymbol {
527    GroupField(FieldSlot),
528    AggregateIndex(usize),
529}
530
531///
532/// GroupHavingClause
533///
534/// One conservative grouped HAVING clause.
535/// This clause model intentionally supports one symbol-to-literal comparison
536/// and excludes arbitrary expression trees in grouped v1.
537///
538
539#[derive(Clone, Debug, Eq, PartialEq)]
540pub(crate) struct GroupHavingClause {
541    pub(crate) symbol: GroupHavingSymbol,
542    pub(crate) op: CompareOp,
543    pub(crate) value: Value,
544}
545
546///
547/// GroupHavingValueExpr
548///
549/// Slot-resolved grouped HAVING value expression.
550/// Leaves are restricted to grouped key slots, finalized aggregate outputs,
551/// and literals so grouped HAVING stays on the post-aggregate surface.
552///
553
554#[derive(Clone, Debug, Eq, PartialEq)]
555pub(crate) enum GroupHavingValueExpr {
556    GroupField(FieldSlot),
557    AggregateIndex(usize),
558    Literal(Value),
559    FunctionCall {
560        function: Function,
561        args: Vec<Self>,
562    },
563    Binary {
564        op: BinaryOp,
565        left: Box<Self>,
566        right: Box<Self>,
567    },
568}
569
570///
571/// GroupHavingExpr
572///
573/// Post-aggregate grouped HAVING boolean expression.
574/// This is the `0.86` grouped HAVING backbone: grouped runtime evaluates this
575/// tree over finalized grouped outputs without changing grouping mechanics.
576///
577
578// Grouped HAVING keeps compare nodes inline so the runtime evaluator can recurse over one
579// owned tree shape without adding another layer of pointer chasing to every compare node.
580#[expect(clippy::large_enum_variant)]
581#[derive(Clone, Debug, Eq, PartialEq)]
582pub(crate) enum GroupHavingExpr {
583    Compare {
584        left: GroupHavingValueExpr,
585        op: CompareOp,
586        right: GroupHavingValueExpr,
587    },
588    And(Vec<Self>),
589}
590
591///
592/// ScalarPlan
593///
594/// Pure scalar logical query intent produced by the planner.
595///
596/// A `ScalarPlan` represents the access-independent query semantics:
597/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
598/// and read-consistency mode.
599///
600/// Design notes:
601/// - Predicates are applied *after* data access
602/// - Ordering is applied after filtering
603/// - Pagination is applied after ordering (load only)
604/// - Delete limits are applied after ordering (delete only)
605/// - Missing-row policy is explicit and must not depend on access strategy
606///
607/// This struct is the logical compiler stage output and intentionally excludes
608/// access-path details.
609///
610
611#[derive(Clone, Debug, Eq, PartialEq)]
612pub(crate) struct ScalarPlan {
613    /// Load vs delete intent.
614    pub(crate) mode: QueryMode,
615
616    /// Optional residual predicate applied after access.
617    pub(crate) predicate: Option<PredicateExecutionModel>,
618
619    /// Optional ordering specification.
620    pub(crate) order: Option<OrderSpec>,
621
622    /// Optional distinct semantics over ordered rows.
623    pub(crate) distinct: bool,
624
625    /// Optional ordered delete window (delete intents only).
626    pub(crate) delete_limit: Option<DeleteLimitSpec>,
627
628    /// Optional pagination specification.
629    pub(crate) page: Option<PageSpec>,
630
631    /// Missing-row policy for execution.
632    pub(crate) consistency: MissingRowPolicy,
633}
634
635///
636/// GroupPlan
637///
638/// Pure grouped logical intent emitted by grouped planning.
639/// Group metadata is carried through one canonical `GroupSpec` contract.
640///
641
642#[derive(Clone, Debug, Eq, PartialEq)]
643pub(crate) struct GroupPlan {
644    pub(crate) scalar: ScalarPlan,
645    pub(crate) group: GroupSpec,
646    pub(crate) having_expr: Option<GroupHavingExpr>,
647}
648
649///
650/// LogicalPlan
651///
652/// Exclusive logical query intent emitted by planning.
653/// Scalar and grouped semantics are distinct variants by construction.
654///
655
656// Logical plans keep scalar and grouped shapes inline because planner/executor handoff
657// passes these variants by ownership and boxing would widen that boundary for little benefit.
658#[expect(clippy::large_enum_variant)]
659#[derive(Clone, Debug, Eq, PartialEq)]
660pub(crate) enum LogicalPlan {
661    Scalar(ScalarPlan),
662    Grouped(GroupPlan),
663}