icydb_core/db/query/plan/validate/
mod.rs1mod access;
12mod order;
13mod semantics;
14
15#[cfg(test)]
16mod tests;
17
18use crate::{
19 db::query::{
20 plan::LogicalPlan,
21 predicate::{self, SchemaInfo},
22 },
23 error::{ErrorClass, ErrorOrigin, InternalError},
24 model::{entity::EntityModel, index::IndexModel},
25 traits::EntityKind,
26 value::Value,
27};
28use thiserror::Error as ThisError;
29
30pub(crate) use access::{validate_access_plan, validate_access_plan_model};
31pub(crate) use order::{validate_order, validate_primary_key_tie_break};
32
33#[derive(Debug, ThisError)]
43pub enum PlanError {
44 #[error("predicate validation failed: {0}")]
45 PredicateInvalid(#[from] predicate::ValidateError),
46
47 #[error("unknown order field '{field}'")]
49 UnknownOrderField { field: String },
50
51 #[error("order field '{field}' is not orderable")]
53 UnorderableField { field: String },
54
55 #[error("index '{index}' not found on entity")]
57 IndexNotFound { index: IndexModel },
58
59 #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
61 IndexPrefixTooLong { prefix_len: usize, field_len: usize },
62
63 #[error("index prefix must include at least one value")]
65 IndexPrefixEmpty,
66
67 #[error("index prefix value for field '{field}' is incompatible")]
69 IndexPrefixValueMismatch { field: String },
70
71 #[error("primary key field '{field}' is not key-compatible")]
73 PrimaryKeyNotKeyable { field: String },
74
75 #[error("key '{key:?}' is incompatible with primary key '{field}'")]
77 PrimaryKeyMismatch { field: String, key: Value },
78
79 #[error("key range start is greater than end")]
81 InvalidKeyRange,
82
83 #[error("order specification must include at least one field")]
85 EmptyOrderSpec,
86
87 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
89 MissingPrimaryKeyTieBreak { field: String },
90
91 #[error("delete plans must not include pagination")]
93 DeletePlanWithPagination,
94
95 #[error("load plans must not include delete limits")]
97 LoadPlanWithDeleteLimit,
98
99 #[error("delete limit requires an explicit ordering")]
101 DeleteLimitRequiresOrder,
102
103 #[error(
105 "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."
106 )]
107 UnorderedPagination,
108
109 #[error("cursor pagination requires an explicit ordering")]
111 CursorRequiresOrder,
112
113 #[error("invalid continuation cursor: {reason}")]
115 InvalidContinuationCursor { reason: String },
116
117 #[error("unsupported continuation cursor version: {version}")]
119 ContinuationCursorVersionMismatch { version: u8 },
120
121 #[error(
123 "continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
124 )]
125 ContinuationCursorSignatureMismatch {
126 entity_path: &'static str,
127 expected: String,
128 actual: String,
129 },
130
131 #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
133 ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
134
135 #[error(
137 "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
138 )]
139 ContinuationCursorBoundaryTypeMismatch {
140 field: String,
141 expected: String,
142 value: Value,
143 },
144
145 #[error(
147 "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
148 )]
149 ContinuationCursorPrimaryKeyTypeMismatch {
150 field: String,
151 expected: String,
152 value: Option<Value>,
153 },
154}
155
156pub(crate) fn validate_logical_plan_model(
165 schema: &SchemaInfo,
166 model: &EntityModel,
167 plan: &LogicalPlan<Value>,
168) -> Result<(), PlanError> {
169 if let Some(predicate) = &plan.predicate {
170 predicate::validate(schema, predicate)?;
171 }
172
173 if let Some(order) = &plan.order {
174 validate_order(schema, order)?;
175 validate_primary_key_tie_break(model, order)?;
176 }
177
178 validate_access_plan_model(schema, model, &plan.access)?;
179 semantics::validate_plan_semantics(plan)?;
180
181 Ok(())
182}
183
184pub(crate) fn validate_executor_plan<E: EntityKind>(
193 plan: &LogicalPlan<E::Key>,
194) -> Result<(), InternalError> {
195 let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
196 InternalError::new(
197 ErrorClass::InvariantViolation,
198 ErrorOrigin::Query,
199 format!("entity schema invalid for {}: {err}", E::PATH),
200 )
201 })?;
202
203 if let Some(predicate) = &plan.predicate {
204 predicate::validate(&schema, predicate).map_err(|err| {
205 InternalError::new(
206 ErrorClass::InvariantViolation,
207 ErrorOrigin::Query,
208 err.to_string(),
209 )
210 })?;
211 }
212
213 if let Some(order) = &plan.order {
214 order::validate_executor_order(&schema, order).map_err(|err| {
215 InternalError::new(
216 ErrorClass::InvariantViolation,
217 ErrorOrigin::Query,
218 err.to_string(),
219 )
220 })?;
221 validate_primary_key_tie_break(E::MODEL, order).map_err(|err| {
222 InternalError::new(
223 ErrorClass::InvariantViolation,
224 ErrorOrigin::Query,
225 err.to_string(),
226 )
227 })?;
228 }
229
230 validate_access_plan(&schema, E::MODEL, &plan.access).map_err(|err| {
231 InternalError::new(
232 ErrorClass::InvariantViolation,
233 ErrorOrigin::Query,
234 err.to_string(),
235 )
236 })?;
237
238 semantics::validate_plan_semantics(plan).map_err(|err| {
239 InternalError::new(
240 ErrorClass::InvariantViolation,
241 ErrorOrigin::Query,
242 err.to_string(),
243 )
244 })?;
245
246 Ok(())
247}