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