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