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        codec::cursor::CursorDecodeError,
22        query::{
23            plan::LogicalPlan,
24            policy::PlanPolicyError,
25            predicate::{self, SchemaInfo},
26        },
27    },
28    error::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 window offset does not match the current query window shape.
197    #[error(
198        "continuation cursor offset mismatch: expected {expected_offset}, found {actual_offset}"
199    )]
200    ContinuationCursorWindowMismatch {
201        expected_offset: u32,
202        actual_offset: u32,
203    },
204
205    /// Cursor boundary value type mismatch for a non-primary-key ordered field.
206    #[error(
207        "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
208    )]
209    ContinuationCursorBoundaryTypeMismatch {
210        field: String,
211        expected: String,
212        value: Value,
213    },
214
215    /// Cursor primary-key boundary does not match the entity key type.
216    #[error(
217        "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
218    )]
219    ContinuationCursorPrimaryKeyTypeMismatch {
220        field: String,
221        expected: String,
222        value: Option<Value>,
223    },
224}
225
226impl From<PlanPolicyError> for PolicyPlanError {
227    fn from(err: PlanPolicyError) -> Self {
228        match err {
229            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
230            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
231            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
232            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
233            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
234        }
235    }
236}
237
238impl From<predicate::ValidateError> for PlanError {
239    fn from(err: predicate::ValidateError) -> Self {
240        Self::PredicateInvalid(Box::new(err))
241    }
242}
243
244impl From<OrderPlanError> for PlanError {
245    fn from(err: OrderPlanError) -> Self {
246        Self::Order(Box::new(err))
247    }
248}
249
250impl From<AccessPlanError> for PlanError {
251    fn from(err: AccessPlanError) -> Self {
252        Self::Access(Box::new(err))
253    }
254}
255
256impl From<PolicyPlanError> for PlanError {
257    fn from(err: PolicyPlanError) -> Self {
258        Self::Policy(Box::new(err))
259    }
260}
261
262impl From<CursorPlanError> for PlanError {
263    fn from(err: CursorPlanError) -> Self {
264        Self::Cursor(Box::new(err))
265    }
266}
267
268impl From<PlanPolicyError> for PlanError {
269    fn from(err: PlanPolicyError) -> Self {
270        Self::from(PolicyPlanError::from(err))
271    }
272}
273
274/// Validate a logical plan with model-level key values.
275///
276/// Ownership:
277/// - semantic owner for user-facing query validity at planning boundaries
278/// - failures here are user-visible planning failures (`PlanError`)
279///
280/// New user-facing validation rules must be introduced here first, then mirrored
281/// defensively in downstream layers without changing semantics.
282pub(crate) fn validate_logical_plan_model(
283    schema: &SchemaInfo,
284    model: &EntityModel,
285    plan: &LogicalPlan<Value>,
286) -> Result<(), PlanError> {
287    validate_plan_core(
288        schema,
289        model,
290        plan,
291        validate_order,
292        |schema, model, plan| validate_access_plan_model(schema, model, &plan.access),
293    )?;
294
295    Ok(())
296}
297
298/// Validate plans at executor boundaries and surface invariant violations.
299///
300/// Ownership:
301/// - defensive execution-boundary guardrail, not a semantic owner
302/// - must enforce structural integrity only, never user-shape semantics
303///
304/// Any disagreement with logical validation indicates an internal bug and is not
305/// a recoverable user-input condition.
306pub(crate) fn validate_executor_plan<E: EntityKind>(
307    plan: &LogicalPlan<E::Key>,
308) -> Result<(), InternalError> {
309    let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
310        InternalError::query_invariant(format!("entity schema invalid for {}: {err}", E::PATH))
311    })?;
312
313    validate_access_plan(&schema, E::MODEL, &plan.access)
314        .map_err(InternalError::from_executor_plan_error)?;
315
316    Ok(())
317}
318
319// Shared logical plan validation core owned by planner semantics.
320fn validate_plan_core<K, FOrder, FAccess>(
321    schema: &SchemaInfo,
322    model: &EntityModel,
323    plan: &LogicalPlan<K>,
324    validate_order_fn: FOrder,
325    validate_access_fn: FAccess,
326) -> Result<(), PlanError>
327where
328    FOrder: Fn(&SchemaInfo, &crate::db::query::plan::OrderSpec) -> Result<(), PlanError>,
329    FAccess: Fn(&SchemaInfo, &EntityModel, &LogicalPlan<K>) -> Result<(), PlanError>,
330{
331    if let Some(predicate) = &plan.predicate {
332        predicate::validate(schema, predicate)?;
333    }
334
335    if let Some(order) = &plan.order {
336        validate_order_fn(schema, order)?;
337        validate_primary_key_tie_break(model, order)?;
338    }
339
340    validate_access_fn(schema, model, plan)?;
341    semantics::validate_plan_semantics(plan)?;
342
343    Ok(())
344}