Skip to content

Commit 90539aa

Browse files
committed
[Blog] SeaORM Sync
1 parent 3565661 commit 90539aa

File tree

1 file changed

+298
-0
lines changed

1 file changed

+298
-0
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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+
.set_email("[email protected]")
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

Comments
 (0)