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