toasty_core/schema/mapping/
model.rs

1use super::Field;
2use crate::{
3    schema::{
4        app::ModelId,
5        db::{ColumnId, TableId},
6    },
7    stmt,
8};
9
10/// Defines the bidirectional mapping between a single model and its backing
11/// table.
12///
13/// This struct contains the expression templates used during lowering to
14/// translate between model-level field references and table-level column
15/// references. The mapping supports scenarios where field names differ from
16/// column names, where type conversions are required (e.g., `Id<T>` to
17/// `String`), and where multiple models share a single table.
18#[derive(Debug, Clone)]
19pub struct Model {
20    /// The model this mapping applies to.
21    pub id: ModelId,
22
23    /// The database table that stores this model's data.
24    pub table: TableId,
25
26    /// Ordered list of columns that comprise this model's storage
27    /// representation.
28    ///
29    /// The order corresponds to the `model_to_table` expression record: the
30    /// i-th expression in `model_to_table` produces the value for the i-th
31    /// column here.
32    pub columns: Vec<ColumnId>,
33
34    /// Per-field mappings.
35    ///
36    /// Indexed by field index within the model. Primitive fields and embedded
37    /// fields have their respective mappings, while relation fields use
38    /// `Field::Relation` since they don't map directly to columns.
39    pub fields: Vec<Field>,
40
41    /// Expression template for converting model field values to table column
42    /// values.
43    ///
44    /// Used during `INSERT` and `UPDATE` lowering. Each expression in the
45    /// record references model fields (via `Expr::Reference`) and produces a
46    /// column value. May include type casts (e.g., `Id<T>` to `String`) or
47    /// concatenations for discriminated storage formats.
48    pub model_to_table: stmt::ExprRecord,
49
50    /// Expression template for converting table column values to model field
51    /// values.
52    ///
53    /// Used during `SELECT` lowering to construct the `RETURNING` clause. Each
54    /// expression references table columns (via `Expr::Reference`) and produces
55    /// a model field value. Relation fields are initialized to `Null` and
56    /// replaced with subqueries when `include()` is used.
57    pub table_to_model: TableToModel,
58}
59
60/// Expression template for converting table rows into model records.
61///
62/// Contains one expression per model field. Each expression references table
63/// columns and produces the corresponding model field value. During lowering,
64/// these expressions construct `SELECT` clauses that return model-shaped data.
65#[derive(Debug, Default, Clone)]
66pub struct TableToModel {
67    /// One expression per model field, indexed by field position.
68    expr: stmt::ExprRecord,
69}
70
71impl TableToModel {
72    /// Creates a new `TableToModel` from the given expression record.
73    pub fn new(expr: stmt::ExprRecord) -> TableToModel {
74        TableToModel { expr }
75    }
76
77    /// Returns the complete expression record for use in a `RETURNING` clause.
78    pub fn lower_returning_model(&self) -> stmt::Expr {
79        self.expr.clone().into()
80    }
81
82    /// Returns the expression for a single field reference.
83    ///
84    /// # Arguments
85    ///
86    /// * `nesting` - The scope nesting level. Non-zero when the reference
87    ///   appears in a subquery relative to the table source.
88    /// * `index` - The field index within the model.
89    pub fn lower_expr_reference(&self, nesting: usize, index: usize) -> stmt::Expr {
90        let mut expr = self.expr[index].clone();
91        let n = nesting;
92
93        if n > 0 {
94            stmt::visit_mut::for_each_expr_mut(&mut expr, |expr| {
95                if let stmt::Expr::Reference(stmt::ExprReference::Column(expr_column)) = expr {
96                    expr_column.nesting = n;
97                }
98            });
99        }
100
101        expr
102    }
103}
104
105impl Model {
106    /// Resolves a projection to the corresponding field mapping.
107    ///
108    /// Handles both single-step projections (primitive/embedded fields) and multi-step
109    /// projections (nested embedded struct fields). Supports arbitrary nesting depth.
110    ///
111    /// # Examples
112    ///
113    /// - `[2]` → field at index 2 (primitive or embedded)
114    /// - `[2, 1]` → embedded field at index 2, subfield at index 1
115    /// - `[2, 1, 0]` → nested embedded field at index 2, subfield 1, sub-subfield 0
116    ///
117    /// # Returns
118    ///
119    /// Returns `Some(&Field)` if the projection is valid. The field can be:
120    /// - `Field::Primitive` for partial updates to a specific primitive
121    /// - `Field::Struct` for full replacement of an embedded struct
122    ///
123    /// Returns `None` if the projection is invalid or points to a relation field.
124    pub fn resolve_field_mapping(&self, projection: &stmt::Projection) -> Option<&Field> {
125        let [first, rest @ ..] = projection.as_slice() else {
126            return None;
127        };
128
129        // Get the first field from the root
130        let mut current_field = self.fields.get(*first)?;
131
132        // Walk through remaining steps
133        for step in rest {
134            match current_field {
135                Field::Struct(field_struct) => {
136                    current_field = field_struct.fields.get(*step)?;
137                }
138                Field::Primitive(_) => {
139                    // Cannot project through primitive fields
140                    return None;
141                }
142                _ => {
143                    return None;
144                }
145            }
146        }
147
148        Some(current_field)
149    }
150}