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