A blazingly fast Python ORM powered by Rust
Project description
OrmKit
A blazingly fast Python ORM powered by Rust.
Features
- SQLAlchemy-like API: Familiar declarative model syntax with
Mapped[]type hints - Custom Rust Drivers: PostgreSQL driver with custom wire protocol, SQLite via rusqlite - no external dependencies
- Async-first: Native async/await support throughout
- PostgreSQL + SQLite: Production and development databases covered
- Multiple API styles: From simple one-liners to full Unit of Work pattern
- Relationships: One-to-many, many-to-one, and many-to-many with eager loading
- Django-style Queries: Intuitive filter operators (
age__gt,name__like,tags__in, etc.) - Lazy Row Conversion: Data stays in Rust until accessed - minimizes Python/Rust boundary crossings
- Advanced Features: Soft delete, upsert, JSON columns, migrations, Q objects for complex queries
Installation
pip install ormkit
Quick Start
Define Your Models
from ormkit import (
Base, Mapped, mapped_column, ForeignKey, relationship,
create_engine, AsyncSession, selectinload, JSON
)
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(max_length=100)
email: Mapped[str] = mapped_column(unique=True)
age: Mapped[int | None] = mapped_column(nullable=True)
metadata: Mapped[dict] = mapped_column(JSON) # JSONB on PostgreSQL
# One-to-many relationship
posts: Mapped[list["Post"]] = relationship(back_populates="author")
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(max_length=200)
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
# Many-to-one relationship
author: Mapped[User] = relationship(back_populates="posts")
Simple API (Recommended)
async def main():
engine = await create_engine("postgresql://localhost/mydb")
# Or for SQLite:
# engine = await create_engine("sqlite:///mydb.sqlite")
session = AsyncSession(engine)
# Insert - returns model with generated ID
user = await session.insert(User(name="Alice", email="alice@example.com"))
print(f"Created user with id={user.id}")
# Query with Django-style filters
adults = await session.query(User).filter(age__gte=18).all()
user = await session.query(User).filter(email="alice@example.com").first()
# Get by primary key
user = await session.get(User, 1)
# Update
await session.update(user, name="Alicia", age=26)
# Delete
await session.remove(user)
Relationships & Eager Loading
# Load users with their posts in a single query
users = await session.query(User).options(selectinload("posts")).all()
for user in users:
print(f"{user.name} has {len(user.posts)} posts")
for post in user.posts:
print(f" - {post.title}")
# Load posts with their authors
posts = await session.query(Post).options(joinedload("author")).all()
for post in posts:
print(f"{post.title} by {post.author.name}")
Transaction Context (Auto-commit)
from ormkit import session_context
async with session_context(engine) as session:
await session.insert(User(name="Alice", email="alice@example.com"))
await session.insert(User(name="Bob", email="bob@example.com"))
# Commits automatically on exit, rolls back on exception
Batch Operations
async with session.begin() as tx:
tx.add(User(name="Alice", email="alice@example.com"))
tx.add(User(name="Bob", email="bob@example.com"))
tx.add(User(name="Charlie", email="charlie@example.com"))
# Commits automatically
# Or use insert_all for bulk inserts
users = await session.insert_all([
User(name="User1", email="user1@example.com"),
User(name="User2", email="user2@example.com"),
User(name="User3", email="user3@example.com"),
])
Query Builder (Django-style filters)
# Comparison operators
users = await session.query(User).filter(age__gt=18).all() # age > 18
users = await session.query(User).filter(age__gte=18).all() # age >= 18
users = await session.query(User).filter(age__lt=65).all() # age < 65
users = await session.query(User).filter(age__lte=65).all() # age <= 65
users = await session.query(User).filter(age__ne=0).all() # age != 0
# Pattern matching
users = await session.query(User).filter(name__like="A%").all() # LIKE pattern
users = await session.query(User).filter(name__ilike="a%").all() # Case-insensitive (PostgreSQL)
users = await session.query(User).filter(name__contains="ali").all() # Contains substring
users = await session.query(User).filter(name__startswith="A").all() # Starts with
users = await session.query(User).filter(name__endswith="e").all() # Ends with
# IN and NOT IN
users = await session.query(User).filter(role__in=["admin", "mod"]).all()
users = await session.query(User).filter(status__notin=["banned", "deleted"]).all()
# NULL checks
users = await session.query(User).filter(deleted_at__isnull=True).all()
# Multiple filters (AND)
users = await session.query(User).filter(age__gte=18, age__lt=65).all()
# Complex queries with Q objects (OR, AND, NOT)
from ormkit import Q
users = await session.query(User).filter(
Q(age__gt=18) | Q(vip=True)
).all()
# Chaining
users = await session.query(User) \
.filter(age__gte=18) \
.order_by("-created_at") \
.limit(10) \
.offset(20) \
.all()
# Aggregates
count = await session.query(User).filter(age__gte=18).count()
total = await session.query(Order).filter(status="completed").sum("amount")
avg_age = await session.query(User).avg("age")
exists = await session.query(User).filter(email="admin@example.com").exists()
# Bulk operations
deleted = await session.query(User).filter(age__lt=18).delete()
updated = await session.query(User).filter(role="guest").update(role="member")
JSON Column Queries
# Query nested JSON fields
users = await session.query(User).filter(metadata__plan="premium").all()
users = await session.query(User).filter(metadata__settings__theme="dark").all()
Upsert (INSERT ... ON CONFLICT)
# Insert or update on conflict
user = await session.upsert(
User(email="alice@example.com", name="Alice"),
conflict_target="email",
update_fields=["name"]
)
# Bulk upsert
users = await session.upsert_all(
[User(email="a@example.com", name="A"), User(email="b@example.com", name="B")],
conflict_target="email",
update_fields=["name"]
)
Soft Delete
from ormkit import SoftDeleteMixin
class Article(SoftDeleteMixin, Base):
__tablename__ = "articles"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
# Soft delete (sets deleted_at timestamp)
await session.soft_delete(article)
# Restore a soft-deleted record
await session.restore(article)
# Query excludes soft-deleted by default
articles = await session.query(Article).all() # Only non-deleted
# Include soft-deleted records
all_articles = await session.query(Article).with_deleted().all()
# Query only soft-deleted records
deleted = await session.query(Article).only_deleted().all()
# Permanently delete
await session.force_delete(article)
Streaming Large Result Sets
# Process large datasets without loading all into memory
async for user in session.query(User).stream(batch_size=1000):
process_user(user)
Eager Loading Options
from ormkit import selectinload, joinedload, noload
# selectinload - loads using SELECT IN query (best for collections)
users = await session.query(User).options(selectinload("posts")).all()
# joinedload - loads using JOIN (best for single objects)
posts = await session.query(Post).options(joinedload("author")).all()
# noload - explicitly disable loading
users = await session.query(User).options(noload("posts")).all()
# user.posts will be empty []
# Chain multiple options
users = await session.query(User).options(
selectinload("posts"),
selectinload("profile"),
).all()
Traditional SQLAlchemy-style API
from ormkit import select
async with AsyncSession(engine) as session:
# Manual add/commit
user = User(name="Alice", email="alice@example.com")
session.add(user)
await session.commit()
# Query with select()
stmt = select(User).where(User.age >= 18)
result = await session.execute(stmt)
users = result.scalars().all()
Raw SQL Queries
# Execute raw SQL
result = await session.execute_raw(
"SELECT * FROM users WHERE age > ?",
[18]
)
for row in result.all():
print(row["name"], row["age"])
# Get results as tuples (faster for large result sets)
result = await engine.execute("SELECT id, name FROM users", [])
tuples = result.tuples() # [(1, "Alice"), (2, "Bob"), ...]
# Get a single column
names = result.column("name") # ["Alice", "Bob", ...]
API Reference
Model Definition
| Function | Description |
|---|---|
mapped_column(primary_key=False, nullable=False, unique=False, index=False, default=None, max_length=None) |
Define a database column |
ForeignKey("table.column", ondelete=None, onupdate=None) |
Define a foreign key reference |
relationship(back_populates=None, lazy="select", uselist=None, secondary=None) |
Define a relationship |
JSON |
Marker for JSON/JSONB columns |
Session Methods
| Method | Description |
|---|---|
session.insert(instance) |
Insert and return with generated ID |
session.insert_all(instances) |
Bulk insert multiple instances |
session.get(Model, id) |
Get by primary key |
session.get_or_raise(Model, id) |
Get by primary key, raise if not found |
session.update(instance, **values) |
Update an instance |
session.remove(instance) |
Delete an instance |
session.query(Model) |
Create a query builder |
session.upsert(instance, conflict_target, update_fields) |
Insert or update on conflict |
session.upsert_all(instances, conflict_target, update_fields) |
Bulk upsert |
session.bulk_update(Model, values, **filters) |
Bulk update matching records |
session.soft_delete(instance) |
Soft delete (sets deleted_at) |
session.restore(instance) |
Restore soft-deleted record |
session.force_delete(instance) |
Permanently delete |
session.begin() |
Start a transaction context |
session.commit() |
Commit pending changes |
session.rollback() |
Rollback pending changes |
Query Methods
| Method | Description |
|---|---|
query.filter(**kwargs) |
Filter with Django-style operators |
query.filter_by(**kwargs) |
Filter with exact matches |
query.order_by(*columns) |
Order results (prefix with - for DESC) |
query.limit(n) |
Limit results |
query.offset(n) |
Offset results |
query.distinct() |
Return distinct results |
query.group_by(*columns) |
Group by columns |
query.having(**kwargs) |
Filter on aggregates |
query.options(*load_options) |
Add eager loading options |
query.all() |
Get all results |
query.first() |
Get first result |
query.one() |
Get exactly one result (raises if not 1) |
query.one_or_none() |
Get one or None (raises if > 1) |
query.count() |
Count matching rows |
query.sum(column) |
Sum of column values |
query.avg(column) |
Average of column values |
query.min(column) |
Minimum value |
query.max(column) |
Maximum value |
query.exists() |
Check if any rows match |
query.delete() |
Delete matching rows |
query.update(**values) |
Update matching rows |
query.values(*columns) |
Return dicts with specific columns |
query.values_list(*columns) |
Return tuples with specific columns |
query.stream(batch_size) |
Stream results in batches |
query.with_deleted() |
Include soft-deleted records |
query.only_deleted() |
Return only soft-deleted records |
Filter Operators
| Operator | SQL | Example |
|---|---|---|
| (none) | = |
filter(name="Alice") |
__gt |
> |
filter(age__gt=18) |
__gte |
>= |
filter(age__gte=18) |
__lt |
< |
filter(age__lt=65) |
__lte |
<= |
filter(age__lte=65) |
__ne |
!= |
filter(status__ne="deleted") |
__in |
IN |
filter(role__in=["admin", "mod"]) |
__notin |
NOT IN |
filter(status__notin=["banned"]) |
__like |
LIKE |
filter(name__like="A%") |
__ilike |
ILIKE |
filter(name__ilike="a%") (PostgreSQL) |
__contains |
LIKE %x% |
filter(name__contains="ali") |
__icontains |
ILIKE %x% |
filter(name__icontains="ali") |
__startswith |
LIKE x% |
filter(name__startswith="A") |
__endswith |
LIKE %x |
filter(name__endswith="e") |
__isnull |
IS NULL / IS NOT NULL |
filter(deleted_at__isnull=True) |
Benchmarks
Single Row Queries (Where Latency Matters)
| Operation | OrmKit | aiosqlite | Notes |
|---|---|---|---|
| Single row by ID | 0.036ms | 0.036ms | Identical performance |
For typical web application queries, OrmKit matches raw aiosqlite performance.
Bulk Operations (10,000 rows)
| Operation | OrmKit | aiosqlite | Relative |
|---|---|---|---|
| SELECT * (tuples) | 11ms | 5.7ms | 0.52x |
| SELECT * (dicts) | 11ms | 6.5ms | 0.59x |
| Bulk Insert | 2.3ms | 2.0ms | 0.87x |
Model Instantiation (10,000 rows)
This is where Rust shines - converting raw data to ORM model instances:
| Method | Time | vs Pure Python |
|---|---|---|
| Raw tuples | 0.96ms | - |
| Raw dicts | 1.1ms | - |
| Rust → Python models | 2.0ms | 4.9x faster |
Python _from_row_fast |
9.6ms | baseline |
Python __init__ |
11.6ms | 0.83x |
OrmKit's Rust-powered model instantiation is 4.9x faster than pure Python.
Time Breakdown (10,000 row SELECT)
SQL execution + fetch: 85.6% (9.6ms) - Rust driver layer
Python conversion: 14.4% (1.6ms) - highly optimized
For maximum speed, use result.tuples() or result.column() methods which bypass model creation entirely.
Development
# Install dependencies
uv venv && source .venv/bin/activate
uv pip install maturin pytest pytest-asyncio
# Build the Rust extension (debug)
maturin develop
# Build with optimizations (release)
maturin develop --release
# Run tests
pytest tests/ -v
# Run benchmarks
python benchmarks/run_all.py
Architecture
OrmKit uses a layered architecture:
┌─────────────────────────────────────────────────────────────┐
│ Python Layer │
│ - Declarative model definitions (like SQLAlchemy 2.0) │
│ - Type hints with Mapped[] / mapped_column() │
│ - Pythonic query builder API │
│ - Async session management │
│ - Relationship loading (selectinload, joinedload) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Rust Core (PyO3) │
│ - Query execution │
│ - Connection pool management │
│ - Lazy row conversion (data stays in Rust until accessed) │
│ - Type-safe parameter binding │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Custom Rust Drivers │
│ - PostgreSQL (custom wire protocol implementation) │
│ - SQLite (rusqlite with tokio async wrapper) │
└─────────────────────────────────────────────────────────────┘
Supported Databases
- PostgreSQL 12+ -
postgresql://user:pass@host:port/dbname - SQLite 3 -
sqlite:///path/to/db.sqliteorsqlite::memory:
Python Type Support
| Python Type | PostgreSQL | SQLite |
|---|---|---|
int |
INTEGER/SERIAL | INTEGER |
str |
TEXT/VARCHAR | TEXT |
float |
DOUBLE PRECISION | REAL |
bool |
BOOLEAN | INTEGER (0/1) |
bytes |
BYTEA | BLOB |
datetime |
TIMESTAMP | TEXT |
date |
DATE | TEXT |
time |
TIME | TEXT |
dict / list (JSON) |
JSONB | TEXT |
Optional[T] |
T (nullable) | T (nullable) |
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ormkit-0.1.4.tar.gz.
File metadata
- Download URL: ormkit-0.1.4.tar.gz
- Upload date:
- Size: 263.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b5cbabcd9239458c7168c4e238b9789d02beb5a48ebbe6a338f24e76ba8e0a2
|
|
| MD5 |
6bc80b333009873dc74ea9f59c430bd8
|
|
| BLAKE2b-256 |
07e72d4bbbdc45417b83d1b0779665f10ce59acd603a0e24da1f2a71cbd86b34
|
Provenance
The following attestation bundles were made for ormkit-0.1.4.tar.gz:
Publisher:
publish.yml on alexogeny/ormkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ormkit-0.1.4.tar.gz -
Subject digest:
7b5cbabcd9239458c7168c4e238b9789d02beb5a48ebbe6a338f24e76ba8e0a2 - Sigstore transparency entry: 976416353
- Sigstore integration time:
-
Permalink:
alexogeny/ormkit@c4f3708d86e9453b206fbdc8db3e7ef4a3a177bf -
Branch / Tag:
refs/heads/main - Owner: https://github.com/alexogeny
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c4f3708d86e9453b206fbdc8db3e7ef4a3a177bf -
Trigger Event:
push
-
Statement type:
File details
Details for the file ormkit-0.1.4-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: ormkit-0.1.4-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 1.5 MB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41a656dd7b15e617c996f8506128f58ef53b4a068be8e325e04aa66e39e0b5f4
|
|
| MD5 |
4b11801f190c86f726bb149d0102f5e9
|
|
| BLAKE2b-256 |
6ba65d50b0b7f1812d7b640c7bcf62dbb1c0e72b4cd49bd0f5a06f3478c2e59d
|
Provenance
The following attestation bundles were made for ormkit-0.1.4-cp312-cp312-win_amd64.whl:
Publisher:
publish.yml on alexogeny/ormkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ormkit-0.1.4-cp312-cp312-win_amd64.whl -
Subject digest:
41a656dd7b15e617c996f8506128f58ef53b4a068be8e325e04aa66e39e0b5f4 - Sigstore transparency entry: 976416364
- Sigstore integration time:
-
Permalink:
alexogeny/ormkit@c4f3708d86e9453b206fbdc8db3e7ef4a3a177bf -
Branch / Tag:
refs/heads/main - Owner: https://github.com/alexogeny
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c4f3708d86e9453b206fbdc8db3e7ef4a3a177bf -
Trigger Event:
push
-
Statement type:
File details
Details for the file ormkit-0.1.4-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: ormkit-0.1.4-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.5 MB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bac647cc1da31221dc80dca05412571a697d8d79e8547469c379499b254c8005
|
|
| MD5 |
0158a536e2c2e8cbba114a2538d287e6
|
|
| BLAKE2b-256 |
43496f87f3e4dafc65a2d0244a90d53c857fffe04db23b8e79dcaa3df10d5cea
|
Provenance
The following attestation bundles were made for ormkit-0.1.4-cp312-cp312-macosx_11_0_arm64.whl:
Publisher:
publish.yml on alexogeny/ormkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ormkit-0.1.4-cp312-cp312-macosx_11_0_arm64.whl -
Subject digest:
bac647cc1da31221dc80dca05412571a697d8d79e8547469c379499b254c8005 - Sigstore transparency entry: 976416357
- Sigstore integration time:
-
Permalink:
alexogeny/ormkit@c4f3708d86e9453b206fbdc8db3e7ef4a3a177bf -
Branch / Tag:
refs/heads/main - Owner: https://github.com/alexogeny
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c4f3708d86e9453b206fbdc8db3e7ef4a3a177bf -
Trigger Event:
push
-
Statement type:
File details
Details for the file ormkit-0.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: ormkit-0.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.7 MB
- Tags: CPython 3.8, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3ff5383283fd2ada8e88bffc035bec7e5c91bc162adb3662a4cf1334e147a44
|
|
| MD5 |
e081fd100027531bf4a49e0f2f08257a
|
|
| BLAKE2b-256 |
5e0b56c002da64f5a2d4dc5da882c7e54b54058ce4faf1e62e2354728c1609f6
|
Provenance
The following attestation bundles were made for ormkit-0.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
publish.yml on alexogeny/ormkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ormkit-0.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
b3ff5383283fd2ada8e88bffc035bec7e5c91bc162adb3662a4cf1334e147a44 - Sigstore transparency entry: 976416360
- Sigstore integration time:
-
Permalink:
alexogeny/ormkit@c4f3708d86e9453b206fbdc8db3e7ef4a3a177bf -
Branch / Tag:
refs/heads/main - Owner: https://github.com/alexogeny
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c4f3708d86e9453b206fbdc8db3e7ef4a3a177bf -
Trigger Event:
push
-
Statement type: