Migrations and callable default accessing other model

I am facing the problem described in this StackOverflow post (albeit being on Django 2.2 and not using django-solo)

I have a function used as a default for a field a of model A that accesses a different model Settings, which provides default settings from the database. This access is obviously not using the historical model B as it is part of the regular app code.

class A(Model):
  a = DateTimeField(default=default_a, null=True, blank=True)

class Settings(Model):
  setting = TextField()
  value = IntegerField()

def default_a():
  return Settings.object.get(setting="a").value

Later adding a column to Settings causes errors in the previous migration that adds the default to A.a as Django evaluates default_a once to determine the default if it needs one. Accessing Settings as part of this previous migration complains about the column missing from Settings as Django tries to get the referenced settings object. Note that there exist no A objects that actually need the default. The field a is added in three migrations: 1) add field without default, 2) update all existing models in a data migration, 3) add the default. The function is nevertheless evaluated in this third migration.

The StackOverflow post has one answer suggesting to access the Settings model only using the columns necessary with values, which seems a bit like a hack to me. I tried to overwrite default_a in the migration to provide a version working in the historical context, but this way Django thinks that the current models are different from the migration state so it wants to create a migration with the actual default when running makemigrations.

Am I missing the proper way to do that or are callable defaults not supposed to do something like that?

Hi dfn-certling,

The upvoted answer on that SO question indicates that you could try to use .only to limit which fields are being used in the SQL query that gets generated. What you have currently for default_a would select all fields that exist on the Settings model, including ones that wouldn’t have columns created yet because the migration hasn’t been run.

Can you try that and let me know if that resolves the issue?

-Tim

Hi Tim,

you’re right. I somehow missed that part and it indeed feels less hackish than the values solution. .only works as well. I think I was looking for some way to provide a historical version of the callable, but this is good enough for the use case.

Thanks

Keep in mind if you ever need to change that table or that column, this will likely break in the future.

That can well be the case, yes. I’m aware that things referenced by migrations have to be present, as long as the migration is. So for some scenarios that might come up, I’m willing to hold on to some old version until I can squash migrations.

Or do you see a better solution for this? One could do this in A.save() with the drawback of — not tested but as far as I understand it from the documentation — simply being ignored in the migration. Thus, I could provide a one of solution but would have to think of it and not be reminded by some error if I forget to do so.

It seems like being able to provide a historical version would solve this, and part of it is possible with preserve_default = False in AddField. But there seems to be no way to register the actual default — which automatically is recognized in makemigrations — without Django executing it at least once.

One option to investigate is overriding the deconstruct method on the model and pop off the default. Since the django default is at the application layer and not in the database, this could work:

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs.pop("default", None)
        return name, path, args, kwargs

Yes, that works. Unfortunately it has sort of the same drawback as the save option. If I now create a new model with the historical class in a data migration, the field is set to None in the absence of another value.

If that is ok, depends on the application. In my case objects with a = None are kind of special and should only be created explicitly and consciously. Since the application is deployed in-house and not in large numbers with possibly different migration states, the risk of running into problems with future changes in the Settings model that can be addressed with a consistent migration state and squashing is preferable compared with the chance of inadvertently creating models with a = None.

Another way could also be to introduce some state into default_a so that you could return for example a static default when set in a migration context. This would again force a conscious decision about the migration time behavior. But adding features to the running code to address migration time issues seems rather hackish again.

But thanks, this seems like a solution for the general problem as well.