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::query::{
21        plan::{AccessPlannedQuery, cursor::CursorPlanError},
22        policy::PlanPolicyError,
23        predicate::{self, SchemaInfo},
24    },
25    error::InternalError,
26    model::{entity::EntityModel, index::IndexModel},
27    traits::EntityKind,
28    value::Value,
29};
30use thiserror::Error as ThisError;
31
32// re-exports
33pub(crate) use access::{validate_access_plan, validate_access_plan_model};
34pub(crate) use order::{
35    validate_no_duplicate_non_pk_order_fields, validate_order, validate_primary_key_tie_break,
36};
37#[cfg(test)]
38pub(crate) use pushdown::assess_secondary_order_pushdown_if_applicable;
39pub(crate) use pushdown::{
40    PushdownApplicability, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
41    SecondaryOrderPushdownRejection, assess_secondary_order_pushdown,
42    assess_secondary_order_pushdown_if_applicable_validated,
43};
44
45///
46/// PlanError
47///
48/// Executor-visible validation failures for logical plans.
49///
50/// These errors indicate that a plan cannot be safely executed against the
51/// current schema or entity definition. They are *not* planner bugs.
52///
53
54#[derive(Debug, ThisError)]
55pub enum PlanError {
56    #[error("predicate validation failed: {0}")]
57    PredicateInvalid(Box<predicate::ValidateError>),
58
59    #[error("{0}")]
60    Order(Box<OrderPlanError>),
61
62    #[error("{0}")]
63    Access(Box<AccessPlanError>),
64
65    #[error("{0}")]
66    Policy(Box<PolicyPlanError>),
67
68    #[error("{0}")]
69    Cursor(Box<CursorPlanError>),
70}
71
72///
73/// OrderPlanError
74///
75/// ORDER BY-specific validation failures.
76///
77#[derive(Debug, ThisError)]
78pub enum OrderPlanError {
79    /// ORDER BY references an unknown field.
80    #[error("unknown order field '{field}'")]
81    UnknownField { field: String },
82
83    /// ORDER BY references a field that cannot be ordered.
84    #[error("order field '{field}' is not orderable")]
85    UnorderableField { field: String },
86
87    /// ORDER BY references the same non-primary-key field multiple times.
88    #[error("order field '{field}' appears multiple times")]
89    DuplicateOrderField { field: String },
90
91    /// Ordered plans must terminate with the primary-key tie-break.
92    #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
93    MissingPrimaryKeyTieBreak { field: String },
94}
95
96///
97/// AccessPlanError
98///
99/// Access-path and key-shape validation failures.
100///
101#[derive(Debug, ThisError)]
102pub enum AccessPlanError {
103    /// Access plan references an index not declared on the entity.
104    #[error("index '{index}' not found on entity")]
105    IndexNotFound { index: IndexModel },
106
107    /// Index prefix exceeds the number of indexed fields.
108    #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
109    IndexPrefixTooLong { prefix_len: usize, field_len: usize },
110
111    /// Index prefix must include at least one value.
112    #[error("index prefix must include at least one value")]
113    IndexPrefixEmpty,
114
115    /// Index prefix literal does not match indexed field type.
116    #[error("index prefix value for field '{field}' is incompatible")]
117    IndexPrefixValueMismatch { field: String },
118
119    /// Primary key field exists but is not key-compatible.
120    #[error("primary key field '{field}' is not key-compatible")]
121    PrimaryKeyNotKeyable { field: String },
122
123    /// Supplied key does not match the primary key type.
124    #[error("key '{key:?}' is incompatible with primary key '{field}'")]
125    PrimaryKeyMismatch { field: String, key: Value },
126
127    /// Key range has invalid ordering.
128    #[error("key range start is greater than end")]
129    InvalidKeyRange,
130}
131
132///
133/// PolicyPlanError
134///
135/// Plan-shape policy failures.
136///
137#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
138pub enum PolicyPlanError {
139    /// ORDER BY must specify at least one field.
140    #[error("order specification must include at least one field")]
141    EmptyOrderSpec,
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
162impl From<PlanPolicyError> for PolicyPlanError {
163    fn from(err: PlanPolicyError) -> Self {
164        match err {
165            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
166            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
167            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
168            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
169            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
170        }
171    }
172}
173
174impl From<predicate::ValidateError> for PlanError {
175    fn from(err: predicate::ValidateError) -> Self {
176        Self::PredicateInvalid(Box::new(err))
177    }
178}
179
180impl From<OrderPlanError> for PlanError {
181    fn from(err: OrderPlanError) -> Self {
182        Self::Order(Box::new(err))
183    }
184}
185
186impl From<AccessPlanError> for PlanError {
187    fn from(err: AccessPlanError) -> Self {
188        Self::Access(Box::new(err))
189    }
190}
191
192impl From<PolicyPlanError> for PlanError {
193    fn from(err: PolicyPlanError) -> Self {
194        Self::Policy(Box::new(err))
195    }
196}
197
198impl From<CursorPlanError> for PlanError {
199    fn from(err: CursorPlanError) -> Self {
200        Self::Cursor(Box::new(err))
201    }
202}
203
204impl From<PlanPolicyError> for PlanError {
205    fn from(err: PlanPolicyError) -> Self {
206        Self::from(PolicyPlanError::from(err))
207    }
208}
209
210/// Validate a logical plan with model-level key values.
211///
212/// Ownership:
213/// - semantic owner for user-facing query validity at planning boundaries
214/// - failures here are user-visible planning failures (`PlanError`)
215///
216/// New user-facing validation rules must be introduced here first, then mirrored
217/// defensively in downstream layers without changing semantics.
218pub(crate) fn validate_logical_plan_model(
219    schema: &SchemaInfo,
220    model: &EntityModel,
221    plan: &AccessPlannedQuery<Value>,
222) -> Result<(), PlanError> {
223    validate_plan_core(
224        schema,
225        model,
226        plan,
227        validate_order,
228        |schema, model, plan| validate_access_plan_model(schema, model, &plan.access),
229    )?;
230
231    Ok(())
232}
233
234/// Validate plans at executor boundaries and surface invariant violations.
235///
236/// Ownership:
237/// - defensive execution-boundary guardrail, not a semantic owner
238/// - must enforce structural integrity only, never user-shape semantics
239///
240/// Any disagreement with logical validation indicates an internal bug and is not
241/// a recoverable user-input condition.
242pub(crate) fn validate_executor_plan<E: EntityKind>(
243    plan: &AccessPlannedQuery<E::Key>,
244) -> Result<(), InternalError> {
245    let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
246        InternalError::query_invariant(format!("entity schema invalid for {}: {err}", E::PATH))
247    })?;
248
249    validate_access_plan(&schema, E::MODEL, &plan.access)
250        .map_err(InternalError::from_executor_plan_error)?;
251
252    Ok(())
253}
254
255// Shared logical plan validation core owned by planner semantics.
256fn validate_plan_core<K, FOrder, FAccess>(
257    schema: &SchemaInfo,
258    model: &EntityModel,
259    plan: &AccessPlannedQuery<K>,
260    validate_order_fn: FOrder,
261    validate_access_fn: FAccess,
262) -> Result<(), PlanError>
263where
264    FOrder: Fn(&SchemaInfo, &crate::db::query::plan::OrderSpec) -> Result<(), PlanError>,
265    FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
266{
267    if let Some(predicate) = &plan.predicate {
268        predicate::validate(schema, predicate)?;
269    }
270
271    if let Some(order) = &plan.order {
272        validate_order_fn(schema, order)?;
273        validate_no_duplicate_non_pk_order_fields(model, order)?;
274        validate_primary_key_tie_break(model, order)?;
275    }
276
277    validate_access_fn(schema, model, plan)?;
278    semantics::validate_plan_semantics(plan)?;
279
280    Ok(())
281}