1use crate::db::query::plan::{AccessPlannedQuery, OrderSpec};
11use crate::{
12 db::{
13 access::{
14 AccessPlanError,
15 validate_access_structure_model as validate_access_structure_model_shared,
16 },
17 contracts::{SchemaInfo, ValidateError},
18 cursor::CursorPlanError,
19 policy::{self, PlanPolicyError},
20 query::predicate,
21 },
22 model::entity::EntityModel,
23 value::Value,
24};
25use std::collections::BTreeSet;
26use thiserror::Error as ThisError;
27
28#[derive(Debug, ThisError)]
38pub enum PlanError {
39 #[error("predicate validation failed: {0}")]
40 PredicateInvalid(Box<ValidateError>),
41
42 #[error("{0}")]
43 Order(Box<OrderPlanError>),
44
45 #[error("{0}")]
46 Access(Box<AccessPlanError>),
47
48 #[error("{0}")]
49 Policy(Box<PolicyPlanError>),
50
51 #[error("{0}")]
52 Cursor(Box<CursorPlanError>),
53}
54
55#[derive(Debug, ThisError)]
61pub enum OrderPlanError {
62 #[error("unknown order field '{field}'")]
64 UnknownField { field: String },
65
66 #[error("order field '{field}' is not orderable")]
68 UnorderableField { field: String },
69
70 #[error("order field '{field}' appears multiple times")]
72 DuplicateOrderField { field: String },
73
74 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
76 MissingPrimaryKeyTieBreak { field: String },
77}
78
79#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
85pub enum PolicyPlanError {
86 #[error("order specification must include at least one field")]
88 EmptyOrderSpec,
89
90 #[error("delete plans must not include pagination")]
92 DeletePlanWithPagination,
93
94 #[error("load plans must not include delete limits")]
96 LoadPlanWithDeleteLimit,
97
98 #[error("delete limit requires an explicit ordering")]
100 DeleteLimitRequiresOrder,
101
102 #[error(
104 "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."
105 )]
106 UnorderedPagination,
107}
108
109impl From<PlanPolicyError> for PolicyPlanError {
110 fn from(err: PlanPolicyError) -> Self {
111 match err {
112 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
113 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
114 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
115 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
116 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
117 }
118 }
119}
120
121impl From<ValidateError> for PlanError {
122 fn from(err: ValidateError) -> Self {
123 Self::PredicateInvalid(Box::new(err))
124 }
125}
126
127impl From<OrderPlanError> for PlanError {
128 fn from(err: OrderPlanError) -> Self {
129 Self::Order(Box::new(err))
130 }
131}
132
133impl From<AccessPlanError> for PlanError {
134 fn from(err: AccessPlanError) -> Self {
135 Self::Access(Box::new(err))
136 }
137}
138
139impl From<PolicyPlanError> for PlanError {
140 fn from(err: PolicyPlanError) -> Self {
141 Self::Policy(Box::new(err))
142 }
143}
144
145impl From<CursorPlanError> for PlanError {
146 fn from(err: CursorPlanError) -> Self {
147 Self::Cursor(Box::new(err))
148 }
149}
150
151impl From<PlanPolicyError> for PlanError {
152 fn from(err: PlanPolicyError) -> Self {
153 Self::from(PolicyPlanError::from(err))
154 }
155}
156
157pub(crate) fn validate_query_semantics(
166 schema: &SchemaInfo,
167 model: &EntityModel,
168 plan: &AccessPlannedQuery<Value>,
169) -> Result<(), PlanError> {
170 validate_plan_core(
171 schema,
172 model,
173 plan,
174 validate_order,
175 |schema, model, plan| {
176 validate_access_structure_model_shared(schema, model, &plan.access)
177 .map_err(PlanError::from)
178 },
179 )?;
180
181 Ok(())
182}
183
184fn validate_plan_core<K, FOrder, FAccess>(
186 schema: &SchemaInfo,
187 model: &EntityModel,
188 plan: &AccessPlannedQuery<K>,
189 validate_order_fn: FOrder,
190 validate_access_fn: FAccess,
191) -> Result<(), PlanError>
192where
193 FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
194 FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
195{
196 if let Some(predicate) = &plan.predicate {
197 predicate::validate(schema, predicate)?;
198 }
199
200 if let Some(order) = &plan.order {
201 validate_order_fn(schema, order)?;
202 validate_no_duplicate_non_pk_order_fields(model, order)?;
203 validate_primary_key_tie_break(model, order)?;
204 }
205
206 validate_access_fn(schema, model, plan)?;
207 policy::validate_plan_shape(plan)?;
208
209 Ok(())
210}
211pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
221 for (field, _) in &order.fields {
222 let field_type = schema
223 .field(field)
224 .ok_or_else(|| OrderPlanError::UnknownField {
225 field: field.clone(),
226 })
227 .map_err(PlanError::from)?;
228
229 if !field_type.is_orderable() {
230 return Err(PlanError::from(OrderPlanError::UnorderableField {
232 field: field.clone(),
233 }));
234 }
235 }
236
237 Ok(())
238}
239
240pub(crate) fn validate_no_duplicate_non_pk_order_fields(
242 model: &EntityModel,
243 order: &OrderSpec,
244) -> Result<(), PlanError> {
245 let mut seen = BTreeSet::new();
246 let pk_field = model.primary_key.name;
247
248 for (field, _) in &order.fields {
249 if field == pk_field {
250 continue;
251 }
252 if !seen.insert(field.as_str()) {
253 return Err(PlanError::from(OrderPlanError::DuplicateOrderField {
254 field: field.clone(),
255 }));
256 }
257 }
258
259 Ok(())
260}
261
262pub(crate) fn validate_primary_key_tie_break(
265 model: &EntityModel,
266 order: &OrderSpec,
267) -> Result<(), PlanError> {
268 if order.fields.is_empty() {
269 return Ok(());
270 }
271
272 let pk_field = model.primary_key.name;
273 let pk_count = order
274 .fields
275 .iter()
276 .filter(|(field, _)| field == pk_field)
277 .count();
278 let trailing_pk = order
279 .fields
280 .last()
281 .is_some_and(|(field, _)| field == pk_field);
282
283 if pk_count == 1 && trailing_pk {
284 Ok(())
285 } else {
286 Err(PlanError::from(OrderPlanError::MissingPrimaryKeyTieBreak {
287 field: pk_field.to_string(),
288 }))
289 }
290}
291
292#[cfg(test)]
293mod tests;