Skip to main content

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

1//! Query-plan validation at logical and executor boundaries.
2//!
3//! Validation ownership contract:
4//! - `validate_logical_plan_model` owns user-facing query semantics and emits `PlanError`.
5//! - `validate_executor_plan` is defensive: it re-checks owned semantics/invariants before
6//!   execution and must not introduce new user-visible semantics.
7//!
8//! Future rule changes must declare a semantic owner. Defensive re-check layers may mirror
9//! rules, but must not reinterpret semantics or error class intent.
10
11mod access;
12mod order;
13mod semantics;
14
15#[cfg(test)]
16mod tests;
17
18use crate::{
19    db::query::{
20        plan::LogicalPlan,
21        policy::{CursorOrderPolicyError, PlanPolicyError},
22        predicate::{self, SchemaInfo},
23    },
24    error::{ErrorClass, ErrorOrigin, InternalError},
25    model::{entity::EntityModel, index::IndexModel},
26    traits::EntityKind,
27    value::Value,
28};
29use thiserror::Error as ThisError;
30
31pub(crate) use access::{validate_access_plan, validate_access_plan_model};
32pub(crate) use order::{validate_order, validate_primary_key_tie_break};
33
34///
35/// PlanError
36///
37/// Executor-visible validation failures for logical plans.
38///
39/// These errors indicate that a plan cannot be safely executed against the
40/// current schema or entity definition. They are *not* planner bugs.
41///
42
43#[derive(Debug, ThisError)]
44pub enum PlanError {
45    #[error("predicate validation failed: {0}")]
46    PredicateInvalid(#[from] predicate::ValidateError),
47
48    /// ORDER BY references an unknown field.
49    #[error("unknown order field '{field}'")]
50    UnknownOrderField { field: String },
51
52    /// ORDER BY references a field that cannot be ordered.
53    #[error("order field '{field}' is not orderable")]
54    UnorderableField { field: String },
55
56    /// Access plan references an index not declared on the entity.
57    #[error("index '{index}' not found on entity")]
58    IndexNotFound { index: IndexModel },
59
60    /// Index prefix exceeds the number of indexed fields.
61    #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
62    IndexPrefixTooLong { prefix_len: usize, field_len: usize },
63
64    /// Index prefix must include at least one value.
65    #[error("index prefix must include at least one value")]
66    IndexPrefixEmpty,
67
68    /// Index prefix literal does not match indexed field type.
69    #[error("index prefix value for field '{field}' is incompatible")]
70    IndexPrefixValueMismatch { field: String },
71
72    /// Primary key field exists but is not key-compatible.
73    #[error("primary key field '{field}' is not key-compatible")]
74    PrimaryKeyNotKeyable { field: String },
75
76    /// Supplied key does not match the primary key type.
77    #[error("key '{key:?}' is incompatible with primary key '{field}'")]
78    PrimaryKeyMismatch { field: String, key: Value },
79
80    /// Key range has invalid ordering.
81    #[error("key range start is greater than end")]
82    InvalidKeyRange,
83
84    /// ORDER BY must specify at least one field.
85    #[error("order specification must include at least one field")]
86    EmptyOrderSpec,
87
88    /// Ordered plans must terminate with the primary-key tie-break.
89    #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
90    MissingPrimaryKeyTieBreak { field: String },
91
92    /// Delete plans must not carry pagination.
93    #[error("delete plans must not include pagination")]
94    DeletePlanWithPagination,
95
96    /// Load plans must not carry delete limits.
97    #[error("load plans must not include delete limits")]
98    LoadPlanWithDeleteLimit,
99
100    /// Delete limits require an explicit ordering.
101    #[error("delete limit requires an explicit ordering")]
102    DeleteLimitRequiresOrder,
103
104    /// Pagination requires an explicit ordering.
105    #[error(
106        "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."
107    )]
108    UnorderedPagination,
109
110    /// Cursor continuation requires an explicit ordering.
111    #[error("cursor pagination requires an explicit ordering")]
112    CursorRequiresOrder,
113
114    /// Cursor token could not be decoded.
115    #[error("invalid continuation cursor: {reason}")]
116    InvalidContinuationCursor { reason: String },
117
118    /// Cursor token version is unsupported.
119    #[error("unsupported continuation cursor version: {version}")]
120    ContinuationCursorVersionMismatch { version: u8 },
121
122    /// Cursor token does not belong to this canonical query shape.
123    #[error(
124        "continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
125    )]
126    ContinuationCursorSignatureMismatch {
127        entity_path: &'static str,
128        expected: String,
129        actual: String,
130    },
131
132    /// Cursor boundary width does not match canonical order width.
133    #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
134    ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
135
136    /// Cursor boundary value type mismatch for a non-primary-key ordered field.
137    #[error(
138        "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
139    )]
140    ContinuationCursorBoundaryTypeMismatch {
141        field: String,
142        expected: String,
143        value: Value,
144    },
145
146    /// Cursor primary-key boundary does not match the entity key type.
147    #[error(
148        "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
149    )]
150    ContinuationCursorPrimaryKeyTypeMismatch {
151        field: String,
152        expected: String,
153        value: Option<Value>,
154    },
155}
156
157impl From<PlanPolicyError> for PlanError {
158    fn from(err: PlanPolicyError) -> Self {
159        match err {
160            PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
161            PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
162            PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
163            PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
164            PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
165        }
166    }
167}
168
169impl From<CursorOrderPolicyError> for PlanError {
170    fn from(err: CursorOrderPolicyError) -> Self {
171        match err {
172            CursorOrderPolicyError::CursorRequiresOrder => Self::CursorRequiresOrder,
173        }
174    }
175}
176
177/// Validate a logical plan with model-level key values.
178///
179/// Ownership:
180/// - semantic owner for user-facing query validity at planning boundaries
181/// - failures here are user-visible planning failures (`PlanError`)
182///
183/// New user-facing validation rules must be introduced here first, then mirrored
184/// defensively in downstream layers without changing semantics.
185pub(crate) fn validate_logical_plan_model(
186    schema: &SchemaInfo,
187    model: &EntityModel,
188    plan: &LogicalPlan<Value>,
189) -> Result<(), PlanError> {
190    if let Some(predicate) = &plan.predicate {
191        predicate::validate(schema, predicate)?;
192    }
193
194    if let Some(order) = &plan.order {
195        validate_order(schema, order)?;
196        validate_primary_key_tie_break(model, order)?;
197    }
198
199    validate_access_plan_model(schema, model, &plan.access)?;
200    semantics::validate_plan_semantics(plan)?;
201
202    Ok(())
203}
204
205/// Validate plans at executor boundaries and surface invariant violations.
206///
207/// Ownership:
208/// - defensive execution-boundary guardrail, not a semantic owner
209/// - must map violations to internal invariant failures, never new user semantics
210///
211/// Any disagreement with logical validation indicates an internal bug and is not
212/// a recoverable user-input condition.
213pub(crate) fn validate_executor_plan<E: EntityKind>(
214    plan: &LogicalPlan<E::Key>,
215) -> Result<(), InternalError> {
216    let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
217        InternalError::new(
218            ErrorClass::InvariantViolation,
219            ErrorOrigin::Query,
220            format!("entity schema invalid for {}: {err}", E::PATH),
221        )
222    })?;
223
224    if let Some(predicate) = &plan.predicate {
225        predicate::validate(&schema, predicate).map_err(|err| {
226            InternalError::new(
227                ErrorClass::InvariantViolation,
228                ErrorOrigin::Query,
229                err.to_string(),
230            )
231        })?;
232    }
233
234    if let Some(order) = &plan.order {
235        order::validate_executor_order(&schema, order).map_err(|err| {
236            InternalError::new(
237                ErrorClass::InvariantViolation,
238                ErrorOrigin::Query,
239                err.to_string(),
240            )
241        })?;
242        validate_primary_key_tie_break(E::MODEL, order).map_err(|err| {
243            InternalError::new(
244                ErrorClass::InvariantViolation,
245                ErrorOrigin::Query,
246                err.to_string(),
247            )
248        })?;
249    }
250
251    validate_access_plan(&schema, E::MODEL, &plan.access).map_err(|err| {
252        InternalError::new(
253            ErrorClass::InvariantViolation,
254            ErrorOrigin::Query,
255            err.to_string(),
256        )
257    })?;
258
259    semantics::validate_plan_semantics(plan).map_err(|err| {
260        InternalError::new(
261            ErrorClass::InvariantViolation,
262            ErrorOrigin::Query,
263            err.to_string(),
264        )
265    })?;
266
267    Ok(())
268}