Skip to content

Keep sea-orm-sync's DatabaseConnection Send + Sync (#3070) - #3166

Open
teddytennant wants to merge 1 commit into
SeaQL:masterfrom
teddytennant:fix/3070-sync-send-sync
Open

Keep sea-orm-sync's DatabaseConnection Send + Sync (#3070)#3166
teddytennant wants to merge 1 commit into
SeaQL:masterfrom
teddytennant:fix/3070-sync-send-sync

Conversation

@teddytennant

Copy link
Copy Markdown

Fixes #3070.

The bug

In sea-orm-sync, DatabaseConnection is neither Send nor Sync, so it cannot be stored in a static:

static DB_LOCK: std::sync::OnceLock<DatabaseConnection> = std::sync::OnceLock::new();
error[E0277]: `(dyn for<'a, 'b> Fn(&'a sea_orm::metric::Info<'b>) + 'static)` cannot be shared between threads safely
   = note: required for `Arc<(dyn for<'a, 'b> Fn(&'a sea_orm::metric::Info<'b>) + 'static)>` to implement `Sync`
note: required because it appears within the type `RusqliteSharedConnection`
note: required because it appears within the type `DatabaseConnectionType`
note: required because it appears within the type `DatabaseConnection`
   = note: shared static variables must have a type that implements `Sync`

This is not a property of the sync design — RusqliteSharedConnection already holds its connection in an Arc<Mutex<State>>, and make-sync.sh deliberately maps futures_util::lock::Mutex to std::sync::Mutex. The only things standing in the way are two trait objects that lost their bounds during generation.

Root cause

make-sync.sh strips Send/Sync from every bound in src/:

replace_rs 's/Send + Sync + //' src
replace_rs 's/ + Sync//' src
replace_rs 's/ + Send//' src
replace_rs 's/Send + //' src

That is right for future/executor bounds, but the strip is indiscriminate and also hits two trait objects that are plain data stored inside DatabaseConnection:

async source generated sea-orm-sync
type Callback = Arc<dyn Fn(&Info<'_>) + Send + Sync> type Callback = Arc<dyn Fn(&Info<'_>)>
pub trait MockDatabaseTrait: Send + Debug pub trait MockDatabaseTrait: Debug

Neither has anything to do with async. The script already recognises this class of over-strip and repairs it one line below, for the error type:

replace_rs 's/Arc<dyn std::error::Error>/Arc<dyn std::error::Error + Send + Sync>/' src

@Huliiiiii diagnosed this on the issue the day it was filed ("make sync script lacks coverage") — this PR fixes it in the generator rather than hand-editing sea-orm-sync/, since that directory is generated output.

The fix

build-tools/make-sync.sh — three restores placed next to the existing Arc<dyn std::error::Error> one: the metric::Callback alias, the set_metric_callback F bound it is constructed from, and MockDatabaseTrait. sea-orm-sync/ is then regenerated; the regeneration is idempotent (running the script twice produces a byte-identical tree, and running it on master before this change is a no-op).

src/driver/rusqlite.rsRusqliteSharedConnection::set_metric_callback was declared as F: Fn(&Info<'_>) + 'static, while the three sqlx drivers all use F: Fn(&Info<'_>) + Send + Sync + 'static. The async crate never catches this because the root crate cannot build with --features rusqlite (it needs sea_query_rusqlite, which is only wired up in sea-orm-sync/Cargo.toml). Aligned with its siblings.

Test

The repo already pins this invariant in src/database/db_connection.rs, but with #[cfg(not(feature = "sync"))] so it never ran for the sync crate. The gate is removed so the assertion covers both crates. It is written as two single-bound helpers rather than one assert_send_sync because make-sync.sh would strip a multi-bound clause.

#[test]
fn assert_database_connection_traits() {
    fn assert_send<T: Send>() {}
    fn assert_sync<T: Sync>() {}

    assert_send::<DatabaseConnection>();
    assert_sync::<DatabaseConnection>();
}

Before (cd sea-orm-sync && cargo test --lib --features rusqlite, with the test present but the generator unfixed):

error[E0277]: `(dyn for<'a, 'b> Fn(&'a metric::Info<'b>) + 'static)` cannot be shared between threads safely
error[E0277]: `(dyn for<'a, 'b> Fn(&'a metric::Info<'b>) + 'static)` cannot be sent between threads safely
error[E0277]: `(dyn driver::mock::MockDatabaseTrait + 'static)` cannot be sent between threads safely
error: could not compile `sea-orm-sync` (lib test) due to 6 previous errors

After:

test database::db_connection::tests::assert_database_connection_traits ... ok
test result: ok. 249 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

The reporter's original snippet now compiles against sea-orm-sync as well.

Also run:

  • cd sea-orm-sync && DATABASE_URL="sqlite::memory:" cargo test --test '*' --features tests-features,rusqlite — all suites green (the rusqlite CI job's command)
  • cargo test --lib on the async crate — 249 passed
  • cargo fmt --all -- --check (nightly) and the same for sea-orm-sync — clean
  • cargo clippy --all -- -D warnings and cargo clippy --all --features runtime-tokio-native-tls,sqlx-all -- -D warnings — clean

I checked that a partial fix is still caught: restoring only Send on the callback alias, or dropping only the MockDatabaseTrait restore, both leave the assertion failing to compile.

Deliberately not changed

  • No hand edits under sea-orm-sync/src/ — everything there in this diff is generator output.
  • MockDatabaseTrait gaining Send is a bound tightening for sea-orm-sync implementors, but it only restores parity with sea-orm, where the trait has always required Send.
  • The rusqlite CI job runs cargo test --test '*', so it does not execute lib unit tests and will not enforce this assertion for sea-orm-sync (same gap noted in Fix sync-variant futures_util handling; regenerate sea-orm-sync #3112). Happy to add --lib to that job if you want it enforced; I left CI alone to keep the diff focused.

make-sync.sh strips the Send and Sync bounds from every trait bound in src/ so
that the generated crate carries no executor bounds. The strip is
indiscriminate, so it also removed the bounds from two trait objects that are
stored as plain data inside DatabaseConnection: the metric callback
(Arc<dyn Fn(&Info<'_>)>) and MockDatabaseTrait. That left sea-orm-sync's
DatabaseConnection neither Send nor Sync, so it cannot be put in a static
OnceLock, even though the connection it wraps is already an Arc<Mutex<State>>.

Restore both bounds after the blanket strip, alongside the existing
Arc<dyn std::error::Error> restore, and drop the cfg gate that excluded
sea-orm-sync from the DatabaseConnection trait assertion.

RusqliteSharedConnection::set_metric_callback was missing the bound in the
async source too, unlike its three sqlx siblings; it is never type-checked
there because the root crate cannot build with --features rusqlite.
@Huliiiiii

Copy link
Copy Markdown
Member

@teddytennant

Copy link
Copy Markdown
Author

Thanks. I did use an LLM on this PR (mostly on the writeup and while
digging through make-sync.sh). I understand the change: the sync generator
strips Send/Sync too broadly, and DatabaseConnection loses Sync because of
the metric callback and MockDatabaseTrait. Fix is restore those bounds next
to the existing Error restore, then regenerate.

Say if you want the description shortened.

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.

sea-orm-sync + rusqlite , cannot put DatabaseConnection in OnceLock because it is not Sync

2 participants