Skip to main content

ORMX (Python)

Pydantic-native async ORM for Postgres, with a Rust core for the hot path and a real migration engine.

The headline trick: one class plays three roles — DB persistence object, framework request body, OpenAPI response model — without a translation layer. Drop into Rivex, FastAPI, Litestar, anything that knows Pydantic.

from rivex import Depends, Rivex
from ormx import Field, Model, TransactionMiddleware, connect, disconnect, get_db


class User(Model):
    __tablename__ = "users"
    id: int = Field(primary_key=True)
    email: str = Field(unique=True, db_index=True, pattern=r"^[^@\s]+@[^@\s]+$")
    name: str = Field(min_length=1, max_length=100)
    age: int = Field(ge=0, le=120)


app = Rivex()
app.add_middleware(TransactionMiddleware)


@app.on_event("startup")
async def init(): await connect("postgres://localhost/myapp")

@app.on_event("shutdown")
async def close(): await disconnect()


@app.get("/users/{id}", response_model=User)
async def get_user(id: int, db=Depends(get_db)):
    return await User.get(id)


@app.post("/users", response_model=User)
async def create_user(user: User, db=Depends(get_db)):
    return await user.save()

That's the entire stack.

Status

4.4.2 — stable. 345 tests passing.

Feature Status
Pydantic-native Model (every constraint, alias, validator)
Framework-agnostic TransactionMiddleware
Async-generator get_db dependency
Rust transaction API (Engine.begin() / tx.commit() / tx.rollback())
Multi-statement DDL via simple-query protocol
One-to-many eager loading via .include()
Migration engine — CREATE/DROP TABLE, ADD/DROP COLUMN, ALTER COLUMN TYPE, indexes, foreign keys with ON DELETE/ON UPDATE, multi-schema, NULL ↔ NOT NULL
Sharding — hash / range / geo / list / custom routing
Multi-schema (__schema__ on Model)
Query builder — where / order_by / limit / offset / select / distinct / group_by / having / count / exists / scalar / values / update / delete / stream
Streaming reads via server-side cursor (.stream(chunk_size=))
Model.refresh() for re-reading rows touched externally
BYTEA / bytes round-trip ✅ (since v3.0.2)
Many-to-many eager loading 🚧
Beyond Postgres (MySQL / SQLite drivers) 🚧
Composite (multi-column) indexes + foreign keys 🚧

Install

pip install ormx-py

The PyPI distribution is ormx-py — the ormx and pyormx names there belong to unrelated packages. The import is ormx.

The Rust core is compiled into the wheel (vendored at vendor/ormx-core, tracking core.ormx.vexarr.com), with abi3 wheels for Linux (x86_64 + aarch64), macOS (Intel + Apple Silicon), and Windows — one pip install, no separate core package. Source builds need rustc 1.75+ and a submodule-initialized checkout (git clone --recurse-submodules). See RELEASING.md for the full distribution story.

Querying — the cookbook

ORMX query builders are chainable. Every chain method returns self; terminal methods (all, first, count, exists, scalar, values, update, delete, stream) execute the query and return.

# Basic WHERE
users = await User.query().where(User.age > 18).all()

# Chained — AND
adults = await (
    User.query()
    .where(User.age > 18)
    .where(User.email.like("%@example.com"))
    .order_by("name")
    .limit(50)
    .all()
)

# Count / exists / scalar
total = await User.query().count()
has_admin = await User.query().where(User.email == "admin@x.com").exists()

# Aggregates with GROUP BY + HAVING (HAVING is a raw SQL fragment)
buckets = await (
    User.query()
    .select("age")
    .group_by("age")
    .having("count(*) > 5")
    .all(raw=True)
)
# → [{"age": 30}, {"age": 31}, ...]

# DISTINCT and DISTINCT ON
unique_ages = await User.query().select("age").distinct().values("age")
latest_per_user = await (
    Event.query()
    .order_by("user_id").order_by("created_at", "desc")
    .distinct("user_id")
    .all()
)

# Bulk update / delete (skip per-row hooks)
n = await User.query().where(User.age < 13).update(age=13)
n = await User.query().where(User.deleted_at.is_not_null()).delete()

# Streaming — memory-bounded, server-side cursor
async for user in User.query().where(User.age > 50).stream(chunk_size=1000):
    process(user)

# Refresh a stale instance after an external write
await user.refresh()  # re-reads by PK

Relationships and eager loading

class User(Model):
    __tablename__ = "users"
    id: int = Field(primary_key=True)
    posts = Relationship("Post", back_populates="user")

