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 support**`SyncAdapter` 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 migration**`repo.migrate()` creates the table if it doesn't exist
- **Feature-gated backends** — only compile what you use

---

## Installation

```toml
[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

```rust
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)

```rust
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.

```rust
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.

```rust
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

```rust
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

```rust
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

```rust
// 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