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