Skip to main content

toasty_core/schema/app/
schema.rs

1use super::{EnumVariant, Field, FieldId, FieldPrimitive, FieldTy, Model, ModelId, VariantId};
2
3use crate::{Result, stmt};
4use indexmap::IndexMap;
5use std::collections::HashSet;
6
7/// The result of resolving a [`stmt::Projection`] through the application
8/// schema.
9///
10/// A projection can resolve to either a concrete [`Field`] or an
11/// [`EnumVariant`] (when the projection stops at a variant discriminant
12/// without descending into the variant's data fields).
13///
14/// # Examples
15///
16/// ```ignore
17/// use toasty_core::schema::app::Resolved;
18///
19/// match schema.resolve(root_model, &projection) {
20///     Some(Resolved::Field(f)) => println!("field: {}", f.name),
21///     Some(Resolved::Variant(v)) => println!("variant: {}", v.discriminant),
22///     None => println!("could not resolve"),
23/// }
24/// ```
25#[derive(Debug)]
26pub enum Resolved<'a> {
27    /// The projection resolved to a concrete field.
28    Field(&'a Field),
29    /// The projection resolved to an enum variant (discriminant-only access).
30    Variant(&'a EnumVariant),
31}
32
33/// The top-level application schema, containing all registered models.
34///
35/// `Schema` is the entry point for looking up models, fields, and variants by
36/// their IDs, and for resolving projections through the model graph.
37///
38/// Schemas are typically constructed via `Schema::from_macro` (called by the
39/// `#[derive(Model)]` proc macro) or built manually for testing.
40///
41/// # Examples
42///
43/// ```
44/// use toasty_core::schema::app::Schema;
45///
46/// let schema = Schema::default();
47/// assert_eq!(schema.models().count(), 0);
48/// ```
49#[derive(Debug, Default)]
50pub struct Schema {
51    /// All models in the schema, keyed by [`ModelId`].
52    pub models: IndexMap<ModelId, Model>,
53}
54
55#[derive(Default)]
56struct Builder {
57    models: IndexMap<ModelId, Model>,
58}
59
60impl Schema {
61    /// Builds a `Schema` from a slice of models, linking relations and
62    /// validating consistency.
63    ///
64    /// This is the primary constructor used by the derive macro infrastructure.
65    pub fn from_macro(models: impl IntoIterator<Item = Model>) -> Result<Self> {
66        Builder::from_macro(models)
67    }
68
69    /// Returns a reference to the [`Field`] identified by `id`.
70    ///
71    /// # Panics
72    ///
73    /// Panics if the model or field index is invalid.
74    pub fn field(&self, id: FieldId) -> &Field {
75        self.model(id.model)
76            .fields()
77            .get(id.index)
78            .expect("invalid field ID")
79    }
80
81    /// Returns a reference to the [`EnumVariant`] identified by `id`.
82    ///
83    /// # Panics
84    ///
85    /// Panics if the model is not an [`EmbeddedEnum`](super::EmbeddedEnum) or
86    /// the variant index is out of bounds.
87    pub fn variant(&self, id: VariantId) -> &EnumVariant {
88        let Model::EmbeddedEnum(e) = self.model(id.model) else {
89            panic!("VariantId references a non-enum model");
90        };
91        e.variants.get(id.index).expect("invalid variant index")
92    }
93
94    /// Returns an iterator over all models in the schema.
95    pub fn models(&self) -> impl Iterator<Item = &Model> {
96        self.models.values()
97    }
98
99    /// Try to get a model by ID, returning `None` if not found.
100    pub fn get_model(&self, id: impl Into<ModelId>) -> Option<&Model> {
101        self.models.get(&id.into())
102    }
103
104    /// Returns a reference to the [`Model`] identified by `id`.
105    ///
106    /// # Panics
107    ///
108    /// Panics if no model with the given ID exists in the schema.
109    pub fn model(&self, id: impl Into<ModelId>) -> &Model {
110        self.models.get(&id.into()).expect("invalid model ID")
111    }
112
113    /// The fields of the model `id`, in declaration order.
114    ///
115    /// An embedded struct backs a `#[document]` column, so this doubles as a
116    /// document column's field layout — the embed is the single source of
117    /// truth for its shape. Callers map each [`Field`] to what they need (its
118    /// [`name`](Field::name), its [`expr_ty`](Field::expr_ty)); a field typed
119    /// `Type::Model` (or `List(Model)`) signals a nested document to recurse
120    /// into.
121    ///
122    /// Panics if no model has the given ID.
123    pub fn fields(&self, id: impl Into<ModelId>) -> &[Field] {
124        self.model(id).fields()
125    }
126
127    /// Walks a positional `projection` through `model`'s fields, descending
128    /// into the nested model whenever a step lands on a model-typed field
129    /// (`Type::Model`). Yields the [`Field`] at each step, in order — the last
130    /// one is the projection's leaf. The caller takes whatever it needs from
131    /// each field (its name, its type).
132    ///
133    /// Iteration stops short of `projection.len()` when a step cannot be
134    /// taken: its index is out of range, or it descends past a field that is
135    /// not model-typed. A projection therefore resolves fully iff the iterator
136    /// yields exactly `projection.len()` fields. Callers that read the leaf
137    /// (the engine's JSON-path lowering, the DynamoDB driver's document-path
138    /// rendering) work from projections already validated by
139    /// [`resolve`](Self::resolve).
140    ///
141    /// Nothing constrains `model` to a `#[document]` embed — the walk follows
142    /// any model-typed field — though document paths are its only use today.
143    pub fn project_fields<'s>(
144        &'s self,
145        model: ModelId,
146        projection: &[usize],
147    ) -> impl Iterator<Item = &'s Field> {
148        let mut current = Some(model);
149        let mut steps = projection.iter();
150
151        std::iter::from_fn(move || {
152            let &index = steps.next()?;
153            let field = self.get_model(current?)?.fields().get(index)?;
154            current = match field.expr_ty() {
155                stmt::Type::Model(nested) => Some(*nested),
156                _ => None,
157            };
158            Some(field)
159        })
160    }
161
162    /// Resolve a projection through the schema, returning either a field or
163    /// an enum variant.
164    ///
165    /// Starting from the root model, walks through each step of the projection,
166    /// resolving fields, following relations/embedded types, and recognizing
167    /// enum variant discriminant access.
168    ///
169    /// Returns `None` if:
170    /// - The projection is empty
171    /// - Any step references an invalid field/variant index
172    /// - A step tries to project through a primitive type
173    pub fn resolve<'a>(
174        &'a self,
175        root: &'a Model,
176        projection: &stmt::Projection,
177    ) -> Option<Resolved<'a>> {
178        let [first, rest @ ..] = projection.as_slice() else {
179            return None;
180        };
181
182        // Get the first field from the root model
183        let mut current_field = root.as_root_unwrap().fields.get(*first)?;
184
185        // Walk through remaining steps. Uses a manual iterator because
186        // embedded enums consume two steps (variant discriminant + field index).
187        let mut steps = rest.iter();
188        while let Some(step) = steps.next() {
189            match &current_field.ty {
190                // A `#[document]` embed stores as one column whose sub-fields
191                // live in the document type rather than as `app::Field`s. The
192                // remaining steps index into the document; validate them and
193                // resolve to the document field itself (the leaf has no
194                // `app::Field`). The path was already type-checked by the
195                // generated accessors.
196                FieldTy::Primitive(FieldPrimitive {
197                    ty: stmt::Type::Model(embed_id),
198                    ..
199                }) => {
200                    // `step` and the remaining `steps` are a contiguous tail
201                    // of `rest`; the steps consumed so far (including `step`)
202                    // place `step` at index `consumed - 1`. The document path
203                    // is that tail, valid iff every step resolves to a field.
204                    let consumed = rest.len() - steps.as_slice().len();
205                    let doc_path = &rest[consumed - 1..];
206                    return (self.project_fields(*embed_id, doc_path).count() == doc_path.len())
207                        .then_some(Resolved::Field(current_field));
208                }
209                FieldTy::Primitive(..) => {
210                    // Cannot project through primitive fields
211                    return None;
212                }
213                FieldTy::Embedded(embedded) => {
214                    let target = self.model(embedded.target);
215                    match target {
216                        Model::EmbeddedStruct(s) => {
217                            current_field = s.fields.get(*step)?;
218                        }
219                        Model::EmbeddedEnum(e) => {
220                            let variant = e.variants.get(*step)?;
221
222                            // Check if there's a field index step after the variant
223                            if let Some(field_step) = steps.next() {
224                                // Two steps: variant disc + field index → field
225                                current_field = e.fields.get(*field_step)?;
226                            } else {
227                                // Single step: variant discriminant only → variant
228                                return Some(Resolved::Variant(variant));
229                            }
230                        }
231                        _ => return None,
232                    }
233                }
234                FieldTy::BelongsTo(belongs_to) => {
235                    current_field = belongs_to.target(self).as_root_unwrap().fields.get(*step)?;
236                }
237                FieldTy::Has(has) => {
238                    current_field = has.target(self).as_root_unwrap().fields.get(*step)?;
239                }
240                FieldTy::Via(via) => {
241                    current_field = via.target(self).as_root_unwrap().fields.get(*step)?;
242                }
243            };
244        }
245
246        Some(Resolved::Field(current_field))
247    }
248
249    /// Resolve a projection to a field, walking through the schema.
250    ///
251    /// Returns `None` if the projection is empty, invalid, or resolves to an
252    /// enum variant rather than a field.
253    pub fn resolve_field<'a>(
254        &'a self,
255        root: &'a Model,
256        projection: &stmt::Projection,
257    ) -> Option<&'a Field> {
258        match self.resolve(root, projection) {
259            Some(Resolved::Field(field)) => Some(field),
260            _ => None,
261        }
262    }
263
264    /// Resolves a [`stmt::Path`] to a [`Field`] by extracting the root model
265    /// from the path and delegating to [`resolve_field`](Schema::resolve_field).
266    pub fn resolve_field_path<'a>(&'a self, path: &stmt::Path) -> Option<&'a Field> {
267        let model = self.model(path.root.as_model_unwrap());
268        self.resolve_field(model, &path.projection)
269    }
270}
271
272impl Builder {
273    pub(crate) fn from_macro(models: impl IntoIterator<Item = Model>) -> Result<Schema> {
274        let mut builder = Self { ..Self::default() };
275
276        for model in models {
277            builder.models.insert(model.id(), model);
278        }
279
280        builder.process_models()?;
281        builder.into_schema()
282    }
283
284    fn into_schema(self) -> Result<Schema> {
285        Ok(Schema {
286            models: self.models,
287        })
288    }
289
290    fn process_models(&mut self) -> Result<()> {
291        // All models have been discovered and initialized at some level, now do
292        // the relation linking.
293        self.link_relations()?;
294        self.resolve_via_targets()?;
295        self.verify_no_eager_load_cycles()?;
296
297        Ok(())
298    }
299
300    /// Resolve the `target` of every scalar-terminal `via` relation.
301    ///
302    /// A relation-terminal via knows its target at macro-expansion time (the
303    /// field's element type). A scalar-terminal via does not — the model that
304    /// owns the projected field is whatever the relation chain reaches — so the
305    /// derive leaves `target` unset and it is computed here by walking the
306    /// chain. Runs after [`link_relations`](Self::link_relations) so every
307    /// `Has`/`BelongsTo` target is final.
308    fn resolve_via_targets(&mut self) -> crate::Result<()> {
309        // Collect first; the walk borrows other models immutably.
310        let mut updates = Vec::new();
311
312        for curr in 0..self.models.len() {
313            if self.models[curr].is_embedded() {
314                continue;
315            }
316            let src = self.models[curr].id();
317            for index in 0..self.models[curr].as_root_unwrap().fields.len() {
318                let field = &self.models[curr].as_root_unwrap().fields[index];
319                let FieldTy::Via(via) = &field.ty else {
320                    continue;
321                };
322                let Some(terminal) = via.terminal else {
323                    continue;
324                };
325
326                // The relation chain is the path minus its terminal field.
327                let projection = via.path.projection.as_slice();
328                let relation_steps = &projection[..projection.len() - 1];
329                let field_name = field.name.app_unwrap().to_string();
330                let target = self.walk_via_relation_chain(src, relation_steps, &field_name)?;
331
332                // The terminal must be a stored scalar on the reached model.
333                let terminal_field = &self.models[&target].as_root_unwrap().fields[terminal];
334                if !matches!(terminal_field.ty, FieldTy::Primitive(_)) {
335                    return Err(crate::Error::invalid_schema(format!(
336                        "the `via` terminal `{}::{}` is not a scalar field",
337                        self.models[&target].name().upper_camel_case(),
338                        terminal_field.name.app_unwrap(),
339                    )));
340                }
341
342                updates.push((curr, index, target));
343            }
344        }
345
346        for (curr, index, target) in updates {
347            if let FieldTy::Via(via) = &mut self.models[curr].as_root_mut_unwrap().fields[index].ty
348            {
349                via.target = target;
350            }
351        }
352
353        Ok(())
354    }
355
356    /// Walk a via relation chain, splicing any nested via's own chain, and
357    /// return the model it reaches. Every step must be a relation.
358    fn walk_via_relation_chain(
359        &self,
360        declaring: ModelId,
361        steps: &[usize],
362        field_name: &str,
363    ) -> crate::Result<ModelId> {
364        let mut current = declaring;
365        let mut queue: Vec<usize> = steps.iter().rev().copied().collect();
366
367        while let Some(idx) = queue.pop() {
368            let field = &self.models[&current].as_root_unwrap().fields[idx];
369            match &field.ty {
370                FieldTy::Has(has) => current = has.target,
371                FieldTy::BelongsTo(belongs_to) => current = belongs_to.target,
372                // A nested via contributes its own relation chain (its terminal,
373                // if scalar, is not part of the path through it).
374                FieldTy::Via(inner) => {
375                    let inner_projection = inner.path.projection.as_slice();
376                    let inner_steps = match inner.terminal {
377                        Some(_) => &inner_projection[..inner_projection.len() - 1],
378                        None => inner_projection,
379                    };
380                    for step in inner_steps.iter().rev() {
381                        queue.push(*step);
382                    }
383                }
384                _ => {
385                    return Err(crate::Error::invalid_schema(format!(
386                        "the `via` path for `{}::{}` traverses `{}`, which is not a relation",
387                        self.models[&declaring].name().upper_camel_case(),
388                        field_name,
389                        field.name.app_unwrap(),
390                    )));
391                }
392            }
393        }
394
395        Ok(current)
396    }
397
398    fn verify_no_eager_load_cycles(&self) -> crate::Result<()> {
399        let mut visited = HashSet::new();
400        let mut model_stack = Vec::new();
401        let mut field_stack = Vec::new();
402
403        for model in self.models.values() {
404            if model.is_embedded() {
405                continue;
406            }
407            self.visit_eager_load_graph(
408                model.id(),
409                &mut visited,
410                &mut model_stack,
411                &mut field_stack,
412            )?;
413        }
414
415        Ok(())
416    }
417
418    fn visit_eager_load_graph(
419        &self,
420        model_id: ModelId,
421        visited: &mut HashSet<ModelId>,
422        model_stack: &mut Vec<ModelId>,
423        field_stack: &mut Vec<FieldId>,
424    ) -> crate::Result<()> {
425        if model_stack.contains(&model_id) {
426            return Ok(());
427        }
428
429        if !visited.insert(model_id) {
430            return Ok(());
431        }
432
433        model_stack.push(model_id);
434
435        let model = self.models[&model_id].as_root_unwrap();
436        for field in &model.fields {
437            let Some(target) = eager_relation_target(field) else {
438                continue;
439            };
440
441            if let Some(pos) = model_stack.iter().position(|id| *id == target) {
442                let mut cycle = field_stack[pos..].to_vec();
443                cycle.push(field.id);
444                return Err(crate::Error::invalid_schema(format!(
445                    "eager relation cycle detected: {}",
446                    self.format_eager_load_cycle(&cycle, target)
447                )));
448            }
449
450            field_stack.push(field.id);
451            self.visit_eager_load_graph(target, visited, model_stack, field_stack)?;
452            field_stack.pop();
453        }
454
455        model_stack.pop();
456        Ok(())
457    }
458
459    fn format_eager_load_cycle(&self, fields: &[FieldId], target: ModelId) -> String {
460        let mut parts = Vec::new();
461        for field_id in fields {
462            let model = &self.models[&field_id.model];
463            let field = &model.as_root_unwrap().fields[field_id.index];
464            parts.push(format!(
465                "{}::{}",
466                model.name().upper_camel_case(),
467                field.name.app_unwrap()
468            ));
469        }
470        parts.push(self.models[&target].name().upper_camel_case());
471        parts.join(" -> ")
472    }
473
474    /// Go through all relations and link them to their pairs
475    fn link_relations(&mut self) -> crate::Result<()> {
476        // Because arbitrary models will be mutated throughout the linking
477        // process, models cannot be iterated as that would hold a reference to
478        // `self`. Instead, we use index based iteration.
479
480        // First, link all has-many relations. Has-manys are linked first because
481        // linking them may result in converting has-one relations to BelongTo.
482        // We need this conversion to happen before any of the other processing.
483        for curr in 0..self.models.len() {
484            if self.models[curr].is_embedded() {
485                continue;
486            }
487            for index in 0..self.models[curr].as_root_unwrap().fields.len() {
488                let model = &self.models[curr];
489                let src = model.id();
490                let field = &model.as_root_unwrap().fields[index];
491
492                if let FieldTy::Has(has) = &field.ty
493                    && has.is_many()
494                {
495                    let target = has.target;
496                    let field_name = field.name.app_unwrap().to_string();
497                    let pair = if has.pair_id.is_placeholder() {
498                        self.find_has_many_pair(src, target, &field_name)?
499                    } else {
500                        self.validate_pair(src, target, &field_name, has.pair_id)?;
501                        has.pair_id
502                    };
503                    self.models[curr].as_root_mut_unwrap().fields[index]
504                        .ty
505                        .as_has_mut_unwrap()
506                        .pair_id = pair;
507                }
508            }
509        }
510
511        // Link has-one relations and compute BelongsTo foreign keys
512        for curr in 0..self.models.len() {
513            if self.models[curr].is_embedded() {
514                continue;
515            }
516            for index in 0..self.models[curr].as_root_unwrap().fields.len() {
517                let model = &self.models[curr];
518                let src = model.id();
519                let field = &model.as_root_unwrap().fields[index];
520
521                match &field.ty {
522                    FieldTy::Has(has) if has.is_one() => {
523                        let target = has.target;
524                        let field_name = field.name.app_unwrap().to_string();
525                        let pair = if has.pair_id.is_placeholder() {
526                            match self.find_belongs_to_pair(src, target, &field_name)? {
527                                Some(pair) => pair,
528                                None => {
529                                    return Err(crate::Error::invalid_schema(format!(
530                                        "field `{}::{}` has no matching `BelongsTo` relation on the target model",
531                                        self.models[curr].name().upper_camel_case(),
532                                        field_name,
533                                    )));
534                                }
535                            }
536                        } else {
537                            self.validate_pair(src, target, &field_name, has.pair_id)?;
538                            has.pair_id
539                        };
540
541                        self.models[curr].as_root_mut_unwrap().fields[index]
542                            .ty
543                            .as_has_mut_unwrap()
544                            .pair_id = pair;
545                    }
546                    FieldTy::BelongsTo(belongs_to) => {
547                        assert!(!belongs_to.foreign_key.is_placeholder());
548                        continue;
549                    }
550                    _ => {}
551                }
552            }
553        }
554
555        // Finally, link BelongsTo relations with their pairs
556        for curr in 0..self.models.len() {
557            if self.models[curr].is_embedded() {
558                continue;
559            }
560            for index in 0..self.models[curr].as_root_unwrap().fields.len() {
561                let model = &self.models[curr];
562                let field_id = model.as_root_unwrap().fields[index].id;
563
564                let pair = match &self.models[curr].as_root_unwrap().fields[index].ty {
565                    FieldTy::BelongsTo(belongs_to) => {
566                        let mut pair = None;
567                        let target = match self.models.get_index_of(&belongs_to.target) {
568                            Some(target) => target,
569                            None => {
570                                let model = &self.models[curr];
571                                return Err(crate::Error::invalid_schema(format!(
572                                    "field `{}::{}` references a model that was not registered \
573                                     with the schema; did you forget to register it with `Db::builder()`?",
574                                    model.name().upper_camel_case(),
575                                    model.as_root_unwrap().fields[index].name(),
576                                )));
577                            }
578                        };
579
580                        for target_index in 0..self.models[target].as_root_unwrap().fields.len() {
581                            pair = match &self.models[target].as_root_unwrap().fields[target_index]
582                                .ty
583                            {
584                                FieldTy::Has(has) if has.pair_id == field_id => {
585                                    assert!(pair.is_none());
586                                    Some(
587                                        self.models[target].as_root_unwrap().fields[target_index]
588                                            .id,
589                                    )
590                                }
591                                _ => continue,
592                            }
593                        }
594
595                        if pair.is_none() {
596                            continue;
597                        }
598
599                        pair
600                    }
601                    _ => continue,
602                };
603
604                self.models[curr].as_root_mut_unwrap().fields[index]
605                    .ty
606                    .as_belongs_to_mut_unwrap()
607                    .pair = pair;
608            }
609        }
610
611        Ok(())
612    }
613
614    fn find_belongs_to_pair(
615        &self,
616        src: ModelId,
617        target: ModelId,
618        field_name: &str,
619    ) -> crate::Result<Option<FieldId>> {
620        let src_model = &self.models[&src];
621
622        let target = match self.models.get(&target) {
623            Some(target) => target,
624            None => {
625                return Err(crate::Error::invalid_schema(format!(
626                    "field `{}::{}` references a model that was not registered with the schema; \
627                     did you forget to register it with `Db::builder()`?",
628                    src_model.name().upper_camel_case(),
629                    field_name,
630                )));
631            }
632        };
633
634        // Find all BelongsTo relations that reference the model
635        let belongs_to: Vec<_> = target
636            .as_root_unwrap()
637            .fields
638            .iter()
639            .filter(|field| match &field.ty {
640                FieldTy::BelongsTo(rel) => rel.target == src,
641                _ => false,
642            })
643            .collect();
644
645        match &belongs_to[..] {
646            [field] => Ok(Some(field.id)),
647            [] => Ok(None),
648            _ => Err(crate::Error::invalid_schema(format!(
649                "model `{}` has more than one `BelongsTo` relation targeting `{}`; \
650                 disambiguate by adding `pair = <field>` on the paired `has_many`/`has_one` \
651                 field",
652                target.name().upper_camel_case(),
653                src_model.name().upper_camel_case(),
654            ))),
655        }
656    }
657
658    fn find_has_many_pair(
659        &mut self,
660        src: ModelId,
661        target: ModelId,
662        field_name: &str,
663    ) -> crate::Result<FieldId> {
664        if let Some(field_id) = self.find_belongs_to_pair(src, target, field_name)? {
665            return Ok(field_id);
666        }
667
668        Err(crate::Error::invalid_schema(format!(
669            "field `{}::{}` has no matching `BelongsTo` relation on the target model",
670            self.models[&src].name().upper_camel_case(),
671            field_name,
672        )))
673    }
674
675    /// Verify that `pair` — resolved from `#[has_many(pair = <field>)]` or
676    /// `#[has_one(pair = <field>)]` via `field_name_to_id` on the target —
677    /// names a `BelongsTo` field on `target` that points back at `src`.
678    fn validate_pair(
679        &self,
680        src: ModelId,
681        target: ModelId,
682        field_name: &str,
683        pair: FieldId,
684    ) -> crate::Result<()> {
685        let src_model = &self.models[&src];
686
687        let target_model = match self.models.get(&target) {
688            Some(target) => target,
689            None => {
690                return Err(crate::Error::invalid_schema(format!(
691                    "field `{}::{}` references a model that was not registered with the schema; \
692                     did you forget to register it with `Db::builder()`?",
693                    src_model.name().upper_camel_case(),
694                    field_name,
695                )));
696            }
697        };
698
699        if pair.model != target {
700            return Err(crate::Error::invalid_schema(format!(
701                "field `{}::{}` specifies a `pair` on a model other than its target `{}`",
702                src_model.name().upper_camel_case(),
703                field_name,
704                target_model.name().upper_camel_case(),
705            )));
706        }
707
708        let paired = &target_model.as_root_unwrap().fields[pair.index];
709        match &paired.ty {
710            FieldTy::BelongsTo(rel) if rel.target == src => Ok(()),
711            _ => Err(crate::Error::invalid_schema(format!(
712                "field `{}::{}` specifies `pair = {}`, but `{}::{}` is not a `BelongsTo` \
713                 targeting `{}`",
714                src_model.name().upper_camel_case(),
715                field_name,
716                paired.name.app_unwrap(),
717                target_model.name().upper_camel_case(),
718                paired.name.app_unwrap(),
719                src_model.name().upper_camel_case(),
720            ))),
721        }
722    }
723}
724
725fn eager_relation_target(field: &Field) -> Option<ModelId> {
726    if field.deferred {
727        return None;
728    }
729
730    field.relation_target_id()
731}