summaryrefslogtreecommitdiff
path: root/django/db
diff options
context:
space:
mode:
authorMarkus Holtermann <info@markusholtermann.eu>2026-01-23 14:45:33 +0100
committerJacob Walls <jacobtylerwalls@gmail.com>2026-01-28 16:13:05 -0500
commit83622b824b7014977dfc7086bbc2628ea53f4cd0 (patch)
tree17886b3b0f3b4c53c68c066631433cfaed44170a /django/db
parent5d5f95da40afbaede9f483de891c14f5da0e8218 (diff)
Fixed #36878 -- Unified data type for *_together options in ModelState.
Ever since the beginning of Django's migration framework, there's been a bit of an inconsistency on how index_together and unique_together values have been stored on the ModelState[^1]. It's only really obvious, when looking at the current code for `from_model()`[^2] and the `rename_field()` state alteration code[^3]. The problem in the autodetector's detection of the `*_together` options as raised in the ticket, reinforces the inconsistency[^4]: the old value is being normalized to a set of tuples, whereas the new value is taken as-is. Why this hasn't been caught before, is likely to the fact, that we never really look at a `to_state` that comes from migration operations in the autodetector. Instead, in both usages in Django[^5], [^6] the `to_state` is a `ProjectState.from_apps()`. And that state is consistently using sets of tuples and not lists of lists. [^1]: https://github.com/django/django/commit/67dcea711e92025d0e8676b869b7ef15dbc6db73#diff-5dd147e9e978e645313dd99eab3a7bab1f1cb0a53e256843adb68aeed71e61dcR85-R87 [^2]: https://github.com/django/django/blob/b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010/django/db/migrations/state.py#L842 [^3]: https://github.com/django/django/blob/b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010/django/db/migrations/state.py#L340-L345 [^4]: https://github.com/django/django/blob/b1ffa9a9d78b0c2c5ad6ed5a1d84e380d5cfd010/django/db/migrations/autodetector.py#L1757-L1771 [^5]: https://github.com/django/django/blob/2351c1b12cc9cf82d642f769c774bc3ea0cc4006/django/core/management/commands/makemigrations.py#L215-L219 [^6]: https://github.com/django/django/blob/2351c1b12cc9cf82d642f769c774bc3ea0cc4006/django/core/management/commands/migrate.py#L329-L332
Diffstat (limited to 'django/db')
-rw-r--r--django/db/migrations/state.py13
1 files changed, 7 insertions, 6 deletions
diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py
index 802aeb0b5e..9e9cc58fae 100644
--- a/django/db/migrations/state.py
+++ b/django/db/migrations/state.py
@@ -192,9 +192,10 @@ class ProjectState:
def remove_model_options(self, app_label, model_name, option_name, value_to_remove):
model_state = self.models[app_label, model_name]
if objs := model_state.options.get(option_name):
- model_state.options[option_name] = [
- obj for obj in objs if tuple(obj) != tuple(value_to_remove)
- ]
+ new_value = [obj for obj in objs if tuple(obj) != tuple(value_to_remove)]
+ if option_name in {"index_together", "unique_together"}:
+ new_value = set(normalize_together(new_value))
+ model_state.options[option_name] = new_value
self.reload_model(app_label, model_name, delay=True)
def alter_model_managers(self, app_label, model_name, managers):
@@ -339,10 +340,10 @@ class ProjectState:
options = model_state.options
for option in ("index_together", "unique_together"):
if option in options:
- options[option] = [
- [new_name if n == old_name else n for n in together]
+ options[option] = {
+ tuple(new_name if n == old_name else n for n in together)
for together in options[option]
- ]
+ }
# Fix to_fields to refer to the new field.
delay = True
references = get_references(self, model_key, (old_name, found))