Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Proxy model migrations #2645

Closed
115 changes: 93 additions & 22 deletions django/db/migrations/autodetector.py
Expand Up @@ -53,20 +53,48 @@ def _detect_changes(self):
old_apps = self.from_state.render(ignore_swappable=True) old_apps = self.from_state.render(ignore_swappable=True)
new_apps = self.to_state.render() new_apps = self.to_state.render()
# Prepare lists of old/new model keys that we care about # Prepare lists of old/new model keys that we care about
# (i.e. ignoring proxy ones and unmigrated ones) # (i.e. unmigrated ones)


old_model_keys = [] old_model_keys = []
for al, mn in self.from_state.models.keys(): for al, mn in self.from_state.models.keys():
model = old_apps.get_model(al, mn) model = old_apps.get_model(al, mn)
if not model._meta.proxy and model._meta.managed and al not in self.from_state.real_apps: if model._meta.managed and al not in self.from_state.real_apps:
old_model_keys.append((al, mn)) old_model_keys.append((al, mn))


new_model_keys = [] new_model_keys = []
for al, mn in self.to_state.models.keys(): for al, mn in self.to_state.models.keys():
model = new_apps.get_model(al, mn) model = new_apps.get_model(al, mn)
if not model._meta.proxy and model._meta.managed and al not in self.to_state.real_apps: if model._meta.managed and al not in self.to_state.real_apps:
new_model_keys.append((al, mn)) new_model_keys.append((al, mn))


# TODO good idea treat proxy to non-proxy as new (for field handling)?
kept_models = set(old_model_keys).intersection(new_model_keys)
for app_label, model_name in kept_models:
old_model_state = self.from_state.models[app_label, model_name]
new_model_state = self.to_state.models[app_label, model_name]
if old_model_state.options.get('proxy') and not new_model_state.options.get('proxy'):
self.add_to_migration(
app_label,
operations.DeleteModel(
name=old_model_state.name,
orm_only=True,
)
)
old_model_keys.remove((app_label, model_name))
if not old_model_state.options.get('proxy') and new_model_state.options.get('proxy'):
self.add_to_migration(
app_label,
operations.CreateModel(
name=new_model_state.name,
fields=[],
options=new_model_state.options,
bases=new_model_state.bases,
orm_only=True,
)
)
new_model_keys.remove((app_label, model_name))


def _deep_deconstruct(obj, field=True): def _deep_deconstruct(obj, field=True):
""" """
Recursive deconstruction for a field and its arguments. Recursive deconstruction for a field and its arguments.
Expand Down Expand Up @@ -112,11 +140,16 @@ def _rel_agnostic_fields_def(fields):
rem_model_fields_def = _rel_agnostic_fields_def(rem_model_state.fields) rem_model_fields_def = _rel_agnostic_fields_def(rem_model_state.fields)
if model_fields_def == rem_model_fields_def: if model_fields_def == rem_model_fields_def:
if self.questioner.ask_rename_model(rem_model_state, model_state): if self.questioner.ask_rename_model(rem_model_state, model_state):
if model_state.options.get('proxy'):
orm_only = True
else:
orm_only = False
self.add_to_migration( self.add_to_migration(
app_label, app_label,
operations.RenameModel( operations.RenameModel(
old_name=rem_model_state.name, old_name=rem_model_state.name,
new_name=model_state.name, new_name=model_state.name,
orm_only=orm_only,
) )
) )
renamed_models[app_label, model_name] = rem_model_name renamed_models[app_label, model_name] = rem_model_name
Expand All @@ -127,12 +160,16 @@ def _rel_agnostic_fields_def(fields):


