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