Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,32 @@ maturin develop # compile Rust + install in dev mode
```

### Run Tests

```bash
# Rust unit tests (no DB needed)
cargo test

# Python unit tests (no DB needed)
python test.py

# Integration tests (SQLite)
python test.py --integration

# All tests
python test.py --all
```


### Run Benchmarks

To measure the performance of the query compiler:

```bash
cd ryx-query && cargo bench
```

### Type Check


```bash
mypy ryx/
```
Expand Down
66 changes: 15 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,70 +75,34 @@ async with ryx.transaction():
| **Backends** | All | All | **PG · MySQL · SQLite** |
| **Migrations** | Built-in | Alembic | **Built-in** |

## Performance
## Architecture

<p align="center">
<img src="ryx_architecture.svg" alt="Ryx Architecture" width="100%" />
</p>

Your Python queries are compiled to SQL in Rust, executed by sqlx, and decoded back — all without blocking the Python event loop.

Since v0.1.3, the query engine has been extracted into a standalone crate `ryx-query`. This decouples the SQL compilation logic from the PyO3 bindings, enabling extreme performance and independent testing.

## Performance

Benchmark of 1 000 rows on SQLite (lower is better):

| Operation | Ryx ORM | SQLAlchemy ORM | SQLAlchemy Core | Ryx raw |
|-----------|--------:|---------------:|----------------:|--------:|
| **bulk_create** | 0.0074 s | 0.1696 s | 0.0022 s | 0.0011 s |
| **bulk_update** | 0.0023 s | 0.0018 s | 0.0010 s | 0.0005 s |
| **bulk_delete** | 0.0005 s | 0.0012 s | 0.0009 s | 0.0004 s |
| **filter + order + limit** | 0.0009 s | 0.0019 s | 0.0008 s | 0.0004 s |
| **aggregate** | 0.0002 s | 0.0015 s | 0.0005 s | 0.0001 s |

Ryx ORM is **16× faster** than SQLAlchemy ORM on bulk inserts and **2× faster** on deletes — while keeping the same Django-style API. The raw SQL layer (`raw_execute` / `raw_fetch`) gives you near-C speed when you need it.

**Internal Compilation Speed**: Our query compiler is blindingly fast, with simple lookups compiled in **~248ns** and complex query trees in **~1µs**.

Run the benchmark yourself:

```bash
uv add sqlalchemy[asyncio] aiosqlite
uv run python examples/13_benchmark_sqlalchemy.py
```

## Quick Start

```bash
pip install maturin
maturin develop # compile Rust + install
```

```python
import asyncio, ryx
from ryx import Model, CharField

class Article(Model):
title = CharField(max_length=200)

async def main():
await ryx.setup("sqlite:///app.db")
await ryx.migrate([Article])
await Article.objects.create(title="Hello Ryx")
print(await Article.objects.all())

asyncio.run(main())
```

## Key Features

- **30+ field types** — from `AutoField` to `JSONField`, with validation built in
- **Q objects** — complex `AND` / `OR` / `NOT` expressions with nesting
- **Aggregations** — `Count`, `Sum`, `Avg`, `Min`, `Max` with `GROUP BY` and `HAVING`
- **Relationships** — `ForeignKey`, `OneToOneField`, `ManyToManyField` with `select_related` / `prefetch_related`
- **Transactions** — async context managers with nested savepoints
- **Signals** — `pre_save`, `post_save`, `pre_delete`, `post_delete` and more
- **Migrations** — autodetect schema changes, generate and apply
- **Validation** — field-level + model-level, collects all errors before raising
- **Sync/async bridge** — use from sync or async code seamlessly
- **CLI** — `python -m ryx migrate`, `makemigrations`, `shell`, `inspectdb`

## Architecture

<p align="center">
<img src="ryx_architecture.svg" alt="Ryx Architecture" width="100%" />
</p>

Your Python queries are compiled to SQL in Rust, executed by sqlx, and decoded back — all without blocking the Python event loop.

## Documentation

Expand Down
1 change: 1 addition & 0 deletions docs/doc/advanced/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ Deep-dive topics for production-ready applications.
- **[Caching](./caching)** — Query result caching
- **[Custom Lookups](./custom-lookups)** — Extend the query API
- **[Sync/Async](./sync-async)** — Bridge between sync and async code
- **[Multi-Databases](./multi-db)** - Multi-Database Support
- **[Raw SQL](./raw-sql)** — Escape hatch for complex queries
- **[CLI](./cli)** — Command-line management commands
99 changes: 99 additions & 0 deletions docs/doc/advanced/multi-db.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
sidebar_position: 11
title: Multi-Database Support
description: Learn how to route queries across multiple databases in Ryx.
---

Ryx supports routing queries across multiple databases, allowing you to separate read and write workloads, split data across different servers, or use a dedicated database for specific models.

## Configuration

To enable multi-database support, provide a dictionary of URLs to `ryx_core.setup` instead of a single string. Each key in the dictionary serves as an **alias** for that database pool.

```python
from ryx import ryx_core

