Metadata-Version: 2.4
Name: ferrum-orm
Version: 0.1.16
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Dist: pydantic>=2.0
Requires-Dist: ferrum-orm[pg] ; extra == 'all'
Requires-Dist: ferrum-orm[mysql] ; extra == 'all'
Requires-Dist: ferrum-orm[sqlite] ; extra == 'all'
Requires-Dist: ferrum-orm[mssql] ; extra == 'all'
Requires-Dist: ferrum-orm[msgpack] ; extra == 'all'
Requires-Dist: ferrum-orm[cli] ; extra == 'all'
Requires-Dist: ferrum-orm[dotenv] ; extra == 'all'
Requires-Dist: typer>=0.12 ; extra == 'cli'
Requires-Dist: rich>=13 ; extra == 'cli'
Requires-Dist: ferrum-orm[pg,cli,dotenv] ; extra == 'dev'
Requires-Dist: maturin>=1.7,<2 ; extra == 'dev'
Requires-Dist: pytest>=8 ; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
Requires-Dist: hypothesis>=6.100 ; extra == 'dev'
Requires-Dist: pytest-benchmark>=4 ; extra == 'dev'
Requires-Dist: ruff>=0.4 ; extra == 'dev'
Requires-Dist: ty>=0.0.49 ; extra == 'dev'
Requires-Dist: import-linter>=2 ; extra == 'dev'
Requires-Dist: testcontainers[postgres]>=4 ; extra == 'dev'
Requires-Dist: pip-audit>=2 ; extra == 'dev'
Requires-Dist: fastapi>=0.110 ; extra == 'dev'
Requires-Dist: python-dotenv>=1.0 ; extra == 'dotenv'
Requires-Dist: msgpack>=1.0 ; extra == 'msgpack'
Requires-Dist: aioodbc>=0.5 ; extra == 'mssql'
Requires-Dist: asyncmy>=0.2 ; extra == 'mysql'
Requires-Dist: opentelemetry-api>=1.20 ; extra == 'otel'
Requires-Dist: asyncpg>=0.29 ; extra == 'pg'
Requires-Dist: aiosqlite>=0.19 ; extra == 'sqlite'
Requires-Dist: uuid6>=2024.1 ; extra == 'uuid7'
Provides-Extra: all
Provides-Extra: cli
Provides-Extra: dev
Provides-Extra: dotenv
Provides-Extra: fastapi
Provides-Extra: msgpack
Provides-Extra: mssql
Provides-Extra: mysql
Provides-Extra: otel
Provides-Extra: pg
Provides-Extra: sqlite
Provides-Extra: uuid7
License-File: LICENSE
Summary: Next-generation async ORM for Python with a Rust-powered core
Keywords: orm,async,postgresql,mysql,sqlite,mssql,pydantic,rust,asyncpg,query-builder
Author: Ferrum Contributors
License: Apache-2.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/ferrum-orm/ferrum/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/ferrum-orm/ferrum/tree/main/docs
Project-URL: Homepage, https://github.com/ferrum-orm/ferrum
Project-URL: Issues, https://github.com/ferrum-orm/ferrum/issues
Project-URL: Repository, https://github.com/ferrum-orm/ferrum

# Ferrum

> A next-generation async ORM for Python.
> Rust-powered engine. Pydantic-native models. Django-inspired developer experience.

Ferrum is an async-first ORM designed for modern Python applications.

Built around a Rust-powered core and a Python-native API, Ferrum combines the ergonomics of Django's ORM, the type safety of Pydantic, and the performance of Rust.

## Why Ferrum?

Existing Python ORMs often force developers to choose between:

- Developer experience
- Async support
- Type safety
- Performance

Ferrum aims to provide all four.

### Goals

- Native async from day one
- Pydantic-first models
- Django-inspired ORM experience
- Rust-powered query engine
- PostgreSQL-first architecture (MySQL, SQLite, and SQL Server via optional extras)
- Type-safe query construction
- Automatic migrations
- High-performance result hydration
- Production-ready observability

---

## Quick Example

