Skip to main content

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")
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.sqlite or sqlite::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

Metadata

Release files for ormkit 0.1.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ormkit 0.1.5
File Size Uploaded
ormkit-0.1.5.tar.gz 265.1 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for ormkit 0.1.5
File Interpreter ABI Platform
ormkit-0.1.5-cp312-cp312-win_amd64.whl CPython 3.12 CPython 3.12 Windows x86-64 Details
ormkit-0.1.5-cp312-cp312-macosx_11_0_arm64.whl CPython 3.12 CPython 3.12 macOS 11.0+ ARM64 Details
ormkit-0.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.8 CPython 3.8 Linux glibc 2.17+ x86-64 Details

Total release size: 4.9 MB

Release files / ormkit-0.1.5.tar.gz

Download URL ormkit-0.1.5.tar.gz
Size 265.1 kB
Tags Source
SHA-256 checksum
How to use checksums
957856a47fbdc9100fd91381de56e797382d95e0654ec59693717005a095e222
BLAKE2b-256 checksum
How to use checksums
df18cbd5170d9b23d3da7fb92f9397533fe3e74f327f69c3be7c456a2db515f1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 22, 2026.

Transparency log

Release files / ormkit-0.1.5-cp312-cp312-win_amd64.whl

Download URL ormkit-0.1.5-cp312-cp312-win_amd64.whl
Size 1.5 MB
Tags CPython 3.12 Windows x86-64
SHA-256 checksum
How to use checksums
852785798c2b7622f24c2c1431a09593519b7850d3249da3c0e1c06a3840e831
BLAKE2b-256 checksum
How to use checksums
0eab15d6371211f174d301884e9609ac870cc17701e352f2f928f12f10adcf0e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 22, 2026.

Transparency log

Release files / ormkit-0.1.5-cp312-cp312-macosx_11_0_arm64.whl

Download URL ormkit-0.1.5-cp312-cp312-macosx_11_0_arm64.whl
Size 1.5 MB
Tags CPython 3.12 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
aade12362d9b8a6466f07a545b69941860dbde382788fdf2b8e4661545facfe1
BLAKE2b-256 checksum
How to use checksums
1e2174dec9758177064d3d1609300bce2569a732dbbec2cb574b1b8c8833bd00
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 22, 2026.

Transparency log

Release files / ormkit-0.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL ormkit-0.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 1.7 MB
Tags CPython 3.8 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
ecbdde65a8537b408592b2261e1e7b5460d1c38b758e9658a70b1474b64ee05c
BLAKE2b-256 checksum
How to use checksums
a3ab2a2f2554666a23c0b329ddd019cdcac3d0e254a7a1e6969ff7e78cb171b8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Feb 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.5 This release

4 release files

0.1.4

4 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page