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