icydb_core/db/query/plan/validate/
mod.rs1mod 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::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};
37pub(crate) use order::{validate_order, validate_primary_key_tie_break};
38#[cfg(test)]
39pub(crate) use pushdown::assess_secondary_order_pushdown_if_applicable;
40pub(crate) use pushdown::{
41 PushdownApplicability, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
42 SecondaryOrderPushdownRejection, assess_secondary_order_pushdown,
43 assess_secondary_order_pushdown_if_applicable_validated,
44};
45
46#[derive(Debug, ThisError)]
56pub enum PlanError {
57 #[error("predicate validation failed: {0}")]
58 PredicateInvalid(Box<predicate::ValidateError>),
59
60 #[error("{0}")]
61 Order(Box<OrderPlanError>),
62
63 #[error("{0}")]
64 Access(Box<AccessPlanError>),
65
66 #[error("{0}")]
67 Policy(Box<PolicyPlanError>),
68
69 #[error("{0}")]
70 Cursor(Box<CursorPlanError>),
71}
72
73#[derive(Debug, ThisError)]
79pub enum OrderPlanError {
80 #[error("unknown order field '{field}'")]
82 UnknownField { field: String },
83
84 #[error("order field '{field}' is not orderable")]
86 UnorderableField { field: String },
87
88 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
90 MissingPrimaryKeyTieBreak { field: String },
91}
92
93#[derive(Debug, ThisError)]
99pub enum AccessPlanError {
100 #[error("index '{index}' not found on entity")]
102 IndexNotFound { index: IndexModel },
103
104 #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
106 IndexPrefixTooLong { prefix_len: usize, field_len: usize },
107
108 #[error("index prefix must include at least one value")]
110 IndexPrefixEmpty,
111
112 #[error("index prefix value for field '{field}' is incompatible")]
114 IndexPrefixValueMismatch { field: String },
115
116 #[error("primary key field '{field}' is not key-compatible")]
118 PrimaryKeyNotKeyable { field: String },
119
120 #[error("key '{key:?}' is incompatible with primary key '{field}'")]
122 PrimaryKeyMismatch { field: String, key: Value },
123
124 #[error("key range start is greater than end")]
126 InvalidKeyRange,
127}
128
129#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
135pub enum PolicyPlanError {
136 #[error("order specification must include at least one field")]
138 EmptyOrderSpec,
139
140 #[error("delete plans must not include pagination")]
142 DeletePlanWithPagination,
143
144 #[error("load plans must not include delete limits")]
146 LoadPlanWithDeleteLimit,
147
148 #[error("delete limit requires an explicit ordering")]
150 DeleteLimitRequiresOrder,
151
152 #[error(
154 "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."
155 )]
156 UnorderedPagination,
157}
158
159#[derive(Debug, ThisError)]
165pub enum CursorPlanError {
166 #[error("cursor pagination requires an explicit ordering")]
168 CursorRequiresOrder,
169
170 #[error("invalid continuation cursor: {reason}")]
172 InvalidContinuationCursor { reason: CursorDecodeError },
173
174 #[error("invalid continuation cursor: {reason}")]
176 InvalidContinuationCursorPayload { reason: String },
177
178 #[error("unsupported continuation cursor version: {version}")]
180 ContinuationCursorVersionMismatch { version: u8 },
181
182 #[error(
184 "continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
185 )]
186 ContinuationCursorSignatureMismatch {
187 entity_path: &'static str,
188 expected: String,
189 actual: String,
190 },
191
192 #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
194 ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
195
196 #[error(
198 "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
199 )]
200 ContinuationCursorBoundaryTypeMismatch {
201 field: String,
202 expected: String,
203 value: Value,
204 },
205
206 #[error(
208 "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
209 )]
210 ContinuationCursorPrimaryKeyTypeMismatch {
211 field: String,
212 expected: String,
213 value: Option<Value>,
214 },
215}
216
217impl From<PlanPolicyError> for PolicyPlanError {
218 fn from(err: PlanPolicyError) -> Self {
219 match err {
220 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
221 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
222 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
223 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
224 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
225 }
226 }
227}
228
229impl From<predicate::ValidateError> for PlanError {
230 fn from(err: predicate::ValidateError) -> Self {
231 Self::PredicateInvalid(Box::new(err))
232 }
233}
234
235impl From<OrderPlanError> for PlanError {
236 fn from(err: OrderPlanError) -> Self {
237 Self::Order(Box::new(err))
238 }
239}
240
241impl From<AccessPlanError> for PlanError {
242 fn from(err: AccessPlanError) -> Self {
243 Self::Access(Box::new(err))
244 }
245}
246
247impl From<PolicyPlanError> for PlanError {
248 fn from(err: PolicyPlanError) -> Self {
249 Self::Policy(Box::new(err))
250 }
251}
252
253impl From<CursorPlanError> for PlanError {
254 fn from(err: CursorPlanError) -> Self {
255 Self::Cursor(Box::new(err))
256 }
257}
258
259impl From<PlanPolicyError> for PlanError {
260 fn from(err: PlanPolicyError) -> Self {
261 Self::from(PolicyPlanError::from(err))
262 }
263}
264
265pub(crate) fn validate_logical_plan_model(
274 schema: &SchemaInfo,
275 model: &EntityModel,
276 plan: &LogicalPlan<Value>,
277) -> Result<(), PlanError> {
278 validate_plan_core(
279 schema,
280 model,
281 plan,
282 validate_order,
283 |schema, model, plan| validate_access_plan_model(schema, model, &plan.access),
284 )?;
285
286 Ok(())
287}
288
289pub(crate) fn validate_executor_plan<E: EntityKind>(
298 plan: &LogicalPlan<E::Key>,
299) -> Result<(), InternalError> {
300 let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
301 InternalError::new(
302 ErrorClass::InvariantViolation,
303 ErrorOrigin::Query,
304 format!("entity schema invalid for {}: {err}", E::PATH),
305 )
306 })?;
307
308 validate_access_plan(&schema, E::MODEL, &plan.access)
309 .map_err(InternalError::from_executor_plan_error)?;
310
311 Ok(())
312}
313
314fn validate_plan_core<K, FOrder, FAccess>(
316 schema: &SchemaInfo,
317 model: &EntityModel,
318 plan: &LogicalPlan<K>,
319 validate_order_fn: FOrder,
320 validate_access_fn: FAccess,
321) -> Result<(), PlanError>
322where
323 FOrder: Fn(&SchemaInfo, &crate::db::query::plan::OrderSpec) -> Result<(), PlanError>,
324 FAccess: Fn(&SchemaInfo, &EntityModel, &LogicalPlan<K>) -> Result<(), PlanError>,
325{
326 if let Some(predicate) = &plan.predicate {
327 predicate::validate(schema, predicate)?;
328 }
329
330 if let Some(order) = &plan.order {
331 validate_order_fn(schema, order)?;
332 validate_primary_key_tie_break(model, order)?;
333 }
334
335 validate_access_fn(schema, model, plan)?;
336 semantics::validate_plan_semantics(plan)?;
337
338 Ok(())
339}
340
341impl InternalError {
343 fn from_executor_plan_error(err: PlanError) -> Self {
344 Self::new(
345 ErrorClass::InvariantViolation,
346 ErrorOrigin::Query,
347 err.to_string(),
348 )
349 }
350}