Skip to main content

rust_ef/
db_set.rs

1//! DbSet<T> �?entry point for querying and manipulating entity collections.
2//!
3//! `DbSet<T>` represents a typed collection of entities that can be queried
4//! and mutated. It implements two interfaces following ISP:
5//!   - `IQueryable<T>` �?query capabilities
6//!   - `IDbSet<T>`     �?collection mutation capabilities
7
8use crate::entity::{
9    EntityState, IEntitySnapshot, IEntityType, IFromRow, IGetKeyValues, INavigationSetter,
10};
11use crate::error::EFResult;
12use crate::provider::{DbValue, IDatabaseProvider};
13use crate::query::{BoolExpr, IQueryable, QueryBuilder};
14use std::collections::HashMap;
15use std::sync::Arc;
16
17// ---------------------------------------------------------------------------
18// IDbSet<T> �?interface for entity collection manipulation
19// ---------------------------------------------------------------------------
20
21/// Interface for manipulating a typed entity collection.
22///
23/// Separates mutation concerns from query concerns (see `IQueryable<T>`).
24/// Implemented by `DbSet<T>`.
25pub trait IDbSet<T: IEntityType>: IQueryable<T> + Send + Sync {
26    /// Adds a new entity to the set in Added state.
27    fn add(&mut self, entity: T);
28
29    /// Marks all entities in the set as Deleted.
30    fn remove_all(&mut self);
31
32    /// Marks the entity at the given index as Deleted.
33    fn remove_at(&mut self, index: usize) -> EFResult<()>;
34
35    /// Attaches an existing entity in Unchanged state.
36    fn attach(&mut self, entity: T);
37
38    /// Returns references to entities in Added state.
39    fn added_entities(&self) -> Vec<&T>;
40
41    /// Returns references to entities in Modified state.
42    fn modified_entities(&self) -> Vec<&T>;
43
44    /// Returns references to entities in Deleted state.
45    fn deleted_entities(&self) -> Vec<&T>;
46
47    /// Returns an iterator over entity references and their states.
48    fn entries_with_state(&self) -> Vec<(&T, EntityState)>;
49
50    /// Clears all tracked entries from the set.
51    fn clear_entries(&mut self);
52
53    /// Returns the number of tracked entries.
54    fn len(&self) -> usize;
55
56    /// Returns whether the set is empty.
57    fn is_empty(&self) -> bool;
58}
59
60// ---------------------------------------------------------------------------
61// DbSet<T> �?concrete implementation
62// ---------------------------------------------------------------------------
63
64pub struct DbSet<T: IEntityType> {
65    pub(crate) entries: Vec<TrackedEntry<T>>,
66    table_name: String,
67    provider: Option<Arc<dyn IDatabaseProvider>>,
68    query_filter: Option<BoolExpr>,
69    filter_map: Option<Arc<HashMap<String, crate::query::CompiledFilter>>>,
70    lazy_loading_enabled: bool,
71}
72
73pub struct TrackedEntry<T: IEntityType> {
74    pub entity: T,
75    pub state: EntityState,
76    /// Snapshot taken when the entity was attached (for change detection).
77    pub original: Option<HashMap<String, DbValue>>,
78    /// Field names that differ from `original` (populated by `detect_changes`).
79    /// Empty when `detect_changes` hasn't run or the entity was marked Modified
80    /// directly via `update()`. Used by `execute_updates` to generate partial
81    /// UPDATE statements (SET only dirty columns).
82    pub modified_properties: Vec<String>,
83    /// When `true`, the entry is in Added state but should be saved via
84    /// `execute_upserts` (INSERT ... ON CONFLICT DO UPDATE) instead of a plain
85    /// INSERT. Set by `upsert()`.
86    pub is_upsert: bool,
87}
88
89impl<T: IEntityType + IEntitySnapshot> DbSet<T> {
90    pub fn new(table_name: impl Into<String>) -> Self {
91        Self {
92            entries: Vec::new(),
93            table_name: table_name.into(),
94            provider: None,
95            query_filter: None,
96            filter_map: None,
97            lazy_loading_enabled: false,
98        }
99    }
100
101    pub fn with_provider(
102        table_name: impl Into<String>,
103        provider: Arc<dyn IDatabaseProvider>,
104    ) -> Self {
105        Self {
106            entries: Vec::new(),
107            table_name: table_name.into(),
108            provider: Some(provider),
109            query_filter: None,
110            filter_map: None,
111            lazy_loading_enabled: false,
112        }
113    }
114
115    pub fn set_query_filter(&mut self, filter: BoolExpr) {
116        self.query_filter = Some(filter);
117    }
118
119    /// Sets the global filter map (table_name → BoolExpr) used by
120    /// NavigationLoader to scope secondary queries.
121    pub fn set_filter_map(&mut self, map: Arc<HashMap<String, crate::query::CompiledFilter>>) {
122        self.filter_map = Some(map);
123    }
124
125    /// Propagates the lazy-loading flag from `DbContextOptions` to this set.
126    pub(crate) fn set_lazy_loading_enabled(&mut self, enabled: bool) {
127        self.lazy_loading_enabled = enabled;
128    }
129
130    /// Returns the configured query filter, if any. Used by `save_one_set`
131    /// to apply tenant isolation to UPDATE/DELETE WHERE clauses.
132    pub(crate) fn query_filter(&self) -> Option<&BoolExpr> {
133        self.query_filter.as_ref()
134    }
135
136    pub fn set_provider(&mut self, provider: Arc<dyn IDatabaseProvider>) {
137        self.provider = Some(provider);
138    }
139
140    // ── Convenience inherent methods �?delegate to trait implementations ──
141
142    /// Convenience inherent method �?delegates to `IDbSet::add`.
143    pub fn add(&mut self, entity: T) {
144        IDbSet::add(self, entity);
145    }
146
147    /// Convenience inherent method �?delegates to `IDbSet::remove_all`.
148    pub fn remove_all(&mut self) {
149        IDbSet::remove_all(self);
150    }
151
152    /// Convenience inherent method �?delegates to `IDbSet::remove_at`.
153    pub fn remove_at(&mut self, index: usize) -> EFResult<()> {
154        IDbSet::remove_at(self, index)
155    }
156
157    /// Convenience inherent method �?delegates to `IDbSet::clear_entries`.
158    pub fn clear_entries(&mut self) {
159        IDbSet::clear_entries(self);
160    }
161
162    /// Convenience inherent method �?delegates to `IDbSet::len`.
163    pub fn len(&self) -> usize {
164        IDbSet::len(self)
165    }
166
167    /// Convenience inherent method �?delegates to `IDbSet::is_empty`.
168    pub fn is_empty(&self) -> bool {
169        IDbSet::is_empty(self)
170    }
171
172    /// Convenience inherent method — delegates to `IQueryable::query`.
173    pub fn query(&self) -> QueryBuilder<T> {
174        IQueryable::query(self)
175    }
176
177    /// Returns a query builder that bypasses the configured query filter.
178    /// Use for administrative / cross-tenant queries.
179    pub fn query_ignore_filters(&self) -> QueryBuilder<T> {
180        let qb = match &self.provider {
181            Some(p) => QueryBuilder::with_provider(&self.table_name, p.clone()),
182            None => QueryBuilder::new(&self.table_name),
183        };
184        qb.with_filter_map(self.filter_map.clone())
185            .with_lazy_loading(self.lazy_loading_enabled)
186    }
187
188    pub fn attach(&mut self, entity: T) {
189        IDbSet::attach(self, entity);
190    }
191
192    pub fn tracked_entries(&self) -> impl Iterator<Item = &T> {
193        self.entries.iter().map(|e| &e.entity)
194    }
195
196    pub fn tracked_entries_mut(&mut self) -> impl Iterator<Item = &mut T> {
197        self.entries.iter_mut().map(|e| &mut e.entity)
198    }
199
200    pub fn retain(&mut self, f: impl FnMut(&TrackedEntry<T>) -> bool) {
201        self.entries.retain(f);
202    }
203
204    /// Marks multiple entities as deleted.
205    pub fn remove_range(&mut self, entities: &[T])
206    where
207        T: PartialEq,
208    {
209        for entity in entities {
210            if let Some(entry) = self.entries.iter_mut().find(|e| e.entity == *entity) {
211                entry.state = EntityState::Deleted;
212            }
213        }
214    }
215
216    /// Loads all rows from the database into the change tracker as Unchanged.
217    pub async fn load_all(&mut self) -> EFResult<()>
218    where
219        T: IFromRow
220            + INavigationSetter
221            + IGetKeyValues
222            + IEntitySnapshot
223            + crate::entity::ILazyInit,
224    {
225        let items = self.query().to_list().await?;
226        self.clear_entries();
227        for item in items {
228            self.attach(item);
229        }
230        Ok(())
231    }
232
233    /// Marks an entity as modified.
234    pub fn update(&mut self, entity: T) {
235        self.entries.push(TrackedEntry {
236            entity,
237            state: EntityState::Modified,
238            original: None,
239            modified_properties: Vec::new(),
240            is_upsert: false,
241        });
242    }
243
244    /// Marks an entity for upsert (INSERT ... ON CONFLICT DO UPDATE).
245    ///
246    /// The entity is tracked in `Added` state with `is_upsert = true`. During
247    /// `save_changes`, upsert entries are routed to `execute_upserts` instead
248    /// of `execute_inserts`. The conflict target is the entity's primary key
249    /// column(s).
250    pub fn upsert(&mut self, entity: T) {
251        self.entries.push(TrackedEntry {
252            entity,
253            state: EntityState::Added,
254            original: None,
255            modified_properties: Vec::new(),
256            is_upsert: true,
257        });
258    }
259
260    /// Compares attached entities against their original snapshots and marks changes.
261    /// Populates `modified_properties` with the field names that differ from the
262    /// original snapshot, enabling partial UPDATEs (SET only dirty columns).
263    pub fn detect_changes(&mut self)
264    where
265        T: IEntitySnapshot,
266    {
267        for entry in &mut self.entries {
268            if entry.state != EntityState::Unchanged {
269                continue;
270            }
271            if let Some(ref original) = entry.original {
272                let current = entry.entity.snapshot();
273                if current == *original {
274                    entry.modified_properties.clear();
275                    continue;
276                }
277                // Collect the field names that actually differ so the
278                // change executor can generate a partial UPDATE.
279                let changed: Vec<String> = current
280                    .iter()
281                    .filter(|(k, v)| original.get(k.as_str()) != Some(v))
282                    .map(|(k, _)| k.clone())
283                    .collect();
284                if !changed.is_empty() {
285                    entry.state = EntityState::Modified;
286                    entry.modified_properties = changed;
287                }
288            }
289        }
290    }
291
292    /// Returns tracked entities in the given state with their original snapshots,
293    /// modified-property lists, and `is_upsert` flag.
294    #[allow(clippy::type_complexity)]
295    pub(crate) fn tracked_by_state(
296        &self,
297        state: EntityState,
298    ) -> Vec<(&T, Option<&HashMap<String, DbValue>>, &[String], bool)> {
299        self.entries
300            .iter()
301            .filter(|e| e.state == state)
302            .map(|e| {
303                (
304                    &e.entity,
305                    e.original.as_ref(),
306                    e.modified_properties.as_slice(),
307                    e.is_upsert,
308                )
309            })
310            .collect()
311    }
312
313    /// Backfills database-generated auto-increment PKs into Added entries.
314    ///
315    /// Called by `save_one_set` after `execute_inserts`. `keys[i]` corresponds
316    /// to the i-th non-upsert Added entry. Upsert entries (`is_upsert = true`)
317    /// are skipped — their PKs are caller-managed. A key of `0` means no PK was
318    /// generated (non-auto-increment or backfill skipped).
319    pub(crate) fn backfill_added_keys(&mut self, keys: &[i64])
320    where
321        T: IGetKeyValues,
322    {
323        let mut idx = 0usize;
324        for entry in &mut self.entries {
325            if entry.state != EntityState::Added || entry.is_upsert {
326                continue;
327            }
328            if let Some(&key) = keys.get(idx) {
329                if key != 0 {
330                    entry.entity.set_auto_increment_key(key);
331                }
332            }
333            idx += 1;
334        }
335    }
336
337    /// Accepts all pending changes: removes Deleted entries, transitions
338    /// Added/Modified → Unchanged, and refreshes original snapshots so future
339    /// `detect_changes` compares against the post-save state.
340    ///
341    /// Called by `save_changes()` after a successful commit. Mirrors the legacy
342    /// `ChangeTracker::accept_all_changes` but operates on `DbSet.entries`.
343    /// Unlike `clear_entries`, this keeps saved entities tracked (with their
344    /// DB-generated PKs) so callers can read backfilled keys after save.
345    pub(crate) fn accept_all_changes(&mut self)
346    where
347        T: IEntitySnapshot,
348    {
349        self.entries.retain(|e| e.state != EntityState::Deleted);
350        for entry in &mut self.entries {
351            if entry.state == EntityState::Added || entry.state == EntityState::Modified {
352                entry.state = EntityState::Unchanged;
353                entry.original = Some(entry.entity.snapshot());
354                entry.modified_properties.clear();
355            }
356        }
357    }
358    pub async fn exists_by_id(&self, key_values: HashMap<String, DbValue>) -> EFResult<bool>
359    where
360        T: IFromRow
361            + INavigationSetter
362            + IGetKeyValues
363            + IEntitySnapshot
364            + crate::entity::ILazyInit,
365    {
366        let pairs: Vec<(&str, DbValue)> = key_values
367            .iter()
368            .map(|(k, v)| (k.as_str(), v.clone()))
369            .collect();
370        Ok(self.query().find_by_key(&pairs).await?.is_some())
371    }
372
373    /// Starts a query filtered by a compile-time LINQ expression tree (`linq!(�?`).
374    pub fn filter<F>(&self, apply: F) -> QueryBuilder<T>
375    where
376        F: FnOnce(QueryBuilder<T>) -> QueryBuilder<T>,
377    {
378        apply(self.query())
379    }
380}
381
382// ---------------------------------------------------------------------------
383// IQueryable<T> implementation
384// ---------------------------------------------------------------------------
385
386impl<T: IEntityType> IQueryable<T> for DbSet<T> {
387    fn query(&self) -> QueryBuilder<T> {
388        let mut qb = match &self.provider {
389            Some(p) => QueryBuilder::with_provider(&self.table_name, p.clone()),
390            None => QueryBuilder::new(&self.table_name),
391        };
392        if let Some(ref filter) = self.query_filter {
393            qb = qb.apply_query_filter(filter.clone());
394        }
395        qb.with_filter_map(self.filter_map.clone())
396            .with_lazy_loading(self.lazy_loading_enabled)
397    }
398}
399
400// ---------------------------------------------------------------------------
401// IDbSet<T> implementation
402// ---------------------------------------------------------------------------
403
404impl<T: IEntityType + IEntitySnapshot> IDbSet<T> for DbSet<T> {
405    fn add(&mut self, entity: T) {
406        self.entries.push(TrackedEntry {
407            entity,
408            state: EntityState::Added,
409            original: None,
410            modified_properties: Vec::new(),
411            is_upsert: false,
412        });
413    }
414
415    fn remove_all(&mut self) {
416        for entry in &mut self.entries {
417            entry.state = EntityState::Deleted;
418        }
419    }
420
421    fn remove_at(&mut self, index: usize) -> EFResult<()> {
422        if let Some(entry) = self.entries.get_mut(index) {
423            entry.state = EntityState::Deleted;
424            Ok(())
425        } else {
426            Err(crate::error::EFError::not_found(
427                "Entity not found at the given index".to_string(),
428            ))
429        }
430    }
431
432    fn attach(&mut self, entity: T) {
433        let original = entity.snapshot();
434        self.entries.push(TrackedEntry {
435            entity,
436            state: EntityState::Unchanged,
437            original: Some(original),
438            modified_properties: Vec::new(),
439            is_upsert: false,
440        });
441    }
442
443    fn added_entities(&self) -> Vec<&T> {
444        self.entries
445            .iter()
446            .filter(|e| e.state == EntityState::Added)
447            .map(|e| &e.entity)
448            .collect()
449    }
450
451    fn modified_entities(&self) -> Vec<&T> {
452        self.entries
453            .iter()
454            .filter(|e| e.state == EntityState::Modified)
455            .map(|e| &e.entity)
456            .collect()
457    }
458
459    fn deleted_entities(&self) -> Vec<&T> {
460        self.entries
461            .iter()
462            .filter(|e| e.state == EntityState::Deleted)
463            .map(|e| &e.entity)
464            .collect()
465    }
466
467    fn entries_with_state(&self) -> Vec<(&T, EntityState)> {
468        self.entries.iter().map(|e| (&e.entity, e.state)).collect()
469    }
470
471    fn clear_entries(&mut self) {
472        self.entries.clear();
473    }
474
475    fn len(&self) -> usize {
476        self.entries.len()
477    }
478
479    fn is_empty(&self) -> bool {
480        self.entries.is_empty()
481    }
482}