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