Rullst ORM 🌟
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
ONclauses 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 Utilities —
map,filter,chunk,implode, and more on everyVec<Model>.
📚 Documentation & Planning
- Changelog: Detailed release history.
- ISSUES: Any issues? Please report.
- Spec: Single Source of Truth for macros, API, and architecture.
- Getting Started: Step-by-step first model and queries.
- Admin Panel: Serving the built-in dashboard.
- AI Agents & Automation: Example prompts and agent context for contributors.
🛠️ Installation
[]
= { = "4.0", = ["postgres"] }
# or features = ["mysql"]
# or features = ["sqlite"]
= { = "1", = ["full"] }
📖 Quick Start
use Orm;
// 1. Define your model
async
✨ 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>
🛡️ Raw Queries & SQL Injection Prevention
Never interpolate user input directly into .where_raw(). Always follow with .bind():
// ❌ DANGEROUS — SQL Injection risk
let query = query.where_raw;
// ✅ SECURE — parameterized binding
let query = query
.where_raw
.bind
.bind;
🚀 Advanced Subqueries & Joins
Constrained Joins
let posts = query
.join_constrained
.where_eq
.get
.await?;
Subqueries (where_exists)
let active_users = query
.where_exists
.get
.await?;
🛡️ Global Lifecycle Observers
;
// Register globally once:
observe;
Supported events: saving, saved, creating, created, updating, updated, deleting, deleted.
🏢 Enterprise Scaling
Read/Write Connection Splitting
init_with_replicas.await?;
// SELECTs go to replicas automatically (round-robin)
let users = query.get.await?;
// INSERT/UPDATE/DELETE go to primary automatically
let mut user = query.find.await?.unwrap;
user.name = "Updated".to_string;
user.save.await?;
Redis Caching Layer
init_redis.await?;
let active_users = query
.where_eq
.remember // cache for 1 hour
.get
.await?;
Query Chunking
query
.where_eq
.chunk
.await?;
🏢 Multi-Tenancy
use ;
with_tenant.await;
📋 Audit Logging
use ;
create_audit_table.await?;
log_audit_diff.await?;
🖥️ Admin Dashboard
use dashboard_html;
// Axum example:
let app = new
.route;
🐘 Artisan CLI (Migrations & Seeding)
// In your CLI entry point:
run_artisan.await;
Commands:
make:migration create_users_table— scaffold a.rsmigration filemigrate— execute pending migrationsmigrate:rollback— undo the previous batchdb:seed— run database seeders
🔎 Query Debug Logging
enable_query_log;
// All SQL, parameters, limits, and offsets print to STDOUT
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()