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::{validate_order, validate_primary_key_tie_break};
38#[cfg(test)]
39pub(crate) use pushdown::assess_secondary_order_pushdown_if_applicable;
40pub(crate) use pushdown::{
41 PushdownApplicability, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
42 SecondaryOrderPushdownRejection, assess_secondary_order_pushdown,
43 assess_secondary_order_pushdown_if_applicable_validated,
44};
45
46#[derive(Debug, ThisError)]
56pub enum PlanError {
57 #[error("predicate validation failed: {0}")]
58 PredicateInvalid(Box<predicate::ValidateError>),
59
60 #[error("{0}")]
61 Order(Box<OrderPlanError>),
62
63 #[error("{0}")]
64 Access(Box<AccessPlanError>),
65
66 #[error("{0}")]
67 Policy(Box<PolicyPlanError>),
68
69 #[error("{0}")]
70 Cursor(Box<CursorPlanError>),
71}
72
73#[derive(Debug, ThisError)]
79pub enum OrderPlanError {
80 #[error("unknown order field '{field}'")]
82 UnknownField { field: String },
83
84 #[error("order field '{field}' is not orderable")]
86 UnorderableField { field: String },
87
88 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
90 MissingPrimaryKeyTieBreak { field: String },
91}
92
93#[derive(Debug, ThisError)]
99pub enum AccessPlanError {
100 #[error("index '{index}' not found on entity")]
102 IndexNotFound { index: IndexModel },
103
104 #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
106 IndexPrefixTooLong { prefix_len: usize, field_len: usize },
107
108 #[error("index prefix must include at least one value")]
110 IndexPrefixEmpty,
111
112 #[error("index prefix value for field '{field}' is incompatible")]
114 IndexPrefixValueMismatch { field: String },
115
116 #[error("primary key field '{field}' is not key-compatible")]
118 PrimaryKeyNotKeyable { field: String },
119
120 #[error("key '{key:?}' is incompatible with primary key '{field}'")]
122 PrimaryKeyMismatch { field: String, key: Value },
123
124 #[error("key range start is greater than end")]
126 InvalidKeyRange,
127}
128
129#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
135pub enum PolicyPlanError {
136 #[error("order specification must include at least one field")]
138 EmptyOrderSpec,
139
140 #[error("delete plans must not include pagination")]
142 DeletePlanWithPagination,
143
144 #[error("load plans must not include delete limits")]
146 LoadPlanWithDeleteLimit,
147
148 #[error("delete limit requires an explicit ordering")]
150 DeleteLimitRequiresOrder,
151
152 #[error(
154 "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."
155 )]
156 UnorderedPagination,
157}
158
159#[derive(Debug, ThisError)]
165pub enum CursorPlanError {
166 #[error("cursor pagination requires an explicit ordering")]
168 CursorRequiresOrder,
169
170 #[error("invalid continuation cursor: {reason}")]
172 InvalidContinuationCursor { reason: CursorDecodeError },
173
174 #[error("invalid continuation cursor: {reason}")]
176 InvalidContinuationCursorPayload { reason: String },
177
178 #[error("unsupported continuation cursor version: {version}")]
180 ContinuationCursorVersionMismatch { version: u8 },
181
182 #[error(
184 "continuation cursor does not match query plan signature for '{entity_path}': expected={expected}, actual={actual}"
185 )]
186 ContinuationCursorSignatureMismatch {
187 entity_path: &'static str,
188 expected: String,
189 actual: String,
190 },
191
192 #[error("continuation cursor boundary arity mismatch: expected {expected}, found {found}")]
194 ContinuationCursorBoundaryArityMismatch { expected: usize, found: usize },
195
196 #[error(
198 "continuation cursor offset mismatch: expected {expected_offset}, found {actual_offset}"
199 )]
200 ContinuationCursorWindowMismatch {
201 expected_offset: u32,
202 actual_offset: u32,
203 },
204
205 #[error(
207 "continuation cursor boundary type mismatch for field '{field}': expected {expected}, found {value:?}"
208 )]
209 ContinuationCursorBoundaryTypeMismatch {
210 field: String,
211 expected: String,
212 value: Value,
213 },
214
215 #[error(
217 "continuation cursor primary key type mismatch for '{field}': expected {expected}, found {value:?}"
218 )]
219 ContinuationCursorPrimaryKeyTypeMismatch {
220 field: String,
221 expected: String,
222 value: Option<Value>,
223 },
224}
225
226impl From<PlanPolicyError> for PolicyPlanError {
227 fn from(err: PlanPolicyError) -> Self {
228 match err {
229 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
230 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
231 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
232 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
233 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
234 }
235 }
236}
237
238impl From<predicate::ValidateError> for PlanError {
239 fn from(err: predicate::ValidateError) -> Self {
240 Self::PredicateInvalid(Box::new(err))
241 }
242}
243
244impl From<OrderPlanError> for PlanError {
245 fn from(err: OrderPlanError) -> Self {
246 Self::Order(Box::new(err))
247 }
248}
249
250impl From<AccessPlanError> for PlanError {
251 fn from(err: AccessPlanError) -> Self {
252 Self::Access(Box::new(err))
253 }
254}
255
256impl From<PolicyPlanError> for PlanError {
257 fn from(err: PolicyPlanError) -> Self {
258 Self::Policy(Box::new(err))
259 }
260}
261
262impl From<CursorPlanError> for PlanError {
263 fn from(err: CursorPlanError) -> Self {
264 Self::Cursor(Box::new(err))
265 }
266}
267
268impl From<PlanPolicyError> for PlanError {
269 fn from(err: PlanPolicyError) -> Self {
270 Self::from(PolicyPlanError::from(err))
271 }
272}
273
274pub(crate) fn validate_logical_plan_model(
283 schema: &SchemaInfo,
284 model: &EntityModel,
285 plan: &LogicalPlan<Value>,
286) -> Result<(), PlanError> {
287 validate_plan_core(
288 schema,
289 model,
290 plan,
291 validate_order,
292 |schema, model, plan| validate_access_plan_model(schema, model, &plan.access),
293 )?;
294
295 Ok(())
296}
297
298pub(crate) fn validate_executor_plan<E: EntityKind>(
307 plan: &LogicalPlan<E::Key>,
308) -> Result<(), InternalError> {
309 let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
310 InternalError::query_invariant(format!("entity schema invalid for {}: {err}", E::PATH))
311 })?;
312
313 validate_access_plan(&schema, E::MODEL, &plan.access)
314 .map_err(InternalError::from_executor_plan_error)?;
315
316 Ok(())
317}
318
319fn validate_plan_core<K, FOrder, FAccess>(
321 schema: &SchemaInfo,
322 model: &EntityModel,
323 plan: &LogicalPlan<K>,
324 validate_order_fn: FOrder,
325 validate_access_fn: FAccess,
326) -> Result<(), PlanError>
327where
328 FOrder: Fn(&SchemaInfo, &crate::db::query::plan::OrderSpec) -> Result<(), PlanError>,
329 FAccess: Fn(&SchemaInfo, &EntityModel, &LogicalPlan<K>) -> Result<(), PlanError>,
330{
331 if let Some(predicate) = &plan.predicate {
332 predicate::validate(schema, predicate)?;
333 }
334
335 if let Some(order) = &plan.order {
336 validate_order_fn(schema, order)?;
337 validate_primary_key_tie_break(model, order)?;
338 }
339
340 validate_access_fn(schema, model, plan)?;
341 semantics::validate_plan_semantics(plan)?;
342
343 Ok(())
344}