Skip to main content

rust_ef/
model_builder.rs

1//! Model builder  - ?Fluent API for configuring entity types.
2//!
3//! The `ModelBuilder` is the central configuration hub. It collects
4//! `EntityTypeMeta` from all registered entity types and entity
5//! configurations, and produces the final metadata model used by
6//! the migration engine and query builder.
7
8use crate::entity::{IEntitySnapshot, IEntityType};
9use crate::metadata::EntityTypeMeta;
10use crate::provider::DbValue;
11use crate::query::{BoolExpr, CompiledFilter};
12use std::any::TypeId;
13use std::collections::HashMap;
14use std::marker::PhantomData;
15use std::sync::Arc;
16use std::sync::OnceLock;
17
18// ---------------------------------------------------------------------------
19// EntityConfig  - ?stored configuration overrides
20// ---------------------------------------------------------------------------
21
22#[derive(Debug, Clone, Default)]
23pub(crate) struct EntityConfig {
24    pub(crate) table_name: Option<String>,
25    pub(crate) primary_key_fields: Option<Vec<String>>,
26    pub(crate) property_overrides: HashMap<String, PropertyConfigOverride>,
27    pub(crate) query_filter: Option<BoolExpr>,
28    pub(crate) seed_rows: Vec<HashMap<String, DbValue>>,
29}
30
31#[derive(Debug, Clone, Default)]
32pub(crate) struct PropertyConfigOverride {
33    pub(crate) column_name: Option<String>,
34    pub(crate) is_required: Option<bool>,
35    pub(crate) max_length: Option<usize>,
36    pub(crate) is_unique: Option<bool>,
37    pub(crate) has_index: Option<bool>,
38}
39
40// ---------------------------------------------------------------------------
41// ModelBuilder
42// ---------------------------------------------------------------------------
43
44/// Central configuration point for the EF model.
45pub struct ModelBuilder {
46    entity_metas: Vec<EntityTypeMeta>,
47    configs: HashMap<TypeId, EntityConfig>,
48    /// Cache of `build()` output. Invalidated by any mutation.
49    build_cache: OnceLock<Vec<EntityTypeMeta>>,
50    /// Cache of `filters_by_table()` output, shared via `Arc` so DbSets can
51    /// hold a cheap clone without re-traversing configs on every query.
52    filter_cache: OnceLock<Arc<HashMap<String, CompiledFilter>>>,
53}
54
55impl ModelBuilder {
56    pub fn new() -> Self {
57        Self {
58            entity_metas: Vec::new(),
59            configs: HashMap::new(),
60            build_cache: OnceLock::new(),
61            filter_cache: OnceLock::new(),
62        }
63    }
64
65    /// Constructs a `ModelBuilder` pre-populated from a cached `BuiltMetadata`.
66    ///
67    /// Used by `DbContext::from_options()` to skip re-iterating `inventory::iter`
68    /// and re-running `IEntityTypeConfiguration::configure()` on every request.
69    /// The `build_cache` and `filter_cache` are left empty (lazy) — they will
70    /// be populated on first access, same as the non-cached path.
71    ///
72    /// Per-instance mutations (`has_query_filter`, `entity::<T>()`, etc.) after
73    /// construction only affect this `ModelBuilder` instance, not the cache.
74    pub(crate) fn from_built(built: &crate::metadata_cache::BuiltMetadata) -> Self {
75        Self {
76            entity_metas: built.model_metas.clone(),
77            configs: built.configs.clone(),
78            build_cache: OnceLock::new(),
79            filter_cache: OnceLock::new(),
80        }
81    }
82
83    /// Read-only access to the `configs` map. Used by `MetadataCache::build()`
84    /// to snapshot the configs produced by `IEntityTypeConfiguration::configure()`
85    /// callbacks into the process-level cache.
86    pub(crate) fn configs(&self) -> &HashMap<TypeId, EntityConfig> {
87        &self.configs
88    }
89
90    /// Read-only access to the `entity_metas` vec. Used by `MetadataCache::build()`
91    /// to snapshot the registered entity metas into the process-level cache.
92    pub(crate) fn entity_metas_vec(&self) -> &[EntityTypeMeta] {
93        &self.entity_metas
94    }
95
96    /// Drops the cached `build()` and `filters_by_table()` results.
97    ///
98    /// Called by every mutating method so that subsequent reads observe the
99    /// new configuration. Must be `&mut self` to enforce exclusive access.
100    fn invalidate_cache(&mut self) {
101        self.build_cache.take();
102        self.filter_cache.take();
103    }
104
105    /// Test-only hook: returns `true` if `build()` has populated its cache.
106    #[doc(hidden)]
107    pub fn build_cache_populated(&self) -> bool {
108        self.build_cache.get().is_some()
109    }
110
111    /// Test-only hook: returns `true` if `filters_by_table()` has populated
112    /// its cache.
113    #[doc(hidden)]
114    pub fn filter_cache_populated(&self) -> bool {
115        self.filter_cache.get().is_some()
116    }
117
118    pub fn entity<T: IEntityType>(&mut self) -> EntityTypeBuilder<'_, T> {
119        let meta = T::entity_meta();
120        let type_id = meta.type_id;
121        if !self.entity_metas.iter().any(|m| m.type_id == type_id) {
122            self.entity_metas.push(meta);
123            self.invalidate_cache();
124        }
125        self.configs.entry(type_id).or_default();
126        EntityTypeBuilder::new(self, type_id)
127    }
128
129    pub fn apply_configuration<C, T>(&mut self) -> &mut Self
130    where
131        C: IEntityTypeConfiguration<T> + Default + Send + Sync + 'static,
132        T: IEntityType,
133    {
134        let meta = T::entity_meta();
135        let type_id = meta.type_id;
136        if !self.entity_metas.iter().any(|m| m.type_id == type_id) {
137            self.entity_metas.push(meta);
138            self.invalidate_cache();
139        }
140
141        let config = C::default();
142        let mut builder = EntityTypeBuilder::new(self, type_id);
143        config.configure(&mut builder);
144
145        self
146    }
147
148    pub fn build(&self) -> Vec<EntityTypeMeta> {
149        self.build_cache
150            .get_or_init(|| {
151                self.entity_metas
152                    .iter()
153                    .map(|meta| self.apply_config_to_meta(meta))
154                    .collect()
155            })
156            .clone()
157    }
158
159    fn apply_config_to_meta(&self, meta: &EntityTypeMeta) -> EntityTypeMeta {
160        let config = match self.configs.get(&meta.type_id) {
161            Some(c) => c,
162            None => return meta.clone(),
163        };
164
165        let mut result = meta.clone();
166        if let Some(ref table) = config.table_name {
167            result.table_name = std::borrow::Cow::Owned(table.clone());
168        }
169
170        if let Some(ref pk_fields) = config.primary_key_fields {
171            for prop in &mut result.properties {
172                prop.is_primary_key = pk_fields.iter().any(|f| f == prop.field_name.as_ref());
173            }
174        }
175
176        for prop in &mut result.properties {
177            if let Some(override_cfg) = config.property_overrides.get(prop.field_name.as_ref()) {
178                if let Some(ref col) = override_cfg.column_name {
179                    prop.column_name = std::borrow::Cow::Owned(col.clone());
180                }
181                if let Some(required) = override_cfg.is_required {
182                    prop.is_required = required;
183                }
184                if let Some(max_len) = override_cfg.max_length {
185                    prop.max_length = Some(max_len);
186                }
187                if let Some(unique) = override_cfg.is_unique {
188                    prop.is_unique = unique;
189                }
190                if let Some(index) = override_cfg.has_index {
191                    prop.has_index = index;
192                }
193            }
194        }
195
196        result
197    }
198
199    /// Escape hatch for direct mutation of `entity_metas`.
200    ///
201    /// **Cache caveat**: this returns a `&mut Vec`, so mutations performed
202    /// through it cannot auto-invalidate `build()` / `filters_by_table()`.
203    /// Callers that mutate via this handle must drop the borrow and perform
204    /// any subsequent Fluent API mutation (or never read the caches again
205    /// in this context). Prefer `register_entity_meta()` / `entity::<T>()`
206    /// for cache-safe registration.
207    pub fn entity_metas_mut(&mut self) -> &mut Vec<EntityTypeMeta> {
208        // Note: cannot call invalidate_cache() here because the returned
209        // &mut Vec would conflict. Documented as a manual-invalidation path.
210        &mut self.entity_metas
211    }
212
213    pub fn find_entity<T: IEntityType>(&self) -> Option<&EntityTypeMeta> {
214        let type_id = TypeId::of::<T>();
215        self.entity_metas.iter().find(|m| m.type_id == type_id)
216    }
217
218    /// Returns `true` if an entity with the given `type_id` is already
219    /// registered in STORE B.
220    pub fn has_entity(&self, type_id: TypeId) -> bool {
221        self.entity_metas.iter().any(|m| m.type_id == type_id)
222    }
223
224    /// Registers an entity meta directly, without going through
225    /// `entity::<T>()`. Used by `DbContext::discover_entities()` to populate
226    /// STORE B from `inventory::iter::<EntityRegistration>()`.
227    ///
228    /// Ensures a corresponding `EntityConfig` entry exists so that subsequent
229    /// Fluent API calls (`to_table`, `property_named`, etc.) have somewhere
230    /// to write their overrides.
231    pub fn register_entity_meta(&mut self, meta: EntityTypeMeta) {
232        let type_id = meta.type_id;
233        if !self.entity_metas.iter().any(|m| m.type_id == type_id) {
234            self.entity_metas.push(meta);
235            self.invalidate_cache();
236        }
237        self.configs.entry(type_id).or_default();
238    }
239
240    /// Registers a global query filter for entity type `T`.
241    ///
242    /// Accepts a `BoolExpr` produced by `linq!(filter |b: T| ...)`.
243    pub fn has_query_filter<T: IEntityType>(&mut self, filter: BoolExpr) -> &mut Self {
244        let type_id = TypeId::of::<T>();
245        let config = self.configs.entry(type_id).or_default();
246        config.query_filter = Some(filter);
247
248        let meta = T::entity_meta();
249        if !self.entity_metas.iter().any(|m| m.type_id == type_id) {
250            self.entity_metas.push(meta);
251        }
252
253        self.invalidate_cache();
254        self
255    }
256
257    pub fn get_query_filter(&self, type_id: &TypeId) -> Option<&BoolExpr> {
258        self.configs
259            .get(type_id)
260            .and_then(|c| c.query_filter.as_ref())
261    }
262
263    /// Collects all registered query filters keyed by compile-time table name
264    /// (i.e. `EntityTypeMeta.table_name` from `#[table("...")]`).
265    ///
266    /// This must match the `related_table` stored in navigation metadata, which
267    /// is also the compile-time name — NOT the Fluent API override. If a Fluent
268    /// `to_table()` override renames the table, the navigation SQL and the
269    /// filter lookup both use the compile-time name, staying consistent.
270    ///
271    /// Each filter is wrapped in a `CompiledFilter` that pre-collects parameter
272    /// values at registration time, avoiding redundant `collect_bool_expr_values`
273    /// traversals on every query. The SQL fragment is still compiled per query
274    /// (dialect-specific placeholders depend on the provider and query context).
275    pub fn filters_by_table(&self) -> Arc<HashMap<String, CompiledFilter>> {
276        self.filter_cache
277            .get_or_init(|| {
278                let mut map = HashMap::new();
279                for meta in &self.entity_metas {
280                    if let Some(config) = self.configs.get(&meta.type_id) {
281                        if let Some(filter) = &config.query_filter {
282                            map.insert(
283                                meta.table_name.to_string(),
284                                CompiledFilter::new(filter.clone()),
285                            );
286                        }
287                    }
288                }
289                Arc::new(map)
290            })
291            .clone()
292    }
293
294    /// Returns seed rows configured via `EntityTypeBuilder::has_data`.
295    pub fn seed_rows_for(&self, type_id: &TypeId) -> &[HashMap<String, DbValue>] {
296        self.configs
297            .get(type_id)
298            .map(|c| c.seed_rows.as_slice())
299            .unwrap_or(&[])
300    }
301}
302
303impl Default for ModelBuilder {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309// ---------------------------------------------------------------------------
310// EntityTypeBuilder<T>
311// ---------------------------------------------------------------------------
312
313pub struct EntityTypeBuilder<'a, T> {
314    model: &'a mut ModelBuilder,
315    type_id: TypeId,
316    _phantom: PhantomData<T>,
317}
318
319impl<'a, T> EntityTypeBuilder<'a, T> {
320    pub fn new(model: &'a mut ModelBuilder, type_id: TypeId) -> Self {
321        Self {
322            model,
323            type_id,
324            _phantom: PhantomData,
325        }
326    }
327
328    pub fn to_table(&mut self, name: &str) -> &mut Self {
329        if let Some(config) = self.model.configs.get_mut(&self.type_id) {
330            config.table_name = Some(name.to_string());
331        }
332        self.model.invalidate_cache();
333        self
334    }
335
336    pub fn property_named<'b>(&'b mut self, field_name: &'static str) -> PropertyBuilder<'b, T> {
337        PropertyBuilder {
338            model: self.model,
339            type_id: self.type_id,
340            field_name,
341            _entity: PhantomData,
342            _value: PhantomData,
343        }
344    }
345
346    pub fn has_key_named(&mut self, field_name: &str) -> &mut Self {
347        if let Some(config) = self.model.configs.get_mut(&self.type_id) {
348            config.primary_key_fields = Some(vec![field_name.to_string()]);
349        }
350        self.model.invalidate_cache();
351        self
352    }
353
354    pub fn has_keys(&mut self, field_names: &[&str]) -> &mut Self {
355        if let Some(config) = self.model.configs.get_mut(&self.type_id) {
356            config.primary_key_fields = Some(field_names.iter().map(|s| s.to_string()).collect());
357        }
358        self.model.invalidate_cache();
359        self
360    }
361
362    /// Configures the primary key from column constants.
363    ///
364    /// `#[doc(hidden)]` — called by `linq!(key |b: T| b.id)` expansion (Form C).
365    #[doc(hidden)]
366    pub fn has_key(&mut self, columns: &'static [&'static str]) -> &mut Self {
367        if let Some(config) = self.model.configs.get_mut(&self.type_id) {
368            config.primary_key_fields = Some(columns.iter().map(|s| s.to_string()).collect());
369        }
370        self.model.invalidate_cache();
371        self
372    }
373
374    /// Configures a multi-column index from column constants.
375    ///
376    /// `#[doc(hidden)]` — called by `linq!(index |b: T| (b.author_id, b.created_at))`
377    /// expansion (Form C). Currently marks the first column as indexed; full
378    /// composite index support will be added in a future iteration.
379    #[doc(hidden)]
380    pub fn has_index(&mut self, columns: &'static [&'static str]) -> &mut Self {
381        if let Some(config) = self.model.configs.get_mut(&self.type_id) {
382            // Mark each column as indexed (single-column indexes).
383            for col in columns {
384                let override_cfg = config
385                    .property_overrides
386                    .entry(col.to_string())
387                    .or_default();
388                override_cfg.has_index = Some(true);
389            }
390        }
391        self.model.invalidate_cache();
392        self
393    }
394
395    /// Seeds initial data (applied on `DbContext::ensure_created`).
396    pub fn has_data(&mut self, data: &[T]) -> &mut Self
397    where
398        T: IEntitySnapshot,
399    {
400        if let Some(config) = self.model.configs.get_mut(&self.type_id) {
401            config.seed_rows = data.iter().map(|e| e.snapshot()).collect();
402        }
403        self.model.invalidate_cache();
404        self
405    }
406}
407
408// ---------------------------------------------------------------------------
409// PropertyBuilder
410// ---------------------------------------------------------------------------
411
412pub struct PropertyBuilder<'a, T, V = ()> {
413    model: &'a mut ModelBuilder,
414    type_id: TypeId,
415    field_name: &'static str,
416    _entity: PhantomData<T>,
417    _value: PhantomData<V>,
418}
419
420impl<'a, T, V> PropertyBuilder<'a, T, V> {
421    fn override_entry(&mut self) -> &mut PropertyConfigOverride {
422        // Invalidate caches before mutating so a subsequent `build()` /
423        // `filters_by_table()` reflects the new override.
424        self.model.invalidate_cache();
425        self.model
426            .configs
427            .entry(self.type_id)
428            .or_default()
429            .property_overrides
430            .entry(self.field_name.to_string())
431            .or_default()
432    }
433
434    pub fn is_required(mut self) -> Self {
435        self.override_entry().is_required = Some(true);
436        self
437    }
438
439    pub fn has_max_length(mut self, n: usize) -> Self {
440        self.override_entry().max_length = Some(n);
441        self
442    }
443
444    pub fn has_column_name(mut self, name: &'static str) -> Self {
445        self.override_entry().column_name = Some(name.to_string());
446        self
447    }
448
449    pub fn is_unique(mut self) -> Self {
450        self.override_entry().is_unique = Some(true);
451        self
452    }
453
454    pub fn has_index(mut self) -> Self {
455        self.override_entry().has_index = Some(true);
456        self
457    }
458}
459
460// ---------------------------------------------------------------------------
461// IEntityTypeConfiguration<T>
462// ---------------------------------------------------------------------------
463
464pub trait IEntityTypeConfiguration<T: IEntityType> {
465    fn configure(&self, entity: &mut EntityTypeBuilder<'_, T>);
466}