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