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}