sea-orm 2.0.1

🐚 An async & dynamic ORM for Rust
Documentation
//! Regression tests for <https://github.com/SeaQL/sea-orm/issues/3147>: the
//! mutation methods generated by `#[sea_orm::model]` must produce `Send`
//! futures, including when the caller is generic over the connection type.
//!
//! The generated `action` opens a transaction and then saves related models
//! inside it, so `action::<C>` recurses into `action::<C::Transaction>`. Proving
//! such a future `Send` without knowing `C` used to walk an unbounded
//! `C::Transaction::Transaction::...` chain and never terminate (E0275).
//!
//! `TransactionTrait` now requires `Sync` and pins `Transaction` to a fixed
//! point, which collapses that chain to a single type. The `assert_send` calls
//! below are the actual regression tests: each one fails to compile without
//! those bounds. `clippy::future_not_send` is denied as a second net, since that
//! is the lint the original reporter hit.
//!
//! This is an async-only concern: the sync crate (`sea-orm-sync`) has no futures
//! to prove `Send`, so the gate below compiles the whole file out there. `Send`
//! is only ever demanded in a `sqlx` (async) build, hence `feature = "sqlx-dep"`.

#![cfg(feature = "sqlx-dep")]
#![deny(clippy::future_not_send)]
#![allow(dead_code)]

mod parent {
    use sea_orm::entity::prelude::*;

    #[sea_orm::model]
    #[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "sf_parent")]
    pub struct Model {
        #[sea_orm(primary_key)]
        pub id: i32,
        pub name: String,
        #[sea_orm(has_many)]
        pub children: HasMany<super::child::Entity>,
        #[sea_orm(has_one)]
        pub detail: HasOne<super::detail::Entity>,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

mod child {
    use sea_orm::entity::prelude::*;

    #[sea_orm::model]
    #[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "sf_child")]
    pub struct Model {
        #[sea_orm(primary_key)]
        pub id: i32,
        pub parent_id: Option<i32>,
        #[sea_orm(belongs_to, from = "parent_id", to = "id")]
        pub parent: BelongsTo<Option<super::parent::Entity>>,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

mod detail {
    use sea_orm::entity::prelude::*;

    #[sea_orm::model]
    #[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "sf_detail")]
    pub struct Model {
        #[sea_orm(primary_key)]
        pub id: i32,
        pub parent_id: i32,
        #[sea_orm(belongs_to, from = "parent_id", to = "id")]
        pub parent: BelongsTo<super::parent::Entity>,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

// Many-to-many through a junction table exercises the `many_to_many_action`
// recursion path, which the macro generates separately from has_many.
mod post {
    use sea_orm::entity::prelude::*;

    #[sea_orm::model]
    #[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "sf_post")]
    pub struct Model {
        #[sea_orm(primary_key)]
        pub id: i32,
        pub title: String,
        #[sea_orm(has_many, via = "post_tag")]
        pub tags: HasMany<super::tag::Entity>,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

mod tag {
    use sea_orm::entity::prelude::*;

    #[sea_orm::model]
    #[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "sf_tag")]
    pub struct Model {
        #[sea_orm(primary_key)]
        pub id: i32,
        pub name: String,
        #[sea_orm(has_many, via = "post_tag")]
        pub posts: HasMany<super::post::Entity>,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

mod post_tag {
    use sea_orm::entity::prelude::*;

    #[sea_orm::model]
    #[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "sf_post_tag")]
    pub struct Model {
        #[sea_orm(primary_key, auto_increment = false)]
        pub post_id: i32,
        #[sea_orm(primary_key, auto_increment = false)]
        pub tag_id: i32,
        #[sea_orm(belongs_to, from = "post_id", to = "id")]
        pub post: BelongsTo<super::post::Entity>,
        #[sea_orm(belongs_to, from = "tag_id", to = "id")]
        pub tag: BelongsTo<super::tag::Entity>,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

// A self-referential relation is the tightest case: the recursion returns to the
// same entity, so nothing but the fixed-point bound can terminate it.
mod node {
    use sea_orm::entity::prelude::*;

