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 /// The contiguous range of this variant's fields in [`EmbeddedEnum::fields`].
341 /// Ranges follow variant order and partition the enum's fields, including
342 /// empty ranges for unit variants.
343 pub field_range: std::ops::Range<usize>,
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) -> &[Field] {
354 &self.fields[self.variants[variant_index].field_range.clone()]
355 }
356
357 pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
358 self.verify_variant_field_layout()?;
359
360 for field in &self.fields {
361 field.verify(db)?;
362 }
363 Ok(())
364 }
365
366 fn verify_variant_field_layout(&self) -> Result<()> {
367 let mut next_field = 0;
368
369 for (variant_index, variant) in self.variants.iter().enumerate() {
370 self.verify_variant_field_range(variant_index, variant, next_field)?;
371 next_field = variant.field_range.end;
372 }
373
374 if next_field != self.fields.len() {
375 return Err(crate::Error::invalid_schema(format!(
376 "variant field ranges do not cover enum {}",
377 self.name.upper_camel_case(),
378 )));
379 }
380
381 Ok(())
382 }
383
384 fn verify_variant_field_range(
385 &self,
386 variant_index: usize,
387 variant: &EnumVariant,
388 expected_start: usize,
389 ) -> Result<()> {
390 let range = &variant.field_range;
391
392 if range.start != expected_start {
393 return Err(self.invalid_variant_field_range(variant));
394 }
395
396 let Some(fields) = self.fields.get(range.clone()) else {
397 return Err(self.invalid_variant_field_range(variant));
398 };
399
400 let variant_id = VariantId {
401 model: self.id,
402 index: variant_index,
403 };
404
405 for (field_index, field) in range.clone().zip(fields) {
406 if field.id != self.id.field(field_index) || field.variant != Some(variant_id) {
407 return Err(self.invalid_variant_field_range(variant));
408 }
409 }
410
411 Ok(())
412 }
413
414 fn invalid_variant_field_range(&self, variant: &EnumVariant) -> crate::Error {
415 crate::Error::invalid_schema(format!(
416 "invalid field range for enum variant {}::{}",
417 self.name.upper_camel_case(),
418 variant.name.upper_camel_case(),
419 ))
420 }
421}
422
423/// Uniquely identifies a [`Model`] within a [`Schema`](super::Schema).
424///
425/// `ModelId` wraps a `usize` index into the schema's model map. It is `Copy`
426/// and can be used as a key for lookups.
427///
428/// # Examples
429///
430/// ```
431/// use toasty_core::schema::app::ModelId;
432///
433/// let id = ModelId(0);
434/// let field_id = id.field(2);
435/// assert_eq!(field_id.model, id);
436/// assert_eq!(field_id.index, 2);
437/// ```
438#[derive(Copy, Clone, Eq, PartialEq, Hash)]
439#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
440pub struct ModelId(pub usize);
441
442impl Model {
443 /// Returns this model's [`ModelId`].
444 pub fn id(&self) -> ModelId {
445 match self {
446 Model::Root(root) => root.id,
447 Model::EmbeddedStruct(embedded) => embedded.id,
448 Model::EmbeddedEnum(e) => e.id,
449 }
450 }
451
452 /// Returns a reference to this model's [`Name`].
453 pub fn name(&self) -> &Name {
454 match self {
455 Model::Root(root) => &root.name,
456 Model::EmbeddedStruct(embedded) => &embedded.name,
457 Model::EmbeddedEnum(e) => &e.name,
458 }
459 }
460
461 /// Returns true if this is a root model (has a table and primary key)
462 pub fn is_root(&self) -> bool {
463 matches!(self, Model::Root(_))
464 }
465
466 /// Returns true if this is an embedded model (flattened into parent)
467 pub fn is_embedded(&self) -> bool {
468 matches!(self, Model::EmbeddedStruct(_) | Model::EmbeddedEnum(_))
469 }
470
471 /// Returns the inner [`ModelRoot`] if this is a root model.
472 pub fn as_root(&self) -> Option<&ModelRoot> {
473 match self {
474 Model::Root(root) => Some(root),
475 _ => None,
476 }
477 }
478
479 /// The model's fields. For an [`EmbeddedEnum`] these are the flattened
480 /// variant fields ([`EmbeddedEnum::fields`]), not the variants themselves.
481 pub fn fields(&self) -> &[Field] {
482 match self {
483 Model::Root(root) => &root.fields,
484 Model::EmbeddedStruct(embedded) => &embedded.fields,
485 Model::EmbeddedEnum(e) => &e.fields,
486 }
487 }
488
489 /// Returns a reference to the root model data.
490 ///
491 /// # Panics
492 ///
493 /// Panics if this is not a [`Model::Root`].
494 pub fn as_root_unwrap(&self) -> &ModelRoot {
495 match self {
496 Model::Root(root) => root,
497 Model::EmbeddedStruct(_) => panic!("expected root model, found embedded struct"),
498 Model::EmbeddedEnum(_) => panic!("expected root model, found embedded enum"),
499 }
500 }
501
502 /// Returns a mutable reference to the root model data.
503 ///
504 /// # Panics
505 ///
506 /// Panics if this is not a [`Model::Root`].
507 pub fn as_root_mut_unwrap(&mut self) -> &mut ModelRoot {
508 match self {
509 Model::Root(root) => root,
510 Model::EmbeddedStruct(_) => panic!("expected root model, found embedded struct"),
511 Model::EmbeddedEnum(_) => panic!("expected root model, found embedded enum"),
512 }
513 }
514
515 /// Returns a reference to the embedded struct data.
516 ///
517 /// # Panics
518 ///
519 /// Panics if this is not a [`Model::EmbeddedStruct`].
520 pub fn as_embedded_struct_unwrap(&self) -> &EmbeddedStruct {
521 match self {
522 Model::EmbeddedStruct(embedded) => embedded,
523 Model::Root(_) => panic!("expected embedded struct, found root model"),
524 Model::EmbeddedEnum(_) => panic!("expected embedded struct, found embedded enum"),
525 }
526 }
527
528 /// Returns a reference to the embedded enum data.
529 ///
530 /// # Panics
531 ///
532 /// Panics if this is not a [`Model::EmbeddedEnum`].
533 pub fn as_embedded_enum_unwrap(&self) -> &EmbeddedEnum {
534 match self {
535 Model::EmbeddedEnum(e) => e,
536 Model::Root(_) => panic!("expected embedded enum, found root model"),
537 Model::EmbeddedStruct(_) => panic!("expected embedded enum, found embedded struct"),
538 }
539 }
540
541 pub(crate) fn verify(&self, db: &driver::Capability) -> Result<()> {
542 match self {
543 Model::Root(root) => root.verify(db),
544 Model::EmbeddedStruct(embedded) => embedded.verify(db),
545 Model::EmbeddedEnum(e) => e.verify(db),
546 }
547 }
548}
549
550/// Identifies a specific variant within an [`EmbeddedEnum`] model.
551///
552/// # Examples
553///
554/// ```
555/// use toasty_core::schema::app::{ModelId, VariantId};
556///
557/// let variant_id = VariantId { model: ModelId(1), index: 0 };
558/// assert_eq!(variant_id.model, ModelId(1));
559/// assert_eq!(variant_id.index, 0);
560/// ```
561#[derive(Copy, Clone, PartialEq, Eq, Hash)]
562pub struct VariantId {
563 /// The enum model this variant belongs to.
564 pub model: ModelId,
565 /// Index of the variant within `EmbeddedEnum::variants`.
566 pub index: usize,
567}
568
569impl fmt::Debug for VariantId {
570 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
571 write!(fmt, "VariantId({}/{})", self.model.0, self.index)
572 }
573}
574
575impl ModelId {
576 /// Create a `FieldId` representing the current model's field at index
577 /// `index`.
578 pub const fn field(self, index: usize) -> FieldId {
579 FieldId { model: self, index }
580 }
581
582 pub(crate) const fn placeholder() -> Self {
583 Self(usize::MAX)
584 }
585}
586
587impl From<&Self> for ModelId {
588 fn from(src: &Self) -> Self {
589 *src
590 }
591}
592
593impl From<&mut Self> for ModelId {
594 fn from(src: &mut Self) -> Self {
595 *src
596 }
597}
598
599impl From<&Model> for ModelId {
600 fn from(value: &Model) -> Self {
601 value.id()
602 }
603}
604
605impl From<&ModelRoot> for ModelId {
606 fn from(value: &ModelRoot) -> Self {
607 value.id
608 }
609}
610
611impl fmt::Debug for ModelId {
612 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
613 write!(fmt, "ModelId({})", self.0)
614 }
615}