class Post(Model):
    __tablename__ = "posts"
    id: int = Field(primary_key=True)
    user_id: int = ForeignKey("users.id", on_delete="CASCADE")
    title: str = Field(db_index=True)
    user = Relationship("User", back_populates="posts")


# One IN-query per included relation — no N+1.
users = await User.query().include("posts").all()
for u in users:
    print(u.name, [p.title for p in u.posts])

Transactions

Three flavours, pick what fits:

# 1. Explicit context manager
async with ormx.transaction():
    await User.create(name="alice")
    await Order.create(user_id=1, total=10)
# Rollback automatic on exception; commit on clean exit.

# 2. Unit-of-Work (batched flush at the end)
async with ormx.uow_session():
    user = User(id=1, name="alice", age=30)
    user.age = 31         # implicitly dirty
    await user.save()      # flushed at context exit, all in one tx

# 3. Per-request middleware (the most common shape)
app.add_middleware(TransactionMiddleware)
# Every handler wrapped automatically.
# Default: rollback on 5xx responses + raised exceptions; configurable.

Migrations

# Diff models vs DB, write a new timestamped migration file
ormx makemigrations

# Apply unapplied migrations in order
ormx migrate

# Roll back the most recently applied one
ormx migrate --rollback

# Drop orphan tables (off by default — safety against accidental data loss)
ormx makemigrations --allow-drop-tables

State lives in an ormx_migrations table the runner creates on first use. Each migration runs in its own transaction; partial-failure rolls back cleanly.

Detects: CREATE/DROP TABLE, ADD/DROP COLUMN, ALTER COLUMN TYPE (with USING), CREATE/DROP INDEX, ADD/DROP FOREIGN KEY, SET/DROP NOT NULL, CREATE SCHEMA for non-public-schema models. Multi-statement DDL + PL/pgSQL with dollar-quoted bodies work via the Postgres simple-query protocol — executor.execute_many(sql).

Multi-schema

class AuditEvent(Model):
    __tablename__ = "events"
    __schema__ = "audit"   # ← non-public schema; CREATE SCHEMA emitted automatically
    id: int = Field(primary_key=True)
    event_type: str

# Cross-schema FK
class AuditRef(Model):
    __tablename__ = "refs"
    event_id: int = ForeignKey("audit.events.id")

Sharding

import ormx_core
from ormx import set_router
from ormx.sharding import Router, HashSharding

# Connect each shard
await ormx.connect("postgres://shard0/...", shard_name="shard0", is_default=True)
await ormx.connect("postgres://shard1/...", shard_name="shard1")

# Install a routing strategy
set_router(Router(shards=["shard0", "shard1"], strategy=HashSharding()))

# Pass ``sharding_value`` and ORMX routes to the right shard
await User.query().where(User.id == 42).first(sharding_value=42)

Hash / range / geo / list / custom routing all supported via ormx_core.ShardingConfig.

Settings management

ORMX itself doesn't ship one — use pydantic-settings (standard) or write a small helper. Combined with Rivex's Settings base class:

from rivex import Settings

class Config(Settings):
    database_url: str
    debug: bool = False

cfg = Config.from_env()
await ormx.connect(cfg.database_url)

Upgrading from 2.x

See MIGRATION-3.0.md. TL;DR: type annotations on fields are now required (Pydantic uses them); ormx.fastapi was replaced by framework-agnostic primitives (TransactionMiddleware + get_db work with FastAPI, Rivex, Litestar — anything that knows async-generator dependencies).

Examples

Documentation

Full docs at docs/mkdocs serve to read locally, auto-publishes to https://ormx.dev/docs via the docs workflow.

License

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ormx_py-4.4.3-cp37-abi3-macosx_11_0_arm64.whl (4.5 MB view details)

Uploaded CPython 3.7+macOS 11.0+ ARM64

File details

Details for the file ormx_py-4.4.3-cp37-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for ormx_py-4.4.3-cp37-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 2df727ed66cfb4604d7fa92ee860b6f39b282125fcfaa4237dc403239eb28218
MD5 d8d76ebf170715dabe677a98ff217270
BLAKE2b-256 707ce2be453b794a7f10466baa5393da57980f89999e537c3f698cc09704aca6

See more details on using hashes here.

Release history Release notifications | RSS feed

4.6.0

4 files

4.5.2

1 file

4.5.1

1 file

4.5.0

1 file

This release

4.4.3 This release

1 file

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