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 pushdown;
12mod semantics;
13
14#[cfg(test)]
15mod tests;
16
17use crate::{
18    db::{
19        access::validate_access_plan_model as validate_access_plan_model_shared,
20        cursor::CursorPlanError,
21        plan::{AccessPlannedQuery, OrderSpec},
22        policy::PlanPolicyError,
23        query::predicate::{self, SchemaInfo},
24    },
25    model::entity::EntityModel,
26    value::Value,
27};
28use thiserror::Error as ThisError;
29
30// re-exports
31pub(crate) use crate::db::access::AccessPlanError;
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/// PolicyPlanError
99///
100/// Plan-shape policy failures.
101///
102#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
103pub enum PolicyPlanError {
104    /// ORDER BY must specify at least one field.
105    #[error("order specification must include at least one field")]
106    EmptyOrderSpec,
107
108    /// Delete plans must not carry pagination.
109    #[error("delete plans must not include pagination")]
110    DeletePlanWithPagination,
111
112    /// Load plans must not carry delete limits.
113    #[error("load plans must not include delete limits")]
114    LoadPlanWithDeleteLimit,
115
116    /// Delete limits require an explicit ordering.
117    #[error("delete limit requires an explicit ordering")]
118    DeleteLimitRequiresOrder,
119
120    /// Pagination requires an explicit ordering.
121    #[error(
122        "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."
123    )]
124    UnorderedPagination,
125}
126
127impl From<PlanPolicyError> for PolicyPlanError {
128    fn from(err: PlanPolicyError) -> Self {
129        match err {
130            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
131            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
132            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
133            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
134            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
135        }
136    }
137}
138
139impl From<predicate::ValidateError> for PlanError {
140    fn from(err: predicate::ValidateError) -> Self {
141        Self::PredicateInvalid(Box::new(err))
142    }
143}
144
145impl From<OrderPlanError> for PlanError {
146    fn from(err: OrderPlanError) -> Self {
147        Self::Order(Box::new(err))
148    }
149}
150
151impl From<AccessPlanError> for PlanError {
152    fn from(err: AccessPlanError) -> Self {
153        Self::Access(Box::new(err))
154    }
155}
156
157impl From<PolicyPlanError> for PlanError {
158    fn from(err: PolicyPlanError) -> Self {
159        Self::Policy(Box::new(err))
160    }
161}
162
163impl From<CursorPlanError> for PlanError {
164    fn from(err: CursorPlanError) -> Self {
165        Self::Cursor(Box::new(err))
166    }
167}
168
169impl From<PlanPolicyError> for PlanError {
170    fn from(err: PlanPolicyError) -> Self {
171        Self::from(PolicyPlanError::from(err))
172    }
173}
174
175/// Validate a logical plan with model-level key values.
176///
177/// Ownership:
178/// - semantic owner for user-facing query validity at planning boundaries
179/// - failures here are user-visible planning failures (`PlanError`)
180///
181/// New user-facing validation rules must be introduced here first, then mirrored
182/// defensively in downstream layers without changing semantics.
183pub(crate) fn validate_logical_plan_model(
184    schema: &SchemaInfo,
185    model: &EntityModel,
186    plan: &AccessPlannedQuery<Value>,
187) -> Result<(), PlanError> {
188    validate_plan_core(
189        schema,
190        model,
191        plan,
192        validate_order,
193        |schema, model, plan| {
194            validate_access_plan_model_shared(schema, model, &plan.access).map_err(PlanError::from)
195        },
196    )?;
197
198    Ok(())
199}
200
201// Shared logical plan validation core owned by planner semantics.
202fn validate_plan_core<K, FOrder, FAccess>(
203    schema: &SchemaInfo,
204    model: &EntityModel,
205    plan: &AccessPlannedQuery<K>,
206    validate_order_fn: FOrder,
207    validate_access_fn: FAccess,
208) -> Result<(), PlanError>
209where
210    FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
211    FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
212{
213    if let Some(predicate) = &plan.predicate {
214        predicate::validate(schema, predicate)?;
215    }
216
217    if let Some(order) = &plan.order {
218        validate_order_fn(schema, order)?;
219        validate_no_duplicate_non_pk_order_fields(model, order)?;
220        validate_primary_key_tie_break(model, order)?;
221    }
222
223    validate_access_fn(schema, model, plan)?;
224    semantics::validate_plan_semantics(plan)?;
225
226    Ok(())
227}