rullst-orm 4.0.4

An Active Record ORM for Rust.
Documentation

Rullst ORM 🌟

Crates.io Downloads Docs.rs License: MIT Databases CI

An Active Record ORM for Rust.

Built on top of sqlx and procedural macros, rullst-orm brings a clean, fluent Active Record API to the Rust ecosystem. It supports PostgreSQL, MySQL, and SQLite through compile-time feature flags.

🚀 Why Rullst ORM?

In traditional Rust database handling, you write raw SQL, manage connection pools manually across every function, and bind variables repetitively. Rullst ORM solves this by abstracting the heavy lifting behind a single #[derive(Orm)] macro.

Rullst ORM v4.0 includes:

  • Read/Write Connection Splitting — automatic routing to read replicas.
  • Integrated Redis Caching — speed up repeating queries with .remember(ttl).
  • Query Chunking — memory-safe large dataset processing.
  • Constrained Eager Loading — fetch deep relationships without N+1 queries.
  • Global Lifecycle Observers — intercept operations before/after they happen.
  • Subqueries & Advanced Joins — multi-constraint ON clauses with binding safety.
  • Artisan Migrations CLI — auto-generate, run, and roll back database schemas.
  • Dynamic Query Logging — toggle STDOUT SQL logging at runtime.
  • Multi-Tenancy — async-safe tenant isolation via task-local context.
  • Audit Logging — automatic diff-based change trails.
  • Admin Dashboard — built-in dark-mode web panel, zero dependencies.
  • API Resources — transform models to JSON with a clean trait.
  • Collection Utilitiesmap, filter, chunk, implode, and more on every Vec<Model>.

📚 Documentation & Planning


🛠️ Installation

[dependencies]
rullst-orm = { version = "4.0", features = ["postgres"] }
# or features = ["mysql"]
# or features = ["sqlite"]
tokio = { version = "1", features = ["full"] }

📖 Quick Start

use rullst_orm::Orm;

// 1. Define your model
#[derive(Debug, Clone, Orm)]
#[orm(table = "users")]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,
    #[orm(hidden)] // excluded from .to_json()
    pub password: String,
}

#[tokio::main]
async fn main() -> Result<(), rullst_orm::Error> {
    // 2. Initialize the global connection pool
    Orm::init("sqlite::memory:").await?;

    // 3. Create a new user
    let mut user = User {
        id: 0,
        name: "Vene Louis".to_string(),
        email: "vene@cosmos.com".to_string(),
        password: "secret".to_string(),
    };
    user.save().await?; // Runs INSERT and updates the ID automatically

    // 4. Update the user
    user.name = "John Doe".to_string();
    user.save().await?; // Detects id > 0, runs UPDATE automatically

    // 5. Fetch from database
    let found = User::query().find(1).await?;
    println!("Found: {:?}", found);

    // 6. Delete
    if let Some(u) = found {
        u.delete().await?;
    }

    Ok(())
}

✨ Query Builder API

The #[derive(Orm)] macro injects a full Query Builder into your model.

🔍 Active Record Methods

Method Description
Model::query() Start a new Query Builder instance
Model::query().find(id) Find a single record by Primary Key
Model::query().first() First matching record (LIMIT 1)
Model::query().get() All matching records as Vec<Model>
model.save() INSERT if id == 0, else UPDATE
model.delete() Delete the record (or soft-delete if deleted_at is present)

⛓️ Query Filters (Chainable)

All values are automatically bound to prevent SQL Injection.

AND Filters:

  • .where_eq(column, value) / .where_not_eq(column, value)
  • .where_gt(column, value) / .where_lt(column, value) / .where_gte / .where_lte
  • .where_like(column, value)
  • .where_null(column) / .where_not_null(column)
  • .where_in(column, vec_of_values) / .where_not_in(column, vec_of_values)
  • .where_between(column, min, max) / .where_not_between(column, min, max)

OR Filters:

  • .or_where(column, value) / .or_where_not_eq(column, value)
  • .or_where_like(column, value) / .or_where_not_null(column)
  • .or_where_in(column, vec_of_values) / .or_where_gt(column, value)

Raw SQL:

  • .where_raw(sql) — raw SQL fragment
  • .bind(value) — bind a typed value to the previous ? placeholder

🔢 Sorting, Limits & Aggregation

  • .order_by(column) / .order_by_desc(column)
  • .limit(n) / .offset(n) — aliases: .take(n) / .skip(n)
  • .latest(column) / .oldest(column)
  • .select_raw("col1, col2") / .group_by(column)
  • .count().await?i64
  • .delete_all().await? — delete all matching rows

