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}