rusticx 1.0.0

Blazingly fast, async-first, multi-database ORM for Rust (PostgreSQL, MySQL, MongoDB)
Documentation

Rusticx ORM

Blazingly fast, async-first, multi-database ORM for Rust.

Rusticx lets you define models with a derive macro, auto-generate tables, and perform full CRUD — synchronously or asynchronously — across PostgreSQL, MySQL/MariaDB, and MongoDB from a single unified API.


Features

  • Multi-database — PostgreSQL, MySQL, MariaDB, MongoDB (more planned: CockroachDB, Cassandra, SQLite)
  • Async-first — built on tokio + sqlx + mongodb async drivers
  • Sync supportSyncAdapter wrapper for blocking contexts
  • Derive macro#[derive(Model)] generates all boilerplate from your struct
  • Type-safe query builder — composable, no raw strings required for common queries
  • Connection pooling — built-in via sqlx pool for SQL, native pool for MongoDB
  • Schema migrationrepo.migrate() creates the table if it doesn't exist
  • Feature-gated backends — only compile what you use

Installation

[dependencies]
# Pick your backends:
rusticx = { version = "0.1", features = ["postgres"] }          # PostgreSQL only
rusticx = { version = "0.1", features = ["mysql"] }             # MySQL / MariaDB only
rusticx = { version = "0.1", features = ["mongo"] }             # MongoDB only
rusticx = { version = "0.1", features = ["full"] }              # All backends

tokio   = { version = "1", features = ["full"] }
serde   = { version = "1", features = ["derive"] }
uuid    = { version = "1", features = ["v4", "serde"] }

Quick Start

1. Define a Model

use rusticx::prelude::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use chrono::{DateTime, Utc};

#[derive(Debug, Clone, Serialize, Deserialize, Model)]
#[rusticx(table = "users")]          // optional: defaults to "users" (snake_case plural)
pub struct User {
    #[rusticx(primary_key)]
    pub id: Uuid,

    pub name: String,

    #[rusticx(unique)]
    pub email: String,

    pub age: i32,

    pub active: bool,

    pub created_at: DateTime<Utc>,
}

Supported #[rusticx(...)] attributes

Attribute Scope Description
table = "name" struct Override table / collection name
primary_key = "field" struct Override PK field name (default: id)
primary_key field Mark this field as primary key
nullable field Allow NULL (auto-detected for Option<T>)
unique field Add UNIQUE constraint
default = "expr" field SQL DEFAULT expression
column = "name" field Override column name
skip field Do not persist this field

2. Async Usage (recommended)

use rusticx::prelude::*;
use std::sync::Arc;
use uuid::Uuid;
use chrono::Utc;

