1use super::{AccessPath, AccessPlan, LogicalPlan, OrderSpec};
11use crate::{
12 db::query::predicate::{self, SchemaInfo, coercion::canonical_cmp},
13 error::{ErrorClass, ErrorOrigin, InternalError},
14 model::entity::EntityModel,
15 model::index::IndexModel,
16 traits::{EntityKind, FieldValue},
17 value::Value,
18};
19use thiserror::Error as ThisError;
20
21#[derive(Debug, ThisError)]
31pub enum PlanError {
32 #[error("predicate validation failed: {0}")]
33 PredicateInvalid(#[from] predicate::ValidateError),
34
35 #[error("unknown order field '{field}'")]
37 UnknownOrderField { field: String },
38
39 #[error("order field '{field}' is not orderable")]
41 UnorderableField { field: String },
42
43 #[error("index '{index}' not found on entity")]
45 IndexNotFound { index: IndexModel },
46
47 #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
49 IndexPrefixTooLong { prefix_len: usize, field_len: usize },
50
51 #[error("index prefix must include at least one value")]
53 IndexPrefixEmpty,
54
55 #[error("index prefix value for field '{field}' is incompatible")]
57 IndexPrefixValueMismatch { field: String },
58
59 #[error("primary key field '{field}' is not key-compatible")]
61 PrimaryKeyNotKeyable { field: String },
62
63 #[error("key '{key:?}' is incompatible with primary key '{field}'")]
65 PrimaryKeyMismatch { field: String, key: Value },
66
67 #[error("key range start is greater than end")]
69 InvalidKeyRange,
70
71 #[error("order specification must include at least one field")]
73 EmptyOrderSpec,
74
75 #[error("delete plans must not include pagination")]
77 DeletePlanWithPagination,
78
79 #[error("load plans must not include delete limits")]
81 LoadPlanWithDeleteLimit,
82
83 #[error("delete limit requires an explicit ordering")]
85 DeleteLimitRequiresOrder,
86
87 #[error(
89 "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."
90 )]
91 UnorderedPagination,
92}
93
94pub(crate) fn validate_logical_plan_model(
103 schema: &SchemaInfo,
104 model: &EntityModel,
105 plan: &LogicalPlan<Value>,
106) -> Result<(), PlanError> {
107 if let Some(predicate) = &plan.predicate {
108 predicate::validate(schema, predicate)?;
109 }
110
111 if let Some(order) = &plan.order {
112 validate_order(schema, order)?;
113 }
114
115 validate_access_plan_model(schema, model, &plan.access)?;
116 validate_plan_semantics(plan)?;
117
118 Ok(())
119}
120
121fn validate_plan_semantics<K>(plan: &LogicalPlan<K>) -> Result<(), PlanError> {
123 if let Some(order) = &plan.order
124 && order.fields.is_empty()
125 {
126 return Err(PlanError::EmptyOrderSpec);
127 }
128
129 if plan.mode.is_delete() {
130 if plan.page.is_some() {
131 return Err(PlanError::DeletePlanWithPagination);
132 }
133
134 if plan.delete_limit.is_some()
135 && plan
136 .order
137 .as_ref()
138 .is_none_or(|order| order.fields.is_empty())
139 {
140 return Err(PlanError::DeleteLimitRequiresOrder);
141 }
142 }
143
144 if plan.mode.is_load() {
145 if plan.delete_limit.is_some() {
146 return Err(PlanError::LoadPlanWithDeleteLimit);
147 }
148
149 if plan.page.is_some()
150 && plan
151 .order
152 .as_ref()
153 .is_none_or(|order| order.fields.is_empty())
154 {
155 return Err(PlanError::UnorderedPagination);
156 }
157 }
158
159 Ok(())
160}
161
162pub(crate) fn validate_executor_plan<E: EntityKind>(
171 plan: &LogicalPlan<E::Key>,
172) -> Result<(), InternalError> {
173 let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
174 InternalError::new(
175 ErrorClass::InvariantViolation,
176 ErrorOrigin::Query,
177 format!("entity schema invalid for {}: {err}", E::PATH),
178 )
179 })?;
180
181 if let Some(predicate) = &plan.predicate {
182 predicate::validate(&schema, predicate).map_err(|err| {
183 InternalError::new(
184 ErrorClass::InvariantViolation,
185 ErrorOrigin::Query,
186 err.to_string(),
187 )
188 })?;
189 }
190
191 if let Some(order) = &plan.order {
192 validate_executor_order(&schema, order).map_err(|err| {
193 InternalError::new(
194 ErrorClass::InvariantViolation,
195 ErrorOrigin::Query,
196 err.to_string(),
197 )
198 })?;
199 }
200
201 validate_access_plan(&schema, E::MODEL, &plan.access).map_err(|err| {
202 InternalError::new(
203 ErrorClass::InvariantViolation,
204 ErrorOrigin::Query,
205 err.to_string(),
206 )
207 })?;
208
209 validate_plan_semantics(plan).map_err(|err| {
210 InternalError::new(
211 ErrorClass::InvariantViolation,
212 ErrorOrigin::Query,
213 err.to_string(),
214 )
215 })?;
216
217 Ok(())
218}
219
220pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
222 for (field, _) in &order.fields {
223 let field_type = schema
224 .field(field)
225 .ok_or_else(|| PlanError::UnknownOrderField {
226 field: field.clone(),
227 })?;
228
229 if !field_type.is_orderable() {
230 return Err(PlanError::UnorderableField {
232 field: field.clone(),
233 });
234 }
235 }
236
237 Ok(())
238}
239
240fn validate_executor_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
244 validate_order(schema, order)
245}
246
247trait AccessPlanKeyAdapter<K> {
253 fn validate_pk_key(
255 &self,
256 schema: &SchemaInfo,
257 model: &EntityModel,
258 key: &K,
259 ) -> Result<(), PlanError>;
260
261 fn validate_key_range(
263 &self,
264 schema: &SchemaInfo,
265 model: &EntityModel,
266 start: &K,
267 end: &K,
268 ) -> Result<(), PlanError>;
269}
270
271struct GenericKeyAdapter;
277
278impl<K> AccessPlanKeyAdapter<K> for GenericKeyAdapter
279where
280 K: FieldValue + Ord,
281{
282 fn validate_pk_key(
283 &self,
284 schema: &SchemaInfo,
285 model: &EntityModel,
286 key: &K,
287 ) -> Result<(), PlanError> {
288 validate_pk_key(schema, model, key)
289 }
290
291 fn validate_key_range(
292 &self,
293 schema: &SchemaInfo,
294 model: &EntityModel,
295 start: &K,
296 end: &K,
297 ) -> Result<(), PlanError> {
298 validate_pk_key(schema, model, start)?;
299 validate_pk_key(schema, model, end)?;
300 if start > end {
301 return Err(PlanError::InvalidKeyRange);
302 }
303
304 Ok(())
305 }
306}
307
308struct ValueKeyAdapter;
314
315impl AccessPlanKeyAdapter<Value> for ValueKeyAdapter {
316 fn validate_pk_key(
317 &self,
318 schema: &SchemaInfo,
319 model: &EntityModel,
320 key: &Value,
321 ) -> Result<(), PlanError> {
322 validate_pk_value(schema, model, key)
323 }
324
325 fn validate_key_range(
326 &self,
327 schema: &SchemaInfo,
328 model: &EntityModel,
329 start: &Value,
330 end: &Value,
331 ) -> Result<(), PlanError> {
332 validate_pk_value(schema, model, start)?;
333 validate_pk_value(schema, model, end)?;
334 let ordering = canonical_cmp(start, end);
335 if ordering == std::cmp::Ordering::Greater {
336 return Err(PlanError::InvalidKeyRange);
337 }
338
339 Ok(())
340 }
341}
342
343fn validate_access_plan_with<K>(
345 schema: &SchemaInfo,
346 model: &EntityModel,
347 access: &AccessPlan<K>,
348 adapter: &impl AccessPlanKeyAdapter<K>,
349) -> Result<(), PlanError> {
350 match access {
351 AccessPlan::Path(path) => validate_access_path_with(schema, model, path, adapter),
352 AccessPlan::Union(children) | AccessPlan::Intersection(children) => {
353 for child in children {
354 validate_access_plan_with(schema, model, child, adapter)?;
355 }
356
357 Ok(())
358 }
359 }
360}
361
362fn validate_access_path_with<K>(
364 schema: &SchemaInfo,
365 model: &EntityModel,
366 access: &AccessPath<K>,
367 adapter: &impl AccessPlanKeyAdapter<K>,
368) -> Result<(), PlanError> {
369 match access {
370 AccessPath::ByKey(key) => adapter.validate_pk_key(schema, model, key),
371 AccessPath::ByKeys(keys) => {
372 if keys.is_empty() {
374 return Ok(());
375 }
376 for key in keys {
377 adapter.validate_pk_key(schema, model, key)?;
378 }
379
380 Ok(())
381 }
382 AccessPath::KeyRange { start, end } => {
383 adapter.validate_key_range(schema, model, start, end)
384 }
385 AccessPath::IndexPrefix { index, values } => {
386 validate_index_prefix(schema, model, index, values)
387 }
388 AccessPath::FullScan => Ok(()),
389 }
390}
391
392pub(crate) fn validate_access_plan<K>(
396 schema: &SchemaInfo,
397 model: &EntityModel,
398 access: &AccessPlan<K>,
399) -> Result<(), PlanError>
400where
401 K: FieldValue + Ord,
402{
403 validate_access_plan_with(schema, model, access, &GenericKeyAdapter)
404}
405
406pub(crate) fn validate_access_plan_model(
408 schema: &SchemaInfo,
409 model: &EntityModel,
410 access: &AccessPlan<Value>,
411) -> Result<(), PlanError> {
412 validate_access_plan_with(schema, model, access, &ValueKeyAdapter)
413}
414
415fn validate_pk_key<K>(schema: &SchemaInfo, model: &EntityModel, key: &K) -> Result<(), PlanError>
417where
418 K: FieldValue,
419{
420 let field = model.primary_key.name;
421
422 let field_type = schema
423 .field(field)
424 .ok_or_else(|| PlanError::PrimaryKeyNotKeyable {
425 field: field.to_string(),
426 })?;
427
428 if !field_type.is_keyable() {
429 return Err(PlanError::PrimaryKeyNotKeyable {
430 field: field.to_string(),
431 });
432 }
433
434 let value = key.to_value();
435 if !predicate::validate::literal_matches_type(&value, field_type) {
436 return Err(PlanError::PrimaryKeyMismatch {
437 field: field.to_string(),
438 key: value,
439 });
440 }
441
442 Ok(())
443}
444
445fn validate_pk_value(
447 schema: &SchemaInfo,
448 model: &EntityModel,
449 key: &Value,
450) -> Result<(), PlanError> {
451 let field = model.primary_key.name;
452
453 let field_type = schema
454 .field(field)
455 .ok_or_else(|| PlanError::PrimaryKeyNotKeyable {
456 field: field.to_string(),
457 })?;
458
459 if !field_type.is_keyable() {
460 return Err(PlanError::PrimaryKeyNotKeyable {
461 field: field.to_string(),
462 });
463 }
464
465 if !predicate::validate::literal_matches_type(key, field_type) {
466 return Err(PlanError::PrimaryKeyMismatch {
467 field: field.to_string(),
468 key: key.clone(),
469 });
470 }
471
472 Ok(())
473}
474
475fn validate_index_prefix(
477 schema: &SchemaInfo,
478 model: &EntityModel,
479 index: &IndexModel,
480 values: &[Value],
481) -> Result<(), PlanError> {
482 if !model.indexes.contains(&index) {
483 return Err(PlanError::IndexNotFound { index: *index });
484 }
485
486 if values.is_empty() {
487 return Err(PlanError::IndexPrefixEmpty);
488 }
489
490 if values.len() > index.fields.len() {
491 return Err(PlanError::IndexPrefixTooLong {
492 prefix_len: values.len(),
493 field_len: index.fields.len(),
494 });
495 }
496
497 for (field, value) in index.fields.iter().zip(values.iter()) {
498 let field_type =
499 schema
500 .field(field)
501 .ok_or_else(|| PlanError::IndexPrefixValueMismatch {
502 field: field.to_string(),
503 })?;
504
505 if !predicate::validate::literal_matches_type(value, field_type) {
506 return Err(PlanError::IndexPrefixValueMismatch {
507 field: field.to_string(),
508 });
509 }
510 }
511
512 Ok(())
513}
514
515#[cfg(test)]
523mod tests {
524 use super::{PlanError, validate_logical_plan_model};
526 use crate::{
527 db::query::{
528 plan::{AccessPath, AccessPlan, LogicalPlan, OrderDirection, OrderSpec, PageSpec},
529 predicate::{SchemaInfo, ValidateError},
530 },
531 model::{
532 entity::EntityModel,
533 field::{EntityFieldKind, EntityFieldModel},
534 index::IndexModel,
535 },
536 test_fixtures::InvalidEntityModelBuilder,
537 traits::EntitySchema,
538 types::Ulid,
539 value::Value,
540 };
541
542 fn field(name: &'static str, kind: EntityFieldKind) -> EntityFieldModel {
543 EntityFieldModel { name, kind }
544 }
545
546 const INDEX_FIELDS: [&str; 1] = ["tag"];
547 const INDEX_MODEL: IndexModel =
548 IndexModel::new("test::idx_tag", "test::IndexStore", &INDEX_FIELDS, false);
549
550 crate::test_entity_schema! {
551 PlanValidateIndexedEntity,
552 id = Ulid,
553 path = "plan_validate::IndexedEntity",
554 entity_name = "IndexedEntity",
555 primary_key = "id",
556 pk_index = 0,
557 fields = [
558 ("id", EntityFieldKind::Ulid),
559 ("tag", EntityFieldKind::Text),
560 ],
561 indexes = [&INDEX_MODEL],
562 }
563
564 crate::test_entity_schema! {
565 PlanValidateListEntity,
566 id = Ulid,
567 path = "plan_validate::ListEntity",
568 entity_name = "ListEntity",
569 primary_key = "id",
570 pk_index = 0,
571 fields = [
572 ("id", EntityFieldKind::Ulid),
573 ("tags", EntityFieldKind::List(&EntityFieldKind::Text)),
574 ],
575 indexes = [],
576 }
577
578 fn model_with_index() -> &'static EntityModel {
580 <PlanValidateIndexedEntity as EntitySchema>::MODEL
581 }
582
583 #[test]
584 fn model_rejects_missing_primary_key() {
585 let fields: &'static [EntityFieldModel] =
588 Box::leak(vec![field("id", EntityFieldKind::Ulid)].into_boxed_slice());
589 let missing_pk = Box::leak(Box::new(field("missing", EntityFieldKind::Ulid)));
590
591 let model = InvalidEntityModelBuilder::from_static(
592 "test::Entity",
593 "TestEntity",
594 missing_pk,
595 fields,
596 &[],
597 );
598
599 assert!(matches!(
600 SchemaInfo::from_entity_model(&model),
601 Err(ValidateError::InvalidPrimaryKey { .. })
602 ));
603 }
604
605 #[test]
606 fn model_rejects_duplicate_fields() {
607 let model = InvalidEntityModelBuilder::from_fields(
608 vec![
609 field("dup", EntityFieldKind::Text),
610 field("dup", EntityFieldKind::Text),
611 ],
612 0,
613 );
614
615 assert!(matches!(
616 SchemaInfo::from_entity_model(&model),
617 Err(ValidateError::DuplicateField { .. })
618 ));
619 }
620
621 #[test]
622 fn model_rejects_invalid_primary_key_type() {
623 let model = InvalidEntityModelBuilder::from_fields(
624 vec![field("pk", EntityFieldKind::List(&EntityFieldKind::Text))],
625 0,
626 );
627
628 assert!(matches!(
629 SchemaInfo::from_entity_model(&model),
630 Err(ValidateError::InvalidPrimaryKeyType { .. })
631 ));
632 }
633
634 #[test]
635 fn model_rejects_index_unknown_field() {
636 const INDEX_FIELDS: [&str; 1] = ["missing"];
637 const INDEX_MODEL: IndexModel = IndexModel::new(
638 "test::idx_missing",
639 "test::IndexStore",
640 &INDEX_FIELDS,
641 false,
642 );
643 const INDEXES: [&IndexModel; 1] = [&INDEX_MODEL];
644
645 let fields: &'static [EntityFieldModel] =
646 Box::leak(vec![field("id", EntityFieldKind::Ulid)].into_boxed_slice());
647 let model = InvalidEntityModelBuilder::from_static(
648 "test::Entity",
649 "TestEntity",
650 &fields[0],
651 fields,
652 &INDEXES,
653 );
654
655 assert!(matches!(
656 SchemaInfo::from_entity_model(&model),
657 Err(ValidateError::IndexFieldUnknown { .. })
658 ));
659 }
660
661 #[test]
662 fn model_rejects_index_non_queryable_field() {
663 const INDEX_FIELDS: [&str; 1] = ["broken"];
664 const INDEX_MODEL: IndexModel =
665 IndexModel::new("test::idx_broken", "test::IndexStore", &INDEX_FIELDS, false);
666 const INDEXES: [&IndexModel; 1] = [&INDEX_MODEL];
667
668 let fields: &'static [EntityFieldModel] = Box::leak(
669 vec![
670 field("id", EntityFieldKind::Ulid),
671 field("broken", EntityFieldKind::Structured { queryable: false }),
672 ]
673 .into_boxed_slice(),
674 );
675 let model = InvalidEntityModelBuilder::from_static(
676 "test::Entity",
677 "TestEntity",
678 &fields[0],
679 fields,
680 &INDEXES,
681 );
682
683 assert!(matches!(
684 SchemaInfo::from_entity_model(&model),
685 Err(ValidateError::IndexFieldNotQueryable { .. })
686 ));
687 }
688
689 #[test]
690 fn model_rejects_index_map_field_in_0_7_x() {
691 const INDEX_FIELDS: [&str; 1] = ["attributes"];
692 const INDEX_MODEL: IndexModel = IndexModel::new(
693 "test::idx_attributes",
694 "test::IndexStore",
695 &INDEX_FIELDS,
696 false,
697 );
698 const INDEXES: [&IndexModel; 1] = [&INDEX_MODEL];
699
700 let fields: &'static [EntityFieldModel] = Box::leak(
701 vec![
702 field("id", EntityFieldKind::Ulid),
703 field(
704 "attributes",
705 EntityFieldKind::Map {
706 key: &EntityFieldKind::Text,
707 value: &EntityFieldKind::Uint,
708 },
709 ),
710 ]
711 .into_boxed_slice(),
712 );
713 let model = InvalidEntityModelBuilder::from_static(
714 "test::Entity",
715 "TestEntity",
716 &fields[0],
717 fields,
718 &INDEXES,
719 );
720
721 assert!(matches!(
722 SchemaInfo::from_entity_model(&model),
723 Err(ValidateError::IndexFieldMapNotQueryable { .. })
724 ));
725 }
726
727 #[test]
728 fn model_rejects_duplicate_index_names() {
729 const INDEX_FIELDS_A: [&str; 1] = ["id"];
730 const INDEX_FIELDS_B: [&str; 1] = ["other"];
731 const INDEX_A: IndexModel = IndexModel::new(
732 "test::dup_index",
733 "test::IndexStore",
734 &INDEX_FIELDS_A,
735 false,
736 );
737 const INDEX_B: IndexModel = IndexModel::new(
738 "test::dup_index",
739 "test::IndexStore",
740 &INDEX_FIELDS_B,
741 false,
742 );
743 const INDEXES: [&IndexModel; 2] = [&INDEX_A, &INDEX_B];
744
745 let fields: &'static [EntityFieldModel] = Box::leak(
746 vec![
747 field("id", EntityFieldKind::Ulid),
748 field("other", EntityFieldKind::Text),
749 ]
750 .into_boxed_slice(),
751 );
752 let model = InvalidEntityModelBuilder::from_static(
753 "test::Entity",
754 "TestEntity",
755 &fields[0],
756 fields,
757 &INDEXES,
758 );
759
760 assert!(matches!(
761 SchemaInfo::from_entity_model(&model),
762 Err(ValidateError::DuplicateIndexName { .. })
763 ));
764 }
765
766 #[test]
767 fn plan_rejects_unorderable_field() {
768 let model = <PlanValidateListEntity as EntitySchema>::MODEL;
769
770 let schema = SchemaInfo::from_entity_model(model).expect("valid model");
771 let plan: LogicalPlan<Value> = LogicalPlan {
772 mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
773 access: AccessPlan::Path(AccessPath::FullScan),
774 predicate: None,
775 order: Some(OrderSpec {
776 fields: vec![("tags".to_string(), OrderDirection::Asc)],
777 }),
778 delete_limit: None,
779 page: None,
780 consistency: crate::db::query::ReadConsistency::MissingOk,
781 };
782
783 let err =
784 validate_logical_plan_model(&schema, model, &plan).expect_err("unorderable field");
785 assert!(matches!(err, PlanError::UnorderableField { .. }));
786 }
787
788 #[test]
789 fn plan_rejects_index_prefix_too_long() {
790 let model = model_with_index();
791 let schema = SchemaInfo::from_entity_model(model).expect("valid model");
792 let plan: LogicalPlan<Value> = LogicalPlan {
793 mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
794 access: AccessPlan::Path(AccessPath::IndexPrefix {
795 index: INDEX_MODEL,
796 values: vec![Value::Text("a".to_string()), Value::Text("b".to_string())],
797 }),
798 predicate: None,
799 order: None,
800 delete_limit: None,
801 page: None,
802 consistency: crate::db::query::ReadConsistency::MissingOk,
803 };
804
805 let err =
806 validate_logical_plan_model(&schema, model, &plan).expect_err("index prefix too long");
807 assert!(matches!(err, PlanError::IndexPrefixTooLong { .. }));
808 }
809
810 #[test]
811 fn plan_rejects_empty_index_prefix() {
812 let model = model_with_index();
813 let schema = SchemaInfo::from_entity_model(model).expect("valid model");
814 let plan: LogicalPlan<Value> = LogicalPlan {
815 mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
816 access: AccessPlan::Path(AccessPath::IndexPrefix {
817 index: INDEX_MODEL,
818 values: vec![],
819 }),
820 predicate: None,
821 order: None,
822 delete_limit: None,
823 page: None,
824 consistency: crate::db::query::ReadConsistency::MissingOk,
825 };
826
827 let err =
828 validate_logical_plan_model(&schema, model, &plan).expect_err("index prefix empty");
829 assert!(matches!(err, PlanError::IndexPrefixEmpty));
830 }
831
832 #[test]
833 fn plan_accepts_model_based_validation() {
834 let model = <PlanValidateIndexedEntity as EntitySchema>::MODEL;
835 let schema = SchemaInfo::from_entity_model(model).expect("valid model");
836 let plan: LogicalPlan<Value> = LogicalPlan {
837 mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
838 access: AccessPlan::Path(AccessPath::ByKey(Value::Ulid(Ulid::nil()))),
839 predicate: None,
840 order: None,
841 delete_limit: None,
842 page: None,
843 consistency: crate::db::query::ReadConsistency::MissingOk,
844 };
845
846 validate_logical_plan_model(&schema, model, &plan).expect("valid plan");
847 }
848
849 #[test]
850 fn plan_rejects_unordered_pagination() {
851 let model = <PlanValidateIndexedEntity as EntitySchema>::MODEL;
852 let schema = SchemaInfo::from_entity_model(model).expect("valid model");
853 let plan: LogicalPlan<Value> = LogicalPlan {
854 mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
855 access: AccessPlan::Path(AccessPath::FullScan),
856 predicate: None,
857 order: None,
858 delete_limit: None,
859 page: Some(PageSpec {
860 limit: Some(10),
861 offset: 2,
862 }),
863 consistency: crate::db::query::ReadConsistency::MissingOk,
864 };
865
866 let err = validate_logical_plan_model(&schema, model, &plan)
867 .expect_err("pagination without ordering must be rejected");
868 assert!(matches!(err, PlanError::UnorderedPagination));
869 }
870
871 #[test]
872 fn plan_accepts_ordered_pagination() {
873 let model = <PlanValidateIndexedEntity as EntitySchema>::MODEL;
874 let schema = SchemaInfo::from_entity_model(model).expect("valid model");
875 let plan: LogicalPlan<Value> = LogicalPlan {
876 mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
877 access: AccessPlan::Path(AccessPath::FullScan),
878 predicate: None,
879 order: Some(OrderSpec {
880 fields: vec![("id".to_string(), OrderDirection::Asc)],
881 }),
882 delete_limit: None,
883 page: Some(PageSpec {
884 limit: Some(10),
885 offset: 2,
886 }),
887 consistency: crate::db::query::ReadConsistency::MissingOk,
888 };
889
890 validate_logical_plan_model(&schema, model, &plan).expect("ordered pagination is valid");
891 }
892}