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}