Rustorm
Rustorm is a Rust-exclusive Object-Relational Mapping (ORM) library that is "a SQL-centered ORM library with a focus on ease of use on conversion of database types to their appropriate Rust type." Rather than abstracting SQL away, it emphasizes SQL operations, providing a lightweight and practical approach that supports PostgreSQL and SQLite databases. Through derive macros (ToDao, FromDao, ToColumnNames, ToTableName) for struct definitions, it achieves type-safe database operations and enables the construction of performance-focused data access layers that leverage Rust's Memory Safety and Zero-cost Abstraction benefits.
GitHub Overview
Topics
Star History
Library
Rustorm
Overview
Rustorm is a Rust-exclusive Object-Relational Mapping (ORM) library that is "a SQL-centered ORM library with a focus on ease of use on conversion of database types to their appropriate Rust type." Rather than abstracting SQL away, it emphasizes SQL operations, providing a lightweight and practical approach that supports PostgreSQL and SQLite databases. Through derive macros (ToDao, FromDao, ToColumnNames, ToTableName) for struct definitions, it achieves type-safe database operations and enables the construction of performance-focused data access layers that leverage Rust's Memory Safety and Zero-cost Abstraction benefits.
Details
Rustorm 2025 edition leverages the latest features of Rust 1.75+ (async/await, Generic Associated Types, const generics, etc.) to provide more ergonomic and type-safe database access. The SQL-centered approach allows direct writing of complex queries, and its design compatible with Rust's ownership system maximizes memory efficiency and runtime performance. Pool-based connection management, transaction control, and automatic type conversion enable the construction of robust data layers suitable for use in high-load web applications and embedded systems. Additionally, flexible serialization through serde integration, migration functionality, and test support utilities strongly support production environment operations.
Key Features
- SQL-Centered Design: Query control through direct SQL writing with minimal abstraction
- Type Conversion Focus: Automatic conversion from database types to Rust types
- Derive Macros: Boilerplate code reduction through ToDao, FromDao, and other macros
- Multi-DB Support: Supports both PostgreSQL and SQLite
- Pool Management: Efficient connection pool functionality
- Lightweight Design: High performance with minimal dependencies
Pros and Cons
Pros
- Leverage SQL knowledge while benefiting from Rust's type safety
- Lightweight with low learning cost, easier introduction than other Rust ORMs
- High transparency allowing direct SQL writing for complex queries
- High memory efficiency with good compatibility with Rust's ownership system
- Flexibility supporting both PostgreSQL and SQLite
- Improved development efficiency through boilerplate code reduction with derive macros
Cons
- Rust-exclusive, not usable for cross-platform development
- Limited functionality compared to Diesel or SeaORM
- Advanced ORM features (relationship management, etc.) require manual implementation
- Small community size with limited documentation and samples
- No support for NoSQL databases
- Limited abstraction benefits for complex schema designs
Reference Pages
Code Examples
Setup
# Cargo.toml
[dependencies]
rustorm = "0.20"
rustorm_dao = "0.20"
# When using PostgreSQL
postgres = "0.19"
tokio-postgres = "0.7"
# When using SQLite
rusqlite = "0.29"
# Async runtime
tokio = { version = "1.0", features = ["full"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1.0", features = ["v4", "serde"] }
[dev-dependencies]
tokio-test = "0.4"
// src/lib.rs
use rustorm::{Pool, DbError, Dao, FromDao, ToDao, ToColumnNames, ToTableName};
use rustorm::table::Table;
use rustorm::column::Column;
use rustorm::value::Value;
use std::collections::HashMap;
// Database connection configuration
pub fn establish_connection() -> Result<Pool, DbError> {
// PostgreSQL connection string
let postgres_url = "postgres://username:password@localhost/mydatabase";
// SQLite connection string
let sqlite_url = "sqlite://./mydatabase.db";
// Create connection pool
Pool::new(&postgres_url)
}
pub fn establish_sqlite_connection() -> Result<Pool, DbError> {
let sqlite_url = "sqlite://./mydatabase.db";
Pool::new(&sqlite_url)
}
Entity Definition
use rustorm::{FromDao, ToDao, ToColumnNames, ToTableName};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use uuid::Uuid;
// Basic entity
#[derive(Debug, Clone, Serialize, Deserialize, FromDao, ToDao, ToColumnNames, ToTableName)]
#[table_name = "users"]
pub struct User {
pub id: Option<i32>,
pub username: String,
pub email: String,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
pub is_active: bool,
}
impl User {
pub fn new(username: String, email: String) -> Self {
User {
id: None,
username,
email,
created_at: Some(Utc::now()),
updated_at: None,
is_active: true,
}
}
}
// Related entity
#[derive(Debug, Clone, Serialize, Deserialize, FromDao, ToDao, ToColumnNames, ToTableName)]
#[table_name = "posts"]
pub struct Post {
pub id: Option<i32>,
pub user_id: i32,
pub title: String,
pub content: String,
pub published: bool,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
}
impl Post {
pub fn new(user_id: i32, title: String, content: String) -> Self {
Post {
id: None,
user_id,
title,
content,
published: false,
created_at: Some(Utc::now()),
updated_at: None,
}
}
}
// Entity using UUID
#[derive(Debug, Clone, Serialize, Deserialize, FromDao, ToDao, ToColumnNames, ToTableName)]
#[table_name = "sessions"]
pub struct Session {
pub id: Uuid,
pub user_id: i32,
pub token: String,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
impl Session {
pub fn new(user_id: i32, token: String, expires_at: DateTime<Utc>) -> Self {
Session {
id: Uuid::new_v4(),
user_id,
token,
expires_at,
created_at: Utc::now(),
}
}
}
// Struct for joined data
#[derive(Debug, Clone, Serialize, Deserialize, FromDao)]
pub struct UserWithPostCount {
pub id: i32,
pub username: String,
pub email: String,
pub post_count: i64,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, FromDao)]
pub struct PostWithUser {
pub post_id: i32,
pub title: String,
pub content: String,
pub published: bool,
pub post_created_at: DateTime<Utc>,
pub user_id: i32,
pub username: String,
pub user_email: String,
}
Basic CRUD Operations
use rustorm::{Pool, DbError, Dao};
use crate::{User, Post, Session};
pub struct UserRepository {
pool: Pool,
}
impl UserRepository {
pub fn new(pool: Pool) -> Self {
UserRepository { pool }
}
// Create user
pub fn create_user(&self, mut user: User) -> Result<User, DbError> {
let mut db = self.pool.connect()?;
let sql = r#"
INSERT INTO users (username, email, created_at, is_active)
VALUES ($1, $2, $3, $4)
RETURNING id, username, email, created_at, updated_at, is_active
"#;
let params = vec![
user.username.clone().into(),
user.email.clone().into(),
user.created_at.into(),
user.is_active.into(),
];
let result = db.execute_sql_with_return(&sql, ¶ms)?;
if let Some(row) = result.iter().next() {
let created_user: User = User::from_dao(&row)?;
Ok(created_user)
} else {
Err(DbError::NoRecord)
}
}
// Get user by ID
pub fn get_user_by_id(&self, user_id: i32) -> Result<Option<User>, DbError> {
let mut db = self.pool.connect()?;
let sql = "SELECT id, username, email, created_at, updated_at, is_active FROM users WHERE id = $1";
let params = vec![user_id.into()];
let result = db.execute_sql_with_return(&sql, ¶ms)?;
if let Some(row) = result.iter().next() {
let user: User = User::from_dao(&row)?;
Ok(Some(user))
} else {
Ok(None)
}
}
// Get user by username
pub fn get_user_by_username(&self, username: &str) -> Result<Option<User>, DbError> {
let mut db = self.pool.connect()?;
let sql = "SELECT id, username, email, created_at, updated_at, is_active FROM users WHERE username = $1";
let params = vec![username.into()];
let result = db.execute_sql_with_return(&sql, ¶ms)?;
if let Some(row) = result.iter().next() {
let user: User = User::from_dao(&row)?;
Ok(Some(user))
} else {
Ok(None)
}
}
// Get all users
pub fn get_all_users(&self) -> Result<Vec<User>, DbError> {
let mut db = self.pool.connect()?;
let sql = "SELECT id, username, email, created_at, updated_at, is_active FROM users ORDER BY created_at DESC";
let result = db.execute_sql_with_return(&sql, &[])?;
let mut users = Vec::new();
for row in result.iter() {
let user: User = User::from_dao(&row)?;
users.push(user);
}
Ok(users)
}
// Update user
pub fn update_user(&self, user: &User) -> Result<(), DbError> {
let mut db = self.pool.connect()?;
let sql = r#"
UPDATE users
SET username = $1, email = $2, updated_at = $3, is_active = $4
WHERE id = $5
"#;
let params = vec![
user.username.clone().into(),
user.email.clone().into(),
Some(Utc::now()).into(),
user.is_active.into(),
user.id.into(),
];
db.execute_sql_with_return(&sql, ¶ms)?;
Ok(())
}
// Delete user
pub fn delete_user(&self, user_id: i32) -> Result<(), DbError> {
let mut db = self.pool.connect()?;
let sql = "DELETE FROM users WHERE id = $1";
let params = vec![user_id.into()];
db.execute_sql_with_return(&sql, ¶ms)?;
Ok(())
}
// Search active users
pub fn get_active_users(&self) -> Result<Vec<User>, DbError> {
let mut db = self.pool.connect()?;
let sql = r#"
SELECT id, username, email, created_at, updated_at, is_active
FROM users
WHERE is_active = true
ORDER BY username ASC
"#;
let result = db.execute_sql_with_return(&sql, &[])?;
let mut users = Vec::new();
for row in result.iter() {
let user: User = User::from_dao(&row)?;
users.push(user);
}
Ok(users)
}
}
Complex Queries and Joins
use crate::{UserWithPostCount, PostWithUser};
impl UserRepository {
// Get users with post count
pub fn get_users_with_post_count(&self) -> Result<Vec<UserWithPostCount>, DbError> {
let mut db = self.pool.connect()?;
let sql = r#"
SELECT
u.id,
u.username,
u.email,
u.created_at,
COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.is_active = true
GROUP BY u.id, u.username, u.email, u.created_at
ORDER BY post_count DESC, u.username ASC
"#;
let result = db.execute_sql_with_return(&sql, &[])?;
let mut users_with_counts = Vec::new();
for row in result.iter() {
let user_with_count: UserWithPostCount = UserWithPostCount::from_dao(&row)?;
users_with_counts.push(user_with_count);
}
Ok(users_with_counts)
}
// Get posts with user information
pub fn get_posts_with_users(&self, limit: i32) -> Result<Vec<PostWithUser>, DbError> {
let mut db = self.pool.connect()?;
let sql = r#"
SELECT
p.id as post_id,
p.title,
p.content,
p.published,
p.created_at as post_created_at,
u.id as user_id,
u.username,
u.email as user_email
FROM posts p
INNER JOIN users u ON p.user_id = u.id
WHERE p.published = true AND u.is_active = true
ORDER BY p.created_at DESC
LIMIT $1
"#;
let params = vec![limit.into()];
let result = db.execute_sql_with_return(&sql, ¶ms)?;
let mut posts_with_users = Vec::new();
for row in result.iter() {
let post_with_user: PostWithUser = PostWithUser::from_dao(&row)?;
posts_with_users.push(post_with_user);
}
Ok(posts_with_users)
}
// Search functionality
pub fn search_users(&self, search_term: &str) -> Result<Vec<User>, DbError> {
let mut db = self.pool.connect()?;
let sql = r#"
SELECT id, username, email, created_at, updated_at, is_active
FROM users
WHERE (username ILIKE $1 OR email ILIKE $1)
AND is_active = true
ORDER BY username ASC
"#;
let search_pattern = format!("%{}%", search_term);
let params = vec![search_pattern.into()];
let result = db.execute_sql_with_return(&sql, ¶ms)?;
let mut users = Vec::new();
for row in result.iter() {
let user: User = User::from_dao(&row)?;
users.push(user);
}
Ok(users)
}
}
Transaction Processing
use rustorm::Transaction;
impl UserRepository {
// Create user and post simultaneously using transaction
pub fn create_user_with_post(
&self,
mut user: User,
mut post: Post,
) -> Result<(User, Post), DbError> {
let mut db = self.pool.connect()?;
let mut transaction = db.start_transaction()?;
// Create user
let user_sql = r#"
INSERT INTO users (username, email, created_at, is_active)
VALUES ($1, $2, $3, $4)
RETURNING id, username, email, created_at, updated_at, is_active
"#;
let user_params = vec![
user.username.clone().into(),
user.email.clone().into(),
user.created_at.into(),
user.is_active.into(),
];
let user_result = transaction.execute_sql_with_return(&user_sql, &user_params)?;
let created_user: User = if let Some(row) = user_result.iter().next() {
User::from_dao(&row)?
} else {
transaction.rollback()?;
return Err(DbError::NoRecord);
};
// Create post
post.user_id = created_user.id.unwrap();
let post_sql = r#"
INSERT INTO posts (user_id, title, content, published, created_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, user_id, title, content, published, created_at, updated_at
"#;
let post_params = vec![
post.user_id.into(),
post.title.clone().into(),
post.content.clone().into(),
post.published.into(),
post.created_at.into(),
];
let post_result = transaction.execute_sql_with_return(&post_sql, &post_params)?;
let created_post: Post = if let Some(row) = post_result.iter().next() {
Post::from_dao(&row)?
} else {
transaction.rollback()?;
return Err(DbError::NoRecord);
};
// Commit transaction
transaction.commit()?;
Ok((created_user, created_post))
}
// Bulk operations
pub fn bulk_update_user_status(&self, user_ids: &[i32], is_active: bool) -> Result<usize, DbError> {
let mut db = self.pool.connect()?;
let mut transaction = db.start_transaction()?;
let sql = "UPDATE users SET is_active = $1, updated_at = $2 WHERE id = ANY($3)";
let user_ids_array = user_ids.iter().map(|&id| id.into()).collect::<Vec<_>>();
let params = vec![
is_active.into(),
Some(Utc::now()).into(),
user_ids_array.into(),
];
let result = transaction.execute_sql_with_return(&sql, ¶ms)?;
let affected_rows = result.len();
transaction.commit()?;
Ok(affected_rows)
}
}
Test Implementation
#[cfg(test)]
mod tests {
use super::*;
use tokio_test;
fn setup_test_db() -> Pool {
// In-memory SQLite database for testing
Pool::new("sqlite://:memory:").expect("Failed to create test database")
}
fn create_test_tables(pool: &Pool) -> Result<(), DbError> {
let mut db = pool.connect()?;
// Create tables
let users_table = r#"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME,
is_active BOOLEAN DEFAULT 1
)
"#;
let posts_table = r#"
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
published BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME,
FOREIGN KEY (user_id) REFERENCES users (id)
)
"#;
db.execute_sql_with_return(&users_table, &[])?;
db.execute_sql_with_return(&posts_table, &[])?;
Ok(())
}
#[tokio::test]
async fn test_create_and_get_user() {
let pool = setup_test_db();
create_test_tables(&pool).unwrap();
let repo = UserRepository::new(pool);
// Create user
let user = User::new("testuser".to_string(), "[email protected]".to_string());
let created_user = repo.create_user(user).unwrap();
assert!(created_user.id.is_some());
assert_eq!(created_user.username, "testuser");
assert_eq!(created_user.email, "[email protected]");
assert!(created_user.is_active);
// Get user
let retrieved_user = repo.get_user_by_id(created_user.id.unwrap()).unwrap();
assert!(retrieved_user.is_some());
let retrieved_user = retrieved_user.unwrap();
assert_eq!(retrieved_user.username, "testuser");
assert_eq!(retrieved_user.email, "[email protected]");
}
#[tokio::test]
async fn test_update_user() {
let pool = setup_test_db();
create_test_tables(&pool).unwrap();
let repo = UserRepository::new(pool);
// Create user
let user = User::new("original".to_string(), "[email protected]".to_string());
let mut created_user = repo.create_user(user).unwrap();
// Update user
created_user.username = "updated".to_string();
created_user.email = "[email protected]".to_string();
repo.update_user(&created_user).unwrap();
// Verify update
let updated_user = repo.get_user_by_id(created_user.id.unwrap()).unwrap().unwrap();
assert_eq!(updated_user.username, "updated");
assert_eq!(updated_user.email, "[email protected]");
assert!(updated_user.updated_at.is_some());
}
#[tokio::test]
async fn test_user_search() {
let pool = setup_test_db();
create_test_tables(&pool).unwrap();
let repo = UserRepository::new(pool);
// Create test data
let users = vec![
User::new("alice".to_string(), "[email protected]".to_string()),
User::new("bob".to_string(), "[email protected]".to_string()),
User::new("charlie".to_string(), "[email protected]".to_string()),
];
for user in users {
repo.create_user(user).unwrap();
}
// Search test
let search_results = repo.search_users("alice").unwrap();
assert_eq!(search_results.len(), 1);
assert_eq!(search_results[0].username, "alice");
let email_search_results = repo.search_users("example.com").unwrap();
assert_eq!(email_search_results.len(), 2);
}
#[tokio::test]
async fn test_transaction() {
let pool = setup_test_db();
create_test_tables(&pool).unwrap();
let repo = UserRepository::new(pool);
let user = User::new("testuser".to_string(), "[email protected]".to_string());
let post = Post::new(0, "Test Post".to_string(), "This is a test post.".to_string());
let (created_user, created_post) = repo.create_user_with_post(user, post).unwrap();
assert!(created_user.id.is_some());
assert!(created_post.id.is_some());
assert_eq!(created_post.user_id, created_user.id.unwrap());
assert_eq!(created_post.title, "Test Post");
}
}