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