v3.1.0
Minor release. Closes the Django-parity gaps that 3.0's smoke
testing surfaced and adds runtime helpers we were missing. No
breaking changes vs 3.0: every addition is opt-in or zero-cost
when unused. Pool-teardown race fix (CI worker SIGSEGV under
pytest -n N on Python 3.14) lands here too — production users
running long-lived async pools see the same force_close_sync
ordering improvement.
Added — reverse-FK / reverse-O2O / M2M traversal in lookups
Author.objects.filter(book_set__title="x")now emits a
LEFT OUTER JOIN books ON books.author_id = authors.idand
filters bybooks.title. Same shape Django produces. Works
for the default<lower>_setaccessor, customrelated_name,
reverse-O2O accessors, and M2M descriptors. Reverse-FK was a
known-broken gap in 3.0 (smoke marked itskip); the lookup
resolver now walks the descriptor chain the same way forward
FK lookups already did.Author.objects.annotate(book_count=Count("book_set"))works
end-to-end. The aggregate threads through the new
query=kwarg on :meth:Aggregate.as_sqlso the column
resolves to the joined target's pk; the annotation pipeline
auto-emitsGROUP BY <outer columns>when an aggregate
forced a JOIN, so PG no longer rejects the query with
"column X must appear in GROUP BY".- The same path handles M2M (
Article.objects.filter(tags__name="x"))
end-to-end — two joins (outer → junction → target) emitted in
the right order, bothLEFT OUTERso a parent without
children stays in the result set when the user explicitly
asks for it.
Added — JSON path traversal in lookups
filter(jsonfield__nested__key="x")now emits the vendor's
JSON-path operator instead of raisingFieldDoesNotExist:
PG usescol #>> '{nested,key}'(text result; pair with
Castfor typed comparisons), SQLite emits
json_extract(col, '$.nested.key'). Multi-level paths work
on both backends; path components are validated as identifiers
before splicing so user input never reaches the literal.- 3.0 surfaced a
FieldDoesNotExistfor any sub-key traversal
on JSONField (better than 2.x's silent-empty-results bug, but
still a feature gap). 3.1 fills it in.
Added — Manager.using(alias) shortcut
Model.objects.using("replica")returns a queryset bound to
alias in one call — same shape Django exposes. Equivalent to
Model.objects.get_queryset().using(alias)/ the existing
Manager.db_manager(alias).all(). Smoke surfaced this gap
in 3.0.
Added — Field(db_default=…)
- New keyword argument on every Field. Lands in the column DDL
asDEFAULT <literal>, distinct fromdefault=(which
only fires when the PythonModelconstructor doesn't see
a value). Both can coexist —defaultwins on Python writes,
db_defaultcovers raw SQL inserts and the
CREATE TABLEshape that downstream tools / DBAs read. - Accepts Python literals (rendered via the field's
get_db_prep_value) or :class:dorm.expressions.RawSQLfor
vendor-specific server-side defaults:RawSQL("now()"),
RawSQL("gen_random_uuid()"), sequence calls. The literal
is spliced verbatim, so the caller is responsible for the
vendor compatibility of the SQL fragment.
Added — extended window-function family
- :class:
NthValue:NTH_VALUE(expr, n) OVER (...)— value at
the n-th row of the window frame (1-indexed). The integer
nis rendered inline (not as a bound parameter) so PG's
type inference doesn't rejectunknowntypes in
nth_value(int, $1). - :class:
PercentRank:PERCENT_RANK() OVER (...)— relative
rank in[0, 1]of each row within its partition. - :class:
CumeDist:CUME_DIST() OVER (...)— cumulative
distribution in(0, 1]. - All three exported from :mod:
dormand :mod:dorm.functions.
Fixed — PG async pool teardown race (SIGSEGV under pytest -n N)
- :meth:
PostgreSQLAsyncWrapper.force_close_syncpreviously
scheduledpool.close()on the original loop before
draining the libpq sockets synchronously. Underpytest -n 2
on Python 3.14 + FastAPITestClientthe two paths could
reach the samepgconnconcurrently — the C-level
pgconn.finish()ran while the asyncpool.close()
coroutine was still iterating, dereferencing freed memory and
taking the worker down with a SIGSEGV. Reorder: mark the pool
closed first, drainpgconn.finish()for every idle conn,
clear the deque, then schedule the async close (with a
fut.result(timeout=0.5)so the coroutine has a chance to
finish on the same loop iteration).
Fixed — Settings.__getattr__ raises AttributeError for
unknown-but-configured settings
getattr(settings, name, default)now returns default for
missing settings after :func:dorm.configureran. Previously
__getattr__raised :class:ImproperlyConfiguredregardless,
which only matchesAttributeErrorfor the default-fallback
shape thatgetattrlooks for — so callers like
:mod:dorm.contrib.encrypted._resolve_keys(which probes both
FIELD_ENCRYPTION_KEYandFIELD_ENCRYPTION_KEYSvia
getattr-with-default) crashed when only one of the two
was set. The not-configured case still raises
ImproperlyConfiguredso missing-bootstrap errors are loud.
Fixed — EncryptedFieldMixin was a no-op (security regression)
- The mixin overrode
get_prep_value(Django convention) but
dorm's INSERT / UPDATE pipeline calls
get_db_prep_value. The encryption hook was silently
bypassed and plaintext landed on disk. Fixed by adding the
matchingget_db_prep_valueoverride that routes through
_encryptbefore the bound parameter reaches the cursor.
Audit any fields you defined usingEncryptedCharField/
EncryptedTextFieldagainst rows written under 3.0 — they
may need re-encryption. The fix landed mid-3.0 patch line but
is documented here for visibility.
Fixed — registry alias for Meta.app_label override
_model_registrynow stores under both the module-derived
app label and theMeta.app_labeloverride (when they
differ — e.g.dorm.contrib.auth.Userandauth.User).
Smoke surfaced this whendorm makemigrationscouldn't find
contrib-app models. The CLI also gained a
_resolve_app_modulereverse-lookup sodorm migrate auth
anddorm migrate dorm.contrib.authboth work.
Fixed — CombinedExpression.as_sql signature mismatch
F("x") + 1inannotate(...)used to crash with
TypeError: unexpected keyword argument 'model'— the
annotation pipeline calls every node with
(table_alias, model=…, connection=…), but
CombinedExpression.as_sqlwas still on the older
(compiler, connection)form. Both signatures now share
the F-style shape via a_compile_operandhelper.
Fixed — CreateModel auto-emits M2M junction tables
- 3.0
CreateModel.database_forwardsskipped M2M fields
entirely (a long-standing bug masked by the test suite that
built junctions by hand in conftest). 3.1 emits the implicit
junction (<table>_<fname>) with FKs to the source + target
pks plus aUNIQUE (src_id, tgt_id)so duplicate
associations error at the DB. Reverse drop in
database_backwardsremoves the junction before the parent
table.
Fixed — CLI threads Meta.app_label through every subcommand
- Resolver helper :func:
_resolve_app_labelmaps an INSTALLED_APPS
entry to the actualMeta.app_labeldeclared by its models.
Threaded throughmakemigrations,migrate,
squashmigrations,showmigrationsanddbcheckso
contrib-app migrations land in the recorder under the right
label. Reverse helper :func:_resolve_app_modulemaps the
short label back to the package path so
dorm migrate authresolves to
dorm/contrib/auth/migrations/for the file lookup.
Fixed — CLI forwards every uppercase setting
_load_settingspreviously only forwardedDATABASESand
INSTALLED_APPS; everything else (SECRET_KEY,
USE_TZ,CACHES, every memoised knob) silently never
reached :data:dorm.conf.settings. Now every uppercase
top-level attribute on the settings module is forwarded
through :func:dorm.configure.
Tests
tests/test_v3_1_release.py— reverse-FK filter / Count
aggregate, distinct dedup, JSON path traversal, manager-level
using(), db_default DDL emission (literal + RawSQL forms),
window function extras (NthValue / PercentRank / CumeDist),
reverse-O2O filter.tests/test_smoke_coverage.py,tests/test_smoke_extended.py,
tests/test_smoke_remaining.py— 184 end-to-end smoke
cases promoted fromexample/smoke.pycovering every
public-API contract that lacked runtime tests in 3.0.