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