#[tokio::main]
async fn main() -> rusticx::Result<()> {
    // ── Connect ───────────────────────────────────────────────────────
    let adapter = PostgresAdapter::connect_url("postgres://user:pass@localhost/mydb").await?;
    let repo: Repository<User, _> = Repository::new(Arc::new(adapter));

    // ── Migrate (creates table if not exists) ─────────────────────────
    repo.migrate().await?;

    // ── Insert ────────────────────────────────────────────────────────
    let user = User {
        id: Uuid::new_v4(),
        name: "Alice".into(),
        email: "alice@example.com".into(),
        age: 30,
        active: true,
        created_at: Utc::now(),
    };
    let inserted = repo.insert(&user).await?;
    println!("Inserted: {:?}", inserted);

    // ── Insert many ───────────────────────────────────────────────────
    let users = vec![
        User { id: Uuid::new_v4(), name: "Bob".into(), email: "bob@example.com".into(), age: 25, active: true, created_at: Utc::now() },
        User { id: Uuid::new_v4(), name: "Carol".into(), email: "carol@example.com".into(), age: 22, active: false, created_at: Utc::now() },
    ];
    let count = repo.insert_many(&users).await?;
    println!("Inserted {count} users");

    // ── Find by primary key ───────────────────────────────────────────
    let found = repo.find_by_id(inserted.id).await?;
    println!("Found: {:?}", found);

    // ── Find all ──────────────────────────────────────────────────────
    let all = repo.find_all().await?;
    println!("Total users: {}", all.len());

    // ── Query builder ─────────────────────────────────────────────────
    let adults = repo.find(
        repo.query()
            .r#where("age", CondOp::Gte, 18)
            .r#where("active", CondOp::Eq, true)
            .order_by("name", Direction::Asc)
            .limit(10)
    ).await?;

    // ── Find one ──────────────────────────────────────────────────────
    let alice = repo.find_one(
        repo.query().r#where("email", CondOp::Eq, "alice@example.com")
    ).await?;

    // ── Paginate ──────────────────────────────────────────────────────
    let page_1 = repo.paginate(1, 20).await?; // page 1, 20 per page

    // ── Count ─────────────────────────────────────────────────────────
    let total = repo.count(None).await?;
    let active_count = repo.count(Some(
        repo.query().r#where("active", CondOp::Eq, true)
    )).await?;

    // ── Update ────────────────────────────────────────────────────────
    let updated = repo.update(
        repo.query()
            .r#where("email", CondOp::Eq, "alice@example.com")
            .set("age", 31)
            .set("active", false)
    ).await?;
    println!("Updated {updated} rows");

    // ── Save (insert-or-update) ───────────────────────────────────────
    let mut alice_model = alice.unwrap();
    alice_model.age = 32;
    repo.save(&alice_model).await?;

    // ── Delete by ID ──────────────────────────────────────────────────
    repo.delete_by_id(inserted.id).await?;

    // ── Delete by query ───────────────────────────────────────────────
    repo.delete(
        repo.query().r#where("active", CondOp::Eq, false)
    ).await?;

    // ── Raw SQL ───────────────────────────────────────────────────────
    repo.adapter().execute_raw(
        "UPDATE users SET active = $1 WHERE age < $2",
        vec![Value::Bool(false), Value::Int(18)]
    ).await?;

    let rows = repo.adapter().query_raw(
        "SELECT id, name FROM users WHERE age > $1",
        vec![Value::Int(21)]
    ).await?;

    Ok(())
}

3. Sync Usage

Use SyncAdapter to wrap any async adapter for blocking contexts — no tokio::main required.

use rusticx::prelude::*;
use std::sync::Arc;
use uuid::Uuid;
use chrono::Utc;

fn main() -> rusticx::Result<()> {
    // Build the async adapter first (SyncAdapter creates its own runtime)
    let rt = tokio::runtime::Runtime::new().unwrap();
    let async_adapter = rt.block_on(
        PostgresAdapter::connect_url("postgres://user:pass@localhost/mydb")
    )?;

    // Wrap in SyncAdapter
    let sync_adapter = SyncAdapter::new(async_adapter)?;

    // ── Schema ────────────────────────────────────────────────────────
    let schema = TableSchema::from_model::<User>();
    sync_adapter.create_table(&schema)?;

    // ── Insert ────────────────────────────────────────────────────────
    let mut row = rusticx::Row::new();
    row.insert("id".into(), Value::Uuid(Uuid::new_v4()));
    row.insert("name".into(), Value::Text("Dave".into()));
    row.insert("email".into(), Value::Text("dave@example.com".into()));
    row.insert("age".into(), Value::Int(28));
    row.insert("active".into(), Value::Bool(true));
    row.insert("created_at".into(), Value::DateTime(Utc::now()));

    let inserted = sync_adapter.insert("users", row)?;
    println!("Inserted: {:?}", inserted);

    // ── Find ──────────────────────────────────────────────────────────
    let query = QueryBuilder::table("users")
        .r#where("age", CondOp::Gte, 18)
        .order_by("name", Direction::Asc)
        .limit(5);

    let rows = sync_adapter.find(&query)?;
    println!("Found {} rows", rows.len());

    // ── Count ─────────────────────────────────────────────────────────
    let count = sync_adapter.count(&QueryBuilder::table("users"))?;
    println!("Total: {count}");

    // ── Update ────────────────────────────────────────────────────────
    let update_query = QueryBuilder::table("users")
        .r#where("email", CondOp::Eq, "dave@example.com")
        .set("age", 29);
    sync_adapter.update(&update_query)?;

    // ── Delete ────────────────────────────────────────────────────────
    let delete_query = QueryBuilder::table("users")
        .r#where("active", CondOp::Eq, false);
    sync_adapter.delete(&delete_query)?;

    // ── Raw SQL ───────────────────────────────────────────────────────
    sync_adapter.execute_raw(
        "TRUNCATE TABLE users",
        vec![]
    )?;

    Ok(())
}

