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