Skip to main content

toasty_core/schema/app/
model.rs

1use super::{Field, FieldId, FieldPrimitive, FieldTy, Index, Name, PrimaryKey};
2use crate::{Result, driver, stmt};
3use indexmap::IndexMap;
4use std::fmt;
5
6/// A model in the application schema.
7///
8/// Models come in three flavors:
9///
10/// - [`Model::Root`] -- a top-level model backed by its own database table.
11/// - [`Model::EmbeddedStruct`] -- a struct whose fields are flattened into a
12///   parent model's table.
13/// - [`Model::EmbeddedEnum`] -- an enum stored via a discriminant column plus
14///   optional per-variant data columns in the parent table.
15///
16/// # Examples
17///
18/// ```ignore
19/// use toasty_core::schema::app::{Model, Schema};
20///
21/// let schema: Schema = /* built from derive macros */;
22/// for model in schema.models() {
23///     if model.is_root() {
24///         println!("Root model: {}", model.name().upper_camel_case());
25///     }
26/// }
27/// ```
28#[derive(Debug, Clone)]
29pub enum Model {
30    /// A root model that maps to its own database table and can be queried
31    /// directly.
32    Root(ModelRoot),
33    /// An embedded struct whose fields are flattened into its parent model's
34    /// table.
35    EmbeddedStruct(EmbeddedStruct),
36    /// An embedded enum stored as a discriminant column (plus optional
37    /// per-variant data columns) in the parent table.
38    EmbeddedEnum(EmbeddedEnum),
39}
40
41/// An ordered collection of [`Model`] definitions.
42///
43/// `ModelSet` is the primary container used to hold all models in a schema.
44/// Models are stored in insertion order and can be iterated over by reference
45/// or by value.
46///
47/// # Examples
48///
49/// ```
50/// use toasty_core::schema::app::{Model, ModelSet};
51///
52/// let set = ModelSet::new();
53/// assert_eq!((&set).into_iter().len(), 0);
54/// ```
55#[derive(Debug, Clone, Default)]
56pub struct ModelSet {
57    models: IndexMap<ModelId, Model>,
58}
59
60impl ModelSet {
61    /// Creates an empty `ModelSet`.
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Returns the number of models in the set.
67    pub fn len(&self) -> usize {
68        self.models.len()
69    }
70
71    /// Returns `true` if the set contains no models.
72    pub fn is_empty(&self) -> bool {
73        self.models.is_empty()
74    }
75
76    /// Returns `true` if the set contains a model with the given ID.
77    pub fn contains(&self, id: ModelId) -> bool {
78        self.models.contains_key(&id)
79    }
80
81    /// Inserts a model into the set, keyed by its [`ModelId`].
82    ///
83    /// If a model with the same ID already exists, it is replaced.
84    pub fn add(&mut self, model: Model) {
85        self.models.insert(model.id(), model);
86    }
87}
88
89impl<'a> IntoIterator for &'a ModelSet {
90    type Item = &'a Model;
91    type IntoIter = indexmap::map::Values<'a, ModelId, Model>;
92
93    fn into_iter(self) -> Self::IntoIter {
94        self.models.values()
95    }
96}
97
98impl IntoIterator for ModelSet {
99    type Item = Model;
100    type IntoIter = ModelSetIntoIter;
101
102    fn into_iter(self) -> Self::IntoIter {
103        ModelSetIntoIter {
104            inner: self.models.into_iter(),
105        }
106    }
107}
108
109/// An owning iterator over the models in a [`ModelSet`].
110pub struct ModelSetIntoIter {
111    inner: indexmap::map::IntoIter<ModelId, Model>,
112}
113
114impl Iterator for ModelSetIntoIter {
115    type Item = Model;
116
117    fn next(&mut self) -> Option<Self::Item> {
118        self.inner.next().map(|(_, model)| model)
119    }
120
121    fn size_hint(&self) -> (usize, Option<usize>) {
122        self.inner.size_hint()
123    }
124}
125
126impl ExactSizeIterator for ModelSetIntoIter {}
127
128/// A root model backed by its own database table.
129///
130/// Root models have a primary key, may define indices, and are the only model
131/// kind that can be the target of relations. They are the main entities users
132/// interact with through Toasty's query API.
133///
134/// # Examples
135///
136/// ```ignore
137/// let root = model.as_root_unwrap();
138/// let pk_fields: Vec<_> = root.primary_key_fields().collect();
139/// ```
140#[derive(Debug, Clone)]
141pub struct ModelRoot {
142    /// Uniquely identifies this model within the schema.
143    pub id: ModelId,
144
145    /// The model's name.
146    pub name: Name,
147
148    /// All fields defined on this model.
149    pub fields: Vec<Field>,
150
151    /// The primary key definition. Root models always have a primary key.
152    pub primary_key: PrimaryKey,
153
154    /// The table this model maps to, before any builder-level prefix is
155    /// applied. Always set by the caller constructing the schema: `#[derive(Model)]`
156    /// derives the default (snake_case + pluralized) name at compile time, or
157    /// uses the explicit `#[table = "..."]` override.
158    pub table_name: String,
159
160    /// Secondary indices defined on this model.
161    pub indices: Vec<Index>,
162
163    /// The versionable field, if any. Points directly into `fields` to avoid scanning.
164    pub version_field: Option<FieldId>,
165}
166
167impl ModelRoot {
168    /// Builds a `SELECT` query that filters by this model's primary key using
169    /// the supplied `input` to resolve argument values.
170    pub fn find_by_id(&self, mut input: impl stmt::Input) -> stmt::Query {
171        let filter = match &self.primary_key.fields[..] {
172            [pk_field] => stmt::Expr::eq(
173                stmt::Expr::ref_self_field(pk_field),
174                input
175                    .resolve_arg(&0.into(), &stmt::Projection::identity())
176                    .unwrap(),
177            ),
178            pk_fields => stmt::Expr::and_from_vec(
179                pk_fields
180                    .iter()
181                    .enumerate()
182                    .map(|(i, pk_field)| {
183                        stmt::Expr::eq(
184                            stmt::Expr::ref_self_field(pk_field),
185                            input
186                                .resolve_arg(&i.into(), &stmt::Projection::identity())
187                                .unwrap(),
188                        )
189                    })
190                    .collect(),
191            ),
192        };
193
194        stmt::Query::new_select(self.id, filter)
195    }
196
197    /// Iterate over the fields used for the model's primary key.
198    pub fn primary_key_fields(&self) -> impl ExactSizeIterator<Item = &'_ Field> {
199        self.primary_key
200            .fields
201            .iter()
202            .map(|pk_field| &self.fields[pk_field.index])
203    }
204
205    /// Returns the versionable field, if one is defined on this model.
206    pub fn version_field(&self) -> Option<&Field> {
207        self.version_field.map(|id| &self.fields[id.index])
208    }
209
210    /// Looks up a field by its application-level name.
211    ///
212    /// Returns `None` if no field with that name exists on this model.
213    pub fn field_by_name(&self, name: &str) -> Option<&Field> {
214        self.fields
215            .iter()
216            .find(|field| field.name.app.as_deref() == Some(name))
217    }
218
219    pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
220        for field in &self.fields {
221            field.verify(db)?;
222
223            // Multi-step (`via`) relations lower to nested `IN` subqueries.
224            // Only SQL drivers can evaluate them today; key-value drivers
225            // would need a separate per-step batched fetch strategy that
226            // is not yet implemented.
227            if matches!(&field.ty, FieldTy::Via(_)) && !db.sql() {
228                return Err(crate::Error::invalid_schema(format!(
229                    "field `{}::{}` declares a multi-step `via` relation, which \
230                     requires a SQL-capable driver; the configured driver does not \
231                     support SQL",
232                    self.name.upper_camel_case(),
233                    field.name,
234                )));
235            }
236        }
237        Ok(())
238    }
239}
240
241/// An embedded struct model whose fields are flattened into its parent model's
242/// database table.
243///
244/// Embedded structs do not have their own table or primary key. Their fields
245/// become additional columns in the parent table. Indices declared on an
246/// embedded struct's fields are propagated to physical DB indices on the parent
247/// table.
248///
249/// # Examples
250///
251/// ```ignore
252/// let embedded = model.as_embedded_struct_unwrap();
253/// for field in &embedded.fields {
254///     println!("  embedded field: {}", field.name);
255/// }
256/// ```
257#[derive(Debug, Clone)]
258pub struct EmbeddedStruct {
259    /// Uniquely identifies this model within the schema.
260    pub id: ModelId,
261
262    /// The model's name.
263    pub name: Name,
264
265    /// Fields contained by this embedded struct.
266    pub fields: Vec<Field>,
267
268    /// Indices defined on this embedded struct's fields.
269    ///
270    /// These reference fields within this embedded struct (not the parent
271    /// model). The schema builder propagates them to physical DB indices on
272    /// the parent table's flattened columns.
273    pub indices: Vec<Index>,
274}
275
276impl EmbeddedStruct {
277    pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
278        for field in &self.fields {
279            field.verify(db)?;
280        }
281        Ok(())
282    }
283}
284
285/// An embedded enum model stored in the parent table via a discriminant column
286/// and optional per-variant data columns.
287///
288/// The discriminant column holds a value (integer or string) identifying the active variant.
289/// Variants may optionally carry data fields, which are stored as additional
290/// nullable columns in the parent table.
291///
292/// # Examples
293///
294/// ```ignore
295/// let ee = model.as_embedded_enum_unwrap();
296/// for variant in &ee.variants {
297///     println!("variant {} = {}", variant.name.upper_camel_case(), variant.discriminant);
298/// }
299/// ```
300#[derive(Debug, Clone)]
301pub struct EmbeddedEnum {
302    /// Uniquely identifies this model within the schema.
303    pub id: ModelId,
304
305    /// The model's name.
306    pub name: Name,
307
308    /// The primitive type used for the discriminant column.
309    pub discriminant: FieldPrimitive,
310
311    /// The enum's variants.
312    pub variants: Vec<EnumVariant>,
313
314    /// All fields across all variants, with global indices. Each field's
315    /// [`variant`](Field::variant) identifies which variant it belongs to.
316    pub fields: Vec<Field>,
317
318    /// Indices defined on this embedded enum's variant fields.
319    ///
320    /// These reference fields within this embedded enum (not the parent
321    /// model). The schema builder propagates them to physical DB indices on
322    /// the parent table's flattened columns.
323    pub indices: Vec<Index>,
324}
325
326/// One variant of an [`EmbeddedEnum`].
327///
328/// Each variant has a name and a discriminant value (integer or string) that is
329/// stored in the database to identify which variant is active.
330#[derive(Debug, Clone)]
331pub struct EnumVariant {
332    /// The Rust variant name.
333    pub name: Name,
334
335    /// The discriminant value stored in the database column.
336    /// Typically `Value::I64` for integer discriminants or `Value::String` for
337    /// string discriminants.
338    pub discriminant: stmt::Value,
339}
340
341impl EmbeddedEnum {
342    /// Returns true if at least one variant carries data fields.
343    pub fn has_data_variants(&self) -> bool {
344        !self.fields.is_empty()
345    }
346
347    /// Returns fields belonging to a specific variant.
348    pub fn variant_fields(&self, variant_index: usize) -> impl Iterator<Item = &Field> {
349        let variant_id = VariantId {
350            model: self.id,
351            index: variant_index,
352        };
353        self.fields
354            .iter()
355            .filter(move |f| f.variant == Some(variant_id))
356    }
357
358    pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
359        for field in &self.fields {
360            field.verify(db)?;
361        }
362        Ok(())
363    }
364}
365
366/// Uniquely identifies a [`Model`] within a [`Schema`](super::Schema).
367///
368/// `ModelId` wraps a `usize` index into the schema's model map. It is `Copy`
369/// and can be used as a key for lookups.
370///
371/// # Examples
372///
373/// ```
374/// use toasty_core::schema::app::ModelId;
375///
376/// let id = ModelId(0);
377/// let field_id = id.field(2);
378/// assert_eq!(field_id.model, id);
379/// assert_eq!(field_id.index, 2);
380/// ```
381#[derive(Copy, Clone, Eq, PartialEq, Hash)]
382#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
383pub struct ModelId(pub usize);
384
385impl Model {
386    /// Returns this model's [`ModelId`].
387    pub fn id(&self) -> ModelId {
388        match self {
389            Model::Root(root) => root.id,
390            Model::EmbeddedStruct(embedded) => embedded.id,
391            Model::EmbeddedEnum(e) => e.id,
392        }
393    }
394
395    /// Returns a reference to this model's [`Name`].
396    pub fn name(&self) -> &Name {
397        match self {
398            Model::Root(root) => &root.name,
399            Model::EmbeddedStruct(embedded) => &embedded.name,
400            Model::EmbeddedEnum(e) => &e.name,
401        }
402    }
403
404    /// Returns true if this is a root model (has a table and primary key)
405    pub fn is_root(&self) -> bool {
406        matches!(self, Model::Root(_))
407    }
408
409    /// Returns true if this is an embedded model (flattened into parent)
410    pub fn is_embedded(&self) -> bool {
411        matches!(self, Model::EmbeddedStruct(_) | Model::EmbeddedEnum(_))
412    }
413
414    /// Returns the inner [`ModelRoot`] if this is a root model.
415    pub fn as_root(&self) -> Option<&ModelRoot> {
416        match self {
417            Model::Root(root) => Some(root),
418            _ => None,
419        }
420    }
421
422    /// The model's fields. For an [`EmbeddedEnum`] these are the flattened
423    /// variant fields ([`EmbeddedEnum::fields`]), not the variants themselves.
424    pub fn fields(&self) -> &[Field] {
425        match self {
426            Model::Root(root) => &root.fields,
427            Model::EmbeddedStruct(embedded) => &embedded.fields,
428            Model::EmbeddedEnum(e) => &e.fields,
429        }
430    }
431
432    /// Returns a reference to the root model data.
433    ///
434    /// # Panics
435    ///
436    /// Panics if this is not a [`Model::Root`].
437    pub fn as_root_unwrap(&self) -> &ModelRoot {
438        match self {
439            Model::Root(root) => root,
440            Model::EmbeddedStruct(_) => panic!("expected root model, found embedded struct"),
441            Model::EmbeddedEnum(_) => panic!("expected root model, found embedded enum"),
442        }
443    }
444
445    /// Returns a mutable reference to the root model data.
446    ///
447    /// # Panics
448    ///
449    /// Panics if this is not a [`Model::Root`].
450    pub fn as_root_mut_unwrap(&mut self) -> &mut ModelRoot {
451        match self {
452            Model::Root(root) => root,
453            Model::EmbeddedStruct(_) => panic!("expected root model, found embedded struct"),
454            Model::EmbeddedEnum(_) => panic!("expected root model, found embedded enum"),
455        }
456    }
457
458    /// Returns a reference to the embedded struct data.
459    ///
460    /// # Panics
461    ///
462    /// Panics if this is not a [`Model::EmbeddedStruct`].
463    pub fn as_embedded_struct_unwrap(&self) -> &EmbeddedStruct {
464        match self {
465            Model::EmbeddedStruct(embedded) => embedded,
466            Model::Root(_) => panic!("expected embedded struct, found root model"),
467            Model::EmbeddedEnum(_) => panic!("expected embedded struct, found embedded enum"),
468        }
469    }
470
471    /// Returns a reference to the embedded enum data.
472    ///
473    /// # Panics
474    ///
475    /// Panics if this is not a [`Model::EmbeddedEnum`].
476    pub fn as_embedded_enum_unwrap(&self) -> &EmbeddedEnum {
477        match self {
478            Model::EmbeddedEnum(e) => e,
479            Model::Root(_) => panic!("expected embedded enum, found root model"),
480            Model::EmbeddedStruct(_) => panic!("expected embedded enum, found embedded struct"),
481        }
482    }
483
484    pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
485        match self {
486            Model::Root(root) => root.verify(db),
487            Model::EmbeddedStruct(embedded) => embedded.verify(db),
488            Model::EmbeddedEnum(e) => e.verify(db),
489        }
490    }
491}
492
493/// Identifies a specific variant within an [`EmbeddedEnum`] model.
494///
495/// # Examples
496///
497/// ```
498/// use toasty_core::schema::app::{ModelId, VariantId};
499///
500/// let variant_id = VariantId { model: ModelId(1), index: 0 };
501/// assert_eq!(variant_id.model, ModelId(1));
502/// assert_eq!(variant_id.index, 0);
503/// ```
504#[derive(Copy, Clone, PartialEq, Eq, Hash)]
505pub struct VariantId {
506    /// The enum model this variant belongs to.
507    pub model: ModelId,
508    /// Index of the variant within `EmbeddedEnum::variants`.
509    pub index: usize,
510}
511
512impl fmt::Debug for VariantId {
513    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
514        write!(fmt, "VariantId({}/{})", self.model.0, self.index)
515    }
516}
517
518impl ModelId {
519    /// Create a `FieldId` representing the current model's field at index
520    /// `index`.
521    pub const fn field(self, index: usize) -> FieldId {
522        FieldId { model: self, index }
523    }
524
525    pub(crate) const fn placeholder() -> Self {
526        Self(usize::MAX)
527    }
528}
529
530impl From<&Self> for ModelId {
531    fn from(src: &Self) -> Self {
532        *src
533    }
534}
535
536impl From<&mut Self> for ModelId {
537    fn from(src: &mut Self) -> Self {
538        *src
539    }
540}
541
542impl From<&Model> for ModelId {
543    fn from(value: &Model) -> Self {
544        value.id()
545    }
546}
547
548impl From<&ModelRoot> for ModelId {
549    fn from(value: &ModelRoot) -> Self {
550        value.id
551    }
552}
553
554impl fmt::Debug for ModelId {
555    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
556        write!(fmt, "ModelId({})", self.0)
557    }
558}