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, Predicate, 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: impl Into<BoolExpr>) {
77        self.query_filter = Some(filter.into());
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 predicate (`linq!(|b: T| ...)`).
164    pub fn filter(&self, predicate: impl Into<Predicate<T>>) -> QueryBuilder<T> {
165        self.query().filter(predicate)
166    }
167
168    /// Checks whether an entity with the given key values exists in the database.
169    pub async fn exists_by_id(&self, key_values: HashMap<String, DbValue>) -> EFResult<bool>
170    where
171        T: IFromRow
172            + INavigationSetter
173            + IGetKeyValues
174            + IEntitySnapshot
175            + crate::entity::ILazyInit,
176    {
177        let pairs: Vec<(&str, DbValue)> = key_values
178            .iter()
179            .map(|(k, v)| (k.as_str(), v.clone()))
180            .collect();
181        Ok(self.query().find_by_key(&pairs).await?.is_some())
182    }
183}
184
185// ---------------------------------------------------------------------------
186// IQueryable<T> implementation
187// ---------------------------------------------------------------------------
188
189impl<T: IEntityType> IQueryable<T> for DbSet<T> {
190    fn query(&self) -> QueryBuilder<T> {
191        let mut qb = match &self.provider {
192            Some(p) => QueryBuilder::with_provider(&self.table_name, p.clone()),
193            None => QueryBuilder::new(&self.table_name),
194        };
195        if let Some(ref filter) = self.query_filter {
196            qb = qb.apply_query_filter(filter.clone());
197        }
198        qb.with_filter_map(self.filter_map.clone())
199            .with_lazy_loading(self.lazy_loading_enabled)
200    }
201}
202
203// ---------------------------------------------------------------------------
204// IDbSet<T> implementation
205// ---------------------------------------------------------------------------
206
207impl<T: IEntityType + IEntitySnapshot> IDbSet<T> for DbSet<T> {
208    fn clear_entries(&mut self) {
209        self.entries.clear();
210    }
211
212    fn len(&self) -> usize {
213        self.entries.len()
214    }
215
216    fn is_empty(&self) -> bool {
217        self.entries.is_empty()
218    }
219}