Skip to main content

toasty_core/schema/app/
model.rs

1use super::{Field, FieldId, FieldPrimitive, Index, Name, PrimaryKey};
2use crate::{Result, driver, stmt};
3use std::fmt;
4
5/// A model in the application schema.
6///
7/// Models come in three flavors:
8///
9/// - [`Model::Root`] -- a top-level model backed by its own database table.
10/// - [`Model::EmbeddedStruct`] -- a struct whose fields are flattened into a
11///   parent model's table.
12/// - [`Model::EmbeddedEnum`] -- an enum stored via a discriminant column plus
13///   optional per-variant data columns in the parent table.
14///
15/// # Examples
16///
17/// ```ignore
18/// use toasty_core::schema::app::{Model, Schema};
19///
20/// let schema: Schema = /* built from derive macros */;
21/// for model in schema.models() {
22///     if model.is_root() {
23///         println!("Root model: {}", model.name().upper_camel_case());
24///     }
25/// }
26/// ```
27#[derive(Debug, Clone)]
28pub enum Model {
29    /// A root model that maps to its own database table and can be queried
30    /// directly.
31    Root(ModelRoot),
32    /// An embedded struct whose fields are flattened into its parent model's
33    /// table.
34    EmbeddedStruct(EmbeddedStruct),
35    /// An embedded enum stored as a discriminant column (plus optional
36    /// per-variant data columns) in the parent table.
37    EmbeddedEnum(EmbeddedEnum),
38}
39
40/// A root model backed by its own database table.
41///
42/// Root models have a primary key, may define indices, and are the only model
43/// kind that can be the target of relations. They are the main entities users
44/// interact with through Toasty's query API.
45///
46/// # Examples
47///
48/// ```ignore
49/// let root = model.as_root_unwrap();
50/// let pk_fields: Vec<_> = root.primary_key_fields().collect();
51/// ```
52#[derive(Debug, Clone)]
53pub struct ModelRoot {
54    /// Uniquely identifies this model within the schema.
55    pub id: ModelId,
56
57    /// The model's name.
58    pub name: Name,
59
60    /// All fields defined on this model.
61    pub fields: Vec<Field>,
62
63    /// The primary key definition. Root models always have a primary key.
64    pub primary_key: PrimaryKey,
65
66    /// Optional explicit table name. When `None`, a name is derived from the
67    /// model name.
68    pub table_name: Option<String>,
69
70    /// Secondary indices defined on this model.
71    pub indices: Vec<Index>,
72}
73
74impl ModelRoot {
75    /// Builds a `SELECT` query that filters by this model's primary key using
76    /// the supplied `input` to resolve argument values.
77    pub fn find_by_id(&self, mut input: impl stmt::Input) -> stmt::Query {
78        let filter = match &self.primary_key.fields[..] {
79            [pk_field] => stmt::Expr::eq(
80                stmt::Expr::ref_self_field(pk_field),
81                input
82                    .resolve_arg(&0.into(), &stmt::Projection::identity())
83                    .unwrap(),
84            ),
85            pk_fields => stmt::Expr::and_from_vec(
86                pk_fields
87                    .iter()
88                    .enumerate()
89                    .map(|(i, pk_field)| {
90                        stmt::Expr::eq(
91                            stmt::Expr::ref_self_field(pk_field),
92                            input
93                                .resolve_arg(&i.into(), &stmt::Projection::identity())
94                                .unwrap(),
95                        )
96                    })
97                    .collect(),
98            ),
99        };
100
101        stmt::Query::new_select(self.id, filter)
102    }
103
104    /// Iterate over the fields used for the model's primary key.
105    pub fn primary_key_fields(&self) -> impl ExactSizeIterator<Item = &'_ Field> {
106        self.primary_key
107            .fields
108            .iter()
109            .map(|pk_field| &self.fields[pk_field.index])
110    }
111
112    /// Looks up a field by its application-level name.
113    ///
114    /// Returns `None` if no field with that name exists on this model.
115    pub fn field_by_name(&self, name: &str) -> Option<&Field> {
116        self.fields.iter().find(|field| field.name.app_name == name)
117    }
118
119    pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
120        for field in &self.fields {
121            field.verify(db)?;
122        }
123        Ok(())
124    }
125}
126
127/// An embedded struct model whose fields are flattened into its parent model's
128/// database table.
129///
130/// Embedded structs do not have their own table or primary key. Their fields
131/// become additional columns in the parent table. Indices declared on an
132/// embedded struct's fields are propagated to physical DB indices on the parent
133/// table.
134///
135/// # Examples
136///
137/// ```ignore
138/// let embedded = model.as_embedded_struct_unwrap();
139/// for field in &embedded.fields {
140///     println!("  embedded field: {}", field.name.app_name);
141/// }
142/// ```
143#[derive(Debug, Clone)]
144pub struct EmbeddedStruct {
145    /// Uniquely identifies this model within the schema.
146    pub id: ModelId,
147
148    /// The model's name.
149    pub name: Name,
150
151    /// Fields contained by this embedded struct.
152    pub fields: Vec<Field>,
153
154    /// Indices defined on this embedded struct's fields.
155    ///
156    /// These reference fields within this embedded struct (not the parent
157    /// model). The schema builder propagates them to physical DB indices on
158    /// the parent table's flattened columns.
159    pub indices: Vec<Index>,
160}
161
162impl EmbeddedStruct {
163    pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
164        for field in &self.fields {
165            field.verify(db)?;
166        }
167        Ok(())
168    }
169}
170
171/// An embedded enum model stored in the parent table via a discriminant column
172/// and optional per-variant data columns.
173///
174/// The discriminant column holds a value (integer or string) identifying the active variant.
175/// Variants may optionally carry data fields, which are stored as additional
176/// nullable columns in the parent table.
177///
178/// # Examples
179///
180/// ```ignore
181/// let ee = model.as_embedded_enum_unwrap();
182/// for variant in &ee.variants {
183///     println!("variant {} = {}", variant.name.upper_camel_case(), variant.discriminant);
184/// }
185/// ```
186#[derive(Debug, Clone)]
187pub struct EmbeddedEnum {
188    /// Uniquely identifies this model within the schema.
189    pub id: ModelId,
190
191    /// The model's name.
192    pub name: Name,
193
194    /// The primitive type used for the discriminant column.
195    pub discriminant: FieldPrimitive,
196
197    /// The enum's variants.
198    pub variants: Vec<EnumVariant>,
199
200    /// All fields across all variants, with global indices. Each field's
201    /// [`variant`](Field::variant) identifies which variant it belongs to.
202    pub fields: Vec<Field>,
203
204    /// Indices defined on this embedded enum's variant fields.
205    ///
206    /// These reference fields within this embedded enum (not the parent
207    /// model). The schema builder propagates them to physical DB indices on
208    /// the parent table's flattened columns.
209    pub indices: Vec<Index>,
210}
211
212/// One variant of an [`EmbeddedEnum`].
213///
214/// Each variant has a name and a discriminant value (integer or string) that is
215/// stored in the database to identify which variant is active.
216#[derive(Debug, Clone)]
217pub struct EnumVariant {
218    /// The Rust variant name.
219    pub name: Name,
220
221    /// The discriminant value stored in the database column.
222    /// Typically `Value::I64` for integer discriminants or `Value::String` for
223    /// string discriminants.
224    pub discriminant: stmt::Value,
225}
226
227impl EmbeddedEnum {
228    /// Returns true if at least one variant carries data fields.
229    pub fn has_data_variants(&self) -> bool {
230        !self.fields.is_empty()
231    }
232
233    /// Returns fields belonging to a specific variant.
234    pub fn variant_fields(&self, variant_index: usize) -> impl Iterator<Item = &Field> {
235        let variant_id = VariantId {
236            model: self.id,
237            index: variant_index,
238        };
239        self.fields
240            .iter()
241            .filter(move |f| f.variant == Some(variant_id))
242    }
243
244    pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
245        for field in &self.fields {
246            field.verify(db)?;
247        }
248        Ok(())
249    }
250}
251
252/// Uniquely identifies a [`Model`] within a [`Schema`](super::Schema).
253///
254/// `ModelId` wraps a `usize` index into the schema's model map. It is `Copy`
255/// and can be used as a key for lookups.
256///
257/// # Examples
258///
259/// ```
260/// use toasty_core::schema::app::ModelId;
261///
262/// let id = ModelId(0);
263/// let field_id = id.field(2);
264/// assert_eq!(field_id.model, id);
265/// assert_eq!(field_id.index, 2);
266/// ```
267#[derive(Copy, Clone, Eq, PartialEq, Hash)]
268#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
269pub struct ModelId(pub usize);
270
271impl Model {
272    /// Returns this model's [`ModelId`].
273    pub fn id(&self) -> ModelId {
274        match self {
275            Model::Root(root) => root.id,
276            Model::EmbeddedStruct(embedded) => embedded.id,
277            Model::EmbeddedEnum(e) => e.id,
278        }
279    }
280
281    /// Returns a reference to this model's [`Name`].
282    pub fn name(&self) -> &Name {
283        match self {
284            Model::Root(root) => &root.name,
285            Model::EmbeddedStruct(embedded) => &embedded.name,
286            Model::EmbeddedEnum(e) => &e.name,
287        }
288    }
289
290    /// Returns true if this is a root model (has a table and primary key)
291    pub fn is_root(&self) -> bool {
292        matches!(self, Model::Root(_))
293    }
294
295    /// Returns true if this is an embedded model (flattened into parent)
296    pub fn is_embedded(&self) -> bool {
297        matches!(self, Model::EmbeddedStruct(_) | Model::EmbeddedEnum(_))
298    }
299
300    /// Returns true if this model can be the target of a relation
301    pub fn can_be_relation_target(&self) -> bool {
302        self.is_root()
303    }
304
305    /// Returns the inner [`ModelRoot`] if this is a root model.
306    pub fn as_root(&self) -> Option<&ModelRoot> {
307        match self {
308            Model::Root(root) => Some(root),
309            _ => None,
310        }
311    }
312
313    /// Returns a reference to the root model data.
314    ///
315    /// # Panics
316    ///
317    /// Panics if this is not a [`Model::Root`].
318    pub fn as_root_unwrap(&self) -> &ModelRoot {
319        match self {
320            Model::Root(root) => root,
321            Model::EmbeddedStruct(_) => panic!("expected root model, found embedded struct"),
322            Model::EmbeddedEnum(_) => panic!("expected root model, found embedded enum"),
323        }
324    }
325
326    /// Returns a mutable reference to the root model data.
327    ///
328    /// # Panics
329    ///
330    /// Panics if this is not a [`Model::Root`].
331    pub fn as_root_mut_unwrap(&mut self) -> &mut ModelRoot {
332        match self {
333            Model::Root(root) => root,
334            Model::EmbeddedStruct(_) => panic!("expected root model, found embedded struct"),
335            Model::EmbeddedEnum(_) => panic!("expected root model, found embedded enum"),
336        }
337    }
338
339    /// Returns a reference to the embedded struct data.
340    ///
341    /// # Panics
342    ///
343    /// Panics if this is not a [`Model::EmbeddedStruct`].
344    pub fn as_embedded_struct_unwrap(&self) -> &EmbeddedStruct {
345        match self {
346            Model::EmbeddedStruct(embedded) => embedded,
347            Model::Root(_) => panic!("expected embedded struct, found root model"),
348            Model::EmbeddedEnum(_) => panic!("expected embedded struct, found embedded enum"),
349        }
350    }
351
352    /// Returns a reference to the embedded enum data.
353    ///
354    /// # Panics
355    ///
356    /// Panics if this is not a [`Model::EmbeddedEnum`].
357    pub fn as_embedded_enum_unwrap(&self) -> &EmbeddedEnum {
358        match self {
359            Model::EmbeddedEnum(e) => e,
360            Model::Root(_) => panic!("expected embedded enum, found root model"),
361            Model::EmbeddedStruct(_) => panic!("expected embedded enum, found embedded struct"),
362        }
363    }
364
365    pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
366        match self {
367            Model::Root(root) => root.verify(db),
368            Model::EmbeddedStruct(embedded) => embedded.verify(db),
369            Model::EmbeddedEnum(e) => e.verify(db),
370        }
371    }
372}
373
374/// Identifies a specific variant within an [`EmbeddedEnum`] model.
375///
376/// # Examples
377///
378/// ```
379/// use toasty_core::schema::app::ModelId;
380///
381/// let variant_id = ModelId(1).variant(0);
382/// assert_eq!(variant_id.model, ModelId(1));
383/// assert_eq!(variant_id.index, 0);
384/// ```
385#[derive(Copy, Clone, PartialEq, Eq, Hash)]
386pub struct VariantId {
387    /// The enum model this variant belongs to.
388    pub model: ModelId,
389    /// Index of the variant within `EmbeddedEnum::variants`.
390    pub index: usize,
391}
392
393impl fmt::Debug for VariantId {
394    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
395        write!(fmt, "VariantId({}/{})", self.model.0, self.index)
396    }
397}
398
399impl ModelId {
400    /// Create a `FieldId` representing the current model's field at index
401    /// `index`.
402    pub const fn field(self, index: usize) -> FieldId {
403        FieldId { model: self, index }
404    }
405
406    /// Create a `VariantId` representing the current model's variant at
407    /// `index`.
408    pub const fn variant(self, index: usize) -> VariantId {
409        VariantId { model: self, index }
410    }
411
412    pub(crate) const fn placeholder() -> Self {
413        Self(usize::MAX)
414    }
415}
416
417impl From<&Self> for ModelId {
418    fn from(src: &Self) -> Self {
419        *src
420    }
421}
422
423impl From<&mut Self> for ModelId {
424    fn from(src: &mut Self) -> Self {
425        *src
426    }
427}
428
429impl From<&Model> for ModelId {
430    fn from(value: &Model) -> Self {
431        value.id()
432    }
433}
434
435impl From<&ModelRoot> for ModelId {
436    fn from(value: &ModelRoot) -> Self {
437        value.id
438    }
439}
440
441impl fmt::Debug for ModelId {
442    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
443        write!(fmt, "ModelId({})", self.0)
444    }
445}