```python
from ferrum import Model


class User(Model):
    id: int
    email: str
    is_active: bool = True


user = await User.objects.create(
    conn,
    email="john@example.com",
)

# Insert from an instance (sentinel PK is dropped so the DB default runs)
draft = User(id=0, email="dana@example.com")
created = await User.objects.create(conn, draft)

# Persist one instance's changes by primary key
created.is_active = False
await User.objects.update_instance(conn, created, fields=["is_active"])

users = await (
    User.objects
    .filter(is_active=True)
    .order_by("-id")
    .limit(10)
    .all(conn)
)

async with conn.transaction() as tx:
    user = await User.objects.create(tx, email="jane@example.com")
    await AuditLog.objects.create(tx, user_id=user.id, action="signup")
```

---

## Features

### Async First

No synchronous compatibility layer.

Ferrum is designed around modern async Python applications.

```python
users = await User.objects.all(conn)
```

### Pydantic Native

Models are built directly on top of Pydantic.

```python
class User(Model):
    id: int
    email: str
```

No duplicate schema definitions.

### Field Options

`ferrum.Field()` accepts column-level constraints that are carried through to DDL:

| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `db_default` | `str \| None` | DB-side `DEFAULT` expression (e.g. `"NOW()"`, `"''"`, `"GEN_RANDOM_UUID()"`). Token casing is normalised (`now()` → `NOW()`). |
| `nullable` | `bool \| None` | Override annotation-derived nullability. Use `nullable=False` to force `NOT NULL` even on a `T \| None`-annotated field. |
| `db_index` | `bool` | Emit a `CREATE INDEX` on this column (auto-named `idx_{table}_{field}`). |
| `unique` | `bool` | Emit a `UNIQUE` constraint. |
| `db_column` | `str \| None` | Override the database column name. |
| `primary_key` | `bool` | Declare this field as the primary key. |
| `uuid_generate` | `"v4" \| "v7" \| None` | Set `GEN_RANDOM_UUID()` / `UUIDV7()` as the DB default for UUID PK columns. |
| `max_length` | `int \| None` | Emit `VARCHAR(n)` instead of `TEXT`. |
| `max_digits` / `decimal_places` | `int \| None` | Emit `NUMERIC(p,s)`. |
| `vector_dimensions` | `int \| None` | Required for `Vector` columns; emits `VECTOR(n)`. |
| `jsonb_list` | `bool` | Store `list[T]` as JSONB instead of a PostgreSQL ARRAY. |
| `generated` | `bool` | Select/hydrate the DB-generated column, but exclude it from writes. |
| `read_only` | `bool` | Select/hydrate the column, but reject explicit write assignments. |

```python
from datetime import datetime
from typing import Annotated
from ferrum import Model, Field

class Event(Model):
    id: int
    # DB-side timestamp default; Python annotation allows None but column is NOT NULL
    created_at: Annotated[datetime | None, Field(db_default="now()", nullable=False)]
    # Indexed text column
    kind: Annotated[str, Field(db_index=True, max_length=64)]
```

`makemigrations` honours these field attributes:
- `db_default` / `nullable` are written into `CREATE TABLE` and `ADD COLUMN` DDL.
- `db_index=True` emits `CREATE INDEX` after the table op.
- For **already-existing tables**, `makemigrations` now **autodiffs** index and column
  attribute changes against the state projected from prior migration files:
  - Adding `db_index=True` to an existing field → `AddIndex` op.
  - Removing `db_index=True` → `DropIndex` op.
  - Adding or changing `db_default` → `AlterColumn … SET DEFAULT …` op.
  - Removing `db_default` → `AlterColumn … DROP DEFAULT` op.
  - Changing nullability → `AlterColumn … SET / DROP NOT NULL` op (SET NOT NULL is
    classified destructive and requires `--confirm`).
  - Column type changes, renames, and drops remain manual (out of scope for v0.1).

### Django-Inspired API

Familiar query interface.

```python
users = await (
    User.objects
    .filter(email__contains="@gmail.com")
    .order_by("-created_at")
    .all(conn)
)
```

Typed aggregates and bounded PostgreSQL streaming use the same metadata allowlists
and bound-parameter compiler path:

