toasty_core/schema/app/
field.rs

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