icydb_core/db/query/plan/validate/
mod.rs1mod order;
11
12#[cfg(test)]
13mod tests;
14
15use crate::db::query::plan::{AccessPlannedQuery, OrderSpec};
16use crate::{
17 db::{
18 access::{
19 AccessPlanError,
20 validate_access_structure_model as validate_access_structure_model_shared,
21 },
22 contracts::SchemaInfo,
23 cursor::CursorPlanError,
24 policy::{self, PlanPolicyError},
25 query::predicate::{self},
26 },
27 model::entity::EntityModel,
28 value::Value,
29};
30use thiserror::Error as ThisError;
31
32pub(crate) use order::{
33 validate_no_duplicate_non_pk_order_fields, validate_order, validate_primary_key_tie_break,
34};
35
36#[derive(Debug, ThisError)]
46pub enum PlanError {
47 #[error("predicate validation failed: {0}")]
48 PredicateInvalid(Box<predicate::ValidateError>),
49
50 #[error("{0}")]
51 Order(Box<OrderPlanError>),
52
53 #[error("{0}")]
54 Access(Box<AccessPlanError>),
55
56 #[error("{0}")]
57 Policy(Box<PolicyPlanError>),
58
59 #[error("{0}")]
60 Cursor(Box<CursorPlanError>),
61}
62
63#[derive(Debug, ThisError)]
69pub enum OrderPlanError {
70 #[error("unknown order field '{field}'")]
72 UnknownField { field: String },
73
74 #[error("order field '{field}' is not orderable")]
76 UnorderableField { field: String },
77
78 #[error("order field '{field}' appears multiple times")]
80 DuplicateOrderField { field: String },
81
82 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
84 MissingPrimaryKeyTieBreak { field: String },
85}
86
87#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
93pub enum PolicyPlanError {
94 #[error("order specification must include at least one field")]
96 EmptyOrderSpec,
97
98 #[error("delete plans must not include pagination")]
100 DeletePlanWithPagination,
101
102 #[error("load plans must not include delete limits")]
104 LoadPlanWithDeleteLimit,
105
106 #[error("delete limit requires an explicit ordering")]
108 DeleteLimitRequiresOrder,
109
110 #[error(
112 "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."
113 )]
114 UnorderedPagination,
115}
116
117impl From<PlanPolicyError> for PolicyPlanError {
118 fn from(err: PlanPolicyError) -> Self {
119 match err {
120 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
121 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
122 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
123 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
124 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
125 }
126 }
127}
128
129impl From<predicate::ValidateError> for PlanError {
130 fn from(err: predicate::ValidateError) -> Self {
131 Self::PredicateInvalid(Box::new(err))
132 }
133}
134
135impl From<OrderPlanError> for PlanError {
136 fn from(err: OrderPlanError) -> Self {
137 Self::Order(Box::new(err))
138 }
139}
140
141impl From<AccessPlanError> for PlanError {
142 fn from(err: AccessPlanError) -> Self {
143 Self::Access(Box::new(err))
144 }
145}
146
147impl From<PolicyPlanError> for PlanError {
148 fn from(err: PolicyPlanError) -> Self {
149 Self::Policy(Box::new(err))
150 }
151}
152
153impl From<CursorPlanError> for PlanError {
154 fn from(err: CursorPlanError) -> Self {
155 Self::Cursor(Box::new(err))
156 }
157}
158
159impl From<PlanPolicyError> for PlanError {
160 fn from(err: PlanPolicyError) -> Self {
161 Self::from(PolicyPlanError::from(err))
162 }
163}
164
165pub(crate) fn validate_query_semantics(
174 schema: &SchemaInfo,
175 model: &EntityModel,
176 plan: &AccessPlannedQuery<Value>,
177) -> Result<(), PlanError> {
178 validate_plan_core(
179 schema,
180 model,
181 plan,
182 validate_order,
183 |schema, model, plan| {
184 validate_access_structure_model_shared(schema, model, &plan.access)
185 .map_err(PlanError::from)
186 },
187 )?;
188
189 Ok(())
190}
191
192fn validate_plan_core<K, FOrder, FAccess>(
194 schema: &SchemaInfo,
195 model: &EntityModel,
196 plan: &AccessPlannedQuery<K>,
197 validate_order_fn: FOrder,
198 validate_access_fn: FAccess,
199) -> Result<(), PlanError>
200where
201 FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
202 FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
203{
204 if let Some(predicate) = &plan.predicate {
205 predicate::validate(schema, predicate)?;
206 }
207
208 if let Some(order) = &plan.order {
209 validate_order_fn(schema, order)?;
210 validate_no_duplicate_non_pk_order_fields(model, order)?;
211 validate_primary_key_tie_break(model, order)?;
212 }
213
214 validate_access_fn(schema, model, plan)?;
215 policy::validate_plan_shape(plan)?;
216
217 Ok(())
218}