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