Skip to content

About

Rust ORM inspired by Python's SQLModel: derive-macro models, type-safe query builder, multi-dialect SQL generation, and cancel-correct async via asupersync

Topics

Resources

Stars

8 stars

Watchers

1 watching

Forks

Latest commit

 

History

765 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SQLModel Rust

SQLModel Rust - SQL databases in Rust, designed to be intuitive and type-safe

SQL databases in Rust, designed to be intuitive and type-safe.

License: MIT Rust: Nightly

A Rust port of tiangolo/sqlmodel (Python), extended with asupersync for structured concurrency and cancel-correct async database operations.


TL;DR

The Problem: Existing Rust ORMs are either too low-level (raw SQL strings), too magical (runtime reflection), or force you to learn complex DSLs. You shouldn't need a PhD in database theory to insert a row.

The Solution: SQLModel Rust provides Python SQLModel's developer experience with Rust's compile-time safety. Define your models with derive macros, query with type-safe builders, and let the compiler catch your mistakes.

Why SQLModel Rust?

Feature What It Does
Zero-cost derive macros #[derive(Model)] generates efficient code at compile time—no runtime reflection
Type-safe query builder Compile-time validation of SQL expressions, columns, and joins
Cancel-correct async Built on asupersync for structured concurrency
Multi-dialect support Single codebase generates Postgres, SQLite, or MySQL SQL
Lean dependencies No tokio/sqlx/diesel/sea-orm; core stays small, drivers/validation use focused crypto/regex deps

Quick Example

use sqlmodel::prelude::*;

#[derive(Model, Debug)]
#[sqlmodel(table = "heroes")]
struct Hero {
    #[sqlmodel(primary_key, auto_increment)]
    id: Option<i64>,

    #[sqlmodel(unique)]
    name: String,

    secret_name: String,

    #[sqlmodel(nullable)]
    age: Option<i32>,

    #[sqlmodel(foreign_key = "teams.id")]
    team_id: Option<i64>,
}

// Type-safe query building; the SQL can be rendered for any dialect without a connection
let query = select!(Hero)
    .filter(Expr::col("age").gt(18))
    .order_by(Expr::col("name").asc())
    .limit(10);
let (sql, params) = query.build_with_dialect(Dialect::Postgres);
assert_eq!(
    sql,
    "SELECT * FROM \"heroes\" WHERE \"age\" > $1 ORDER BY \"name\" ASC LIMIT 10"
);
assert_eq!(params, vec![Value::Int(18)]);

// Execute against a connection: every operation takes a `Cx` and returns an
// `Outcome` (Ok / Err / Cancelled / Panicked)
async fn adult_heroes(cx: &Cx, conn: &impl Connection) -> Outcome<Vec<Hero>, Error> {
    select!(Hero)
        .filter(Expr::col("age").gt(18))
        .order_by(Expr::col("name").asc())
        .limit(10)
        .all(cx, conn)
        .await
}

With the nightly-try feature (this workspace pins a nightly toolchain anyway), ? works on Outcome and propagates errors and cancellation alike; the examples below use it. Without the feature, match on the four variants.


Design Philosophy

1. First-Principles, Not Translation

We extracted the behavior specification from Python SQLModel/SQLAlchemy/Pydantic, then implemented fresh in Rust. No line-by-line translation. Rust has compile-time types and macros—we use them instead of runtime validation.

2. Zero-Cost Abstractions

All Model implementations are generated at compile time via proc macros. No runtime reflection, no vtables. Measured against hand-written equivalents (13-column model, criterion, RCH worker hz3, 2026-09-06): to_row runs ≈197 ns vs 150 ns hand-written; SQL emission costs ≈0.15–2.8 µs per statement by shape. The one measured gap: generated from_row resolves columns in a single-pass scan (Row::locate_columns, bd-n5q4) and runs ≈491 ns vs 206 ns hand-written (down from 1.43 µs). Benchmarks live in crates/sqlmodel-e2e/benches/, with CI thresholds in benches/thresholds.toml.

3. Structured Concurrency

Every async operation takes &Cx (capability context) and returns Outcome<T, E> instead of Result. This enables:

  • Cancel-correct operations (no leaked resources)
  • Budget/timeout enforcement
  • Proper panic boundaries

4. Type Safety Over Convenience

The query builder validates at compile time when possible, and provides clear error messages when runtime checks are needed. We'd rather fail at compile time than corrupt your database.

