Skip to main content

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///
19/// # Examples
20///
21/// ```ignore
22/// use toasty_core::schema::mapping::Model;
23///
24/// let model_mapping: &Model = schema.mapping_for(model_id);
25/// println!("model {:?} -> table {:?}", model_mapping.id, model_mapping.table);
26/// println!("{} columns, {} fields", model_mapping.columns.len(), model_mapping.fields.len());
27/// ```
28#[derive(Debug, Clone)]
29pub struct Model {
30    /// The model this mapping applies to.
31    pub id: ModelId,
32
33    /// The database table that stores this model's data.
34    pub table: TableId,
35
36    /// Ordered list of columns that comprise this model's storage
37    /// representation.
38    ///
39    /// The order corresponds to the `model_to_table` expression record: the
40    /// i-th expression in `model_to_table` produces the value for the i-th
41    /// column here.
42    pub columns: Vec<ColumnId>,
43
44    /// Per-field mappings.
45    ///
46    /// Indexed by field index within the model. Primitive fields and embedded
47    /// fields have their respective mappings, while relation fields use
48    /// `Field::Relation` since they don't map directly to columns.
49    pub fields: Vec<Field>,
50
51    /// Expression template for converting model field values to table column
52    /// values.
53    ///
54    /// Used during `INSERT` and `UPDATE` lowering. Each expression in the
55    /// record references model fields (via `Expr::Reference`) and produces a
56    /// column value. May include type casts (e.g., `Id<T>` to `String`) or
57    /// concatenations for discriminated storage formats.
58    pub model_to_table: stmt::ExprRecord,
59
60    /// Expression template for converting table column values to model field
61    /// values.
62    ///
63    /// Used during `SELECT` lowering to construct the `RETURNING` clause. Each
64    /// expression references table columns (via `Expr::Reference`) and produces
65    /// a model field value. Relation fields are initialized to `Null` and
66    /// replaced with subqueries when `include()` is used.
67    pub table_to_model: TableToModel,
68
69    /// Pre-computed default `RETURNING` expression for this model.
70    ///
71    /// Same shape as `table_to_model`'s record, but with every `#[deferred]`
72    /// field — at this level or nested inside an embedded type — pre-masked
73    /// to `Null`. Lowering starts from this expression and splices loaded
74    /// forms in for fields named by `.include()` (or for every deferred
75    /// field when an `INSERT ... RETURNING` is being lowered).
76    pub default_returning: stmt::Expr,
77}
78
79/// Expression template for converting table rows into model records.
80///
81/// Contains one expression per model field. Each expression references table
82/// columns and produces the corresponding model field value. During lowering,
83/// these expressions construct `SELECT` clauses that return model-shaped data.
84///
85/// # Examples
86///
87/// ```ignore
88/// use toasty_core::schema::mapping::TableToModel;
89///
90/// let t2m: &TableToModel = &model_mapping.table_to_model;
91/// // Get the full returning expression for SELECT
92/// let returning = t2m.lower_returning_model();
93/// ```
94#[derive(Debug, Default, Clone)]
95pub struct TableToModel {
96    /// One expression per model field, indexed by field position.
97    expr: stmt::ExprRecord,
98}
99
100impl TableToModel {
101    /// Creates a new `TableToModel` from the given expression record.
102    ///
103    /// # Examples
104    ///
105    /// ```ignore
106    /// use toasty_core::schema::mapping::TableToModel;
107    /// use toasty_core::stmt::ExprRecord;
108    ///
109    /// let record = ExprRecord::default();
110    /// let t2m = TableToModel::new(record);
111    /// ```
112    pub fn new(expr: stmt::ExprRecord) -> TableToModel {
113        TableToModel { expr }
114    }
115
116    /// Returns the complete expression record for use in a `RETURNING` clause.
117    ///
118    /// # Examples
119    ///
120    /// ```ignore
121    /// let expr = table_to_model.lower_returning_model();
122    /// // Use `expr` in a SELECT or RETURNING clause
123    /// ```
124    pub fn lower_returning_model(&self) -> stmt::Expr {
125        self.expr.clone().into()
126    }
127
128    /// Returns the expression for a single field reference.
129    ///
130    /// # Arguments
131    ///
132    /// * `nesting` - The scope nesting level. Non-zero when the reference
133    ///   appears in a subquery relative to the table source.
134    /// * `index` - The field index within the model.
135    ///
136    /// # Examples
137    ///
138    /// ```ignore
139    /// // Get the expression for field 0 at the top-level scope
140    /// let expr = table_to_model.lower_expr_reference(0, 0);
141    /// ```
142    pub fn lower_expr_reference(&self, nesting: usize, index: usize) -> stmt::Expr {
143        let mut expr = self.expr[index].clone();
144        let n = nesting;
145
146        if n > 0 {
147            stmt::visit_mut::for_each_expr_mut(&mut expr, |expr| {
148                if let stmt::Expr::Reference(stmt::ExprReference::Column(expr_column)) = expr {
149                    expr_column.nesting = n;
150                }
151            });
152        }
153
154        expr
155    }
156}
157
158impl Model {
159    /// Resolves a projection to the corresponding field mapping.
160    ///
161    /// Handles both single-step projections (primitive/embedded fields) and multi-step
162    /// projections (nested embedded struct fields). Supports arbitrary nesting depth.
163    ///
164    /// # Examples
165    ///
166    /// - `[2]` → field at index 2 (primitive or embedded)
167    /// - `[2, 1]` → embedded field at index 2, subfield at index 1
168    /// - `[2, 1, 0]` → nested embedded field at index 2, subfield 1, sub-subfield 0
169    ///
170    /// # Returns
171    ///
172    /// Returns `Some(&Field)` if the projection is valid. The field can be:
173    /// - `Field::Primitive` for partial updates to a specific primitive
174    /// - `Field::Struct` for full replacement of an embedded struct
175    ///
176    /// Returns `None` if the projection is invalid or points to a relation field.
177    pub fn resolve_field_mapping(&self, projection: &stmt::Projection) -> Option<&Field> {
178        let [first, rest @ ..] = projection.as_slice() else {
179            return None;
180        };
181
182        // Get the first field from the root
183        let mut current_field = self.fields.get(*first)?;
184
185        // Walk through remaining steps
186        for step in rest {
187            match current_field {
188                Field::Struct(field_struct) => {
189                    current_field = field_struct.fields.get(*step)?;
190                }
191                Field::Primitive(_) => {
192                    // Cannot project through primitive fields
193                    return None;
194                }
195                _ => {
196                    return None;
197                }
198            }
199        }
200
201        Some(current_field)
202    }
203}