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("unknown grouped aggregate target field at index={index}: '{field}'")]
159 UnknownAggregateTargetField { index: usize, field: String },
160
161 #[error(
163 "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
164 )]
165 FieldTargetAggregatesUnsupported {
166 index: usize,
167 kind: String,
168 field: String,
169 },
170}
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
178pub(crate) enum CursorOrderPlanShapeError {
179 MissingExplicitOrder,
180 EmptyOrderSpec,
181}
182
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
189pub(crate) enum IntentKeyAccessKind {
190 Single,
191 Many,
192 Only,
193}
194
195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub(crate) enum IntentKeyAccessPolicyViolation {
202 KeyAccessConflict,
203 ByIdsWithPredicate,
204 OnlyWithPredicate,
205}
206
207#[derive(Clone, Copy, Debug, Eq, PartialEq)]
213pub(crate) enum IntentTerminalPolicyViolation {
214 GroupedFieldTargetExtremaUnsupported,
215}
216
217#[derive(Clone, Copy, Debug, Eq, PartialEq)]
223pub(crate) enum FluentLoadPolicyViolation {
224 CursorRequiresPagedExecution,
225 GroupedRequiresExecuteGrouped,
226 CursorRequiresOrder,
227 CursorRequiresLimit,
228}
229
230impl From<ValidateError> for PlanError {
231 fn from(err: ValidateError) -> Self {
232 Self::PredicateInvalid(Box::new(err))
233 }
234}
235
236impl From<OrderPlanError> for PlanError {
237 fn from(err: OrderPlanError) -> Self {
238 Self::Order(Box::new(err))
239 }
240}
241
242impl From<AccessPlanError> for PlanError {
243 fn from(err: AccessPlanError) -> Self {
244 Self::Access(Box::new(err))
245 }
246}
247
248impl From<PolicyPlanError> for PlanError {
249 fn from(err: PolicyPlanError) -> Self {
250 Self::Policy(Box::new(err))
251 }
252}
253
254impl From<CursorPlanError> for PlanError {
255 fn from(err: CursorPlanError) -> Self {
256 Self::Cursor(Box::new(err))
257 }
258}
259
260impl From<GroupPlanError> for PlanError {
261 fn from(err: GroupPlanError) -> Self {
262 Self::Group(Box::new(err))
263 }
264}
265
266pub(crate) fn validate_query_semantics(
275 schema: &SchemaInfo,
276 model: &EntityModel,
277 plan: &AccessPlannedQuery<Value>,
278) -> Result<(), PlanError> {
279 let logical = plan.scalar_plan();
280
281 validate_plan_core(
282 schema,
283 model,
284 logical,
285 plan,
286 validate_order,
287 |schema, model, plan| {
288 validate_access_structure_model_shared(schema, model, &plan.access)
289 .map_err(PlanError::from)
290 },
291 )?;
292
293 Ok(())
294}
295
296pub(crate) fn validate_group_query_semantics(
302 schema: &SchemaInfo,
303 model: &EntityModel,
304 plan: &AccessPlannedQuery<Value>,
305) -> Result<(), PlanError> {
306 let logical = plan.scalar_plan();
307 let group = match &plan.logical {
308 LogicalPlan::Grouped(grouped) => &grouped.group,
309 LogicalPlan::Scalar(_) => {
310 return Err(PlanError::from(GroupPlanError::GroupedLogicalPlanRequired));
311 }
312 };
313
314 validate_plan_core(
315 schema,
316 model,
317 logical,
318 plan,
319 validate_order,
320 |schema, model, plan| {
321 validate_access_structure_model_shared(schema, model, &plan.access)
322 .map_err(PlanError::from)
323 },
324 )?;
325 validate_group_spec(schema, model, group)?;
326
327 Ok(())
328}
329
330pub(crate) fn validate_group_spec(
332 schema: &SchemaInfo,
333 model: &EntityModel,
334 group: &GroupSpec,
335) -> Result<(), PlanError> {
336 if group.group_fields.is_empty() {
337 return Err(PlanError::from(GroupPlanError::EmptyGroupFields));
338 }
339 if group.aggregates.is_empty() {
340 return Err(PlanError::from(GroupPlanError::EmptyAggregates));
341 }
342
343 let mut seen_group_slots = BTreeSet::<usize>::new();
344 for field_slot in &group.group_fields {
345 if model.fields.get(field_slot.index()).is_none() {
346 return Err(PlanError::from(GroupPlanError::UnknownGroupField {
347 field: field_slot.field().to_string(),
348 }));
349 }
350 if !seen_group_slots.insert(field_slot.index()) {
351 return Err(PlanError::from(GroupPlanError::DuplicateGroupField {
352 field: field_slot.field().to_string(),
353 }));
354 }
355 }
356
357 for (index, aggregate) in group.aggregates.iter().enumerate() {
358 let Some(target_field) = aggregate.target_field.as_ref() else {
359 continue;
360 };
361 if schema.field(target_field).is_none() {
362 return Err(PlanError::from(
363 GroupPlanError::UnknownAggregateTargetField {
364 index,
365 field: target_field.clone(),
366 },
367 ));
368 }
369 return Err(PlanError::from(
370 GroupPlanError::FieldTargetAggregatesUnsupported {
371 index,
372 kind: format!("{:?}", aggregate.kind),
373 field: target_field.clone(),
374 },
375 ));
376 }
377
378 Ok(())
379}
380
381fn validate_plan_core<K, FOrder, FAccess>(
383 schema: &SchemaInfo,
384 model: &EntityModel,
385 logical: &ScalarPlan,
386 plan: &AccessPlannedQuery<K>,
387 validate_order_fn: FOrder,
388 validate_access_fn: FAccess,
389) -> Result<(), PlanError>
390where
391 FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
392 FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
393{
394 if let Some(predicate) = &logical.predicate {
395 validate(schema, predicate)?;
396 }
397
398 if let Some(order) = &logical.order {
399 validate_order_fn(schema, order)?;
400 validate_no_duplicate_non_pk_order_fields(model, order)?;
401 validate_primary_key_tie_break(model, order)?;
402 }
403
404 validate_access_fn(schema, model, plan)?;
405 validate_plan_shape(&plan.logical)?;
406
407 Ok(())
408}
409#[must_use]
418pub(crate) fn has_explicit_order(order: Option<&OrderSpec>) -> bool {
419 order.is_some_and(|order| !order.fields.is_empty())
420}
421
422#[must_use]
424pub(crate) fn has_empty_order(order: Option<&OrderSpec>) -> bool {
425 order.is_some_and(|order| order.fields.is_empty())
426}
427
428pub(crate) fn validate_order_shape(order: Option<&OrderSpec>) -> Result<(), PolicyPlanError> {
430 if has_empty_order(order) {
431 return Err(PolicyPlanError::EmptyOrderSpec);
432 }
433
434 Ok(())
435}
436
437pub(crate) fn validate_intent_plan_shape(
439 mode: QueryMode,
440 order: Option<&OrderSpec>,
441) -> Result<(), PolicyPlanError> {
442 validate_order_shape(order)?;
443
444 let has_order = has_explicit_order(order);
445 if matches!(mode, QueryMode::Delete(spec) if spec.limit.is_some()) && !has_order {
446 return Err(PolicyPlanError::DeleteLimitRequiresOrder);
447 }
448
449 Ok(())
450}
451
452pub(crate) const fn validate_cursor_paging_requirements(
454 has_order: bool,
455 spec: LoadSpec,
456) -> Result<(), CursorPagingPolicyError> {
457 if !has_order {
458 return Err(CursorPagingPolicyError::CursorRequiresOrder);
459 }
460 if spec.limit.is_none() {
461 return Err(CursorPagingPolicyError::CursorRequiresLimit);
462 }
463
464 Ok(())
465}
466
467pub(crate) const fn validate_cursor_order_plan_shape(
469 order: Option<&OrderSpec>,
470 require_explicit_order: bool,
471) -> Result<Option<&OrderSpec>, CursorOrderPlanShapeError> {
472 let Some(order) = order else {
473 if require_explicit_order {
474 return Err(CursorOrderPlanShapeError::MissingExplicitOrder);
475 }
476
477 return Ok(None);
478 };
479
480 if order.fields.is_empty() {
481 return Err(CursorOrderPlanShapeError::EmptyOrderSpec);
482 }
483
484 Ok(Some(order))
485}
486
487pub(crate) fn resolve_group_field_slot(
489 model: &EntityModel,
490 field: &str,
491) -> Result<FieldSlot, PlanError> {
492 FieldSlot::resolve(model, field).ok_or_else(|| {
493 PlanError::from(GroupPlanError::UnknownGroupField {
494 field: field.to_string(),
495 })
496 })
497}
498
499pub(crate) const fn validate_intent_key_access_policy(
501 key_access_conflict: bool,
502 key_access_kind: Option<IntentKeyAccessKind>,
503 has_predicate: bool,
504) -> Result<(), IntentKeyAccessPolicyViolation> {
505 if key_access_conflict {
506 return Err(IntentKeyAccessPolicyViolation::KeyAccessConflict);
507 }
508
509 match key_access_kind {
510 Some(IntentKeyAccessKind::Many) if has_predicate => {
511 Err(IntentKeyAccessPolicyViolation::ByIdsWithPredicate)
512 }
513 Some(IntentKeyAccessKind::Only) if has_predicate => {
514 Err(IntentKeyAccessPolicyViolation::OnlyWithPredicate)
515 }
516 Some(
517 IntentKeyAccessKind::Single | IntentKeyAccessKind::Many | IntentKeyAccessKind::Only,
518 )
519 | None => Ok(()),
520 }
521}
522
523pub(crate) const fn validate_grouped_field_target_extrema_policy()
525-> Result<(), IntentTerminalPolicyViolation> {
526 Err(IntentTerminalPolicyViolation::GroupedFieldTargetExtremaUnsupported)
527}
528
529pub(crate) const fn validate_fluent_non_paged_mode(
531 has_cursor_token: bool,
532 has_grouping: bool,
533) -> Result<(), FluentLoadPolicyViolation> {
534 if has_cursor_token {
535 return Err(FluentLoadPolicyViolation::CursorRequiresPagedExecution);
536 }
537 if has_grouping {
538 return Err(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
539 }
540
541 Ok(())
542}
543
544pub(crate) fn validate_fluent_paged_mode(
546 has_grouping: bool,
547 has_explicit_order: bool,
548 spec: Option<LoadSpec>,
549) -> Result<(), FluentLoadPolicyViolation> {
550 if has_grouping {
551 return Err(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
552 }
553
554 let Some(spec) = spec else {
555 return Ok(());
556 };
557
558 validate_cursor_paging_requirements(has_explicit_order, spec).map_err(|err| match err {
559 CursorPagingPolicyError::CursorRequiresOrder => {
560 FluentLoadPolicyViolation::CursorRequiresOrder
561 }
562 CursorPagingPolicyError::CursorRequiresLimit => {
563 FluentLoadPolicyViolation::CursorRequiresLimit
564 }
565 })
566}
567
568pub(crate) fn validate_plan_shape(plan: &LogicalPlan) -> Result<(), PolicyPlanError> {
570 let grouped = matches!(plan, LogicalPlan::Grouped(_));
571 let plan = match plan {
572 LogicalPlan::Scalar(plan) => plan,
573 LogicalPlan::Grouped(plan) => &plan.scalar,
574 };
575 validate_order_shape(plan.order.as_ref())?;
576
577 let has_order = has_explicit_order(plan.order.as_ref());
578 if plan.delete_limit.is_some() && !has_order {
579 return Err(PolicyPlanError::DeleteLimitRequiresOrder);
580 }
581
582 match plan.mode {
583 QueryMode::Delete(_) => {
584 if plan.page.is_some() {
585 return Err(PolicyPlanError::DeletePlanWithPagination);
586 }
587 }
588 QueryMode::Load(_) => {
589 if plan.delete_limit.is_some() {
590 return Err(PolicyPlanError::LoadPlanWithDeleteLimit);
591 }
592 if plan.page.is_some() && !has_order && !grouped {
596 return Err(PolicyPlanError::UnorderedPagination);
597 }
598 }
599 }
600
601 Ok(())
602}
603
604pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
606 for (field, _) in &order.fields {
607 let field_type = schema
608 .field(field)
609 .ok_or_else(|| OrderPlanError::UnknownField {
610 field: field.clone(),
611 })
612 .map_err(PlanError::from)?;
613
614 if !field_type.is_orderable() {
615 return Err(PlanError::from(OrderPlanError::UnorderableField {
617 field: field.clone(),
618 }));
619 }
620 }
621
622 Ok(())
623}
624
625pub(crate) fn validate_no_duplicate_non_pk_order_fields(
627 model: &EntityModel,
628 order: &OrderSpec,
629) -> Result<(), PlanError> {
630 let mut seen = BTreeSet::new();
631 let pk_field = model.primary_key.name;
632
633 for (field, _) in &order.fields {
634 if field == pk_field {
635 continue;
636 }
637 if !seen.insert(field.as_str()) {
638 return Err(PlanError::from(OrderPlanError::DuplicateOrderField {
639 field: field.clone(),
640 }));
641 }
642 }
643
644 Ok(())
645}
646
647pub(crate) fn validate_primary_key_tie_break(
650 model: &EntityModel,
651 order: &OrderSpec,
652) -> Result<(), PlanError> {
653 if order.fields.is_empty() {
654 return Ok(());
655 }
656
657 let pk_field = model.primary_key.name;
658 let pk_count = order
659 .fields
660 .iter()
661 .filter(|(field, _)| field == pk_field)
662 .count();
663 let trailing_pk = order
664 .fields
665 .last()
666 .is_some_and(|(field, _)| field == pk_field);
667
668 if pk_count == 1 && trailing_pk {
669 Ok(())
670 } else {
671 Err(PlanError::from(OrderPlanError::MissingPrimaryKeyTieBreak {
672 field: pk_field.to_string(),
673 }))
674 }
675}