Architecture
Ryx is built in three layers, each with a clear responsibility.
Layer Diagram
┌──────────────────────────────────────────────────────────┐
│ Python Layer (ryx/) │
│ Model · QuerySet · Q · Fields · Validators · Signals │
│ Transactions · Relations · Migrations · CLI │
├──────────────────────────────────────────────────────────┤
│ PyO3 Boundary (src/lib.rs) │
│ QueryBuilder · TransactionHandle · Type Bridge · Async │
├──────────────────────────────────────────────────────────┤
│ Rust Core (src/) │
│ AST · Q-Trees · SQL Compiler · Executor · Pool · Tx │
├──────────────────────────────────────────────────────────┤
│ sqlx 0.8.6 + tokio 1.40 │
│ AnyPool · Async Drivers · Transactions │
├──────────────────────────────────────────────────────────┤
│ PostgreSQL · MySQL · SQLite │
└──────────────────────────────────────────────────────────┘
Query Execution Flow
Python: Post.objects.filter(active=True).order_by("-views").limit(10)
│
▼
QuerySet builds QueryNode (immutable builder pattern)
│
▼
PyQueryBuilder.fetch_all() — crosses PyO3 boundary
│
▼
compiler::compile(&QueryNode) → CompiledQuery { sql, values }
│
▼
executor::fetch_all(compiled) → sqlx::query(sql).bind(values).fetch_all(pool)
│
▼
decode_row(AnyRow) → HashMap<String, JsonValue>
│
▼
json_to_py() → PyDict
│
▼
Model._from_row(row) → List[Model]
Key Design Decisions
Immutable Builders
Both Python QuerySet and Rust QueryNode use immutable builders — every method returns a new instance:
// Rust: self by value, not &mut self
pub fn add_filter(self, field: &str, lookup: &str, value: SqlValue) -> Self { ... }
# Python: returns new QuerySet
def filter(self, **kwargs) -> "QuerySet":
clone = self._clone()
clone.builder.add_filter(...)
return clone
GIL Minimization
The Rust executor holds no Python GIL during SQL execution:
- Decode
AnyRow→HashMap<String, JsonValue>(no GIL) - Convert
HashMap→PyDict(brief GIL hold at boundary) - Python wraps
PyDict→Modelinstances
ContextVar Transaction Propagation
Active transactions use contextvars.ContextVar — they propagate through async call stacks automatically:
async with ryx.transaction():
await some_function() # Uses the same transaction
await another_function() # Still in the same transaction
AnyPool Over Typed Pools
sqlx::any::AnyPool provides a single code path for all backends:
- Pro: One codebase, runtime database selection
- Con: No compile-time query checking
- Trade-off: Worth it for a dynamic ORM
Next Steps
→ Rust Core — Deep dive into each Rust module