# Configure multiple databases
urls = {
"default": "postgresql://user:pass@localhost/main_db",
"users": "postgresql://user:pass@localhost/user_db",
"logs": "sqlite::memory:",
}

await ryx_core.setup(urls)
```

## Routing Strategies

Ryx resolves which database to use for a query in the following order of priority:

1. **Explicit Routing**: Using `.using(alias)` on a QuerySet.
2. **Dynamic Router**: Using a configured `BaseRouter`.
3. **Model Metadata**: Using the `database` option in `Model.Meta`.
4. **Default**: Falling back to the `'default'` alias.

### 1. Explicit Routing

You can force a query to run on a specific database using the `.using()` method. This is useful for one-off queries or manual routing.

```python
# Read from the 'users' database
users = await User.objects.using("users").all()

# Write to the 'logs' database
await Log.objects.using("logs").create(message="System boot")
```

### 2. Model-Level Routing

You can assign a model to a specific database by default using the `database` option in its `Meta` class.

```python
class Log(Model):
message = CharField()

class Meta:
database = "logs"
```

Any query on `Log` will now use the `logs` database unless overridden by `.using()`.

### 3. Dynamic Routing (The Router)

For more complex logic (e.g., routing based on the environment, user, or model type), you can implement a custom router by inheriting from `BaseRouter`.

```python
from ryx.router import BaseRouter, set_router

class MyProjectRouter(BaseRouter):
def db_for_read(self, model, **hints):
if model.__name__ == "User":
return "users"
return None # Fallback to default

def db_for_write(self, model, **hints):
if model.__name__ == "User":
return "users"
return None

# Activate the router globally
set_router(MyProjectRouter())
```

## Multi-Database Transactions

Transactions in Ryx are tied to a specific database connection. To start a transaction on a non-default database, pass the `alias` to the `transaction()` context manager.

```python
import ryx

async with Ryx.transaction(alias="users"):
await User.objects.create(name="Alice")
await User.objects.create(name="Bob")
# If an exception occurs, only changes to 'users' DB are rolled back.
```

### Nesting and Multiple Databases

- If you start a transaction on a database that already has an active transaction on the current task, Ryx creates a **SAVEPOINT**.
- If you start a transaction on a *different* database while another is active, Ryx starts a new independent transaction for that database.
10 changes: 7 additions & 3 deletions docs/doc/internals/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sidebar_position: 2
Ryx is built in three layers, each with a clear responsibility.

## Layer Diagram

```
┌──────────────────────────────────────────────────────────┐
│ Python Layer (ryx/) │
Expand All @@ -17,8 +17,11 @@ Ryx is built in three layers, each with a clear responsibility.
│ PyO3 Boundary (src/lib.rs) │
│ QueryBuilder · TransactionHandle · Type Bridge · Async │
├──────────────────────────────────────────────────────────┤
│ Rust Core (src/) │
│ AST · Q-Trees · SQL Compiler · Executor · Pool · Tx │
│ Modular Query Engine (ryx-query crate) │
│ AST · Q-Trees · SQL Compiler · Lookup Registry │
├──────────────────────────────────────────────────────────┤
│ Rust Core (src/) │
│ Executor · Pool · Transaction Logic │
├──────────────────────────────────────────────────────────┤
│ sqlx 0.8.6 + tokio 1.40 │
│ AnyPool · Async Drivers · Transactions │
Expand All @@ -27,6 +30,7 @@ Ryx is built in three layers, each with a clear responsibility.
└──────────────────────────────────────────────────────────┘
```


## Query Execution Flow

```
Expand Down
59 changes: 30 additions & 29 deletions docs/doc/internals/query-compiler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,41 @@ sidebar_position: 4
---