# Adding models. Phase 1 is adding models with no outward relationships. # Adding models. Phase 1 is adding models with no outward relationships.
added_models = set(new_model_keys) - set(old_model_keys) added_models = set(new_model_keys) - set(old_model_keys)
pending_add = {} pending_related_fields = {}
pending_proxy_base = {}
for app_label, model_name in added_models: for app_label, model_name in added_models:
model_state = self.to_state.models[app_label, model_name] model_state = self.to_state.models[app_label, model_name]
# Are there any relationships out from this model? if so, punt it to the next phase. # Are there any relationships out from this model, or is it a proxy
# for another model? if so, punt it to the next phase.
related_fields = [] related_fields = []
for field in new_apps.get_model(app_label, model_name)._meta.local_fields: options = new_apps.get_model(app_label, model_name)._meta
proxy_for_model = options.proxy_for_model
for field in options.local_fields:
if field.rel: if field.rel:
if field.rel.to: if field.rel.to:
related_fields.append((field.name, field.rel.to._meta.app_label, field.rel.to._meta.model_name)) related_fields.append((field.name, field.rel.to._meta.app_label, field.rel.to._meta.model_name))
Expand All @@ -144,7 +181,9 @@ def _rel_agnostic_fields_def(fields):
if hasattr(field.rel, "through") and not field.rel.through._meta.auto_created: if hasattr(field.rel, "through") and not field.rel.through._meta.auto_created:
related_fields.append((field.name, field.rel.through._meta.app_label, field.rel.through._meta.model_name)) related_fields.append((field.name, field.rel.through._meta.app_label, field.rel.through._meta.model_name))
if related_fields: if related_fields:
pending_add[app_label, model_name] = related_fields pending_related_fields[app_label, model_name] = related_fields
elif proxy_for_model is not None:
pending_proxy_base[app_label, model_name] = (proxy_for_model._meta.app_label, proxy_for_model._meta.model_name)
else: else:
self.add_to_migration( self.add_to_migration(
app_label, app_label,
Expand All @@ -161,15 +200,20 @@ def _rel_agnostic_fields_def(fields):
pending_new_fks = [] pending_new_fks = []
pending_unique_together = [] pending_unique_together = []
added_phase_2 = set() added_phase_2 = set()
while pending_add: while pending_related_fields or pending_proxy_base:
# Is there one we can add that has all dependencies satisfied? # Is there one we can add that has all dependencies satisfied?
satisfied = [ satisfied_related_fields = [
(m, rf) (m, rf)
for m, rf in pending_add.items() for m, rf in pending_related_fields.items()
if all((al, mn) not in pending_add for f, al, mn in rf) if all((al, mn) not in (pending_related_fields.keys() + pending_proxy_base.keys())
for f, al, mn in rf)
]
satisfied_proxy_model_bases = [
m for m, parent in pending_proxy_base.items()
if parent not in (pending_related_fields.keys() + pending_proxy_base.keys())
] ]
if satisfied: if satisfied_related_fields:
(app_label, model_name), related_fields = sorted(satisfied)[0] (app_label, model_name), related_fields = sorted(satisfied_related_fields)[0]
model_state = self.to_state.models[app_label, model_name] model_state = self.to_state.models[app_label, model_name]
self.add_to_migration( self.add_to_migration(
app_label, app_label,
Expand All @@ -184,17 +228,37 @@ def _rel_agnostic_fields_def(fields):
new=any((al, mn) in added_phase_2 for f, al, mn in related_fields), new=any((al, mn) in added_phase_2 for f, al, mn in related_fields),
) )
added_phase_2.add((app_label, model_name)) added_phase_2.add((app_label, model_name))
elif satisfied_proxy_model_bases:
(app_label, model_name) = sorted(satisfied_proxy_model_bases)[0]
model_state = self.to_state.models[app_label, model_name]
other_app_label, _ = pending_proxy_base[(app_label, model_name)]
self.add_to_migration(
app_label,
operations.CreateModel(
name=model_state.name,
fields=[],
options=model_state.options,
bases=model_state.bases,
orm_only=True,
),
# If it's already been added in phase 2 put it in a new
# migration for safety.
# TODO: do we need this for proxies?
new=any((al, mn) in added_phase_2 for f, al, mn in related_fields),
)
# TODO: do we need this for proxies?
added_phase_2.add((app_label, model_name))
# Ah well, we'll need to split one. Pick deterministically. # Ah well, we'll need to split one. Pick deterministically.
else: else:
(app_label, model_name), related_fields = sorted(pending_add.items())[0] (app_label, model_name), related_fields = sorted(pending_related_fields.items())[0]
model_state = self.to_state.models[app_label, model_name] model_state = self.to_state.models[app_label, model_name]
# Defer unique together constraints creation, see ticket #22275 # Defer unique together constraints creation, see ticket #22275
unique_together_constraints = model_state.options.pop('unique_together', None) unique_together_constraints = model_state.options.pop('unique_together', None)
if unique_together_constraints: if unique_together_constraints:
pending_unique_together.append((app_label, model_name, pending_unique_together.append((app_label, model_name,
unique_together_constraints)) unique_together_constraints))
# Work out the fields that need splitting out # Work out the fields that need splitting out
bad_fields = dict((f, (al, mn)) for f, al, mn in related_fields if (al, mn) in pending_add) bad_fields = dict((f, (al, mn)) for f, al, mn in related_fields if (al, mn) in pending_related_fields)
# Create the model, without those # Create the model, without those
self.add_to_migration( self.add_to_migration(
app_label, app_label,
Expand All @@ -208,14 +272,19 @@ def _rel_agnostic_fields_def(fields):
# Add the bad fields to be made in a phase 3 # Add the bad fields to be made in a phase 3
for field_name, (other_app_label, other_model_name) in bad_fields.items(): for field_name, (other_app_label, other_model_name) in bad_fields.items():
pending_new_fks.append((app_label, model_name, field_name, other_app_label)) pending_new_fks.append((app_label, model_name, field_name, other_app_label))
for field_name, other_app_label, other_model_name in related_fields: if new_apps.get_model(app_label, model_name)._meta.proxy:
# If it depends on a swappable something, add a dynamic depend'cy if app_label != other_app_label:
swappable_setting = new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0].swappable_setting
if swappable_setting is not None:
self.add_swappable_dependency(app_label, swappable_setting)
elif app_label != other_app_label:
self.add_dependency(app_label, other_app_label) self.add_dependency(app_label, other_app_label)
del pending_add[app_label, model_name] pending_proxy_base.pop((app_label, model_name))
else:
for field_name, other_app_label, other_model_name in related_fields:
# If it depends on a swappable something, add a dynamic depend'cy
swappable_setting = new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0].swappable_setting
if swappable_setting is not None:
self.add_swappable_dependency(app_label, swappable_setting)
elif app_label != other_app_label:
self.add_dependency(app_label, other_app_label)
pending_related_fields.pop((app_label, model_name))


# Phase 3 is adding the final set of FKs as separate new migrations. # Phase 3 is adding the final set of FKs as separate new migrations.
for app_label, model_name, field_name, other_app_label in pending_new_fks: for app_label, model_name, field_name, other_app_label in pending_new_fks:
Expand Down Expand Up @@ -267,6 +336,7 @@ def _rel_agnostic_fields_def(fields):
unique_together=new_model_state.options.get("unique_together", set()), unique_together=new_model_state.options.get("unique_together", set()),
) )
)) ))

