Skip to main content

icydb_core/db/query/
plan_validate.rs

1//! Query-plan validation for planner-owned logical semantics.
2//!
3//! Validation ownership contract:
4//! - `validate_query_semantics` 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
10use crate::db::query::plan::{AccessPlannedQuery, OrderSpec};
11use crate::{
12    db::{
13        access::{
14            AccessPlanError,
15            validate_access_structure_model as validate_access_structure_model_shared,
16        },
17        contracts::{SchemaInfo, ValidateError},
18        cursor::CursorPlanError,
19        policy::{self, PlanPolicyError},
20        query::predicate,
21    },
22    model::entity::EntityModel,
23    value::Value,
24};
25use std::collections::BTreeSet;
26use thiserror::Error as ThisError;
27
28///
29/// PlanError
30///
31/// Executor-visible validation failures for logical plans.
32///
33/// These errors indicate that a plan cannot be safely executed against the
34/// current schema or entity definition. They are *not* planner bugs.
35///
36
37#[derive(Debug, ThisError)]
38pub enum PlanError {
39    #[error("predicate validation failed: {0}")]
40    PredicateInvalid(Box<ValidateError>),
41
42    #[error("{0}")]
43    Order(Box<OrderPlanError>),
44
45    #[error("{0}")]
46    Access(Box<AccessPlanError>),
47
48    #[error("{0}")]
49    Policy(Box<PolicyPlanError>),
50
51    #[error("{0}")]
52    Cursor(Box<CursorPlanError>),
53}
54
55///
56/// OrderPlanError
57///
58/// ORDER BY-specific validation failures.
59///
60#[derive(Debug, ThisError)]
61pub enum OrderPlanError {
62    /// ORDER BY references an unknown field.
63    #[error("unknown order field '{field}'")]
64    UnknownField { field: String },
65
66    /// ORDER BY references a field that cannot be ordered.
67    #[error("order field '{field}' is not orderable")]
68    UnorderableField { field: String },
69
70    /// ORDER BY references the same non-primary-key field multiple times.
71    #[error("order field '{field}' appears multiple times")]
72    DuplicateOrderField { field: String },
73
74    /// Ordered plans must terminate with the primary-key tie-break.
75    #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
76    MissingPrimaryKeyTieBreak { field: String },
77}
78
79///
80/// PolicyPlanError
81///
82/// Plan-shape policy failures.
83///
84#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
85pub enum PolicyPlanError {
86    /// ORDER BY must specify at least one field.
87    #[error("order specification must include at least one field")]
88    EmptyOrderSpec,
89
90    /// Delete plans must not carry pagination.
91    #[error("delete plans must not include pagination")]
92    DeletePlanWithPagination,
93
94    /// Load plans must not carry delete limits.
95    #[error("load plans must not include delete limits")]
96    LoadPlanWithDeleteLimit,
97
98    /// Delete limits require an explicit ordering.
99    #[error("delete limit requires an explicit ordering")]
100    DeleteLimitRequiresOrder,
101
102    /// Pagination requires an explicit ordering.
103    #[error(
104        "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."
105    )]
106    UnorderedPagination,
107}
108
109impl From<PlanPolicyError> for PolicyPlanError {
110    fn from(err: PlanPolicyError) -> Self {
111        match err {
112            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
113            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
114            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
115            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
116            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
117        }
118    }
119}
120
121impl From<ValidateError> for PlanError {
122    fn from(err: ValidateError) -> Self {
123        Self::PredicateInvalid(Box::new(err))
124    }
125}
126
127impl From<OrderPlanError> for PlanError {
128    fn from(err: OrderPlanError) -> Self {
129        Self::Order(Box::new(err))
130    }
131}
132
133impl From<AccessPlanError> for PlanError {
134    fn from(err: AccessPlanError) -> Self {
135        Self::Access(Box::new(err))
136    }
137}
138
139impl From<PolicyPlanError> for PlanError {
140    fn from(err: PolicyPlanError) -> Self {
141        Self::Policy(Box::new(err))
142    }
143}
144
145impl From<CursorPlanError> for PlanError {
146    fn from(err: CursorPlanError) -> Self {
147        Self::Cursor(Box::new(err))
148    }
149}
150
151impl From<PlanPolicyError> for PlanError {
152    fn from(err: PlanPolicyError) -> Self {
153        Self::from(PolicyPlanError::from(err))
154    }
155}
156
157/// Validate a logical plan with model-level key values.
158///
159/// Ownership:
160/// - semantic owner for user-facing query validity at planning boundaries
161/// - failures here are user-visible planning failures (`PlanError`)
162///
163/// New user-facing validation rules must be introduced here first, then mirrored
164/// defensively in downstream layers without changing semantics.
165pub(crate) fn validate_query_semantics(
166    schema: &SchemaInfo,
167    model: &EntityModel,
168    plan: &AccessPlannedQuery<Value>,
169) -> Result<(), PlanError> {
170    validate_plan_core(
171        schema,
172        model,
173        plan,
174        validate_order,
175        |schema, model, plan| {
176            validate_access_structure_model_shared(schema, model, &plan.access)
177                .map_err(PlanError::from)
178        },
179    )?;
180
181    Ok(())
182}
183
184// Shared logical plan validation core owned by planner semantics.
185fn validate_plan_core<K, FOrder, FAccess>(
186    schema: &SchemaInfo,
187    model: &EntityModel,
188    plan: &AccessPlannedQuery<K>,
189    validate_order_fn: FOrder,
190    validate_access_fn: FAccess,
191) -> Result<(), PlanError>
192where
193    FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
194    FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
195{
196    if let Some(predicate) = &plan.predicate {
197        predicate::validate(schema, predicate)?;
198    }
199
200    if let Some(order) = &plan.order {
201        validate_order_fn(schema, order)?;
202        validate_no_duplicate_non_pk_order_fields(model, order)?;
203        validate_primary_key_tie_break(model, order)?;
204    }
205
206    validate_access_fn(schema, model, plan)?;
207    policy::validate_plan_shape(plan)?;
208
209    Ok(())
210}
211// ORDER validation ownership contract:
212// - This module owns ORDER semantic validation (field existence/orderability/tie-break).
213// - ORDER canonicalization (primary-key tie-break insertion) is performed at the
214//   intent boundary via `canonicalize_order_spec` before plan validation.
215// - Shape-policy checks (for example empty ORDER, pagination/order coupling) are owned by
216//   `db::policy`.
217// - Executor/runtime layers may defend execution preconditions only.
218
219/// Validate ORDER BY fields against the schema.
220pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
221    for (field, _) in &order.fields {
222        let field_type = schema
223            .field(field)
224            .ok_or_else(|| OrderPlanError::UnknownField {
225                field: field.clone(),
226            })
227            .map_err(PlanError::from)?;
228
229        if !field_type.is_orderable() {
230            // CONTRACT: ORDER BY rejects non-queryable or unordered fields.
231            return Err(PlanError::from(OrderPlanError::UnorderableField {
232                field: field.clone(),
233            }));
234        }
235    }
236
237    Ok(())
238}
239
240/// Reject duplicate non-primary-key fields in ORDER BY.
241pub(crate) fn validate_no_duplicate_non_pk_order_fields(
242    model: &EntityModel,
243    order: &OrderSpec,
244) -> Result<(), PlanError> {
245    let mut seen = BTreeSet::new();
246    let pk_field = model.primary_key.name;
247
248    for (field, _) in &order.fields {
249        if field == pk_field {
250            continue;
251        }
252        if !seen.insert(field.as_str()) {
253            return Err(PlanError::from(OrderPlanError::DuplicateOrderField {
254                field: field.clone(),
255            }));
256        }
257    }
258
259    Ok(())
260}
261
262// Ordered plans must include exactly one terminal primary-key field so ordering is total and
263// deterministic across explain, fingerprint, and executor comparison paths.
264pub(crate) fn validate_primary_key_tie_break(
265    model: &EntityModel,
266    order: &OrderSpec,
267) -> Result<(), PlanError> {
268    if order.fields.is_empty() {
269        return Ok(());
270    }
271
272    let pk_field = model.primary_key.name;
273    let pk_count = order
274        .fields
275        .iter()
276        .filter(|(field, _)| field == pk_field)
277        .count();
278    let trailing_pk = order
279        .fields
280        .last()
281        .is_some_and(|(field, _)| field == pk_field);
282
283    if pk_count == 1 && trailing_pk {
284        Ok(())
285    } else {
286        Err(PlanError::from(OrderPlanError::MissingPrimaryKeyTieBreak {
287            field: pk_field.to_string(),
288        }))
289    }
290}
291
292#[cfg(test)]
293mod tests;