```python
from ferrum.queryset import Aggregate

counts = await (
    Ticket.objects
    .group_by("team_id")
    .date_trunc("created_at", "day", alias="day")
    .having(total__gte=10)
    .aggregate(conn, total=Aggregate.count())
)

async with Ticket.objects.filter(active=True).stream(conn, chunk_size=500) as chunks:
    async for tickets in chunks:
        await process(tickets)
```

`stream()` preserves projection/deferred/value-queryset materialization and
deterministically releases its cursor on exhaustion, early break, cancellation, or
error. It rejects `prefetch_related()` because cross-chunk relationship batching is
not safe. Filtered `update_returning()` provides atomic compare-and-set semantics:
encode the expected state in `filter()` and treat an empty returned list as a lost race.

### First-Class IDE Support

Ferrum ships a PEP 561 `py.typed` marker, so editors and type checkers (`mypy`, `pyright`,
`ty`) resolve its inline hints out of the box. `Model.objects` is typed as
`QuerySet[YourModel]`, chaining preserves the model type, and terminals infer precise results —
no casts:

```python
users: list[User] = await User.objects.filter(is_active=True).all(conn)   # list[User]
user: User | None = await User.objects.first(conn)                        # User | None
rows: list[dict[str, Any]] = await User.objects.values("id", "email").all(conn)
ids: list[Any] = await User.objects.values_list("id", flat=True).all(conn)
```

`values()` / `values_list()` return dedicated `ValuesQuerySet`, `ValuesListQuerySet`, and
`FlatValuesListQuerySet` variants (all exported from `ferrum`) so `all()` returns
`list[dict[str, Any]]`, `list[tuple[Any, ...]]`, or `list[Any]` respectively.

### Rust-Powered Core

Performance-critical components are implemented in Rust:

- Query compilation
- SQL generation
- Result decoding
- Schema analysis
- Migration planning

This allows Ferrum to maintain a Pythonic API without sacrificing performance.

### Cross-Driver Full-Text Search

Native full-text search across PostgreSQL, MySQL, SQLite FTS5, and SQL Server — one
QuerySet API, dialect-specific SQL emit and migration DDL.

**Query modes** (filter lookups and ranking):

| Mode        | Lookup operator   | Typical use                          |
| ----------- | ----------------- | ------------------------------------ |
| `plain`     | `__match`         | Natural-language terms               |
| `phrase`    | `__match_phrase`  | Exact phrase                         |
| `websearch` | `__match_websearch` | Web-style quotes, `-` negation   |
| `boolean`   | `__match_boolean` | Boolean operators (`&`, `\|`, `!`) |

**Convenience methods:**

```python
# Filter + relevance ranking in one call
hits = await Article.objects.search(
    "python async orm", field="body", mode="websearch"
).limit(10).all(conn)

# Rank without an implicit filter
ranked = await Article.objects.rank_by("body", "rust", mode="plain").all(conn)
```

**Index declaration** — PostgreSQL uses `TSVector` columns; other drivers index base
`text` columns via `Meta.full_text_indexes`:

```python
from ferrum.models import Field, FullTextIndex

class Article(Model):
    search_vector: Annotated[TSVector, Field(fts_config="english")] | None = None
    body: str = ""

    class Meta:
        full_text_indexes = [FullTextIndex(fields=("body",), config="english")]
```

Query strings are always bound parameters; `fts_config` and index names come from
model-metadata allowlists only. See [Getting Started → Vector and full-text columns](docs/getting-started.md)
and [API Reference](docs/api-reference.md) for per-dialect DDL and operator mapping.

## Architecture

```text
┌──────────────────────────┐
│      Python API          │
│  Models / QuerySets      │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│      Ferrum Core         │
│      (Rust Engine)       │
├──────────────────────────┤
│ Query Compiler           │
│ SQL AST                  │
│ Result Decoder           │
│ Migration Planner        │
└────────────┬─────────────┘
             │
             ▼
┌──────────────────────────┐
│       PostgreSQL         │
└──────────────────────────┘
```

## Roadmap

### v0.1 (complete)

