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 cursor::CursorPlanError,
17 policy::{self, PlanPolicyError},
18 predicate::{SchemaInfo, ValidateError, validate},
19 query::plan::{AccessPlannedQuery, GroupSpec, LogicalPlan, OrderSpec, ScalarPlan},
20 },
21 model::entity::EntityModel,
22 value::Value,
23};
24use std::collections::BTreeSet;
25use thiserror::Error as ThisError;
26
27#[derive(Debug, ThisError)]
37pub enum PlanError {
38 #[error("predicate validation failed: {0}")]
39 PredicateInvalid(Box<ValidateError>),
40
41 #[error("{0}")]
42 Order(Box<OrderPlanError>),
43
44 #[error("{0}")]
45 Access(Box<AccessPlanError>),
46
47 #[error("{0}")]
48 Policy(Box<PolicyPlanError>),
49
50 #[error("{0}")]
51 Cursor(Box<CursorPlanError>),
52
53 #[error("{0}")]
54 Group(Box<GroupPlanError>),
55}
56
57#[derive(Debug, ThisError)]
64pub enum OrderPlanError {
65 #[error("unknown order field '{field}'")]
67 UnknownField { field: String },
68
69 #[error("order field '{field}' is not orderable")]
71 UnorderableField { field: String },
72
73 #[error("order field '{field}' appears multiple times")]
75 DuplicateOrderField { field: String },
76
77 #[error("order specification must end with primary key '{field}' as deterministic tie-break")]
79 MissingPrimaryKeyTieBreak { field: String },
80}
81
82#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
89pub enum PolicyPlanError {
90 #[error("order specification must include at least one field")]
92 EmptyOrderSpec,
93
94 #[error("delete plans must not include pagination")]
96 DeletePlanWithPagination,
97
98 #[error("load plans must not include delete limits")]
100 LoadPlanWithDeleteLimit,
101
102 #[error("delete limit requires an explicit ordering")]
104 DeleteLimitRequiresOrder,
105
106 #[error(
108 "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."
109 )]
110 UnorderedPagination,
111}
112
113#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
120pub enum GroupPlanError {
121 #[error("group query validation requires grouped logical plan variant")]
123 GroupedLogicalPlanRequired,
124
125 #[error("group specification must include at least one group field")]
127 EmptyGroupFields,
128
129 #[error("group specification must include at least one aggregate terminal")]
131 EmptyAggregates,
132
133 #[error("unknown group field '{field}'")]
135 UnknownGroupField { field: String },
136
137 #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
139 UnknownAggregateTargetField { index: usize, field: String },
140
141 #[error(
143 "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
144 )]
145 FieldTargetAggregatesUnsupported {
146 index: usize,
147 kind: String,
148 field: String,
149 },
150}
151
152impl From<PlanPolicyError> for PolicyPlanError {
153 fn from(err: PlanPolicyError) -> Self {
154 match err {
155 PlanPolicyError::EmptyOrderSpec => Self::EmptyOrderSpec,
156 PlanPolicyError::DeletePlanWithPagination => Self::DeletePlanWithPagination,
157 PlanPolicyError::LoadPlanWithDeleteLimit => Self::LoadPlanWithDeleteLimit,
158 PlanPolicyError::DeleteLimitRequiresOrder => Self::DeleteLimitRequiresOrder,
159 PlanPolicyError::UnorderedPagination => Self::UnorderedPagination,
160 }
161 }
162}
163
164impl From<ValidateError> for PlanError {
165 fn from(err: ValidateError) -> Self {
166 Self::PredicateInvalid(Box::new(err))
167 }
168}
169
170impl From<OrderPlanError> for PlanError {
171 fn from(err: OrderPlanError) -> Self {
172 Self::Order(Box::new(err))
173 }
174}
175
176impl From<AccessPlanError> for PlanError {
177 fn from(err: AccessPlanError) -> Self {
178 Self::Access(Box::new(err))
179 }
180}
181
182impl From<PolicyPlanError> for PlanError {
183 fn from(err: PolicyPlanError) -> Self {
184 Self::Policy(Box::new(err))
185 }
186}
187
188impl From<CursorPlanError> for PlanError {
189 fn from(err: CursorPlanError) -> Self {
190 Self::Cursor(Box::new(err))
191 }
192}
193
194impl From<GroupPlanError> for PlanError {
195 fn from(err: GroupPlanError) -> Self {
196 Self::Group(Box::new(err))
197 }
198}
199
200impl From<PlanPolicyError> for PlanError {
201 fn from(err: PlanPolicyError) -> Self {
202 Self::from(PolicyPlanError::from(err))
203 }
204}
205
206pub(crate) fn validate_query_semantics(
215 schema: &SchemaInfo,
216 model: &EntityModel,
217 plan: &AccessPlannedQuery<Value>,
218) -> Result<(), PlanError> {
219 let logical = plan.scalar_plan();
220
221 validate_plan_core(
222 schema,
223 model,
224 logical,
225 plan,
226 validate_order,
227 |schema, model, plan| {
228 validate_access_structure_model_shared(schema, model, &plan.access)
229 .map_err(PlanError::from)
230 },
231 )?;
232
233 Ok(())
234}
235
236pub(crate) fn validate_group_query_semantics(
242 schema: &SchemaInfo,
243 model: &EntityModel,
244 plan: &AccessPlannedQuery<Value>,
245) -> Result<(), PlanError> {
246 let logical = plan.scalar_plan();
247 let group = match &plan.logical {
248 LogicalPlan::Grouped(grouped) => &grouped.group,
249 LogicalPlan::Scalar(_) => {
250 return Err(PlanError::from(GroupPlanError::GroupedLogicalPlanRequired));
251 }
252 };
253
254 validate_plan_core(
255 schema,
256 model,
257 logical,
258 plan,
259 validate_order,
260 |schema, model, plan| {
261 validate_access_structure_model_shared(schema, model, &plan.access)
262 .map_err(PlanError::from)
263 },
264 )?;
265 validate_group_spec(schema, model, group)?;
266
267 Ok(())
268}
269
270pub(crate) fn validate_group_spec(
272 schema: &SchemaInfo,
273 model: &EntityModel,
274 group: &GroupSpec,
275) -> Result<(), PlanError> {
276 if group.group_fields.is_empty() {
277 return Err(PlanError::from(GroupPlanError::EmptyGroupFields));
278 }
279 if group.aggregates.is_empty() {
280 return Err(PlanError::from(GroupPlanError::EmptyAggregates));
281 }
282
283 for field_slot in &group.group_fields {
284 if model.fields.get(field_slot.index()).is_none() {
285 return Err(PlanError::from(GroupPlanError::UnknownGroupField {
286 field: field_slot.field().to_string(),
287 }));
288 }
289 }
290
291 for (index, aggregate) in group.aggregates.iter().enumerate() {
292 let Some(target_field) = aggregate.target_field.as_ref() else {
293 continue;
294 };
295 if schema.field(target_field).is_none() {
296 return Err(PlanError::from(
297 GroupPlanError::UnknownAggregateTargetField {
298 index,
299 field: target_field.clone(),
300 },
301 ));
302 }
303 return Err(PlanError::from(
304 GroupPlanError::FieldTargetAggregatesUnsupported {
305 index,
306 kind: format!("{:?}", aggregate.kind),
307 field: target_field.clone(),
308 },
309 ));
310 }
311
312 Ok(())
313}
314
315fn validate_plan_core<K, FOrder, FAccess>(
317 schema: &SchemaInfo,
318 model: &EntityModel,
319 logical: &ScalarPlan,
320 plan: &AccessPlannedQuery<K>,
321 validate_order_fn: FOrder,
322 validate_access_fn: FAccess,
323) -> Result<(), PlanError>
324where
325 FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
326 FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
327{
328 if let Some(predicate) = &logical.predicate {
329 validate(schema, predicate)?;
330 }
331
332 if let Some(order) = &logical.order {
333 validate_order_fn(schema, order)?;
334 validate_no_duplicate_non_pk_order_fields(model, order)?;
335 validate_primary_key_tie_break(model, order)?;
336 }
337
338 validate_access_fn(schema, model, plan)?;
339 policy::validate_plan_shape(&plan.logical)?;
340
341 Ok(())
342}
343pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
353 for (field, _) in &order.fields {
354 let field_type = schema
355 .field(field)
356 .ok_or_else(|| OrderPlanError::UnknownField {
357 field: field.clone(),
358 })
359 .map_err(PlanError::from)?;
360
361 if !field_type.is_orderable() {
362 return Err(PlanError::from(OrderPlanError::UnorderableField {
364 field: field.clone(),
365 }));
366 }
367 }
368
369 Ok(())
370}
371
372pub(crate) fn validate_no_duplicate_non_pk_order_fields(
374 model: &EntityModel,
375 order: &OrderSpec,
376) -> Result<(), PlanError> {
377 let mut seen = BTreeSet::new();
378 let pk_field = model.primary_key.name;
379
380 for (field, _) in &order.fields {
381 if field == pk_field {
382 continue;
383 }
384 if !seen.insert(field.as_str()) {
385 return Err(PlanError::from(OrderPlanError::DuplicateOrderField {
386 field: field.clone(),
387 }));
388 }
389 }
390
391 Ok(())
392}
393
394pub(crate) fn validate_primary_key_tie_break(
397 model: &EntityModel,
398 order: &OrderSpec,
399) -> Result<(), PlanError> {
400 if order.fields.is_empty() {
401 return Ok(());
402 }
403
404 let pk_field = model.primary_key.name;
405 let pk_count = order
406 .fields
407 .iter()
408 .filter(|(field, _)| field == pk_field)
409 .count();
410 let trailing_pk = order
411 .fields
412 .last()
413 .is_some_and(|(field, _)| field == pk_field);
414
415 if pk_count == 1 && trailing_pk {
416 Ok(())
417 } else {
418 Err(PlanError::from(OrderPlanError::MissingPrimaryKeyTieBreak {
419 field: pk_field.to_string(),
420 }))
421 }
422}