Skip to main content

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

1//! Query-plan validation at logical and executor boundaries.
2//!
3//! Validation ownership contract:
4//! - `validate_logical_plan_model` owns user-facing query semantics and emits `PlanError`.
5//! - `validate_executor_plan` is defensive: it re-checks owned semantics/invariants before
6//!   execution and must not introduce new user-visible semantics.
7//!
8//! Future rule changes must declare a semantic owner. Defensive re-check layers may mirror
9//! rules, but must not reinterpret semantics or error class intent.
10
11mod access;
12mod order;
13mod pushdown;
14mod semantics;
15
16#[cfg(test)]
17mod tests;
18
19use crate::{
20    db::{
21        cursor::CursorDecodeError,
22        query::{
23            plan::LogicalPlan,
24            policy::{CursorOrderPolicyError, PlanPolicyError},
25            predicate::{self, SchemaInfo},
26        },
27    },
28    error::{ErrorClass, ErrorOrigin, InternalError},
29    model::{entity::EntityModel, index::IndexModel},
30    traits::EntityKind,
31    value::Value,
32};
33use thiserror::Error as ThisError;
34
35// re-exports
36pub(crate) use access::{validate_access_plan, validate_access_plan_model};
37pub(crate) use order::{validate_order, validate_primary_key_tie_break};
38#[cfg(test)]
39pub(crate) use pushdown::assess_secondary_order_pushdown_if_applicable;
40pub(crate) use pushdown::{
41    PushdownApplicability, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
42    SecondaryOrderPushdownRejection, assess_secondary_order_pushdown,
43    assess_secondary_order_pushdown_if_applicable_validated,
44};
45
46///
47/// PlanError
48///
49/// Executor-visible validation failures for logical plans.
50///
51/// These errors indicate that a plan cannot be safely executed against the
52/// current schema or entity definition. They are *not* planner bugs.
53///
54
55#[derive(Debug, ThisError)]
56pub enum PlanError {
57    #[error("predicate validation failed: {0}")]
58    PredicateInvalid(Box<predicate::ValidateError>),
59
60    #[error("{0}")]
61    Order(Box<OrderPlanError>),
62
63    #[error("{0}")]
64    Access(Box<AccessPlanError>),
65
66    #[error("{0}")]
67    Policy(Box<PolicyPlanError>),
68
69    #[error("{0}")]
70    Cursor(Box<CursorPlanError>),
71}
72
73///
74/// OrderPlanError
75///
76/// ORDER BY-specific validation failures.
77///
78#[derive(Debug, ThisError)]
79pub enum OrderPlanError {
80    /// ORDER BY references an unknown field.
81    #[error("unknown order field '{field}'")]
82    UnknownField { field: String },
83
84    /// ORDER BY references a field that cannot be ordered.
85    #[error("order field '{field}' is not orderable")]
86    UnorderableField { field: String },
87
88    /// Ordered plans must terminate with the primary-key tie-break.
89    #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
90    MissingPrimaryKeyTieBreak { field: String },
91}
92
93///
94/// AccessPlanError
95///
96/// Access-path and key-shape validation failures.
97///
98#[derive(Debug, ThisError)]
99pub enum AccessPlanError {
100    /// Access plan references an index not declared on the entity.
101    #[error("index '{index}' not found on entity")]
102    IndexNotFound { index: IndexModel },
103
104    /// Index prefix exceeds the number of indexed fields.
105    #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
106    IndexPrefixTooLong { prefix_len: usize, field_len: usize },
107
108    /// Index prefix must include at least one value.
109    #[error("index prefix must include at least one value")]
110    IndexPrefixEmpty,
111
112    /// Index prefix literal does not match indexed field type.
113    #[error("index prefix value for field '{field}' is incompatible")]
114    IndexPrefixValueMismatch { field: String },
115
116    /// Primary key field exists but is not key-compatible.
117    #[error("primary key field '{field}' is not key-compatible")]
118    PrimaryKeyNotKeyable { field: String },
119
120    /// Supplied key does not match the primary key type.
121    #[error("key '{key:?}' is incompatible with primary key '{field}'")]
122    PrimaryKeyMismatch { field: String, key: Value },
123
124    /// Key range has invalid ordering.
125    #[error("key range start is greater than end")]
126    InvalidKeyRange,
127}
128
129///
130/// PolicyPlanError
131///
132/// Plan-shape policy failures.
133///
134#[derive(Clone, 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 pagination.
141    #[error("delete plans must not include pagination")]
142    DeletePlanWithPagination,
143
144    /// Load plans must not carry delete limits.
145    #[error("load plans must not include delete limits")]
146    LoadPlanWithDeleteLimit,
147
148    /// Delete limits require an explicit ordering.
149    #[error("delete limit requires an explicit ordering")]
150    DeleteLimitRequiresOrder,
151
152    /// Pagination requires an explicit ordering.
153    #[error(
154        "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."
155    )]
156    UnorderedPagination,
157}
158
159///
160/// CursorPlanError
161///
162/// Cursor token and continuation boundary validation failures.
163///
164#[derive(Debug, ThisError)]
165pub enum CursorPlanError {
166    /// Cursor continuation requires an explicit ordering.
167    #[error("cursor pagination requires an explicit ordering")]
168    CursorRequiresOrder,
169
170    /// Cursor token could not be decoded.
171    #[error("invalid continuation cursor: {reason}")]
172    InvalidContinuationCursor { reason: CursorDecodeError },
173
174    /// Cursor token payload/semantics are invalid after token decode.
175    #[error("invalid continuation cursor: {reason}")]
176    InvalidContinuationCursorPayload { reason: String },
177
178    /// Cursor token version is unsupported.
179    #[error("unsupported continuation cursor version: {version}")]
180    ContinuationCursorVersionMismatch { version: u8 },
181
182    /// Cursor token does not belong to this canonical query shape.
183    #[error(
184        "continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
185    )]
186    ContinuationCursorSignatureMismatch {
187        entity_path: &'static str,
188        expected: String,
189        actual: String,
190    },
191
192    /// Cursor boundary width does not match canonical order width.
193    #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
194    ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
195
196    /// Cursor boundary value type mismatch for a non-primary-key ordered field.
197    #[error(
198        "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
199    )]
200    ContinuationCursorBoundaryTypeMismatch {
201        field: String,
202        expected: String,
203        value: Value,
204    },
205
206    /// Cursor primary-key boundary does not match the entity key type.
207    #[error(
208        "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
209    )]
210    ContinuationCursorPrimaryKeyTypeMismatch {
211        field: String,
212        expected: String,
213        value: Option<Value>,
214    },
215}
216
217impl From<PlanPolicyError> for PolicyPlanError {
218    fn from(err: PlanPolicyError) -> Self {
219        match err {
220            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
221            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
222            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
223            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
224            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
225        }
226    }
227}
228
229impl From<predicate::ValidateError> for PlanError {
230    fn from(err: predicate::ValidateError) -> Self {
231        Self::PredicateInvalid(Box::new(err))
232    }
233}
234
235impl From<OrderPlanError> for PlanError {
236    fn from(err: OrderPlanError) -> Self {
237        Self::Order(Box::new(err))
238    }
239}
240
241impl From<AccessPlanError> for PlanError {
242    fn from(err: AccessPlanError) -> Self {
243        Self::Access(Box::new(err))
244    }
245}
246
247impl From<PolicyPlanError> for PlanError {
248    fn from(err: PolicyPlanError) -> Self {
249        Self::Policy(Box::new(err))
250    }
251}
252
253impl From<CursorPlanError> for PlanError {
254    fn from(err: CursorPlanError) -> Self {
255        Self::Cursor(Box::new(err))
256    }
257}
258
259impl From<PlanPolicyError> for PlanError {
260    fn from(err: PlanPolicyError) -> Self {
261        Self::from(PolicyPlanError::from(err))
262    }
263}
264
265impl From<CursorOrderPolicyError> for PlanError {
266    fn from(err: CursorOrderPolicyError) -> Self {
267        match err {
268            CursorOrderPolicyError::CursorRequiresOrder => {
269                Self::from(CursorPlanError::CursorRequiresOrder)
270            }
271        }
272    }
273}
274
275/// Validate a logical plan with model-level key values.
276///
277/// Ownership:
278/// - semantic owner for user-facing query validity at planning boundaries
279/// - failures here are user-visible planning failures (`PlanError`)
280///
281/// New user-facing validation rules must be introduced here first, then mirrored
282/// defensively in downstream layers without changing semantics.
283pub(crate) fn validate_logical_plan_model(
284    schema: &SchemaInfo,
285    model: &EntityModel,
286    plan: &LogicalPlan<Value>,
287) -> Result<(), PlanError> {
288    validate_plan_core(
289        schema,
290        model,
291        plan,
292        validate_order,
293        |schema, model, plan| validate_access_plan_model(schema, model, &plan.access),
294    )?;
295
296    Ok(())
297}
298
299/// Validate plans at executor boundaries and surface invariant violations.
300///
301/// Ownership:
302/// - defensive execution-boundary guardrail, not a semantic owner
303/// - must map violations to internal invariant failures, never new user semantics
304///
305/// Any disagreement with logical validation indicates an internal bug and is not
306/// a recoverable user-input condition.
307pub(crate) fn validate_executor_plan<E: EntityKind>(
308    plan: &LogicalPlan<E::Key>,
309) -> Result<(), InternalError> {
310    let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
311        InternalError::new(
312            ErrorClass::InvariantViolation,
313            ErrorOrigin::Query,
314            format!("entity schema invalid for {}: {err}", E::PATH),
315        )
316    })?;
317
318    validate_plan_core(
319        &schema,
320        E::MODEL,
321        plan,
322        order::validate_executor_order,
323        |schema, model, plan| validate_access_plan(schema, model, &plan.access),
324    )
325    .map_err(InternalError::from_executor_plan_error)?;
326
327    Ok(())
328}
329
330// Shared logical/structural plan validation core used by planner and executor.
331//
332// Boundary-specific behavior is injected via:
333// - `validate_order_fn` (planner vs executor order surface)
334// - `validate_access_fn` (model-key vs typed-key access validation)
335fn validate_plan_core<K, FOrder, FAccess>(
336    schema: &SchemaInfo,
337    model: &EntityModel,
338    plan: &LogicalPlan<K>,
339    validate_order_fn: FOrder,
340    validate_access_fn: FAccess,
341) -> Result<(), PlanError>
342where
343    FOrder: Fn(&SchemaInfo, &crate::db::query::plan::OrderSpec) -> Result<(), PlanError>,
344    FAccess: Fn(&SchemaInfo, &EntityModel, &LogicalPlan<K>) -> Result<(), PlanError>,
345{
346    if let Some(predicate) = &plan.predicate {
347        predicate::validate(schema, predicate)?;
348    }
349
350    if let Some(order) = &plan.order {
351        validate_order_fn(schema, order)?;
352        validate_primary_key_tie_break(model, order)?;
353    }
354
355    validate_access_fn(schema, model, plan)?;
356    semantics::validate_plan_semantics(plan)?;
357
358    Ok(())
359}
360
361// Map shared `PlanError` validation failures into executor-boundary invariant errors.
362impl InternalError {
363    fn from_executor_plan_error(err: PlanError) -> Self {
364        Self::new(
365            ErrorClass::InvariantViolation,
366            ErrorOrigin::Query,
367            err.to_string(),
368        )
369    }
370}