Skip to main content

rust_ef/
db_set.rs

1//! `DbSet<T>` — typed entity collection for querying and storage.
2//!
3//! `DbSet<T>` holds typed entities + `entry_id` links to `ChangeTracker`.
4//! Tracking state (EntityState, original snapshot, modified properties,
5//! is_upsert) lives in `ChangeTracker`, joined by `entry_id` during save.
6//! Mutations (`add`, `attach`, `update`, `upsert`, `remove`) go through
7//! `DbContext` methods which coordinate DbSet + ChangeTracker.
8
9use crate::entity::{IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter};
10use crate::error::EFResult;
11use crate::provider::{DbValue, IDatabaseProvider};
12use crate::query::{BoolExpr, IQueryable, QueryBuilder};
13use std::collections::HashMap;
14use std::sync::Arc;
15
16/// A single entry in a `DbSet` — the typed entity + its `ChangeTracker` id.
17///
18/// Tracking state is NOT stored here. Use `ChangeTracker::entry_state(id)` /
19/// `entry_original(id)` / `entry_modified(id)` / `entry_is_upsert(id)` to
20/// query tracking state, joined by `entry_id`.
21pub struct DbSetEntry<T: IEntityType> {
22    pub entity: T,
23    pub entry_id: u64,
24}
25
26/// Collection-level operations on a typed entity collection.
27///
28/// Mutation methods (`add`, `attach`, `update`, `upsert`, `remove`) are on
29/// `DbContext`, not here — they need to coordinate with `ChangeTracker`.
30pub trait IDbSet<T: IEntityType>: IQueryable<T> + Send + Sync {
31    /// Clears all entries from the set (does not affect ChangeTracker).
32    fn clear_entries(&mut self);
33
34    /// Returns the number of entries.
35    fn len(&self) -> usize;
36
37    /// Returns whether the set is empty.
38    fn is_empty(&self) -> bool;
39}
40
41pub struct DbSet<T: IEntityType> {
42    pub(crate) entries: Vec<DbSetEntry<T>>,
43    table_name: String,
44    provider: Option<Arc<dyn IDatabaseProvider>>,
45    query_filter: Option<BoolExpr>,
46    filter_map: Option<Arc<HashMap<String, crate::query::CompiledFilter>>>,
47    lazy_loading_enabled: bool,
48}
49
50impl<T: IEntityType + IEntitySnapshot> DbSet<T> {
51    pub fn new(table_name: impl Into<String>) -> Self {
52        Self {
53            entries: Vec::new(),
54            table_name: table_name.into(),
55            provider: None,
56            query_filter: None,
57            filter_map: None,
58            lazy_loading_enabled: false,
59        }
60    }
61
62    pub fn with_provider(
63        table_name: impl Into<String>,
64        provider: Arc<dyn IDatabaseProvider>,
65    ) -> Self {
66        Self {
67            entries: Vec::new(),
68            table_name: table_name.into(),
69            provider: Some(provider),
70            query_filter: None,
71            filter_map: None,
72            lazy_loading_enabled: false,
73        }
74    }
75
76    pub fn set_query_filter(&mut self, filter: BoolExpr) {
77        self.query_filter = Some(filter);
78    }
79
80    /// Sets the global filter map (table_name → BoolExpr) used by
81    /// NavigationLoader to scope secondary queries.
82    pub fn set_filter_map(&mut self, map: Arc<HashMap<String, crate::query::CompiledFilter>>) {
83        self.filter_map = Some(map);
84    }
85
86    /// Propagates the lazy-loading flag from `DbContextOptions` to this set.
87    pub(crate) fn set_lazy_loading_enabled(&mut self, enabled: bool) {
88        self.lazy_loading_enabled = enabled;
89    }
90
91    /// Returns the configured query filter, if any. Used by save phases
92    /// to apply tenant isolation to UPDATE/DELETE WHERE clauses.
93    pub(crate) fn query_filter(&self) -> Option<&BoolExpr> {
94        self.query_filter.as_ref()
95    }
96
97    pub fn set_provider(&mut self, provider: Arc<dyn IDatabaseProvider>) {
98        self.provider = Some(provider);
99    }
100
101    // ── Internal: entry management (used by DbContext mutations) ──
102
103    /// Pushes a new entry with a pre-assigned `entry_id` from ChangeTracker.
104    pub(crate) fn push_entry(&mut self, entity: T, entry_id: u64) {
105        self.entries.push(DbSetEntry { entity, entry_id });
106    }
107
108    /// Returns the `entry_id` at the given index, or `None` if out of bounds.
109    pub(crate) fn entry_id_at(&self, idx: usize) -> Option<u64> {
110        self.entries.get(idx).map(|e| e.entry_id)
111    }
112
113    // ── Collection access ──
114
115    /// Returns an iterator over entity references.
116    pub fn tracked_entries(&self) -> impl Iterator<Item = &T> {
117        self.entries.iter().map(|e| &e.entity)
118    }
119
120    /// Returns a mutable iterator over entity references.
121    pub fn tracked_entries_mut(&mut self) -> impl Iterator<Item = &mut T> {
122        self.entries.iter_mut().map(|e| &mut e.entity)
123    }
124
125    /// Retains entries matching the predicate (drops others).
126    pub fn retain(&mut self, f: impl FnMut(&DbSetEntry<T>) -> bool) {
127        self.entries.retain(f);
128    }
129
130    /// Convenience inherent method — delegates to `IDbSet::clear_entries`.
131    pub fn clear_entries(&mut self) {
132        IDbSet::clear_entries(self);
133    }
134
135    /// Convenience inherent method — delegates to `IDbSet::len`.
136    pub fn len(&self) -> usize {
137        IDbSet::len(self)
138    }
139
140    /// Convenience inherent method — delegates to `IDbSet::is_empty`.
141    pub fn is_empty(&self) -> bool {
142        IDbSet::is_empty(self)
143    }
144
145    // ── Query methods ──
146
147    /// Convenience inherent method — delegates to `IQueryable::query`.
148    pub fn query(&self) -> QueryBuilder<T> {
149        IQueryable::query(self)
150    }
151
152    /// Returns a query builder that bypasses the configured query filter.
153    /// Use for administrative / cross-tenant queries.
154    pub fn query_ignore_filters(&self) -> QueryBuilder<T> {
155        let qb = match &self.provider {
156            Some(p) => QueryBuilder::with_provider(&self.table_name, p.clone()),
157            None => QueryBuilder::new(&self.table_name),
158        };
159        qb.with_filter_map(self.filter_map.clone())
160            .with_lazy_loading(self.lazy_loading_enabled)
161    }
162
163    /// Starts a query filtered by a compile-time LINQ expression tree (`linq!()`)
164    pub fn filter<F>(&self, apply: F) -> QueryBuilder<T>
165    where
166        F: FnOnce(QueryBuilder<T>) -> QueryBuilder<T>,
167    {
168        apply(self.query())
169    }
170
171    /// Checks whether an entity with the given key values exists in the database.
172    pub async fn exists_by_id(&self, key_values: HashMap<String, DbValue>) -> EFResult<bool>
173    where
174        T: IFromRow
175            + INavigationSetter
176            + IGetKeyValues
177            + IEntitySnapshot
178            + crate::entity::ILazyInit,
179    {
180        let pairs: Vec<(&str, DbValue)> = key_values
181            .iter()
182            .map(|(k, v)| (k.as_str(), v.clone()))
183            .collect();
184        Ok(self.query().find_by_key(&pairs).await?.is_some())
185    }
186}
187
188// ---------------------------------------------------------------------------
189// IQueryable<T> implementation
190// ---------------------------------------------------------------------------
191
192impl<T: IEntityType> IQueryable<T> for DbSet<T> {
193    fn query(&self) -> QueryBuilder<T> {
194        let mut qb = match &self.provider {
195            Some(p) => QueryBuilder::with_provider(&self.table_name, p.clone()),
196            None => QueryBuilder::new(&self.table_name),
197        };
198        if let Some(ref filter) = self.query_filter {
199            qb = qb.apply_query_filter(filter.clone());
200        }
201        qb.with_filter_map(self.filter_map.clone())
202            .with_lazy_loading(self.lazy_loading_enabled)
203    }
204}
205
206// ---------------------------------------------------------------------------
207// IDbSet<T> implementation
208// ---------------------------------------------------------------------------
209
210impl<T: IEntityType + IEntitySnapshot> IDbSet<T> for DbSet<T> {
211    fn clear_entries(&mut self) {
212        self.entries.clear();
213    }
214
215    fn len(&self) -> usize {
216        self.entries.len()
217    }
218
219    fn is_empty(&self) -> bool {
220        self.entries.is_empty()
221    }
222}