Skip to main content

toasty_core/schema/app/
field.rs

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