What's new in 4.0¶
Major release. Direct jump from 3.3 — everything that was scheduled
for 3.4 (framework-agnostic helpers, PG helpers, sibling packages
djanorm-mypy and pytest-djanorm) ships in this release together
with the seven headline features that justify the major bump.
1. Expanded full-text search¶
On top of the existing SearchVector / SearchQuery /
SearchRank / SearchHeadline:
from dorm.search import (
TrigramSimilarity, TrigramWordSimilarity, search_index,
)
# Trigram-based fuzzy ranking (requires pg_trgm extension)
qs = (
Author.objects
.annotate(score=TrigramSimilarity("name", "alise"))
.order_by("-score")
)
# Functional GIN index for __search lookups
from dorm.migrations.operations import RunSQL
operations = [
RunSQL(
search_index("articles", "title", "body"),
reverse_sql='DROP INDEX IF EXISTS ix_articles_search'
),
]
2. Online (zero-downtime) migrations¶
from dorm.migrations.operations import (
AddFieldOnline, BackfillBatch, SetNotNullOnline,
)
operations = [
AddFieldOnline(
"Order",
"currency",
dorm.CharField(max_length=3, null=False, default="USD"),
),
BackfillBatch(
table="orders",
update_sql=(
'UPDATE "orders" SET "currency" = \'USD\' '
'WHERE "id" BETWEEN %s AND %s AND "currency" IS NULL'
),
batch_size=10_000,
sleep_seconds=0.05,
),
SetNotNullOnline("Order", "currency"),
]
PG ≥ 12 path: never rewrites the table.
3. Enriched OpenTelemetry instrumentation¶
Every span carries SemConv 1.20+ attributes: db.operation,
db.sql.table / db.collection.name, db.dorm.alias. Span
name uses "<OPERATION> <table>" so tracing UIs sort by hot
tables out of the box.
4. Recursive CTEs (tree walks)¶
from dorm.tree import descendants, ancestors, descendants_cte
rows = descendants(Category, parent_field="parent_id", root_pk=42)
# Or compose into a manual queryset:
cte = descendants_cte(
Category,
parent_field="parent_id",
root_pk=42,
cycle_field="path", # cycle protection (PG-only)
)
qs = Category.objects.with_cte(descendants=cte).raw(
'SELECT id, name FROM descendants ORDER BY id'
)
5. Row-level multi-tenancy¶
import dorm
from dorm.contrib.tenants_row import TenantModel, current_tenant
class Note(TenantModel):
title = dorm.CharField(max_length=200)
with current_tenant(request.user.tenant_id):
Note.objects.create(title="hi") # tenant_id auto-filled
notes = list(Note.objects.all()) # auto-filtered
all_notes = list(Note.unscoped.all()) # escape hatch
Works on every backend (PG, MySQL, SQLite, libsql, DuckDB).
NoActiveTenantError when no tenant is active — no silent
fallback to "every tenant".
6. DuckDB backend¶
The first OLAP/analytics backend in the family.
import dorm
dorm.configure(
DATABASES={"default": {"ENGINE": "duckdb", "NAME": "analytics.duckdb"}},
INSTALLED_APPS=["dashboards"],
)
Capabilities:
- Atomic transactions (no SAVEPOINT — nested atomic() degrades
to a no-op boundary; outer rollback discards everything).
- Streaming iterator via execute_streaming.
- information_schema introspection (dorm diff works).
- Async wrapper runs sync calls in a thread executor.
Install: pip install 'djanorm[duckdb]'.
7. HStoreField + native PG ENUM¶
import enum, dorm
class Status(enum.Enum):
ACTIVE = "active"
ARCHIVED = "archived"
class Article(dorm.Model):
metadata = dorm.HStoreField(null=True, blank=True)
status = dorm.EnumField(Status, native=True, type_name="article_status")
Migrations:
from dorm.migrations.operations import (
CreatePGEnum, DropPGEnum, AddPGEnumValue, CreateModel, RunSQL,
)
operations = [
RunSQL("CREATE EXTENSION IF NOT EXISTS hstore",
reverse_sql="DROP EXTENSION IF EXISTS hstore"),
CreatePGEnum("article_status", ["active", "archived"]),
CreateModel("Article", fields=[...]),
]
# Later, to append a value:
operations = [AddPGEnumValue("article_status", "deleted")]
HStoreField falls back to JSON-encoded TEXT on SQLite.
EnumField(native=True) falls back to VARCHAR on non-PG backends.
Sibling packages¶
See sibling packages:
djanorm-mypy— mypy plugin validatingfilter()kwargs, lookup suffixes, synthesisingpk/id.pytest-djanorm— fixturestransactional_db,atransactional_db,pg_container,nplusone_guard.
Extracted to standalone packages so the main wheel never pulls dev tooling at runtime.
Migrating from 3.3.0¶
Zero mandatory changes. Every addition is opt-in:
pip install --upgrade djanorm— runtime stays identical.- For DuckDB:
pip install 'djanorm[duckdb]'. - For mypy/pytest tooling:
pip install djanorm-mypy pytest-djanorm. - For v4 helpers:
from dorm.tree import ...,from dorm.contrib.tenants_row import ..., etc.