1use crate::{
11 db::{
12 access::{
13 AccessPlanError,
14 validate_access_structure_model as validate_access_structure_model_shared,
15 },
16 contracts::{SchemaInfo, ValidateError},
17 cursor::CursorPlanError,
18 policy::{self, PlanPolicyError},
19 query::{
20 grouped::{GroupAggregateKind, GroupSpec, GroupedPlan},
21 plan::{AccessPlannedQuery, OrderSpec},
22 predicate,
23 },
24 },
25 model::entity::EntityModel,
26 value::Value,
27};
28use std::collections::BTreeSet;
29use thiserror::Error as ThisError;
30
31#[derive(Debug, ThisError)]
41pub enum PlanError {
42 #[error("predicate validation failed: {0}")]
43 PredicateInvalid(Box<ValidateError>),
44
45 #[error("{0}")]
46 Order(Box<OrderPlanError>),
47
48 #[error("{0}")]
49 Access(Box<AccessPlanError>),
50
51 #[error("{0}")]
52 Policy(Box<PolicyPlanError>),
53
54 #[error("{0}")]
55 Cursor(Box<CursorPlanError>),
56
57 #[error("{0}")]
58 Group(Box<GroupPlanError>),
59}
60
61#[derive(Debug, ThisError)]
67pub enum OrderPlanError {
68 #[error("unknown order field '{field}'")]
70 UnknownField { field: String },
71
72 #[error("order field '{field}' is not orderable")]
74 UnorderableField { field: String },
75
76 #[error("order field '{field}' appears multiple times")]
78 DuplicateOrderField { field: String },
79
80 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
82 MissingPrimaryKeyTieBreak { field: String },
83}
84
85#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
91pub enum PolicyPlanError {
92 #[error("order specification must include at least one field")]
94 EmptyOrderSpec,
95
96 #[error("delete plans must not include pagination")]
98 DeletePlanWithPagination,
99
100 #[error("load plans must not include delete limits")]
102 LoadPlanWithDeleteLimit,
103
104 #[error("delete limit requires an explicit ordering")]
106 DeleteLimitRequiresOrder,
107
108 #[error(
110 "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."
111 )]
112 UnorderedPagination,
113}
114
115#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
121pub enum GroupPlanError {
122 #[error("group specification must include at least one group field")]
124 EmptyGroupFields,
125
126 #[error("group specification must include at least one aggregate terminal")]
128 EmptyAggregates,
129
130 #[error("unknown group field '{field}'")]
132 UnknownGroupField { field: String },
133
134 #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
136 UnknownAggregateTargetField { index: usize, field: String },
137
138 #[error(
140 "grouped aggregate at index={index} requires MIN/MAX when targeting field '{field}': found {kind}"
141 )]
142 FieldTargetRequiresExtrema {
143 index: usize,
144 kind: String,
145 field: String,
146 },
147}
148
149impl From<PlanPolicyError> for PolicyPlanError {
150 fn from(err: PlanPolicyError) -> Self {
151 match err {
152 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
153 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
154 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
155 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
156 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
157 }
158 }
159}
160
161impl From<ValidateError> for PlanError {
162 fn from(err: ValidateError) -> Self {
163 Self::PredicateInvalid(Box::new(err))
164 }
165}
166
167impl From<OrderPlanError> for PlanError {
168 fn from(err: OrderPlanError) -> Self {
169 Self::Order(Box::new(err))
170 }
171}
172
173impl From<AccessPlanError> for PlanError {
174 fn from(err: AccessPlanError) -> Self {
175 Self::Access(Box::new(err))
176 }
177}
178
179impl From<PolicyPlanError> for PlanError {
180 fn from(err: PolicyPlanError) -> Self {
181 Self::Policy(Box::new(err))
182 }
183}
184
185impl From<CursorPlanError> for PlanError {
186 fn from(err: CursorPlanError) -> Self {
187 Self::Cursor(Box::new(err))
188 }
189}
190
191impl From<GroupPlanError> for PlanError {
192 fn from(err: GroupPlanError) -> Self {
193 Self::Group(Box::new(err))
194 }
195}
196
197impl From<PlanPolicyError> for PlanError {
198 fn from(err: PlanPolicyError) -> Self {
199 Self::from(PolicyPlanError::from(err))
200 }
201}
202
203pub(crate) fn validate_query_semantics(
212 schema: &SchemaInfo,
213 model: &EntityModel,
214 plan: &AccessPlannedQuery<Value>,
215) -> Result<(), PlanError> {
216 validate_plan_core(
217 schema,
218 model,
219 plan,
220 validate_order,
221 |schema, model, plan| {
222 validate_access_structure_model_shared(schema, model, &plan.access)
223 .map_err(PlanError::from)
224 },
225 )?;
226
227 Ok(())
228}
229
230#[allow(dead_code)]
236pub(crate) fn validate_group_query_semantics(
237 schema: &SchemaInfo,
238 model: &EntityModel,
239 plan: &GroupedPlan<Value>,
240) -> Result<(), PlanError> {
241 validate_plan_core(
242 schema,
243 model,
244 &plan.base,
245 validate_order,
246 |schema, model, plan| {
247 validate_access_structure_model_shared(schema, model, &plan.access)
248 .map_err(PlanError::from)
249 },
250 )?;
251 validate_group_spec(schema, &plan.group)?;
252
253 Ok(())
254}
255
256pub(crate) fn validate_group_spec(schema: &SchemaInfo, group: &GroupSpec) -> Result<(), PlanError> {
258 if group.group_fields.is_empty() {
259 return Err(PlanError::from(GroupPlanError::EmptyGroupFields));
260 }
261 if group.aggregates.is_empty() {
262 return Err(PlanError::from(GroupPlanError::EmptyAggregates));
263 }
264
265 for field in &group.group_fields {
266 if schema.field(field).is_none() {
267 return Err(PlanError::from(GroupPlanError::UnknownGroupField {
268 field: field.clone(),
269 }));
270 }
271 }
272
273 for (index, aggregate) in group.aggregates.iter().enumerate() {
274 let Some(target_field) = aggregate.target_field.as_ref() else {
275 continue;
276 };
277 if schema.field(target_field).is_none() {
278 return Err(PlanError::from(
279 GroupPlanError::UnknownAggregateTargetField {
280 index,
281 field: target_field.clone(),
282 },
283 ));
284 }
285 if !matches!(
286 aggregate.kind,
287 GroupAggregateKind::Min | GroupAggregateKind::Max
288 ) {
289 return Err(PlanError::from(
290 GroupPlanError::FieldTargetRequiresExtrema {
291 index,
292 kind: format!("{:?}", aggregate.kind),
293 field: target_field.clone(),
294 },
295 ));
296 }
297 }
298
299 Ok(())
300}
301
302fn validate_plan_core<K, FOrder, FAccess>(
304 schema: &SchemaInfo,
305 model: &EntityModel,
306 plan: &AccessPlannedQuery<K>,
307 validate_order_fn: FOrder,
308 validate_access_fn: FAccess,
309) -> Result<(), PlanError>
310where
311 FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
312 FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
313{
314 if let Some(predicate) = &plan.predicate {
315 predicate::validate(schema, predicate)?;
316 }
317
318 if let Some(order) = &plan.order {
319 validate_order_fn(schema, order)?;
320 validate_no_duplicate_non_pk_order_fields(model, order)?;
321 validate_primary_key_tie_break(model, order)?;
322 }
323
324 validate_access_fn(schema, model, plan)?;
325 policy::validate_plan_shape(plan)?;
326
327 Ok(())
328}
329pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
339 for (field, _) in &order.fields {
340 let field_type = schema
341 .field(field)
342 .ok_or_else(|| OrderPlanError::UnknownField {
343 field: field.clone(),
344 })
345 .map_err(PlanError::from)?;
346
347 if !field_type.is_orderable() {
348 return Err(PlanError::from(OrderPlanError::UnorderableField {
350 field: field.clone(),
351 }));
352 }
353 }
354
355 Ok(())
356}
357
358pub(crate) fn validate_no_duplicate_non_pk_order_fields(
360 model: &EntityModel,
361 order: &OrderSpec,
362) -> Result<(), PlanError> {
363 let mut seen = BTreeSet::new();
364 let pk_field = model.primary_key.name;
365
366 for (field, _) in &order.fields {
367 if field == pk_field {
368 continue;
369 }
370 if !seen.insert(field.as_str()) {
371 return Err(PlanError::from(OrderPlanError::DuplicateOrderField {
372 field: field.clone(),
373 }));
374 }
375 }
376
377 Ok(())
378}
379
380pub(crate) fn validate_primary_key_tie_break(
383 model: &EntityModel,
384 order: &OrderSpec,
385) -> Result<(), PlanError> {
386 if order.fields.is_empty() {
387 return Ok(());
388 }
389
390 let pk_field = model.primary_key.name;
391 let pk_count = order
392 .fields
393 .iter()
394 .filter(|(field, _)| field == pk_field)
395 .count();
396 let trailing_pk = order
397 .fields
398 .last()
399 .is_some_and(|(field, _)| field == pk_field);
400
401 if pk_count == 1 && trailing_pk {
402 Ok(())
403 } else {
404 Err(PlanError::from(OrderPlanError::MissingPrimaryKeyTieBreak {
405 field: pk_field.to_string(),
406 }))
407 }
408}
409
410#[cfg(test)]
411mod tests;