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