Skip to main content

toasty_core/schema/app/
field.rs

1mod primitive;
2pub use primitive::{FieldPrimitive, SerializeFormat};
3
4use super::{
5    AutoStrategy, BelongsTo, Constraint, Embedded, Has, Model, ModelId, Schema, VariantId, Via,
6};
7use crate::{Result, driver, schema::Name, stmt};
8use std::fmt;
9
10/// A single field within a model.
11///
12/// Fields are the building blocks of a model's data structure. Each field has a
13/// unique [`FieldId`], a name, a type (primitive, embedded, or relation), and
14/// metadata such as nullability, primary-key membership, auto-population
15/// strategy, and validation constraints.
16///
17/// # Examples
18///
19/// ```ignore
20/// use toasty_core::schema::app::{Field, Schema};
21///
22/// let schema: Schema = /* ... */;
23/// let model = schema.model(model_id).as_root_unwrap();
24/// for field in &model.fields {
25///     println!("{}: primary_key={}", field.name, field.primary_key);
26/// }
27/// ```
28#[derive(Debug, Clone)]
29pub struct Field {
30    /// Uniquely identifies this field within its containing model.
31    pub id: FieldId,
32
33    /// The field's application and storage names.
34    pub name: FieldName,
35
36    /// The field's type: primitive, embedded, or a relation variant.
37    pub ty: FieldTy,
38
39    /// `true` if this field accepts `None` / `NULL` values.
40    pub nullable: bool,
41
42    /// `true` if this field is part of the model's primary key.
43    pub primary_key: bool,
44
45    /// If set, Toasty automatically populates this field on insert.
46    pub auto: Option<AutoStrategy>,
47
48    /// If `true`, this field tracks an OCC version counter.
49    pub versionable: bool,
50
51    /// If `true`, this field is excluded from default queries and must be
52    /// loaded on demand via the per-field `.exec()` method.
53    pub deferred: bool,
54
55    /// Validation constraints applied to this field's values.
56    pub constraints: Vec<Constraint>,
57
58    /// If this field belongs to an enum variant, identifies that variant.
59    /// `None` for fields on root models and embedded structs.
60    pub variant: Option<VariantId>,
61
62    /// The shared logical field this variant field participates in, from
63    /// `#[shared(<ident>)]`. Variant fields declaring the same identifier are
64    /// backed by a single shared column. `None` for fields that own their
65    /// column outright (including all fields outside enum variants).
66    pub shared: Option<Name>,
67}
68
69/// Uniquely identifies a [`Field`] within a schema.
70///
71/// Composed of the owning model's [`ModelId`] and a positional index into that
72/// model's field list.
73///
74/// # Examples
75///
76/// ```
77/// use toasty_core::schema::app::{FieldId, ModelId};
78///
79/// let id = FieldId { model: ModelId(0), index: 2 };
80/// assert_eq!(id.index, 2);
81/// ```
82#[derive(Copy, Clone, PartialEq, Eq, Hash)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84pub struct FieldId {
85    /// The model this field belongs to.
86    pub model: ModelId,
87    /// Positional index within the model's field list.
88    pub index: usize,
89}
90
91/// The name of a field, with separate application and storage representations.
92///
93/// The `app` field is the Rust-facing name (e.g., `user_name`). It is
94/// `Option<String>` to support unnamed (tuple) fields in the future; for now it
95/// is always `Some`. The optional `storage` field overrides the column name used
96/// in the database; when `None`, `app` is used as the storage name.
97///
98/// # Examples
99///
100/// ```
101/// use toasty_core::schema::app::FieldName;
102///
103/// let name = FieldName {
104///     app: Some("user_name".to_string()),
105///     storage: Some("username".to_string()),
106/// };
107/// assert_eq!(name.storage_name(), Some("username"));
108///
109/// let default_name = FieldName {
110///     app: Some("email".to_string()),
111///     storage: None,
112/// };
113/// assert_eq!(default_name.storage_name(), Some("email"));
114/// ```
115#[derive(Debug, Clone)]
116pub struct FieldName {
117    /// The application-level (Rust) name of the field. `None` for unnamed
118    /// (tuple) fields.
119    pub app: Option<String>,
120    /// Optional override for the database column name. When `None`, `app` is
121    /// used.
122    pub storage: Option<String>,
123}
124
125impl FieldName {
126    /// Returns the application-level (Rust) name of this field.
127    ///
128    /// This is a convenience accessor that unwraps the `app` field, which is
129    /// `Option<String>` to support unnamed (tuple) fields. Most fields have an
130    /// application name, and this method provides direct access without manual
131    /// unwrapping.
132    ///
133    /// # Panics
134    ///
135    /// Panics if `app` is `None` (i.e., the field is unnamed).
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use toasty_core::schema::app::FieldName;
141    ///
142    /// let name = FieldName {
143    ///     app: Some("user_name".to_string()),
144    ///     storage: None,
145    /// };
146    /// assert_eq!(name.app_unwrap(), "user_name");
147    /// ```
148    #[track_caller]
149    pub fn app_unwrap(&self) -> &str {
150        self.app.as_deref().unwrap()
151    }
152
153    /// Returns the storage (database column) name for this field, if one can
154    /// be determined.
155    ///
156    /// Returns `storage` if set, otherwise falls back to `app`. Returns `None`
157    /// only when both fields are `None`.
158    pub fn storage_name(&self) -> Option<&str> {
159        self.storage.as_deref().or(self.app.as_deref())
160    }
161
162    /// Returns the storage (database column) name for this field.
163    ///
164    /// This is a convenience wrapper around [`storage_name`](FieldName::storage_name)
165    /// for callers that expect a name to always be present.
166    ///
167    /// # Panics
168    ///
169    /// Panics if both `storage` and `app` are `None`.
170    pub fn storage_name_unwrap(&self) -> &str {
171        self.storage_name()
172            .expect("must specify app name or storage name")
173    }
174}
175
176impl fmt::Display for FieldName {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        f.write_str(self.app.as_deref().unwrap_or("<unnamed>"))
179    }
180}
181
182/// The type of a [`Field`], distinguishing primitives, embedded types, and
183/// relation variants.
184///
185/// # Examples
186///
187/// ```
188/// use toasty_core::schema::app::{FieldPrimitive, FieldTy};
189/// use toasty_core::stmt::Type;
190///
191/// let ty = FieldTy::Primitive(FieldPrimitive {
192///     ty: Type::String,
193///     storage_ty: None,
194///     serialize: None,
195/// });
196/// assert!(ty.is_primitive());
197/// assert!(!ty.is_relation());
198/// ```
199#[derive(Clone)]
200pub enum FieldTy {
201    /// A primitive (scalar) field backed by a single column.
202    Primitive(FieldPrimitive),
203    /// An embedded struct or enum, flattened into the parent table.
204    Embedded(Embedded),
205    /// The owning side of a relationship (stores the foreign key).
206    BelongsTo(BelongsTo),
207    /// The inverse side of a relationship.
208    Has(Has),
209    /// A relation reached by following a path of existing relations.
210    Via(Via),
211}
212
213impl Field {
214    /// Returns this field's [`FieldId`].
215    pub fn id(&self) -> FieldId {
216        self.id
217    }
218
219    /// Returns a reference to this field's [`FieldName`].
220    pub fn name(&self) -> &FieldName {
221        &self.name
222    }
223
224    /// Returns a reference to this field's [`FieldTy`].
225    pub fn ty(&self) -> &FieldTy {
226        &self.ty
227    }
228
229    /// Returns `true` if this field is nullable.
230    pub fn nullable(&self) -> bool {
231        self.nullable
232    }
233
234    /// Returns `true` if this field is part of the primary key.
235    pub fn primary_key(&self) -> bool {
236        self.primary_key
237    }
238
239    /// Returns the auto-population strategy, if one is configured.
240    pub fn auto(&self) -> Option<&AutoStrategy> {
241        self.auto.as_ref()
242    }
243
244    /// Returns `true` if this field uses auto-increment for value generation.
245    pub fn is_auto_increment(&self) -> bool {
246        self.auto().map(|auto| auto.is_increment()).unwrap_or(false)
247    }
248
249    /// Returns `true` if this field tracks an OCC version counter.
250    pub fn is_versionable(&self) -> bool {
251        self.versionable
252    }
253
254    /// Returns `true` if this field is a relation (`BelongsTo`, `Has`, or
255    /// `Via`).
256    pub fn is_relation(&self) -> bool {
257        self.ty.is_relation()
258    }
259
260    /// Returns a fully qualified name for the field.
261    pub fn full_name(&self, schema: &Schema) -> Option<String> {
262        self.name.app.as_ref().map(|app_name| {
263            let model = schema.model(self.id.model);
264            format!("{}::{}", model.name().upper_camel_case(), app_name)
265        })
266    }
267
268    /// If the field is a relation, return the relation's target ModelId.
269    pub fn relation_target_id(&self) -> Option<ModelId> {
270        match &self.ty {
271            FieldTy::BelongsTo(belongs_to) => Some(belongs_to.target),
272            FieldTy::Has(has) => Some(has.target),
273            FieldTy::Via(via) => Some(via.target),
274            _ => None,
275        }
276    }
277
278    /// If the field is a relation, return the target of the relation.
279    pub fn relation_target<'a>(&self, schema: &'a Schema) -> Option<&'a Model> {
280        self.relation_target_id().map(|id| schema.model(id))
281    }
282
283    /// Returns the expression type this field evaluates to.
284    ///
285    /// For primitives this is the scalar type; for relations and embedded types
286    /// it is the type visible to the application layer.
287    pub fn expr_ty(&self) -> &stmt::Type {
288        match &self.ty {
289            FieldTy::Primitive(primitive) => &primitive.ty,
290            FieldTy::Embedded(embedded) => &embedded.expr_ty,
291            FieldTy::BelongsTo(belongs_to) => &belongs_to.expr_ty,
292            FieldTy::Has(has) => &has.expr_ty,
293            FieldTy::Via(via) => &via.expr_ty,
294        }
295    }
296
297    /// Returns the paired relation field, if this field is a relation.
298    ///
299    /// For `BelongsTo` this returns the inverse `Has` relation (if linked).
300    /// For `Has` this returns the paired `BelongsTo`.
301    /// Returns `None` for primitive and embedded fields, and for multi-step
302    /// (`via`) relations, which have no pair.
303    pub fn pair(&self) -> Option<FieldId> {
304        match &self.ty {
305            FieldTy::Primitive(_) => None,
306            FieldTy::Embedded(_) => None,
307            FieldTy::BelongsTo(belongs_to) => belongs_to.pair,
308            FieldTy::Has(has) => Some(has.pair_id),
309            FieldTy::Via(_) => None,
310        }
311    }
312
313    pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
314        if let FieldTy::Primitive(primitive) = &self.ty
315            && let Some(storage_ty) = &primitive.storage_ty
316        {
317            storage_ty.verify(db)?;
318        }
319
320        Ok(())
321    }
322}
323
324impl FieldTy {
325    /// Returns `true` if this is a [`FieldTy::Primitive`].
326    pub fn is_primitive(&self) -> bool {
327        matches!(self, Self::Primitive(..))
328    }
329
330    /// Returns the inner [`FieldPrimitive`] if this is a primitive field.
331    pub fn as_primitive(&self) -> Option<&FieldPrimitive> {
332        match self {
333            Self::Primitive(primitive) => Some(primitive),
334            _ => None,
335        }
336    }
337
338    /// Returns the inner [`FieldPrimitive`], panicking if this is not a
339    /// primitive field.
340    ///
341    /// # Panics
342    ///
343    /// Panics if `self` is not [`FieldTy::Primitive`].
344    #[track_caller]
345    pub fn as_primitive_unwrap(&self) -> &FieldPrimitive {
346        match self {
347            Self::Primitive(simple) => simple,
348            _ => panic!("expected simple field, but was {self:?}"),
349        }
350    }
351
352    /// Returns a mutable reference to the inner [`FieldPrimitive`], panicking
353    /// if this is not a primitive field.
354    ///
355    /// # Panics
356    ///
357    /// Panics if `self` is not [`FieldTy::Primitive`].
358    #[track_caller]
359    pub fn as_primitive_mut_unwrap(&mut self) -> &mut FieldPrimitive {
360        match self {
361            Self::Primitive(simple) => simple,
362            _ => panic!("expected simple field, but was {self:?}"),
363        }
364    }
365
366    /// Returns `true` if this is a [`FieldTy::Embedded`].
367    pub fn is_embedded(&self) -> bool {
368        matches!(self, Self::Embedded(..))
369    }
370
371    /// Returns the inner [`Embedded`] if this is an embedded field.
372    pub fn as_embedded(&self) -> Option<&Embedded> {
373        match self {
374            Self::Embedded(embedded) => Some(embedded),
375            _ => None,
376        }
377    }
378
379    /// Returns the inner [`Embedded`], panicking if this is not an embedded
380    /// field.
381    ///
382    /// # Panics
383    ///
384    /// Panics if `self` is not [`FieldTy::Embedded`].
385    #[track_caller]
386    pub fn as_embedded_unwrap(&self) -> &Embedded {
387        match self {
388            Self::Embedded(embedded) => embedded,
389            _ => panic!("expected embedded field, but was {self:?}"),
390        }
391    }
392
393    /// Returns a mutable reference to the inner [`Embedded`], panicking if
394    /// this is not an embedded field.
395    ///
396    /// # Panics
397    ///
398    /// Panics if `self` is not [`FieldTy::Embedded`].
399    #[track_caller]
400    pub fn as_embedded_mut_unwrap(&mut self) -> &mut Embedded {
401        match self {
402            Self::Embedded(embedded) => embedded,
403            _ => panic!("expected embedded field, but was {self:?}"),
404        }
405    }
406
407    /// Returns `true` if this is a relation type (`BelongsTo`, `Has`, or
408    /// `Via`).
409    pub fn is_relation(&self) -> bool {
410        matches!(self, Self::BelongsTo(..) | Self::Has(..) | Self::Via(..))
411    }
412
413    /// Returns `true` if this is a [`FieldTy::Has`] relation.
414    pub fn is_has_n(&self) -> bool {
415        matches!(self, Self::Has(..))
416    }
417
418    /// Returns the inner [`Has`] if this is a has field.
419    pub fn as_has(&self) -> Option<&Has> {
420        match self {
421            Self::Has(has) => Some(has),
422            _ => None,
423        }
424    }
425
426    /// Returns the inner [`Has`], panicking if this is not a has field.
427    ///
428    /// # Panics
429    ///
430    /// Panics if `self` is not [`FieldTy::Has`].
431    #[track_caller]
432    pub fn as_has_unwrap(&self) -> &Has {
433        match self {
434            Self::Has(has) => has,
435            _ => panic!("expected field to be `Has`, but was {self:?}"),
436        }
437    }
438
439    /// Returns a mutable reference to the inner [`Has`], panicking if this is
440    /// not a has field.
441    ///
442    /// # Panics
443    ///
444    /// Panics if `self` is not [`FieldTy::Has`].
445    #[track_caller]
446    pub fn as_has_mut_unwrap(&mut self) -> &mut Has {
447        match self {
448            Self::Has(has) => has,
449            _ => panic!("expected field to be `Has`, but was {self:?}"),
450        }
451    }
452
453    /// Returns `true` if this is a many-valued [`FieldTy::Has`].
454    pub fn is_has_many(&self) -> bool {
455        self.as_has().is_some_and(Has::is_many)
456    }
457
458    /// Returns the inner [`Has`] if this is a many-valued has field.
459    pub fn as_has_many(&self) -> Option<&Has> {
460        match self {
461            Self::Has(has) if has.is_many() => Some(has),
462            _ => None,
463        }
464    }
465
466    /// Returns the inner [`Has`], panicking if this is not a many-valued has
467    /// field.
468    ///
469    /// # Panics
470    ///
471    /// Panics if `self` is not a many-valued [`FieldTy::Has`].
472    #[track_caller]
473    pub fn as_has_many_unwrap(&self) -> &Has {
474        self.as_has_many()
475            .unwrap_or_else(|| panic!("expected field to be `HasMany`, but was {self:?}"))
476    }
477
478    /// Returns a mutable reference to the inner [`Has`], panicking if this is
479    /// not a many-valued has field.
480    ///
481    /// # Panics
482    ///
483    /// Panics if `self` is not a many-valued [`FieldTy::Has`].
484    #[track_caller]
485    pub fn as_has_many_mut_unwrap(&mut self) -> &mut Has {
486        if !self.is_has_many() {
487            panic!("expected field to be `HasMany`, but was {self:?}");
488        }
489        match self {
490            Self::Has(has) => has,
491            _ => unreachable!(),
492        }
493    }
494
495    /// Returns the inner [`Has`] if this is a one-valued has field.
496    pub fn as_has_one(&self) -> Option<&Has> {
497        match self {
498            Self::Has(has) if has.is_one() => Some(has),
499            _ => None,
500        }
501    }
502
503    /// Returns `true` if this is a one-valued [`FieldTy::Has`].
504    pub fn is_has_one(&self) -> bool {
505        self.as_has().is_some_and(Has::is_one)
506    }
507
508    /// Returns the inner [`Has`], panicking if this is not a one-valued has
509    /// field.
510    ///
511    /// # Panics
512    ///
513    /// Panics if `self` is not a one-valued [`FieldTy::Has`].
514    #[track_caller]
515    pub fn as_has_one_unwrap(&self) -> &Has {
516        self.as_has_one()
517            .unwrap_or_else(|| panic!("expected field to be `HasOne`, but it was {self:?}"))
518    }
519
520    /// Returns a mutable reference to the inner [`Has`], panicking if this is
521    /// not a one-valued has field.
522    ///
523    /// # Panics
524    ///
525    /// Panics if `self` is not a one-valued [`FieldTy::Has`].
526    #[track_caller]
527    pub fn as_has_one_mut_unwrap(&mut self) -> &mut Has {
528        if !self.is_has_one() {
529            panic!("expected field to be `HasOne`, but it was {self:?}");
530        }
531        match self {
532            Self::Has(has) => has,
533            _ => unreachable!(),
534        }
535    }
536
537    /// Returns `true` if this is a [`FieldTy::BelongsTo`].
538    pub fn is_belongs_to(&self) -> bool {
539        matches!(self, Self::BelongsTo(..))
540    }
541
542    /// Returns the inner [`BelongsTo`] if this is a belongs-to field.
543    pub fn as_belongs_to(&self) -> Option<&BelongsTo> {
544        match self {
545            Self::BelongsTo(belongs_to) => Some(belongs_to),
546            _ => None,
547        }
548    }
549
550    /// Returns the inner [`BelongsTo`], panicking if this is not a belongs-to
551    /// field.
552    ///
553    /// # Panics
554    ///
555    /// Panics if `self` is not [`FieldTy::BelongsTo`].
556    #[track_caller]
557    pub fn as_belongs_to_unwrap(&self) -> &BelongsTo {
558        match self {
559            Self::BelongsTo(belongs_to) => belongs_to,
560            _ => panic!("expected field to be `BelongsTo`, but was {self:?}"),
561        }
562    }
563
564    /// Returns a mutable reference to the inner [`BelongsTo`], panicking if
565    /// this is not a belongs-to field.
566    ///
567    /// # Panics
568    ///
569    /// Panics if `self` is not [`FieldTy::BelongsTo`].
570    #[track_caller]
571    pub fn as_belongs_to_mut_unwrap(&mut self) -> &mut BelongsTo {
572        match self {
573            Self::BelongsTo(belongs_to) => belongs_to,
574            _ => panic!("expected field to be `BelongsTo`, but was {self:?}"),
575        }
576    }
577}
578
579impl fmt::Debug for FieldTy {
580    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
581        match self {
582            Self::Primitive(ty) => ty.fmt(fmt),
583            Self::Embedded(ty) => ty.fmt(fmt),
584            Self::BelongsTo(ty) => ty.fmt(fmt),
585            Self::Has(ty) => ty.fmt(fmt),
586            Self::Via(ty) => ty.fmt(fmt),
587        }
588    }
589}
590
591impl FieldId {
592    pub(crate) fn placeholder() -> Self {
593        Self {
594            model: ModelId::placeholder(),
595            index: usize::MAX,
596        }
597    }
598
599    pub(crate) fn is_placeholder(&self) -> bool {
600        self.index == usize::MAX && self.model == ModelId::placeholder()
601    }
602}
603
604impl From<&Self> for FieldId {
605    fn from(val: &Self) -> Self {
606        *val
607    }
608}
609
610impl From<&Field> for FieldId {
611    fn from(val: &Field) -> Self {
612        val.id
613    }
614}
615
616impl From<FieldId> for usize {
617    fn from(val: FieldId) -> Self {
618        val.index
619    }
620}
621
622impl fmt::Debug for FieldId {
623    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
624        write!(fmt, "FieldId({}/{})", self.model.0, self.index)
625    }
626}