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 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}