    #[sea_orm::model]
    #[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "sf_node")]
    pub struct Model {
        #[sea_orm(primary_key)]
        pub id: i32,
        #[sea_orm(enum_name = "ParentId")]
        pub parent_ref: Option<i32>,
        #[sea_orm(self_ref, relation_enum = "Parent", from = "ParentId", to = "id")]
        pub parent: BelongsTo<Option<Entity>>,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

// An entity with no relations at all still gets the same generated methods, so
// it must stay callable from the same generic contexts.
mod standalone {
    use sea_orm::entity::prelude::*;

    #[sea_orm::model]
    #[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
    #[sea_orm(table_name = "sf_standalone")]
    pub struct Model {
        #[sea_orm(primary_key)]
        pub id: i32,
        pub name: String,
    }

    impl ActiveModelBehavior for ActiveModel {}
}

use sea_orm::entity::prelude::async_trait;
use sea_orm::{
    ActiveModelAction, ConnectionTrait, DatabaseConnection, DatabaseExecutor, DatabaseTransaction,
    DbErr, TransactionTrait,
};

fn assert_send<T: Send>(_: T) {}

// ---------------------------------------------------------------------------
// Generic over `C: TransactionTrait` -- the bound the generated methods declare.
// Every generated method, on every relation shape, must yield a `Send` future
// here without the caller restating any extra bound.
// ---------------------------------------------------------------------------

fn generic_every_method<C: TransactionTrait>(
    db: &C,
    active: parent::ActiveModelEx,
    model: parent::Model,
    model_ex: parent::ModelEx,
) {
    assert_send(active.clone().insert(db));
    assert_send(active.clone().update(db));
    assert_send(active.clone().save(db));
    assert_send(active.clone().delete(db));
    assert_send(active.action(ActiveModelAction::Save, db));
    assert_send(model.cascade_delete(db));
    assert_send(model_ex.delete(db));
}

fn generic_has_one<C: TransactionTrait>(db: &C, active: detail::ActiveModelEx) {
    assert_send(active.insert(db));
}

fn generic_belongs_to<C: TransactionTrait>(db: &C, active: child::ActiveModelEx) {
    assert_send(active.insert(db));
}

fn generic_many_to_many<C: TransactionTrait>(
    db: &C,
    post: post::ActiveModelEx,
    junction: post_tag::ActiveModelEx,
) {
    assert_send(post.insert(db));
    assert_send(junction.insert(db));
}

fn generic_self_ref<C: TransactionTrait>(db: &C, active: node::ActiveModelEx) {
    assert_send(active.insert(db));
}

fn generic_no_relations<C: TransactionTrait>(db: &C, active: standalone::ActiveModelEx) {
    assert_send(active.insert(db));
}

// ---------------------------------------------------------------------------
// Generic over `C: ConnectionTrait + TransactionTrait` -- the idiomatic bound
// for a repository helper that accepts both a pool and a transaction. This is
// the shape most likely to appear in user code, so it gets its own coverage.
// ---------------------------------------------------------------------------

async fn repository_helper<C>(db: &C, name: &str) -> Result<parent::ModelEx, DbErr>
where
    C: ConnectionTrait + TransactionTrait,
{
    parent::ActiveModel::builder()
        .set_name(name)
        .insert(db)
        .await
}

fn repository_helper_is_send<C: ConnectionTrait + TransactionTrait>(db: &C) {
    assert_send(repository_helper(db, "a"));
}

async fn impl_trait_helper(
    db: &(impl ConnectionTrait + TransactionTrait),
    name: &str,
) -> Result<parent::ModelEx, DbErr> {
    parent::ActiveModel::builder()
        .set_name(name)
        .insert(db)
        .await
}

// A generic helper that opens its own transaction and passes it down, so the
// generated methods are instantiated at `C::Transaction` as well as at `C`.
// This is what forces the recursion to be walked more than one level deep.
async fn nested_helper<C>(db: &C, name: &str) -> Result<parent::ModelEx, DbErr>
where
    C: ConnectionTrait + TransactionTrait,
{
    let txn = db.begin().await?;
    parent::ActiveModel::builder()
        .set_name(name)
        .insert(&txn)
        .await
}

fn nested_helper_is_send<C: ConnectionTrait + TransactionTrait>(db: &C) {
    assert_send(nested_helper(db, "a"));
}

// ---------------------------------------------------------------------------
// Concrete connection types. These all worked before the fix; they are here so
// that tightening the bounds can never silently exclude one of them.
// ---------------------------------------------------------------------------

fn concrete_connection(db: &DatabaseConnection, active: parent::ActiveModelEx) {
    assert_send(active.insert(db));
}

fn concrete_transaction(db: &DatabaseTransaction, active: parent::ActiveModelEx) {
    assert_send(active.insert(db));
}

fn concrete_executor(db: &DatabaseExecutor<'_>, active: parent::ActiveModelEx) {
    assert_send(active.insert(db));
}

// ---------------------------------------------------------------------------
// `TransactionTrait` must stay implementable outside the crate. Delegating to
// `DatabaseTransaction` is the realistic downstream shape, and it satisfies the
// fixed point because `DatabaseTransaction` is its own transaction type.
// ---------------------------------------------------------------------------

struct CustomConnection {
    inner: DatabaseConnection,
}

#[async_trait::async_trait]
impl TransactionTrait for CustomConnection {
    type Transaction = DatabaseTransaction;

