toasty_core/schema/mapping/field.rs
1use crate::{
2 schema::{app::ModelId, db::ColumnId},
3 stmt::{self, 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///
15/// # Examples
16///
17/// ```ignore
18/// use toasty_core::schema::mapping::Field;
19///
20/// match &field {
21/// Field::Primitive(p) => println!("column {:?}", p.column),
22/// Field::Struct(s) => println!("{} nested fields", s.fields.len()),
23/// Field::Enum(e) => println!("discriminant col {:?}", e.discriminant.column),
24/// Field::Relation(_) => println!("relation (no columns)"),
25/// }
26/// ```
27#[derive(Debug, Clone)]
28pub enum Field {
29 /// A primitive field stored in a single column.
30 Primitive(FieldPrimitive),
31
32 /// An embedded struct field flattened into multiple columns.
33 Struct(FieldStruct),
34
35 /// An embedded enum field stored as a discriminant column plus per-variant data columns.
36 Enum(FieldEnum),
37
38 /// A relation field that doesn't map to columns in this table.
39 Relation(FieldRelation),
40}
41
42impl Field {
43 /// Returns the update coverage mask for this field.
44 ///
45 /// Each primitive (leaf) field in the model is assigned a unique bit.
46 /// The mask for a given mapping field is the set of those bits that
47 /// correspond to the primitives it covers:
48 ///
49 /// - `Primitive` → singleton set containing only its own bit
50 /// - `Struct` → union of all nested primitive bits (recursively)
51 /// - `Enum` → singleton set (the whole enum value changes atomically)
52 /// - `Relation` → singleton set (assigned a bit for uniform tracking)
53 ///
54 /// Masks are used during update lowering to determine whether a partial
55 /// update fully covers an embedded field or only touches some of its
56 /// sub-fields. Intersecting `changed_mask` with a field's `field_mask`
57 /// yields the subset of that field's primitives being updated; equality
58 /// with the full `field_mask` means full coverage.
59 pub fn field_mask(&self) -> PathFieldSet {
60 match self {
61 Field::Primitive(p) => p.field_mask.clone(),
62 Field::Struct(s) => s.field_mask.clone(),
63 Field::Enum(e) => e.field_mask.clone(),
64 Field::Relation(r) => r.field_mask.clone(),
65 }
66 }
67
68 /// Returns the sub-projection from the root model field to this field
69 /// within the embedded type hierarchy. Identity for root-level fields.
70 pub fn sub_projection(&self) -> &Projection {
71 static IDENTITY: Projection = Projection::identity();
72 match self {
73 Field::Primitive(p) => &p.sub_projection,
74 Field::Struct(s) => &s.sub_projection,
75 Field::Enum(e) => &e.sub_projection,
76 Field::Relation(_) => &IDENTITY,
77 }
78 }
79
80 /// Returns `true` if this is a [`Field::Relation`].
81 pub fn is_relation(&self) -> bool {
82 matches!(self, Field::Relation(_))
83 }
84
85 /// Returns the inner [`FieldPrimitive`] if this is a `Primitive` variant,
86 /// or `None` otherwise.
87 pub fn as_primitive(&self) -> Option<&FieldPrimitive> {
88 match self {
89 Field::Primitive(p) => Some(p),
90 _ => None,
91 }
92 }
93
94 /// Returns a mutable reference to the inner [`FieldPrimitive`] if this is
95 /// a `Primitive` variant, or `None` otherwise.
96 pub fn as_primitive_mut(&mut self) -> Option<&mut FieldPrimitive> {
97 match self {
98 Field::Primitive(p) => Some(p),
99 _ => None,
100 }
101 }
102
103 /// Returns the inner [`FieldStruct`] if this is a `Struct` variant, or
104 /// `None` otherwise.
105 pub fn as_struct(&self) -> Option<&FieldStruct> {
106 match self {
107 Field::Struct(s) => Some(s),
108 _ => None,
109 }
110 }
111
112 /// Returns the inner [`FieldEnum`] if this is an `Enum` variant, or
113 /// `None` otherwise.
114 pub fn as_enum(&self) -> Option<&FieldEnum> {
115 match self {
116 Field::Enum(e) => Some(e),
117 _ => None,
118 }
119 }
120
121 /// Returns an iterator over all (column, lowering) pairs impacted by this field.
122 ///
123 /// For primitive fields, yields a single pair.
124 /// For struct fields, yields all flattened columns.
125 /// For enum fields, yields the discriminant column plus all variant data columns.
126 /// For relation fields, yields nothing.
127 pub fn columns(&self) -> impl Iterator<Item = (ColumnId, usize)> + '_ {
128 match self {
129 Field::Primitive(fp) => Box::new(std::iter::once((fp.column, fp.lowering)))
130 as Box<dyn Iterator<Item = (ColumnId, usize)> + '_>,
131 Field::Struct(fs) => Box::new(fs.columns.iter().map(|(k, v)| (*k, *v))),
132 Field::Enum(fe) => Box::new(
133 std::iter::once((fe.discriminant.column, fe.discriminant.lowering)).chain(
134 fe.variants
135 .iter()
136 .flat_map(|v| v.fields.iter().flat_map(|f| f.columns())),
137 ),
138 ),
139 Field::Relation(_) => Box::new(std::iter::empty()),
140 }
141 }
142}
143
144/// Maps a primitive field to its table column.
145///
146/// # Examples
147///
148/// ```ignore
149/// use toasty_core::schema::mapping::FieldPrimitive;
150///
151/// let prim: &FieldPrimitive = field.as_primitive().unwrap();
152/// println!("stored in column {:?}, lowering index {}", prim.column, prim.lowering);
153/// ```
154#[derive(Debug, Clone)]
155pub struct FieldPrimitive {
156 /// The table column that stores this field's value.
157 pub column: ColumnId,
158
159 /// Index into `Model::model_to_table` for this field's lowering expression.
160 ///
161 /// The expression at this index converts the model field value to the
162 /// column value during `INSERT` and `UPDATE` operations.
163 pub lowering: usize,
164
165 /// Update coverage mask for this primitive field.
166 ///
167 /// A singleton bitset containing the unique bit assigned to this primitive
168 /// within the model's field mask space. During update lowering, accumulated
169 /// `changed_mask` bits are intersected with each field's `field_mask` to
170 /// determine which fields are affected by a partial update.
171 pub field_mask: PathFieldSet,
172
173 /// The projection from the root model field (the top-level embedded field
174 /// containing this primitive) down to this primitive within the embedded
175 /// type hierarchy. Identity for root-level primitives.
176 ///
177 /// Used when building `Returning::Changed` expressions: we emit
178 /// `project(ref_self_field(root_field_id), sub_projection)` so the
179 /// existing lowering and constantization pipeline resolves it to the
180 /// correct column value without needing to carry assignment expressions.
181 pub sub_projection: Projection,
182
183 /// Pre-computed table→model expression for this primitive — a column
184 /// reference, possibly wrapped in a cast when the storage type differs
185 /// from the primitive's expression type.
186 ///
187 /// Cached so that lowering can splice the loaded form `Record([..])` for
188 /// a `#[deferred]` primitive without re-deriving the column expression
189 /// from the column id and schema.
190 pub column_expr: stmt::Expr,
191}
192
193/// Maps an embedded struct field to its flattened column representation.
194///
195/// Embedded fields are stored by flattening their primitive fields into columns
196/// with names like `{field}_{embedded_field}`. This structure tracks the mapping
197/// for each field in the embedded struct.
198///
199/// # Examples
200///
201/// ```ignore
202/// use toasty_core::schema::mapping::FieldStruct;
203///
204/// let s: &FieldStruct = field.as_struct().unwrap();
205/// println!("{} nested fields, {} columns", s.fields.len(), s.columns.len());
206/// ```
207#[derive(Debug, Clone)]
208pub struct FieldStruct {
209 /// The [`ModelId`] of the embedded struct model this mapping corresponds to.
210 pub id: ModelId,
211
212 /// Per-field mappings for the embedded struct's fields.
213 ///
214 /// Indexed by field index within the embedded model.
215 pub fields: Vec<Field>,
216
217 /// Flattened mapping from columns to lowering expression indices.
218 ///
219 /// This map contains all columns impacted by this embedded field, paired
220 /// with their corresponding lowering expression index in `Model::model_to_table`.
221 pub columns: IndexMap<ColumnId, usize>,
222
223 /// Update coverage mask for this embedded field.
224 ///
225 /// The union of the `field_mask` bits of every primitive nested within this
226 /// embedded struct (recursively).
227 pub field_mask: PathFieldSet,
228
229 /// The projection from the root model field down to this embedded field
230 /// within the type hierarchy. Identity for root-level embedded fields.
231 pub sub_projection: Projection,
232
233 /// Pre-computed default record expression for this embedded struct.
234 ///
235 /// `Record([..])` shape matching the struct's fields, with deferred
236 /// sub-fields (direct or further nested) pre-masked to `Null`. Spliced
237 /// in by `process_includes` when a parent `.include()` activates a
238 /// `Deferred<EmbedStruct>` field.
239 pub default_returning: stmt::Expr,
240}
241
242/// Maps an embedded enum field to its discriminant column and per-variant data columns.
243///
244/// The discriminant column stores the active variant's discriminant (integer or string).
245/// Each data variant additionally has nullable columns for its fields; unit variants
246/// have no extra columns (all variant-field columns are NULL for them).
247///
248/// # Examples
249///
250/// ```ignore
251/// use toasty_core::schema::mapping::FieldEnum;
252///
253/// let e: &FieldEnum = field.as_enum().unwrap();
254/// println!("discriminant column: {:?}", e.discriminant.column);
255/// println!("{} variants", e.variants.len());
256/// ```
257#[derive(Debug, Clone)]
258pub struct FieldEnum {
259 /// Mapping for the discriminant column.
260 pub discriminant: FieldPrimitive,
261
262 /// Per-variant mappings, in the same order as `app::EmbeddedEnum::variants`.
263 pub variants: Vec<EnumVariant>,
264
265 /// Update coverage mask for the enum field (singleton: the whole enum changes atomically).
266 pub field_mask: PathFieldSet,
267
268 /// Sub-projection from the root model field to this enum field.
269 pub sub_projection: Projection,
270
271 /// Pre-computed default expression for this embedded enum.
272 ///
273 /// For unit-only enums this is the discriminant column reference. For
274 /// data-carrying enums it is the full `Match { disc, arms[], else }`
275 /// expression with per-arm records (currently identical to the raw
276 /// `table_to_model` shape — `#[deferred]` on a variant field is
277 /// rejected at the macro layer; if it is ever lifted, the per-arm
278 /// records would mask their deferred fields here).
279 pub default_returning: stmt::Expr,
280}
281
282/// Mapping for a single variant of an embedded enum.
283///
284/// # Examples
285///
286/// ```ignore
287/// use toasty_core::schema::mapping::EnumVariant;
288///
289/// for variant in &enum_mapping.variants {
290/// println!("discriminant={}, fields={}", variant.discriminant, variant.fields.len());
291/// }
292/// ```
293#[derive(Debug, Clone)]
294pub struct EnumVariant {
295 /// The discriminant value for this variant (`Value::I64` or `Value::String`).
296 pub discriminant: crate::stmt::Value,
297
298 /// Field mappings for this variant's data fields, in declaration order.
299 /// Empty for unit variants. Supports nesting (each entry is a full `Field`).
300 pub fields: Vec<Field>,
301}
302
303/// Maps a relation field (`BelongsTo`, `HasMany`, `HasOne`).
304///
305/// Relations don't map to columns in this table -- they are resolved through
306/// joins or foreign keys in other tables. A unique bit is assigned in the
307/// model's field mask space so that relation assignments are detected uniformly
308/// through the same mask intersection logic used for primitive and embedded fields.
309///
310/// # Examples
311///
312/// ```ignore
313/// use toasty_core::schema::mapping::FieldRelation;
314///
315/// if field.is_relation() {
316/// // No columns to iterate over
317/// assert_eq!(field.columns().count(), 0);
318/// }
319/// ```
320#[derive(Debug, Clone)]
321pub struct FieldRelation {
322 /// Update coverage mask for this relation field.
323 pub field_mask: PathFieldSet,
324}