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}