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::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#[derive(Debug, ThisError)]
53pub enum PlanError {
54 #[error("predicate validation failed: {0}")]
55 PredicateInvalid(#[from] predicate::ValidateError),
56
57 #[error("unknown order field '{field}'")]
59 UnknownOrderField { field: String },
60
61 #[error("order field '{field}' is not orderable")]
63 UnorderableField { field: String },
64
65 #[error("index '{index}' not found on entity")]
67 IndexNotFound { index: IndexModel },
68
69 #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
71 IndexPrefixTooLong { prefix_len: usize, field_len: usize },
72
73 #[error("index prefix must include at least one value")]
75 IndexPrefixEmpty,
76
77 #[error("index prefix value for field '{field}' is incompatible")]
79 IndexPrefixValueMismatch { field: String },
80
81 #[error("primary key field '{field}' is not key-compatible")]
83 PrimaryKeyNotKeyable { field: String },
84
85 #[error("key '{key:?}' is incompatible with primary key '{field}'")]
87 PrimaryKeyMismatch { field: String, key: Value },
88
89 #[error("key range start is greater than end")]
91 InvalidKeyRange,
92
93 #[error("order specification must include at least one field")]
95 EmptyOrderSpec,
96
97 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
99 MissingPrimaryKeyTieBreak { field: String },
100
101 #[error("delete plans must not include pagination")]
103 DeletePlanWithPagination,
104
105 #[error("load plans must not include delete limits")]
107 LoadPlanWithDeleteLimit,
108
109 #[error("delete limit requires an explicit ordering")]
111 DeleteLimitRequiresOrder,
112
113 #[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 #[error("cursor pagination requires an explicit ordering")]
121 CursorRequiresOrder,
122
123 #[error("invalid continuation cursor: {reason}")]
125 InvalidContinuationCursor { reason: String },
126
127 #[error("unsupported continuation cursor version: {version}")]
129 ContinuationCursorVersionMismatch { version: u8 },
130
131 #[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 #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
143 ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
144
145 #[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 #[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
186pub(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
214pub(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}