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::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#[derive(Debug, ThisError)]
52pub enum PlanError {
53 #[error("predicate validation failed: {0}")]
54 PredicateInvalid(#[from] predicate::ValidateError),
55
56 #[error("unknown order field '{field}'")]
58 UnknownOrderField { field: String },
59
60 #[error("order field '{field}' is not orderable")]
62 UnorderableField { field: String },
63
64 #[error("index '{index}' not found on entity")]
66 IndexNotFound { index: IndexModel },
67
68 #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
70 IndexPrefixTooLong { prefix_len: usize, field_len: usize },
71
72 #[error("index prefix must include at least one value")]
74 IndexPrefixEmpty,
75
76 #[error("index prefix value for field '{field}' is incompatible")]
78 IndexPrefixValueMismatch { field: String },
79
80 #[error("primary key field '{field}' is not key-compatible")]
82 PrimaryKeyNotKeyable { field: String },
83
84 #[error("key '{key:?}' is incompatible with primary key '{field}'")]
86 PrimaryKeyMismatch { field: String, key: Value },
87
88 #[error("key range start is greater than end")]
90 InvalidKeyRange,
91
92 #[error("order specification must include at least one field")]
94 EmptyOrderSpec,
95
96 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
98 MissingPrimaryKeyTieBreak { field: String },
99
100 #[error("delete plans must not include pagination")]
102 DeletePlanWithPagination,
103
104 #[error("load plans must not include delete limits")]
106 LoadPlanWithDeleteLimit,
107
108 #[error("delete limit requires an explicit ordering")]
110 DeleteLimitRequiresOrder,
111
112 #[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 #[error("cursor pagination requires an explicit ordering")]
120 CursorRequiresOrder,
121
122 #[error("invalid continuation cursor: {reason}")]
124 InvalidContinuationCursor { reason: String },
125
126 #[error("unsupported continuation cursor version: {version}")]
128 ContinuationCursorVersionMismatch { version: u8 },
129
130 #[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 #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
142 ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
143
144 #[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 #[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
185pub(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 validate_access_plan_model(schema, model, &plan.access)?;
220 semantics::validate_plan_semantics(plan)?;
221
222 Ok(())
223}
224
225pub(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}