Skip to content

Require TransactionTrait::Transaction to be a fixed point (#3147) - #3153

Merged
tyt2y3 merged 1 commit into
masterfrom
transaction-trait-fixed-point
Jul 31, 2026
Merged

Require TransactionTrait::Transaction to be a fixed point (#3147)#3153
tyt2y3 merged 1 commit into
masterfrom
transaction-trait-fixed-point

Conversation

@tyt2y3

@tyt2y3 tyt2y3 commented Jul 30, 2026

Copy link
Copy Markdown
Member

Closes #3147. Alternative to #3149 by @Huliiiiii

Problem

The methods generated by #[sea_orm::model] open a transaction and save related models inside it, so action::<C> recurses into action::<C::Transaction>. Proving those futures Send without a concrete C walks the unbounded C::Transaction::Transaction::… chain and overflows (E0275 / clippy::future_not_send) — the documented 2.0 entity format fails to compile under clippy.

Fix

On the trait, not the generated methods. TransactionTrait now requires Sync and pins its associated Transaction to a Send fixed point (Transaction::Transaction == Transaction), collapsing the chain to one type so the obligation terminates.

Why on the trait

  • Sync / Send exclude no implementor — the #[async_trait] desugaring already forces both (begin holds &self across an await; TransactionSession::commit takes self by value). Only a non-fixed-point Transaction is newly rejected, and it errors at the offending impl, not at #[sea_orm::model]. All in-crate implementors are fixed points already.
  • Putting the bounds on the generated methods (Add Send bounds for transactions in ActiveModelEx methods #3149) leaks into user call sites: fn create<C: ConnectionTrait + TransactionTrait>(db: &C) compiles on 2.0.0 and stops compiling, with no exported alias to restate. On the trait it is invisible to callers.

@Huliiiiii

Copy link
Copy Markdown
Member

Is it possible that TransactionTrait::Transition is different from itself?

@tyt2y3

tyt2y3 commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

I worried about this too. claude research says:

To be broken by the fixed-point requirement you need a hand-written impl TransactionTrait whose Transaction type is itself not a fixed point — two custom layers deep. But to implement the trait at all you must produce a Transaction: ConnectionTrait + TransactionTrait + TransactionSession, and the only realistic way to get one is to delegate to DatabaseTransaction

meaning, unlikely anyone would have a custom impl that is not backed by a transaction manager

@Huliiiiii

Copy link
Copy Markdown
Member

I worried about this too. claude research says:

To be broken by the fixed-point requirement you need a hand-written impl TransactionTrait whose Transaction type is itself not a fixed point — two custom layers deep. But to implement the trait at all you must produce a Transaction: ConnectionTrait + TransactionTrait + TransactionSession, and the only realistic way to get one is to delegate to DatabaseTransaction

meaning, unlikely anyone would have a custom impl that is not backed by a transaction manager

Sounds reasonable to me.

Comment thread sea-orm-sync/src/database/connection.rs Outdated
@tyt2y3
tyt2y3 force-pushed the transaction-trait-fixed-point branch from 6752398 to ee9edc9 Compare July 31, 2026 16:15
The methods generated by `#[sea_orm::model]` (`insert`/`update`/`save`/
`delete`/`action`/`cascade_delete`) open a transaction and then save related
models inside it, so `action::<C>` recurses into `action::<C::Transaction>`.
Proving those futures `Send` without a concrete `C` walked the unbounded
`C::Transaction::Transaction::...` chain and overflowed (E0275 /
`clippy::future_not_send`), so the documented 2.0 entity format failed to
compile under clippy.

Fix it on the trait rather than on the generated methods: `TransactionTrait`
now requires `Sync` and pins its associated `Transaction` to a fixed point
(`Transaction::Transaction == Transaction`) that is `Send`. That collapses the
chain to a single type, so the obligation terminates.

`Sync` and `Send` exclude no implementor: the `#[async_trait]` desugaring
already forces both, since `begin` holds `&self` across an await and
`TransactionSession::commit` takes `self` by value. Only a non-fixed-point
`Transaction` is newly rejected, and it errors at the offending `impl` rather
than at `#[sea_orm::model]`. All in-crate implementors are fixed points already.

Putting the bounds on the generated methods instead (as in #3149) fixes the
same overflow but leaks into user call sites: a helper generic over the
connection, e.g. `fn create<C: ConnectionTrait + TransactionTrait>(db: &C)`,
compiles on 2.0.0 and stops compiling, with no exported alias to restate. On
the trait the requirement is invisible to callers.

The regression test is async-only (the sync crate has no futures to prove
`Send`), so it lives in the main crate's `tests/` gated on `feature =
"sqlx-dep"` and compiles out in `sea-orm-sync`. Its `assert_send` calls are the
actual checks, covering every generated method generically over the connection
across has_many / has_one / belongs_to / many-to-many / self-ref / no-relation
shapes, the `ConnectionTrait + TransactionTrait` and `impl Trait` caller forms,
the concrete connection types, and a downstream `TransactionTrait` impl. Without
the fix it fails with 7 E0275, checked by plain `cargo test`.
@tyt2y3
tyt2y3 force-pushed the transaction-trait-fixed-point branch from ee9edc9 to 17c0ed4 Compare July 31, 2026 21:41
@tyt2y3
tyt2y3 merged commit d2e0e58 into master Jul 31, 2026
39 checks passed
@tyt2y3
tyt2y3 deleted the transaction-trait-fixed-point branch July 31, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2.0 - New Entity Format - Compiler (recursion) error

2 participants