Skip to main content

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`, `Has`) 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 the inner [`FieldStruct`] if this is a `Struct` variant, or
95    /// `None` otherwise.
96    pub fn as_struct(&self) -> Option<&FieldStruct> {
97        match self {
98            Field::Struct(s) => Some(s),
99            _ => None,
100        }
101    }
102
103    /// Returns the inner [`FieldEnum`] if this is an `Enum` variant, or
104    /// `None` otherwise.
105    pub fn as_enum(&self) -> Option<&FieldEnum> {
106        match self {
107            Field::Enum(e) => Some(e),
108            _ => None,
109        }
110    }
111
112    /// Returns an iterator over all (column, lowering) pairs impacted by this field.
113    ///
114    /// For primitive fields, yields a single pair.
115    /// For struct fields, yields all flattened columns.
116    /// For enum fields, yields the discriminant column plus all variant data columns.
117    /// For relation fields, yields nothing.
118    pub fn columns(&self) -> impl Iterator<Item = (ColumnId, usize)> + '_ {
119        match self {
120            Field::Primitive(fp) => Box::new(std::iter::once((fp.column, fp.lowering)))
121                as Box<dyn Iterator<Item = (ColumnId, usize)> + '_>,
122            Field::Struct(fs) => Box::new(fs.columns.iter().map(|(k, v)| (*k, *v))),
123            Field::Enum(fe) => Box::new(
124                std::iter::once((fe.discriminant.column, fe.discriminant.lowering)).chain(
125                    fe.variants
126                        .iter()
127                        .flat_map(|v| v.fields.iter().flat_map(|f| f.columns())),
128                ),
129            ),
130            Field::Relation(_) => Box::new(std::iter::empty()),
131        }
132    }
133}
134
135/// Maps a primitive field to its table column.
136///
137/// # Examples
138///
139/// ```ignore
140/// use toasty_core::schema::mapping::FieldPrimitive;
141///
142/// let prim: &FieldPrimitive = field.as_primitive().unwrap();
143/// println!("stored in column {:?}, lowering index {}", prim.column, prim.lowering);
144/// ```
145#[derive(Debug, Clone)]
146pub struct FieldPrimitive {
147    /// The table column that stores this field's value.
148    pub column: ColumnId,
149
150    /// Index into `Model::model_to_table` for this field's lowering expression.
151    ///
152    /// The expression at this index converts the model field value to the
153    /// column value during `INSERT` and `UPDATE` operations.
154    pub lowering: usize,
155
156    /// Update coverage mask for this primitive field.
157    ///
158    /// A singleton bitset containing the unique bit assigned to this primitive
159    /// within the model's field mask space. During update lowering, accumulated
160    /// `changed_mask` bits are intersected with each field's `field_mask` to
161    /// determine which fields are affected by a partial update.
162    pub field_mask: PathFieldSet,
163
164    /// The projection from the root model field (the top-level embedded field
165    /// containing this primitive) down to this primitive within the embedded
166    /// type hierarchy. Identity for root-level primitives.
167    ///
168    /// Used when building `Returning::Changed` expressions: we emit
169    /// `project(ref_self_field(root_field_id), sub_projection)` so the
170    /// existing lowering and constantization pipeline resolves it to the
171    /// correct column value without needing to carry assignment expressions.
172    pub sub_projection: Projection,
173
174    /// Pre-computed table→model expression for this primitive — a column
175    /// reference, possibly wrapped in a cast when the storage type differs
176    /// from the primitive's expression type.
177    ///
178    /// Cached so that lowering can splice the loaded form `Record([..])` for
179    /// a deferred primitive without re-deriving the column expression
180    /// from the column id and schema.
181    pub column_expr: stmt::Expr,
182}
183
184/// Maps an embedded struct field to its flattened column representation.
185///
186/// Embedded fields are stored by flattening their primitive fields into columns
187/// with names like `{field}_{embedded_field}`. This structure tracks the mapping
188/// for each field in the embedded struct.
189///
190/// # Examples
191///
192/// ```ignore
193/// use toasty_core::schema::mapping::FieldStruct;
194///
195/// let s: &FieldStruct = field.as_struct().unwrap();
196/// println!("{} nested fields, {} columns", s.fields.len(), s.columns.len());
197/// ```
198#[derive(Debug, Clone)]
199pub struct FieldStruct {
200    /// The [`ModelId`] of the embedded struct model this mapping corresponds to.
201    pub id: ModelId,
202
203    /// Per-field mappings for the embedded struct's fields.
204    ///
205    /// Indexed by field index within the embedded model.
206    pub fields: Vec<Field>,
207
208    /// Flattened mapping from columns to lowering expression indices.
209    ///
210    /// This map contains all columns impacted by this embedded field, paired
211    /// with their corresponding lowering expression index in `Model::model_to_table`.
212    pub columns: IndexMap<ColumnId, usize>,
213
214    /// Update coverage mask for this embedded field.
215    ///
216    /// The union of the `field_mask` bits of every primitive nested within this
217    /// embedded struct (recursively).
218    pub field_mask: PathFieldSet,
219
220    /// The projection from the root model field down to this embedded field
221    /// within the type hierarchy. Identity for root-level embedded fields.
222    pub sub_projection: Projection,
223
224    /// Pre-computed default record expression for this embedded struct.
225    ///
226    /// `Record([..])` shape matching the struct's fields, with deferred
227    /// sub-fields (direct or further nested) pre-masked to `Null`. Spliced
228    /// in by `process_includes` when a parent `.include()` activates a
229    /// `Deferred<EmbedStruct>` field.
230    pub default_returning: stmt::Expr,
231
232    /// The presence head column of a nullable embedded struct (`Option<Embed>`).
233    ///
234    /// `None` for a non-nullable embed. `Some(column)` when the field is
235    /// `Option<Embed>`: the head column whose null-ness is the option's
236    /// none-ness (`NULL` = `None`), like `Option<scalar>` and an embedded
237    /// enum's discriminant. Usually a dedicated nullable `bool` column
238    /// (`NULL` = `None`, `true` = `Some`); for a single-column (newtype) embed
239    /// it is the flattened leaf reused as the head. The encode lowering forces
240    /// every flattened leaf column nullable and the decode wraps the struct
241    /// record in a `Match` on this column, so a `None` value round-trips
242    /// without colliding with a `Some` whose fields are all themselves `None`.
243    /// The column's own encode lowering (if dedicated) is tracked in `columns`.
244    pub presence: Option<ColumnId>,
245}
246
247/// Maps an embedded enum field to its discriminant column and per-variant data columns.
248///
249/// The discriminant column stores the active variant's discriminant (integer or string).
250/// Each data variant additionally has nullable columns for its fields; unit variants
251/// have no extra columns (all variant-field columns are NULL for them).
252///
253/// # Examples
254///
255/// ```ignore
256/// use toasty_core::schema::mapping::FieldEnum;
257///
258/// let e: &FieldEnum = field.as_enum().unwrap();
259/// println!("discriminant column: {:?}", e.discriminant.column);
260/// println!("{} variants", e.variants.len());
261/// ```
262#[derive(Debug, Clone)]
263pub struct FieldEnum {
264    /// Mapping for the discriminant column.
265    pub discriminant: FieldPrimitive,
266
267    /// Per-variant mappings, in the same order as `app::EmbeddedEnum::variants`.
268    pub variants: Vec<EnumVariant>,
269
270    /// Update coverage mask for the enum field (singleton: the whole enum changes atomically).
271    pub field_mask: PathFieldSet,
272
273    /// Sub-projection from the root model field to this enum field.
274    pub sub_projection: Projection,
275
276    /// Pre-computed default expression for this embedded enum.
277    ///
278    /// For unit-only enums this is the discriminant column reference. For
279    /// data-carrying enums it is the full `Match { disc, arms[], else }`
280    /// expression with per-arm records (currently identical to the raw
281    /// `table_to_model` shape with deferred fields masked in each arm.
282    pub default_returning: stmt::Expr,
283}
284
285/// Mapping for a single variant of an embedded enum.
286///
287/// # Examples
288///
289/// ```ignore
290/// use toasty_core::schema::mapping::EnumVariant;
291///
292/// for variant in &enum_mapping.variants {
293///     println!("discriminant={}, fields={}", variant.discriminant, variant.fields.len());
294/// }
295/// ```
296#[derive(Debug, Clone)]
297pub struct EnumVariant {
298    /// The discriminant value for this variant (`Value::I64` or `Value::String`).
299    pub discriminant: crate::stmt::Value,
300
301    /// Field mappings for this variant's data fields, in declaration order.
302    /// Empty for unit variants. Supports nesting (each entry is a full `Field`).
303    pub fields: Vec<Field>,
304}
305
306/// Maps a relation field (`BelongsTo`, `Has`).
307///
308/// Relations don't map to columns in this table -- they are resolved through
309/// joins or foreign keys in other tables. A unique bit is assigned in the
310/// model's field mask space so that relation assignments are detected uniformly
311/// through the same mask intersection logic used for primitive and embedded fields.
312///
313/// # Examples
314///
315/// ```ignore
316/// use toasty_core::schema::mapping::FieldRelation;
317///
318/// if field.is_relation() {
319///     // No columns to iterate over
320///     assert_eq!(field.columns().count(), 0);
321/// }
322/// ```
323#[derive(Debug, Clone)]
324pub struct FieldRelation {
325    /// Update coverage mask for this relation field.
326    pub field_mask: PathFieldSet,
327}