    async fn begin(&self) -> Result<Self::Transaction, DbErr> {
        self.inner.begin().await
    }

    async fn begin_with_config(
        &self,
        isolation_level: Option<sea_orm::IsolationLevel>,
        access_mode: Option<sea_orm::AccessMode>,
    ) -> Result<Self::Transaction, DbErr> {
        self.inner
            .begin_with_config(isolation_level, access_mode)
            .await
    }

    async fn begin_with_options(
        &self,
        options: sea_orm::TransactionOptions,
    ) -> Result<Self::Transaction, DbErr> {
        self.inner.begin_with_options(options).await
    }

    async fn transaction<F, T, E>(&self, callback: F) -> Result<T, sea_orm::TransactionError<E>>
    where
        F: for<'c> FnOnce(
                &'c Self::Transaction,
            ) -> std::pin::Pin<
                Box<dyn std::future::Future<Output = Result<T, E>> + Send + 'c>,
            > + Send,
        T: Send,
        E: std::fmt::Display + std::fmt::Debug + Send,
    {
        self.inner.transaction(callback).await
    }

    async fn transaction_with_config<F, T, E>(
        &self,
        callback: F,
        isolation_level: Option<sea_orm::IsolationLevel>,
        access_mode: Option<sea_orm::AccessMode>,
    ) -> Result<T, sea_orm::TransactionError<E>>
    where
        F: for<'c> FnOnce(
                &'c Self::Transaction,
            ) -> std::pin::Pin<
                Box<dyn std::future::Future<Output = Result<T, E>> + Send + 'c>,
            > + Send,
        T: Send,
        E: std::fmt::Display + std::fmt::Debug + Send,
    {
        self.inner
            .transaction_with_config(callback, isolation_level, access_mode)
            .await
    }
}

fn custom_connection_works(db: &CustomConnection, active: parent::ActiveModelEx) {
    assert_send(active.insert(db));
}

// ---------------------------------------------------------------------------
// The bounds hold for every `TransactionTrait` implementor. If one ever stops
// holding it breaks all generated mutation methods at once, so assert it
// directly rather than inferring it from a call site.
// ---------------------------------------------------------------------------

fn assert_transaction_trait_bounds<C>()
where
    C: TransactionTrait + Sync,
    C::Transaction: TransactionTrait<Transaction = C::Transaction> + Send,
{
}

#[test]
fn transaction_trait_implementors_satisfy_bounds() {
    assert_transaction_trait_bounds::<DatabaseConnection>();
    assert_transaction_trait_bounds::<DatabaseTransaction>();
    assert_transaction_trait_bounds::<DatabaseExecutor<'_>>();
    assert_transaction_trait_bounds::<CustomConnection>();
}

#[test]
fn generated_active_model_ex_mutation_futures_are_send() {
    let _ = parent::ActiveModel::builder();
    let _ = child::ActiveModel::builder();
}