SQLModel is a Python library that combines Pydantic (data validation) and SQLAlchemy (SQL toolkit/ORM) to provide intuitive, type-safe database operations. This document outlines the strategy for porting SQLModel to Rust while preserving its developer experience and enhancing performance.
Critical insight: In Python, SQLModel depends on Pydantic and SQLAlchemy because Python lacks:
- Compile-time type checking (Pydantic provides runtime validation)
- Zero-cost abstractions (SQLAlchemy abstracts away database differences)
- Powerful macro system (everything done via metaclasses/decorators)
In Rust, we have all of these natively:
| Python Library | What It Provides | Rust Native Equivalent |
|---|---|---|
| Pydantic | Runtime type validation | Rust's type system (compile-time) |
| Pydantic | JSON serialization | serde + serde_json |
| Pydantic | Field metadata | Proc macro attributes |
| SQLAlchemy Core | Connection management | Our sqlmodel-core |
| SQLAlchemy Core | Query building | Our sqlmodel-query |
| SQLAlchemy ORM | Model→Table mapping | Our #[derive(Model)] macro |
| SQLAlchemy ORM | Session/UoW | Explicit transactions (simpler!) |
| SQLAlchemy | Migrations | Our sqlmodel-schema |
| SQLAlchemy | Connection pooling | Our sqlmodel-pool |
The legacy repos are REFERENCE ONLY - we study them to understand:
- What SQL each operation should generate
- What edge cases exist
- What the user-facing API should feel like
We do NOT translate their code. We implement the essence directly in idiomatic Rust.
- Intuitive API - Define models as simple structs with derive macros
- Type Safety - Compile-time checks for queries and data access
- Single Definition - One struct for validation, serialization, AND database mapping
- Minimal Boilerplate - Derive macros generate all the glue code
- Flexible Queries - Both ORM-style and raw SQL supported
sqlmodel (facade)
├── sqlmodel-core (types, traits)
├── sqlmodel-macros (derive macros)
├── sqlmodel-query (query builder)
├── sqlmodel-schema (DDL, migrations)
└── sqlmodel-pool (connection pooling)
Required:
asupersync- Async runtime with structured concurrencyserde/serde_json- Serializationproc-macro2/quote/syn- Macro support
NOT using:
tokio- Replaced by asupersyncsqlx- Building custom for zero-copy and asupersync integrationdiesel- Different design philosophysea-orm- Too much runtime overhead
Earlier drafts of this document scoped out a number of Python SQLModel/Pydantic/SQLAlchemy behaviors. The current project goal is feature-for-feature parity with the legacy Python SQLModel library.
Treat the items below as historical notes, not policy: if something is missing, it should be implemented
or explicitly justified and tracked (see FEATURE_PARITY.md and Beads issues).
- Python: Uses
__annotations__,get_type_hints(), runtime type inspection - Rust: Use proc macros for compile-time code generation
- Python: SQLModel inherits from both
BaseModeland SQLAlchemy - Rust: Separate
ModelandValidatederive macros using serde
- Python: Complex session/unit-of-work pattern
- Rust: Direct connection operations, explicit transactions
- No need to support legacy APIs or deprecated features
- Design for Rust idioms from the start
- Python: Automatic lazy loading of relationships
- Rust: Explicit eager loading with joins (no magic)
- Python: Supports building queries at runtime from strings
- Rust: Type-safe query builder only (raw SQL available for escape hatch)
- Phase 1: SQLite only
- Phase 2: PostgreSQL
- Phase 3: MySQL
- Simpler migration system without auto-generation
- Explicit up/down SQL scripts
- Python: Complex async session context managers
- Rust: Explicit connection passing with asupersync Cx
- Python:
Field(alias="...")for different JSON/DB names - Rust: Use serde's
#[serde(rename = "...")]separately
- Python:
@field_validator,@model_validator,@field_serializer - Rust: Separate validation trait with explicit methods
- Python:
@computed_fieldfor derived values - Rust: Use regular methods on the struct
- Python:
SQLModel[T]generic support - Rust: Use concrete types or trait bounds
- Defer to separate crate or manual implementation
- Not core to database operations
- Python: Complex inheritance patterns
- Rust: Use enums or composition instead
SQLModel Rust depends on asupersync for:
| Feature | asupersync Component |
|---|---|
| Async operations | Cx capability context |
| Cancellation | cx.checkpoint(), Outcome::Cancelled |
| Timeouts | Budget |
| Connection pool | Channels (when available) |
| Testing | LabRuntime |
| Component | Status | Impact on SQLModel |
|---|---|---|
| Scheduler | ✅ Done | Can run async ops |
| Cx context | ✅ Done | Core integration |
| Channels | ✅ Done | Pool implementation |
| TCP/IO | 🔜 Phase 2 | Database drivers blocked |
- Workspace setup
- Core type definitions
- Query builder skeleton
- asupersync integration patterns
- Model derive macro (full implementation)
- SELECT with type conversion
- INSERT/UPDATE/DELETE
- Transaction support
- CREATE TABLE generation
- Migration tracking table
- Migration execution
- Database introspection
- Connection pool using asupersync channels
- Health checks
- Connection recycling
- Validate derive macro
- Constraint checking
- Error message generation
- SQLite protocol implementation
- Type mapping
- Zero-copy optimizations
- PostgreSQL protocol
- Binary format support
- Array types
| Metric | Target |
|---|---|
| Code size | 10-20x smaller than Python SQLModel |
| Binary size | < 5MB with LTO |
| Startup time | < 10ms |
| Query latency | Competitive with native drivers |
| Type safety | 100% compile-time checked |
use sqlmodel::prelude::*;
#[derive(Model, Debug, Clone)]
#[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>,
}
async fn example(cx: &Cx, conn: &impl Connection) -> Outcome<(), Error> {
// Create table
conn.execute(cx, &create_table::<Hero>().build(), &[]).await?;
// Insert
let hero = Hero {
id: None,
name: "Spider-Man".into(),
secret_name: "Peter Parker".into(),
age: Some(25),
team_id: None,
};
let id = insert!(hero).execute(cx, conn).await?;
// Query
let heroes = select!(Hero)
.filter(Expr::col("age").gt(18))
.order_by(OrderBy::asc("name"))
.limit(10)
.all(cx, conn)
.await?;
// Transaction
let tx = conn.begin(cx).await?;
tx.execute(cx, "UPDATE heroes SET age = age + 1", &[]).await?;
tx.commit(cx).await?;
Outcome::Ok(())
}- Relationship handling - Should we support
Vec<Related>fields? - Connection string parsing - Build custom or use existing crate?
- SSL/TLS - Use rustls or native-tls?
- Prepared statements - Cache at connection or pool level?
- Complete Model derive macro implementation
- Extract full spec from legacy Python code
- Implement SELECT query execution
- Write comprehensive tests using LabRuntime