Active-record models, a fluent query builder, relations, migrations and factories. One API, two SQL dialects.
If you know Laravel Eloquent, you already know this library. Rustique brings the same model-centric workflow to Rust, with plain structs, compile-time column constants and no runtime magic.
use rustique::prelude::*;
#[rustique::model(table = "users", timestamps, soft_deletes)]
#[derive(Debug, Clone, Default)]
pub struct User {
pub id: i64,
pub name: String,
pub email: String,
pub created_at: Option<NaiveDateTime>,
pub updated_at: Option<NaiveDateTime>,
pub deleted_at: Option<NaiveDateTime>,
#[rustique(has_many(Post))]
pub posts: Rel<Vec<Post>>,
}
let db = Db::connect("mysql://user:pass@localhost/app").await?;
// Query, filter, eager load
let users = User::query()
.where_(User::EMAIL, "like", "%@example.com")
.order_by_desc(User::ID)
.with("posts.comments") // one query per level, no N+1
.get(&db).await?;
// Create, update (dirty columns only), soft delete
let mut user = User::create(&db, User { name: "Ann".into(), ..Default::default() }).await?;
user.name = "Anne".into();
user.save(&db).await?;
user.delete(&db).await?;
// Transactions roll back on error or drop
db.transaction(async |tx| {
user.save(tx).await?;
order.save(tx).await?;
Ok::<_, rustique::Error>(order.id)
}).await?;| Active-record models | #[rustique::model] derives the whole thing. Dirty tracking, timestamps, soft deletes, UUID / ULID keys, mass assignment. |
| Fluent query builder | Wheres (dates, JSON, subqueries), joins, unions, aggregates, upsert, bulk insert. Every column gets a constant, so typos fail to compile. |
| Relationships | has_one, has_many, belongs_to, belongs_to_many, through, polymorphic and morph_to_many. Eager loading, with_count, where_has, pivots. |
| Two databases, one API | MySQL 8.4+ (pure Rust driver, rustls) and Oracle 19c+. The grammar handles quoting, limits, identity columns and JSON for you. |
| Migrations and schema builder | Schema::create, Blueprint, a Migrator with fresh / reset / --step, seeders and a ready-made CLI. |
| Scopes, hooks, observers | Local and global scopes, lifecycle hooks (creating, saved, deleting, ...) and observers. |
| Large results | paginate, simple_paginate, cursor_paginate, chunk, chunk_by_id and streaming cursors. |
| Factories | User::factory().count(10).create(&db) for tests and seeders. |
| Casts | #[derive(rustique::Cast)] maps JSON and custom types to columns. |
| Safe by default | unsafe is forbidden. N+1 detection (prevent_lazy_loading), deadlock retries, savepoints, query logging through tracing. |
Published on crates.io as rustique-orm; the library is imported as rustique:
[dependencies]
rustique-orm = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
chrono = "0.4"MySQL only? Skip the Oracle client entirely:
rustique-orm = { version = "0.3", default-features = false, features = ["mysql", "mysql-tls"] }| Feature | Default | What it does |
|---|---|---|
mysql |
yes | MySQL driver (mysql_async, pure Rust) |
mysql-tls |
yes | TLS through rustls (ring provider; mysql-tls-aws-lc-rs for aws-lc-rs) |
oracle |
yes | Oracle driver. Needs Oracle Instant Client at runtime only |
schema-check |
no | Checks model structs against a schema snapshot at compile time |
Requires Rust 1.88 or newer.
Relations and constrained eager loading
let users = User::query()
.with_where("posts", |q| q.where_("published", "=", true))
.with("posts:id,title")
.get(&db).await?;
let drafts = user.posts().where_("published", "=", false).get(&db).await?;Migrations
Schema::create(&db, "users", |t| {
t.id();
t.string("email", 255).unique();
t.boolean("active").default(true);
t.foreign_id("country_id").constrained("countries").cascade_on_delete();
t.json("settings").nullable();
t.timestamps();
t.soft_deletes();
}).await?;Pagination without the offset cost
let page = User::query().cursor_paginate(&db, 50, None).await?;
// page.next_cursor for the next oneFactories
impl HasFactory for User {
fn definition(n: u64) -> Self {
User { name: format!("User {n}"), email: format!("user{n}@example.com"), ..Default::default() }
}
}Mean time per operation against diesel and Laravel Eloquent on the same data (100,000 users, 1M posts). Bars are scaled per scenario, so compare within a group.
All scenarios, percentiles and memory: MySQL · Oracle · how it is measured.
The pages follow the Laravel Eloquent docs layout. Every code block compiles as a doctest.
| Getting started | Install, features, connecting, pool and TLS |
| Query builder | Selects, wheres, joins, aggregates, writes |
| Models | Retrieving, saving, timestamps, soft deletes, dirty tracking |
| Relationships | All relation kinds, eager loading, pivots |
| Casts | Cast, FromValue, custom types |
| Scopes, hooks and observers | Reusable filters and lifecycle events |
| Pagination, chunking and cursors | Working with big tables |
| Transactions and raw SQL | Replicas, nested transactions, retries, logging |
| Factories | Test data and seeding |
| Schema and migrations | Schema builder, Migrator, CLI |
| Database differences | What changes between MySQL and Oracle |
| Eloquent compatibility | What maps to Laravel, what does not |
Run the database-free tests (they check generated SQL for both dialects):
cargo test --workspace --all-featuresIntegration tests need real databases. Start them and point the tests at them:
docker compose up -d
export RUSTIQUE_TEST_MYSQL_URL=mysql://orm:secret@127.0.0.1:13306/orm_test
export RUSTIQUE_TEST_ORACLE_URL=oracle://orm:secret@127.0.0.1:11521/XEPDB1
cargo test --workspace --all-featuresBefore sending a change:
cargo fmt --all
cargo clippy --workspace --all-targets --all-featuresLayout: rustique/ is the library, rustique-macros/ the proc macros,
rustique/docs/ the user guide, and bench/ the benchmark against diesel and Eloquent.
Pre-release. The API covers the full model, relation, migration and query surface described above and is tested against MySQL 8.4 and Oracle XE 21c.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.