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