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 policy::{CursorOrderPolicyError, PlanPolicyError},
22 predicate::{self, SchemaInfo},
23 },
24 error::{ErrorClass, ErrorOrigin, InternalError},
25 model::{entity::EntityModel, index::IndexModel},
26 traits::EntityKind,
27 value::Value,
28};
29use thiserror::Error as ThisError;
30
31pub(crate) use access::{validate_access_plan, validate_access_plan_model};
32pub(crate) use order::{validate_order, validate_primary_key_tie_break};
33
34#[derive(Debug, ThisError)]
44pub enum PlanError {
45 #[error("predicate validation failed: {0}")]
46 PredicateInvalid(#[from] predicate::ValidateError),
47
48 #[error("unknown order field '{field}'")]
50 UnknownOrderField { field: String },
51
52 #[error("order field '{field}' is not orderable")]
54 UnorderableField { field: String },
55
56 #[error("index '{index}' not found on entity")]
58 IndexNotFound { index: IndexModel },
59
60 #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
62 IndexPrefixTooLong { prefix_len: usize, field_len: usize },
63
64 #[error("index prefix must include at least one value")]
66 IndexPrefixEmpty,
67
68 #[error("index prefix value for field '{field}' is incompatible")]
70 IndexPrefixValueMismatch { field: String },
71
72 #[error("primary key field '{field}' is not key-compatible")]
74 PrimaryKeyNotKeyable { field: String },
75
76 #[error("key '{key:?}' is incompatible with primary key '{field}'")]
78 PrimaryKeyMismatch { field: String, key: Value },
79
80 #[error("key range start is greater than end")]
82 InvalidKeyRange,
83
84 #[error("order specification must include at least one field")]
86 EmptyOrderSpec,
87
88 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
90 MissingPrimaryKeyTieBreak { field: String },
91
92 #[error("delete plans must not include pagination")]
94 DeletePlanWithPagination,
95
96 #[error("load plans must not include delete limits")]
98 LoadPlanWithDeleteLimit,
99
100 #[error("delete limit requires an explicit ordering")]
102 DeleteLimitRequiresOrder,
103
104 #[error(
106 "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."
107 )]
108 UnorderedPagination,
109
110 #[error("cursor pagination requires an explicit ordering")]
112 CursorRequiresOrder,
113
114 #[error("invalid continuation cursor: {reason}")]
116 InvalidContinuationCursor { reason: String },
117
118 #[error("unsupported continuation cursor version: {version}")]
120 ContinuationCursorVersionMismatch { version: u8 },
121
122 #[error(
124 "continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
125 )]
126 ContinuationCursorSignatureMismatch {
127 entity_path: &'static str,
128 expected: String,
129 actual: String,
130 },
131
132 #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
134 ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
135
136 #[error(
138 "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
139 )]
140 ContinuationCursorBoundaryTypeMismatch {
141 field: String,
142 expected: String,
143 value: Value,
144 },
145
146 #[error(
148 "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
149 )]
150 ContinuationCursorPrimaryKeyTypeMismatch {
151 field: String,
152 expected: String,
153 value: Option<Value>,
154 },
155}
156
157impl From<PlanPolicyError> for PlanError {
158 fn from(err: PlanPolicyError) -> Self {
159 match err {
160 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
161 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
162 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
163 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
164 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
165 }
166 }
167}
168
169impl From<CursorOrderPolicyError> for PlanError {
170 fn from(err: CursorOrderPolicyError) -> Self {
171 match err {
172 CursorOrderPolicyError::CursorRequiresOrder => Self::CursorRequiresOrder,
173 }
174 }
175}
176
177pub(crate) fn validate_logical_plan_model(
186 schema: &SchemaInfo,
187 model: &EntityModel,
188 plan: &LogicalPlan<Value>,
189) -> Result<(), PlanError> {
190 if let Some(predicate) = &plan.predicate {
191 predicate::validate(schema, predicate)?;
192 }
193
194 if let Some(order) = &plan.order {
195 validate_order(schema, order)?;
196 validate_primary_key_tie_break(model, order)?;
197 }
198
199 validate_access_plan_model(schema, model, &plan.access)?;
200 semantics::validate_plan_semantics(plan)?;
201
202 Ok(())
203}
204
205pub(crate) fn validate_executor_plan<E: EntityKind>(
214 plan: &LogicalPlan<E::Key>,
215) -> Result<(), InternalError> {
216 let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
217 InternalError::new(
218 ErrorClass::InvariantViolation,
219 ErrorOrigin::Query,
220 format!("entity schema invalid for {}: {err}", E::PATH),
221 )
222 })?;
223
224 if let Some(predicate) = &plan.predicate {
225 predicate::validate(&schema, predicate).map_err(|err| {
226 InternalError::new(
227 ErrorClass::InvariantViolation,
228 ErrorOrigin::Query,
229 err.to_string(),
230 )
231 })?;
232 }
233
234 if let Some(order) = &plan.order {
235 order::validate_executor_order(&schema, order).map_err(|err| {
236 InternalError::new(
237 ErrorClass::InvariantViolation,
238 ErrorOrigin::Query,
239 err.to_string(),
240 )
241 })?;
242 validate_primary_key_tie_break(E::MODEL, order).map_err(|err| {
243 InternalError::new(
244 ErrorClass::InvariantViolation,
245 ErrorOrigin::Query,
246 err.to_string(),
247 )
248 })?;
249 }
250
251 validate_access_plan(&schema, E::MODEL, &plan.access).map_err(|err| {
252 InternalError::new(
253 ErrorClass::InvariantViolation,
254 ErrorOrigin::Query,
255 err.to_string(),
256 )
257 })?;
258
259 semantics::validate_plan_semantics(plan).map_err(|err| {
260 InternalError::new(
261 ErrorClass::InvariantViolation,
262 ErrorOrigin::Query,
263 err.to_string(),
264 )
265 })?;
266
267 Ok(())
268}