icydb_core/db/query/plan/
validate.rs1use 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 plan::{
21 AccessPlannedQuery, GroupAggregateKind, GroupSpec, LogicalPlan, OrderSpec,
22 ScalarPlan,
23 },
24 predicate,
25 },
26 },
27 model::entity::EntityModel,
28 value::Value,
29};
30use std::collections::BTreeSet;
31use thiserror::Error as ThisError;
32
33#[derive(Debug, ThisError)]
43pub enum PlanError {
44 #[error("predicate validation failed: {0}")]
45 PredicateInvalid(Box<ValidateError>),
46
47 #[error("{0}")]
48 Order(Box<OrderPlanError>),
49
50 #[error("{0}")]
51 Access(Box<AccessPlanError>),
52
53 #[error("{0}")]
54 Policy(Box<PolicyPlanError>),
55
56 #[error("{0}")]
57 Cursor(Box<CursorPlanError>),
58
59 #[error("{0}")]
60 Group(Box<GroupPlanError>),
61}
62
63#[derive(Debug, ThisError)]
70pub enum OrderPlanError {
71 #[error("unknown order field '{field}'")]
73 UnknownField { field: String },
74
75 #[error("order field '{field}' is not orderable")]
77 UnorderableField { field: String },
78
79 #[error("order field '{field}' appears multiple times")]
81 DuplicateOrderField { field: String },
82
83 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
85 MissingPrimaryKeyTieBreak { field: String },
86}
87
88#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
95pub enum PolicyPlanError {
96 #[error("order specification must include at least one field")]
98 EmptyOrderSpec,
99
100 #[error("delete plans must not include pagination")]
102 DeletePlanWithPagination,
103
104 #[error("load plans must not include delete limits")]
106 LoadPlanWithDeleteLimit,
107
108 #[error("delete limit requires an explicit ordering")]
110 DeleteLimitRequiresOrder,
111
112 #[error(
114 "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."
115 )]
116 UnorderedPagination,
117}
118
119#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
126pub enum GroupPlanError {
127 #[error("group query validation requires grouped logical plan variant")]
129 GroupedLogicalPlanRequired,
130
131 #[error("group specification must include at least one group field")]
133 EmptyGroupFields,
134
135 #[error("group specification must include at least one aggregate terminal")]
137 EmptyAggregates,
138
139 #[error("unknown group field '{field}'")]
141 UnknownGroupField { field: String },
142
143 #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
145 UnknownAggregateTargetField { index: usize, field: String },
146
147 #[error(
149 "grouped aggregate at index={index} requires MIN/MAX when targeting field '{field}': found {kind}"
150 )]
151 FieldTargetRequiresExtrema {
152 index: usize,
153 kind: String,
154 field: String,
155 },
156}
157
158impl From<PlanPolicyError> for PolicyPlanError {
159 fn from(err: PlanPolicyError) -> Self {
160 match err {
161 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
162 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
163 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
164 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
165 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
166 }
167 }
168}
169
170impl From<ValidateError> for PlanError {
171 fn from(err: ValidateError) -> Self {
172 Self::PredicateInvalid(Box::new(err))
173 }
174}
175
176impl From<OrderPlanError> for PlanError {
177 fn from(err: OrderPlanError) -> Self {
178 Self::Order(Box::new(err))
179 }
180}
181
182impl From<AccessPlanError> for PlanError {
183 fn from(err: AccessPlanError) -> Self {
184 Self::Access(Box::new(err))
185 }
186}
187
188impl From<PolicyPlanError> for PlanError {
189 fn from(err: PolicyPlanError) -> Self {
190 Self::Policy(Box::new(err))
191 }
192}
193
194impl From<CursorPlanError> for PlanError {
195 fn from(err: CursorPlanError) -> Self {
196 Self::Cursor(Box::new(err))
197 }
198}
199
200impl From<GroupPlanError> for PlanError {
201 fn from(err: GroupPlanError) -> Self {
202 Self::Group(Box::new(err))
203 }
204}
205
206impl From<PlanPolicyError> for PlanError {
207 fn from(err: PlanPolicyError) -> Self {
208 Self::from(PolicyPlanError::from(err))
209 }
210}
211
212pub(crate) fn validate_query_semantics(
221 schema: &SchemaInfo,
222 model: &EntityModel,
223 plan: &AccessPlannedQuery<Value>,
224) -> Result<(), PlanError> {
225 let logical = plan.scalar_plan();
226
227 validate_plan_core(
228 schema,
229 model,
230 logical,
231 plan,
232 validate_order,
233 |schema, model, plan| {
234 validate_access_structure_model_shared(schema, model, &plan.access)
235 .map_err(PlanError::from)
236 },
237 )?;
238
239 Ok(())
240}
241
242pub(crate) fn validate_group_query_semantics(
248 schema: &SchemaInfo,
249 model: &EntityModel,
250 plan: &AccessPlannedQuery<Value>,
251) -> Result<(), PlanError> {
252 let logical = plan.scalar_plan();
253 let group = match &plan.logical {
254 LogicalPlan::Grouped(grouped) => &grouped.group,
255 LogicalPlan::Scalar(_) => {
256 return Err(PlanError::from(GroupPlanError::GroupedLogicalPlanRequired));
257 }
258 };
259
260 validate_plan_core(
261 schema,
262 model,
263 logical,
264 plan,
265 validate_order,
266 |schema, model, plan| {
267 validate_access_structure_model_shared(schema, model, &plan.access)
268 .map_err(PlanError::from)
269 },
270 )?;
271 validate_group_spec(schema, model, group)?;
272
273 Ok(())
274}
275
276pub(crate) fn validate_group_spec(
278 schema: &SchemaInfo,
279 model: &EntityModel,
280 group: &GroupSpec,
281) -> Result<(), PlanError> {
282 if group.group_fields.is_empty() {
283 return Err(PlanError::from(GroupPlanError::EmptyGroupFields));
284 }
285 if group.aggregates.is_empty() {
286 return Err(PlanError::from(GroupPlanError::EmptyAggregates));
287 }
288
289 for field_slot in &group.group_fields {
290 if model.fields.get(field_slot.index()).is_none() {
291 return Err(PlanError::from(GroupPlanError::UnknownGroupField {
292 field: field_slot.field().to_string(),
293 }));
294 }
295 }
296
297 for (index, aggregate) in group.aggregates.iter().enumerate() {
298 let Some(target_field) = aggregate.target_field.as_ref() else {
299 continue;
300 };
301 if schema.field(target_field).is_none() {
302 return Err(PlanError::from(
303 GroupPlanError::UnknownAggregateTargetField {
304 index,
305 field: target_field.clone(),
306 },
307 ));
308 }
309 if !matches!(
310 aggregate.kind,
311 GroupAggregateKind::Min | GroupAggregateKind::Max
312 ) {
313 return Err(PlanError::from(
314 GroupPlanError::FieldTargetRequiresExtrema {
315 index,
316 kind: format!("{:?}", aggregate.kind),
317 field: target_field.clone(),
318 },
319 ));
320 }
321 }
322
323 Ok(())
324}
325
326fn validate_plan_core<K, FOrder, FAccess>(
328 schema: &SchemaInfo,
329 model: &EntityModel,
330 logical: &ScalarPlan,
331 plan: &AccessPlannedQuery<K>,
332 validate_order_fn: FOrder,
333 validate_access_fn: FAccess,
334) -> Result<(), PlanError>
335where
336 FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
337 FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
338{
339 if let Some(predicate) = &logical.predicate {
340 predicate::validate(schema, predicate)?;
341 }
342
343 if let Some(order) = &logical.order {
344 validate_order_fn(schema, order)?;
345 validate_no_duplicate_non_pk_order_fields(model, order)?;
346 validate_primary_key_tie_break(model, order)?;
347 }
348
349 validate_access_fn(schema, model, plan)?;
350 policy::validate_plan_shape(&plan.logical)?;
351
352 Ok(())
353}
354pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
364 for (field, _) in &order.fields {
365 let field_type = schema
366 .field(field)
367 .ok_or_else(|| OrderPlanError::UnknownField {
368 field: field.clone(),
369 })
370 .map_err(PlanError::from)?;
371
372 if !field_type.is_orderable() {
373 return Err(PlanError::from(OrderPlanError::UnorderableField {
375 field: field.clone(),
376 }));
377 }
378 }
379
380 Ok(())
381}
382
383pub(crate) fn validate_no_duplicate_non_pk_order_fields(
385 model: &EntityModel,
386 order: &OrderSpec,
387) -> Result<(), PlanError> {
388 let mut seen = BTreeSet::new();
389 let pk_field = model.primary_key.name;
390
391 for (field, _) in &order.fields {
392 if field == pk_field {
393 continue;
394 }
395 if !seen.insert(field.as_str()) {
396 return Err(PlanError::from(OrderPlanError::DuplicateOrderField {
397 field: field.clone(),
398 }));
399 }
400 }
401
402 Ok(())
403}
404
405pub(crate) fn validate_primary_key_tie_break(
408 model: &EntityModel,
409 order: &OrderSpec,
410) -> Result<(), PlanError> {
411 if order.fields.is_empty() {
412 return Ok(());
413 }
414
415 let pk_field = model.primary_key.name;
416 let pk_count = order
417 .fields
418 .iter()
419 .filter(|(field, _)| field == pk_field)
420 .count();
421 let trailing_pk = order
422 .fields
423 .last()
424 .is_some_and(|(field, _)| field == pk_field);
425
426 if pk_count == 1 && trailing_pk {
427 Ok(())
428 } else {
429 Err(PlanError::from(OrderPlanError::MissingPrimaryKeyTieBreak {
430 field: pk_field.to_string(),
431 }))
432 }
433}