# 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):