Skip to main content

icydb_core/db/query/plan/
validate.rs

1//! Executor-ready plan validation against a concrete entity schema.
2use super::{AccessPath, AccessPlan, LogicalPlan, OrderSpec};
3use crate::{
4    db::query::predicate::{self, SchemaInfo},
5    error::{ErrorClass, ErrorOrigin, InternalError},
6    model::entity::EntityModel,
7    model::index::IndexModel,
8    traits::{EntityKind, FieldValue},
9    value::Value,
10};
11use thiserror::Error as ThisError;
12
13///
14/// PlanError
15///
16/// Executor-visible validation failures for logical plans.
17///
18/// These errors indicate that a plan cannot be safely executed against the
19/// current schema or entity definition. They are *not* planner bugs.
20///
21
22#[derive(Debug, ThisError)]
23pub enum PlanError {
24    #[error("predicate validation failed: {0}")]
25    PredicateInvalid(#[from] predicate::ValidateError),
26
27    /// ORDER BY references an unknown field.
28    #[error("unknown order field '{field}'")]
29    UnknownOrderField { field: String },
30
31    /// ORDER BY references a field that cannot be ordered.
32    #[error("order field '{field}' is not orderable")]
33    UnorderableField { field: String },
34
35    /// Access plan references an index not declared on the entity.
36    #[error("index '{index}' not found on entity")]
37    IndexNotFound { index: IndexModel },
38
39    /// Index prefix exceeds the number of indexed fields.
40    #[error("index prefix length {prefix_len} exceeds index field count {field_len}")]
41    IndexPrefixTooLong { prefix_len: usize, field_len: usize },
42
43    /// Index prefix must include at least one value.
44    #[error("index prefix must include at least one value")]
45    IndexPrefixEmpty,
46
47    /// Index prefix literal does not match indexed field type.
48    #[error("index prefix value for field '{field}' is incompatible")]
49    IndexPrefixValueMismatch { field: String },
50
51    /// Primary key field exists but is not key-compatible.
52    #[error("primary key field '{field}' is not key-compatible")]
53    PrimaryKeyUnsupported { field: String },
54
55    /// Supplied key does not match the primary key type.
56    #[error("key '{key:?}' is incompatible with primary key '{field}'")]
57    PrimaryKeyMismatch { field: String, key: Value },
58
59    /// Key range has invalid ordering.
60    #[error("key range start is greater than end")]
61    InvalidKeyRange,
62
63    /// ORDER BY must specify at least one field.
64    #[error("order specification must include at least one field")]
65    EmptyOrderSpec,
66
67    /// Delete plans must not carry pagination.
68    #[error("delete plans must not include pagination")]
69    DeletePlanWithPagination,
70
71    /// Load plans must not carry delete limits.
72    #[error("load plans must not include delete limits")]
73    LoadPlanWithDeleteLimit,
74
75    /// Delete limits require an explicit ordering.
76    #[error("delete limit requires an explicit ordering")]
77    DeleteLimitRequiresOrder,
78}
79
80/// Validate a logical plan using a prebuilt schema surface.
81#[cfg(test)]
82pub(crate) fn validate_plan_with_schema_info<K>(
83    schema: &SchemaInfo,
84    model: &EntityModel,
85    plan: &LogicalPlan<K>,
86) -> Result<(), PlanError>
87where
88    K: FieldValue + Ord,
89{
90    validate_logical_plan(schema, model, plan)
91}
92
93/// Validate a logical plan against the runtime entity model.
94///
95/// This is the executor-safe entrypoint and must not consult global schema.
96#[cfg(test)]
97#[expect(dead_code)]
98pub(crate) fn validate_plan_with_model<K>(
99    plan: &LogicalPlan<K>,
100    model: &EntityModel,
101) -> Result<(), PlanError>
102where
103    K: FieldValue + Ord,
104{
105    let schema = SchemaInfo::from_entity_model(model)?;
106    validate_plan_with_schema_info(&schema, model, plan)
107}
108
109/// Validate a logical plan against schema and plan-level invariants.
110#[cfg(test)]
111pub(crate) fn validate_logical_plan<K>(
112    schema: &SchemaInfo,
113    model: &EntityModel,
114    plan: &LogicalPlan<K>,
115) -> Result<(), PlanError>
116where
117    K: FieldValue + Ord,
118{
119    if let Some(predicate) = &plan.predicate {
120        predicate::validate(schema, predicate)?;
121    }
122
123    if let Some(order) = &plan.order {
124        validate_order(schema, order)?;
125    }
126
127    validate_access_plan(schema, model, &plan.access)?;
128    validate_plan_semantics(plan)?;
129
130    Ok(())
131}
132
133/// Validate a logical plan with model-level key values.
134pub(crate) fn validate_logical_plan_model(
135    schema: &SchemaInfo,
136    model: &EntityModel,
137    plan: &LogicalPlan<Value>,
138) -> Result<(), PlanError> {
139    if let Some(predicate) = &plan.predicate {
140        predicate::validate(schema, predicate)?;
141    }
142
143    if let Some(order) = &plan.order {
144        validate_order(schema, order)?;
145    }
146
147    validate_access_plan_model(schema, model, &plan.access)?;
148    validate_plan_semantics(plan)?;
149
150    Ok(())
151}
152
153/// Validate plan-level invariants not covered by schema checks.
154fn validate_plan_semantics<K>(plan: &LogicalPlan<K>) -> Result<(), PlanError> {
155    if let Some(order) = &plan.order
156        && order.fields.is_empty()
157    {
158        return Err(PlanError::EmptyOrderSpec);
159    }
160
161    if plan.mode.is_delete() {
162        if plan.page.is_some() {
163            return Err(PlanError::DeletePlanWithPagination);
164        }
165
166        if plan.delete_limit.is_some()
167            && plan
168                .order
169                .as_ref()
170                .is_none_or(|order| order.fields.is_empty())
171        {
172            return Err(PlanError::DeleteLimitRequiresOrder);
173        }
174    }
175
176    if plan.mode.is_load() && plan.delete_limit.is_some() {
177        return Err(PlanError::LoadPlanWithDeleteLimit);
178    }
179
180    Ok(())
181}
182
183/// Validate plans at executor boundaries and surface invariant violations.
184pub(crate) fn validate_executor_plan<E: EntityKind>(
185    plan: &LogicalPlan<E::Id>,
186) -> Result<(), InternalError> {
187    let schema = SchemaInfo::from_entity_model(E::MODEL).map_err(|err| {
188        InternalError::new(
189            ErrorClass::InvariantViolation,
190            ErrorOrigin::Query,
191            format!("entity schema invalid for {}: {err}", E::PATH),
192        )
193    })?;
194
195    if let Some(predicate) = &plan.predicate {
196        predicate::validate(&schema, predicate).map_err(|err| {
197            InternalError::new(
198                ErrorClass::InvariantViolation,
199                ErrorOrigin::Query,
200                err.to_string(),
201            )
202        })?;
203    }
204
205    if let Some(order) = &plan.order {
206        validate_executor_order(&schema, order).map_err(|err| {
207            InternalError::new(
208                ErrorClass::InvariantViolation,
209                ErrorOrigin::Query,
210                err.to_string(),
211            )
212        })?;
213    }
214
215    validate_access_plan(&schema, E::MODEL, &plan.access).map_err(|err| {
216        InternalError::new(
217            ErrorClass::InvariantViolation,
218            ErrorOrigin::Query,
219            err.to_string(),
220        )
221    })?;
222
223    validate_plan_semantics(plan).map_err(|err| {
224        InternalError::new(
225            ErrorClass::InvariantViolation,
226            ErrorOrigin::Query,
227            err.to_string(),
228        )
229    })?;
230
231    Ok(())
232}
233
234/// Validate ORDER BY fields against the schema.
235pub(crate) fn validate_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
236    for (field, _) in &order.fields {
237        let field_type = schema
238            .field(field)
239            .ok_or_else(|| PlanError::UnknownOrderField {
240                field: field.clone(),
241            })?;
242
243        if !field_type.is_orderable() {
244            // CONTRACT: ORDER BY rejects unsupported or unordered fields.
245            return Err(PlanError::UnorderableField {
246                field: field.clone(),
247            });
248        }
249    }
250
251    Ok(())
252}
253
254/// Validate ORDER BY fields for executor-only plans.
255///
256/// CONTRACT: executor ordering validation matches planner rules.
257fn validate_executor_order(schema: &SchemaInfo, order: &OrderSpec) -> Result<(), PlanError> {
258    validate_order(schema, order)
259}
260
261/// Validate executor-visible access paths.
262///
263/// This ensures keys, ranges, and index prefixes are schema-compatible.
264pub(crate) fn validate_access_plan<K>(
265    schema: &SchemaInfo,
266    model: &EntityModel,
267    access: &AccessPlan<K>,
268) -> Result<(), PlanError>
269where
270    K: FieldValue + Ord,
271{
272    match access {
273        AccessPlan::Path(path) => validate_access_path(schema, model, path),
274        AccessPlan::Union(children) | AccessPlan::Intersection(children) => {
275            for child in children {
276                validate_access_plan(schema, model, child)?;
277            }
278            Ok(())
279        }
280    }
281}
282
283/// Validate access paths that carry model-level key values.
284pub(crate) fn validate_access_plan_model(
285    schema: &SchemaInfo,
286    model: &EntityModel,
287    access: &AccessPlan<Value>,
288) -> Result<(), PlanError> {
289    match access {
290        AccessPlan::Path(path) => validate_access_path_model(schema, model, path),
291        AccessPlan::Union(children) | AccessPlan::Intersection(children) => {
292            for child in children {
293                validate_access_plan_model(schema, model, child)?;
294            }
295            Ok(())
296        }
297    }
298}
299
300fn validate_access_path<K>(
301    schema: &SchemaInfo,
302    model: &EntityModel,
303    access: &AccessPath<K>,
304) -> Result<(), PlanError>
305where
306    K: FieldValue + Ord,
307{
308    match access {
309        AccessPath::ByKey(key) => validate_pk_key(schema, model, key),
310        AccessPath::ByKeys(keys) => {
311            // Empty key lists are a valid no-op.
312            if keys.is_empty() {
313                return Ok(());
314            }
315            for key in keys {
316                validate_pk_key(schema, model, key)?;
317            }
318            Ok(())
319        }
320        AccessPath::KeyRange { start, end } => {
321            validate_pk_key(schema, model, start)?;
322            validate_pk_key(schema, model, end)?;
323            if start > end {
324                return Err(PlanError::InvalidKeyRange);
325            }
326            Ok(())
327        }
328        AccessPath::IndexPrefix { index, values } => {
329            validate_index_prefix(schema, model, index, values)
330        }
331        AccessPath::FullScan => Ok(()),
332    }
333}
334
335// Validate executor-visible access paths that carry model-level key values.
336fn validate_access_path_model(
337    schema: &SchemaInfo,
338    model: &EntityModel,
339    access: &AccessPath<Value>,
340) -> Result<(), PlanError> {
341    match access {
342        AccessPath::ByKey(key) => validate_pk_value(schema, model, key),
343        AccessPath::ByKeys(keys) => {
344            if keys.is_empty() {
345                return Ok(());
346            }
347            for key in keys {
348                validate_pk_value(schema, model, key)?;
349            }
350            Ok(())
351        }
352        AccessPath::KeyRange { start, end } => {
353            validate_pk_value(schema, model, start)?;
354            validate_pk_value(schema, model, end)?;
355            let Some(ordering) = start.partial_cmp(end) else {
356                return Err(PlanError::InvalidKeyRange);
357            };
358            if ordering == std::cmp::Ordering::Greater {
359                return Err(PlanError::InvalidKeyRange);
360            }
361            Ok(())
362        }
363        AccessPath::IndexPrefix { index, values } => {
364            validate_index_prefix(schema, model, index, values)
365        }
366        AccessPath::FullScan => Ok(()),
367    }
368}
369
370/// Validate that a key matches the entity's primary key type.
371fn validate_pk_key<K>(schema: &SchemaInfo, model: &EntityModel, key: &K) -> Result<(), PlanError>
372where
373    K: FieldValue,
374{
375    let field = model.primary_key.name;
376
377    let field_type = schema
378        .field(field)
379        .ok_or_else(|| PlanError::PrimaryKeyUnsupported {
380            field: field.to_string(),
381        })?;
382
383    if !field_type.is_keyable() {
384        return Err(PlanError::PrimaryKeyUnsupported {
385            field: field.to_string(),
386        });
387    }
388
389    let value = key.to_value();
390    if !predicate::validate::literal_matches_type(&value, field_type) {
391        return Err(PlanError::PrimaryKeyMismatch {
392            field: field.to_string(),
393            key: value,
394        });
395    }
396
397    Ok(())
398}
399
400// Validate that a model-level key value matches the entity's primary key type.
401fn validate_pk_value(
402    schema: &SchemaInfo,
403    model: &EntityModel,
404    key: &Value,
405) -> Result<(), PlanError> {
406    let field = model.primary_key.name;
407
408    let field_type = schema
409        .field(field)
410        .ok_or_else(|| PlanError::PrimaryKeyUnsupported {
411            field: field.to_string(),
412        })?;
413
414    if !field_type.is_keyable() {
415        return Err(PlanError::PrimaryKeyUnsupported {
416            field: field.to_string(),
417        });
418    }
419
420    if !predicate::validate::literal_matches_type(key, field_type) {
421        return Err(PlanError::PrimaryKeyMismatch {
422            field: field.to_string(),
423            key: key.clone(),
424        });
425    }
426
427    Ok(())
428}
429
430/// Validate that an index prefix is valid for execution.
431fn validate_index_prefix(
432    schema: &SchemaInfo,
433    model: &EntityModel,
434    index: &IndexModel,
435    values: &[Value],
436) -> Result<(), PlanError> {
437    if !model.indexes.contains(&index) {
438        return Err(PlanError::IndexNotFound { index: *index });
439    }
440
441    if values.is_empty() {
442        return Err(PlanError::IndexPrefixEmpty);
443    }
444
445    if values.len() > index.fields.len() {
446        return Err(PlanError::IndexPrefixTooLong {
447            prefix_len: values.len(),
448            field_len: index.fields.len(),
449        });
450    }
451
452    for (field, value) in index.fields.iter().zip(values.iter()) {
453        let field_type =
454            schema
455                .field(field)
456                .ok_or_else(|| PlanError::IndexPrefixValueMismatch {
457                    field: field.to_string(),
458                })?;
459
460        if !predicate::validate::literal_matches_type(value, field_type) {
461            return Err(PlanError::IndexPrefixValueMismatch {
462                field: field.to_string(),
463            });
464        }
465    }
466
467    Ok(())
468}
469
470/// Map scalar field types to compatible key variants.
471///
472/// Non-scalar and unsupported field types are intentionally excluded.
473///
474/// TESTS
475///
476
477#[cfg(test)]
478mod tests {
479    // NOTE: Legacy helpers remain only for intentionally invalid schemas.
480    use super::{PlanError, validate_logical_plan_model};
481    use crate::{
482        db::query::{
483            plan::{AccessPath, AccessPlan, LogicalPlan, OrderDirection, OrderSpec},
484            predicate::{SchemaInfo, ValidateError},
485        },
486        model::{
487            entity::EntityModel,
488            field::{EntityFieldKind, EntityFieldModel},
489            index::IndexModel,
490        },
491        test_fixtures::LegacyTestEntityModel,
492        traits::EntitySchema,
493        types::Ulid,
494        value::Value,
495    };
496
497    fn field(name: &'static str, kind: EntityFieldKind) -> EntityFieldModel {
498        EntityFieldModel { name, kind }
499    }
500
501    const INDEX_FIELDS: [&str; 1] = ["tag"];
502    const INDEX_MODEL: IndexModel =
503        IndexModel::new("test::idx_tag", "test::IndexStore", &INDEX_FIELDS, false);
504
505    crate::test_entity_schema! {
506        PlanValidateIndexedEntity,
507        id = Ulid,
508        path = "plan_validate::IndexedEntity",
509        entity_name = "IndexedEntity",
510        primary_key = "id",
511        pk_index = 0,
512        fields = [
513            ("id", EntityFieldKind::Ulid),
514            ("tag", EntityFieldKind::Text),
515        ],
516        indexes = [&INDEX_MODEL],
517    }
518
519    crate::test_entity_schema! {
520        PlanValidateListEntity,
521        id = Ulid,
522        path = "plan_validate::ListEntity",
523        entity_name = "ListEntity",
524        primary_key = "id",
525        pk_index = 0,
526        fields = [
527            ("id", EntityFieldKind::Ulid),
528            ("tags", EntityFieldKind::List(&EntityFieldKind::Text)),
529        ],
530        indexes = [],
531    }
532
533    // Helper for tests that need the indexed model derived from a typed schema.
534    fn model_with_index() -> &'static EntityModel {
535        <PlanValidateIndexedEntity as EntitySchema>::MODEL
536    }
537
538    #[test]
539    fn model_rejects_missing_primary_key() {
540        // Legacy test scaffolding: invalid models are hand-built to exercise
541        // validation failures that helpers intentionally prevent.
542        let fields: &'static [EntityFieldModel] =
543            Box::leak(vec![field("id", EntityFieldKind::Ulid)].into_boxed_slice());
544        let missing_pk = Box::leak(Box::new(field("missing", EntityFieldKind::Ulid)));
545
546        let model = LegacyTestEntityModel::from_static(
547            "test::Entity",
548            "TestEntity",
549            missing_pk,
550            fields,
551            &[],
552        );
553
554        assert!(matches!(
555            SchemaInfo::from_entity_model(&model),
556            Err(ValidateError::InvalidPrimaryKey { .. })
557        ));
558    }
559
560    #[test]
561    fn model_rejects_duplicate_fields() {
562        let model = LegacyTestEntityModel::from_fields(
563            vec![
564                field("dup", EntityFieldKind::Text),
565                field("dup", EntityFieldKind::Text),
566            ],
567            0,
568        );
569
570        assert!(matches!(
571            SchemaInfo::from_entity_model(&model),
572            Err(ValidateError::DuplicateField { .. })
573        ));
574    }
575
576    #[test]
577    fn model_rejects_invalid_primary_key_type() {
578        let model = LegacyTestEntityModel::from_fields(
579            vec![field("pk", EntityFieldKind::List(&EntityFieldKind::Text))],
580            0,
581        );
582
583        assert!(matches!(
584            SchemaInfo::from_entity_model(&model),
585            Err(ValidateError::InvalidPrimaryKeyType { .. })
586        ));
587    }
588
589    #[test]
590    fn model_rejects_index_unknown_field() {
591        const INDEX_FIELDS: [&str; 1] = ["missing"];
592        const INDEX_MODEL: IndexModel = IndexModel::new(
593            "test::idx_missing",
594            "test::IndexStore",
595            &INDEX_FIELDS,
596            false,
597        );
598        const INDEXES: [&IndexModel; 1] = [&INDEX_MODEL];
599
600        let fields: &'static [EntityFieldModel] =
601            Box::leak(vec![field("id", EntityFieldKind::Ulid)].into_boxed_slice());
602        let model = LegacyTestEntityModel::from_static(
603            "test::Entity",
604            "TestEntity",
605            &fields[0],
606            fields,
607            &INDEXES,
608        );
609
610        assert!(matches!(
611            SchemaInfo::from_entity_model(&model),
612            Err(ValidateError::IndexFieldUnknown { .. })
613        ));
614    }
615
616    #[test]
617    fn model_rejects_index_unsupported_field() {
618        const INDEX_FIELDS: [&str; 1] = ["broken"];
619        const INDEX_MODEL: IndexModel =
620            IndexModel::new("test::idx_broken", "test::IndexStore", &INDEX_FIELDS, false);
621        const INDEXES: [&IndexModel; 1] = [&INDEX_MODEL];
622
623        let fields: &'static [EntityFieldModel] = Box::leak(
624            vec![
625                field("id", EntityFieldKind::Ulid),
626                field("broken", EntityFieldKind::Unsupported),
627            ]
628            .into_boxed_slice(),
629        );
630        let model = LegacyTestEntityModel::from_static(
631            "test::Entity",
632            "TestEntity",
633            &fields[0],
634            fields,
635            &INDEXES,
636        );
637
638        assert!(matches!(
639            SchemaInfo::from_entity_model(&model),
640            Err(ValidateError::IndexFieldUnsupported { .. })
641        ));
642    }
643
644    #[test]
645    fn model_rejects_duplicate_index_names() {
646        const INDEX_FIELDS_A: [&str; 1] = ["id"];
647        const INDEX_FIELDS_B: [&str; 1] = ["other"];
648        const INDEX_A: IndexModel = IndexModel::new(
649            "test::dup_index",
650            "test::IndexStore",
651            &INDEX_FIELDS_A,
652            false,
653        );
654        const INDEX_B: IndexModel = IndexModel::new(
655            "test::dup_index",
656            "test::IndexStore",
657            &INDEX_FIELDS_B,
658            false,
659        );
660        const INDEXES: [&IndexModel; 2] = [&INDEX_A, &INDEX_B];
661
662        let fields: &'static [EntityFieldModel] = Box::leak(
663            vec![
664                field("id", EntityFieldKind::Ulid),
665                field("other", EntityFieldKind::Text),
666            ]
667            .into_boxed_slice(),
668        );
669        let model = LegacyTestEntityModel::from_static(
670            "test::Entity",
671            "TestEntity",
672            &fields[0],
673            fields,
674            &INDEXES,
675        );
676
677        assert!(matches!(
678            SchemaInfo::from_entity_model(&model),
679            Err(ValidateError::DuplicateIndexName { .. })
680        ));
681    }
682
683    #[test]
684    fn plan_rejects_unorderable_field() {
685        let model = <PlanValidateListEntity as EntitySchema>::MODEL;
686
687        let schema = SchemaInfo::from_entity_model(model).expect("valid model");
688        let plan: LogicalPlan<Value> = LogicalPlan {
689            mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
690            access: AccessPlan::Path(AccessPath::FullScan),
691            predicate: None,
692            order: Some(OrderSpec {
693                fields: vec![("tags".to_string(), OrderDirection::Asc)],
694            }),
695            delete_limit: None,
696            page: None,
697            consistency: crate::db::query::ReadConsistency::MissingOk,
698        };
699
700        let err =
701            validate_logical_plan_model(&schema, model, &plan).expect_err("unorderable field");
702        assert!(matches!(err, PlanError::UnorderableField { .. }));
703    }
704
705    #[test]
706    fn plan_rejects_index_prefix_too_long() {
707        let model = model_with_index();
708        let schema = SchemaInfo::from_entity_model(model).expect("valid model");
709        let plan: LogicalPlan<Value> = LogicalPlan {
710            mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
711            access: AccessPlan::Path(AccessPath::IndexPrefix {
712                index: INDEX_MODEL,
713                values: vec![Value::Text("a".to_string()), Value::Text("b".to_string())],
714            }),
715            predicate: None,
716            order: None,
717            delete_limit: None,
718            page: None,
719            consistency: crate::db::query::ReadConsistency::MissingOk,
720        };
721
722        let err =
723            validate_logical_plan_model(&schema, model, &plan).expect_err("index prefix too long");
724        assert!(matches!(err, PlanError::IndexPrefixTooLong { .. }));
725    }
726
727    #[test]
728    fn plan_rejects_empty_index_prefix() {
729        let model = model_with_index();
730        let schema = SchemaInfo::from_entity_model(model).expect("valid model");
731        let plan: LogicalPlan<Value> = LogicalPlan {
732            mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
733            access: AccessPlan::Path(AccessPath::IndexPrefix {
734                index: INDEX_MODEL,
735                values: vec![],
736            }),
737            predicate: None,
738            order: None,
739            delete_limit: None,
740            page: None,
741            consistency: crate::db::query::ReadConsistency::MissingOk,
742        };
743
744        let err =
745            validate_logical_plan_model(&schema, model, &plan).expect_err("index prefix empty");
746        assert!(matches!(err, PlanError::IndexPrefixEmpty));
747    }
748
749    #[test]
750    fn plan_accepts_model_based_validation() {
751        let model = <PlanValidateIndexedEntity as EntitySchema>::MODEL;
752        let schema = SchemaInfo::from_entity_model(model).expect("valid model");
753        let plan: LogicalPlan<Value> = LogicalPlan {
754            mode: crate::db::query::QueryMode::Load(crate::db::query::LoadSpec::new()),
755            access: AccessPlan::Path(AccessPath::ByKey(Value::Ulid(Ulid::nil()))),
756            predicate: None,
757            order: None,
758            delete_limit: None,
759            page: None,
760            consistency: crate::db::query::ReadConsistency::MissingOk,
761        };
762
763        validate_logical_plan_model(&schema, model, &plan).expect("valid plan");
764    }
765}