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 codec::cursor::CursorDecodeError,
22 query::{
23 plan::LogicalPlan,
24 policy::PlanPolicyError,
25 predicate::{self, SchemaInfo},
26 },
27 },
28 error::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};
37pub(crate) use order::{
38 validate_no_duplicate_non_pk_order_fields, validate_order, validate_primary_key_tie_break,
39};
40#[cfg(test)]
41pub(crate) use pushdown::assess_secondary_order_pushdown_if_applicable;
42pub(crate) use pushdown::{
43 PushdownApplicability, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
44 SecondaryOrderPushdownRejection, assess_secondary_order_pushdown,
45 assess_secondary_order_pushdown_if_applicable_validated,
46};
47
48#[derive(Debug, ThisError)]
58pub enum PlanError {
59 #[error("predicate validation failed: {0}")]
60 PredicateInvalid(Box<predicate::ValidateError>),
61
62 #[error("{0}")]
63 Order(Box<OrderPlanError>),
64
65 #[error("{0}")]
66 Access(Box<AccessPlanError>),
67
68 #[error("{0}")]
69 Policy(Box<PolicyPlanError>),
70
71 #[error("{0}")]
72 Cursor(Box<CursorPlanError>),
73}
74
75#[derive(Debug, ThisError)]
81pub enum OrderPlanError {
82 #[error("unknown order field '{field}'")]
84 UnknownField { field: String },
85
86 #[error("order field '{field}' is not orderable")]
88 UnorderableField { field: String },
89
90 #[error("order field '{field}' appears multiple times")]
92 DuplicateOrderField { field: String },
93
94 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
96 MissingPrimaryKeyTieBreak { field: String },
97}
98
99#[derive(Debug, ThisError)]
105pub enum AccessPlanError {
106 #[error("index '{index}' not found on entity")]
108 IndexNotFound { index: IndexModel },
109
110 #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
112 IndexPrefixTooLong { prefix_len: usize, field_len: usize },
113
114 #[error("index prefix must include at least one value")]
116 IndexPrefixEmpty,
117
118 #[error("index prefix value for field '{field}' is incompatible")]
120 IndexPrefixValueMismatch { field: String },
121
122 #[error("primary key field '{field}' is not key-compatible")]
124 PrimaryKeyNotKeyable { field: String },
125
126 #[error("key '{key:?}' is incompatible with primary key '{field}'")]
128 PrimaryKeyMismatch { field: String, key: Value },
129
130 #[error("key range start is greater than end")]
132 InvalidKeyRange,
133}
134
135#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
141pub enum PolicyPlanError {
142 #[error("order specification must include at least one field")]
144 EmptyOrderSpec,
145
146 #[error("delete plans must not include pagination")]
148 DeletePlanWithPagination,
149
150 #[error("load plans must not include delete limits")]
152 LoadPlanWithDeleteLimit,
153
154 #[error("delete limit requires an explicit ordering")]
156 DeleteLimitRequiresOrder,
157
158 #[error(
160 "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."
161 )]
162 UnorderedPagination,
163}
164
165#[derive(Debug, ThisError)]
171pub enum CursorPlanError {
172 #[error("cursor pagination requires an explicit ordering")]
174 CursorRequiresOrder,
175
176 #[error("invalid continuation cursor: {reason}")]
178 InvalidContinuationCursor { reason: CursorDecodeError },
179
180 #[error("invalid continuation cursor: {reason}")]
182 InvalidContinuationCursorPayload { reason: String },
183
184 #[error("unsupported continuation cursor version: {version}")]
186 ContinuationCursorVersionMismatch { version: u8 },
187
188 #[error(
190 "continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
191 )]
192 ContinuationCursorSignatureMismatch {
193 entity_path: &'static str,
194 expected: String,
195 actual: String,
196 },
197
198 #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
200 ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
201
202 #[error(
204 "continuation cursor offset mismatch: expected {expected_offset}, found {actual_offset}"
205 )]
206 ContinuationCursorWindowMismatch {
207 expected_offset: u32,
208 actual_offset: u32,
209 },
210
211 #[error(
213 "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
214 )]
215 ContinuationCursorBoundaryTypeMismatch {
216 field: String,
217 expected: String,
218 value: Value,
219 },
220
221 #[error(
223 "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
224 )]
225 ContinuationCursorPrimaryKeyTypeMismatch {
226 field: String,
227 expected: String,
228 value: Option<Value>,
229 },
230}
231
232impl From<PlanPolicyError> for PolicyPlanError {
233 fn from(err: PlanPolicyError) -> Self {
234 match err {
235 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
236 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
237 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
238 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
239 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
240 }
241 }
242}
243
244impl From<predicate::ValidateError> for PlanError {
245 fn from(err: predicate::ValidateError) -> Self {
246 Self::PredicateInvalid(Box::new(err))
247 }
248}
249
250impl From<OrderPlanError> for PlanError {
251 fn from(err: OrderPlanError) -> Self {
252 Self::Order(Box::new(err))
253 }
254}
255
256impl From<AccessPlanError> for PlanError {
257 fn from(err: AccessPlanError) -> Self {
258 Self::Access(Box::new(err))
259 }
260}
261
262impl From<PolicyPlanError> for PlanError {
263 fn from(err: PolicyPlanError) -> Self {
264 Self::Policy(Box::new(err))
265 }
266}
267
268impl From<CursorPlanError> for PlanError {
269 fn from(err: CursorPlanError) -> Self {
270 Self::Cursor(Box::new(err))
271 }
272}
273
274impl From<PlanPolicyError> for PlanError {
275 fn from(err: PlanPolicyError) -> Self {
276 Self::from(PolicyPlanError::from(err))
277 }
278}
279
280pub(crate) fn validate_logical_plan_model(
289 schema: &SchemaInfo,
290 model: &EntityModel,
291 plan: &LogicalPlan<Value>,
292) -> Result<(), PlanError> {
293 validate_plan_core(
294 schema,
295 model,
296 plan,
297 validate_order,
298 |schema, model, plan| validate_access_plan_model(schema, model, &plan.access),
299 )?;
300
301 Ok(())
302}
303
304pub(crate) fn validate_executor_plan<E: EntityKind>(
313 plan: &LogicalPlan<E::Key>,
314) -> Result<(), InternalError> {
315 let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
316 InternalError::query_invariant(format!("entity schema invalid for {}: {err}", E::PATH))
317 })?;
318
319 validate_access_plan(&schema, E::MODEL, &plan.access)
320 .map_err(InternalError::from_executor_plan_error)?;
321
322 Ok(())
323}
324
325fn validate_plan_core<K, FOrder, FAccess>(
327 schema: &SchemaInfo,
328 model: &EntityModel,
329 plan: &LogicalPlan<K>,
330 validate_order_fn: FOrder,
331 validate_access_fn: FAccess,
332) -> Result<(), PlanError>
333where
334 FOrder: Fn(&SchemaInfo, &crate::db::query::plan::OrderSpec) -> Result<(), PlanError>,
335 FAccess: Fn(&SchemaInfo, &EntityModel, &LogicalPlan<K>) -> Result<(), PlanError>,
336{
337 if let Some(predicate) = &plan.predicate {
338 predicate::validate(schema, predicate)?;
339 }
340
341 if let Some(order) = &plan.order {
342 validate_order_fn(schema, order)?;
343 validate_no_duplicate_non_pk_order_fields(model, order)?;
344 validate_primary_key_tie_break(model, order)?;
345 }
346
347 validate_access_fn(schema, model, plan)?;
348 semantics::validate_plan_semantics(plan)?;
349
350 Ok(())
351}