1use 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::plan::{
19 AccessPlannedQuery, FieldSlot, GroupSpec, LoadSpec, LogicalPlan, OrderSpec, QueryMode,
20 ScalarPlan,
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, Copy, 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, Copy, Debug, Eq, PartialEq, ThisError)]
121pub enum CursorPagingPolicyError {
122 #[error("cursor pagination requires an explicit ordering")]
123 CursorRequiresOrder,
124
125 #[error("cursor pagination requires a limit")]
126 CursorRequiresLimit,
127}
128
129#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
136pub enum GroupPlanError {
137 #[error("group query validation requires grouped logical plan variant")]
139 GroupedLogicalPlanRequired,
140
141 #[error("group specification must include at least one group field")]
143 EmptyGroupFields,
144
145 #[error("group specification must include at least one aggregate terminal")]
147 EmptyAggregates,
148
149 #[error("unknown group field '{field}'")]
151 UnknownGroupField { field: String },
152
153 #[error("group specification has duplicate group key: '{field}'")]
155 DuplicateGroupField { field: String },
156
157 #[error(
159 "grouped DISTINCT requires adjacency-based ordered-group eligibility proof in this release"
160 )]
161 DistinctAdjacencyEligibilityRequired,
162
163 #[error("grouped ORDER BY must start with GROUP BY key prefix in this release")]
165 OrderPrefixNotAlignedWithGroupKeys,
166
167 #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
169 UnknownAggregateTargetField { index: usize, field: String },
170
171 #[error(
173 "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
174 )]
175 FieldTargetAggregatesUnsupported {
176 index: usize,
177 kind: String,
178 field: String,
179 },
180}
181
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
188pub(crate) enum CursorOrderPlanShapeError {
189 MissingExplicitOrder,
190 EmptyOrderSpec,
191}
192
193#[derive(Clone, Copy, Debug, Eq, PartialEq)]
199pub(crate) enum IntentKeyAccessKind {
200 Single,
201 Many,
202 Only,
203}
204
205#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub(crate) enum IntentKeyAccessPolicyViolation {
212 KeyAccessConflict,
213 ByIdsWithPredicate,
214 OnlyWithPredicate,
215}
216
217#[derive(Clone, Copy, Debug, Eq, PartialEq)]
223pub(crate) enum IntentTerminalPolicyViolation {
224 GroupedFieldTargetExtremaUnsupported,
225}
226
227#[derive(Clone, Copy, Debug, Eq, PartialEq)]
233pub(crate) enum FluentLoadPolicyViolation {
234 CursorRequiresPagedExecution,
235 GroupedRequiresExecuteGrouped,
236 CursorRequiresOrder,
237 CursorRequiresLimit,
238}
239
240impl From<ValidateError> for PlanError {
241 fn from(err: ValidateError) -> Self {
242 Self::PredicateInvalid(Box::new(err))
243 }
244}
245
246impl From<OrderPlanError> for PlanError {
247 fn from(err: OrderPlanError) -> Self {
248 Self::Order(Box::new(err))
249 }
250}
251
252impl From<AccessPlanError> for PlanError {
253 fn from(err: AccessPlanError) -> Self {
254 Self::Access(Box::new(err))
255 }
256}
257
258impl From<PolicyPlanError> for PlanError {
259 fn from(err: PolicyPlanError) -> Self {
260 Self::Policy(Box::new(err))
261 }
262}
263
264impl From<CursorPlanError> for PlanError {
265 fn from(err: CursorPlanError) -> Self {
266 Self::Cursor(Box::new(err))
267 }
268}
269
270impl From<GroupPlanError> for PlanError {
271 fn from(err: GroupPlanError) -> Self {
272 Self::Group(Box::new(err))
273 }
274}
275
276pub(crate) fn validate_query_semantics(
285 schema: &SchemaInfo,
286 model: &EntityModel,
287 plan: &AccessPlannedQuery<Value>,
288) -> Result<(), PlanError> {
289 let logical = plan.scalar_plan();
290
291 validate_plan_core(
292 schema,
293 model,
294 logical,
295 plan,
296 validate_order,
297 |schema, model, plan| {
298 validate_access_structure_model_shared(schema, model, &plan.access)
299 .map_err(PlanError::from)
300 },
301 )?;
302
303 Ok(())
304}
305
306pub(crate) fn validate_group_query_semantics(
312 schema: &SchemaInfo,
313 model: &EntityModel,
314 plan: &AccessPlannedQuery<Value>,
315) -> Result<(), PlanError> {
316 let logical = plan.scalar_plan();
317 let group = match &plan.logical {
318 LogicalPlan::Grouped(grouped) => &grouped.group,
319 LogicalPlan::Scalar(_) => {
320 return Err(PlanError::from(GroupPlanError::GroupedLogicalPlanRequired));
321 }
322 };
323
324 validate_plan_core(
325 schema,
326 model,
327 logical,
328 plan,
329 validate_order,
330 |schema, model, plan| {
331 validate_access_structure_model_shared(schema, model, &plan.access)
332 .map_err(PlanError::from)
333 },
334 )?;
335 validate_grouped_distinct_and_order_policy(logical, group)?;
336 validate_group_spec(schema, model, group)?;
337
338 Ok(())
339}
340
341fn validate_grouped_distinct_and_order_policy(
343 logical: &ScalarPlan,
344 group: &GroupSpec,
345) -> Result<(), PlanError> {
346 if logical.distinct {
347 return Err(PlanError::from(
348 GroupPlanError::DistinctAdjacencyEligibilityRequired,
349 ));
350 }
351
352 let Some(order) = logical.order.as_ref() else {
353 return Ok(());
354 };
355 if order_prefix_aligned_with_group_fields(order, group.group_fields.as_slice()) {
356 return Ok(());
357 }
358
359 Err(PlanError::from(
360 GroupPlanError::OrderPrefixNotAlignedWithGroupKeys,
361 ))
362}
363
364fn order_prefix_aligned_with_group_fields(order: &OrderSpec, group_fields: &[FieldSlot]) -> bool {
366 if order.fields.len() < group_fields.len() {
367 return false;
368 }
369
370 group_fields
371 .iter()
372 .zip(order.fields.iter())
373 .all(|(group_field, (order_field, _))| order_field == group_field.field())
374}
375
376pub(crate) fn validate_group_spec(
378 schema: &SchemaInfo,
379 model: &EntityModel,
380 group: &GroupSpec,
381) -> Result<(), PlanError> {
382 if group.group_fields.is_empty() {
383 return Err(PlanError::from(GroupPlanError::EmptyGroupFields));
384 }
385 if group.aggregates.is_empty() {
386 return Err(PlanError::from(GroupPlanError::EmptyAggregates));
387 }
388
389 let mut seen_group_slots = BTreeSet::<usize>::new();
390 for field_slot in &group.group_fields {
391 if model.fields.get(field_slot.index()).is_none() {
392 return Err(PlanError::from(GroupPlanError::UnknownGroupField {
393 field: field_slot.field().to_string(),
394 }));
395 }
396 if !seen_group_slots.insert(field_slot.index()) {
397 return Err(PlanError::from(GroupPlanError::DuplicateGroupField {
398 field: field_slot.field().to_string(),
399 }));
400 }
401 }
402
403 for (index, aggregate) in group.aggregates.iter().enumerate() {
404 let Some(target_field) = aggregate.target_field.as_ref() else {
405 continue;
406 };
407 if schema.field(target_field).is_none() {
408 return Err(PlanError::from(
409 GroupPlanError::UnknownAggregateTargetField {
410 index,
411 field: target_field.clone(),
412 },
413 ));
414 }
415 return Err(PlanError::from(
416 GroupPlanError::FieldTargetAggregatesUnsupported {
417 index,
418 kind: format!("{:?}", aggregate.kind),
419 field: target_field.clone(),
420 },
421 ));
422 }
423
424 Ok(())
425}
426
427fn validate_plan_core<K, FOrder, FAccess>(
429 schema: &SchemaInfo,
430 model: &EntityModel,
431 logical: &ScalarPlan,
432 plan: &AccessPlannedQuery<K>,
433 validate_order_fn: FOrder,
434 validate_access_fn: FAccess,
435) -> Result<(), PlanError>
436where
437 FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
438 FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
439{
440 if let Some(predicate) = &logical.predicate {
441 validate(schema, predicate)?;
442 }
443
444 if let Some(order) = &logical.order {
445 validate_order_fn(schema, order)?;
446 validate_no_duplicate_non_pk_order_fields(model, order)?;
447 validate_primary_key_tie_break(model, order)?;
448 }
449
450 validate_access_fn(schema, model, plan)?;
451 validate_plan_shape(&plan.logical)?;
452
453 Ok(())
454}
455#[must_use]
464pub(crate) fn has_explicit_order(order: Option<&OrderSpec>) -> bool {
465 order.is_some_and(|order| !order.fields.is_empty())
466}
467
468#[must_use]
470pub(crate) fn has_empty_order(order: Option<&OrderSpec>) -> bool {
471 order.is_some_and(|order| order.fields.is_empty())
472}
473
474pub(crate) fn validate_order_shape(order: Option<&OrderSpec>) -> Result<(), PolicyPlanError> {
476 if has_empty_order(order) {
477 return Err(PolicyPlanError::EmptyOrderSpec);
478 }
479
480 Ok(())
481}
482
483pub(crate) fn validate_intent_plan_shape(
485 mode: QueryMode,
486 order: Option<&OrderSpec>,
487) -> Result<(), PolicyPlanError> {
488 validate_order_shape(order)?;
489
490 let has_order = has_explicit_order(order);
491 if matches!(mode, QueryMode::Delete(spec) if spec.limit.is_some()) && !has_order {
492 return Err(PolicyPlanError::DeleteLimitRequiresOrder);
493 }
494
495 Ok(())
496}
497
498pub(crate) const fn validate_cursor_paging_requirements(
500 has_order: bool,
501 spec: LoadSpec,
502) -> Result<(), CursorPagingPolicyError> {
503 if !has_order {
504 return Err(CursorPagingPolicyError::CursorRequiresOrder);
505 }
506 if spec.limit.is_none() {
507 return Err(CursorPagingPolicyError::CursorRequiresLimit);
508 }
509
510 Ok(())
511}
512
513pub(crate) const fn validate_cursor_order_plan_shape(
515 order: Option<&OrderSpec>,
516 require_explicit_order: bool,
517) -> Result<Option<&OrderSpec>, CursorOrderPlanShapeError> {
518 let Some(order) = order else {
519 if require_explicit_order {
520 return Err(CursorOrderPlanShapeError::MissingExplicitOrder);
521 }
522
523 return Ok(None);
524 };
525
526 if order.fields.is_empty() {
527 return Err(CursorOrderPlanShapeError::EmptyOrderSpec);
528 }
529
530 Ok(Some(order))
531}
532
533pub(crate) fn resolve_group_field_slot(
535 model: &EntityModel,
536 field: &str,
537) -> Result<FieldSlot, PlanError> {
538 FieldSlot::resolve(model, field).ok_or_else(|| {
539 PlanError::from(GroupPlanError::UnknownGroupField {
540 field: field.to_string(),
541 })
542 })
543}
544
545pub(crate) const fn validate_intent_key_access_policy(
547 key_access_conflict: bool,
548 key_access_kind: Option<IntentKeyAccessKind>,
549 has_predicate: bool,
550) -> Result<(), IntentKeyAccessPolicyViolation> {
551 if key_access_conflict {
552 return Err(IntentKeyAccessPolicyViolation::KeyAccessConflict);
553 }
554
555 match key_access_kind {
556 Some(IntentKeyAccessKind::Many) if has_predicate => {
557 Err(IntentKeyAccessPolicyViolation::ByIdsWithPredicate)
558 }
559 Some(IntentKeyAccessKind::Only) if has_predicate => {
560 Err(IntentKeyAccessPolicyViolation::OnlyWithPredicate)
561 }
562 Some(
563 IntentKeyAccessKind::Single | IntentKeyAccessKind::Many | IntentKeyAccessKind::Only,
564 )
565 | None => Ok(()),
566 }
567}
568
569pub(crate) const fn validate_grouped_field_target_extrema_policy()
571-> Result<(), IntentTerminalPolicyViolation> {
572 Err(IntentTerminalPolicyViolation::GroupedFieldTargetExtremaUnsupported)
573}
574
575pub(crate) const fn validate_fluent_non_paged_mode(
577 has_cursor_token: bool,
578 has_grouping: bool,
579) -> Result<(), FluentLoadPolicyViolation> {
580 if has_cursor_token {
581 return Err(FluentLoadPolicyViolation::CursorRequiresPagedExecution);
582 }
583 if has_grouping {
584 return Err(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
585 }
586
587 Ok(())
588}
589
590pub(crate) fn validate_fluent_paged_mode(
592 has_grouping: bool,
593 has_explicit_order: bool,
594 spec: Option<LoadSpec>,
595) -> Result<(), FluentLoadPolicyViolation> {
596 if has_grouping {
597 return Err(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
598 }
599
600 let Some(spec) = spec else {
601 return Ok(());
602 };
603
604 validate_cursor_paging_requirements(has_explicit_order, spec).map_err(|err| match err {
605 CursorPagingPolicyError::CursorRequiresOrder => {
606 FluentLoadPolicyViolation::CursorRequiresOrder
607 }
608 CursorPagingPolicyError::CursorRequiresLimit => {
609 FluentLoadPolicyViolation::CursorRequiresLimit
610 }
611 })
612}
613
614pub(crate) fn validate_plan_shape(plan: &LogicalPlan) -> Result<(), PolicyPlanError> {
616 let grouped = matches!(plan, LogicalPlan::Grouped(_));
617 let plan = match plan {
618 LogicalPlan::Scalar(plan) => plan,
619 LogicalPlan::Grouped(plan) => &plan.scalar,
620 };
621 validate_order_shape(plan.order.as_ref())?;
622
623 let has_order = has_explicit_order(plan.order.as_ref());
624 if plan.delete_limit.is_some() && !has_order {
625 return Err(PolicyPlanError::DeleteLimitRequiresOrder);
626 }
627
628 match plan.mode {
629 QueryMode::Delete(_) => {
630 if plan.page.is_some() {
631 return Err(PolicyPlanError::DeletePlanWithPagination);
632 }
633 }
634 QueryMode::Load(_) => {
635 if plan.delete_limit.is_some() {
636 return Err(PolicyPlanError::LoadPlanWithDeleteLimit);
637 }
638 if plan.page.is_some() && !has_order && !grouped {
642 return Err(PolicyPlanError::UnorderedPagination);
643 }
644 }
645 }
646
647 Ok(())
648}
649
650pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
652 for (field, _) in &order.fields {
653 let field_type = schema
654 .field(field)
655 .ok_or_else(|| OrderPlanError::UnknownField {
656 field: field.clone(),
657 })
658 .map_err(PlanError::from)?;
659
660 if !field_type.is_orderable() {
661 return Err(PlanError::from(OrderPlanError::UnorderableField {
663 field: field.clone(),
664 }));
665 }
666 }
667
668 Ok(())
669}
670
671pub(crate) fn validate_no_duplicate_non_pk_order_fields(
673 model: &EntityModel,
674 order: &OrderSpec,
675) -> Result<(), PlanError> {
676 let mut seen = BTreeSet::new();
677 let pk_field = model.primary_key.name;
678
679 for (field, _) in &order.fields {
680 if field == pk_field {
681 continue;
682 }
683 if !seen.insert(field.as_str()) {
684 return Err(PlanError::from(OrderPlanError::DuplicateOrderField {
685 field: field.clone(),
686 }));
687 }
688 }
689
690 Ok(())
691}
692
693pub(crate) fn validate_primary_key_tie_break(
696 model: &EntityModel,
697 order: &OrderSpec,
698) -> Result<(), PlanError> {
699 if order.fields.is_empty() {
700 return Ok(());
701 }
702
703 let pk_field = model.primary_key.name;
704 let pk_count = order
705 .fields
706 .iter()
707 .filter(|(field, _)| field == pk_field)
708 .count();
709 let trailing_pk = order
710 .fields
711 .last()
712 .is_some_and(|(field, _)| field == pk_field);
713
714 if pk_count == 1 && trailing_pk {
715 Ok(())
716 } else {
717 Err(PlanError::from(OrderPlanError::MissingPrimaryKeyTieBreak {
718 field: pk_field.to_string(),
719 }))
720 }
721}