| Crates.io | sqlorm |
| lib.rs | sqlorm |
| version | 0.8.2 |
| created_at | 2025-09-13 08:42:14.125639+00 |
| updated_at | 2025-10-07 06:41:29.329448+00 |
| description | An ergonomic and type-safe ORM for database interactions |
| homepage | |
| repository | https://github.com/levlavryniuk/sqlorm |
| max_upload_size | |
| id | 1837449 |
| size | 757,506 |
An ergonomic, lightweight SQL ORM for Rust with type-safe query building and powerful entity relationships.
Sqlorm is a modern mini-ORM built on top of sqlx that provides compile-time safety, powerful macro-generated APIs, and an intuitive query builder. It's designed for developers who want the performance of sqlx with the convenience of an ORM.
belongs_to, has_many and has_many relations with eager/lazy loadingAdd to your Cargo.toml:
[dependencies]
sqlorm = { version = "0.8", features = ["postgres", "uuid" ] }
# important to use the same version of sqlx, to avoid compile time increase
sqlx={version = "=0.8.*"}
tokio = { version = "1.0", features = ["full"] }
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
serde = { version = "1.0", features = ["derive"] }
Choose one feature:
postgres - PostgreSQL supportsqlite - SQLite supportOptional features:
uuid - UUID supportextra-traits - Additional query methods for better DXuse sqlorm::prelude::*;
use chrono::{DateTime, Utc};
#[table(name = "users")]
#[derive(Debug, Clone, Default)]
pub struct User {
#[sql(pk)]
pub id: i64,
#[sql(unique)]
pub email: String,
pub username: String,
pub first_name: String,
pub last_name: String,
pub bio: Option<String>,
#[sql(timestamp(created_at, chrono::Utc::now()))]
pub created_at: DateTime<Utc>,
#[sql(timestamp(updated_at, chrono::Utc::now()))]
pub updated_at: DateTime<Utc>,
}
use sqlorm::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pool = Pool::connect("postgres://user:pass@localhost/db").await?;
// CREATE - Insert a new user
let user = User {
email: "alice@example.com".to_string(),
username: "alice".to_string(),
first_name: "Alice".to_string(),
last_name: "Smith".to_string(),
bio: Some("Rust developer".to_string()),
..Default::default()
}
.save(&pool) // Returns the inserted user with generated ID and timestamp fields
.await?;
println!("Created user with ID: {}", user.id);
// READ - Find by primary key
let found_user = User::query().filter(User::ID.eq(user.id)).fetch_one(&pool)
.await?
.expect("User should exist");
// READ - Find by unique field. Note, this requires `extra-traits` feature
let found_by_email = User::find_by_email(&pool,"alice@example.com".to_string());
.await?
.expect("User should exist");
// UPDATE - Modify and save
let mut updated_user = found_user;
updated_user.bio = Some("Senior Rust developer".to_string());
let updated_user = updated_user.update().columns((User::BIO)).execute(&pool).await?; // updated_at auto-updated
user.delete().execute(&pool).await.unwrap()
Ok(())
}
Sqlorm provides a powerful, type-safe query builder:
// Simple filtering
let active_users = User::query()
.filter(User::BIO.is_not_null())
.fetch_all(&pool)
.await?;
// Comparison operators
let recent_users = User::query()
.filter(User::CREATED_AT.gt(chrono::Utc::now() - chrono::Duration::days(30)))
.filter(User::ID.ge(100))
.fetch_all(&pool)
.await?;
// Pattern matching
let rust_developers = User::query()
.filter(User::BIO.like("%Rust%".to_string()))
.fetch_all(&pool)
.await?;
// Multiple conditions
let specific_users = User::query()
.filter(User::ID.in_(vec![1, 2, 3, 4, 5]))
.filter(User::EMAIL.ne("admin@example.com".to_string()))
.fetch_all(&pool)
.await?;
// Range queries
let mid_range_users = User::query()
.filter(User::ID.between(10, 50))
.fetch_one(&pool) // Get first result
.await?;
// NULL checks
let users_without_bio = User::query()
.filter(User::BIO.is_null())
.fetch_all(&pool)
.await?;
Optimize your queries by selecting only needed fields:
// Select specific fields as tuple
let (id, email) = User::query()
.filter(User::USERNAME.eq("alice".to_string()))
.select((User::ID, User::EMAIL))
.fetch_one_as(&pool)
.await?;
// Select multiple fields
let user_summaries: Vec<(String, String, Option<String>)> = User::query()
.select((
User::USERNAME,
User::EMAIL,
User::BIO,
))
.fetch_all_as(&pool)
.await?;
Define and work with entity relationships:
#[table(name = "posts")]
#[derive(Debug, Clone, Default)]
pub struct Post {
#[sql(pk)]
pub id: i64,
pub title: String,
pub content: String,
// Foreign key relationship
#[sql(relation(belongs_to -> User, relation = "author", on = id))]
pub user_id: i64,
#[sql(timestamp(created_at, chrono::Utc::now()))]
pub created_at: DateTime<Utc>,
}
#[table(name = "users")]
#[derive(Debug, Clone, Default)]
pub struct User {
#[sql(pk)]
// Define reverse relationship
#[sql(relation(has_many -> Post, relation = "posts", on = user_id))]
pub id: i64,
// ... other fields
}
// Lazy loading - fetch related data when needed
let user = User::find_by_id(&pool, 1).await?.expect("User exists");
let user_posts = user.posts(&pool).await?; // Separate query
let post = Post::find_by_id(&pool, 1).await?.expect("Post exists");
let author = post.author(&pool).await?.expect("Author exists");
let user_with_posts = User::query()
.filter(User::ID.eq(1))
.with_posts()
.fetch_one(&pool)
.await?;
let posts = user_with_posts.posts.expect("Posts loaded");
let post_with_author = Post::query()
.filter(Post::ID.eq(1))
.with_author()
.fetch_one(&pool)
.await?;
let author = post_with_author.author.expect("Author loaded");
SQLOrm automatically handles timestamp fields:
#[table]
#[derive(Debug, Clone, Default)]
pub struct Article {
#[sql(pk)]
pub id: i64,
pub title: String,
// Automatically set on insert
#[sql(timestamp(created_at, chrono::Utc::now()))]
pub created_at: DateTime<Utc>,
// Automatically updated on save
#[sql(timestamp(updated_at, chrono::Utc::now()))]
pub updated_at: DateTime<Utc>,
// Optional soft delete timestamp
#[sql(timestamp(deleted_at, chrono::Utc::now()))]
pub deleted_at: Option<DateTime<Utc>>,
}
// Custom timestamp functions
#[sql(timestamp(created_at, get_custom_timestamp()))]
pub created_at: i64, // Unix timestamp
fn get_custom_timestamp() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
SQLOrm supports various primary key types:
use uuid::Uuid;
// Auto-incrementing integer
#[table]
#[derive(Debug, Clone, Default)]
pub struct User {
#[sql(pk)]
pub id: i64, // BIGSERIAL in PostgreSQL
// ...
}
// UUID primary key
#[table]
#[derive(Debug, Clone, Default)]
pub struct Session {
#[sql(pk)]
pub id: Uuid, // Auto-generated UUID (PostgreSQL only. Sqlite does not like UUIDs)
// ...
}
// Custom primary key
#[table]
#[derive(Debug, Clone, Default)]
pub struct Setting {
#[sql(pk)]
pub key: String, // String primary key
pub value: String,
// ...
}
#[table]
#[derive(Debug, Clone, Default)]
pub struct Product {
#[sql(pk)]
pub id: i64,
pub name: String,
pub description: Option<String>, // Nullable text
pub price: f64, // Numeric
pub is_active: bool, // Boolean
pub tags: Option<Vec<String>>, // JSON array (PostgreSQL)
pub metadata: Option<serde_json::Value>, // JSON
#[sql(timestamp(created_at, chrono::Utc::now()))]
pub created_at: DateTime<Utc>,
}
The #[table] macro generates extensive APIs for each entity:
save() - Insert or update (smart detection)insert() - Force insertupdate() - Force updateextra-traits feature:find_by_id() - Find by primary keyfind_by_<unique_field>() - Find by unique fieldsquery() - Start query builderfilter() - Add WHERE conditionsselect() - Specify columns to fetchfetch_one() - Get single resultfetch_all() - Get all resultsfetch_one_as() - Get result as tuple/custom typefetch_all_as() - Get results as Vec of tuples/custom typeeq() / ne() - Equality / Not equalgt() / ge() - Greater than / Greater equallt() / le() - Less than / Less equallike() - Pattern matchingin_() / not_in() - List membershipbetween() / not_between() - Range queriesis_null() / is_not_null() - NULL checks<relation_name>() - Lazy load related entitieswith_<relation_name>() - Eager load in query builder#[table] // Use struct name as table name
#[table(name = "custom_name")] // Custom table name
#[sql(pk)] // Primary key
#[sql(unique)] // Unique constraint
#[sql(timestamp(created_at, chrono::Utc::now()))] // Auto timestamp
#[sql(relation(belongs_to -> Parent, relation = "parent", on = id))]
#[sql(relation(has_one -> Sister, relation = "sister", on = id))]
#[sql(relation(has_many -> Child, relation = "children", on = parent_id))]
# Test with all drivers
just test
# Test with specific driver
just test postgres # or sqlite
# Run examples
just examples
# Test with PostgreSQL
cargo test --features postgres
# Test with SQLite
cargo test --features sqlite
# Run examples
cargo run --example basic --features "postgres uuid chrono"
cargo run --example crud --features "postgres uuid chrono"
cargo run --example relations --features "postgres uuid chrono"
Check the examples/ directory for complete working examples:
basic: Simple CRUD operationscrud: Comprehensive CRUD with multiple entitiesrelations: Working with entity relationshipsContributions are welcome! Please feel free to submit a Pull Request. Make sure to:
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Built on the excellent sqlx crate. Inspired by Rails Active Record and Laravel Eloquent.