Skip to main content

icydb_core/db/query/plan/validate/
mod.rs

1//! Module: query::plan::validate
2//! Responsibility: planner-owned query semantic validation and plan-policy enforcement.
3//! Does not own: executor defensive runtime checks or execution-route dispatch semantics.
4//! Boundary: emits plan-domain typed errors before executor handoff.
5//!
6//! Query-plan validation for planner-owned logical semantics.
7//!
8//! Validation ownership contract:
9//! - `validate_query_semantics` owns user-facing query semantics and emits `PlanError`.
10//! - executor-boundary defensive checks live in `db::executor::plan_validate`.
11//!
12//! Future rule changes must declare a semantic owner. Defensive re-check layers may mirror
13//! rules, but must not reinterpret semantics or error class intent.
14
15mod core;
16mod cursor_policy;
17mod fluent_policy;
18mod grouped;
19mod intent_policy;
20mod order;
21mod plan_shape;
22mod symbols;
23
24use crate::db::{access::AccessPlanError, cursor::CursorPlanError, predicate::ValidateError};
25use thiserror::Error as ThisError;
26
27pub(crate) use core::{validate_group_query_semantics, validate_query_semantics};
28pub(crate) use cursor_policy::{
29    validate_cursor_order_plan_shape, validate_cursor_paging_requirements,
30};
31pub(crate) use fluent_policy::{validate_fluent_non_paged_mode, validate_fluent_paged_mode};
32#[cfg(test)]
33pub(in crate::db::query) use grouped::validate_group_projection_expr_compatibility_for_test;
34pub(crate) use intent_policy::{validate_intent_key_access_policy, validate_intent_plan_shape};
35pub(crate) use order::validate_order;
36pub(crate) use plan_shape::{has_explicit_order, validate_order_shape, validate_plan_shape};
37pub(crate) use symbols::resolve_group_field_slot;
38
39///
40/// PlanError
41///
42/// Root plan validation taxonomy split by domain axis.
43/// User-shape failures are grouped under `PlanUserError`.
44/// Policy/capability failures are grouped under `PlanPolicyError`.
45/// Cursor continuation failures remain in `CursorPlanError`.
46///
47
48#[derive(Debug, ThisError)]
49pub enum PlanError {
50    #[error("{0}")]
51    User(Box<PlanUserError>),
52
53    #[error("{0}")]
54    Policy(Box<PlanPolicyError>),
55
56    #[error("{0}")]
57    Cursor(Box<CursorPlanError>),
58}
59
60///
61/// PlanUserError
62///
63/// Planner user-shape validation failures independent of continuation cursors.
64/// This axis intentionally excludes runtime routing/execution policy state and
65/// release-gating capability decisions.
66///
67
68#[derive(Debug, ThisError)]
69pub enum PlanUserError {
70    #[error("predicate validation failed: {0}")]
71    PredicateInvalid(Box<ValidateError>),
72
73    #[error("{0}")]
74    Order(Box<OrderPlanError>),
75
76    #[error("{0}")]
77    Access(Box<AccessPlanError>),
78
79    #[error("{0}")]
80    Group(Box<GroupPlanError>),
81
82    #[error("{0}")]
83    Expr(Box<ExprPlanError>),
84}
85
86///
87/// PlanPolicyError
88///
89/// Planner policy/capability validation failures.
90/// This axis captures query-shape constraints that are valid syntactically but
91/// not supported by the current execution policy surface.
92///
93
94#[derive(Debug, ThisError)]
95pub enum PlanPolicyError {
96    #[error("{0}")]
97    Policy(Box<PolicyPlanError>),
98
99    #[error("{0}")]
100    Group(Box<GroupPlanError>),
101}
102
103///
104/// OrderPlanError
105///
106/// ORDER BY-specific validation failures.
107///
108
109#[derive(Debug, ThisError)]
110pub enum OrderPlanError {
111    /// ORDER BY references an unknown field.
112    #[error("unknown order field '{field}'")]
113    UnknownField { field: String },
114
115    /// ORDER BY references a field that cannot be ordered.
116    #[error("order field '{field}' is not orderable")]
117    UnorderableField { field: String },
118
119    /// ORDER BY references the same non-primary-key field multiple times.
120    #[error("order field '{field}' appears multiple times")]
121    DuplicateOrderField { field: String },
122
123    /// Ordered plans must terminate with the primary-key tie-break.
124    #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
125    MissingPrimaryKeyTieBreak { field: String },
126}
127
128///
129/// PolicyPlanError
130///
131/// Plan-shape policy failures.
132///
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
135pub enum PolicyPlanError {
136    /// ORDER BY must specify at least one field.
137    #[error("order specification must include at least one field")]
138    EmptyOrderSpec,
139
140    /// Delete plans must not carry offsets.
141    #[error("delete plans must not include OFFSET")]
142    DeletePlanWithOffset,
143
144    /// Delete plans must not carry grouped query wrappers.
145    #[error("delete plans must not include GROUP BY or HAVING")]
146    DeletePlanWithGrouping,
147
148    /// Delete plans must not carry pagination.
149    #[error("delete plans must not include pagination")]
150    DeletePlanWithPagination,
151
152    /// Load plans must not carry delete limits.
153    #[error("load plans must not include delete limits")]
154    LoadPlanWithDeleteLimit,
155
156    /// Delete limits require an explicit ordering.
157    #[error("delete limit requires an explicit ordering")]
158    DeleteLimitRequiresOrder,
159
160    /// Pagination requires an explicit ordering.
161    #[error(
162        "Unordered pagination is not allowed.\nThis query uses LIMIT or OFFSET without an ORDER BY clause.\nPagination without a total ordering is non-deterministic.\nAdd an explicit order_by(...) to make the query stable."
163    )]
164    UnorderedPagination,
165}
166
167///
168/// CursorPagingPolicyError
169///
170/// Cursor pagination readiness errors shared by intent/fluent entry surfaces.
171///
172
173#[derive(Clone, Copy, Debug, Eq, PartialEq, ThisError)]
174pub enum CursorPagingPolicyError {
175    #[error(
176        "{message}",
177        message = CursorPlanError::cursor_requires_order_message()
178    )]
179    CursorRequiresOrder,
180
181    #[error(
182        "{message}",
183        message = CursorPlanError::cursor_requires_limit_message()
184    )]
185    CursorRequiresLimit,
186}
187
188///
189/// GroupPlanError
190///
191/// GROUP BY wrapper validation failures owned by query planning.
192///
193
194#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
195pub enum GroupPlanError {
196    /// HAVING requires GROUP BY grouped plan shape.
197    #[error("HAVING is only supported for GROUP BY queries in this release")]
198    HavingRequiresGroupBy,
199
200    /// Grouped validation entrypoint received a scalar logical plan.
201    #[error("group query validation requires grouped logical plan variant")]
202    GroupedLogicalPlanRequired,
203
204    /// GROUP BY requires at least one declared grouping field.
205    #[error("group specification must include at least one group field")]
206    EmptyGroupFields,
207
208    /// Global DISTINCT aggregate shapes without GROUP BY are restricted.
209    #[error(
210        "global DISTINCT aggregate without GROUP BY must declare exactly one DISTINCT field-target aggregate in this release"
211    )]
212    GlobalDistinctAggregateShapeUnsupported,
213
214    /// GROUP BY requires at least one aggregate terminal.
215    #[error("group specification must include at least one aggregate terminal")]
216    EmptyAggregates,
217
218    /// GROUP BY references an unknown group field.
219    #[error("unknown group field '{field}'")]
220    UnknownGroupField { field: String },
221
222    /// GROUP BY must not repeat the same resolved group slot.
223    #[error("group specification has duplicate group key: '{field}'")]
224    DuplicateGroupField { field: String },
225
226    /// GROUP BY v1 does not accept DISTINCT unless adjacency eligibility is explicit.
227    #[error(
228        "grouped DISTINCT requires adjacency-based ordered-group eligibility proof in this release"
229    )]
230    DistinctAdjacencyEligibilityRequired,
231
232    /// GROUP BY ORDER BY shape must start with grouped-key prefix.
233    #[error("grouped ORDER BY must start with GROUP BY key prefix in this release")]
234    OrderPrefixNotAlignedWithGroupKeys,
235
236    /// GROUP BY ORDER BY requires an explicit LIMIT in grouped v1.
237    #[error("grouped ORDER BY requires LIMIT in this release")]
238    OrderRequiresLimit,
239
240    /// HAVING with DISTINCT is deferred until grouped DISTINCT support expands.
241    #[error("grouped HAVING with DISTINCT is not supported in this release")]
242    DistinctHavingUnsupported,
243
244    /// HAVING currently supports compare operators only.
245    #[error("grouped HAVING clause at index={index} uses unsupported operator: {op}")]
246    HavingUnsupportedCompareOp { index: usize, op: String },
247
248    /// HAVING group-field symbols must reference declared grouped keys.
249    #[error("grouped HAVING clause at index={index} references non-group field '{field}'")]
250    HavingNonGroupFieldReference { index: usize, field: String },
251
252    /// HAVING aggregate references must resolve to declared grouped terminals.
253    #[error(
254        "grouped HAVING clause at index={index} references aggregate index {aggregate_index} but aggregate_count={aggregate_count}"
255    )]
256    HavingAggregateIndexOutOfBounds {
257        index: usize,
258        aggregate_index: usize,
259        aggregate_count: usize,
260    },
261
262    /// DISTINCT grouped terminal kinds are intentionally conservative in v1.
263    #[error(
264        "grouped DISTINCT aggregate at index={index} uses unsupported kind '{kind}' in this release"
265    )]
266    DistinctAggregateKindUnsupported { index: usize, kind: String },
267
268    /// DISTINCT over grouped field-target terminals is deferred with field-target support.
269    #[error(
270        "grouped DISTINCT aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
271    )]
272    DistinctAggregateFieldTargetUnsupported {
273        index: usize,
274        kind: String,
275        field: String,
276    },
277
278    /// Aggregate target fields must resolve in the model schema.
279    #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
280    UnknownAggregateTargetField { index: usize, field: String },
281
282    /// Global DISTINCT SUM requires a numeric field target.
283    #[error(
284        "global DISTINCT SUM aggregate target field at index={index} is not numeric: '{field}'"
285    )]
286    GlobalDistinctSumTargetNotNumeric { index: usize, field: String },
287
288    /// Field-target grouped terminals are not enabled in grouped execution v1.
289    #[error(
290        "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
291    )]
292    FieldTargetAggregatesUnsupported {
293        index: usize,
294        kind: String,
295        field: String,
296    },
297}
298
299///
300/// ExprPlanError
301///
302/// Expression-spine inference failures owned by planner semantics.
303///
304
305#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
306pub enum ExprPlanError {
307    /// Expression references a field that does not exist in schema.
308    #[error("unknown expression field '{field}'")]
309    UnknownExprField { field: String },
310
311    /// Aggregate terminal requires a numeric target field.
312    #[error("aggregate '{kind}' requires numeric target field '{field}'")]
313    NonNumericAggregateTarget { kind: String, field: String },
314
315    /// Aggregate expression requires an explicit target field.
316    #[error("aggregate '{kind}' requires an explicit target field")]
317    AggregateTargetRequired { kind: String },
318
319    /// Unary operation is incompatible with inferred operand type.
320    #[error("unary operator '{op}' is incompatible with operand type {found}")]
321    InvalidUnaryOperand { op: String, found: String },
322
323    /// Binary operation is incompatible with inferred operand types.
324    #[error("binary operator '{op}' is incompatible with operand types ({left}, {right})")]
325    InvalidBinaryOperands {
326        op: String,
327        left: String,
328        right: String,
329    },
330
331    /// GROUP BY projections must not reference fields outside grouped keys.
332    #[error(
333        "grouped projection expression at index={index} references fields outside GROUP BY keys"
334    )]
335    GroupedProjectionReferencesNonGroupField { index: usize },
336}
337
338///
339/// CursorOrderPlanShapeError
340///
341/// Logical cursor-order plan-shape failures used by cursor/runtime boundary adapters.
342///
343
344#[derive(Clone, Copy, Debug, Eq, PartialEq)]
345pub(crate) enum CursorOrderPlanShapeError {
346    MissingExplicitOrder,
347    EmptyOrderSpec,
348}
349
350///
351/// IntentKeyAccessKind
352///
353/// Key-access shape used by intent policy validation.
354///
355
356#[derive(Clone, Copy, Debug, Eq, PartialEq)]
357pub(crate) enum IntentKeyAccessKind {
358    Single,
359    Many,
360    Only,
361}
362
363///
364/// IntentKeyAccessPolicyViolation
365///
366/// Logical key-access policy violations at query-intent boundaries.
367///
368#[derive(Clone, Copy, Debug, Eq, PartialEq)]
369pub(crate) enum IntentKeyAccessPolicyViolation {
370    KeyAccessConflict,
371    ByIdsWithPredicate,
372    OnlyWithPredicate,
373}
374
375///
376/// FluentLoadPolicyViolation
377///
378/// Fluent load-entry policy violations.
379///
380
381#[derive(Clone, Copy, Debug, Eq, PartialEq)]
382pub(crate) enum FluentLoadPolicyViolation {
383    CursorRequiresPagedExecution,
384    GroupedRequiresExecuteGrouped,
385    CursorRequiresOrder,
386    CursorRequiresLimit,
387}
388
389impl From<ValidateError> for PlanError {
390    fn from(err: ValidateError) -> Self {
391        Self::from(PlanUserError::from(err))
392    }
393}
394
395impl From<OrderPlanError> for PlanError {
396    fn from(err: OrderPlanError) -> Self {
397        Self::from(PlanUserError::from(err))
398    }
399}
400
401impl From<AccessPlanError> for PlanError {
402    fn from(err: AccessPlanError) -> Self {
403        Self::from(PlanUserError::from(err))
404    }
405}
406
407impl From<PolicyPlanError> for PlanError {
408    fn from(err: PolicyPlanError) -> Self {
409        Self::from(PlanPolicyError::from(err))
410    }
411}
412
413impl From<CursorPlanError> for PlanError {
414    fn from(err: CursorPlanError) -> Self {
415        Self::Cursor(Box::new(err))
416    }
417}
418
419impl From<GroupPlanError> for PlanError {
420    fn from(err: GroupPlanError) -> Self {
421        if err.belongs_to_policy_axis() {
422            return Self::from(PlanPolicyError::from(err));
423        }
424
425        Self::from(PlanUserError::from(err))
426    }
427}
428
429impl From<ExprPlanError> for PlanError {
430    fn from(err: ExprPlanError) -> Self {
431        Self::from(PlanUserError::from(err))
432    }
433}
434
435impl From<PlanUserError> for PlanError {
436    fn from(err: PlanUserError) -> Self {
437        Self::User(Box::new(err))
438    }
439}
440
441impl From<PlanPolicyError> for PlanError {
442    fn from(err: PlanPolicyError) -> Self {
443        Self::Policy(Box::new(err))
444    }
445}
446
447impl From<ValidateError> for PlanUserError {
448    fn from(err: ValidateError) -> Self {
449        Self::PredicateInvalid(Box::new(err))
450    }
451}
452
453impl From<OrderPlanError> for PlanUserError {
454    fn from(err: OrderPlanError) -> Self {
455        Self::Order(Box::new(err))
456    }
457}
458
459impl From<AccessPlanError> for PlanUserError {
460    fn from(err: AccessPlanError) -> Self {
461        Self::Access(Box::new(err))
462    }
463}
464
465impl From<GroupPlanError> for PlanUserError {
466    fn from(err: GroupPlanError) -> Self {
467        Self::Group(Box::new(err))
468    }
469}
470
471impl From<ExprPlanError> for PlanUserError {
472    fn from(err: ExprPlanError) -> Self {
473        Self::Expr(Box::new(err))
474    }
475}
476
477impl From<PolicyPlanError> for PlanPolicyError {
478    fn from(err: PolicyPlanError) -> Self {
479        Self::Policy(Box::new(err))
480    }
481}
482
483impl From<GroupPlanError> for PlanPolicyError {
484    fn from(err: GroupPlanError) -> Self {
485        Self::Group(Box::new(err))
486    }
487}
488
489impl GroupPlanError {
490    // Group-plan variants that represent release-gating/capability constraints
491    // are classified under the policy axis to keep user-shape and policy
492    // domains separated at the top-level `PlanError`.
493    const fn belongs_to_policy_axis(&self) -> bool {
494        matches!(
495            self,
496            Self::GlobalDistinctAggregateShapeUnsupported
497                | Self::DistinctAdjacencyEligibilityRequired
498                | Self::OrderPrefixNotAlignedWithGroupKeys
499                | Self::OrderRequiresLimit
500                | Self::DistinctHavingUnsupported
501                | Self::HavingUnsupportedCompareOp { .. }
502                | Self::DistinctAggregateKindUnsupported { .. }
503                | Self::DistinctAggregateFieldTargetUnsupported { .. }
504                | Self::FieldTargetAggregatesUnsupported { .. }
505        )
506    }
507}