# New fields # New fields
renamed_fields = {} renamed_fields = {}
for app_label, model_name, field_name in new_fields - old_fields: for app_label, model_name, field_name in new_fields - old_fields:
Expand Down Expand Up @@ -489,6 +559,7 @@ def suggest_name(cls, ops):
but we put some effort in to the fallback name to avoid VCS conflicts but we put some effort in to the fallback name to avoid VCS conflicts
if we can. if we can.
""" """

if len(ops) == 1: if len(ops) == 1:
if isinstance(ops[0], operations.CreateModel): if isinstance(ops[0], operations.CreateModel):
return ops[0].name.lower() return ops[0].name.lower()
Expand Down
28 changes: 25 additions & 3 deletions django/db/migrations/operations/models.py
Expand Up @@ -13,23 +13,29 @@ class CreateModel(Operation):
""" """


serialization_expand_args = ['fields', 'options'] serialization_expand_args = ['fields', 'options']
orm_only = False


def __init__(self, name, fields, options=None, bases=None): def __init__(self, name, fields, options=None, bases=None, orm_only=False):
self.name = name self.name = name
self.fields = fields self.fields = fields
self.options = options or {} self.options = options or {}
self.bases = bases or (models.Model,) self.bases = bases or (models.Model,)
self.orm_only = orm_only


def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
state.models[app_label, self.name.lower()] = ModelState(app_label, self.name, self.fields, self.options, self.bases) state.models[app_label, self.name.lower()] = ModelState(app_label, self.name, self.fields, self.options, self.bases)


def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
if self.orm_only:
return
apps = to_state.render() apps = to_state.render()
model = apps.get_model(app_label, self.name) model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model): if router.allow_migrate(schema_editor.connection.alias, model):
schema_editor.create_model(model) schema_editor.create_model(model)


