Skip to main content

rust_ef/
metadata.rs

1//! Model metadata types for describing entities, properties, and relationships.
2//!
3//! These types form the metadata layer of rust-ef, analogous to EFCore's
4//! `IEntityType`, `IProperty`, `INavigation`, etc.
5
6use std::any::TypeId;
7use std::borrow::Cow;
8use std::collections::HashMap;
9
10// ---------------------------------------------------------------------------
11// PropertyMeta ?describes a single column / property
12// ---------------------------------------------------------------------------
13
14/// Metadata describing a property (column) of an entity type.
15/// Corresponds to EFCore's `IProperty`.
16#[derive(Debug, Clone)]
17pub struct PropertyMeta {
18    /// The Rust field name.
19    pub field_name: Cow<'static, str>,
20    /// The database column name (may differ from field_name via #[column]).
21    pub column_name: Cow<'static, str>,
22    /// The Rust type identifier.
23    pub type_id: TypeId,
24    /// Human-readable type name for debugging.
25    pub type_name: Cow<'static, str>,
26    /// Whether this property is part of the primary key.
27    pub is_primary_key: bool,
28    /// Whether this property is auto-increment / identity.
29    pub is_auto_increment: bool,
30    /// Whether this property is required (NOT NULL).
31    pub is_required: bool,
32    /// Whether this property is a foreign key.
33    pub is_foreign_key: bool,
34    /// Whether this property is used for optimistic concurrency control.
35    pub is_concurrency_token: bool,
36    /// Maximum length for string columns (None = unbounded).
37    pub max_length: Option<usize>,
38    /// Whether this property has a unique index.
39    pub is_unique: bool,
40    /// Whether this property has a regular index.
41    pub has_index: bool,
42    /// Whether this property is excluded from mapping (NotMapped).
43    pub is_not_mapped: bool,
44}
45
46/// Builder for configuring [`PropertyMeta`].
47pub struct PropertyMetaBuilder {
48    meta: PropertyMeta,
49}
50
51impl PropertyMetaBuilder {
52    pub fn new(field_name: &'static str, type_id: TypeId, type_name: &'static str) -> Self {
53        Self {
54            meta: PropertyMeta {
55                field_name: Cow::Borrowed(field_name),
56                column_name: Cow::Borrowed(field_name),
57                type_id,
58                type_name: Cow::Borrowed(type_name),
59                is_primary_key: false,
60                is_auto_increment: false,
61                is_required: false,
62                is_foreign_key: false,
63                is_concurrency_token: false,
64                max_length: None,
65                is_unique: false,
66                has_index: false,
67                is_not_mapped: false,
68            },
69        }
70    }
71
72    pub fn column_name(mut self, name: &'static str) -> Self {
73        self.meta.column_name = Cow::Borrowed(name);
74        self
75    }
76
77    pub fn is_primary_key(mut self, v: bool) -> Self {
78        self.meta.is_primary_key = v;
79        self
80    }
81
82    pub fn is_auto_increment(mut self, v: bool) -> Self {
83        self.meta.is_auto_increment = v;
84        self
85    }
86
87    pub fn is_required(mut self, v: bool) -> Self {
88        self.meta.is_required = v;
89        self
90    }
91
92    pub fn is_foreign_key(mut self, v: bool) -> Self {
93        self.meta.is_foreign_key = v;
94        self
95    }
96
97    pub fn is_concurrency_token(mut self, v: bool) -> Self {
98        self.meta.is_concurrency_token = v;
99        self
100    }
101
102    pub fn max_length(mut self, n: usize) -> Self {
103        self.meta.max_length = Some(n);
104        self
105    }
106
107    pub fn is_unique(mut self, v: bool) -> Self {
108        self.meta.is_unique = v;
109        self
110    }
111
112    pub fn has_index(mut self, v: bool) -> Self {
113        self.meta.has_index = v;
114        self
115    }
116
117    pub fn is_not_mapped(mut self, v: bool) -> Self {
118        self.meta.is_not_mapped = v;
119        self
120    }
121
122    pub fn build(self) -> PropertyMeta {
123        self.meta
124    }
125}
126
127// ---------------------------------------------------------------------------
128// NavigationMeta ?describes a navigation property (relationship)
129// ---------------------------------------------------------------------------
130
131/// Describes the type of navigation.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum NavigationKind {
134    /// BelongsTo<T> ?reference to a single related entity (FK on this side).
135    BelongsTo,
136    /// HasOne<T> ?reference to a single related entity (FK on other side).
137    HasOne,
138    /// HasMany<T> ?collection of related entities.
139    HasMany,
140    /// HasMany<T, Join> ?many-to-many via join entity.
141    ManyToMany,
142}
143
144/// Metadata describing a navigation property (relationship).
145/// Corresponds to EFCore's `INavigation`.
146#[derive(Debug, Clone)]
147pub struct NavigationMeta {
148    /// The Rust field name of this navigation property.
149    pub field_name: Cow<'static, str>,
150    /// The kind of navigation (BelongsTo, HasOne, HasMany).
151    pub kind: NavigationKind,
152    /// The related entity's TypeId.
153    pub related_type_id: TypeId,
154    /// The related entity's type name.
155    pub related_type_name: Cow<'static, str>,
156    /// The foreign key property name on the dependent entity.
157    pub foreign_key_field: Option<Cow<'static, str>>,
158    /// The inverse navigation field name on the related entity.
159    pub inverse_navigation: Option<Cow<'static, str>>,
160    /// For many-to-many: the join entity TypeId.
161    pub through_type_id: Option<TypeId>,
162    /// Join table name (many-to-many).
163    pub through_table: Option<Cow<'static, str>>,
164    /// FK column on the join table pointing to the principal entity.
165    pub through_parent_fk: Option<Cow<'static, str>>,
166    /// FK column on the join table pointing to the related entity.
167    pub through_related_fk: Option<Cow<'static, str>>,
168    /// Column index in join-table rows for the principal FK.
169    pub through_parent_fk_index: usize,
170    /// Column index in join-table rows for the related FK.
171    pub through_related_fk_index: usize,
172    /// Related entity table name (for eager loading).
173    pub related_table: Option<Cow<'static, str>>,
174    /// Foreign key column on the dependent/related table.
175    pub fk_column: Option<Cow<'static, str>>,
176    /// Referenced key column on the principal table.
177    pub referenced_key_column: Option<Cow<'static, str>>,
178    /// Column index in `SELECT *` rows for the FK (HasMany grouping).
179    pub fk_row_index: usize,
180    /// Column index in `SELECT *` rows for the related PK (BelongsTo lookup).
181    pub pk_row_index: usize,
182    /// Resolves metadata for the related entity type (nested Include / ThenInclude).
183    pub related_entity_meta: Option<fn() -> EntityTypeMeta>,
184}
185
186// ---------------------------------------------------------------------------
187// EntityTypeMeta ?describes an entity type as a whole
188// ---------------------------------------------------------------------------
189
190/// Metadata describing an entity type.
191/// Corresponds to EFCore's `IEntityType`.
192#[derive(Debug, Clone)]
193pub struct EntityTypeMeta {
194    /// The Rust type identifier.
195    pub type_id: TypeId,
196    /// The Rust struct name.
197    pub type_name: Cow<'static, str>,
198    /// The database table name.
199    pub table_name: Cow<'static, str>,
200    /// Properties (columns) of this entity.
201    pub properties: Vec<PropertyMeta>,
202    /// Navigation properties (relationships) of this entity.
203    pub navigations: Vec<NavigationMeta>,
204    /// The name of the primary key property (simplified; multi-key via Vec<String> in practice).
205    pub primary_keys: Vec<Cow<'static, str>>,
206    /// Lazily-built `field_name -> properties index` map for O(1) lookup.
207    /// Built on first `find_property` call; empty after clone (rebuilt on demand).
208    /// `#[doc(hidden)]` — internal cache, do not set manually.
209    #[doc(hidden)]
210    pub property_index: std::sync::OnceLock<HashMap<String, usize>>,
211    /// Lazily-built `field_name -> navigations index` map for O(1) lookup.
212    /// Built on first `find_navigation` call; empty after clone (rebuilt on demand).
213    /// `#[doc(hidden)]` — internal cache, do not set manually.
214    #[doc(hidden)]
215    pub navigation_index: std::sync::OnceLock<HashMap<String, usize>>,
216}
217
218impl EntityTypeMeta {
219    /// Find a property by field name.
220    ///
221    /// Uses a lazily-built HashMap index for O(1) lookup instead of linear
222    /// scan. The index is cached for the lifetime of this `EntityTypeMeta`;
223    /// a cloned meta rebuilds its own index on first access.
224    pub fn find_property(&self, field_name: &str) -> Option<&PropertyMeta> {
225        let idx = self
226            .property_index
227            .get_or_init(|| {
228                self.properties
229                    .iter()
230                    .enumerate()
231                    .map(|(i, p)| (p.field_name.to_string(), i))
232                    .collect()
233            })
234            .get(field_name)
235            .copied();
236        idx.and_then(|i| self.properties.get(i))
237    }
238
239    /// Find a navigation by field name.
240    ///
241    /// Uses a lazily-built HashMap index for O(1) lookup instead of linear
242    /// scan. The index is cached for the lifetime of this `EntityTypeMeta`;
243    /// a cloned meta rebuilds its own index on first access.
244    pub fn find_navigation(&self, field_name: &str) -> Option<&NavigationMeta> {
245        let idx = self
246            .navigation_index
247            .get_or_init(|| {
248                self.navigations
249                    .iter()
250                    .enumerate()
251                    .map(|(i, n)| (n.field_name.to_string(), i))
252                    .collect()
253            })
254            .get(field_name)
255            .copied();
256        idx.and_then(|i| self.navigations.get(i))
257    }
258
259    /// Get the primary key property.
260    pub fn primary_key_property(&self) -> Option<&PropertyMeta> {
261        self.properties.iter().find(|p| p.is_primary_key)
262    }
263
264    /// Returns properties that are not navigation and not mapped.
265    pub fn mapped_scalar_properties(&self) -> impl Iterator<Item = &PropertyMeta> {
266        self.properties.iter().filter(|p| !p.is_not_mapped)
267    }
268}
269
270impl Default for EntityTypeMeta {
271    /// Produces an empty `EntityTypeMeta` with lazily-built index caches.
272    /// `type_id` defaults to `TypeId::of::<()>()` since `TypeId` itself has no
273    /// `Default` impl; callers always override it in struct literals via
274    /// `..EntityTypeMeta::default()`.
275    fn default() -> Self {
276        Self {
277            type_id: TypeId::of::<()>(),
278            type_name: Cow::Borrowed(""),
279            table_name: Cow::Borrowed(""),
280            properties: Vec::new(),
281            navigations: Vec::new(),
282            primary_keys: Vec::new(),
283            property_index: std::sync::OnceLock::new(),
284            navigation_index: std::sync::OnceLock::new(),
285        }
286    }
287}