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::{
38    validate_no_duplicate_non_pk_order_fields, validate_order, validate_primary_key_tie_break,
39};
40#[cfg(test)]
41pub(crate) use pushdown::assess_secondary_order_pushdown_if_applicable;
42pub(crate) use pushdown::{
43    PushdownApplicability, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
44    SecondaryOrderPushdownRejection, assess_secondary_order_pushdown,
45    assess_secondary_order_pushdown_if_applicable_validated,
46};
47
48///
49/// PlanError
50///
51/// Executor-visible validation failures for logical plans.
52///
53/// These errors indicate that a plan cannot be safely executed against the
54/// current schema or entity definition. They are *not* planner bugs.
55///
56
57#[derive(Debug, ThisError)]
58pub enum PlanError {
59    #[error("predicate validation failed: {0}")]
60    PredicateInvalid(Box<predicate::ValidateError>),
61
62    #[error("{0}")]
63    Order(Box<OrderPlanError>),
64
65    #[error("{0}")]
66    Access(Box<AccessPlanError>),
67
68    #[error("{0}")]
69    Policy(Box<PolicyPlanError>),
70
71    #[error("{0}")]
72    Cursor(Box<CursorPlanError>),
73}
74
75///
76/// OrderPlanError
77///
78/// ORDER BY-specific validation failures.
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/// AccessPlanError
101///
102/// Access-path and key-shape validation failures.
103///
104#[derive(Debug, ThisError)]
105pub enum AccessPlanError {
106    /// Access plan references an index not declared on the entity.
107    #[error("index '{index}' not found on entity")]
108    IndexNotFound { index: IndexModel },
109
110    /// Index prefix exceeds the number of indexed fields.
111    #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
112    IndexPrefixTooLong { prefix_len: usize, field_len: usize },
113
114    /// Index prefix must include at least one value.
115    #[error("index prefix must include at least one value")]
116    IndexPrefixEmpty,
117
118    /// Index prefix literal does not match indexed field type.
119    #[error("index prefix value for field '{field}' is incompatible")]
120    IndexPrefixValueMismatch { field: String },
121
122    /// Primary key field exists but is not key-compatible.
123    #[error("primary key field '{field}' is not key-compatible")]
124    PrimaryKeyNotKeyable { field: String },
125
126    /// Supplied key does not match the primary key type.
127    #[error("key '{key:?}' is incompatible with primary key '{field}'")]
128    PrimaryKeyMismatch { field: String, key: Value },
129
130    /// Key range has invalid ordering.
131    #[error("key range start is greater than end")]
132    InvalidKeyRange,
133}
134
135///
136/// PolicyPlanError
137///
138/// Plan-shape policy failures.
139///
140#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
141pub enum PolicyPlanError {
142    /// ORDER BY must specify at least one field.
143    #[error("order specification must include at least one field")]
144    EmptyOrderSpec,
145
146    /// Delete plans must not carry pagination.
147    #[error("delete plans must not include pagination")]
148    DeletePlanWithPagination,
149
150    /// Load plans must not carry delete limits.
151    #[error("load plans must not include delete limits")]
152    LoadPlanWithDeleteLimit,
153
154    /// Delete limits require an explicit ordering.
155    #[error("delete limit requires an explicit ordering")]
156    DeleteLimitRequiresOrder,
157
158    /// Pagination requires an explicit ordering.
159    #[error(
160        "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."
161    )]
162    UnorderedPagination,
163}
164
165///
166/// CursorPlanError
167///
168/// Cursor token and continuation boundary validation failures.
169///
170#[derive(Debug, ThisError)]
171pub enum CursorPlanError {
172    /// Cursor continuation requires an explicit ordering.
173    #[error("cursor pagination requires an explicit ordering")]
174    CursorRequiresOrder,
175
176    /// Cursor token could not be decoded.
177    #[error("invalid continuation cursor: {reason}")]
178    InvalidContinuationCursor { reason: CursorDecodeError },
179
180    /// Cursor token payload/semantics are invalid after token decode.
181    #[error("invalid continuation cursor: {reason}")]
182    InvalidContinuationCursorPayload { reason: String },
183
184    /// Cursor token version is unsupported.
185    #[error("unsupported continuation cursor version: {version}")]
186    ContinuationCursorVersionMismatch { version: u8 },
187
188    /// Cursor token does not belong to this canonical query shape.
189    #[error(
190        "continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
191    )]
192    ContinuationCursorSignatureMismatch {
193        entity_path: &'static str,
194        expected: String,
195        actual: String,
196    },
197
198    /// Cursor boundary width does not match canonical order width.
199    #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
200    ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
201
202    /// Cursor window offset does not match the current query window shape.
203    #[error(
204        "continuation cursor offset mismatch: expected {expected_offset}, found {actual_offset}"
205    )]
206    ContinuationCursorWindowMismatch {
207        expected_offset: u32,
208        actual_offset: u32,
209    },
210
211    /// Cursor boundary value type mismatch for a non-primary-key ordered field.
212    #[error(
213        "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
214    )]
215    ContinuationCursorBoundaryTypeMismatch {
216        field: String,
217        expected: String,
218        value: Value,
219    },
220
221    /// Cursor primary-key boundary does not match the entity key type.
222    #[error(
223        "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
224    )]
225    ContinuationCursorPrimaryKeyTypeMismatch {
226        field: String,
227        expected: String,
228        value: Option<Value>,
229    },
230}
231
232impl From<PlanPolicyError> for PolicyPlanError {
233    fn from(err: PlanPolicyError) -> Self {
234        match err {
235            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
236            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
237            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
238            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
239            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
240        }
241    }
242}
243
244impl From<predicate::ValidateError> for PlanError {
245    fn from(err: predicate::ValidateError) -> Self {
246        Self::PredicateInvalid(Box::new(err))
247    }
248}
249
250impl From<OrderPlanError> for PlanError {
251    fn from(err: OrderPlanError) -> Self {
252        Self::Order(Box::new(err))
253    }
254}
255
256impl From<AccessPlanError> for PlanError {
257    fn from(err: AccessPlanError) -> Self {
258        Self::Access(Box::new(err))
259    }
260}
261
262impl From<PolicyPlanError> for PlanError {
263    fn from(err: PolicyPlanError) -> Self {
264        Self::Policy(Box::new(err))
265    }
266}
267
268impl From<CursorPlanError> for PlanError {
269    fn from(err: CursorPlanError) -> Self {
270        Self::Cursor(Box::new(err))
271    }
272}
273
274impl From<PlanPolicyError> for PlanError {
275    fn from(err: PlanPolicyError) -> Self {
276        Self::from(PolicyPlanError::from(err))
277    }
278}
279
280/// Validate a logical plan with model-level key values.
281///
282/// Ownership:
283/// - semantic owner for user-facing query validity at planning boundaries
284/// - failures here are user-visible planning failures (`PlanError`)
285///
286/// New user-facing validation rules must be introduced here first, then mirrored
287/// defensively in downstream layers without changing semantics.
288pub(crate) fn validate_logical_plan_model(
289    schema: &SchemaInfo,
290    model: &EntityModel,
291    plan: &LogicalPlan<Value>,
292) -> Result<(), PlanError> {
293    validate_plan_core(
294        schema,
295        model,
296        plan,
297        validate_order,
298        |schema, model, plan| validate_access_plan_model(schema, model, &plan.access),
299    )?;
300
301    Ok(())
302}
303
304/// Validate plans at executor boundaries and surface invariant violations.
305///
306/// Ownership:
307/// - defensive execution-boundary guardrail, not a semantic owner
308/// - must enforce structural integrity only, never user-shape semantics
309///
310/// Any disagreement with logical validation indicates an internal bug and is not
311/// a recoverable user-input condition.
312pub(crate) fn validate_executor_plan<E: EntityKind>(
313    plan: &LogicalPlan<E::Key>,
314) -> Result<(), InternalError> {
315    let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
316        InternalError::query_invariant(format!("entity schema invalid for {}: {err}", E::PATH))
317    })?;
318
319    validate_access_plan(&schema, E::MODEL, &plan.access)
320        .map_err(InternalError::from_executor_plan_error)?;
321
322    Ok(())
323}
324
325// Shared logical plan validation core owned by planner semantics.
326fn validate_plan_core<K, FOrder, FAccess>(
327    schema: &SchemaInfo,
328    model: &EntityModel,
329    plan: &LogicalPlan<K>,
330    validate_order_fn: FOrder,
331    validate_access_fn: FAccess,
332) -> Result<(), PlanError>
333where
334    FOrder: Fn(&SchemaInfo, &crate::db::query::plan::OrderSpec) -> Result<(), PlanError>,
335    FAccess: Fn(&SchemaInfo, &EntityModel, &LogicalPlan<K>) -> Result<(), PlanError>,
336{
337    if let Some(predicate) = &plan.predicate {
338        predicate::validate(schema, predicate)?;
339    }
340
341    if let Some(order) = &plan.order {
342        validate_order_fn(schema, order)?;
343        validate_no_duplicate_non_pk_order_fields(model, order)?;
344        validate_primary_key_tie_break(model, order)?;
345    }
346
347    validate_access_fn(schema, model, plan)?;
348    semantics::validate_plan_semantics(plan)?;
349
350    Ok(())
351}