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::executor::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 order;
11mod semantics;
12
13#[cfg(test)]
14mod tests;
15
16use crate::{
17    db::{
18        access::validate_access_plan_model as validate_access_plan_model_shared,
19        cursor::CursorPlanError,
20        policy::PlanPolicyError,
21        query::{
22            plan::{AccessPlannedQuery, OrderSpec},
23            predicate::{self, SchemaInfo},
24        },
25    },
26    model::entity::EntityModel,
27    value::Value,
28};
29use thiserror::Error as ThisError;
30
31// re-exports
32pub(crate) use crate::db::access::AccessPlanError;
33#[cfg(test)]
34pub(crate) use crate::db::access::assess_secondary_order_pushdown_if_applicable;
35#[cfg(test)]
36pub(crate) use crate::db::access::{
37    PushdownApplicability, assess_secondary_order_pushdown_if_applicable_validated,
38};
39pub(crate) use crate::db::access::{
40    PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility, SecondaryOrderPushdownRejection,
41    assess_secondary_order_pushdown,
42};
43pub(crate) use order::{
44    validate_no_duplicate_non_pk_order_fields, validate_order, validate_primary_key_tie_break,
45};
46
47///
48/// PlanError
49///
50/// Executor-visible validation failures for logical plans.
51///
52/// These errors indicate that a plan cannot be safely executed against the
53/// current schema or entity definition. They are *not* planner bugs.
54///
55
56#[derive(Debug, ThisError)]
57pub enum PlanError {
58    #[error("predicate validation failed: {0}")]
59    PredicateInvalid(Box<predicate::ValidateError>),
60
61    #[error("{0}")]
62    Order(Box<OrderPlanError>),
63
64    #[error("{0}")]
65    Access(Box<AccessPlanError>),
66
67    #[error("{0}")]
68    Policy(Box<PolicyPlanError>),
69
70    #[error("{0}")]
71    Cursor(Box<CursorPlanError>),
72}
73
74///
75/// OrderPlanError
76///
77/// ORDER BY-specific validation failures.
78///
79#[derive(Debug, ThisError)]
80pub enum OrderPlanError {
81    /// ORDER BY references an unknown field.
82    #[error("unknown order field '{field}'")]
83    UnknownField { field: String },
84
85    /// ORDER BY references a field that cannot be ordered.
86    #[error("order field '{field}' is not orderable")]
87    UnorderableField { field: String },
88
89    /// ORDER BY references the same non-primary-key field multiple times.
90    #[error("order field '{field}' appears multiple times")]
91    DuplicateOrderField { field: String },
92
93    /// Ordered plans must terminate with the primary-key tie-break.
94    #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
95    MissingPrimaryKeyTieBreak { field: String },
96}
97
98///
99/// PolicyPlanError
100///
101/// Plan-shape policy failures.
102///
103#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
104pub enum PolicyPlanError {
105    /// ORDER BY must specify at least one field.
106    #[error("order specification must include at least one field")]
107    EmptyOrderSpec,
108
109    /// Delete plans must not carry pagination.
110    #[error("delete plans must not include pagination")]
111    DeletePlanWithPagination,
112
113    /// Load plans must not carry delete limits.
114    #[error("load plans must not include delete limits")]
115    LoadPlanWithDeleteLimit,
116
117    /// Delete limits require an explicit ordering.
118    #[error("delete limit requires an explicit ordering")]
119    DeleteLimitRequiresOrder,
120
121    /// Pagination requires an explicit ordering.
122    #[error(
123        "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."
124    )]
125    UnorderedPagination,
126}
127
128impl From<PlanPolicyError> for PolicyPlanError {
129    fn from(err: PlanPolicyError) -> Self {
130        match err {
131            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
132            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
133            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
134            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
135            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
136        }
137    }
138}
139
140impl From<predicate::ValidateError> for PlanError {
141    fn from(err: predicate::ValidateError) -> Self {
142        Self::PredicateInvalid(Box::new(err))
143    }
144}
145
146impl From<OrderPlanError> for PlanError {
147    fn from(err: OrderPlanError) -> Self {
148        Self::Order(Box::new(err))
149    }
150}
151
152impl From<AccessPlanError> for PlanError {
153    fn from(err: AccessPlanError) -> Self {
154        Self::Access(Box::new(err))
155    }
156}
157
158impl From<PolicyPlanError> for PlanError {
159    fn from(err: PolicyPlanError) -> Self {
160        Self::Policy(Box::new(err))
161    }
162}
163
164impl From<CursorPlanError> for PlanError {
165    fn from(err: CursorPlanError) -> Self {
166        Self::Cursor(Box::new(err))
167    }
168}
169
170impl From<PlanPolicyError> for PlanError {
171    fn from(err: PlanPolicyError) -> Self {
172        Self::from(PolicyPlanError::from(err))
173    }
174}
175
176/// Validate a logical plan with model-level key values.
177///
178/// Ownership:
179/// - semantic owner for user-facing query validity at planning boundaries
180/// - failures here are user-visible planning failures (`PlanError`)
181///
182/// New user-facing validation rules must be introduced here first, then mirrored
183/// defensively in downstream layers without changing semantics.
184pub(crate) fn validate_logical_plan_model(
185    schema: &SchemaInfo,
186    model: &EntityModel,
187    plan: &AccessPlannedQuery<Value>,
188) -> Result<(), PlanError> {
189    validate_plan_core(
190        schema,
191        model,
192        plan,
193        validate_order,
194        |schema, model, plan| {
195            validate_access_plan_model_shared(schema, model, &plan.access).map_err(PlanError::from)
196        },
197    )?;
198
199    Ok(())
200}
201
202// Shared logical plan validation core owned by planner semantics.
203fn validate_plan_core<K, FOrder, FAccess>(
204    schema: &SchemaInfo,
205    model: &EntityModel,
206    plan: &AccessPlannedQuery<K>,
207    validate_order_fn: FOrder,
208    validate_access_fn: FAccess,
209) -> Result<(), PlanError>
210where
211    FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
212    FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
213{
214    if let Some(predicate) = &plan.predicate {
215        predicate::validate(schema, predicate)?;
216    }
217
218    if let Some(order) = &plan.order {
219        validate_order_fn(schema, order)?;
220        validate_no_duplicate_non_pk_order_fields(model, order)?;
221        validate_primary_key_tie_break(model, order)?;
222    }
223
224    validate_access_fn(schema, model, plan)?;
225    semantics::validate_plan_semantics(plan)?;
226
227    Ok(())
228}