Skip to main content

sea_orm/entity/
active_model_ex.rs

1use super::compound::{BelongsTo, BelongsToCardinality, HasMany, HasOne};
2use crate::{ActiveModelTrait, DbErr, EntityTrait, ModelTrait, TryIntoModel};
3use core::ops::{Index, IndexMut};
4
5/// State carried by a `belongs_to` field on an
6/// [`ActiveModelEx`](crate::EntityTrait::ActiveModelEx). Mirrors the
7/// `NotSet` / `Set` shape of [`ActiveValue`](crate::ActiveValue) but for a
8/// related model.
9///
10/// Unstable: nested-`ActiveModel` relation mutation is exempt from semver — the
11/// semantics of setting or removing related records may change in a minor (2.x) release.
12#[derive_where::derive_where(Debug, Clone, PartialEq, Eq; <T as BelongsToCardinality>::Set)]
13#[derive(Default)]
14pub enum ActiveBelongsTo<T>
15where
16    T: BelongsToCardinality,
17{
18    /// Field is absent; the related model is left as-is on save.
19    #[default]
20    NotSet,
21    /// Set the related ActiveModel on save.
22    Set(<T as BelongsToCardinality>::Set),
23}
24
25/// State carried by a `has_one` field on an
26/// [`ActiveModelEx`](crate::EntityTrait::ActiveModelEx).
27///
28/// Unstable: nested-`ActiveModel` relation mutation is exempt from semver — the
29/// semantics of setting or removing related records may change in a minor (2.x) release.
30#[derive_where::derive_where(Debug, Clone, PartialEq, Eq; E::ActiveModelEx)]
31#[derive(Default)]
32pub enum ActiveHasOne<E>
33where
34    E: EntityTrait,
35{
36    /// Field is absent; the related model is left as-is on save.
37    #[default]
38    NotSet,
39    /// Set or clear the related ActiveModel on save.
40    Set(Option<Box<E::ActiveModelEx>>),
41}
42
43/// State carried by a `has_many` (or many-to-many) field on an
44/// [`ActiveModelEx`](crate::EntityTrait::ActiveModelEx). Chooses between
45/// "leave alone", "additive write", and "destructive replace" semantics.
46///
47/// Unstable: nested-`ActiveModel` relation mutation is exempt from semver — the
48/// semantics of replacing or removing related records may change in a minor (2.x) release.
49#[derive_where::derive_where(Debug, Clone, PartialEq, Eq; E::ActiveModelEx)]
50#[derive(Default)]
51#[non_exhaustive]
52pub enum ActiveHasMany<E: EntityTrait> {
53    /// Field is absent; existing related models are left as-is on save.
54    #[default]
55    NotSet,
56    /// Persist exactly this list of related models, deleting any existing
57    /// children that are not in the list.
58    Replace(Vec<E::ActiveModelEx>),
59    /// Persist these related models alongside any existing children; never
60    /// deletes.
61    Append(Vec<E::ActiveModelEx>),
62}
63
64/// Which save operation an [`ActiveModel`](crate::ActiveModelTrait) is about
65/// to perform — used by hooks and helpers that need to branch on the kind
66/// of write.
67#[derive(Debug, Copy, Clone, PartialEq, Eq)]
68pub enum ActiveModelAction {
69    /// `INSERT`.
70    Insert,
71    /// `UPDATE`.
72    Update,
73    /// Insert if the primary key is `NotSet`, otherwise update.
74    /// Only meaningful for entities with an auto-increment primary key.
75    Save,
76}
77
78impl<T> ActiveBelongsTo<T>
79where
80    T: BelongsToCardinality,
81{
82    /// Construct an `ActiveBelongsTo::Set`. Accepts a bare active model for a
83    /// required relation, or an `Option<active model>` for an optional one.
84    pub fn set<M>(model: T::Value<M>) -> Self
85    where
86        M: Into<<<T as BelongsToCardinality>::Entity as EntityTrait>::ActiveModelEx>,
87    {
88        Self::Set(T::into_set(model))
89    }
90
91    /// Take ownership of this relation state, leaving `NotSet` in place
92    pub fn take(&mut self) -> Self {
93        std::mem::take(self)
94    }
95
96    /// Return true if self is NotSet
97    pub fn is_not_set(&self) -> bool {
98        matches!(self, Self::NotSet)
99    }
100
101    /// Return true if there is a set value
102    pub fn is_set(&self) -> bool {
103        matches!(self, Self::Set(_))
104    }
105}
106
107impl<E> ActiveBelongsTo<E>
108where
109    E: EntityTrait,
110{
111    /// Get a reference, if set
112    pub fn as_ref(&self) -> Option<&E::ActiveModelEx> {
113        match self {
114            Self::Set(model) => Some(model),
115            _ => None,
116        }
117    }
118
119    /// Get a mutable reference, if set
120    #[allow(clippy::should_implement_trait)]
121    pub fn as_mut(&mut self) -> Option<&mut E::ActiveModelEx> {
122        match self {
123            Self::Set(model) => Some(model),
124            _ => None,
125        }
126    }
127
128    /// Return true if the containing model is set and changed
129    pub fn is_changed(&self) -> bool {
130        match self {
131            Self::Set(model) => model.is_changed(),
132            _ => false,
133        }
134    }
135
136    /// Convert into an `Option<ActiveModelEx>`
137    pub fn into_option(self) -> Option<E::ActiveModelEx> {
138        match self {
139            Self::Set(model) => Some(*model),
140            Self::NotSet => None,
141        }
142    }
143
144    /// Convert this back to a `ModelEx` container
145    pub fn try_into_model(self) -> Result<BelongsTo<E>, DbErr>
146    where
147        E::ActiveModelEx: TryIntoModel<E::ModelEx>,
148    {
149        Ok(match self {
150            Self::Set(model) => BelongsTo::Loaded(Box::new((*model).try_into_model()?)),
151            Self::NotSet => BelongsTo::Unloaded,
152        })
153    }
154}
155
156impl<E> ActiveBelongsTo<Option<E>>
157where
158    E: EntityTrait,
159{
160    /// Get a reference, if set
161    pub fn as_ref(&self) -> Option<&E::ActiveModelEx> {
162        match self {
163            Self::Set(Some(model)) => Some(model),
164            _ => None,
165        }
166    }
167
168    /// Get a mutable reference, if set
169    #[allow(clippy::should_implement_trait)]
170    pub fn as_mut(&mut self) -> Option<&mut E::ActiveModelEx> {
171        match self {
172            Self::Set(Some(model)) => Some(model),
173            _ => None,
174        }
175    }
176
177    /// Return true if the containing model is set and changed
178    pub fn is_changed(&self) -> bool {
179        match self {
180            Self::Set(Some(model)) => model.is_changed(),
181            Self::Set(None) => true,
182            Self::NotSet => false,
183        }
184    }
185
186    /// Convert into an `Option<ActiveModelEx>`
187    pub fn into_option(self) -> Option<E::ActiveModelEx> {
188        match self {
189            Self::Set(Some(model)) => Some(*model),
190            Self::Set(None) | Self::NotSet => None,
191        }
192    }
193
194    /// Convert this back to a `ModelEx` container
195    pub fn try_into_model(self) -> Result<BelongsTo<Option<E>>, DbErr>
196    where
197        E::ActiveModelEx: TryIntoModel<E::ModelEx>,
198    {
199        Ok(match self {
200            Self::Set(Some(model)) => BelongsTo::Loaded(Some(Box::new((*model).try_into_model()?))),
201            Self::Set(None) => BelongsTo::Loaded(None),
202            Self::NotSet => BelongsTo::Unloaded,
203        })
204    }
205}
206
207impl<E> ActiveHasOne<E>
208where
209    E: EntityTrait,
210{
211    /// Construct an `ActiveHasOne::Set` from an optional related active model.
212    pub fn set(model: Option<impl Into<E::ActiveModelEx>>) -> Self {
213        Self::Set(model.map(|model| Box::new(model.into())))
214    }
215
216    /// Return true if self is NotSet
217    pub fn is_not_set(&self) -> bool {
218        matches!(self, Self::NotSet)
219    }
220
221    /// Return true if there is a set value
222    pub fn is_set(&self) -> bool {
223        matches!(self, Self::Set(_))
224    }
225
226    /// Get a reference, if set
227    pub fn as_ref(&self) -> Option<&E::ActiveModelEx> {
228        match self {
229            Self::Set(Some(model)) => Some(model),
230            _ => None,
231        }
232    }
233
234    /// Get a mutable reference, if set
235    #[allow(clippy::should_implement_trait)]
236    pub fn as_mut(&mut self) -> Option<&mut E::ActiveModelEx> {
237        match self {
238            Self::Set(Some(model)) => Some(model),
239            _ => None,
240        }
241    }
242
243    /// Return true if the containing model is set and changed
244    pub fn is_changed(&self) -> bool {
245        match self {
246            Self::Set(Some(model)) => model.is_changed(),
247            Self::Set(None) => true,
248            Self::NotSet => false,
249        }
250    }
251
252    /// Convert into an `Option<ActiveModelEx>`
253    pub fn into_option(self) -> Option<E::ActiveModelEx> {
254        match self {
255            Self::Set(Some(model)) => Some(*model),
256            Self::Set(None) | Self::NotSet => None,
257        }
258    }
259
260    /// Convert this back to a `ModelEx` container
261    pub fn try_into_model(self) -> Result<HasOne<E>, DbErr>
262    where
263        E::ActiveModelEx: TryIntoModel<E::ModelEx>,
264    {
265        Ok(match self {
266            Self::Set(Some(model)) => HasOne::Loaded(Some(Box::new((*model).try_into_model()?))),
267            Self::Set(None) => HasOne::Loaded(None),
268            Self::NotSet => HasOne::Unloaded,
269        })
270    }
271}
272
273impl<E> ActiveHasMany<E>
274where
275    E: EntityTrait,
276{
277    /// Take ownership of the models, leaving `NotSet` in place
278    pub fn take(&mut self) -> Self {
279        std::mem::take(self)
280    }
281
282    /// Borrow models as slice
283    pub fn as_slice(&self) -> &[E::ActiveModelEx] {
284        match self {
285            Self::Replace(models) | Self::Append(models) => models,
286            Self::NotSet => &[],
287        }
288    }
289
290    /// Get a mutable vec. If self is `NotSet`, convert to append.
291    pub fn as_mut_vec(&mut self) -> &mut Vec<E::ActiveModelEx> {
292        match self {
293            Self::Replace(models) | Self::Append(models) => models,
294            Self::NotSet => {
295                *self = Self::Append(vec![]);
296                self.as_mut_vec()
297            }
298        }
299    }
300
301    /// Consume self as vector
302    pub fn into_vec(self) -> Vec<E::ActiveModelEx> {
303        match self {
304            Self::Replace(models) | Self::Append(models) => models,
305            Self::NotSet => vec![],
306        }
307    }
308
309    /// Returns an empty container of self
310    pub fn empty_holder(&self) -> Self {
311        match self {
312            Self::Replace(_) => Self::Replace(vec![]),
313            Self::Append(_) => Self::Append(vec![]),
314            Self::NotSet => Self::NotSet,
315        }
316    }
317
318    /// Push an item to self
319    pub fn push<AM: Into<E::ActiveModelEx>>(&mut self, model: AM) -> &mut Self {
320        let model = model.into();
321        match self {
322            Self::Replace(models) | Self::Append(models) => models.push(model),
323            Self::NotSet => {
324                *self = Self::Append(vec![model]);
325            }
326        }
327
328        self
329    }
330
331    /// Push an item to self, but convert Replace to Append
332    pub fn append<AM: Into<E::ActiveModelEx>>(&mut self, model: AM) -> &mut Self {
333        self.convert_to_append().push(model)
334    }
335
336    /// Replace all items in this set
337    pub fn replace_all<I>(&mut self, models: I) -> &mut Self
338    where
339        I: IntoIterator<Item = E::ActiveModelEx>,
340    {
341        *self = Self::Replace(models.into_iter().collect());
342        self
343    }
344
345    /// Convert self to Append, if set
346    pub fn convert_to_append(&mut self) -> &mut Self {
347        match self.take() {
348            Self::Replace(models) | Self::Append(models) => {
349                *self = Self::Append(models);
350            }
351            Self::NotSet => {
352                *self = Self::NotSet;
353            }
354        }
355
356        self
357    }
358
359    /// Reset self to NotSet
360    pub fn not_set(&mut self) {
361        *self = Self::NotSet;
362    }
363
364    /// If self is `Replace`
365    pub fn is_replace(&self) -> bool {
366        matches!(self, Self::Replace(_))
367    }
368
369    /// If self is `Append`
370    pub fn is_append(&self) -> bool {
371        matches!(self, Self::Append(_))
372    }
373
374    /// Return true if self is `Replace` or any containing model is changed
375    pub fn is_changed(&self) -> bool {
376        match self {
377            Self::Replace(_) => true,
378            Self::Append(models) => models.iter().any(|model| model.is_changed()),
379            Self::NotSet => false,
380        }
381    }
382
383    /// Find within the models by primary key, return true if found
384    pub fn find(&self, model: &E::Model) -> bool {
385        let pk = model.get_primary_key_value();
386
387        for item in self.as_slice() {
388            if let Some(pk_item) = item.get_primary_key_value()
389                && pk_item == pk
390            {
391                return true;
392            }
393        }
394
395        false
396    }
397
398    /// Convert this back to a `ModelEx` container
399    pub fn try_into_model(self) -> Result<HasMany<E>, DbErr>
400    where
401        E::ActiveModelEx: TryIntoModel<E::ModelEx>,
402    {
403        Ok(match self {
404            Self::Replace(models) | Self::Append(models) => HasMany::Loaded(
405                models
406                    .into_iter()
407                    .map(|t| t.try_into_model())
408                    .collect::<Result<Vec<_>, DbErr>>()?,
409            ),
410            Self::NotSet => HasMany::Unloaded,
411        })
412    }
413}
414
415impl<E: EntityTrait> From<ActiveHasMany<E>> for Option<Vec<E::ActiveModelEx>> {
416    fn from(value: ActiveHasMany<E>) -> Self {
417        match value {
418            ActiveHasMany::NotSet => None,
419            ActiveHasMany::Replace(models) | ActiveHasMany::Append(models) => Some(models),
420        }
421    }
422}
423
424impl<E: EntityTrait> Index<usize> for ActiveHasMany<E> {
425    type Output = E::ActiveModelEx;
426
427    fn index(&self, index: usize) -> &Self::Output {
428        match self {
429            ActiveHasMany::NotSet => {
430                panic!("index out of bounds: the ActiveHasMany is NotSet (index: {index})")
431            }
432            ActiveHasMany::Replace(models) | ActiveHasMany::Append(models) => models.index(index),
433        }
434    }
435}
436
437impl<E: EntityTrait> IndexMut<usize> for ActiveHasMany<E> {
438    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
439        match self {
440            ActiveHasMany::NotSet => {
441                panic!("index out of bounds: the ActiveHasMany is NotSet (index: {index})")
442            }
443            ActiveHasMany::Replace(models) | ActiveHasMany::Append(models) => {
444                models.index_mut(index)
445            }
446        }
447    }
448}
449
450impl<E: EntityTrait> IntoIterator for ActiveHasMany<E> {
451    type Item = E::ActiveModelEx;
452    type IntoIter = std::vec::IntoIter<E::ActiveModelEx>;
453
454    fn into_iter(self) -> Self::IntoIter {
455        match self {
456            ActiveHasMany::Replace(models) | ActiveHasMany::Append(models) => models.into_iter(),
457            ActiveHasMany::NotSet => Vec::new().into_iter(),
458        }
459    }
460}
461
462/// Converts from a set of models into `Append`, which performs non-destructive action
463impl<E: EntityTrait> From<Vec<E::ActiveModelEx>> for ActiveHasMany<E> {
464    fn from(value: Vec<E::ActiveModelEx>) -> Self {
465        ActiveHasMany::Append(value)
466    }
467}