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}