Skip to content

Commit 6b41389

Browse files
authored
Merge pull request #866 from bckohan/master
Fix database routing bug on save()
2 parents 28994d3 + 03314a4 commit 6b41389

7 files changed

Lines changed: 222 additions & 171 deletions

File tree

.github/workflows/test.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ jobs:
3838
psycopg-version: ['psycopg2', 'psycopg3']
3939
django-version:
4040
- '4.2' # LTS April 2026
41-
- '5.1' # December 2025
4241
- '5.2' # LTS April 2028
4342
- '6.0' #
4443
exclude:
@@ -53,13 +52,9 @@ jobs:
5352

5453
- python-version: '3.14'
5554
django-version: '4.2'
56-
- python-version: '3.14'
57-
django-version: '5.1'
5855
- python-version: '3.14'
5956
django-version: '5.2'
6057

61-
- postgres-version: '12'
62-
django-version: '5.1'
6358
- postgres-version: '12'
6459
django-version: '5.2'
6560
- postgres-version: '12'
@@ -74,8 +69,6 @@ jobs:
7469

7570
- postgres-version: 'latest'
7671
django-version: '4.2'
77-
- postgres-version: 'latest'
78-
django-version: '5.1'
7972

8073
- postgres-version: '12'
8174
psycopg-version: 'psycopg3'

docs/changelog/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Changelog
22
=========
33

4+
v4.11.1 (2026-02-20)
5+
--------------------
6+
7+
* Fixed `Release 4.11 causes UPDATE query to be routed to reader DB <https://github.com/jazzband/django-polymorphic/issues/865>`_
8+
49
v4.11.0 (2026-02-05)
510
--------------------
611

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "django-polymorphic"
7-
version = "4.11.0"
7+
version = "4.11.1"
88
description = "Seamless polymorphic inheritance for Django models."
99
readme = "README.md"
1010
license = "BSD-3-Clause"
@@ -31,7 +31,6 @@ classifiers = [
3131
"Environment :: Web Environment",
3232
"Framework :: Django",
3333
"Framework :: Django :: 4.2",
34-
"Framework :: Django :: 5.1",
3534
"Framework :: Django :: 5.2",
3635
"Framework :: Django :: 6.0",
3736
"Intended Audience :: Developers",

src/polymorphic/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from typing import Final
2323

24-
VERSION: Final[str] = "4.11.0"
24+
VERSION: Final[str] = "4.11.1"
2525

2626
__title__: Final = "Django Polymorphic"
2727
__version__ = VERSION # version synonym for backwards compatibility

src/polymorphic/models.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typing import ClassVar, cast
1010

1111
from django.contrib.contenttypes.models import ContentType
12-
from django.db import models, transaction
12+
from django.db import models, router, transaction
1313
from django.db.models import Q
1414
from django.db.models.base import ModelBase
1515
from django.db.utils import DEFAULT_DB_ALIAS
@@ -127,15 +127,18 @@ def save(
127127
update_fields: Iterable[str] | None = None,
128128
) -> None:
129129
"""Calls :meth:`pre_save_polymorphic` and saves the model."""
130-
# Determine the database to use:
130+
# Determine the database to use via Django's routing infrastructure:
131131
# 1. Explicit 'using' parameter takes precedence
132-
# 2. Otherwise use self._state.db (the database the object was loaded from)
133-
# 3. Fall back to DEFAULT_DB_ALIAS
134-
# This ensures database routers are respected when no explicit database is specified
135-
if using is None:
136-
using = self._state.db or DEFAULT_DB_ALIAS
137-
138-
self.pre_save_polymorphic(using=using)
132+
# 2. Otherwise consult DATABASE_ROUTERS via router.db_for_write()
133+
# 3. The router falls back to _state.db, then DEFAULT_DB_ALIAS
134+
self.pre_save_polymorphic(
135+
using=(
136+
using
137+
or router.db_for_write(self.__class__, instance=self)
138+
or self._state.db
139+
or DEFAULT_DB_ALIAS
140+
)
141+
)
139142
return super().save(
140143
force_insert=force_insert,
141144
force_update=force_update,

src/polymorphic/tests/test_multidb.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,40 @@ def test_database_router_respected(self):
332332

333333
# Verify it wasn't saved to default database
334334
self.assertFalse(Model2B.objects.filter(pk=obj.pk).exists())
335+
336+
def test_save_respects_db_for_write_router(self):
337+
"""
338+
When a DATABASE_ROUTER routes writes to a different database than where
339+
the object was read from, save() should respect db_for_write() rather
340+
than blindly using _state.db. Regression test for issue #865.
341+
"""
342+
343+
class ReadWriteSplitRouter:
344+
"""Router that sends writes to 'default' regardless of _state.db."""
345+
346+
def db_for_read(self, model, **hints):
347+
return None
348+
349+
def db_for_write(self, model, **hints):
350+
return "default"
351+
352+
def allow_relation(self, obj1, obj2, **hints):
353+
return True
354+
355+
def allow_migrate(self, db, app_label, **hints):
356+
return True
357+
358+
# Create an object on "secondary", simulating an object loaded from a
359+
# read-replica whose _state.db points at the replica.
360+
obj = Model2B.objects.using("secondary").create(field1="test", field2="value")
361+
self.assertEqual(obj._state.db, "secondary")
362+
363+
with self.settings(DATABASE_ROUTERS=[ReadWriteSplitRouter()]):
364+
# save() without an explicit `using` should consult the router's
365+
# db_for_write() and route to "default", not to _state.db ("secondary").
366+
obj.field1 = "modified"
367+
obj.save()
368+
369+
# The write should have landed in "default" (the write database).
370+
self.assertTrue(Model2B.objects.using("default").filter(field1="modified").exists())
371+
self.assertFalse(Model2B.objects.using("secondary").filter(field1="modified").exists())

0 commit comments

Comments
 (0)