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