What's new in 4.3¶
Minor release. No breaking changes vs 4.2 — every addition is opt-in.
Bug fixes (release-blocker round)¶
Manager.upsert(returning=True)previously passed Field objects tobulk_create; now uses field names + skips M2M.InboxRecorddocstring requiresMeta.unique_together = [("message_id", "handler_name")]on the concrete subclass so the DB serialises concurrent duplicates.Saga.run()inside an outeratomic()logs a WARNING — step commits become savepoints and lose durability if the outer transaction rolls back.anonymize_modeluses cursor pagination (pk__gt=last_pk) instead of offset — O(N) on million-row tables.two_phase_commitrejects nestedatomic()blocks with a clear RuntimeError.@temporaldocstring warns that bulk_create / bulk_update / queryset.update / queryset.delete bypass post_save / post_delete and miss the temporal mirror.
Tier 1 — strategic¶
Cross-vendor UPSERT¶
Model.objects.upsert(
[Model(slug="a", score=1), Model(slug="b", score=2)],
unique_fields=["slug"],
)
Sugar over bulk_create(update_conflicts=True). Defaults
update_fields to every non-PK / non-unique column.
Temporal tables¶
from dorm.contrib.temporal import temporal, as_of
@temporal
class Article(dorm.Model):
title = dorm.CharField(max_length=200)
snapshot = as_of(Article, t) # rows valid at time t
history = Article.history.filter(pk=42) # every version
Sibling <Model>Temporal table mirrors every write with
valid_from / valid_to / operation columns.
dorm.contrib.tasks — Celery-lite¶
from dorm.contrib.tasks import TaskQueue, task
queue = TaskQueue(model=BackgroundTask, channel="tasks")
@task(queue, name="send-welcome")
def send_welcome(user_id: int) -> None: ...
with dorm.transaction.atomic():
user = User.objects.create(...)
send_welcome.delay(user.id)
# Worker process:
queue.run()
Built on outbox + LISTEN/NOTIFY. Polling fallback when LISTEN/NOTIFY isn't available.
Tier 2 — extra field types¶
from dorm.contrib.extra_fields import (
MoneyField, SemverField, PhoneField, ColorField, JSONSchemaField
)
MoneyField(currency="EUR")— Decimal + ISO-4217 currency.SemverField()— SemVer 2.0.0 grammar.PhoneField()— E.164.ColorField()—#RRGGBB/#RRGGBBAA.JSONSchemaField(schema={...})—JSONField+ per-assignment JSON Schema validation (requiresjsonschema).
Tier 3 — async/sync ergonomics¶
SyncDataLoader¶
loader = SyncDataLoader(lambda ids: Author.objects.in_bulk(ids))
for book in books:
loader.load(book.author_id)
loader.flush()
for book in books:
author = loader.load(book.author_id)
BackgroundTasks¶
from dorm.contrib.background import BackgroundTasks
bg = BackgroundTasks(concurrency=8)
bg.add(send_email, user.email)
bg.add(warm_cache)
await bg.run()
Tier 4 — distributed transactions¶
Saga¶
from dorm.contrib.saga import Saga, Step
saga = Saga(steps=[
Step("reserve_inventory", reserve, release),
Step("charge_card", charge, refund),
Step("ship", ship, cancel_shipping),
])
run = saga.run({"order_id": 42})
if not run.ok:
log.error("saga failed at %s, compensated %s",
run.failure[0], run.compensated)
Two-phase commit (PG)¶
from dorm.contrib.two_phase import two_phase_commit
with two_phase_commit(["primary", "warehouse"]) as txn:
txn.execute("primary", "INSERT INTO orders ...")
txn.execute("warehouse", "UPDATE stock ...")
Inbox (exactly-once consumer)¶
from dorm.contrib.inbox import InboxRecord, idempotent
class Inbox(InboxRecord):
class Meta:
db_table = "inbox"
@idempotent(Inbox)
def handle_payment(message_id: str, payload: dict) -> None: ...
Tier 5 — observability / perf¶
- Plan-drift
history(tag=None)+clear_history— bounded ring (50/tag). dorm_pool_warmup_seconds{alias}Prometheus gauge (auto-recorded bywarmup_pool).RowCache(Model, maxsize=...)— per-model LRU with auto-invalidation on save/delete.suggest_fix(template, model_hint=...)— N+1 detector returns the specificselect_related/prefetch_relatedrecommendation.
Tier 6 — DX¶
Scaffolds settings.py, app/__init__.py, app/models.py and app/main.py ready to run.
Tier 7 — sugar¶
F("nick").coalesce("anon")
# ↓ same as Coalesce(F("nick"), Value("anon"))
subtree_filter(Node, root_pk=1, where_sql="is_active = ?", where_params=[True])
Tier 8 — security¶
from dorm.contrib.anonymizer import anonymize_model
anonymize_model(User, {
"email": "random_email",
"phone": "random_phone",
"notes": "redact",
"credit_card": lambda v: "•••• •••• •••• ••••",
})
from dorm.contrib.auth.tokens import rotate_short_lived_token
new_tok, old_tok = rotate_short_lived_token(current_token)
# Persist new + leave old active during the grace window.
from dorm.db.utils import add_sensitive_pattern, reset_sensitive_patterns
add_sensitive_pattern("ssn", "credit_card", "iban")
Tier 2 expansion — TaskQueue scheduled / cron / priorities¶
from dorm.contrib.tasks import TaskQueue, task, run_cron_tick
queue = TaskQueue(model=BackgroundTask, channel="tasks", max_attempts=5)
@task(queue, name="reindex", priority=5)
def reindex(): ...
@task(queue, name="nightly", cron="0 3 * * *")
def nightly_job(): ...
# Schedule delayed work:
reindex.delay(eta=datetime.utcnow() + timedelta(minutes=10))
reindex.delay(delay_seconds=30, priority=1)
# Cron tick — wire to APScheduler / external cron / dorm worker:
run_cron_tick(queue)
Helpers:
retry_with_backoff(queue, event)— exponential ETA for relay implementations.dead_letters(queue)— queryset overstatus='dead'.
Tier 3 expansion — Migration linter v2 + new ops¶
New codes:
- DORM-M005 — operation is irreversible (
reversible=False). - DORM-M006 — destructive RunSQL (
DROP TABLE/DROP COLUMN/TRUNCATE) with emptyreverse_sql.
New operations:
from dorm.migrations.operations import (
AddCheckConstraintOnline,
SeederMigration,
)
operations = [
# PG: ADD CONSTRAINT NOT VALID + VALIDATE CONSTRAINT — no
# ACCESS EXCLUSIVE rewrite scan.
AddCheckConstraintOnline("orders", "chk_pos_amount", "amount > 0"),
# Seed reference data inside a migration:
SeederMigration(
[
{"model": "shop.Currency", "fields": {"code": "USD", "name": "US Dollar"}},
{"model": "shop.Currency", "fields": {"code": "EUR", "name": "Euro"}},
],
),
]
Tier 4 expansion — DX¶
Tier 6 expansion — new field types¶
from dorm.contrib.extra_fields import (
IPRangeField, TimezoneField, PathField,
PercentageField, CountryField, autoslug,
)
class User(dorm.Model):
# UUIDv7: time-ordered, PK-friendly.
id = dorm.UUIDField(primary_key=True, version=7)
email = dorm.EmailField(pii=True)
network = IPRangeField(null=True) # "10.0.0.0/24"
tz = TimezoneField(default="UTC")
avatar_path = PathField()
completion = PercentageField() # [0, 100]
country = CountryField() # "ES", "US", ...
slug = dorm.SlugField(default=autoslug("name"))
Tier 7 expansion — permissions DSL + rate limiting¶
from dorm.contrib.permissions import requires, requires_any
@requires("article.edit", scope="article")
def edit(user, article, payload): ...
@requires_any("admin.read", "audit.read")
def view_audit(user): ...
from dorm.contrib.rate_limit import TokenBucket, rate_limited
bucket = TokenBucket(rate_per_second=10, burst=20)
@rate_limited(bucket, key=lambda req: req.ip)
def signup(req): ...
Tier 8 — concurrency primitives¶
from dorm.contrib.concurrency import (
named_lock, SerializableSnapshot, with_optimistic_lock,
)
# Distributed mutex (PG advisory lock; in-proc fallback elsewhere).
with named_lock("nightly-report"):
run_nightly_report()
# SERIALIZABLE block with auto-retry on 40001.
SerializableSnapshot(max_attempts=5).run(transfer_money)
# Version-column optimistic concurrency control.
class Account(dorm.Model):
balance = dorm.DecimalField(max_digits=12, decimal_places=2)
version = dorm.IntegerField(default=0)
with_optimistic_lock(account) # raises OptimisticLockError if stale
Tier 9 — hybrid vector + FTS rerank¶
from dorm.contrib.pgvector.reranker import rerank
results = rerank(
Document,
vector_field="embedding",
text_field_tsv="search_vector",
query_vector=embed("machine learning"),
query_text="machine learning",
candidates=200,
weight_vector=0.7,
)
Tier 11 — active/passive router¶
from dorm.contrib.active_passive import ActivePassiveRouter
DATABASE_ROUTERS = [
ActivePassiveRouter(aliases=["node_a", "node_b"], probe_seconds=10.0),
]
Probes pg_is_in_recovery() per alias on a cached schedule;
writes flow to the discovered primary, reads to replicas.
Also: await awarmup_pool(target=5) for ASGI startup hooks.
Tier 12 — serialization helpers¶
from dorm.contrib.serializers import (
stream_msgpack, avro_schema_for, openapi_schema_for,
)
# Streaming MessagePack (requires `pip install msgpack`):
for chunk in stream_msgpack(Article.objects.iterator()):
yield chunk
avro_schema = avro_schema_for(Article)
openapi_components = {
"schemas": {
"Article": openapi_schema_for(Article),
}
}
Tier 13 expansion — sugar¶
from dorm.expressions import F
# range predicate as F method
Person.objects.filter(F("age").between(18, 65))
# exists-first variant of get_or_create
exists, user = User.objects.exists_or_create(
email="x@y", defaults={"name": "X"}
)
Tier 14 — slow-transaction detector¶
Every atomic() block longer than 500 ms emits a WARNING on
dorm.contrib.slow_tx and attaches a dorm.slow_tx event to
the active OTel span.
Deferred to v4.4¶
- MSSQL / ClickHouse / Snowflake / Spanner backends.
- Composite Foreign Keys (query compiler refactor).
- TimescaleDB hypertable helpers.
dorm shell --notebook(Jupyter kernel).pytest-djanormfactories (sibling package release).- Migration squash preserving
RunPython(data-ops risk).
Upgrading 4.2 → 4.3 needs no code changes.