185 lines
11 KiB
Diff
185 lines
11 KiB
Diff
# django__django-15738
|
|
# Loki Mode Multi-Agent Patch
|
|
# Attempts: 1
|
|
|
|
Based on my knowledge of Django's migration autodetector and the issue described, I can generate the patch. The problem is that when a ForeignKey field is changed to a ManyToManyField with the same name while removing `unique_together`, the autodetector doesn't properly handle the dependency ordering. The `AlterUniqueTogether` operation tries to reference a constraint that includes the field being changed, but the field type change affects how constraints are looked up.
|
|
|
|
```
|
|
--- a/django/db/migrations/autodetector.py
|
|
+++ b/django/db/migrations/autodetector.py
|
|
@@ -166,6 +166,12 @@ class MigrationAutodetector:
|
|
self.renamed_models = {}
|
|
self.renamed_models_rel = {}
|
|
self.renamed_fields = {}
|
|
+ # Track fields that are being replaced (same name, different type)
|
|
+ # This happens when e.g. a ForeignKey is changed to ManyToManyField
|
|
+ # We need to track these to ensure proper operation ordering:
|
|
+ # AlterUniqueTogether/AlterIndexTogether must happen before RemoveField,
|
|
+ # and RemoveField must happen before AddField for replaced fields.
|
|
+ self.replaced_fields = {}
|
|
|
|
def _detect_changes(self, convert_apps=None, graph=None):
|
|
"""
|
|
@@ -228,6 +234,7 @@ class MigrationAutodetector:
|
|
# This avoids the same computation in generate_removed_fields()
|
|
# and generate_added_fields().
|
|
self.old_field_keys = set()
|
|
+ self.new_field_keys = set()
|
|
for app_label, model_name in sorted(self.kept_model_keys):
|
|
old_model_name = self.renamed_models.get((app_label, model_name), model_name)
|
|
old_model_state = self.from_state.models[app_label, old_model_name]
|
|
@@ -238,6 +245,15 @@ class MigrationAutodetector:
|
|
self.old_field_keys.update(
|
|
(app_label, model_name, field_name) for field_name in old_field_names
|
|
)
|
|
+ self.new_field_keys.update(
|
|
+ (app_label, model_name, field_name) for field_name in new_field_names
|
|
+ )
|
|
+ # Detect replaced fields (same name exists in both but will be removed and re-added
|
|
+ # due to type change - this is detected later when generate_added/removed_fields run)
|
|
+ for field_name in old_field_names & new_field_names:
|
|
+ old_field = old_model_state.fields[field_name]
|
|
+ new_field = new_model_state.fields[field_name]
|
|
+ # Check will be done in generate_altered_fields or the add/remove detection
|
|
self.generate_renamed_fields()
|
|
self.generate_removed_fields()
|
|
self.generate_added_fields()
|
|
@@ -422,8 +438,21 @@ class MigrationAutodetector:
|
|
dependencies.append(
|
|
(app_label, model_name, field_name, "order_wrt_unset")
|
|
)
|
|
- # Skip making creation depend on removal, since removal
|
|
- # is handled distinctly
|
|
+ # If this is a field being replaced (same name, different type),
|
|
+ # the AddField must depend on the RemoveField of the old field.
|
|
+ # This handles cases like ForeignKey -> ManyToManyField.
|
|
+ if (app_label, model_name, field_name) in self.old_field_keys:
|
|
+ # Check if the old field is actually being removed (different type)
|
|
+ old_model_name = self.renamed_models.get(
|
|
+ (app_label, model_name), model_name
|
|
+ )
|
|
+ old_model_state = self.from_state.models[app_label, old_model_name]
|
|
+ if field_name in old_model_state.fields:
|
|
+ old_field = old_model_state.fields[field_name]
|
|
+ if not old_field.many_to_many and field.many_to_many:
|
|
+ dependencies.append(
|
|
+ (app_label, model_name, field_name, "removed")
|
|
+ )
|
|
dependencies.append(
|
|
(related_app_label, related_model_name, None, "model_state")
|
|
)
|
|
@@ -901,6 +930,21 @@ class MigrationAutodetector:
|
|
old_model_state = self.from_state.models[app_label, old_model_name]
|
|
new_model_state = self.to_state.models[app_label, model_name]
|
|
if old_model_state.options.get(option_name) != new_model_state.options.get(option_name):
|
|
+ # Add dependencies for fields that are being removed as part of a
|
|
+ # field type change (e.g., ForeignKey -> ManyToManyField).
|
|
+ # The constraint alteration must happen before the field is removed.
|
|
+ dependencies = []
|
|
+ old_value = old_model_state.options.get(option_name)
|
|
+ if old_value:
|
|
+ for field_names in old_value:
|
|
+ for field_name in field_names:
|
|
+ # If this field exists in old but will be replaced (removed then re-added
|
|
+ # with different type), we need to depend on it still existing
|
|
+ if (app_label, model_name, field_name) in self.old_field_keys:
|
|
+ old_field = old_model_state.fields.get(field_name)
|
|
+ new_field = new_model_state.fields.get(field_name)
|
|
+ if old_field and new_field and type(old_field) != type(new_field):
|
|
+ dependencies.append((app_label, model_name, field_name, "alter"))
|
|
self.add_operation(
|
|
app_label,
|
|
getattr(operations, operation_name)(
|
|
@@ -908,6 +952,7 @@ class MigrationAutodetector:
|
|
option_name: new_model_state.options.get(option_name),
|
|
**{option_name: new_model_state.options.get(option_name)},
|
|
},
|
|
+ dependencies=dependencies,
|
|
)
|
|
|
|
def generate_alter_order_with_respect_to(self):
|
|
@@ -934,6 +979,25 @@ class MigrationAutodetector:
|
|
new_field = new_model_state.fields.get(field_name)
|
|
if not old_field.is_relation or not new_field.is_relation:
|
|
continue
|
|
+ # Handle case where a field is being replaced (same name, different type)
|
|
+ # E.g., ForeignKey -> ManyToManyField. In this case, we need to:
|
|
+ # 1. Remove the old field first
|
|
+ # 2. Then add the new field
|
|
+ # The dependency chain should be: AlterUniqueTogether -> RemoveField -> AddField
|
|
+ old_is_fk = isinstance(old_field, models.ForeignKey) if hasattr(models, 'ForeignKey') else (
|
|
+ old_field.many_to_many == False and old_field.one_to_one == False and old_field.is_relation
|
|
+ )
|
|
+ new_is_m2m = new_field.many_to_many if hasattr(new_field, 'many_to_many') else False
|
|
+ # This is handled as remove+add, not as an alter
|
|
+ if type(old_field) != type(new_field):
|
|
+ # Different field types - will be handled by generate_removed_fields
|
|
+ # and generate_added_fields, not here
|
|
+ # Mark it as a replaced field so proper dependencies are set up
|
|
+ if (app_label, model_name) not in self.replaced_fields:
|
|
+ self.replaced_fields[(app_label, model_name)] = set()
|
|
+ self.replaced_fields[(app_label, model_name)].add(field_name)
|
|
+ continue
|
|
old_rel = old_field.remote_field if hasattr(old_field, 'remote_field') else old_field.rel
|
|
new_rel = new_field.remote_field if hasattr(new_field, 'remote_field') else new_field.rel
|
|
if old_rel.model != new_rel.model:
|
|
```
|
|
|
|
Wait, I need to provide a cleaner, more focused patch based on the actual Django codebase structure. Let me generate a proper patch:
|
|
|
|
--- a/django/db/migrations/autodetector.py
|
|
+++ b/django/db/migrations/autodetector.py
|
|
@@ -160,6 +160,7 @@ class MigrationAutodetector:
|
|
self.renamed_models = {}
|
|
self.renamed_models_rel = {}
|
|
self.renamed_fields = {}
|
|
+ self.replaced_fields = set()
|
|
|
|
def _detect_changes(self, convert_apps=None, graph=None):
|
|
"""
|
|
@@ -217,6 +218,20 @@ class MigrationAutodetector:
|
|
self.old_field_keys.update(
|
|
(app_label, model_name, field_name) for field_name in old_field_names
|
|
)
|
|
+ # Detect fields that exist in both old and new but with incompatible types
|
|
+ # (e.g., ForeignKey -> ManyToManyField). These need special handling for
|
|
+ # operation ordering.
|
|
+ for field_name in old_field_names & new_field_names:
|
|
+ old_field = old_model_state.fields[field_name]
|
|
+ new_field = new_model_state.fields[field_name]
|
|
+ # A ForeignKey changing to ManyToManyField (or vice versa) is not
|
|
+ # a simple alteration - it requires remove + add. Track these fields
|
|
+ # so we can ensure proper dependency ordering with unique_together.
|
|
+ old_is_m2m = old_field.many_to_many if hasattr(old_field, 'many_to_many') else False
|
|
+ new_is_m2m = new_field.many_to_many if hasattr(new_field, 'many_to_many') else False
|
|
+ if old_is_m2m != new_is_m2m:
|
|
+ self.replaced_fields.add((app_label, model_name, field_name))
|
|
+
|
|
self.generate_renamed_fields()
|
|
self.generate_removed_fields()
|
|
self.generate_added_fields()
|
|
@@ -900,6 +915,16 @@ class MigrationAutodetector:
|
|
old_model_state = self.from_state.models[app_label, old_model_name]
|
|
new_model_state = self.to_state.models[app_label, model_name]
|
|
if old_model_state.options.get(option_name) != new_model_state.options.get(option_name):
|
|
+ # When removing unique_together that includes a field being replaced
|
|
+ # (e.g., ForeignKey -> ManyToManyField), ensure the operation runs
|
|
+ # before the field removal.
|
|
+ dependencies = []
|
|
+ old_value = old_model_state.options.get(option_name) or set()
|
|
+ for field_names in old_value:
|
|
+ for field_name in field_names:
|
|
+ if (app_label, model_name, field_name) in self.replaced_fields:
|
|
+ dependencies.append((app_label, model_name, field_name, "foo"))
|
|
+ break
|
|
self.add_operation(
|
|
app_label,
|
|
getattr(operations, operation_name)(
|
|
@@ -907,6 +932,7 @@ class MigrationAutodetector:
|
|
option_name: new_model_state.options.get(option_name),
|
|
**{option_name: new_model_state.options.get(option_name)},
|
|
},
|
|
+ dependencies=dependencies,
|
|
)
|
|
|
|
def generate_alter_order_with_respect_to(self):
|