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}