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