- [x] PostgreSQL support
- [x] Basic CRUD operations
- [x] Async query execution
- [x] Pydantic models
- [x] Query builder
- [x] Type-safe filters
- [x] Transactions and savepoints
- [x] Bulk operations (`bulk_create`, `bulk_update`, `bulk_delete`)
- [x] Migrations (schema diff, apply, revert, CLI)
- [x] Relationships (ForeignKey, OneToOne, ManyToMany)
- [x] pgvector KNN search and HNSW/IVFFLAT index DDL
- [x] Full-text search (cross-dialect: PostgreSQL, MySQL, SQLite FTS5, SQL Server)
- [x] Observability hooks (Tier A/B/C)
- [x] CLI (`makemigrations`, `migrate`, `revert`, `showmigrations`, `inspectdb`, `resetdb`)

### v0.2 (in progress)

- [x] Upsert API (`upsert`, `bulk_upsert` with conflict targets and `RETURNING`)
- [x] Composite primary keys
- [x] Array field types (`uuid[]`, `text[]`, scalar arrays)
- [x] JSONB operators (`__contains`, `__has_key`)
- [x] RLS / tenant session helpers (`set_config`, `tenant_session`)
- [x] `call_function` for allowlisted stored-procedure calls
- [x] Migration ops for extensions, RLS policies, and function DDL
- [x] pgvector similarity score projection (`vector_search` helper)
- [x] `Field(db_default=..., nullable=...)` — first-class DB-side defaults and nullability override
- [x] `makemigrations` autodiff for index / default / nullability changes on existing tables
- [x] Typed aggregates, filtered `UPDATE … RETURNING`, and bounded QuerySet streaming
- [x] JSONB-list/generated/read-only metadata and read-only PostgreSQL schema drift reports
- [ ] Query optimization (deferred fields, prefetch tuning)
- [ ] Advanced relationship loading

### v1.0

- [ ] Production-ready stability
- [ ] Performance benchmarking suite
- [ ] Full documentation site

## Project Status

Ferrum is currently in active development.

The API is not yet stable and breaking changes should be expected until the first public release.

## Installation

```bash
# PostgreSQL (most common)
pip install 'ferrum-orm[pg]'

# PostgreSQL + migrations CLI
pip install 'ferrum-orm[pg,cli]'

# MySQL
pip install 'ferrum-orm[mysql]'

# SQLite + migrations CLI (testing / local dev)
pip install 'ferrum-orm[sqlite,cli]'

# SQL Server (also needs a system ODBC driver, e.g. msodbcsql18)
pip install 'ferrum-orm[mssql]'

# Optional MessagePack wire format for the Python<->Rust boundary
pip install 'ferrum-orm[msgpack]'

# Everything (all drivers + CLI + dotenv)
pip install 'ferrum-orm[all]'

# Core ORM only (no database driver — install a driver extra before connecting)
pip install ferrum-orm
```

Bare `ferrum-orm` installs Pydantic and the Rust core only. Choose a driver extra
(`pg`, `mysql`, `sqlite`, or `mssql`) before calling `ferrum.connect()`.

MySQL, SQLite, and SQL Server are **thin-parity** backends: they support core CRUD
and migrations but not transactions, upsert, `bulk_update`, RLS, or pgvector
(PostgreSQL only). SQL Server connects via `aioodbc`/`pyodbc` and requires a system
ODBC driver such as `msodbcsql18`; DSNs use the `mssql://` or `sqlserver://` scheme.

### Wire format (advanced)

The Python↔Rust IR/hydration boundary defaults to JSON. Installing the `msgpack`
extra lets you switch it to MessagePack, selected via the `FERRUM_WIRE_FORMAT`
environment variable (`json` | `msgpack`) or the `[ferrum] wire_format` key in
`ferrum.toml` / `pyproject.toml`. JSON remains the default; MessagePack is opt-in.

From source, build the native extension with `maturin develop` (or `mise run dev`).

## Examples

Runnable samples live under [`examples/`](examples/):

- [`examples/simple/`](examples/simple/) — async CRUD script (no web framework)
- [`examples/migrations/`](examples/migrations/) — CLI, plan generation, apply, and forward fix-ups
- [`examples/fastapi_quickstart/`](examples/fastapi_quickstart/) — FastAPI integration

## Contributing

Contributions are welcome. Start with [`CONTRIBUTING.md`](CONTRIBUTING.md) for local setup,
scoped verification, architecture rules, and pull request expectations.

## License

Apache License 2.0

