v4.2.0
Minor release. No breaking changes vs 4.1 — every addition is opt-in.
Added — Tier 1 (DX + observability)
- PG advisory locks —
dorm.contrib.advisory.advisory_lock/
try_advisory_lock(session-scoped),
advisory_xact_lock/try_advisory_xact_lock
(transaction-scoped), plus async equivalents (aadvisory_lock,
atry_advisory_lock). Acceptint/str(deterministic
blake2b-8 hash) /(int, int)tuple keys. - Slow-query EXPLAIN auto-collect —
SLOW_QUERY_EXPLAIN
setting (envDORM_SLOW_QUERY_EXPLAIN). When True every query
exceedingSLOW_QUERY_MSis re-explained via
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)(PG),
EXPLAIN QUERY PLAN(SQLite / libsql) or
EXPLAIN ANALYZE(MySQL / DuckDB); the plan is logged at
WARNING ondorm.db.slow_explainand attached to the current
OTel span as adorm.slow_query.planevent. Re-entrancy guard
blocks the EXPLAIN itself from triggering further plan captures. dorm sqlmigratenow captures real SQL via the dry-run
connection, replacing the previousop.describe()dump.
Added — Tier 2 (operational)
dorm.contrib.querystats— per-SQL-template aggregation
built onpost_query.collector().enable()opts in;
render_text()emits Prometheus lines (dorm_template_count,
dorm_template_total_ms,dorm_template_p50_ms/
p95_ms/p99_ms);render_json()returns dicts for
custom dashboards. Bounded reservoir keeps memory predictable.- OTel log-correlation filter —
dorm.contrib.otel.TraceContextLogFilterstamps every
LogRecordwithotel_trace_id/otel_span_id(32-hex
/ 16-hex;"-"when no span active). Wire via
install_log_correlation([logger_name, ...])— idempotent, no-op
when OTel isn't installed. AlterColumnTypeOnlinemigration op — PostgreSQL-only,
wrapsALTER COLUMN ... TYPEin a short transaction with
SET LOCAL lock_timeoutso the operation aborts under
contention instead of blocking writers. Reversible when
old_type is supplied.dorm migrate --dry-run <target>— the previously
unsupported combination of--dry-runand a rollback target
now prints the exactDROP/ALTERstatements the rollback
would issue.MigrationExecutor.rollback(..., dry_run=True)
returns the captured(sql, params)tuples.
Added — Tier 3 (security + ops maturity)
- EncryptedField key rotation —
dorm.contrib.encrypted.rotate_encryption_keys(Model, fields=None, batch_size=500, progress=None)/
arotate_encryption_keys(...)re-encrypts every row with the
newest key in batches insideatomic(). Optionalprogress
callable is invoked after each chunk for tqdm-style hooks. - PII auto-mask in streaming —
stream_json/stream_jsonl
/stream_ndjson_prettyand async equivalents accept
mask_pii=Trueto redactpii=Truefields in flight (no-op
for plain iterables that don't carry a model). MakeTableAppendOnlymigration op — installs a trigger
that blocksUPDATE/DELETEon a table. Works on PG
(PL/pgSQL exception) and SQLite (RAISE(ABORT, ...)); MySQL /
DuckDB log a warning and skip.allow_delete=Trueblocks only
UPDATE.- Pool saturation metric + warning — Prometheus output now
carriesdorm_pool_saturation{alias}(= in_use / max_size).
settings.POOL_SATURATION_WARN(default 0.8) triggers a
WARNING log ondorm.contrib.prometheus.poolwhen crossed. - Read-replica circuit breaker —
LagAwareReadRoutergained
failure_threshold(default 3) andcooldown_seconds(default
30s). After N consecutive probe failures the router opens a
breaker per replica and skips even the lag probe for the cooldown
window.cooldown_seconds=0disables the breaker. pii_fields()LRU cache — repeated lookups (hot path in
serialisation middleware) now reuse a per-class cached tuple.
Added — Tier 4 (DX)
dorm migrations-graph --format=mermaid|dot— dumps the
migration dependency graph for visualisation (mermaid/
Graphvizdot). AST-walks every migration file so no app
import is required.dorm reset— drops every applied migration and re-applies
them from scratch. Refuses production-looking databases unless
--forceis passed.Manager.cached(timeout=...)sugar — shortcut for
Manager.get_queryset().cache(timeout=...).settings.DEBUG_NPLUSONE— setTrue/"log"/
"raise"to install a process-wide
:class:NPlusOneDetector.DEBUG_NPLUSONE_THRESHOLDoverrides
the default 10.install_debug_global()is idempotent and
returns the active detector.
Added — Tier 5 (advanced)
dorm.contrib.dataloader.DataLoader— coalesce N
concurrentawait loader.load(key)calls into one batched
fetch within the same event-loop tick. Accepts dict / iterable
/ async-iterable batch functions; configurable
max_batch_size,cache,missingsentinel, and
key_attrfor Model-instance results.dorm.contrib.plan_drift— record a baseline EXPLAIN plan
per tag, thencompare()against fresh plans.diff_text()
yields a unified diff. Volatile bits (costs, row estimates,
buffers, planning/execution time) are stripped before comparison
so the alarm only fires on structural changes.dorm.contrib.listen_notify.Broadcaster— multiplexer
that fans one LISTEN connection out to N async subscribers, each
with its own bounded queue (maxsize=100default). Replaces
the awkward "one task per channel" pattern of plain
:func:listen.
Added — Tier 6 (sugar)
F("col").apply(Func, *extra)— chainable transform
wrapping.F("name").apply(Lower).apply(Trim)reads top-down
instead of inside-out. Also available on every :class:Func
subclass.QuerySet.lookup(column=None)— sugar for
Subquery(qs.values(column)). Use as
Article.objects.filter(author__in=top10.lookup("author_id")).Manager.union_with(*others, all=False, order_by=None)—
polymorphic UNION across heterogeneous models. Each branch can
be a Manager, a QuerySet, or a(source, mapping)tuple that
renames / projects columns so the SELECT lists agree.dorm.contrib.sql_allowlist— CSP-style allow-list for
hardened deployments.install(templates, ...)captures the
list at startup; any subsequent query whose template (literals
stripped) isn't on the list raises
SQLNotAllowedError.raise_on_violation=Falseenables
log-only mode for the canary phase.
Added — Polish round (release-blocker pass)
- Auto-install of
DEBUG_NPLUSONE—dorm.configure(DEBUG_NPLUSONE=...)
now calls :func:install_debug_globalautomatically. Idempotent;
passing falsy values is a no-op. dorm version— trivial CLI that printsdjanorm <version>.dorm doctorv4.2 audits — flagsDEBUG_NPLUSONEactive
outsideDEBUG,SLOW_QUERY_EXPLAINwith too-low
SLOW_QUERY_MS,POOL_SATURATION_WARNoutside(0, 1),
LagAwareReadRouterwithcooldown_seconds=0, and missing /
permissivesql_allowlistconfiguration.querystatsembedded inprometheus.metrics_response()—
per-template gauges land in the same payload as pool / query /
cache metrics. No separate endpoint required.DataLoader.prime(key, value)+clear_all()— pre-load
values without batching; drop the whole cache.- Async plan-drift APIs — :func:
arecord_baseline/
:func:acomparefor symmetry with the sync forms. sql_allowlistcapture-mode helpers —
:func:dump_capturedwrites{allowed, rejected}JSON;
:func:load_from_filere-installs from the curated document;
:func:allowed_templatessnapshots the current allow-list.- Integration tests — real-trigger verification for
:class:MakeTableAppendOnlyand an end-to-end test of the slow-
query EXPLAIN path against a live SQLite connection.
Validated
ruff check: clean.ty check: clean.mkdocs build --strict: clean.pytest tests/: 6889 passed, 153 skipped, 0 failures (serial).