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::{CursorOrderPolicyError, 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};
36pub(crate) use order::{validate_order, validate_primary_key_tie_break};
37pub(crate) use pushdown::PushdownApplicability;
38pub(crate) use pushdown::PushdownSurfaceEligibility;
39pub(crate) use pushdown::SecondaryOrderPushdownEligibility;
40pub(crate) use pushdown::SecondaryOrderPushdownRejection;
41pub(crate) use pushdown::assess_secondary_order_pushdown;
42#[cfg(test)]
43pub(crate) use pushdown::assess_secondary_order_pushdown_if_applicable;
44pub(crate) use pushdown::assess_secondary_order_pushdown_if_applicable_validated;
45
46#[derive(Debug, ThisError)]
56pub enum PlanError {
57 #[error("predicate validation failed: {0}")]
58 PredicateInvalid(#[from] predicate::ValidateError),
59
60 #[error("unknown order field '{field}'")]
62 UnknownOrderField { field: String },
63
64 #[error("order field '{field}' is not orderable")]
66 UnorderableField { field: String },
67
68 #[error("index '{index}' not found on entity")]
70 IndexNotFound { index: IndexModel },
71
72 #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
74 IndexPrefixTooLong { prefix_len: usize, field_len: usize },
75
76 #[error("index prefix must include at least one value")]
78 IndexPrefixEmpty,
79
80 #[error("index prefix value for field '{field}' is incompatible")]
82 IndexPrefixValueMismatch { field: String },
83
84 #[error("primary key field '{field}' is not key-compatible")]
86 PrimaryKeyNotKeyable { field: String },
87
88 #[error("key '{key:?}' is incompatible with primary key '{field}'")]
90 PrimaryKeyMismatch { field: String, key: Value },
91
92 #[error("key range start is greater than end")]
94 InvalidKeyRange,
95
96 #[error("order specification must include at least one field")]
98 EmptyOrderSpec,
99
100 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
102 MissingPrimaryKeyTieBreak { field: String },
103
104 #[error("delete plans must not include pagination")]
106 DeletePlanWithPagination,
107
108 #[error("load plans must not include delete limits")]
110 LoadPlanWithDeleteLimit,
111
112 #[error("delete limit requires an explicit ordering")]
114 DeleteLimitRequiresOrder,
115
116 #[error(
118 "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."
119 )]
120 UnorderedPagination,
121
122 #[error("cursor pagination requires an explicit ordering")]
124 CursorRequiresOrder,
125
126 #[error("invalid continuation cursor: {reason}")]
128 InvalidContinuationCursor { reason: CursorDecodeError },
129
130 #[error("invalid continuation cursor: {reason}")]
132 InvalidContinuationCursorPayload { reason: String },
133
134 #[error("unsupported continuation cursor version: {version}")]
136 ContinuationCursorVersionMismatch { version: u8 },
137
138 #[error(
140 "continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
141 )]
142 ContinuationCursorSignatureMismatch {
143 entity_path: &'static str,
144 expected: String,
145 actual: String,
146 },
147
148 #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
150 ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
151
152 #[error(
154 "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
155 )]
156 ContinuationCursorBoundaryTypeMismatch {
157 field: String,
158 expected: String,
159 value: Value,
160 },
161
162 #[error(
164 "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
165 )]
166 ContinuationCursorPrimaryKeyTypeMismatch {
167 field: String,
168 expected: String,
169 value: Option<Value>,
170 },
171}
172
173impl From<PlanPolicyError> for PlanError {
174 fn from(err: PlanPolicyError) -> Self {
175 match err {
176 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
177 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
178 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
179 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
180 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
181 }
182 }
183}
184
185impl From<CursorOrderPolicyError> for PlanError {
186 fn from(err: CursorOrderPolicyError) -> Self {
187 match err {
188 CursorOrderPolicyError::CursorRequiresOrder => Self::CursorRequiresOrder,
189 }
190 }
191}
192
193pub(crate) fn validate_logical_plan_model(
202 schema: &SchemaInfo,
203 model: &EntityModel,
204 plan: &LogicalPlan<Value>,
205) -> Result<(), PlanError> {
206 if let Some(predicate) = &plan.predicate {
207 predicate::validate(schema, predicate)?;
208 }
209
210 if let Some(order) = &plan.order {
211 validate_order(schema, order)?;
212 validate_primary_key_tie_break(model, order)?;
213 }
214
215 validate_access_plan_model(schema, model, &plan.access)?;
216 semantics::validate_plan_semantics(plan)?;
217
218 Ok(())
219}
220
221pub(crate) fn validate_executor_plan<E: EntityKind>(
230 plan: &LogicalPlan<E::Key>,
231) -> Result<(), InternalError> {
232 let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
233 InternalError::new(
234 ErrorClass::InvariantViolation,
235 ErrorOrigin::Query,
236 format!("entity schema invalid for {}: {err}", E::PATH),
237 )
238 })?;
239
240 if let Some(predicate) = &plan.predicate {
241 predicate::validate(&schema, predicate).map_err(|err| {
242 InternalError::new(
243 ErrorClass::InvariantViolation,
244 ErrorOrigin::Query,
245 err.to_string(),
246 )
247 })?;
248 }
249
250 if let Some(order) = &plan.order {
251 order::validate_executor_order(&schema, order).map_err(|err| {
252 InternalError::new(
253 ErrorClass::InvariantViolation,
254 ErrorOrigin::Query,
255 err.to_string(),
256 )
257 })?;
258 validate_primary_key_tie_break(E::MODEL, order).map_err(|err| {
259 InternalError::new(
260 ErrorClass::InvariantViolation,
261 ErrorOrigin::Query,
262 err.to_string(),
263 )
264 })?;
265 }
266
267 validate_access_plan(&schema, E::MODEL, &plan.access).map_err(|err| {
268 InternalError::new(
269 ErrorClass::InvariantViolation,
270 ErrorOrigin::Query,
271 err.to_string(),
272 )
273 })?;
274
275 semantics::validate_plan_semantics(plan).map_err(|err| {
276 InternalError::new(
277 ErrorClass::InvariantViolation,
278 ErrorOrigin::Query,
279 err.to_string(),
280 )
281 })?;
282
283 Ok(())
284}