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.

RustDatabaseORMSQLPostgreSQLSQLiteType Conversion

GitHub Overview

ivanceras/rustorm

an orm for rust

Stars250
Watchers11
Forks23
Created:December 13, 2017
Language:Rust
License:MIT License

Topics

databaseormpostgresqlrustsqlite

Star History

ivanceras/rustorm Star History
Data as of: 7/19/2025, 09:29 AM

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, &params)?;
        
        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, &params)?;
        
        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, &params)?;
        
        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, &params)?;
        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, &params)?;
        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, &params)?;
        
        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, &params)?;
        
        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, &params)?;
        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");
    }
}