diff --git a/django/db/migrations/operations/fields.py b/django/db/migrations/operations/fields.py index b303f70465..641c142191 100644 --- a/django/db/migrations/operations/fields.py +++ b/django/db/migrations/operations/fields.py @@ -1,7 +1,4 @@ -from django.core.exceptions import FieldDoesNotExist -from django.db.migrations.utils import ( - field_is_referenced, field_references, get_references, -) +from django.db.migrations.utils import field_references from django.db.models import NOT_PROVIDED from django.utils.functional import cached_property @@ -85,16 +82,13 @@ class AddField(FieldOperation): ) def state_forwards(self, app_label, state): - # If preserve default is off, don't use the default for future state - if not self.preserve_default: - field = self.field.clone() - field.default = NOT_PROVIDED - else: - field = self.field - state.models[app_label, self.model_name_lower].fields[self.name] = field - # Delay rendering of relationships if it's not a relational field - delay = not field.is_relation - state.reload_model(app_label, self.model_name_lower, delay=delay) + state.add_field( + app_label, + self.model_name_lower, + self.name, + self.field, + self.preserve_default, + ) def database_forwards(self, app_label, schema_editor, from_state, to_state): to_model = to_state.apps.get_model(app_label, self.model_name) @@ -160,11 +154,7 @@ class RemoveField(FieldOperation): ) def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.model_name_lower] - old_field = model_state.fields.pop(self.name) - # Delay rendering of relationships if it's not a relational field - delay = not old_field.is_relation - state.reload_model(app_label, self.model_name_lower, delay=delay) + state.remove_field(app_label, self.model_name_lower, self.name) def database_forwards(self, app_label, schema_editor, from_state, to_state): from_model = from_state.apps.get_model(app_label, self.model_name) @@ -216,24 +206,13 @@ class AlterField(FieldOperation): ) def state_forwards(self, app_label, state): - if not self.preserve_default: - field = self.field.clone() - field.default = NOT_PROVIDED - else: - field = self.field - model_state = state.models[app_label, self.model_name_lower] - model_state.fields[self.name] = field - # TODO: investigate if old relational fields must be reloaded or if it's - # sufficient if the new field is (#27737). - # Delay rendering of relationships if it's not a relational field and - # not referenced by a foreign key. - delay = ( - not field.is_relation and - not field_is_referenced( - state, (app_label, self.model_name_lower), (self.name, field), - ) + state.alter_field( + app_label, + self.model_name_lower, + self.name, + self.field, + self.preserve_default, ) - state.reload_model(app_label, self.model_name_lower, delay=delay) def database_forwards(self, app_label, schema_editor, from_state, to_state): to_model = to_state.apps.get_model(app_label, self.model_name) @@ -301,49 +280,7 @@ class RenameField(FieldOperation): ) def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.model_name_lower] - # Rename the field - fields = model_state.fields - try: - found = fields.pop(self.old_name) - except KeyError: - raise FieldDoesNotExist( - "%s.%s has no field named '%s'" % (app_label, self.model_name, self.old_name) - ) - fields[self.new_name] = found - for field in fields.values(): - # Fix from_fields to refer to the new field. - from_fields = getattr(field, 'from_fields', None) - if from_fields: - field.from_fields = tuple([ - self.new_name if from_field_name == self.old_name else from_field_name - for from_field_name in from_fields - ]) - # Fix index/unique_together to refer to the new field - options = model_state.options - for option in ('index_together', 'unique_together'): - if option in options: - options[option] = [ - [self.new_name if n == self.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( - state, (app_label, self.model_name_lower), (self.old_name, found), - ) - for *_, field, reference in references: - delay = False - if reference.to: - remote_field, to_fields = reference.to - if getattr(remote_field, 'field_name', None) == self.old_name: - remote_field.field_name = self.new_name - if to_fields: - field.to_fields = tuple([ - self.new_name if to_field_name == self.old_name else to_field_name - for to_field_name in to_fields - ]) - state.reload_model(app_label, self.model_name_lower, delay=delay) + state.rename_field(app_label, self.model_name_lower, self.old_name, self.new_name) def database_forwards(self, app_label, schema_editor, from_state, to_state): to_model = to_state.apps.get_model(app_label, self.model_name) diff --git a/django/db/migrations/operations/models.py b/django/db/migrations/operations/models.py index 10f2686dc6..982816be3a 100644 --- a/django/db/migrations/operations/models.py +++ b/django/db/migrations/operations/models.py @@ -1,9 +1,7 @@ from django.db import models from django.db.migrations.operations.base import Operation from django.db.migrations.state import ModelState -from django.db.migrations.utils import ( - field_references, get_references, resolve_relation, -) +from django.db.migrations.utils import field_references, resolve_relation from django.db.models.options import normalize_together from django.utils.functional import cached_property @@ -316,31 +314,7 @@ class RenameModel(ModelOperation): ) def state_forwards(self, app_label, state): - # Add a new model. - renamed_model = state.models[app_label, self.old_name_lower].clone() - renamed_model.name = self.new_name - state.models[app_label, self.new_name_lower] = renamed_model - # Repoint all fields pointing to the old model to the new one. - old_model_tuple = (app_label, self.old_name_lower) - new_remote_model = '%s.%s' % (app_label, self.new_name) - to_reload = set() - for model_state, name, field, reference in get_references(state, old_model_tuple): - changed_field = None - if reference.to: - changed_field = field.clone() - changed_field.remote_field.model = new_remote_model - if reference.through: - if changed_field is None: - changed_field = field.clone() - changed_field.remote_field.through = new_remote_model - if changed_field: - model_state.fields[name] = changed_field - to_reload.add((model_state.app_label, model_state.name_lower)) - # Reload models related to old model before removing the old model. - state.reload_models(to_reload, delay=True) - # Remove the old model. - state.remove_model(app_label, self.old_name_lower) - state.reload_model(app_label, self.new_name_lower, delay=True) + state.rename_model(app_label, self.old_name, self.new_name) def database_forwards(self, app_label, schema_editor, from_state, to_state): new_model = to_state.apps.get_model(app_label, self.new_name) @@ -458,8 +432,7 @@ class AlterModelTable(ModelOptionOperation): ) def state_forwards(self, app_label, state): - state.models[app_label, self.name_lower].options["db_table"] = self.table - state.reload_model(app_label, self.name_lower, delay=True) + state.alter_model_options(app_label, self.name_lower, {'db_table': self.table}) def database_forwards(self, app_label, schema_editor, from_state, to_state): new_model = to_state.apps.get_model(app_label, self.name) @@ -518,9 +491,11 @@ class AlterTogetherOptionOperation(ModelOptionOperation): ) def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.name_lower] - model_state.options[self.option_name] = self.option_value - state.reload_model(app_label, self.name_lower, delay=True) + state.alter_model_options( + app_label, + self.name_lower, + {self.option_name: self.option_value}, + ) def database_forwards(self, app_label, schema_editor, from_state, to_state): new_model = to_state.apps.get_model(app_label, self.name) @@ -596,9 +571,11 @@ class AlterOrderWithRespectTo(ModelOptionOperation): ) def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.name_lower] - model_state.options['order_with_respect_to'] = self.order_with_respect_to - state.reload_model(app_label, self.name_lower, delay=True) + state.alter_model_options( + app_label, + self.name_lower, + {self.option_name: self.order_with_respect_to}, + ) def database_forwards(self, app_label, schema_editor, from_state, to_state): to_model = to_state.apps.get_model(app_label, self.name) @@ -676,12 +653,12 @@ class AlterModelOptions(ModelOptionOperation): ) def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.name_lower] - model_state.options = {**model_state.options, **self.options} - for key in self.ALTER_OPTION_KEYS: - if key not in self.options: - model_state.options.pop(key, False) - state.reload_model(app_label, self.name_lower, delay=True) + state.alter_model_options( + app_label, + self.name_lower, + self.options, + self.ALTER_OPTION_KEYS, + ) def database_forwards(self, app_label, schema_editor, from_state, to_state): pass @@ -714,9 +691,7 @@ class AlterModelManagers(ModelOptionOperation): ) def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.name_lower] - model_state.managers = list(self.managers) - state.reload_model(app_label, self.name_lower, delay=True) + state.alter_model_managers(app_label, self.name_lower, self.managers) def database_forwards(self, app_label, schema_editor, from_state, to_state): pass @@ -753,9 +728,7 @@ class AddIndex(IndexOperation): self.index = index def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.model_name_lower] - model_state.options[self.option_name] = [*model_state.options[self.option_name], self.index.clone()] - state.reload_model(app_label, self.model_name_lower, delay=True) + state.add_index(app_label, self.model_name_lower, self.index) def database_forwards(self, app_label, schema_editor, from_state, to_state): model = to_state.apps.get_model(app_label, self.model_name) @@ -804,10 +777,7 @@ class RemoveIndex(IndexOperation): self.name = name def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.model_name_lower] - indexes = model_state.options[self.option_name] - model_state.options[self.option_name] = [idx for idx in indexes if idx.name != self.name] - state.reload_model(app_label, self.model_name_lower, delay=True) + state.remove_index(app_label, self.model_name_lower, self.name) def database_forwards(self, app_label, schema_editor, from_state, to_state): model = from_state.apps.get_model(app_label, self.model_name) @@ -850,9 +820,7 @@ class AddConstraint(IndexOperation): self.constraint = constraint def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.model_name_lower] - model_state.options[self.option_name] = [*model_state.options[self.option_name], self.constraint] - state.reload_model(app_label, self.model_name_lower, delay=True) + state.add_constraint(app_label, self.model_name_lower, self.constraint) def database_forwards(self, app_label, schema_editor, from_state, to_state): model = to_state.apps.get_model(app_label, self.model_name) @@ -886,10 +854,7 @@ class RemoveConstraint(IndexOperation): self.name = name def state_forwards(self, app_label, state): - model_state = state.models[app_label, self.model_name_lower] - constraints = model_state.options[self.option_name] - model_state.options[self.option_name] = [c for c in constraints if c.name != self.name] - state.reload_model(app_label, self.model_name_lower, delay=True) + state.remove_constraint(app_label, self.model_name_lower, self.name) def database_forwards(self, app_label, schema_editor, from_state, to_state): model = to_state.apps.get_model(app_label, self.model_name) diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 38ec72dd72..af9093c4e8 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -6,7 +6,10 @@ from functools import partial from django.apps import AppConfig from django.apps.registry import Apps, apps as global_apps from django.conf import settings +from django.core.exceptions import FieldDoesNotExist from django.db import models +from django.db.migrations.utils import field_is_referenced, get_references +from django.db.models import NOT_PROVIDED from django.db.models.fields.related import RECURSIVE_RELATIONSHIP_CONSTANT from django.db.models.options import DEFAULT_NAMES, normalize_together from django.db.models.utils import make_model_tuple @@ -107,6 +110,152 @@ class ProjectState: # the cache automatically (#24513) self.apps.clear_cache() + def rename_model(self, app_label, old_name, new_name): + # Add a new model. + old_name_lower = old_name.lower() + new_name_lower = new_name.lower() + renamed_model = self.models[app_label, old_name_lower].clone() + renamed_model.name = new_name + self.models[app_label, new_name_lower] = renamed_model + # Repoint all fields pointing to the old model to the new one. + old_model_tuple = (app_label, old_name_lower) + new_remote_model = f'{app_label}.{new_name}' + to_reload = set() + for model_state, name, field, reference in get_references(self, old_model_tuple): + changed_field = None + if reference.to: + changed_field = field.clone() + changed_field.remote_field.model = new_remote_model + if reference.through: + if changed_field is None: + changed_field = field.clone() + changed_field.remote_field.through = new_remote_model + if changed_field: + model_state.fields[name] = changed_field + to_reload.add((model_state.app_label, model_state.name_lower)) + # Reload models related to old model before removing the old model. + self.reload_models(to_reload, delay=True) + # Remove the old model. + self.remove_model(app_label, old_name_lower) + self.reload_model(app_label, new_name_lower, delay=True) + + def alter_model_options(self, app_label, model_name, options, option_keys=None): + model_state = self.models[app_label, model_name] + model_state.options = {**model_state.options, **options} + if option_keys: + for key in option_keys: + if key not in options: + model_state.options.pop(key, False) + self.reload_model(app_label, model_name, delay=True) + + def alter_model_managers(self, app_label, model_name, managers): + model_state = self.models[app_label, model_name] + model_state.managers = list(managers) + self.reload_model(app_label, model_name, delay=True) + + def _append_option(self, app_label, model_name, option_name, obj): + model_state = self.models[app_label, model_name] + model_state.options[option_name] = [*model_state.options[option_name], obj] + self.reload_model(app_label, model_name, delay=True) + + def _remove_option(self, app_label, model_name, option_name, obj_name): + model_state = self.models[app_label, model_name] + objs = model_state.options[option_name] + model_state.options[option_name] = [obj for obj in objs if obj.name != obj_name] + self.reload_model(app_label, model_name, delay=True) + + def add_index(self, app_label, model_name, index): + self._append_option(app_label, model_name, 'indexes', index) + + def remove_index(self, app_label, model_name, index_name): + self._remove_option(app_label, model_name, 'indexes', index_name) + + def add_constraint(self, app_label, model_name, constraint): + self._append_option(app_label, model_name, 'constraints', constraint) + + def remove_constraint(self, app_label, model_name, constraint_name): + self._remove_option(app_label, model_name, 'constraints', constraint_name) + + def add_field(self, app_label, model_name, name, field, preserve_default): + # If preserve default is off, don't use the default for future state. + if not preserve_default: + field = field.clone() + field.default = NOT_PROVIDED + else: + field = field + self.models[app_label, model_name].fields[name] = field + # Delay rendering of relationships if it's not a relational field. + delay = not field.is_relation + self.reload_model(app_label, model_name, delay=delay) + + def remove_field(self, app_label, model_name, name): + model_state = self.models[app_label, model_name] + old_field = model_state.fields.pop(name) + # Delay rendering of relationships if it's not a relational field. + delay = not old_field.is_relation + self.reload_model(app_label, model_name, delay=delay) + + def alter_field(self, app_label, model_name, name, field, preserve_default): + if not preserve_default: + field = field.clone() + field.default = NOT_PROVIDED + else: + field = field + model_state = self.models[app_label, model_name] + model_state.fields[name] = field + # TODO: investigate if old relational fields must be reloaded or if + # it's sufficient if the new field is (#27737). + # Delay rendering of relationships if it's not a relational field and + # not referenced by a foreign key. + delay = ( + not field.is_relation and + not field_is_referenced(self, (app_label, model_name), (name, field)) + ) + self.reload_model(app_label, model_name, delay=delay) + + def rename_field(self, app_label, model_name, old_name, new_name): + model_state = self.models[app_label, model_name] + # Rename the field. + fields = model_state.fields + try: + found = fields.pop(old_name) + except KeyError: + raise FieldDoesNotExist( + f"{app_label}.{model_name} has no field named '{old_name}'" + ) + fields[new_name] = found + for field in fields.values(): + # Fix from_fields to refer to the new field. + from_fields = getattr(field, 'from_fields', None) + if from_fields: + field.from_fields = tuple([ + new_name if from_field_name == old_name else from_field_name + for from_field_name in from_fields + ]) + # Fix index/unique_together to refer to the new field. + 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] + for together in options[option] + ] + # Fix to_fields to refer to the new field. + delay = True + references = get_references(self, (app_label, model_name), (old_name, found)) + for *_, field, reference in references: + delay = False + if reference.to: + remote_field, to_fields = reference.to + if getattr(remote_field, 'field_name', None) == old_name: + remote_field.field_name = new_name + if to_fields: + field.to_fields = tuple([ + new_name if to_field_name == old_name else to_field_name + for to_field_name in to_fields + ]) + self.reload_model(app_label, model_name, delay=delay) + def _find_reload_model(self, app_label, model_name, delay=False): if delay: self.is_delayed = True