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}