Skip to main content

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

1//! Query-plan validation for planner-owned logical semantics.
2//!
3//! Validation ownership contract:
4//! - `validate_logical_plan_model` owns user-facing query semantics and emits `PlanError`.
5//! - executor-boundary defensive checks live in `db::plan::validate`.
6//!
7//! Future rule changes must declare a semantic owner. Defensive re-check layers may mirror
8//! rules, but must not reinterpret semantics or error class intent.
9
10mod access;
11mod order;
12mod pushdown;
13mod semantics;
14
15#[cfg(test)]
16mod tests;
17
18use crate::{
19    db::{
20        cursor::CursorPlanError,
21        plan::{AccessPlannedQuery, OrderSpec},
22        policy::PlanPolicyError,
23        query::predicate::{self, SchemaInfo},
24    },
25    model::{entity::EntityModel, index::IndexModel},
26    value::Value,
27};
28use thiserror::Error as ThisError;
29
30// re-exports
31pub(crate) use access::{validate_access_plan, validate_access_plan_model};
32pub(crate) use order::{
33    validate_no_duplicate_non_pk_order_fields, validate_order, validate_primary_key_tie_break,
34};
35#[cfg(test)]
36pub(crate) use pushdown::assess_secondary_order_pushdown_if_applicable;
37#[cfg(test)]
38pub(crate) use pushdown::{
39    PushdownApplicability, assess_secondary_order_pushdown_if_applicable_validated,
40};
41pub(crate) use pushdown::{
42    PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility, SecondaryOrderPushdownRejection,
43    assess_secondary_order_pushdown,
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    /// ORDER BY references the same non-primary-key field multiple times.
89    #[error("order field '{field}' appears multiple times")]
90    DuplicateOrderField { field: String },
91
92    /// Ordered plans must terminate with the primary-key tie-break.
93    #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
94    MissingPrimaryKeyTieBreak { field: String },
95}
96
97///
98/// AccessPlanError
99///
100/// Access-path and key-shape validation failures.
101///
102#[derive(Debug, ThisError)]
103pub enum AccessPlanError {
104    /// Access plan references an index not declared on the entity.
105    #[error("index '{index}' not found on entity")]
106    IndexNotFound { index: IndexModel },
107
108    /// Index prefix exceeds the number of indexed fields.
109    #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
110    IndexPrefixTooLong { prefix_len: usize, field_len: usize },
111
112    /// Index prefix must include at least one value.
113    #[error("index prefix must include at least one value")]
114    IndexPrefixEmpty,
115
116    /// Index prefix literal does not match indexed field type.
117    #[error("index prefix value for field '{field}' is incompatible")]
118    IndexPrefixValueMismatch { field: String },
119
120    /// Primary key field exists but is not key-compatible.
121    #[error("primary key field '{field}' is not key-compatible")]
122    PrimaryKeyNotKeyable { field: String },
123
124    /// Supplied key does not match the primary key type.
125    #[error("key '{key:?}' is incompatible with primary key '{field}'")]
126    PrimaryKeyMismatch { field: String, key: Value },
127
128    /// Key range has invalid ordering.
129    #[error("key range start is greater than end")]
130    InvalidKeyRange,
131}
132
133///
134/// PolicyPlanError
135///
136/// Plan-shape policy failures.
137///
138#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
139pub enum PolicyPlanError {
140    /// ORDER BY must specify at least one field.
141    #[error("order specification must include at least one field")]
142    EmptyOrderSpec,
143
144    /// Delete plans must not carry pagination.
145    #[error("delete plans must not include pagination")]
146    DeletePlanWithPagination,
147
148    /// Load plans must not carry delete limits.
149    #[error("load plans must not include delete limits")]
150    LoadPlanWithDeleteLimit,
151
152    /// Delete limits require an explicit ordering.
153    #[error("delete limit requires an explicit ordering")]
154    DeleteLimitRequiresOrder,
155
156    /// Pagination requires an explicit ordering.
157    #[error(
158        "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."
159    )]
160    UnorderedPagination,
161}
162
163impl From<PlanPolicyError> for PolicyPlanError {
164    fn from(err: PlanPolicyError) -> Self {
165        match err {
166            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
167            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
168            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
169            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
170            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
171        }
172    }
173}
174
175impl From<predicate::ValidateError> for PlanError {
176    fn from(err: predicate::ValidateError) -> Self {
177        Self::PredicateInvalid(Box::new(err))
178    }
179}
180
181impl From<OrderPlanError> for PlanError {
182    fn from(err: OrderPlanError) -> Self {
183        Self::Order(Box::new(err))
184    }
185}
186
187impl From<AccessPlanError> for PlanError {
188    fn from(err: AccessPlanError) -> Self {
189        Self::Access(Box::new(err))
190    }
191}
192
193impl From<PolicyPlanError> for PlanError {
194    fn from(err: PolicyPlanError) -> Self {
195        Self::Policy(Box::new(err))
196    }
197}
198
199impl From<CursorPlanError> for PlanError {
200    fn from(err: CursorPlanError) -> Self {
201        Self::Cursor(Box::new(err))
202    }
203}
204
205impl From<PlanPolicyError> for PlanError {
206    fn from(err: PlanPolicyError) -> Self {
207        Self::from(PolicyPlanError::from(err))
208    }
209}
210
211/// Validate a logical plan with model-level key values.
212///
213/// Ownership:
214/// - semantic owner for user-facing query validity at planning boundaries
215/// - failures here are user-visible planning failures (`PlanError`)
216///
217/// New user-facing validation rules must be introduced here first, then mirrored
218/// defensively in downstream layers without changing semantics.
219pub(crate) fn validate_logical_plan_model(
220    schema: &SchemaInfo,
221    model: &EntityModel,
222    plan: &AccessPlannedQuery<Value>,
223) -> Result<(), PlanError> {
224    validate_plan_core(
225        schema,
226        model,
227        plan,
228        validate_order,
229        |schema, model, plan| validate_access_plan_model(schema, model, &plan.access),
230    )?;
231
232    Ok(())
233}
234
235// Shared logical plan validation core owned by planner semantics.
236fn validate_plan_core<K, FOrder, FAccess>(
237    schema: &SchemaInfo,
238    model: &EntityModel,
239    plan: &AccessPlannedQuery<K>,
240    validate_order_fn: FOrder,
241    validate_access_fn: FAccess,
242) -> Result<(), PlanError>
243where
244    FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
245    FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
246{
247    if let Some(predicate) = &plan.predicate {
248        predicate::validate(schema, predicate)?;
249    }
250
251    if let Some(order) = &plan.order {
252        validate_order_fn(schema, order)?;
253        validate_no_duplicate_non_pk_order_fields(model, order)?;
254        validate_primary_key_tie_break(model, order)?;
255    }
256
257    validate_access_fn(schema, model, plan)?;
258    semantics::validate_plan_semantics(plan)?;
259
260    Ok(())
261}