5. Minimal Dependencies

Core crates keep dependencies tight:

  • asupersync - Async runtime with structured concurrency
  • serde / serde_json - Serialization
  • proc-macro2 / quote / syn - Macro support

Drivers and validation add focused dependencies where required (e.g., TLS/auth crypto, regex validation), but we still avoid heavyweight ORM/database stacks.

No tokio, no sqlx, no diesel, no sea-orm. We build what we need.


How SQLModel Rust Compares

Feature SQLModel Rust Diesel SeaORM sqlx
Compile-time safety ✅ Full ✅ Full ⚠️ Partial ⚠️ Partial
Derive macros ✅ Simple ⚠️ Complex ✅ Simple ❌ None
Structured concurrency ✅ Native ❌ None ❌ None ❌ None
Multi-dialect ✅ Postgres/SQLite/MySQL ⚠️ Separate features ✅ Yes ✅ Yes
Dependencies ✅ Minimal 🐢 Heavy 🐢 Heavy ⚠️ Moderate
Learning curve ✅ Low ❌ Steep ⚠️ Moderate ✅ Low

When to use SQLModel Rust:

  • You want Python SQLModel's ergonomics in Rust
  • You need cancel-correct async with structured concurrency
  • You prefer compile-time errors over runtime surprises
  • You're building from scratch and want minimal dependencies

