Skip to main content

shared_framework/data/
repository.rs

1//! Generic repository for SeaORM entities.
2//!
3//! [`PersistentRepository`] centralizes CRUD, counts, cursor-paginated reads,
4//! updates, and deletes for one entity type whose model implements
5//! [`BaseEntity`]. Queries are built with [`QueryData`] and parameterized with
6//! [`RepositoryOptions`].
7//!
8//! Cursor filtering compares `id` against the decoded cursor: ascending order
9//! uses `id > cursor`, descending uses `id < cursor`, and deletes always use
10//! `id > cursor`. On creation, `uid`/`createdAt`/`updatedAt` (plus `createdById`/
11//! `updatedById` when a user id is present) is filled in; on update,
12//! `updatedAt` (plus `updatedById` when a user id is present) is refreshed.
13//! Database columns for these fields use camelCase names.
14//!
15//! When a [`RepositoryOptions`] carries `Some(txn)` (see
16//! [`RepositoryOptions::with_transaction`](RepositoryOptions::with_transaction)),
17//! the operation runs against that transaction; otherwise it uses the
18//! repository's own connection. The [`QueryData`] transaction (set via
19//! `QueryData::with_options`) is honored as a fallback when the method-level
20//! options carry none.
21//!
22//! Repositories can be shared through a process-wide registry keyed by entity
23//! type (see [`PersistentRepository::initialize`] and
24//! [`PersistentRepository::find`]), or used directly via
25//! [`PersistentRepository::new`] without registration.
26//!
27//! ```ignore
28//! use shared_framework::data::{PersistentRepository, QueryData, RepositoryOptions};
29//!
30//! let repo = PersistentRepository::<my_entity::Entity>::new(db);
31//! let page = repo.get_paginated_view(None, RepositoryOptions::from_ctx(ctx).join(RepositoryOptions::new().with_limit(50))).await?;
32//! let all = repo.get_all::<shared_framework::data::NoUser>(Some(QueryData::new(my_entity::Entity::find())), None).await?;
33//! ```
34
35use std::any::{Any, TypeId};
36use std::collections::HashMap;
37use std::fmt::Display;
38use std::str::FromStr;
39use std::sync::{Arc, Mutex, OnceLock};
40
41use crate::data::query::{
42    CursorData, DeleteQueryData, NoUser, PageResult, QueryData, RepositoryOptions,
43};
44use crate::data::types_extra::ChangeResultModel;
45use crate::data::{BaseAuditableEntity, BaseEntity};
46use chrono::Utc;
47use sea_orm::entity::prelude::DateTimeWithTimeZone;
48use sea_orm::sea_query::{Alias, Expr, ExprTrait};
49use sea_orm::{
50    ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, DbErr, EntityTrait,
51    IntoActiveModel, Iterable, ModelTrait, PaginatorTrait, PrimaryKeyToColumn, PrimaryKeyTrait,
52    QueryFilter, QuerySelect, TransactionTrait, Value, entity::EntityLoaderTrait,
53};
54use serde::Serialize;
55use uuid::Uuid;
56
57// ── Singleton registry ───────────────────────────────────────────────────────
58
59static REGISTRY: OnceLock<Mutex<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>> = OnceLock::new();
60
61fn registry() -> &'static Mutex<HashMap<TypeId, Arc<dyn Any + Send + Sync>>> {
62    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
63}
64
65// ── cursor helpers (asc → gt, desc → lt; deletes always gt) ────────────────
66// Select: respects order (asc → gt, desc → lt). Delete: always gt.
67
68fn with_cursor<E>(
69    select: sea_orm::Select<E>,
70    cursor: &Option<String>,
71    order_asc: Option<bool>,
72) -> sea_orm::Select<E>
73where
74    E: EntityTrait,
75{
76    if let Some(s) = cursor {
77        if let Some(data) = CursorData::decode(s) {
78            let is_asc = order_asc.unwrap_or(true);
79            return if is_asc {
80                select.filter(Expr::col(Alias::new("id")).gt(data.cursor))
81            } else {
82                select.filter(Expr::col(Alias::new("id")).lt(data.cursor))
83            };
84        }
85    }
86    select
87}
88
89fn with_cursor_delete<E>(
90    delete: sea_orm::DeleteMany<E>,
91    cursor: &Option<String>,
92) -> sea_orm::DeleteMany<E>
93where
94    E: EntityTrait,
95{
96    if let Some(s) = cursor {
97        if let Some(data) = CursorData::decode(s) {
98            return delete.filter(Expr::col(Alias::new("id")).gt(data.cursor));
99        }
100    }
101    delete
102}
103
104fn with_cursor_loader<E, L>(loader: L, cursor: &Option<String>, order_asc: Option<bool>) -> L
105where
106    E: EntityTrait,
107    L: EntityLoaderTrait<E>,
108{
109    if let Some(s) = cursor {
110        if let Some(data) = CursorData::decode(s) {
111            let is_asc = order_asc.unwrap_or(true);
112            return if is_asc {
113                loader.filter(Expr::col(Alias::new("id")).gt(data.cursor))
114            } else {
115                loader.filter(Expr::col(Alias::new("id")).lt(data.cursor))
116            };
117        }
118    }
119    loader
120}
121
122fn model_id<E, M>(model: &M) -> i64
123where
124    E: EntityTrait,
125    M: ModelTrait<Entity = E>,
126{
127    match model.get(E::PrimaryKey::iter().next().unwrap().into_column()) {
128        Value::BigInt(Some(id)) => id,
129        Value::Int(Some(id)) => i64::from(id),
130        value => panic!("repository cursor requires an integer primary key, got {value:?}"),
131    }
132}
133
134// ── transaction-executor helpers ───────────────────────────────────────────
135// When a `RepositoryOptions` carries `Some(txn)`, operations run against that
136// transaction; otherwise they fall back to the repository connection.
137
138fn opts_txn<U>(opts: Option<&RepositoryOptions<U>>) -> Option<Arc<DatabaseTransaction>>
139where
140    U: BaseEntity + Clone + Send + Sync + 'static,
141{
142    opts.and_then(|o| o.txn.clone())
143}
144
145/// Prefers the explicit method-level `opts` transaction, falling back to the
146/// transaction attached to the [`QueryData`] options (populated via
147/// `QueryData::with_options`).
148fn pick_txn<U>(
149    opts: Option<&RepositoryOptions<U>>,
150    qd_opts_txn: Option<Arc<DatabaseTransaction>>,
151) -> Option<Arc<DatabaseTransaction>>
152where
153    U: BaseEntity + Clone + Send + Sync + 'static,
154{
155    opts_txn(opts).or(qd_opts_txn)
156}
157
158// ── base-field helpers (uid, timestamps, actor ids) ────────────────────────
159
160fn try_set<A>(active: &mut A, col_name: &str, value: Value)
161where
162    A: ActiveModelTrait,
163    A::Entity: EntityTrait,
164    <A::Entity as EntityTrait>::Column: ColumnTrait + FromStr,
165    <<A::Entity as EntityTrait>::Column as FromStr>::Err: std::fmt::Debug,
166{
167    if let Ok(col) = <A::Entity as EntityTrait>::Column::from_str(col_name) {
168        active.set(col, value);
169    }
170}
171
172fn apply_base_create<A, U>(active: &mut A, opts: &RepositoryOptions<U>)
173where
174    A: ActiveModelTrait,
175    A::Entity: EntityTrait,
176    <A::Entity as EntityTrait>::Column: ColumnTrait + FromStr,
177    <<A::Entity as EntityTrait>::Column as FromStr>::Err: std::fmt::Debug,
178    U: BaseEntity + Clone + Send + Sync + 'static,
179{
180    let now: DateTimeWithTimeZone = Utc::now().into();
181    let uid = Uuid::new_v4();
182    // All database columns are camelCase — single canonical name, no fallbacks
183    try_set(active, "uid", Value::Uuid(Some(uid)));
184    try_set(active, "createdAt", Value::ChronoDateTimeWithTimeZone(Some(now)));
185    try_set(active, "updatedAt", Value::ChronoDateTimeWithTimeZone(Some(now)));
186    let user_id_opt = opts.user_id.or_else(|| opts.user.as_ref().map(|u| u.id()));
187    if let Some(uid) = user_id_opt {
188        try_set(active, "createdById", Value::BigInt(Some(uid)));
189        try_set(active, "updatedById", Value::BigInt(Some(uid)));
190    }
191}
192
193fn apply_base_update<A, U>(active: &mut A, opts: &RepositoryOptions<U>)
194where
195    A: ActiveModelTrait,
196    A::Entity: EntityTrait,
197    <A::Entity as EntityTrait>::Column: ColumnTrait + FromStr,
198    <<A::Entity as EntityTrait>::Column as FromStr>::Err: std::fmt::Debug,
199    U: BaseEntity + Clone + Send + Sync + 'static,
200{
201    let now: DateTimeWithTimeZone = Utc::now().into();
202    try_set(active, "updatedAt", Value::ChronoDateTimeWithTimeZone(Some(now)));
203    let user_id_opt = opts.user_id.or_else(|| opts.user.as_ref().map(|u| u.id()));
204    if let Some(uid) = user_id_opt {
205        try_set(active, "updatedById", Value::BigInt(Some(uid)));
206    }
207}
208
209// ── Repository ───────────────────────────────────────────────────────────────
210
211/// Generic database access for one SeaORM entity.
212///
213/// `E` is the SeaORM entity; its model must implement [`BaseEntity`].
214/// Holds a cloned [`DatabaseConnection`] used by every operation.
215#[derive(Clone)]
216pub struct PersistentRepository<E>
217where
218    E: EntityTrait,
219    E::ModelEx: BaseEntity + Send + Sync,
220    <E::ModelEx as BaseEntity>::LoaderType: EntityLoaderTrait<E, ModelEx = E::ModelEx>,
221{
222    /// Connection used by every operation.
223    pub db: DatabaseConnection,
224    _marker: std::marker::PhantomData<E>,
225}
226
227impl<E> PersistentRepository<E>
228where
229    E: EntityTrait,
230    E::ModelEx: BaseEntity + Send + Sync + Clone + Serialize,
231    E::Model: Into<E::ModelEx> + Send + Sync,
232    <E::ModelEx as BaseEntity>::LoaderType: EntityLoaderTrait<E, ModelEx = E::ModelEx>,
233    E::Column: ColumnTrait + FromStr,
234    <E::Column as FromStr>::Err: std::fmt::Debug,
235{
236    /// Creates a repository holding the given connection.
237    pub fn new(db: DatabaseConnection) -> Self {
238        Self {
239            db,
240            _marker: std::marker::PhantomData,
241        }
242    }
243
244    /// Returns the underlying connection.
245    pub fn db(&self) -> &DatabaseConnection {
246        &self.db
247    }
248
249    // ── Singleton registry ───────────────────────────────────────────────
250
251    /// Registers a repository for `E` in the process-wide registry and returns it.
252    ///
253    /// Panics if a repository for `E` was already initialized.
254    pub fn initialize(db: &DatabaseConnection) -> Arc<Self> {
255        let type_id = TypeId::of::<E>();
256        let mut reg = registry().lock().unwrap();
257        if reg.contains_key(&type_id) {
258            panic!(
259                "Repository for {} has already been initialized",
260                std::any::type_name::<E>()
261            );
262        }
263        let repo = Arc::new(Self::new(db.clone()));
264        reg.insert(type_id, repo.clone() as Arc<dyn Any + Send + Sync>);
265        repo
266    }
267
268    /// Returns the registered repository for `E`.
269    ///
270    /// Panics if [`PersistentRepository::initialize`] was not called first.
271    pub fn find() -> Arc<Self> {
272        Self::try_find().unwrap_or_else(|| panic!("Repository for {} has not been initialized — call PersistentRepository::<{}>::initialize(db) first", std::any::type_name::<E>(), std::any::type_name::<E>()))
273    }
274
275    /// Returns the registered repository for `E`, or `None` when uninitialized.
276    pub fn try_find() -> Option<Arc<Self>> {
277        let reg = registry().lock().unwrap();
278        reg.get(&TypeId::of::<E>())
279            .and_then(|arc| Arc::downcast::<Self>(arc.clone()).ok())
280    }
281
282    /// Creates a repository without touching the shared registry.
283    pub fn new_ephemeral(db: DatabaseConnection) -> Self {
284        Self::new(db)
285    }
286
287    /// Starts a fresh [`QueryData`] over `E::find()`.
288    pub fn start(&self) -> QueryData<E>
289    where
290        E::ModelEx: BaseEntity,
291    {
292        QueryData::new(<E::ModelEx as BaseEntity>::load())
293    }
294
295    /// Starts a fresh [`DeleteQueryData`] over `E::delete_many()`.
296    pub fn start_delete(&self) -> DeleteQueryData<E> {
297        DeleteQueryData::new(E::delete_many())
298    }
299
300    /// Returns true when the model and user share the same `id`.
301    pub fn is_owner<U>(&self, model: &E::ModelEx, user: &U) -> bool
302    where
303        U: BaseEntity,
304    {
305        model.id() == user.id()
306    }
307    /// Returns true when the model's creator id matches the user id.
308    pub fn is_owner_auditable<U>(&self, model: &E::ModelEx, user: &U) -> bool
309    where
310        U: BaseEntity + Clone + Send + Sync,
311        E::ModelEx: BaseAuditableEntity<User = U>,
312    {
313        model.created_by().id() == user.id()
314    }
315    /// Returns true when the user may access the model; currently same as [`PersistentRepository::is_owner`].
316    pub fn is_accessible<U>(&self, model: &E::ModelEx, user: &U) -> bool
317    where
318        U: BaseEntity,
319    {
320        self.is_owner(model, user)
321    }
322    /// Returns true when the user created or last updated the model.
323    pub fn is_accessible_auditable<U>(&self, model: &E::ModelEx, user: &U) -> bool
324    where
325        U: BaseEntity + Clone + Send + Sync,
326        E::ModelEx: BaseAuditableEntity<User = U>,
327    {
328        model.created_by().id() == user.id() || model.updated_by().id() == user.id()
329    }
330
331    /// Runs the closure inside a transaction, committing on success.
332    ///
333    /// The closure receives an `Arc<DatabaseTransaction>` so the handle can be
334    /// freely shared — e.g., moved into
335    /// [`RepositoryOptions::with_transaction`](RepositoryOptions::with_transaction)
336    /// for nested repository calls. Do not retain clones beyond the closure:
337    /// committing requires sole ownership of the handle, and a closure error
338    /// drops it uncommitted, rolling the transaction back.
339    ///
340    /// Returns the closure value or the [`DbErr`] from beginning, the closure, or commit.
341    pub async fn transaction<F, T, EType>(&self, f: F) -> Result<T, EType>
342    where
343        EType: From<DbErr> + Display + Send,
344        F: FnOnce(
345                Arc<DatabaseTransaction>,
346            ) -> futures::future::BoxFuture<'static, Result<T, EType>>
347            + Send,
348        T: Send,
349    {
350        let txn = Arc::new(self.db.begin().await?);
351        let res = f(txn.clone()).await?;
352        let owned = Arc::try_unwrap(txn).map_err(|_| {
353            DbErr::Custom(
354                "transaction handle still shared after closure; refusing to commit".to_string(),
355            )
356        })?;
357        owned.commit().await?;
358        Ok(res)
359    }
360
361    /// Runs the closure inside `opts.txn` when one is attached, else in a fresh transaction.
362    ///
363    /// When `opts` carries a transaction, the closure receives that shared
364    /// handle directly and no commit/rollback is performed here — the owner of
365    /// that transaction controls its lifecycle. Otherwise a new transaction is
366    /// begun from the repository connection and committed on success.
367    pub async fn transaction_with_opts<U, F, T, EType>(
368        &self,
369        opts: RepositoryOptions<U>,
370        f: F,
371    ) -> Result<T, EType>
372    where
373        EType: From<DbErr> + Display + Send,
374        U: BaseEntity + Clone + Send + Sync + 'static,
375        F: FnOnce(
376                Arc<DatabaseTransaction>,
377            ) -> futures::future::BoxFuture<'static, Result<T, EType>>
378            + Send,
379        T: Send,
380    {
381        if let Some(txn) = opts.txn {
382            return f(txn).await;
383        }
384        self.transaction(f).await
385    }
386
387    // ── COUNT ───────────────────────────────────────────────────────────────
388
389    /// Counts rows matching the filter, applying the cursor when one is set.
390    ///
391    /// `U` is the [`RepositoryOptions`] identity type. The cursor comes from
392    /// `opts` when present, else from the [`QueryData`] options. Runs against
393    /// `opts.txn` when attached, else against the repository connection.
394    pub async fn get_count<U>(
395        &self,
396        filter: Option<QueryData<E>>,
397        opts: Option<RepositoryOptions<U>>,
398    ) -> Result<u64, DbErr>
399    where
400        U: BaseEntity + Clone + Send + Sync + 'static,
401        E::ModelEx: BaseEntity,
402    {
403        let cursor = opts.as_ref().and_then(|o| o.cursor.clone());
404        let txn = pick_txn(
405            opts.as_ref(),
406            filter.as_ref().and_then(|qd| qd.opts.txn.clone()),
407        );
408        let (loader, query_cursor, order) = match filter {
409            Some(qd) => (qd.loader, qd.opts.cursor, qd.order_asc),
410            None => (<E::ModelEx as BaseEntity>::load(), None, None),
411        };
412        let loader = with_cursor_loader(loader, &cursor.or(query_cursor), order);
413        if let Some(txn) = txn.as_ref() {
414            loader.num_items(txn.as_ref(), 0).await
415        } else {
416            loader.num_items(&self.db, 0).await
417        }
418    }
419
420    /// Counts rows matching the filter inside the given transaction.
421    pub async fn get_count_txn(
422        &self,
423        filter: Option<QueryData<E>>,
424        txn: &DatabaseTransaction,
425    ) -> Result<u64, DbErr>
426    where
427        E::ModelEx: BaseEntity,
428    {
429        let (loader, cursor, order) = match filter {
430            Some(qd) => (qd.loader, qd.opts.cursor, qd.order_asc),
431            None => (<E::ModelEx as BaseEntity>::load(), None, None),
432        };
433        with_cursor_loader(loader, &cursor, order)
434            .num_items(txn, 0)
435            .await
436    }
437
438    /// Counts rows matching the filter with default options.
439    pub async fn count(&self, filter: Option<QueryData<E>>) -> Result<u64, DbErr>
440    where
441        E::ModelEx: BaseEntity,
442    {
443        self.get_count::<NoUser>(filter, None).await
444    }
445
446    // ── PAGINATED ───────────────────────────────────────────────────────────
447
448    /// Returns one cursor page: totals ignore cursor/limit, rows fetch `limit + 1` to detect `has_next`.
449    ///
450    /// `U` is the [`RepositoryOptions`] identity type. The next-page cursor encodes
451    /// `limit` and the last row `id`; it is `None` when there is no next page.
452    /// Runs against `opts.txn` when attached, else against the repository connection.
453    pub async fn get_paginated_view<U>(
454        &self,
455        filter: Option<QueryData<E>>,
456        opts: RepositoryOptions<U>,
457    ) -> Result<PageResult<E::ModelEx>, DbErr>
458    where
459        U: BaseEntity + Clone + Send + Sync + 'static,
460        E::ModelEx: BaseEntity,
461    {
462        let order_asc = filter.as_ref().and_then(|qd| qd.order_asc);
463        let txn = opts
464            .txn
465            .clone()
466            .or_else(|| filter.as_ref().and_then(|qd| qd.opts.txn.clone()));
467        let loader = filter
468            .map(|qd| qd.loader)
469            .unwrap_or_else(<E::ModelEx as BaseEntity>::load);
470        let total_loader = loader.clone();
471        let total = if let Some(txn) = txn.as_ref() {
472            total_loader.num_items(txn.as_ref(), 0).await? as i64
473        } else {
474            total_loader.num_items(&self.db, 0).await? as i64
475        };
476
477        let sel = with_cursor_loader(loader, &opts.cursor, order_asc);
478        let limit = opts.limit;
479        let rows: Vec<E::ModelEx> = if let Some(txn) = txn.as_ref() {
480            sel.fetch(txn.as_ref(), 0, (limit + 1) as u64).await?
481        } else {
482            sel.fetch(&self.db, 0, (limit + 1) as u64).await?
483        };
484        let has_next = rows.len() > limit;
485        let mut data = rows;
486        if has_next {
487            data.truncate(limit);
488        }
489        let next_cursor = if has_next {
490            Some(
491                CursorData {
492                    limit,
493                    cursor: model_id::<E, _>(data.last().unwrap()),
494                }
495                .encode(),
496            )
497        } else {
498            None
499        };
500        Ok(PageResult {
501            data,
502            has_next,
503            cursor: opts.cursor.clone(),
504            next_cursor,
505            total,
506            limit,
507        })
508    }
509
510    /// Paginates a raw `Select`: totals ignore cursor/limit and cursors use `id > cursor`.
511    ///
512    /// Unlike [`PersistentRepository::get_paginated_view`], no recorded ordering
513    /// direction is consulted because the select carries none.
514    /// Runs against `opts.txn` when attached, else against the repository connection.
515    pub async fn paginated(
516        &self,
517        select: sea_orm::Select<E>,
518        opts: RepositoryOptions<NoUser>,
519    ) -> Result<PageResult<E::ModelEx>, DbErr>
520    where
521        E::ModelEx: BaseEntity,
522        E::Model: Into<E::ModelEx> + Send + Sync,
523    {
524        let limit = opts.limit;
525        let cursor = opts.cursor.clone();
526        let txn = opts.txn.clone();
527        // total without cursor/limit.
528        let mut count_sel = select.clone();
529        if opts.distinct {
530            count_sel = count_sel.distinct();
531        }
532        let total = if let Some(txn) = txn.as_ref() {
533            count_sel.paginate(txn.as_ref(), 1).num_items().await? as i64
534        } else {
535            count_sel.paginate(&self.db, 1).num_items().await? as i64
536        };
537
538        let mut sel = with_cursor(select, &cursor, None);
539        if opts.distinct {
540            sel = sel.distinct();
541        }
542        sel = sel.limit((limit + 1) as u64);
543        let rows: Vec<E::ModelEx> = if let Some(txn) = txn.as_ref() {
544            sel.all(txn.as_ref())
545                .await?
546                .into_iter()
547                .map(Into::into)
548                .collect()
549        } else {
550            sel.all(&self.db)
551                .await?
552                .into_iter()
553                .map(Into::into)
554                .collect()
555        };
556        let has_next = rows.len() > limit;
557        let mut data = rows;
558        if has_next {
559            data.truncate(limit);
560        }
561        let next_cursor = if has_next {
562            Some(
563                CursorData {
564                    limit,
565                    cursor: model_id::<E, _>(data.last().unwrap()),
566                }
567                .encode(),
568            )
569        } else {
570            None
571        };
572        Ok(PageResult {
573            data,
574            has_next,
575            cursor: opts.cursor,
576            next_cursor,
577            total,
578            limit: opts.limit,
579        })
580    }
581
582    // ── GET MANY / ALL / DISTINCT / ONE / BY_ID ────────────────────────────
583
584    /// Returns all matching rows, applying cursor (`opts` cursor wins over [`QueryData`] cursor) and distinct.
585    ///
586    /// `U` is the [`RepositoryOptions`] identity type.
587    /// Runs against `opts.txn` (or the [`QueryData`] transaction) when attached,
588    /// else against the repository connection.
589    pub async fn get_all<U>(
590        &self,
591        filter: Option<QueryData<E>>,
592        opts: Option<RepositoryOptions<U>>,
593    ) -> Result<Vec<E::ModelEx>, DbErr>
594    where
595        U: BaseEntity + Clone + Send + Sync + 'static,
596        E::ModelEx: BaseEntity,
597    {
598        let cursor = opts.as_ref().and_then(|o| o.cursor.clone());
599        let txn = pick_txn(
600            opts.as_ref(),
601            filter.as_ref().and_then(|qd| qd.opts.txn.clone()),
602        );
603        let (loader, qd_cursor, order) = match filter {
604            Some(qd) => (qd.loader, qd.opts.cursor, qd.order_asc),
605            None => (<E::ModelEx as BaseEntity>::load(), None, None),
606        };
607        let cursor = cursor.or(qd_cursor);
608        let loader = with_cursor_loader(loader, &cursor, order);
609        if let Some(txn) = txn.as_ref() {
610            loader.fetch(txn.as_ref(), 0, 0).await
611        } else {
612            loader.fetch(&self.db, 0, 0).await
613        }
614    }
615
616    /// Returns up to `opts.limit` matching rows, applying cursor and distinct.
617    ///
618    /// `U` is the [`RepositoryOptions`] identity type. The `opts` cursor is
619    /// preferred when set, else the [`QueryData`] cursor is used.
620    /// Runs against `opts.txn` (or the [`QueryData`] transaction) when attached.
621    pub async fn get_many<U>(
622        &self,
623        filter: Option<QueryData<E>>,
624        opts: RepositoryOptions<U>,
625    ) -> Result<Vec<E::ModelEx>, DbErr>
626    where
627        U: BaseEntity + Clone + Send + Sync + 'static,
628        E::ModelEx: BaseEntity,
629    {
630        let cursor = opts.cursor.clone();
631        let txn = opts
632            .txn
633            .clone()
634            .or_else(|| filter.as_ref().and_then(|qd| qd.opts.txn.clone()));
635        let (loader, qd_cursor, order) = match filter {
636            Some(qd) => (qd.loader, qd.opts.cursor, qd.order_asc),
637            None => (<E::ModelEx as BaseEntity>::load(), None, None),
638        };
639        let cursor = if cursor.is_some() { cursor } else { qd_cursor };
640        let loader = with_cursor_loader(loader, &cursor, order);
641        if let Some(txn) = txn.as_ref() {
642            loader.fetch(txn.as_ref(), 0, opts.limit as u64).await
643        } else {
644            loader.fetch(&self.db, 0, opts.limit as u64).await
645        }
646    }
647
648    /// Alias for [`PersistentRepository::get_many`].
649    pub async fn find_many<U>(
650        &self,
651        filter: Option<QueryData<E>>,
652        opts: RepositoryOptions<U>,
653    ) -> Result<Vec<E::ModelEx>, DbErr>
654    where
655        U: BaseEntity + Clone + Send + Sync + 'static,
656        E::ModelEx: BaseEntity,
657    {
658        self.get_many(filter, opts).await
659    }
660
661    /// Returns all matching rows with default options.
662    pub async fn find_all(&self, filter: Option<QueryData<E>>) -> Result<Vec<E::ModelEx>, DbErr>
663    where
664        E::ModelEx: BaseEntity,
665    {
666        self.get_all::<NoUser>(filter, None).await
667    }
668
669    /// Returns up to `opts.limit` distinct rows matching the filter, applying cursor.
670    ///
671    /// `U` is the [`RepositoryOptions`] identity type.
672    /// Runs against `opts.txn` (or the [`QueryData`] transaction) when attached.
673    pub async fn get_distinct_rows<U>(
674        &self,
675        filter: Option<QueryData<E>>,
676        opts: RepositoryOptions<U>,
677    ) -> Result<Vec<E::ModelEx>, DbErr>
678    where
679        U: BaseEntity + Clone + Send + Sync + 'static,
680        E::ModelEx: BaseEntity,
681    {
682        let cursor = opts.cursor.clone();
683        let txn = opts
684            .txn
685            .clone()
686            .or_else(|| filter.as_ref().and_then(|qd| qd.opts.txn.clone()));
687        let (loader, qd_cursor, order) = match filter {
688            Some(qd) => (qd.loader, qd.opts.cursor, qd.order_asc),
689            None => (<E::ModelEx as BaseEntity>::load(), None, None),
690        };
691        let loader = with_cursor_loader(loader, &cursor.or(qd_cursor), order);
692        if let Some(txn) = txn.as_ref() {
693            loader.fetch(txn.as_ref(), 0, opts.limit as u64).await
694        } else {
695            loader.fetch(&self.db, 0, opts.limit as u64).await
696        }
697    }
698
699    /// Returns the first matching row, if any, applying cursor when one is set.
700    ///
701    /// `U` is the [`RepositoryOptions`] identity type.
702    /// Runs against `opts.txn` (or the [`QueryData`] transaction) when attached.
703    pub async fn get_one<U>(
704        &self,
705        filter: Option<QueryData<E>>,
706        opts: Option<RepositoryOptions<U>>,
707    ) -> Result<Option<E::ModelEx>, DbErr>
708    where
709        U: BaseEntity + Clone + Send + Sync + 'static,
710        E::ModelEx: BaseEntity,
711    {
712        let cursor = opts.as_ref().and_then(|o| o.cursor.clone());
713        let txn = pick_txn(
714            opts.as_ref(),
715            filter.as_ref().and_then(|qd| qd.opts.txn.clone()),
716        );
717        let (loader, qd_cursor, order) = match filter {
718            Some(qd) => (qd.loader, qd.opts.cursor, qd.order_asc),
719            None => (<E::ModelEx as BaseEntity>::load(), None, None),
720        };
721        let loader = with_cursor_loader(loader, &cursor.or(qd_cursor), order);
722        if let Some(txn) = txn.as_ref() {
723            Ok(loader.fetch(txn.as_ref(), 0, 1).await?.into_iter().next())
724        } else {
725            Ok(loader.fetch(&self.db, 0, 1).await?.into_iter().next())
726        }
727    }
728
729    /// Returns the first matching row with default options.
730    pub async fn find_one(&self, filter: Option<QueryData<E>>) -> Result<Option<E::ModelEx>, DbErr>
731    where
732        E::ModelEx: BaseEntity,
733    {
734        self.get_one::<NoUser>(filter, None).await
735    }
736
737    /// Finds a row by primary key converted from `id`; returns `None` when absent.
738    pub async fn get_by_id(&self, id: i64) -> Result<Option<E::ModelEx>, DbErr>
739    where
740        E::PrimaryKey: PrimaryKeyTrait,
741        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
742        E::ModelEx: BaseEntity,
743        <E::ModelEx as BaseEntity>::LoaderType: EntityLoaderTrait<E, ModelEx = E::ModelEx>,
744    {
745        <E::ModelEx as BaseEntity>::load()
746            .filter_by_id(id)
747            .fetch(&self.db, 0, 1)
748            .await
749            .map(|mut rows| rows.pop())
750    }
751
752    /// Alias for [`PersistentRepository::get_by_id`].
753    pub async fn find_by_id(&self, id: i64) -> Result<Option<E::ModelEx>, DbErr>
754    where
755        E::PrimaryKey: PrimaryKeyTrait,
756        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
757        E::ModelEx: BaseEntity,
758    {
759        self.get_by_id(id).await
760    }
761
762    /// Finds a row by id; runs inside `opts.txn` when one is attached.
763    ///
764    /// `U` is the [`RepositoryOptions`] identity type.
765    pub async fn get_by_id_with_opts<U>(
766        &self,
767        id: i64,
768        opts: RepositoryOptions<U>,
769    ) -> Result<Option<E::ModelEx>, DbErr>
770    where
771        U: BaseEntity + Clone + Send + Sync + 'static,
772        E::PrimaryKey: PrimaryKeyTrait,
773        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
774        E::ModelEx: BaseEntity,
775    {
776        let loader = <E::ModelEx as BaseEntity>::load().filter_by_id(id);
777        if let Some(txn) = opts.txn.as_ref() {
778            Ok(loader.fetch(txn.as_ref(), 0, 1).await?.into_iter().next())
779        } else {
780            Ok(loader.fetch(&self.db, 0, 1).await?.into_iter().next())
781        }
782    }
783
784    // ── CREATE ──────────────────────────────────────────────────────────────
785
786    /// Inserts one row, filling `uid`/`createdAt`/`updatedAt` (and audit ids when a user id is present).
787    ///
788    /// `A` is the SeaORM active model for `E`; `U` is the [`RepositoryOptions`] identity type.
789    /// Inserts inside `opts.txn` when attached, else on the repository connection.
790    pub async fn create_one<A, U>(
791        &self,
792        mut active: A,
793        opts: Option<RepositoryOptions<U>>,
794    ) -> Result<E::ModelEx, DbErr>
795    where
796        A: ActiveModelTrait<Entity = E> + Send,
797        A: sea_orm::ActiveModelBehavior + Send,
798        E::Model: IntoActiveModel<A>,
799        E::ModelEx: BaseEntity,
800        U: BaseEntity + Clone + Send + Sync + 'static,
801        E::Column: ColumnTrait + FromStr,
802        <E::Column as FromStr>::Err: std::fmt::Debug,
803    {
804        if let Some(o) = opts.as_ref() {
805            apply_base_create(&mut active, o);
806        } else {
807            let empty: RepositoryOptions<U> = RepositoryOptions::default();
808            apply_base_create(&mut active, &empty);
809        }
810        if let Some(txn) = opts_txn(opts.as_ref()).as_ref() {
811            active.insert(txn.as_ref()).await.map(Into::into)
812        } else {
813            active.insert(&self.db).await.map(Into::into)
814        }
815    }
816
817    // keep legacy no-opts overload
818    /// Inserts one row with default options.
819    pub async fn create_one_simple<A>(&self, active: A) -> Result<E::ModelEx, DbErr>
820    where
821        A: ActiveModelTrait<Entity = E> + Send,
822        A: sea_orm::ActiveModelBehavior + Send,
823        E::Model: IntoActiveModel<A>,
824        E::ModelEx: BaseEntity,
825        E::Column: ColumnTrait + FromStr,
826        <E::Column as FromStr>::Err: std::fmt::Debug,
827    {
828        self.create_one::<A, NoUser>(active, None).await
829    }
830
831    /// Inserts one row with the given options.
832    ///
833    /// `A` is the SeaORM active model for `E`; `U` is the [`RepositoryOptions`] identity type.
834    pub async fn create_one_with_opts<A, U>(
835        &self,
836        active: A,
837        opts: RepositoryOptions<U>,
838    ) -> Result<E::ModelEx, DbErr>
839    where
840        A: ActiveModelTrait<Entity = E> + Send,
841        A: sea_orm::ActiveModelBehavior + Send,
842        E::Model: IntoActiveModel<A>,
843        E::ModelEx: BaseEntity,
844        U: BaseEntity + Clone + Send + Sync + 'static,
845        E::Column: ColumnTrait + FromStr,
846        <E::Column as FromStr>::Err: std::fmt::Debug,
847    {
848        self.create_one(active, Some(opts)).await
849    }
850
851    /// Inserts each row one at a time, applying base/audit fields to every row.
852    ///
853    /// `A` is the SeaORM active model for `E`; `U` is the [`RepositoryOptions`] identity type.
854    /// Inserts inside `opts.txn` when attached, else on the repository connection.
855    pub async fn create_many<A, U>(
856        &self,
857        mut actives: Vec<A>,
858        opts: Option<RepositoryOptions<U>>,
859    ) -> Result<Vec<E::ModelEx>, DbErr>
860    where
861        A: ActiveModelTrait<Entity = E> + Send,
862        A: sea_orm::ActiveModelBehavior + Send,
863        E::ModelEx: BaseEntity,
864        E::Model: IntoActiveModel<A>,
865        U: BaseEntity + Clone + Send + Sync + 'static,
866        E::Column: ColumnTrait + FromStr,
867        <E::Column as FromStr>::Err: std::fmt::Debug,
868    {
869        let mut out = Vec::with_capacity(actives.len());
870        for active in &mut actives {
871            if let Some(o) = opts.as_ref() {
872                apply_base_create(active, o);
873            } else {
874                let empty: RepositoryOptions<U> = RepositoryOptions::default();
875                apply_base_create(active, &empty);
876            }
877        }
878        let txn = opts_txn(opts.as_ref());
879        for a in actives {
880            if let Some(txn) = txn.as_ref() {
881                out.push(a.insert(txn.as_ref()).await?.into());
882            } else {
883                out.push(a.insert(&self.db).await?.into());
884            }
885        }
886        Ok(out)
887    }
888
889    /// Inserts many rows with default options.
890    pub async fn create_many_simple<A>(&self, actives: Vec<A>) -> Result<Vec<E::ModelEx>, DbErr>
891    where
892        A: ActiveModelTrait<Entity = E> + Send,
893        A: sea_orm::ActiveModelBehavior + Send,
894        E::ModelEx: BaseEntity,
895        E::Model: IntoActiveModel<A>,
896        E::Column: ColumnTrait + FromStr,
897        <E::Column as FromStr>::Err: std::fmt::Debug,
898    {
899        self.create_many::<A, NoUser>(actives, None).await
900    }
901
902    /// Inserts many rows with the given options.
903    pub async fn create_many_with_opts<A, U>(
904        &self,
905        actives: Vec<A>,
906        opts: RepositoryOptions<U>,
907    ) -> Result<Vec<E::ModelEx>, DbErr>
908    where
909        A: ActiveModelTrait<Entity = E> + Send,
910        A: sea_orm::ActiveModelBehavior + Send,
911        E::ModelEx: BaseEntity,
912        E::Model: IntoActiveModel<A>,
913        U: BaseEntity + Clone + Send + Sync + 'static,
914        E::Column: ColumnTrait + FromStr,
915        <E::Column as FromStr>::Err: std::fmt::Debug,
916    {
917        self.create_many(actives, Some(opts)).await
918    }
919
920    // ── UPSERT ──────────────────────────────────────────────────────────────
921
922    /// Inserts one row with fresh base fields; does not check for an existing id.
923    pub async fn upsert_one<A>(&self, mut active: A) -> Result<E::ModelEx, DbErr>
924    where
925        A: ActiveModelTrait<Entity = E> + Send,
926        A: sea_orm::ActiveModelBehavior + Send,
927        E::Model: IntoActiveModel<A> + Clone,
928        E::Column: ColumnTrait + FromStr,
929        <E::Column as FromStr>::Err: std::fmt::Debug,
930    {
931        let empty: RepositoryOptions<NoUser> = RepositoryOptions::default();
932        apply_base_create(&mut active, &empty);
933        active.insert(&self.db).await.map(Into::into)
934    }
935
936    /// Inserts each row via [`PersistentRepository::upsert_one`], one at a time.
937    pub async fn upsert_many<A>(&self, actives: Vec<A>) -> Result<Vec<E::ModelEx>, DbErr>
938    where
939        A: ActiveModelTrait<Entity = E> + Send,
940        A: sea_orm::ActiveModelBehavior + Send,
941        E::ModelEx: BaseEntity,
942        E::Model: IntoActiveModel<A> + Clone,
943        E::Column: ColumnTrait + FromStr,
944        <E::Column as FromStr>::Err: std::fmt::Debug,
945    {
946        let mut out = Vec::with_capacity(actives.len());
947        for a in actives {
948            out.push(self.upsert_one(a).await?);
949        }
950        Ok(out)
951    }
952
953    /// Updates the row when `id_fn` yields an existing id, else inserts it.
954    ///
955    /// Updates refresh `updatedAt`; inserts fill the full base field set.
956    pub async fn upsert_one_with_id<A, F>(
957        &self,
958        mut active: A,
959        id_fn: F,
960    ) -> Result<E::ModelEx, DbErr>
961    where
962        A: ActiveModelTrait<Entity = E> + Send,
963        A: sea_orm::ActiveModelBehavior + Send,
964        E::Model: IntoActiveModel<A> + Clone + BaseEntity,
965        F: Fn(&A) -> Option<i64> + Send,
966        E::PrimaryKey: PrimaryKeyTrait,
967        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
968        E::Column: ColumnTrait + FromStr,
969        <E::Column as FromStr>::Err: std::fmt::Debug,
970    {
971        if let Some(id) = id_fn(&active) {
972            if self.get_by_id(id).await?.is_some() {
973                // for update, set updated_at/updated_by
974                let empty: RepositoryOptions<NoUser> = RepositoryOptions::default();
975                apply_base_update(&mut active, &empty);
976                return active.update(&self.db).await.map(Into::into);
977            }
978        }
979        let empty: RepositoryOptions<NoUser> = RepositoryOptions::default();
980        apply_base_create(&mut active, &empty);
981        active.insert(&self.db).await.map(Into::into)
982    }
983
984    // ── UPDATE ──────────────────────────────────────────────────────────────
985
986    /// Loads rows via [`PersistentRepository::get_many`], applies `effector`, and saves changed rows.
987    ///
988    /// The effector mutates each model and returns true to persist it; untouched
989    /// rows are skipped. Returns the pre-save models that were persisted.
990    /// `U` is the [`RepositoryOptions`] identity type used for audit fields.
991    /// Reads and writes run inside `opts.txn` when attached.
992    pub async fn update_many<F, U>(
993        &self,
994        filter: Option<QueryData<E>>,
995        mut effector: F,
996        opts: RepositoryOptions<U>,
997    ) -> Result<ChangeResultModel<E::ModelEx>, DbErr>
998    where
999        F: FnMut(&mut E::Model) -> bool + Send,
1000        U: BaseEntity + Clone + Send + Sync + 'static,
1001        E::ModelEx: Into<E::Model> + BaseEntity,
1002        E::Model: IntoActiveModel<E::ActiveModel> + Clone,
1003        E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
1004        E::Column: ColumnTrait + FromStr,
1005        <E::Column as FromStr>::Err: std::fmt::Debug,
1006    {
1007        let txn = opts.txn.clone();
1008        let items = self.get_many(filter, opts.clone()).await?;
1009        let mut affected = Vec::new();
1010        for model in items {
1011            let mut model: E::Model = model.into();
1012            if effector(&mut model) {
1013                let mut am: E::ActiveModel = model.clone().into_active_model();
1014                apply_base_update(&mut am, &opts);
1015                if let Some(txn) = txn.as_ref() {
1016                    am.update(txn.as_ref()).await?;
1017                } else {
1018                    am.update(&self.db).await?;
1019                }
1020                affected.push(model.into());
1021            }
1022        }
1023        Ok(ChangeResultModel::new(affected))
1024    }
1025
1026    /// Alias for [`PersistentRepository::update_many`].
1027    pub async fn find_update_many<F, U>(
1028        &self,
1029        filter: Option<QueryData<E>>,
1030        effector: F,
1031        opts: RepositoryOptions<U>,
1032    ) -> Result<ChangeResultModel<E::ModelEx>, DbErr>
1033    where
1034        F: FnMut(&mut E::Model) -> bool + Send,
1035        U: BaseEntity + Clone + Send + Sync + 'static,
1036        E::ModelEx: Into<E::Model> + BaseEntity,
1037        E::Model: IntoActiveModel<E::ActiveModel> + Clone,
1038        E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
1039        E::Column: ColumnTrait + FromStr,
1040        <E::Column as FromStr>::Err: std::fmt::Debug,
1041    {
1042        self.update_many(filter, effector, opts).await
1043    }
1044
1045    /// Loads the first matching row, applies `effector`, and saves it when changed.
1046    ///
1047    /// Returns `None` when no row matches; returns the row unchanged when the
1048    /// effector returns false.
1049    pub async fn update_one<F>(
1050        &self,
1051        filter: Option<QueryData<E>>,
1052        mut effector: F,
1053    ) -> Result<Option<E::ModelEx>, DbErr>
1054    where
1055        F: FnMut(&mut E::Model) -> bool + Send,
1056        E::ModelEx: Into<E::Model> + BaseEntity,
1057        E::Model: IntoActiveModel<E::ActiveModel> + Clone ,
1058        E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
1059        E::Column: ColumnTrait + FromStr,
1060        <E::Column as FromStr>::Err: std::fmt::Debug,
1061    {
1062        let Some(model) = self.get_one::<NoUser>(filter, None).await? else {
1063            return Ok(None);
1064        };
1065        let mut model: E::Model = model.into();
1066        if !effector(&mut model) {
1067            return Ok(Some(model.into()));
1068        }
1069        let mut am: E::ActiveModel = model.clone().into_active_model();
1070        let empty: RepositoryOptions<NoUser> = RepositoryOptions::default();
1071        apply_base_update(&mut am, &empty);
1072        let updated = am.update(&self.db).await?;
1073        Ok(Some(updated.into()))
1074    }
1075
1076    /// Loads a row by id, applies `effector` to the model, and saves it when changed.
1077    ///
1078    /// Returns `None` when the id does not exist.
1079    pub async fn update_by_id<F>(
1080        &self,
1081        id: i64,
1082        mut effector: F,
1083    ) -> Result<Option<E::ModelEx>, DbErr>
1084    where
1085        F: FnMut(&mut E::Model) -> bool + Send,
1086        E::ModelEx: Into<E::Model> + BaseEntity,
1087        E::Model: IntoActiveModel<E::ActiveModel> + Clone,
1088        E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
1089        E::PrimaryKey: PrimaryKeyTrait,
1090        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
1091        E::Column: ColumnTrait + FromStr,
1092        <E::Column as FromStr>::Err: std::fmt::Debug,
1093    {
1094        let Some(model) = self.get_by_id(id).await? else {
1095            return Ok(None);
1096        };
1097        let mut model: E::Model = model.into();
1098        if !effector(&mut model) {
1099            return Ok(Some(model.into()));
1100        }
1101        let mut am: E::ActiveModel = model.clone().into_active_model();
1102        let empty: RepositoryOptions<NoUser> = RepositoryOptions::default();
1103        apply_base_update(&mut am, &empty);
1104        let updated = am.update(&self.db).await?;
1105        Ok(Some(updated.into()))
1106    }
1107
1108    /// Loads a row by id, applies `effector` to the active model, and saves it when changed.
1109    ///
1110    /// Returns the refetched row unchanged when the effector returns false,
1111    /// or `None` when the id does not exist.
1112    pub async fn update_by_id_with_active<F>(
1113        &self,
1114        id: i64,
1115        mut effector: F,
1116    ) -> Result<Option<E::ModelEx>, DbErr>
1117    where
1118        F: FnMut(&mut E::ActiveModel) -> bool + Send,
1119        E::ModelEx: Into<E::Model> + BaseEntity,
1120        E::Model: IntoActiveModel<E::ActiveModel> + Clone,
1121        E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
1122        E::PrimaryKey: PrimaryKeyTrait,
1123        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
1124        E::Column: ColumnTrait + FromStr,
1125        <E::Column as FromStr>::Err: std::fmt::Debug,
1126    {
1127        let Some(model) = self.get_by_id(id).await? else {
1128            return Ok(None);
1129        };
1130        let model: E::Model = model.into();
1131        let mut am: E::ActiveModel = model.into_active_model();
1132        if !effector(&mut am) {
1133            return self.get_by_id(id).await;
1134        }
1135        let empty: RepositoryOptions<NoUser> = RepositoryOptions::default();
1136        apply_base_update(&mut am, &empty);
1137        let updated = am.update(&self.db).await?;
1138        Ok(Some(updated.into()))
1139    }
1140
1141    /// Loads the first matching row and updates it, recording audit fields from `opts` when present.
1142    ///
1143    /// `U` is the [`RepositoryOptions`] identity type.
1144    /// Reads and writes run inside `opts.txn` (or the filter transaction) when attached.
1145    pub async fn update_one_with_opts<F, U>(
1146        &self,
1147        filter: Option<QueryData<E>>,
1148        mut effector: F,
1149        opts: Option<RepositoryOptions<U>>,
1150    ) -> Result<Option<E::ModelEx>, DbErr>
1151    where
1152        F: FnMut(&mut E::Model) -> bool + Send,
1153        U: BaseEntity + Clone + Send + Sync + 'static,
1154        E::ModelEx: Into<E::Model>,
1155        E::Model: IntoActiveModel<E::ActiveModel> + Clone,
1156        E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
1157        E::Column: ColumnTrait + FromStr,
1158        <E::Column as FromStr>::Err: std::fmt::Debug,
1159    {
1160        let txn = pick_txn(
1161            opts.as_ref(),
1162            filter.as_ref().and_then(|qd| qd.opts.txn.clone()),
1163        );
1164        let Some(model) = self.get_one(filter, opts.clone()).await? else {
1165            return Ok(None);
1166        };
1167        let mut model: E::Model = model.into();
1168        if !effector(&mut model) {
1169            return Ok(Some(model.into()));
1170        }
1171        let mut am: E::ActiveModel = model.clone().into_active_model();
1172        if let Some(o) = opts.as_ref() {
1173            apply_base_update(&mut am, o);
1174        } else {
1175            let empty: RepositoryOptions<NoUser> = RepositoryOptions::default();
1176            apply_base_update(&mut am, &empty);
1177        }
1178        let updated = if let Some(txn) = txn.as_ref() {
1179            am.update(txn.as_ref()).await?
1180        } else {
1181            am.update(&self.db).await?
1182        };
1183        Ok(Some(updated.into()))
1184    }
1185
1186    /// Loads a row by id and updates it, recording audit fields from `opts` when present.
1187    ///
1188    /// `U` is the [`RepositoryOptions`] identity type. Returns `None` when the id does not exist.
1189    /// Reads and writes run inside `opts.txn` when attached.
1190    pub async fn update_by_id_with_opts<F, U>(
1191        &self,
1192        id: i64,
1193        mut effector: F,
1194        opts: Option<RepositoryOptions<U>>,
1195    ) -> Result<Option<E::ModelEx>, DbErr>
1196    where
1197        F: FnMut(&mut E::Model) -> bool + Send,
1198        U: BaseEntity + Clone + Send + Sync + 'static,
1199        E::ModelEx: Into<E::Model> + BaseEntity,
1200        E::Model: IntoActiveModel<E::ActiveModel> + Clone,
1201        E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
1202        E::PrimaryKey: PrimaryKeyTrait,
1203        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
1204        E::Column: ColumnTrait + FromStr,
1205        <E::Column as FromStr>::Err: std::fmt::Debug,
1206    {
1207        let txn = opts_txn(opts.as_ref());
1208        let Some(model) = (if let Some(o) = opts.clone() {
1209            // `get_by_id_with_opts` is txn-aware, so the read stays in-transaction.
1210            self.get_by_id_with_opts(id, o).await?
1211        } else {
1212            self.get_by_id(id).await?
1213        }) else {
1214            return Ok(None);
1215        };
1216        let mut model: E::Model = model.into();
1217        if !effector(&mut model) {
1218            return Ok(Some(model.into()));
1219        }
1220        let mut am: E::ActiveModel = model.clone().into_active_model();
1221        if let Some(o) = opts.as_ref() {
1222            apply_base_update(&mut am, o);
1223        } else {
1224            let empty: RepositoryOptions<NoUser> = RepositoryOptions::default();
1225            apply_base_update(&mut am, &empty);
1226        }
1227        let updated = if let Some(txn) = txn.as_ref() {
1228            am.update(txn.as_ref()).await?
1229        } else {
1230            am.update(&self.db).await?
1231        };
1232        Ok(Some(updated.into()))
1233    }
1234
1235    // ── DELETE ──────────────────────────────────────────────────────────────
1236
1237    /// Deletes rows matching the filter and returns the affected row count.
1238    pub async fn delete_many(&self, filter: Option<DeleteQueryData<E>>) -> Result<u64, DbErr> {
1239        self.delete_many_with_opts::<NoUser>(filter, None).await
1240    }
1241    /// Deletes rows matching the filter with cursor support and returns the affected row count.
1242    ///
1243    /// `U` is the [`RepositoryOptions`] identity type. Deletes run inside
1244    /// `opts.txn` when attached, else on the repository connection.
1245    pub async fn delete_many_with_opts<U>(
1246        &self,
1247        filter: Option<DeleteQueryData<E>>,
1248        opts: Option<RepositoryOptions<U>>,
1249    ) -> Result<u64, DbErr>
1250    where
1251        U: BaseEntity + Clone + Send + Sync + 'static,
1252    {
1253        let cursor = opts.as_ref().and_then(|o| o.cursor.clone());
1254        let txn = opts_txn(opts.as_ref());
1255        let del = filter
1256            .map(|qd| qd.delete)
1257            .unwrap_or_else(|| E::delete_many());
1258        let del = with_cursor_delete(del, &cursor);
1259        let res = if let Some(txn) = txn.as_ref() {
1260            del.exec(txn.as_ref()).await?
1261        } else {
1262            del.exec(&self.db).await?
1263        };
1264        Ok(res.rows_affected)
1265    }
1266    /// Alias for [`PersistentRepository::delete_many`].
1267    pub async fn remove_many(&self, filter: Option<DeleteQueryData<E>>) -> Result<u64, DbErr> {
1268        self.delete_many(filter).await
1269    }
1270    /// Alias for [`PersistentRepository::delete_many_with_opts`].
1271    pub async fn remove_many_with_opts<U>(
1272        &self,
1273        filter: Option<DeleteQueryData<E>>,
1274        opts: Option<RepositoryOptions<U>>,
1275    ) -> Result<u64, DbErr>
1276    where
1277        U: BaseEntity + Clone + Send + Sync + 'static,
1278    {
1279        self.delete_many_with_opts(filter, opts).await
1280    }
1281    /// Deletes matching rows and returns true when at least one row was deleted.
1282    pub async fn delete_one(&self, filter: Option<DeleteQueryData<E>>) -> Result<bool, DbErr> {
1283        self.delete_one_with_opts::<NoUser>(filter, None).await
1284    }
1285    /// Deletes matching rows with cursor support; returns true when at least one row was deleted.
1286    ///
1287    /// `U` is the [`RepositoryOptions`] identity type. Deletes run inside
1288    /// `opts.txn` when attached.
1289    pub async fn delete_one_with_opts<U>(
1290        &self,
1291        filter: Option<DeleteQueryData<E>>,
1292        opts: Option<RepositoryOptions<U>>,
1293    ) -> Result<bool, DbErr>
1294    where
1295        U: BaseEntity + Clone + Send + Sync + 'static,
1296    {
1297        Ok(self.delete_many_with_opts(filter, opts).await? > 0)
1298    }
1299
1300    /// Deletes the row with the given id and returns the deleted model, or `None` when absent.
1301    pub async fn delete_by_id(&self, id: i64) -> Result<Option<E::ModelEx>, DbErr>
1302    where
1303        E::PrimaryKey: PrimaryKeyTrait,
1304        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
1305        E::ModelEx: BaseEntity,
1306    {
1307        let Some(model) = self.get_by_id(id).await? else {
1308            return Ok(None);
1309        };
1310        let pk = <<E::PrimaryKey as PrimaryKeyTrait>::ValueType as From<i64>>::from(id);
1311        E::delete_by_id(pk).exec(&self.db).await?;
1312        Ok(Some(model))
1313    }
1314
1315    /// Deletes the row with the given id using `opts.txn` when attached.
1316    ///
1317    /// `U` is the [`RepositoryOptions`] identity type. Reads and deletes run
1318    /// inside the attached transaction; otherwise on the repository connection.
1319    pub async fn delete_by_id_with_opts<U>(
1320        &self,
1321        id: i64,
1322        opts: Option<RepositoryOptions<U>>,
1323    ) -> Result<Option<E::ModelEx>, DbErr>
1324    where
1325        U: BaseEntity + Clone + Send + Sync + 'static,
1326        E::PrimaryKey: PrimaryKeyTrait,
1327        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
1328        E::ModelEx: BaseEntity,
1329    {
1330        let txn = opts_txn(opts.as_ref());
1331        let Some(model) = (if let Some(txn) = txn.as_ref() {
1332            <E::ModelEx as BaseEntity>::load()
1333                .filter_by_id(id)
1334                .fetch(txn.as_ref(), 0, 1)
1335                .await?
1336                .into_iter()
1337                .next()
1338        } else {
1339            self.get_by_id(id).await?
1340        }) else {
1341            return Ok(None);
1342        };
1343        let pk = <<E::PrimaryKey as PrimaryKeyTrait>::ValueType as From<i64>>::from(id);
1344        if let Some(txn) = txn.as_ref() {
1345            E::delete_by_id(pk).exec(txn.as_ref()).await?;
1346        } else {
1347            E::delete_by_id(pk).exec(&self.db).await?;
1348        }
1349        Ok(Some(model))
1350    }
1351
1352    /// Deletes the row with the given id inside the transaction, returning the deleted model if found.
1353    pub async fn delete_by_id_txn(
1354        &self,
1355        id: i64,
1356        txn: &DatabaseTransaction,
1357    ) -> Result<Option<E::ModelEx>, DbErr>
1358    where
1359        E::PrimaryKey: PrimaryKeyTrait,
1360        <E::PrimaryKey as PrimaryKeyTrait>::ValueType: From<i64>,
1361        E::ModelEx: BaseEntity,
1362    {
1363        let pk = <<E::PrimaryKey as PrimaryKeyTrait>::ValueType as From<i64>>::from(id);
1364        let Some(model) = <E::ModelEx as BaseEntity>::load()
1365            .filter_by_id(id)
1366            .fetch(txn, 0, 1)
1367            .await?
1368            .into_iter()
1369            .next()
1370        else {
1371            return Ok(None);
1372        };
1373        E::delete_by_id(pk).exec(txn).await?;
1374        Ok(Some(model))
1375    }
1376
1377    /// Deletes rows matching the filter inside the transaction, returning the affected row count.
1378    pub async fn delete_many_txn(
1379        &self,
1380        filter: Option<DeleteQueryData<E>>,
1381        txn: &DatabaseTransaction,
1382    ) -> Result<u64, DbErr> {
1383        self.delete_many_txn_with_opts::<NoUser>(filter, None, txn)
1384            .await
1385    }
1386    /// Deletes rows matching the filter inside the transaction with cursor support.
1387    ///
1388    /// `U` is the [`RepositoryOptions`] identity type. When `opts` carries its
1389    /// own transaction it takes precedence; otherwise the explicit `txn` is used.
1390    pub async fn delete_many_txn_with_opts<U>(
1391        &self,
1392        filter: Option<DeleteQueryData<E>>,
1393        opts: Option<RepositoryOptions<U>>,
1394        txn: &DatabaseTransaction,
1395    ) -> Result<u64, DbErr>
1396    where
1397        U: BaseEntity + Clone + Send + Sync + 'static,
1398    {
1399        let cursor = opts.as_ref().and_then(|o| o.cursor.clone());
1400        let del = filter
1401            .map(|qd| qd.delete)
1402            .unwrap_or_else(|| E::delete_many());
1403        let del = with_cursor_delete(del, &cursor);
1404        if let Some(opts_txn) = opts_txn(opts.as_ref()) {
1405            // `RepositoryOptions` stays the single source of truth when both
1406            // handles are supplied.
1407            let res = del.exec(opts_txn.as_ref()).await?;
1408            return Ok(res.rows_affected);
1409        }
1410        let res = del.exec(txn).await?;
1411        Ok(res.rows_affected)
1412    }
1413}
1414
1415// Helper to generate uid for new entities
1416/// Generates a new UUID v4 for `uid` columns on new rows.
1417pub fn new_uid() -> Uuid {
1418    Uuid::new_v4()
1419}