|
| 1 | +--- |
| 2 | +slug: 2025-12-12-sea-orm-2.0 |
| 3 | +title: 'How we made SeaORM synchronous' |
| 4 | +author: SeaQL Team |
| 5 | +author_title: Chris Tsang |
| 6 | +author_url: https://github.com/SeaQL |
| 7 | +author_image_url: https://www.sea-ql.org/blog/img/SeaQL.png |
| 8 | +image: https://www.sea-ql.org/blog/img/SeaORM%202.0%20Banner.png |
| 9 | +tags: [news] |
| 10 | +--- |
| 11 | + |
| 12 | +<img alt="SeaORM 2.0 Banner" src="/blog/img/SeaORM%202.0%20Banner.png"/> |
| 13 | + |
| 14 | +SeaORM began as Rust's first async‑first ORM. Now we've come full circle with a synchronous crate: perfect for building lightweight CLI programs with SQLite. |
| 15 | + |
| 16 | +In this post, we'll share how we ported a complex library like SeaORM, the tricks we learnt along the way, and the steps we're taking to keep it maintainable for the long run. |
| 17 | + |
| 18 | +## Gist |
| 19 | + |
| 20 | +We took an approach of translation: we wrote a [script](https://github.com/SeaQL/sea-orm/blob/master/build-tools/make-sync.sh) to convert async functions into synchronous ones. (It's more than a simple find‑and‑replace.) |
| 21 | + |
| 22 | +The script would read the `src` directory and rewrite that into a new crate [`sea-orm-sync`](http://crates.io/crates/sea-orm-sync). This crate isn't a fork: it will be continually rebased on sea-orm, inheriting all new features and bug fixes. |
| 23 | + |
| 24 | +`sea-orm-sync` supports the entire SeaORM's API surface, including recent features like Entity Loader, Nested ActiveModel and Entity-First workflow. |
| 25 | + |
| 26 | +## Async -> Sync |
| 27 | + |
| 28 | +At a high level, async Rust can be seen as a more complex form of sync Rust. So converting an async program is possible by stripping out the runtime and removing all `async` / `await` usage. |
| 29 | + |
| 30 | +However, you can't always go from sync to async. Async Rust tightens lifetime rules and introduces `Send` / `Sync` requirements for futures and async closures, so existing sync code may fail those constraints. |
| 31 | + |
| 32 | +Now, let's go from all the necessary conversions, in order of complexity: |
| 33 | + |
| 34 | +### 1. `async` / `await` |
| 35 | + |
| 36 | +Removing all `async` and `.await` keywords will almost make it compile. |
| 37 | + |
| 38 | +### 2. `#[main]` / `#[test]` |
| 39 | + |
| 40 | +Simply remove `tokio` / `async-std` from Cargo and remove `#[tokio::main]`. Then replace `#[tokio::test]` with `#[test]`. |
| 41 | + |
| 42 | +### 3. `async_trait` |
| 43 | + |
| 44 | +Simply remove `#[async_trait]` usage and it's Cargo dependency. |
| 45 | + |
| 46 | +### 4. `Send + Sync` |
| 47 | + |
| 48 | +Most `Send` and `Sync` in trait bounds can be removed. |
| 49 | + |
| 50 | +### 5. `BoxFuture` |
| 51 | + |
| 52 | +If you've used `BoxFuture` you can use the following shim: |
| 53 | + |
| 54 | +```rust |
| 55 | +#[cfg(not(feature = "sync"))] |
| 56 | +type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>; |
| 57 | + |
| 58 | +#[cfg(feature = "sync")] |
| 59 | +type BoxFuture<'a, T> = T; |
| 60 | +``` |
| 61 | + |
| 62 | +### 6. `Box::pin(async move { .. })` |
| 63 | + |
| 64 | +Function signature: `FnOnce() -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'c>>` can simply be `FnOnce() -> Result<T, E>`. |
| 65 | + |
| 66 | +```rust title="async" |
| 67 | +async fn transaction<F, T, E>(&self, _callback: F) -> Result<T, TransactionError<E>> |
| 68 | +where |
| 69 | + F: for<'c> FnOnce( |
| 70 | + &'c DatabaseTransaction, |
| 71 | + ) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'c>> |
| 72 | + + Send, |
| 73 | + T: Send, |
| 74 | + E: std::fmt::Display + std::fmt::Debug + Send, |
| 75 | +{} |
| 76 | +``` |
| 77 | + |
| 78 | +```rust title="sync" |
| 79 | +fn transaction<F, T, E>(&self, _callback: F) -> Result<T, TransactionError<E>> |
| 80 | +where |
| 81 | + F: for<'c> FnOnce(&'c DatabaseTransaction) -> Result<T, E>, |
| 82 | + E: std::fmt::Display + std::fmt::Debug, |
| 83 | +{} |
| 84 | +``` |
| 85 | + |
| 86 | +Usage: Async futures can be simply converted to `{}`. |
| 87 | + |
| 88 | +```rust title="async" |
| 89 | +db |
| 90 | + .transaction::<_, _, DbErr>(|txn| { |
| 91 | + Box::pin(async move { |
| 92 | + let bakeries = Bakery::find() |
| 93 | + .filter(bakery::Column::Name.contains("Bakery")) |
| 94 | + .all(txn) |
| 95 | + .await?; |
| 96 | + |
| 97 | + Ok(()) |
| 98 | + }) |
| 99 | + }) |
| 100 | + .await? |
| 101 | +``` |
| 102 | + |
| 103 | +```rust title="sync" |
| 104 | +db |
| 105 | + .transaction::<_, _, DbErr>(|txn| { |
| 106 | + let bakeries = Bakery::find() |
| 107 | + .filter(bakery::Column::Name.contains("Bakery")) |
| 108 | + .all(txn)?; |
| 109 | + |
| 110 | + Ok(()) |
| 111 | + })? |
| 112 | +``` |
| 113 | + |
| 114 | +### 7. `Mutex` |
| 115 | + |
| 116 | +The semantic difference in `lock()` between a synchronous mutex (`std::sync::Mutex`) and an asynchronous mutex (`tokio::sync::Mutex` or `async_std::sync::Mutex`) is crucial. |
| 117 | + |
| 118 | +#### `std::sync::Mutex::lock()` |
| 119 | + |
| 120 | +```rust |
| 121 | +fn lock(&self) -> LockResult<MutexGuard<T>> |
| 122 | +``` |
| 123 | + |
| 124 | ++ Fallible: Returns a Result because the lock can be poisoned. |
| 125 | + |
| 126 | ++ Poisoning happens if a thread panics while holding the lock. |
| 127 | + |
| 128 | +#### `tokio::sync::Mutex::lock().await` |
| 129 | + |
| 130 | +```rust |
| 131 | +async fn lock(&self) -> MutexGuard<'_, T> |
| 132 | +``` |
| 133 | + |
| 134 | ++ Infallible: always succeeds and returns a guard |
| 135 | + |
| 136 | ++ In async world mutexes don't get poisoned. A panic inside a task would abort the task, but would not affect other tasks. This is actually a problem in async Rust as a task can fail silently |
| 137 | + |
| 138 | +In practice, we did: |
| 139 | + |
| 140 | +```rust |
| 141 | +#[cfg(not(feature = "sync"))] |
| 142 | +let conn = *self.conn.lock().await; |
| 143 | + |
| 144 | +#[cfg(feature = "sync")] |
| 145 | +let conn = *self.conn.lock().map_err(|_| DbErr::MutexPoisonError)?; |
| 146 | +``` |
| 147 | + |
| 148 | +### 8. `Stream` |
| 149 | + |
| 150 | +This is the biggest discrepency between sync and async Rust. Simply put, `Stream` is the async version of `Iterator`: |
| 151 | + |
| 152 | +| | `Iterator` | `Stream` | |
| 153 | +|---------------------|---------------------------------------------|----------------------------------------------------| |
| 154 | +| Definition | Synchronous iterator | Asynchronous iterator | |
| 155 | +| Trait method | `fn next(&mut self) -> Option<Item>` | `fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Item>>` | |
| 156 | +| Consumption | Call `.next()` repeatedly | Call `.next().await` repeatedly (via `StreamExt`) | |
| 157 | +| Blocking vs yielding| Produces items immediately, blocks until ready | Produces items asynchronously, yields if not ready | |
| 158 | +| Use cases | Iterating over collections | Reading database rows | |
| 159 | +| Usage | `for x in iter { ... }` | `while let Some(x) = stream.next().await { ... }` | |
| 160 | + |
| 161 | +`Stream` occurs in many places throughout the SeaORM API. The `Stream` trait is replaced by the `Iterator` trait. |
| 162 | + |
| 163 | +#### `Box<dyn Stream>` |
| 164 | + |
| 165 | +```rust |
| 166 | +#[cfg(not(feature = "sync"))] |
| 167 | +type PinBoxStream<'a> = Pin<Box<dyn Stream<Item = Result<QueryResult, DbErr>> + 'a + Send>>; |
| 168 | + |
| 169 | +#[cfg(feature = "sync")] |
| 170 | +type PinBoxStream<'a> = Box<dyn Iterator<Item = Result<QueryResult, DbErr>> + 'a>; |
| 171 | +``` |
| 172 | + |
| 173 | +#### `impl Stream` bounds |
| 174 | + |
| 175 | +```rust title="async" |
| 176 | +async fn stream<'a: 'b, 'b, C>(self, db: &'a C) |
| 177 | +-> Result<impl Stream<Item = Result<E::Model, DbErr>> + 'b + Send, DbErr> |
| 178 | +where |
| 179 | + C: ConnectionTrait + StreamTrait + Send, |
| 180 | +{} |
| 181 | +``` |
| 182 | + |
| 183 | +```rust title="sync" |
| 184 | +fn stream<'a: 'b, 'b, C>(self, db: &'a C) |
| 185 | +-> Result<impl Iterator<Item = Result<E::Model, DbErr>> + 'b, DbErr> |
| 186 | +where |
| 187 | + C: ConnectionTrait + StreamTrait, |
| 188 | +{} |
| 189 | +``` |
| 190 | + |
| 191 | +#### `impl Stream for` |
| 192 | + |
| 193 | +```rust |
| 194 | +#[cfg(not(feature = "sync"))] |
| 195 | +impl Stream for TransactionStream<'_> { |
| 196 | + type Item = Result<QueryResult, DbErr>; |
| 197 | + |
| 198 | + fn poll_next( |
| 199 | + self: Pin<&mut Self>, |
| 200 | + cx: &mut std::task::Context<'_>, |
| 201 | + ) -> Poll<Option<Self::Item>> { |
| 202 | + Pin::new(self.stream).poll_next(cx) |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +#[cfg(feature = "sync")] |
| 207 | +impl Iterator for TransactionStream<'_> { |
| 208 | + type Item = Result<QueryResult, DbErr>; |
| 209 | + |
| 210 | + fn next(&mut self) -> Option<Self::Item> { |
| 211 | + self.stream.next() |
| 212 | + } |
| 213 | +} |
| 214 | +``` |
| 215 | + |
| 216 | +#### `TryStreamExt` |
| 217 | + |
| 218 | +There is no equivalent to `TryStreamExt` in Rust's standard library, luckily it's very easy to make a shim: |
| 219 | + |
| 220 | +```rust |
| 221 | +while let Some(item) = stream.try_next().await? { |
| 222 | + let item: fruit::ActiveModel = item.into(); |
| 223 | +} |
| 224 | +``` |
| 225 | + |
| 226 | +```rust |
| 227 | +pub trait TryIterator<T, E> { |
| 228 | + fn try_next(&mut self) -> Result<Option<T>, E>; |
| 229 | +} |
| 230 | + |
| 231 | +impl<I, T, E> TryIterator<T, E> for I |
| 232 | +where |
| 233 | + I: Iterator<Item = Result<T, E>>, |
| 234 | +{ |
| 235 | + fn try_next(&mut self) -> Result<Option<T>, E> { |
| 236 | + self.next().transpose() // Option<Result<T>> becomes Result<Option<T>> |
| 237 | + } |
| 238 | +} |
| 239 | +``` |
| 240 | + |
| 241 | +### 9. File / Network I/O |
| 242 | + |
| 243 | +This is very application specific. In SeaORM's case, the external I/O is handled by `rusqlite` and `sqlx` respectively. Their APIs differ significantly, that's why we have written `sea-query-sqlx` and `sea-query-rusqlite` to align them. |
| 244 | + |
| 245 | +For HTTP requests, you can simply use the sync and async versions of `Client` in different contexts. |
| 246 | + |
| 247 | +For file I/O, the API difference between sync and async Rust is very small. |
| 248 | + |
| 249 | +## Conclusion: SQLite + SeaORM Sync = ⚡ |
| 250 | + |
| 251 | +You can now use `sea-orm-sync` in CLI programs, and only bringing in small number of additional dependencies compared to having to bring in the async ecosystem. |
| 252 | + |
| 253 | +In fact, the compilation time speaks for itself. The async version of [quickstart](https://github.com/SeaQL/sea-orm/blob/master/examples/quickstart/src/main.rs) took 30 seconds to compile, while the [sync version](https://github.com/SeaQL/sea-orm/blob/master/sea-orm-sync/examples/quickstart/src/main.rs) only took 15 seconds! |
| 254 | + |
| 255 | +Right now only `rusqlite` is supported, but SeaORM's entire API surface is available. It's a breeze to add SQLite query capabilities to CLI programs where async would be overkill. |
| 256 | + |
| 257 | +```rust |
| 258 | +let db = &sea_orm::Database::connect("sqlite::memory:")?; |
| 259 | + |
| 260 | +// Setup the database: create tables |
| 261 | +db.get_schema_registry("sea_orm_quickstart::*").sync(db)?; |
| 262 | + |
| 263 | +info!("Create user Bob with a profile:"); |
| 264 | +let bob = user::ActiveModel::builder() |
| 265 | + .set_name("Bob") |
| 266 | + |
| 267 | + .set_profile(profile::ActiveModel::builder().set_picture("Tennis")) |
| 268 | + .insert(db)?; |
| 269 | + |
| 270 | +info!("Query user with profile in a single query:"); |
| 271 | +let bob = user::Entity::load() |
| 272 | + .filter_by_id(bob.id) |
| 273 | + .with(profile::Entity) |
| 274 | + .one(db)? |
| 275 | + .expect("Not found"); |
| 276 | +assert_eq!(bob.name, "Bob"); |
| 277 | +assert_eq!(bob.profile.as_ref().unwrap().picture, "Tennis"); |
| 278 | +``` |
| 279 | + |
| 280 | +## SeaORM 2.0 RC |
| 281 | + |
| 282 | +SeaORM 2.0 is shaping up to be our most significant release yet - with a few breaking changes, plenty of enhancements, and a clear focus on developer experience. |
| 283 | + |
| 284 | +SeaORM 2.0 has reached its release candidate phase. We'd love for you to try it out and help shape the final release by [sharing your feedback](https://github.com/SeaQL/sea-orm/discussions/2548). |
| 285 | + |
| 286 | +## 🦀 Rustacean Sticker Pack |
| 287 | + |
| 288 | +The Rustacean Sticker Pack is the perfect way to express your passion for Rust. |
| 289 | +Our stickers are made with a premium water-resistant vinyl with a unique matte finish. |
| 290 | + |
| 291 | +Sticker Pack Contents: |
| 292 | +- Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography |
| 293 | +- Mascots: Ferris the Crab x 3, Terres the Hermit Crab |
| 294 | +- The Rustacean wordmark |
| 295 | + |
| 296 | +[Support SeaQL and get a Sticker Pack!](https://www.sea-ql.org/sticker-pack/) |
| 297 | + |
| 298 | +<a href="https://www.sea-ql.org/sticker-pack/"><img style={{borderRadius: "25px"}} alt="Rustacean Sticker Pack by SeaQL" src="https://www.sea-ql.org/static/sticker-pack-1s.jpg" /></a> |
0 commit comments