All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.
Every Rust project I start, someone suggests Diesel or SeaORM. I've tried both. I keep coming back to raw rusqlite. Here's why.
The ORM appeal
ORMs promise type safety, migrations, and less SQL. On paper, compelling. In practice, for a solo dev building desktop apps, the tradeoffs don't work out.
What ORMs cost in Rust
Compile times. Diesel adds significant compile time through its macro-heavy approach. On an 8-year-old MacBook Air, this matters. Every saved minute of compile time is real productivity.
Learning curve. The ORM API is another thing to learn. rusqlite maps directly to SQL — if you know SQL, you know rusqlite.
Abstraction leaks. Complex queries eventually break ORM abstractions. You end up mixing ORM and raw SQL anyway. Better to just use raw SQL.
What raw rusqlite looks like
fn get_sync_records(
conn: &Connection,
device_id: &str,
limit: usize,
) -> Result<Vec<SyncRecord>, AppError> {
let mut stmt = conn.prepare(
"SELECT id, file_path, last_synced, file_hash
FROM sync_records
WHERE device_id = ?1
ORDER BY last_synced DESC
LIMIT ?2"
)?;
let records = stmt.query_map(params![device_id, limit as i64], |row| {
Ok(SyncRecord {
id: row.get(0)?,
file_path: row.get(1)?,
last_synced: row.get(2)?,
file_hash: row.get(3)?,
})
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(records)
}
Explicit, readable, no magic. The SQL is right there.
Migrations without a framework
For 3-4 schema versions across an app's lifetime, a simple version table works:
fn run_migrations(conn: &Connection) -> Result<(), AppError> {
let version: i64 = conn.query_row(
"SELECT version FROM schema_version",
[],
|r| r.get(0)
).unwrap_or(0);
if version < 1 {
conn.execute_batch("
CREATE TABLE IF NOT EXISTS sync_records (...);
UPDATE schema_version SET version = 1;
")?;
}
if version < 2 {
conn.execute_batch("
ALTER TABLE sync_records ADD COLUMN direction TEXT;
UPDATE schema_version SET version = 2;
")?;
}
Ok(())
}
Not glamorous. Works perfectly for the scale of a solo desktop app.
When I'd use an ORM
Complex relational data with many joins, a team that prefers ORM conventions, or a project where compile time isn't a concern.
For a solo dev building Tauri desktop apps with simple data models: raw rusqlite is the right call.
TL;DR: For solo Tauri desktop apps, skip Diesel/SeaORM. Raw rusqlite means faster compile times, no learning curve beyond SQL itself, and no abstraction leaks on complex queries. A simple version table handles migrations at this scale.
If this was useful, a ❤️ helps more than you'd think — thanks!
HiyokoAutoSync | X → @hiyoyok
Top comments (0)