When SQLModel Rust might not fit:

  • You need an established ecosystem with extensive documentation
  • You require immediate production readiness (we're in active development)
  • You need implicit relationship traversal/lazy loading without explicit load calls (we provide Lazy<T> + Session::load_lazy/load_many, but we avoid hidden N+1 behavior)

Installation

From crates.io (recommended)

Version 0.5.1 is available on crates.io for all 12 SQLModel crates.

# Cargo.toml
[dependencies]
sqlmodel = "0.5.1"
# `#[derive(Model)]` expands to `sqlmodel_core::` paths, so a crate that derives
# models also needs the core crate directly, at the same version (issue #37):
sqlmodel-core = "0.5.1"

# Choose a driver (pick one or more)
sqlmodel-postgres = "0.5.1"
# sqlmodel-mysql = "0.5.1"
# sqlmodel-sqlite = "0.5.1"
# sqlmodel-frankensqlite = "0.5.1"

# Optional rich console output
sqlmodel-console = { version = "0.5.1", features = ["rich"] }

# TLS-only MySQL deployments can drop the `rsa` dependency (the no-TLS
# full-auth path is compiled out; the server must then allow TLS auth):
# sqlmodel-mysql = { version = "0.5.1", default-features = false, features = ["tls"] }

All SQLModel crates are released in lockstep at the same version (0.5.1); mix and match drivers within that release family.

You do not need to add asupersync directly; the Cx and Outcome types are re-exported from sqlmodel and sqlmodel-core.

From Source

git clone https://github.com/Dicklesworthstone/sqlmodel_rust.git
cd sqlmodel_rust

# Build the workspace
cargo build --workspace

# Run tests
cargo test --workspace

Quick Start

1. Define a Model, Create Its Table, Query It

One complete program (this block is compiled as a doctest, so it cannot drift from the API):

use sqlmodel::prelude::*;
use sqlmodel::SchemaBuilder;

#[derive(Model, Debug, Clone)]
#[sqlmodel(table = "users")]
struct User {
    #[sqlmodel(primary_key, auto_increment)]
    id: Option<i64>,

    #[sqlmodel(unique)]
    email: String,

    name: String,

    #[sqlmodel(default = "false")]
    is_active: bool,
}

/// DDL for the connection's own dialect. The builder defaults to SQLite; pass
/// `conn.dialect()` for anything else (identifier quoting, column types, and the
/// auto-increment key form differ per engine).
fn schema_for(conn: &impl Connection) -> Vec<String> {
    SchemaBuilder::new()
        .dialect(conn.dialect())
        .create_table::<User>()
        .build()
}

async fn crud(cx: &Cx, conn: &impl Connection) -> Outcome<(), Error> {
    for statement in schema_for(conn) {
        conn.execute(cx, &statement, &[]).await?;
    }

    // INSERT (returns the new id; PostgreSQL gets an automatic RETURNING clause)
    let alice = User {
        id: None,
        email: "alice@example.com".into(),
        name: "Alice".into(),
        is_active: true,
    };
    let id = insert!(&alice).execute(cx, conn).await?;

    // SELECT
    let active: Vec<User> = select!(User)
        .filter(Expr::col("is_active").eq(true))
        .order_by(Expr::col("name").asc())
        .all(cx, conn)
        .await?;

    // UPDATE by primary key, from a model instance
    let mut alice = active.into_iter().next().expect("inserted above");
    alice.name = "Alice Smith".into();
    let updated = update!(&alice).execute(cx, conn).await?;

    // DELETE by filter
    let deleted = delete!(User)
        .filter(Expr::col("is_active").eq(false))
        .execute(cx, conn)
        .await?;

    println!("inserted {id}, updated {updated}, deleted {deleted}");
    Outcome::Ok(())
}

The SQLite DDL schema_for produces:

CREATE TABLE IF NOT EXISTS "users" (
  "id" INTEGER PRIMARY KEY,
  "email" TEXT NOT NULL,
  "name" TEXT NOT NULL,
  "is_active" BOOLEAN NOT NULL DEFAULT false,
  CONSTRAINT "uk_users_email" UNIQUE ("email")
)

The SQLite "id" INTEGER PRIMARY KEY above is the auto-assigning rowid alias; PostgreSQL gets "id" BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY and MySQL `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY. Introspection reports all three back as auto_increment, so an auto-increment model is a fixpoint of the schema differ on every engine (proven by the e2e attributes scenario). Column types are adapted per engine (BYTEA for byte fields on PostgreSQL; BINARY(16) for UUIDs and VARCHAR(255) for unique, indexed, or key String columns on MySQL, which cannot index TEXT; and so on).

4. Transactions, Concurrent Writers, and Retries

Every driver starts transactions through the same Connection API. TransactionOptions adds a TransactionMode on top of the isolation level: Concurrent selects FrankenSQLite's BEGIN CONCURRENT (page-level MVCC, several writers on one file) and is the native behavior on PostgreSQL and MySQL; Immediate, Exclusive, and Deferred pick SQLite's locking forms. A driver that cannot honor a mode returns TransactionErrorKind::UnsupportedMode instead of silently downgrading (C SQLite has no concurrent-writer mode). Conflicting concurrent writers fail with a retryable error, and retry_transaction re-runs the whole transaction with jittered backoff that respects the Cx budget and never retries after a cancellation:

use sqlmodel::prelude::*;
use sqlmodel::{Session, SessionConfig, TransactionMode, TransactionOptions};

async fn transfer<C: Connection>(cx: &Cx, conn: C) -> Outcome<i64, Error> {
    let moved = retry_transaction(
        cx,
        &conn,
        TransactionOptions::concurrent(),
        &RetryPolicy::default(),
        async |cx, tx| {
            tx.execute(cx, "UPDATE accounts SET balance = balance - 10 WHERE id = ?1", &[Value::BigInt(1)]).await?;
            tx.execute(cx, "UPDATE accounts SET balance = balance + 10 WHERE id = ?1", &[Value::BigInt(2)]).await?;
            Outcome::Ok(10)
        },
    )
    .await?;

    // Sessions can start every transaction concurrently, and `Session::with_retry`
    // re-runs a whole unit of work (rolling back and clearing the identity map
    // between attempts) when a flush or commit hits a retryable conflict:
    let mut session = Session::with_config(
        conn,
        SessionConfig::default().with_transaction_mode(TransactionMode::Concurrent),
    );
    let _ = session
        .with_retry(cx, &RetryPolicy::default(), async |cx, session| {
            session.connection().execute(cx, "UPDATE accounts SET audited = 1", &[]).await
        })
        .await?;
    Outcome::Ok(moved)
}

Console Output

SQLModel Rust includes an optional rich console output system for beautiful terminal feedback.

Features

  • Styled error messages with context, SQL highlighting, and suggestions
  • Formatted query result tables with type-based coloring
  • Schema visualization as interactive trees
  • Progress bars for bulk operations
  • Agent-safe: auto-detects AI coding tools (Claude Code, Codex, Cursor, Aider, etc.)

Quick Setup

Add the console feature to your dependency:

[dependencies]
sqlmodel-console = { version = "0.5.1", features = ["rich"] }

Create and use a console:

// (requires the `console` feature)
use sqlmodel_console::{SqlModelConsole, OutputMode};
use sqlmodel_console::renderables::QueryResultTable;

// Auto-detect mode (rich for humans, plain for agents)
let console = SqlModelConsole::new();

// Display query results
let table = QueryResultTable::new()
    .columns(vec!["id", "name", "email"])
    .row(vec!["1", "Alice", "alice@example.com"])
    .timing_ms(12.34);

console.print_table(&table);

Output Modes

Mode When Used Output
Rich Human on TTY Colors, tables, panels
Plain Agent detected / piped Parseable text
JSON SQLMODEL_JSON=1 Structured JSON

Agent Compatibility

Console output is agent-safe by default. When running under Claude Code, Codex CLI, Cursor, or other AI coding tools, output automatically switches to plain text that agents can parse.

Environment variables for control:

  • SQLMODEL_PLAIN=1 - Force plain text mode
  • SQLMODEL_RICH=1 - Force rich mode (even for agents)
  • SQLMODEL_JSON=1 - Force JSON output

Documentation

Visual Examples

Run the example programs to preview both rich and plain output:

cargo run -p sqlmodel-console --example console_demo
cargo run -p sqlmodel-console --example error_showcase
cargo run -p sqlmodel-console --example query_results
cargo run -p sqlmodel-console --example progress_demo
cargo run -p sqlmodel-console --example schema_visualization

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        sqlmodel (facade)                         │
│            Re-exports all crates for easy import                 │
└─────────────────────────────────────────────────────────────────┘
                               │
       ┌───────────────┬───────────────┬───────────────┬───────────────┬───────────────┐
       ▼               ▼               ▼               ▼               ▼
┌─────────────┐  ┌───────────────────┐  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│sqlmodel-core│  │ sqlmodel-macros   │  │ sqlmodel-query  │  │ sqlmodel-schema │  │ sqlmodel-session│
│ Model trait │  │ #[derive(Model)]  │  │ Query builder  │  │ DDL + migration │  │ Unit of work    │
└─────────────┘  └───────────────────┘  └─────────────────┘  └─────────────────┘  └─────────────────┘
                               │
                 ┌─────────────┴─────────────┐
                 ▼                           ▼
          ┌─────────────┐           ┌─────────────────┐
          │sqlmodel-pool│           │sqlmodel-console │ (optional)
          │Conn pooling │           │Rich output      │
          └─────────────┘           └─────────────────┘
                 │
       ┌─────────┼─────────┬─────────┬──────────────────┐
       ▼         ▼         ▼         ▼                  ▼
sqlmodel-postgres sqlmodel-mysql sqlmodel-sqlite sqlmodel-frankensqlite (drivers)
                                     (C SQLite, FFI)    (pure Rust, MVCC; nightly)

Crate Responsibilities

Crate Purpose
sqlmodel Facade crate—re-exports everything for use sqlmodel::prelude::*
sqlmodel-core Core traits (Model, Connection), types (Value, Row, Error)
sqlmodel-macros #[derive(Model)] proc macro with attribute parsing and code gen
sqlmodel-query Type-safe query builder with multi-dialect support
sqlmodel-schema DDL generation, schema builder, migration support
sqlmodel-session Unit of work + identity map
sqlmodel-pool Connection pooling with asupersync channels
sqlmodel-e2e All-driver end-to-end scenarios, oracles, and golden SQL (not published)
sqlmodel-postgres PostgreSQL wire protocol implementation
sqlmodel-mysql MySQL wire protocol implementation
sqlmodel-sqlite SQLite driver (C SQLite via bundled libsqlite3-sys)
sqlmodel-frankensqlite Pure-Rust SQLite driver (FrankenSQLite, page-level MVCC, BEGIN CONCURRENT); nightly-only
sqlmodel-console Optional rich console output for humans and agents

Model Attributes Reference

// (attribute cheat-sheet, not a compilable model)
#[derive(Model)]
#[sqlmodel(table = "custom_table_name")]  // Override table name
struct MyModel {
    #[sqlmodel(primary_key)]              // Part of primary key
    #[sqlmodel(auto_increment)]           // Auto-increment (usually with primary_key)
    #[sqlmodel(unique)]                   // UNIQUE constraint
    #[sqlmodel(nullable)]                 // Allow NULL values
    #[sqlmodel(column = "db_column")]     // Override column name
    #[sqlmodel(sql_type = "VARCHAR(100)")]// Override SQL type
    #[sqlmodel(default = "value")]        // DEFAULT clause
    #[sqlmodel(foreign_key = "table.col")]// FOREIGN KEY constraint
    #[sqlmodel(index)]                    // Create index on column
    #[sqlmodel(skip)]                     // Exclude from all DB operations
    field: Type,
}

Automatic Type Mapping

The wire format is dependency-free: dates are days since 1970-01-01, times and timestamps are i64 microseconds, UUIDs are 16 bytes, decimals are text (Value::Date, Value::Time, Value::Timestamp, Value::TimestampTz, Value::Uuid, Value::Decimal). The external types in the last rows are opt-in features on sqlmodel / sqlmodel-core; without the feature a model field of that type does not compile.

Rust Type SQL Type Feature
i8 TINYINT —
i16 SMALLINT —
i32 INTEGER —
i64 BIGINT —
f32 REAL —
f64 DOUBLE PRECISION —
bool BOOLEAN —
String TEXT —
char CHAR(1) —
Option<T> Nullable version of T —
Vec<u8> BYTEA / BLOB —
[u8; 16] UUID —
serde_json::Value JSON / JSONB —
chrono::NaiveDate DATE chrono
chrono::NaiveTime TIME chrono
chrono::NaiveDateTime TIMESTAMP (stored as UTC microseconds) chrono
chrono::DateTime<Utc> / DateTime<FixedOffset> TIMESTAMPTZ (offset applied, not stored) chrono
uuid::Uuid UUID (BINARY(16) on MySQL, 16-byte BLOB on SQLite) uuid
rust_decimal::Decimal NUMERIC / DECIMAL (scale preserved; TEXT on SQLite so digits stay exact) decimal
# `nightly-try` enables `?` on `Outcome` (nightly toolchain, see above)
sqlmodel = { version = "0.5.1", features = ["chrono", "uuid", "decimal", "nightly-try"] }

Expression Builder Reference

// (expression catalogue; each line is an `Expr`, the comment shows its SQL)
use sqlmodel::Expr;

// Column references
Expr::col("name")                      // "name"
Expr::qualified("users", "name")       // "users"."name"

// Comparisons
Expr::col("age").eq(18)                // "age" = $1
Expr::col("age").ne(18)                // "age" != $1
Expr::col("age").gt(18)                // "age" > $1
Expr::col("age").ge(18)                // "age" >= $1
Expr::col("age").lt(18)                // "age" < $1
Expr::col("age").le(18)                // "age" <= $1

// Null checks
Expr::col("deleted").is_null()         // "deleted" IS NULL
Expr::col("name").is_not_null()        // "name" IS NOT NULL

// Pattern matching
Expr::col("name").like("%john%")       // "name" LIKE $1
Expr::col("email").ilike("%@GMAIL%")   // "email" ILIKE $1 (Postgres)

// Lists and ranges
Expr::col("status").in_list([1, 2, 3]) // "status" IN ($1, $2, $3)
Expr::col("age").between(18, 65)       // "age" BETWEEN $1 AND $2

// Logical operators
expr1.and(expr2)                       // (expr1) AND (expr2)
expr1.or(expr2)                        // (expr1) OR (expr2)
Expr::not(expr)                        // NOT (expr)

// Aggregates
Expr::count_star()                     // COUNT(*)
Expr::col("id").count()                // COUNT("id")
Expr::col("amount").sum()              // SUM("amount")
Expr::col("price").avg()               // AVG("price")
Expr::col("age").min()                 // MIN("age")
Expr::col("age").max()                 // MAX("age")

// CASE expressions
Expr::case()
    .when(Expr::col("status").eq("active"), "Yes")
    .when(Expr::col("status").eq("pending"), "Maybe")
    .otherwise("No")

Limitations

Implementation Status

Capability Status Notes
Query execution ✅ Complete SELECT/INSERT/UPDATE/DELETE, upserts (on_conflict_*), bulk inserts (insert_many!), RETURNING (re-read by key on MySQL, which has none), the Join builder, typed subqueries (Expr::in_query, into_exists), and one-level eager loading (all_eager), each executed on every driver by the e2e operations scenario
Connection pooling ✅ Complete Generic pool with timeouts, health checks, metrics; proven on real connections (fan-out under one runtime, server-side counts, kills, panics, drains)
Prepared statements ✅ Per driver MySQL: every parameterized query/execute is prepared once per connection (64-entry cache, binary protocol) and executed per call. PostgreSQL: every parameterized query/execute is prepared and cached per connection (64-entry LRU cache, keyed by SQL + parameter OIDs; Close on eviction; unnamed statement for parameterless queries). SQLite drivers: no statement cache. Read from Com_stmt_* / pg_prepared_statements by the e2e session scenario; sqlmodel_query::cache::StatementCache only caches SQL text
Transactions ✅ Complete BEGIN/COMMIT/ROLLBACK with savepoint support
SQLite driver ✅ Complete Full Connection trait with transactions
MySQL driver ✅ Complete Wire protocol + SharedMySqlConnection
PostgreSQL driver ✅ Complete Wire protocol + SharedPgConnection with SCRAM auth
Runtime migrations ✅ Complete Schema diffing, migration runner (one transaction per migration on PostgreSQL and SQLite), version tracking with checksum drift detection
Lazy loading ✅ Explicit Lazy<T> + Session::load_lazy/load_many (batch-friendly; no implicit N+1). Opt-in detector: Session::enable_n1_detection(threshold) counts per-relationship loads, n1_stats() reports them

Known Limitations

  • Rust 1.95+ for the facade and the Postgres/MySQL/SQLite drivers; nightly for sqlmodel-frankensqlite (fsqlite enables core_intrinsics on x86_64) and for building this repo itself (.cargo/config.toml passes -Z threads)
  • No stable release yet: API may change
  • asupersync dependency: Resolved from crates.io as ^0.5.0, matching the runtime used by fsqlite 0.4.x, so a single runtime version unifies across the stack
  • PostgreSQL runtime handoff (#43): published SQLModel 0.5.1 can hang while validating a connection after its holder's current-thread runtime exits on Asupersync 0.5.0. The issue's explicit worker_threads(2) controls pass. The repository now bounds checkout validation, but fixed-runtime integration and release qualification remain open. See the production qualification runner for the staged Asupersync 0.6.1 candidate and the published controls.

Troubleshooting

"Failed to resolve dependency asupersync"

# SQLModel 0.5.x requires asupersync ^0.5.0, the same runtime line fsqlite 0.4.x
# uses, so one asupersync should resolve for the whole graph. If resolution
# fails, another dependency may require an older, incompatible runtime line:
cargo tree -i asupersync     # find who requires the conflicting version
# then upgrade that crate (or use `cargo update -p asupersync` once it allows 0.5.0)
cargo build

"feature core_intrinsics is unstable" / -Z threads rejected on stable

Only two things in this repository need nightly: sqlmodel-frankensqlite (its fsqlite-pager dependency enables core_intrinsics on x86_64) and the workspace's own .cargo/config.toml, which passes -Z threads=4 to rustc. Building the repository uses the pinned toolchain automatically:

rustup show   # installs nightly-2026-08-25 from rust-toolchain.toml on first use

Consumers of the published crates on stable Rust 1.95+ are unaffected unless they depend on sqlmodel-frankensqlite.

Clippy warnings about unsafe_code

The workspace has unsafe_code = "warn" by default. If you need unsafe code (e.g., for FFI), use #[allow(unsafe_code)] locally.

Build takes forever

# Use sccache for faster rebuilds
cargo install sccache
export RUSTC_WRAPPER=sccache
cargo build

FAQ

Why "SQLModel Rust" and not just use Diesel/SeaORM?

We wanted Python SQLModel's simplicity with Rust's safety. Diesel is powerful but has a steep learning curve. SeaORM is good but uses runtime async. We built SQLModel Rust for structured concurrency with asupersync from the ground up.

Why build your own PostgreSQL driver?

Control. We need deep integration with asupersync's capability context for cancel-correct operations. Existing drivers don't support our concurrency model.

Is this production-ready?

Not yet, and the README tries not to overstate it. Every core path exists and is unit-tested (about 2,000 tests): query execution, connection pooling, transactions, migrations, and drivers for PostgreSQL, MySQL, SQLite, and FrankenSQLite. End-to-end coverage against live PostgreSQL and MySQL runs in CI's integration job; the pre-1.0 API may still change, and the tracker (bv --robot-triage) lists the remaining proof and hardening work honestly.

Does it work with tokio?

No. We use asupersync exclusively. Tokio's model doesn't support structured concurrency the way we need.

Can I use this without async?

Not currently. The entire design assumes async operations with capability contexts.


About Contributions

Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via gh and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.


License

MIT License (with OpenAI/Anthropic Rider). See LICENSE.

About

Rust ORM inspired by Python's SQLModel: derive-macro models, type-safe query builder, multi-dialect SQL generation, and cancel-correct async via asupersync

Topics

Resources

Stars

8 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages