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::{CompareOp, SchemaInfo, ValidateError, validate},
18 query::plan::{
19 AccessPlannedQuery, FieldSlot, GroupAggregateSpec, GroupHavingSpec, GroupHavingSymbol,
20 GroupSpec, LoadSpec, LogicalPlan, OrderSpec, QueryMode, 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("HAVING is only supported for GROUP BY queries in this release")]
139 HavingRequiresGroupBy,
140
141 #[error("group query validation requires grouped logical plan variant")]
143 GroupedLogicalPlanRequired,
144
145 #[error("group specification must include at least one group field")]
147 EmptyGroupFields,
148
149 #[error(
151 "global DISTINCT aggregate without GROUP BY must declare exactly one DISTINCT field-target aggregate in this release"
152 )]
153 GlobalDistinctAggregateShapeUnsupported,
154
155 #[error("group specification must include at least one aggregate terminal")]
157 EmptyAggregates,
158
159 #[error("unknown group field '{field}'")]
161 UnknownGroupField { field: String },
162
163 #[error("group specification has duplicate group key: '{field}'")]
165 DuplicateGroupField { field: String },
166
167 #[error(
169 "grouped DISTINCT requires adjacency-based ordered-group eligibility proof in this release"
170 )]
171 DistinctAdjacencyEligibilityRequired,
172
173 #[error("grouped ORDER BY must start with GROUP BY key prefix in this release")]
175 OrderPrefixNotAlignedWithGroupKeys,
176
177 #[error("grouped ORDER BY requires LIMIT in this release")]
179 OrderRequiresLimit,
180
181 #[error("grouped HAVING with DISTINCT is not supported in this release")]
183 DistinctHavingUnsupported,
184
185 #[error("grouped HAVING clause at index={index} uses unsupported operator: {op}")]
187 HavingUnsupportedCompareOp { index: usize, op: String },
188
189 #[error("grouped HAVING clause at index={index} references non-group field '{field}'")]
191 HavingNonGroupFieldReference { index: usize, field: String },
192
193 #[error(
195 "grouped HAVING clause at index={index} references aggregate index {aggregate_index} but aggregate_count={aggregate_count}"
196 )]
197 HavingAggregateIndexOutOfBounds {
198 index: usize,
199 aggregate_index: usize,
200 aggregate_count: usize,
201 },
202
203 #[error(
205 "grouped DISTINCT aggregate at index={index} uses unsupported kind '{kind}' in this release"
206 )]
207 DistinctAggregateKindUnsupported { index: usize, kind: String },
208
209 #[error(
211 "grouped DISTINCT aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
212 )]
213 DistinctAggregateFieldTargetUnsupported {
214 index: usize,
215 kind: String,
216 field: String,
217 },
218
219 #[error("unknown grouped aggregate target field at index={index}: '{field}'")]
221 UnknownAggregateTargetField { index: usize, field: String },
222
223 #[error(
225 "global DISTINCT SUM aggregate target field at index={index} is not numeric: '{field}'"
226 )]
227 GlobalDistinctSumTargetNotNumeric { index: usize, field: String },
228
229 #[error(
231 "grouped aggregate at index={index} cannot target field '{field}' in this release: found {kind}"
232 )]
233 FieldTargetAggregatesUnsupported {
234 index: usize,
235 kind: String,
236 field: String,
237 },
238}
239
240#[derive(Clone, Copy, Debug, Eq, PartialEq)]
246pub(crate) enum CursorOrderPlanShapeError {
247 MissingExplicitOrder,
248 EmptyOrderSpec,
249}
250
251#[derive(Clone, Copy, Debug, Eq, PartialEq)]
257pub(crate) enum IntentKeyAccessKind {
258 Single,
259 Many,
260 Only,
261}
262
263#[derive(Clone, Copy, Debug, Eq, PartialEq)]
269pub(crate) enum IntentKeyAccessPolicyViolation {
270 KeyAccessConflict,
271 ByIdsWithPredicate,
272 OnlyWithPredicate,
273}
274
275#[derive(Clone, Copy, Debug, Eq, PartialEq)]
281pub(crate) enum FluentLoadPolicyViolation {
282 CursorRequiresPagedExecution,
283 GroupedRequiresExecuteGrouped,
284 CursorRequiresOrder,
285 CursorRequiresLimit,
286}
287
288impl From<ValidateError> for PlanError {
289 fn from(err: ValidateError) -> Self {
290 Self::PredicateInvalid(Box::new(err))
291 }
292}
293
294impl From<OrderPlanError> for PlanError {
295 fn from(err: OrderPlanError) -> Self {
296 Self::Order(Box::new(err))
297 }
298}
299
300impl From<AccessPlanError> for PlanError {
301 fn from(err: AccessPlanError) -> Self {
302 Self::Access(Box::new(err))
303 }
304}
305
306impl From<PolicyPlanError> for PlanError {
307 fn from(err: PolicyPlanError) -> Self {
308 Self::Policy(Box::new(err))
309 }
310}
311
312impl From<CursorPlanError> for PlanError {
313 fn from(err: CursorPlanError) -> Self {
314 Self::Cursor(Box::new(err))
315 }
316}
317
318impl From<GroupPlanError> for PlanError {
319 fn from(err: GroupPlanError) -> Self {
320 Self::Group(Box::new(err))
321 }
322}
323
324pub(crate) fn validate_query_semantics(
333 schema: &SchemaInfo,
334 model: &EntityModel,
335 plan: &AccessPlannedQuery<Value>,
336) -> Result<(), PlanError> {
337 let logical = plan.scalar_plan();
338
339 validate_plan_core(
340 schema,
341 model,
342 logical,
343 plan,
344 validate_order,
345 |schema, model, plan| {
346 validate_access_structure_model_shared(schema, model, &plan.access)
347 .map_err(PlanError::from)
348 },
349 )?;
350
351 Ok(())
352}
353
354pub(crate) fn validate_group_query_semantics(
360 schema: &SchemaInfo,
361 model: &EntityModel,
362 plan: &AccessPlannedQuery<Value>,
363) -> Result<(), PlanError> {
364 let (logical, group, having) = match &plan.logical {
365 LogicalPlan::Grouped(grouped) => (&grouped.scalar, &grouped.group, grouped.having.as_ref()),
366 LogicalPlan::Scalar(_) => {
367 return Err(PlanError::from(GroupPlanError::GroupedLogicalPlanRequired));
368 }
369 };
370
371 validate_plan_core(
372 schema,
373 model,
374 logical,
375 plan,
376 validate_order,
377 |schema, model, plan| {
378 validate_access_structure_model_shared(schema, model, &plan.access)
379 .map_err(PlanError::from)
380 },
381 )?;
382 if group.group_fields.is_empty() && having.is_some() {
383 return Err(PlanError::from(
384 GroupPlanError::GlobalDistinctAggregateShapeUnsupported,
385 ));
386 }
387 validate_grouped_distinct_and_order_policy(logical, group, having.is_some())?;
388 validate_group_spec(schema, model, group)?;
389 validate_grouped_having_policy(group, having)?;
390
391 Ok(())
392}
393
394fn validate_grouped_distinct_and_order_policy(
396 logical: &ScalarPlan,
397 group: &GroupSpec,
398 has_having: bool,
399) -> Result<(), PlanError> {
400 if logical.distinct && has_having {
401 return Err(PlanError::from(GroupPlanError::DistinctHavingUnsupported));
402 }
403 if logical.distinct {
404 return Err(PlanError::from(
405 GroupPlanError::DistinctAdjacencyEligibilityRequired,
406 ));
407 }
408
409 let Some(order) = logical.order.as_ref() else {
410 return Ok(());
411 };
412 if logical.page.as_ref().and_then(|page| page.limit).is_none() {
413 return Err(PlanError::from(GroupPlanError::OrderRequiresLimit));
414 }
415 if order_prefix_aligned_with_group_fields(order, group.group_fields.as_slice()) {
416 return Ok(());
417 }
418
419 Err(PlanError::from(
420 GroupPlanError::OrderPrefixNotAlignedWithGroupKeys,
421 ))
422}
423
424fn validate_grouped_having_policy(
426 group: &GroupSpec,
427 having: Option<&GroupHavingSpec>,
428) -> Result<(), PlanError> {
429 let Some(having) = having else {
430 return Ok(());
431 };
432
433 for (index, clause) in having.clauses().iter().enumerate() {
434 if !having_compare_op_supported(clause.op()) {
435 return Err(PlanError::from(
436 GroupPlanError::HavingUnsupportedCompareOp {
437 index,
438 op: format!("{:?}", clause.op()),
439 },
440 ));
441 }
442
443 match clause.symbol() {
444 GroupHavingSymbol::GroupField(field_slot) => {
445 if !group
446 .group_fields
447 .iter()
448 .any(|group_field| group_field.index() == field_slot.index())
449 {
450 return Err(PlanError::from(
451 GroupPlanError::HavingNonGroupFieldReference {
452 index,
453 field: field_slot.field().to_string(),
454 },
455 ));
456 }
457 }
458 GroupHavingSymbol::AggregateIndex(aggregate_index) => {
459 if *aggregate_index >= group.aggregates.len() {
460 return Err(PlanError::from(
461 GroupPlanError::HavingAggregateIndexOutOfBounds {
462 index,
463 aggregate_index: *aggregate_index,
464 aggregate_count: group.aggregates.len(),
465 },
466 ));
467 }
468 }
469 }
470 }
471
472 Ok(())
473}
474
475const fn having_compare_op_supported(op: CompareOp) -> bool {
476 matches!(
477 op,
478 CompareOp::Eq
479 | CompareOp::Ne
480 | CompareOp::Lt
481 | CompareOp::Lte
482 | CompareOp::Gt
483 | CompareOp::Gte
484 )
485}
486
487fn order_prefix_aligned_with_group_fields(order: &OrderSpec, group_fields: &[FieldSlot]) -> bool {
489 if order.fields.len() < group_fields.len() {
490 return false;
491 }
492
493 group_fields
494 .iter()
495 .zip(order.fields.iter())
496 .all(|(group_field, (order_field, _))| order_field == group_field.field())
497}
498
499pub(crate) fn validate_group_spec(
501 schema: &SchemaInfo,
502 model: &EntityModel,
503 group: &GroupSpec,
504) -> Result<(), PlanError> {
505 if group.group_fields.is_empty() {
506 if group.aggregates.iter().any(GroupAggregateSpec::distinct) {
507 validate_global_distinct_aggregate_without_group_keys(schema, group)?;
508 return Ok(());
509 }
510
511 return Err(PlanError::from(GroupPlanError::EmptyGroupFields));
512 }
513 if group.aggregates.is_empty() {
514 return Err(PlanError::from(GroupPlanError::EmptyAggregates));
515 }
516
517 let mut seen_group_slots = BTreeSet::<usize>::new();
518 for field_slot in &group.group_fields {
519 if model.fields.get(field_slot.index()).is_none() {
520 return Err(PlanError::from(GroupPlanError::UnknownGroupField {
521 field: field_slot.field().to_string(),
522 }));
523 }
524 if !seen_group_slots.insert(field_slot.index()) {
525 return Err(PlanError::from(GroupPlanError::DuplicateGroupField {
526 field: field_slot.field().to_string(),
527 }));
528 }
529 }
530
531 for (index, aggregate) in group.aggregates.iter().enumerate() {
532 if aggregate.distinct() && !aggregate.kind().supports_grouped_distinct_v1() {
533 return Err(PlanError::from(
534 GroupPlanError::DistinctAggregateKindUnsupported {
535 index,
536 kind: format!("{:?}", aggregate.kind()),
537 },
538 ));
539 }
540
541 let Some(target_field) = aggregate.target_field.as_ref() else {
542 continue;
543 };
544 if aggregate.distinct() {
545 return Err(PlanError::from(
546 GroupPlanError::DistinctAggregateFieldTargetUnsupported {
547 index,
548 kind: format!("{:?}", aggregate.kind()),
549 field: target_field.clone(),
550 },
551 ));
552 }
553 if schema.field(target_field).is_none() {
554 return Err(PlanError::from(
555 GroupPlanError::UnknownAggregateTargetField {
556 index,
557 field: target_field.clone(),
558 },
559 ));
560 }
561 return Err(PlanError::from(
562 GroupPlanError::FieldTargetAggregatesUnsupported {
563 index,
564 kind: format!("{:?}", aggregate.kind()),
565 field: target_field.clone(),
566 },
567 ));
568 }
569
570 Ok(())
571}
572
573fn validate_global_distinct_aggregate_without_group_keys(
575 schema: &SchemaInfo,
576 group: &GroupSpec,
577) -> Result<(), PlanError> {
578 if group.aggregates.len() != 1 {
579 return Err(PlanError::from(
580 GroupPlanError::GlobalDistinctAggregateShapeUnsupported,
581 ));
582 }
583 let aggregate = &group.aggregates[0];
584 if !aggregate.distinct() {
585 return Err(PlanError::from(
586 GroupPlanError::GlobalDistinctAggregateShapeUnsupported,
587 ));
588 }
589 if !aggregate
590 .kind()
591 .supports_global_distinct_without_group_keys()
592 {
593 return Err(PlanError::from(
594 GroupPlanError::DistinctAggregateKindUnsupported {
595 index: 0,
596 kind: format!("{:?}", aggregate.kind()),
597 },
598 ));
599 }
600
601 let Some(target_field) = aggregate.target_field() else {
602 return Err(PlanError::from(
603 GroupPlanError::GlobalDistinctAggregateShapeUnsupported,
604 ));
605 };
606 let Some(field_type) = schema.field(target_field) else {
607 return Err(PlanError::from(
608 GroupPlanError::UnknownAggregateTargetField {
609 index: 0,
610 field: target_field.to_string(),
611 },
612 ));
613 };
614 if aggregate.kind().is_sum() && !field_type.supports_numeric_coercion() {
615 return Err(PlanError::from(
616 GroupPlanError::GlobalDistinctSumTargetNotNumeric {
617 index: 0,
618 field: target_field.to_string(),
619 },
620 ));
621 }
622
623 Ok(())
624}
625
626fn validate_plan_core<K, FOrder, FAccess>(
628 schema: &SchemaInfo,
629 model: &EntityModel,
630 logical: &ScalarPlan,
631 plan: &AccessPlannedQuery<K>,
632 validate_order_fn: FOrder,
633 validate_access_fn: FAccess,
634) -> Result<(), PlanError>
635where
636 FOrder: Fn(&SchemaInfo, &OrderSpec) -> Result<(), PlanError>,
637 FAccess: Fn(&SchemaInfo, &EntityModel, &AccessPlannedQuery<K>) -> Result<(), PlanError>,
638{
639 if let Some(predicate) = &logical.predicate {
640 validate(schema, predicate)?;
641 }
642
643 if let Some(order) = &logical.order {
644 validate_order_fn(schema, order)?;
645 validate_no_duplicate_non_pk_order_fields(model, order)?;
646 validate_primary_key_tie_break(model, order)?;
647 }
648
649 validate_access_fn(schema, model, plan)?;
650 validate_plan_shape(&plan.logical)?;
651
652 Ok(())
653}
654#[must_use]
663pub(crate) fn has_explicit_order(order: Option<&OrderSpec>) -> bool {
664 order.is_some_and(|order| !order.fields.is_empty())
665}
666
667#[must_use]
669pub(crate) fn has_empty_order(order: Option<&OrderSpec>) -> bool {
670 order.is_some_and(|order| order.fields.is_empty())
671}
672
673pub(crate) fn validate_order_shape(order: Option<&OrderSpec>) -> Result<(), PolicyPlanError> {
675 if has_empty_order(order) {
676 return Err(PolicyPlanError::EmptyOrderSpec);
677 }
678
679 Ok(())
680}
681
682pub(crate) fn validate_intent_plan_shape(
684 mode: QueryMode,
685 order: Option<&OrderSpec>,
686) -> Result<(), PolicyPlanError> {
687 validate_order_shape(order)?;
688
689 let has_order = has_explicit_order(order);
690 if matches!(mode, QueryMode::Delete(spec) if spec.limit.is_some()) && !has_order {
691 return Err(PolicyPlanError::DeleteLimitRequiresOrder);
692 }
693
694 Ok(())
695}
696
697pub(crate) const fn validate_cursor_paging_requirements(
699 has_order: bool,
700 spec: LoadSpec,
701) -> Result<(), CursorPagingPolicyError> {
702 if !has_order {
703 return Err(CursorPagingPolicyError::CursorRequiresOrder);
704 }
705 if spec.limit.is_none() {
706 return Err(CursorPagingPolicyError::CursorRequiresLimit);
707 }
708
709 Ok(())
710}
711
712pub(crate) const fn validate_cursor_order_plan_shape(
714 order: Option<&OrderSpec>,
715 require_explicit_order: bool,
716) -> Result<Option<&OrderSpec>, CursorOrderPlanShapeError> {
717 let Some(order) = order else {
718 if require_explicit_order {
719 return Err(CursorOrderPlanShapeError::MissingExplicitOrder);
720 }
721
722 return Ok(None);
723 };
724
725 if order.fields.is_empty() {
726 return Err(CursorOrderPlanShapeError::EmptyOrderSpec);
727 }
728
729 Ok(Some(order))
730}
731
732pub(crate) fn resolve_group_field_slot(
734 model: &EntityModel,
735 field: &str,
736) -> Result<FieldSlot, PlanError> {
737 FieldSlot::resolve(model, field).ok_or_else(|| {
738 PlanError::from(GroupPlanError::UnknownGroupField {
739 field: field.to_string(),
740 })
741 })
742}
743
744pub(crate) const fn validate_intent_key_access_policy(
746 key_access_conflict: bool,
747 key_access_kind: Option<IntentKeyAccessKind>,
748 has_predicate: bool,
749) -> Result<(), IntentKeyAccessPolicyViolation> {
750 if key_access_conflict {
751 return Err(IntentKeyAccessPolicyViolation::KeyAccessConflict);
752 }
753
754 match key_access_kind {
755 Some(IntentKeyAccessKind::Many) if has_predicate => {
756 Err(IntentKeyAccessPolicyViolation::ByIdsWithPredicate)
757 }
758 Some(IntentKeyAccessKind::Only) if has_predicate => {
759 Err(IntentKeyAccessPolicyViolation::OnlyWithPredicate)
760 }
761 Some(
762 IntentKeyAccessKind::Single | IntentKeyAccessKind::Many | IntentKeyAccessKind::Only,
763 )
764 | None => Ok(()),
765 }
766}
767
768pub(crate) const fn validate_fluent_non_paged_mode(
770 has_cursor_token: bool,
771 has_grouping: bool,
772) -> Result<(), FluentLoadPolicyViolation> {
773 if has_cursor_token {
774 return Err(FluentLoadPolicyViolation::CursorRequiresPagedExecution);
775 }
776 if has_grouping {
777 return Err(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
778 }
779
780 Ok(())
781}
782
783pub(crate) fn validate_fluent_paged_mode(
785 has_grouping: bool,
786 has_explicit_order: bool,
787 spec: Option<LoadSpec>,
788) -> Result<(), FluentLoadPolicyViolation> {
789 if has_grouping {
790 return Err(FluentLoadPolicyViolation::GroupedRequiresExecuteGrouped);
791 }
792
793 let Some(spec) = spec else {
794 return Ok(());
795 };
796
797 validate_cursor_paging_requirements(has_explicit_order, spec).map_err(|err| match err {
798 CursorPagingPolicyError::CursorRequiresOrder => {
799 FluentLoadPolicyViolation::CursorRequiresOrder
800 }
801 CursorPagingPolicyError::CursorRequiresLimit => {
802 FluentLoadPolicyViolation::CursorRequiresLimit
803 }
804 })
805}
806
807pub(crate) fn validate_plan_shape(plan: &LogicalPlan) -> Result<(), PolicyPlanError> {
809 let grouped = matches!(plan, LogicalPlan::Grouped(_));
810 let plan = match plan {
811 LogicalPlan::Scalar(plan) => plan,
812 LogicalPlan::Grouped(plan) => &plan.scalar,
813 };
814 validate_order_shape(plan.order.as_ref())?;
815
816 let has_order = has_explicit_order(plan.order.as_ref());
817 if plan.delete_limit.is_some() && !has_order {
818 return Err(PolicyPlanError::DeleteLimitRequiresOrder);
819 }
820
821 match plan.mode {
822 QueryMode::Delete(_) => {
823 if plan.page.is_some() {
824 return Err(PolicyPlanError::DeletePlanWithPagination);
825 }
826 }
827 QueryMode::Load(_) => {
828 if plan.delete_limit.is_some() {
829 return Err(PolicyPlanError::LoadPlanWithDeleteLimit);
830 }
831 if plan.page.is_some() && !has_order && !grouped {
835 return Err(PolicyPlanError::UnorderedPagination);
836 }
837 }
838 }
839
840 Ok(())
841}
842
843pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
845 for (field, _) in &order.fields {
846 let field_type = schema
847 .field(field)
848 .ok_or_else(|| OrderPlanError::UnknownField {
849 field: field.clone(),
850 })
851 .map_err(PlanError::from)?;
852
853 if !field_type.is_orderable() {
854 return Err(PlanError::from(OrderPlanError::UnorderableField {
856 field: field.clone(),
857 }));
858 }
859 }
860
861 Ok(())
862}
863
864pub(crate) fn validate_no_duplicate_non_pk_order_fields(
866 model: &EntityModel,
867 order: &OrderSpec,
868) -> Result<(), PlanError> {
869 let mut seen = BTreeSet::new();
870 let pk_field = model.primary_key.name;
871
872 for (field, _) in &order.fields {
873 if field == pk_field {
874 continue;
875 }
876 if !seen.insert(field.as_str()) {
877 return Err(PlanError::from(OrderPlanError::DuplicateOrderField {
878 field: field.clone(),
879 }));
880 }
881 }
882
883 Ok(())
884}
885
886pub(crate) fn validate_primary_key_tie_break(
889 model: &EntityModel,
890 order: &OrderSpec,
891) -> Result<(), PlanError> {
892 if order.fields.is_empty() {
893 return Ok(());
894 }
895
896 let pk_field = model.primary_key.name;
897 let pk_count = order
898 .fields
899 .iter()
900 .filter(|(field, _)| field == pk_field)
901 .count();
902 let trailing_pk = order
903 .fields
904 .last()
905 .is_some_and(|(field, _)| field == pk_field);
906
907 if pk_count == 1 && trailing_pk {
908 Ok(())
909 } else {
910 Err(PlanError::from(OrderPlanError::MissingPrimaryKeyTieBreak {
911 field: pk_field.to_string(),
912 }))
913 }
914}