# Query Compiler

The heart of Ryx — transforms Python query expressions into optimized SQL.

The heart of Ryx — transforms Python query expressions into optimized SQL.

Since v0.1.3, the compiler resides in the standalone `ryx-query` crate, decoupled from the Python bindings for maximum performance and testability.
## Pipeline

```
Python QuerySet methods
QueryNode (Rust AST)
compiler::compile()
ryx_query::compiler::compile()
CompiledQuery { sql: String, values: Vec<SqlValue> }
```

## AST Types

### QueryNode

The root of every query:

```rust
pub struct QueryNode {
pub operation: QueryOperation, // Select, Aggregate, Count, Delete, Update, Insert
pub table: String,
pub columns: Vec<String>,
pub backend: Backend, // DB backend for SQL generation
pub operation: QueryOperation, // Select, Aggregate, Count, Delete, Update, Insert
pub filters: Vec<FilterNode>,
pub q_tree: Option<QNode>,
pub q_filter: Option<QNode>,
pub joins: Vec<JoinClause>,
pub annotations: Vec<AggregateExpr>,
pub group_by: Vec<String>,
pub having: Vec<FilterNode>,
pub order_by: Vec<OrderByClause>,
Expand All @@ -43,48 +46,46 @@ pub struct QueryNode {
pub distinct: bool,
}
```

### QNode — Boolean Expression Tree

```rust
pub enum QNode {
Leaf { field: String, lookup: String, value: SqlValue, negated: bool },
And { left: Box<QNode>, right: Box<QNode> },
Or { left: Box<QNode>, right: Box<QNode> },
Not { inner: Box<QNode> },
And(Vec<QNode>),
Or(Vec<QNode>),
Not(Box<QNode>),
}
```

### SqlValue — Type-Safe Values

```rust
pub enum SqlValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
Text(String),
Bytes(Vec<u8>),
Date(chrono::NaiveDate),
Time(chrono::NaiveTime),
DateTime(chrono::NaiveDateTime),
Json(serde_json::Value),
List(Vec<SqlValue>),
}
```

### JoinClause

```rust
pub enum JoinKind { Inner, LeftOuter, RightOuter, FullOuter, Cross }

pub enum JoinKind { Inner, LeftOuter, RightOuter, FullOuter, CrossJoin }
pub struct JoinClause {
pub table: String,
pub condition: String,
pub kind: JoinKind,
pub table: String,
pub alias: Option<String>,
pub on_left: String,
pub on_right: String,
}
```


## Compilation Process

1. **SELECT clause** — `columns` or `*`
Expand Down
2 changes: 1 addition & 1 deletion docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const config = {
},
{
type: 'custom-search-bar',
position: 'right',
position: 'center',
},
{
type: 'custom-github-stats',
Expand Down
4 changes: 2 additions & 2 deletions docs/docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { Config } from '@docusaurus/types';
const config: Config = {
title: 'Ryx ORM',
tagline: 'Django-style Python ORM. Powered by Rust.',
favicon: 'img/favicon.ico',
url: 'https://ryx.alldotpy.dev',
favicon: 'img/logo.svg',
url: 'https://ryx.alldotpy.com',
baseUrl: '/',
organizationName: 'AllDotPy',
projectName: 'Ryx',
Expand Down
Loading
Loading