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