4. MongoDB

Same API, different adapter. migrate() creates a collection + indexes.

use rusticx::prelude::*;
use std::sync::Arc;

#[derive(Debug, Clone, Serialize, Deserialize, Model)]
#[rusticx(table = "products")]
pub struct Product {
    #[rusticx(primary_key, column = "_id")]
    pub id: String,
    pub name: String,
    pub price: f64,
    pub in_stock: bool,
}

#[tokio::main]
async fn main() -> rusticx::Result<()> {
    let adapter = MongoAdapter::connect_url(
        "mongodb://localhost:27017",
        "mydb",
    ).await?;
    let repo: Repository<Product, _> = Repository::new(Arc::new(adapter));

    // Creates the collection if not exists
    repo.migrate().await?;

    // All the same CRUD methods work identically
    let cheap = repo.find(
        repo.query()
            .r#where("price", CondOp::Lt, 100.0)
            .r#where("in_stock", CondOp::Eq, true)
            .order_by("price", Direction::Asc)
    ).await?;

    Ok(())
}

5. MySQL / MariaDB

let adapter = MysqlAdapter::connect_url("mysql://user:pass@localhost/mydb").await?;
let repo: Repository<User, _> = Repository::new(Arc::new(adapter));
repo.migrate().await?;
// ... identical API

Query Builder Reference

repo.query()
    // Conditions (AND by default)
    .r#where("column", CondOp::Eq, value)
    .r#where("column", CondOp::Ne, value)
    .r#where("column", CondOp::Gt, value)
    .r#where("column", CondOp::Gte, value)
    .r#where("column", CondOp::Lt, value)
    .r#where("column", CondOp::Lte, value)
    .r#where("column", CondOp::Like, "%pattern%")
    .r#where("column", CondOp::ILike, "%pattern%")   // case-insensitive (Postgres)
    .r#where("column", CondOp::IsNull, Value::Null)
    .r#where("column", CondOp::IsNotNull, Value::Null)
    // OR condition
    .or_where("column", CondOp::Eq, value)
    // Ordering
    .order_by("column", Direction::Asc)
    .order_by("column", Direction::Desc)
    // Pagination
    .limit(20)
    .offset(40)
    // Mutation (for update)
    .set("column", new_value)

Connection Pool Configuration

// Postgres
let adapter = PostgresAdapter::connect(
    PostgresConfig::new("postgres://localhost/mydb")
        .max_connections(20)
        .min_connections(2)
).await?;

// MySQL
let adapter = MysqlAdapter::connect(
    MysqlConfig::new("mysql://localhost/mydb")
        .max_connections(15)
).await?;

// MongoDB
let adapter = MongoAdapter::connect(
    MongoConfig::new("mongodb://localhost:27017", "mydb")
        .max_pool_size(10)
).await?;

Architecture

rusticx/                    ← public facade crate (feature-gated re-exports)
├── rusticx-core/           ← Model trait, Repository, QueryBuilder, Value, DatabaseAdapter
├── rusticx-macros/         ← #[derive(Model)] procedural macro
├── rusticx-sql/            ← SQL dialect compiler (DDL + DML for Postgres / MySQL)
├── rusticx-postgres/       ← PostgreSQL adapter (sqlx)
├── rusticx-mysql/          ← MySQL / MariaDB adapter (sqlx)
└── rusticx-mongo/          ← MongoDB adapter (mongodb)

Supported Databases

Database Status Feature flag
PostgreSQL Stable postgres
MySQL Stable mysql
MariaDB Stable (MySQL driver) mysql
MongoDB Stable mongo
CockroachDB Planned (PostgreSQL wire)
Cassandra Planned
SQLite Planned

License

MIT — © Tarun Vishwakarma