⚡ Terminal Executors

  • .get().await?Vec<Model>
  • .first().await?Option<Model>
  • .find(id).await?Option<Model>
  • .paginate(page, per_page).await?PaginationResult<Model>
pub struct PaginationResult<T> {
    pub data: Vec<T>,
    pub total: i64,
    pub per_page: usize,
    pub current_page: usize,
    pub last_page: usize,
}

🛡️ Raw Queries & SQL Injection Prevention

Never interpolate user input directly into .where_raw(). Always follow with .bind():

// ❌ DANGEROUS — SQL Injection risk
let query = User::query().where_raw(&format!("email = '{}'", input_email));

// ✅ SECURE — parameterized binding
let query = User::query()
    .where_raw("email = ? AND status = ?")
    .bind(input_email)
    .bind("active");

🚀 Advanced Subqueries & Joins

Constrained Joins

let posts = Post::query()
    .join_constrained("users", |join| {
        join.on("posts.user_id", "=", "users.id")
            .on_eq("users.active", true)
    })
    .where_eq("posts.status", "published")
    .get()
    .await?;

Subqueries (where_exists)

let active_users = User::query()
    .where_exists(
        Post::query()
            .where_column("posts.user_id", "users.id")
            .where_eq("posts.status", "published")
    )
    .get()
    .await?;

🛡️ Global Lifecycle Observers

pub struct UserObserverImpl;

#[rullst_orm::async_trait]
impl UserObserver for UserObserverImpl {
    async fn saving(&self, model: &mut User) -> Result<(), rullst_orm::Error> {
        println!("About to save user: {}", model.name);
        Ok(())
    }
}

// Register globally once:
User::observe(Arc::new(UserObserverImpl));

Supported events: saving, saved, creating, created, updating, updated, deleting, deleted.


🏢 Enterprise Scaling

Read/Write Connection Splitting

Orm::init_with_replicas(
    "postgres://primary_db_url",
    vec![
        "postgres://replica_1_url".to_string(),
        "postgres://replica_2_url".to_string(),
    ],
).await?;

// SELECTs go to replicas automatically (round-robin)
let users = User::query().get().await?;

// INSERT/UPDATE/DELETE go to primary automatically
let mut user = User::query().find(1).await?.unwrap();
user.name = "Updated".to_string();
user.save().await?;

Redis Caching Layer

Orm::init_redis("redis://127.0.0.1/").await?;

let active_users = User::query()
    .where_eq("status", "active")
    .remember(3600) // cache for 1 hour
    .get()
    .await?;

Query Chunking

User::query()
    .where_eq("status", "active")
    .chunk(1000, |batch| Box::pin(async move {
        for user in &batch {
            println!("Processing: {}", user.name);
        }
        Ok(())
    }))
    .await?;

🏢 Multi-Tenancy

use rullst_orm::tenant::{with_tenant, get_tenant_id};

with_tenant("acme_corp", async {
    let tenant = get_tenant_id(); // Some(RullstValue::String("acme_corp"))
    // All ORM queries run inside this scope
}).await;

📋 Audit Logging

use rullst_orm::audit::{create_audit_table, log_audit_diff};

create_audit_table().await?;
log_audit_diff("User", user.id, "updated", &old_json, &new_json).await?;

🖥️ Admin Dashboard

use rullst_orm::admin::dashboard_html;

// Axum example:
let app = Router::new()
    .route("/admin", get(|| async { Html(dashboard_html()) }));

🐘 Artisan CLI (Migrations & Seeding)

// In your CLI entry point:
rullst_orm::schema::run_artisan(std::env::args().collect(), vec![
    // Seeders here
]).await;

Commands:

  • make:migration create_users_table — scaffold a .rs migration file
  • migrate — execute pending migrations
  • migrate:rollback — undo the previous batch
  • db:seed — run database seeders

🔎 Query Debug Logging

Orm::enable_query_log();
// All SQL, parameters, limits, and offsets print to STDOUT
Orm::disable_query_log();

⚙️ Compile-Time Field Methods

The macro inspects your struct at compile time and generates typed methods per field. For a name: String field, you automatically get:

  • .where_name(value)
  • .or_where_name(value)
  • .where_not_name(value)
  • .order_by_name() / .order_by_name_desc()