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