"Write Rust, Run Optimized SQL."
Premix is a Zero-Overhead, Type-Safe ORM for Rust that eliminates the need for manual migration files. It combines the ease of use of Active Record with the raw performance of handcrafted SQL.
Status (Alpha / Research Prototype)
Premix is a research prototype. APIs may change and production use is not recommended yet.
This codebase is an AI-assisted research prototype and should be treated as experimental.
See DISCLAIMER.md.
Note: Versions 1.0.0-1.0.4 were published before we added the-alphasuffix, but they are still considered alpha.
- Fast like raw
sqlx: generated SQL with a minimal runtime layer. - Low ceremony:
save,find,includewith a simple model. - Transparent SQL: inspect
to_sql()before running anything.
- Rust 1.85+ (edition 2024).
- No nightly toolchain required.
- Zero-Overhead: treat ORM as a thin layer on top of raw SQL.
- Mental Model Match: code should look like how you think about data.
- Impossible-to-Fail: push more errors to compile time.
- Glass Box: show the SQL you are about to run.
- Escape Hatch: allow raw SQL when needed.
Why this is practical:
- Zero-Overhead: benchmarks show Premix close to raw
sqlxin common CRUD flows (see below). - Mental Model Match:
user.save()/User::find_in_pool()avoids boilerplate glue. - Impossible-to-Fail: model fields are validated at compile time; schema mismatches surface early.
- Glass Box:
to_sql()/to_update_sql()let you inspect SQL before running it. - Escape Hatch:
Model::raw_sql()gives full control for edge cases.
See docs/plan/DEVELOPMENT.md for the engineering flowplan and docs/plan/PHILOSOPHY_CHECKLIST.md for status details.
- Auto-Sync Schema: Premix syncs your Rust structs directly to the database for rapid prototyping. No manual SQL required.
- Zero Overhead: Uses Rust Macros to generate SQL at compile-time. No runtime reflection.
- Application-Level Joins: Solves the N+1 problem using smart
WHERE INclauses instead of complex SQL JOINs, making scaling easier. - Multi-Database: Write once, run on SQLite, PostgreSQL, or MySQL (feature-gated).
- Web & Metrics: Built-in support for Axum, Actix-web, and Prometheus (optional features).
We don't just say we're fast; we prove it.
TL;DR: Premix is near raw sqlx for inserts/selects and dramatically faster
than loop-based bulk updates in this benchmark suite.
Highlights (Criterion medians from the latest run; see docs/bench/BENCHMARK_RESULTS.md):
- Insert (1 row): Premix 57.345 us vs raw SQLx 55.022 us
- Select (1 row): Premix 53.015 us vs raw SQLx 53.32 us
- Bulk Update (1,000 rows): Premix 91.608 us vs loop 67.645 ms (~739x faster)
- Postgres SELECT: Premix 78.659 us vs raw SQL 71.325 us
Full results: docs/bench/BENCHMARK_RESULTS.md
| Operation | Premix | SeaORM | Rbatis | SQLx (Raw) |
|---|---|---|---|---|
| Insert | 57.345 us | 64.654 us | 29.894 us | 55.022 us |
| Select | 53.015 us | 59.163 us | 31.64 us | 53.32 us |
| Bulk Update (1k) | 91.608 us | - | - | 67.645 ms* |
> Compared to standard loop-based updates.
- Phase 1-3: Foundation (CRUD, Relations, Migrations)
- Phase 4-7: Production Baseline (CLI, Docs, DevOps, Multi-DB)
- Phase 4-6 Additions (Test utilities, scaffolding, integrations, guards, masking)
- Phase 8: Scalability (Read/Write splitting, metrics)
- Phase 9: Advanced Relations (Polymorphic) (Deferred)
- Phase 10: Legacy Support (Composite Keys)
Premix now supports traditional versioned migrations for production environments.
premix migrate create add_users
# Created: migrations/20260118000000_add_users.sql-- migration/2026xxx_add_users.sql
-- up
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
-- down
DROP TABLE users;premix migrate up
# Applying migration: 20260118000000_add_users
# Migrations up to date.use premix_orm::prelude::*;
// No need to import premix_core or premix_macros separately!
#[derive(Model)]
struct User {
id: i32,
name: String,
age: i32,
#[has_many(Post)]
#[premix(ignore)]
posts: Option<Vec<Post>>,
}
#[derive(Model)]
struct Post {
id: i32,
user_id: i32,
title: String,
}// Connect to SQLite (or Postgres!)
let pool = Premix::smart_sqlite_pool("sqlite::memory:").await?;
// This line creates tables automatically.
Premix::sync::<premix_orm::sqlx::Sqlite, User>(&pool).await?;
Premix::sync::<premix_orm::sqlx::Sqlite, Post>(&pool).await?;let users = User::find_in_pool(&pool)
.include(User::posts) // Typed relation name to avoid typos
.filter_gt("age", 18) // Safe parameterized filter
.limit(20)
.all()
.await?;
// Sugar for common cases:
let all_users = User::all(&pool).await?;
let by_id = User::find_by_id(&pool, 1).await?;
let updated = User::update_by_id(&pool, 1, serde_json::json!({ "name": "New" })).await?;cargo new premix-demo
cd premix-demo[dependencies]
premix-orm = { version = "1.0.9-alpha", features = ["postgres", "axum"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }Use the code from Quick Start and run:
cargo runIf you want a copy-pasteable src/main.rs, use:
use premix_orm::prelude::*;
#[derive(Model)]
struct User {
id: i32,
#[premix(rename = "full_name")]
name: String,
#[has_many(Post)]
#[premix(ignore)]
posts: Option<Vec<Post>>,
#[premix(sensitive)]
password_hash: String,
}
#[derive(Model)]
struct Post {
id: i32,
user_id: i32,
title: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pool = Premix::smart_sqlite_pool("sqlite::memory:").await?;
Premix::sync::<premix_orm::sqlx::Sqlite, User>(&pool).await?;
Premix::sync::<premix_orm::sqlx::Sqlite, Post>(&pool).await?;
let users = User::find_in_pool(&pool)
.include(User::posts)
.filter_gt("id", 0)
.all()
.await?;
println!("Loaded {} users", users.len());
Ok(())
}Prefer a template? Start from examples/basic-app and modify as needed.
For a longer-form guide, see orm-book/ in this repository. It covers models, queries, relations, migrations, transactions, and limitations. For a map of the project layout, see docs/guide/PROJECT_STRUCTURE.md. For transparency details, see orm-book/src/glass-box.md.
Examples live under examples/. Start with:
cd examples/basic-app
cargo runRelease notes live in CHANGELOG.md, and the development roadmap is in docs/plan/DEVELOPMENT.md.
table_name(),create_table_sql(),list_columns()- CRUD helpers (
save,find_by_id,update,delete) - Query builder entry points (
find_in_pool,find_in_tx) - Relation helpers (
has_many,belongs_to) and eager loading (include)
See orm-book/models.md and orm-book/queries.md for the generated API surface and SQL inspection helpers. For macro expansion and SQL flow, see orm-book/src/glass-box.md.
Use PremixState as your Axum state wrapper:
use axum::{routing::get, Router};
use premix_orm::{prelude::*, PremixState};
async fn health() -> &'static str {
"ok"
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pool = Premix::smart_sqlite_pool("sqlite::memory:").await?;
Premix::sync::<premix_orm::sqlx::Sqlite, User>(&pool).await?;
let app = Router::new()
.route("/health", get(health))
.with_state(PremixState::new(pool));
Ok(())
}Never accidentally lose data again.
use premix_orm::prelude::*;
#[derive(Model)] // <--- Auto-detected by field name!
struct User {
id: i32,
deleted_at: Option<String>,
}
// Logical delete (sets deleted_at)
user.delete(&pool).await?;
// Fetch only active users (default)
let active = User::find_in_pool(&pool).all().await?;
// Fetch everyone, including deleted
let all = User::find_in_pool(&pool).with_deleted().all().await?;Update thousands of rows in microseconds.
use premix_orm::prelude::*;
use serde_json::json;
// Set all inactive users to 'archived' status
User::find_in_pool(&pool)
.filter_lt("last_login", "2023-01-01")
.update(json!({ "status": "archived" }))
.await?;
// Time: ~50us (Lightning fast!)Inspect the SQL generated by the query builder.
let query = User::find_in_pool(&pool).filter_gt("age", 18).limit(10);
println!("{}", query.to_sql());Run raw SQL and map results to your model.
let users = User::raw_sql("SELECT * FROM users WHERE active = 1")
.fetch_all(&pool)
.await?;let mut tx = pool.begin().await?;
user.balance += 100;
user.save(&mut *tx).await?; // Pass transaction reference
tx.commit().await?;Add this to your Cargo.toml:
[dependencies]
premix-orm = { version = "1.0.9-alpha", features = ["postgres", "axum"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "postgres"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }| Feature | Description |
|---|---|
sqlite |
Enable SQLite support (default) |
postgres |
Enable PostgreSQL support |
mysql |
Enable MySQL support |
axum |
Enable Axum integration (PremixState) |
actix |
Enable Actix-web integration (PremixData) |
metrics |
Enable Prometheus metrics (install_prometheus_recorder) |
- Feature flags: enable database and integration features on
premix-orm. DATABASE_URL: CLI uses it by default; pass--databaseif needed.migrate down: requires a valid-- downsection in your migration file.- Bulk update examples: add
serde_jsonif you usejson!helpers. - Save vs update:
save()updates whenidis set and inserts whenid == 0. - Error mapping:
ModelResultExtmapssqlx::ErrorintoPremixError.
- Raw filters (
filter_raw) require.allow_unsafe()and logRAW(<redacted>). - Bulk update/delete require filters unless you explicitly allow unsafe paths.
unsafe_fast()skips logging/metrics and safety guards by design.
- Use
include()for N+1 avoidance and prefer batched bulk updates when possible. - Inspect SQL with
to_sql()/to_update_sql()and keep filters parameterized. - Tune pool sizes via
Premix::smart_*_pool_with_profilein production. - Use
ColumnRef::static_str("col")for hot filters to avoid allocating column names. - For extreme hot paths, consider
Model::raw_sql(...)orsqlx::query_as!and map manually. - Prepared statements are enabled by default; use
.unprepared()to disable per query.
For hot paths, use the fast path APIs to reduce runtime overhead:
use serde_json::json;
let users = User::find_in_pool(&pool)
.filter_eq("status", "active")
.fast() // skip query logging + metrics
.all()
.await?;
User::find_in_pool(&pool)
.filter_eq("status", "inactive")
.unsafe_fast() // skip logging + metrics + safety guards
.update(json!({ "status": "active" }))
.await?;
// Ultra-fast path (skips logging/metrics/guards AND eager loading)
let users = User::find_in_pool(&pool)
.filter_eq("status", "active")
.ultra_fast()
.all()
.await?;
// Ultra-fast writes (skips hooks/extra checks; may skip RETURNING on Postgres)
let mut user = User { id: 1, name: "Alice".to_string() };
user.save_ultra(&pool).await?;Use premix_query! for critical paths to avoid runtime SQL generation:
// Find by id (SELECT + LIMIT 1)
let user = premix_query!(User, FIND, filter_eq("id", user_id))
.fetch_optional(&pool)
.await?;
// Update by id
premix_query!(
User,
UPDATE,
set("status", "active"),
filter_eq("id", user_id)
)
.execute(&pool)
.await?;
// Update by id and return the updated row
let user = premix_query!(
User,
UPDATE,
set("status", "active"),
filter_eq("id", user_id),
returning_all()
)
.fetch_one(&pool)
.await?;
// Delete by id
premix_query!(
User,
DELETE,
filter_eq("id", user_id)
)
.execute(&pool)
.await?;premix_query! bypasses runtime SQL building and is the recommended path for
latency-sensitive code.
If you need even lower mapping overhead, use raw_sql_fast and select columns
in model field order:
use premix_orm::prelude::*;
let rows = User::raw_sql_fast("SELECT id, name FROM users ORDER BY id")
.fetch_all(&pool)
.await?;
let users: Vec<User> = rows.into_iter().map(|row| row.into_inner()).collect();- Use versioned SQL migrations for production environments.
- In CI/Docker, run
premix migrate upbefore application start. - See orm-book/migrations.md and orm-book/production-checklist.md.
- Premix vs Diesel: Premix favors runtime simplicity and transparent SQL; Diesel offers a richer compile-time query DSL with more compile-time overhead.
- Premix vs SeaORM: Premix trades some dynamic query flexibility for a thinner runtime layer and simpler mental model.
- Premix vs raw SQLx: Premix adds schema/relations conveniences while keeping SQL visible.
- Uses the Tokio runtime (async/await).
sqlxfeatures must match your target database.
See orm-book/limitations.md for current gaps and known constraints.
See orm-book/src/performance-tuning.md for advanced tuning tips and hot-path options.
Try examples/basic-app for a minimal runnable setup.
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
This project is licensed under the MIT License - see the LICENSE file for details.