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