Skip to main content

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

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