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