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}