toasty_core/schema/mapping/
field.rs

1use crate::{
2    schema::db::ColumnId,
3    stmt::{PathFieldSet, Projection},
4};
5use indexmap::IndexMap;
6
7/// Maps a model field to its database storage representation.
8///
9/// Different field types have different storage strategies:
10/// - Primitive fields map to a single column
11/// - Struct fields flatten an embedded struct to multiple columns
12/// - Enum fields map to a discriminant column plus per-variant data columns
13/// - Relation fields (`BelongsTo`, `HasMany`, `HasOne`) don't have direct column storage
14#[derive(Debug, Clone)]
15pub enum Field {
16    /// A primitive field stored in a single column.
17    Primitive(FieldPrimitive),
18
19    /// An embedded struct field flattened into multiple columns.
20    Struct(FieldStruct),
21
22    /// An embedded enum field stored as a discriminant column plus per-variant data columns.
23    Enum(FieldEnum),
24
25    /// A relation field that doesn't map to columns in this table.
26    Relation(FieldRelation),
27}
28
29impl Field {
30    /// Returns the update coverage mask for this field.
31    ///
32    /// Each primitive (leaf) field in the model is assigned a unique bit.
33    /// The mask for a given mapping field is the set of those bits that
34    /// correspond to the primitives it covers:
35    ///
36    /// - `Primitive` → singleton set containing only its own bit
37    /// - `Struct`    → union of all nested primitive bits (recursively)
38    /// - `Enum`      → singleton set (the whole enum value changes atomically)
39    /// - `Relation`  → singleton set (assigned a bit for uniform tracking)
40    ///
41    /// Masks are used during update lowering to determine whether a partial
42    /// update fully covers an embedded field or only touches some of its
43    /// sub-fields. Intersecting `changed_mask` with a field's `field_mask`
44    /// yields the subset of that field's primitives being updated; equality
45    /// with the full `field_mask` means full coverage.
46    pub fn field_mask(&self) -> PathFieldSet {
47        match self {
48            Field::Primitive(p) => p.field_mask.clone(),
49            Field::Struct(s) => s.field_mask.clone(),
50            Field::Enum(e) => e.field_mask.clone(),
51            Field::Relation(r) => r.field_mask.clone(),
52        }
53    }
54
55    /// Returns the sub-projection from the root model field to this field
56    /// within the embedded type hierarchy. Identity for root-level fields.
57    pub fn sub_projection(&self) -> &Projection {
58        static IDENTITY: Projection = Projection::identity();
59        match self {
60            Field::Primitive(p) => &p.sub_projection,
61            Field::Struct(s) => &s.sub_projection,
62            Field::Enum(e) => &e.sub_projection,
63            Field::Relation(_) => &IDENTITY,
64        }
65    }
66
67    pub fn is_relation(&self) -> bool {
68        matches!(self, Field::Relation(_))
69    }
70
71    pub fn as_primitive(&self) -> Option<&FieldPrimitive> {
72        match self {
73            Field::Primitive(p) => Some(p),
74            _ => None,
75        }
76    }
77
78    pub fn as_primitive_mut(&mut self) -> Option<&mut FieldPrimitive> {
79        match self {
80            Field::Primitive(p) => Some(p),
81            _ => None,
82        }
83    }
84
85    pub fn as_struct(&self) -> Option<&FieldStruct> {
86        match self {
87            Field::Struct(s) => Some(s),
88            _ => None,
89        }
90    }
91
92    pub fn as_enum(&self) -> Option<&FieldEnum> {
93        match self {
94            Field::Enum(e) => Some(e),
95            _ => None,
96        }
97    }
98
99    /// Returns an iterator over all (column, lowering) pairs impacted by this field.
100    ///
101    /// For primitive fields, yields a single pair.
102    /// For struct fields, yields all flattened columns.
103    /// For enum fields, yields the discriminant column plus all variant data columns.
104    /// For relation fields, yields nothing.
105    pub fn columns(&self) -> impl Iterator<Item = (ColumnId, usize)> + '_ {
106        match self {
107            Field::Primitive(fp) => Box::new(std::iter::once((fp.column, fp.lowering)))
108                as Box<dyn Iterator<Item = (ColumnId, usize)> + '_>,
109            Field::Struct(fs) => Box::new(fs.columns.iter().map(|(k, v)| (*k, *v))),
110            Field::Enum(fe) => Box::new(
111                std::iter::once((fe.discriminant.column, fe.discriminant.lowering)).chain(
112                    fe.variants
113                        .iter()
114                        .flat_map(|v| v.fields.iter().flat_map(|f| f.columns())),
115                ),
116            ),
117            Field::Relation(_) => Box::new(std::iter::empty()),
118        }
119    }
120}
121
122/// Maps a primitive field to its table column.
123#[derive(Debug, Clone)]
124pub struct FieldPrimitive {
125    /// The table column that stores this field's value.
126    pub column: ColumnId,
127
128    /// Index into `Model::model_to_table` for this field's lowering expression.
129    ///
130    /// The expression at this index converts the model field value to the
131    /// column value during `INSERT` and `UPDATE` operations.
132    pub lowering: usize,
133
134    /// Update coverage mask for this primitive field.
135    ///
136    /// A singleton bitset containing the unique bit assigned to this primitive
137    /// within the model's field mask space. During update lowering, accumulated
138    /// `changed_mask` bits are intersected with each field's `field_mask` to
139    /// determine which fields are affected by a partial update.
140    pub field_mask: PathFieldSet,
141
142    /// The projection from the root model field (the top-level embedded field
143    /// containing this primitive) down to this primitive within the embedded
144    /// type hierarchy. Identity for root-level primitives.
145    ///
146    /// Used when building `Returning::Changed` expressions: we emit
147    /// `project(ref_self_field(root_field_id), sub_projection)` so the
148    /// existing lowering and constantization pipeline resolves it to the
149    /// correct column value without needing to carry assignment expressions.
150    pub sub_projection: Projection,
151}
152
153/// Maps an embedded struct field to its flattened column representation.
154///
155/// Embedded fields are stored by flattening their primitive fields into columns
156/// with names like `{field}_{embedded_field}`. This structure tracks the mapping
157/// for each field in the embedded struct.
158#[derive(Debug, Clone)]
159pub struct FieldStruct {
160    /// Per-field mappings for the embedded struct's fields.
161    ///
162    /// Indexed by field index within the embedded model.
163    pub fields: Vec<Field>,
164
165    /// Flattened mapping from columns to lowering expression indices.
166    ///
167    /// This map contains all columns impacted by this embedded field, paired
168    /// with their corresponding lowering expression index in `Model::model_to_table`.
169    pub columns: IndexMap<ColumnId, usize>,
170
171    /// Update coverage mask for this embedded field.
172    ///
173    /// The union of the `field_mask` bits of every primitive nested within this
174    /// embedded struct (recursively).
175    pub field_mask: PathFieldSet,
176
177    /// The projection from the root model field down to this embedded field
178    /// within the type hierarchy. Identity for root-level embedded fields.
179    pub sub_projection: Projection,
180}
181
182/// Maps an embedded enum field to its discriminant column and per-variant data columns.
183///
184/// The discriminant column always stores the active variant's integer discriminant.
185/// Each data variant additionally has nullable columns for its fields; unit variants
186/// have no extra columns (all variant-field columns are NULL for them).
187#[derive(Debug, Clone)]
188pub struct FieldEnum {
189    /// Mapping for the discriminant column.
190    pub discriminant: FieldPrimitive,
191
192    /// Per-variant mappings, in the same order as `app::EmbeddedEnum::variants`.
193    pub variants: Vec<EnumVariant>,
194
195    /// Update coverage mask for the enum field (singleton: the whole enum changes atomically).
196    pub field_mask: PathFieldSet,
197
198    /// Sub-projection from the root model field to this enum field.
199    pub sub_projection: Projection,
200}
201
202/// Mapping for a single variant of an embedded enum.
203#[derive(Debug, Clone)]
204pub struct EnumVariant {
205    /// The discriminant value for this variant.
206    pub discriminant: i64,
207
208    /// Field mappings for this variant's data fields, in declaration order.
209    /// Empty for unit variants. Supports nesting (each entry is a full `Field`).
210    pub fields: Vec<Field>,
211}
212
213/// Maps a relation field (`BelongsTo`, `HasMany`, `HasOne`).
214///
215/// Relations don't map to columns in this table — they are resolved through
216/// joins or foreign keys in other tables. A unique bit is assigned in the
217/// model's field mask space so that relation assignments are detected uniformly
218/// through the same mask intersection logic used for primitive and embedded fields.
219#[derive(Debug, Clone)]
220pub struct FieldRelation {
221    /// Update coverage mask for this relation field.
222    pub field_mask: PathFieldSet,
223}