def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
if self.orm_only:
return
apps = from_state.render() apps = from_state.render()
model = apps.get_model(app_label, self.name) model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model): if router.allow_migrate(schema_editor.connection.alias, model):
Expand Down Expand Up @@ -64,25 +70,35 @@ def __eq__(self, other):
([(k, f.deconstruct()[1:]) for k, f in self.fields] == [(k, f.deconstruct()[1:]) for k, f in other.fields]) ([(k, f.deconstruct()[1:]) for k, f in self.fields] == [(k, f.deconstruct()[1:]) for k, f in other.fields])
) )


def __ne__(self, other):
return not (self == other)



class DeleteModel(Operation): class DeleteModel(Operation):
""" """
Drops a model's table. Drops a model's table.
""" """


def __init__(self, name): orm_only = False

def __init__(self, name, orm_only=False):
self.name = name self.name = name
self.orm_only = orm_only


def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
del state.models[app_label, self.name.lower()] del state.models[app_label, self.name.lower()]


def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
if self.orm_only:
return
apps = from_state.render() apps = from_state.render()
model = apps.get_model(app_label, self.name) model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model): if router.allow_migrate(schema_editor.connection.alias, model):
schema_editor.delete_model(model) schema_editor.delete_model(model)


def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
if self.orm_only:
return
apps = to_state.render() apps = to_state.render()
model = apps.get_model(app_label, self.name) model = apps.get_model(app_label, self.name)
if router.allow_migrate(schema_editor.connection.alias, model): if router.allow_migrate(schema_editor.connection.alias, model):
Expand All @@ -101,17 +117,21 @@ class RenameModel(Operation):
""" """


reversible = False reversible = False
orm_only = False


def __init__(self, old_name, new_name): def __init__(self, old_name, new_name, orm_only=False):
self.old_name = old_name self.old_name = old_name
self.new_name = new_name self.new_name = new_name
self.orm_only = orm_only


def state_forwards(self, app_label, state): def state_forwards(self, app_label, state):
state.models[app_label, self.new_name.lower()] = state.models[app_label, self.old_name.lower()] state.models[app_label, self.new_name.lower()] = state.models[app_label, self.old_name.lower()]
state.models[app_label, self.new_name.lower()].name = self.new_name state.models[app_label, self.new_name.lower()].name = self.new_name
del state.models[app_label, self.old_name.lower()] del state.models[app_label, self.old_name.lower()]


def database_forwards(self, app_label, schema_editor, from_state, to_state): def database_forwards(self, app_label, schema_editor, from_state, to_state):
if self.orm_only:
return
old_apps = from_state.render() old_apps = from_state.render()
new_apps = to_state.render() new_apps = to_state.render()
old_model = old_apps.get_model(app_label, self.old_name) old_model = old_apps.get_model(app_label, self.old_name)
Expand All @@ -124,6 +144,8 @@ def database_forwards(self, app_label, schema_editor, from_state, to_state):
) )


def database_backwards(self, app_label, schema_editor, from_state, to_state): def database_backwards(self, app_label, schema_editor, from_state, to_state):
if self.orm_only:
return
old_apps = from_state.render() old_apps = from_state.render()
new_apps = to_state.render() new_apps = to_state.render()
old_model = old_apps.get_model(app_label, self.new_name) old_model = old_apps.get_model(app_label, self.new_name)
Expand Down
3 changes: 2 additions & 1 deletion django/db/migrations/state.py
Expand Up @@ -65,7 +65,8 @@ def render(self, include_real=None, ignore_swappable=False):
except InvalidBasesError: except InvalidBasesError:
new_unrendered_models.append(model) new_unrendered_models.append(model)
if len(new_unrendered_models) == len(unrendered_models): if len(new_unrendered_models) == len(unrendered_models):
raise InvalidBasesError("Cannot resolve bases for %r" % new_unrendered_models) raise InvalidBasesError("Cannot resolve bases for %r" % [
s.name for s in new_unrendered_models])
unrendered_models = new_unrendered_models unrendered_models = new_unrendered_models
# make sure apps has no dangling references # make sure apps has no dangling references
if self.apps._pending_lookups: if self.apps._pending_lookups:
Expand Down