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