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
70/// Expression template for converting table rows into model records.
71///
72/// Contains one expression per model field. Each expression references table
73/// columns and produces the corresponding model field value. During lowering,
74/// these expressions construct `SELECT` clauses that return model-shaped data.
75///
76/// # Examples
77///
78/// ```ignore
79/// use toasty_core::schema::mapping::TableToModel;
80///
81/// let t2m: &TableToModel = &model_mapping.table_to_model;
82/// // Get the full returning expression for SELECT
83/// let returning = t2m.lower_returning_model();
84/// ```
85#[derive(Debug, Default, Clone)]
86pub struct TableToModel {
87 /// One expression per model field, indexed by field position.
88 expr: stmt::ExprRecord,
89}
90
91impl TableToModel {
92 /// Creates a new `TableToModel` from the given expression record.
93 ///
94 /// # Examples
95 ///
96 /// ```ignore
97 /// use toasty_core::schema::mapping::TableToModel;
98 /// use toasty_core::stmt::ExprRecord;
99 ///
100 /// let record = ExprRecord::default();
101 /// let t2m = TableToModel::new(record);
102 /// ```
103 pub fn new(expr: stmt::ExprRecord) -> TableToModel {
104 TableToModel { expr }
105 }
106
107 /// Returns the complete expression record for use in a `RETURNING` clause.
108 ///
109 /// # Examples
110 ///
111 /// ```ignore
112 /// let expr = table_to_model.lower_returning_model();
113 /// // Use `expr` in a SELECT or RETURNING clause
114 /// ```
115 pub fn lower_returning_model(&self) -> stmt::Expr {
116 self.expr.clone().into()
117 }
118
119 /// Returns the expression for a single field reference.
120 ///
121 /// # Arguments
122 ///
123 /// * `nesting` - The scope nesting level. Non-zero when the reference
124 /// appears in a subquery relative to the table source.
125 /// * `index` - The field index within the model.
126 ///
127 /// # Examples
128 ///
129 /// ```ignore
130 /// // Get the expression for field 0 at the top-level scope
131 /// let expr = table_to_model.lower_expr_reference(0, 0);
132 /// ```
133 pub fn lower_expr_reference(&self, nesting: usize, index: usize) -> stmt::Expr {
134 let mut expr = self.expr[index].clone();
135 let n = nesting;
136
137 if n > 0 {
138 stmt::visit_mut::for_each_expr_mut(&mut expr, |expr| {
139 if let stmt::Expr::Reference(stmt::ExprReference::Column(expr_column)) = expr {
140 expr_column.nesting = n;
141 }
142 });
143 }
144
145 expr
146 }
147}
148
149impl Model {
150 /// Resolves a projection to the corresponding field mapping.
151 ///
152 /// Handles both single-step projections (primitive/embedded fields) and multi-step
153 /// projections (nested embedded struct fields). Supports arbitrary nesting depth.
154 ///
155 /// # Examples
156 ///
157 /// - `[2]` → field at index 2 (primitive or embedded)
158 /// - `[2, 1]` → embedded field at index 2, subfield at index 1
159 /// - `[2, 1, 0]` → nested embedded field at index 2, subfield 1, sub-subfield 0
160 ///
161 /// # Returns
162 ///
163 /// Returns `Some(&Field)` if the projection is valid. The field can be:
164 /// - `Field::Primitive` for partial updates to a specific primitive
165 /// - `Field::Struct` for full replacement of an embedded struct
166 ///
167 /// Returns `None` if the projection is invalid or points to a relation field.
168 pub fn resolve_field_mapping(&self, projection: &stmt::Projection) -> Option<&Field> {
169 let [first, rest @ ..] = projection.as_slice() else {
170 return None;
171 };
172
173 // Get the first field from the root
174 let mut current_field = self.fields.get(*first)?;
175
176 // Walk through remaining steps
177 for step in rest {
178 match current_field {
179 Field::Struct(field_struct) => {
180 current_field = field_struct.fields.get(*step)?;
181 }
182 Field::Primitive(_) => {
183 // Cannot project through primitive fields
184 return None;
185 }
186 _ => {
187 return None;
188 }
189 }
190 }
191
192 Some(current_field)
193 }
194}