Pass South random unique default values when migrating - django

I am trying to forward migrate a model with existing data. The model has a new field with constraints unique=True and null=False.
When I do
./manage.py schemamigration myapp --auto
South lets me specify a default value for the new field by asking:
Specify a one-off value to use for existing columns now
Usually I set this to None but since this field needs to be unique I was wondering if it is possible to pass South a unique value via:
>>> import uuid; uuid.uuid1().hex[0:35]
This gives me an error message
! Invalid input: invalid syntax
Any ideas if it is possible to pass South random unique default values when migrating via the commandline?
Thanks.

Unfortunately only the datetime module is available for use as a one-off value in a schemamigration.
However, you can achieve the same effect by splitting this up into three migrations:
add new field to the model without constraints (with null=True, unique=False)
use a datamigration to add the UUID to the new field
add the constraint on the new field (with null=False, unique=True)
Tutorial on data migrations: http://south.readthedocs.org/en/0.7.6/tutorial/part3.html#data-migrations

In django 1.7+ you can do the following. It first adds the field with no indexing and no unique. It then assigns the unique values (I based them on the name and used slugify method which you need to create) and finally alters the field again to add index and unique attributes.
from django.db import migrations
import re
import django.contrib.postgres.fields
from common.utils import slugify
import django.core.validators
def set_slugs(apps, schema_editor):
categories = apps.get_model("myapp", "Category").objects.all()
for category in categories:
category.slug = slugify(category.name)
category.save()
class Migration(migrations.Migration):
dependencies = [
('myapp', '0034_auto_20150906_1936'),
]
operations = [
migrations.AddField(
model_name='category',
name='slug',
field=models.CharField(max_length=30, validators=[django.core.validators.MinLengthValidator(2), django.core.validators.RegexValidator(re.compile('^[0-9a-z-]+$'), 'Enter a valid slug.', 'invalid')], help_text='Required. 2 to 30 characters and can only contain a-z, 0-9, and the dash (-)', unique=False, db_index=False, null=True),
preserve_default=False,
),
migrations.RunPython(set_slugs),
migrations.AlterField(
model_name='category',
name='slug',
field=models.CharField(help_text='Required. 2 to 30 characters and can only contain a-z, 0-9, and the dash (-)', unique=True, max_length=30, db_index=True, validators=[django.core.validators.MinLengthValidator(2), django.core.validators.RegexValidator(re.compile('^[0-9a-z-]+$'), 'Enter a valid slug.', 'invalid')]),
),
]

Here is the Django's official how-to on migrating unique fields.
Migrations that add unique fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Applying a "plain" migration that adds a unique non-nullable field to a table
with existing rows will raise an error because the value used to populate
existing rows is generated only once, thus breaking the unique constraint.
Therefore, the following steps should be taken. In this example, we'll add a
non-nullable :class:`~django.db.models.UUIDField` with a default value. Modify
the respective field according to your needs.
* Add the field on your model with ``default=...`` and ``unique=True``
arguments. In the example, we use ``uuid.uuid4`` for the default.
* Run the :djadmin:`makemigrations` command.
* Edit the created migration file.
The generated migration class should look similar to this::
class Migration(migrations.Migration):
dependencies = [
('myapp', '0003_auto_20150129_1705'),
]
operations = [
migrations.AddField(
model_name='mymodel',
name='uuid',
field=models.UUIDField(max_length=32, unique=True, default=uuid.uuid4),
),
]
You will need to make three changes:
* Add a second :class:`~django.db.migrations.operations.AddField` operation
copied from the generated one and change it to
:class:`~django.db.migrations.operations.AlterField`.
* On the first operation (``AddField``), change ``unique=True`` to
``null=True`` -- this will create the intermediary null field.
* Between the two operations, add a
:class:`~django.db.migrations.operations.RunPython` or
:class:`~django.db.migrations.operations.RunSQL` operation to generate a
unique value (UUID in the example) for each existing row.
The resulting migration should look similar to this::
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
def gen_uuid(apps, schema_editor):
MyModel = apps.get_model('myapp', 'MyModel')
for row in MyModel.objects.all():
row.uuid = uuid.uuid4()
row.save()
class Migration(migrations.Migration):
dependencies = [
('myapp', '0003_auto_20150129_1705'),
]
operations = [
migrations.AddField(
model_name='mymodel',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, null=True),
),
# omit reverse_code=... if you don't want the migration to be reversible.
migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name='mymodel',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, unique=True),
),
]
* Now you can apply the migration as usual with the :djadmin:`migrate` command.
Note there is a race condition if you allow objects to be created while this
migration is running. Objects created after the ``AddField`` and before
``RunPython`` will have their original ``uuid``’s overwritten.

You can manually edit your migration file:
I needed to add random character to some field so I have imported random and randint
import random
import string
and changed the value of default to
default=random.choice(string.lowercase)
It worked.

There is way to do unique value for each row with South.
Define slug in models.py as:
class Foo(models.Model):
slug = models.SlugField(unique=True, default='')
....
Create new migration
run python manage.py schemamigration --auto foo
Open new migration file, and edit it:
# Change add_column to this:
db.add_column(u'account_funnel', 'slug',
self.gf('django.db.models.foo.Foo')(default='',
unique=False,
max_length=50),
keep_default=False)
# right above this add such python code:
foos = orm['foo.Foo'].objects.all()
for foo in foos:
foo.slug = slugify(funnel.name)
foo.save()
# Modify slug as unique field
db.create_unique(u'foo_foo', ['slug'])
ps mark this migration as no_dry_run = True
pss do not forget to import slugify function from django.template.defaultfilters import slugify

Related

How to set default value for a model field based on enum in Django?

I'm using Django 2.2.5 and have multiple choice fields based on enums in my models. For an unknown reason I now get a migration error when using an enum for choice field during migration:
django.db.utils.OperationalError: (1067, "Invalid default value for 'protocol'")
model.py
from django.db import models
# See class above
from .utils import NetworkProtocolList
class Networks(models.Model):
ipv4 = models.GenericIPAddressField(blank=False, null=False)
protocol = models.CharField(choices=NetworkProtocolList.choices(), max_length=20,default=NetworkProtocolList.ETH)
class Meta:
managed = True
db_table = 'networks'
utils.py
from enum import Enum
class NetworkProtocolList(Enum):
ETH = 'Ethernet'
MPLS = 'MPLS'
#classmethod
def choices(cls):
return [(key.name, key.value) for key in cls]
I issued
manage.py makemigrations
and subsequent
manage.py migrate
generated the following error:
django.db.utils.OperationalError: (1067, "Invalid default value for
'protocol'")
xxxx_auto_xxxxxxxx_xxxx.py
# Auto generated migration file
import my_.utils
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('my_app', 'yyyy_auto_yyyyyyyy_yyyy'),
]
operations = [
migrations.AddField(
model_name='networks',
name='protocol',
# Field definition here, pay attention to the default value
field=models.CharField(choices=[('ETH', 'Ethernet'), ('MPLS', 'MPLS')], default=my_app.utils.NetworkProtocolList('Ethernet'), max_length=20),
),
]
Than I edited migration file to manually set the default to a string instead of calling enum class:
xxxx_auto_xxxxxxxx_xxxx.py
# Edited migration file
import my_.utils
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('my_app', 'yyyy_auto_yyyyyyyy_yyyy'),
]
operations = [
migrations.AddField(
model_name='networks',
name='protocol',
# Field definition here, pay attention to the modified default value
field=models.CharField(choices=[('ETH', 'Ethernet'), ('MPLS', 'MPLS')], default='Ethernet', max_length=20),
),
]
Now migration works properly but I wonder why I can't define default value like before using the enum instead of a litteral string, because I have other model field for whom this has worked properly.
Is that a bug, what am I lissing here, How to set default value for a model field based on enum in Django?
Since you are using Django<3.X, Django isn't able to identify the enum value. So, use the .value property of enum class as
protocol = models.CharField(
choices=NetworkProtocolList.choices(),
max_length=20,
default=NetworkProtocolList.ETH.value # &lt--- change is here
)

Converting Django BooleanField to NullBooleanField and changing default

I need to change a field from
my_boolean = models.BooleanField(verbose_name="Safe Visiting Space", default=False)
to
my_boolean = models.NullBooleanField(verbose_name="Safe Visiting Space", default=None, blank=True, null=True)
So I've made the above change within the model and run makemigrations to create
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('my_app', '0164_auto_20201027_0820'),
]
operations = [
migrations.AlterField(
model_name='mymodel',
name='my_boolean',
field=models.NullBooleanField(default=None, verbose_name='Safe Visiting Space'),
),
]
But this will not set the default of the current 70k records to None but will leave them as False, so I amended the migrations file to
from django.db import migrations, models
from my_app.models import MyModel
def set_my_boolean_default(apps, schema_editor):
objects= MyModel.objects.active().filter(my_boolean=False)
for object in objectss:
object.my_boolean = None
object.save()
class Migration(migrations.Migration):
dependencies = [
('providers', '0164_auto_20201027_0820'),
]
operations = [
migrations.AlterField(
model_name='organisation',
name='infection_control_safe_visiting_space',
field=models.NullBooleanField(default=None, verbose_name='Safe Visiting Space'),
),
migrations.RunPython(set_my_boolean_default),
]
This will take hours to run. Also, a random check of the database and it doesn't seem to be updating any of the records.
What is the right / better way to do this?
It's taking you so long because of this for loop:
objects= MyModel.objects.active().filter(my_boolean=False)
for object in objects:
object.my_boolean = None
object.save() # Database roundtrip
Namely, for each object, you are hitting the database. The better way is to bulk_update the fields all at once:
objects= MyModel.objects.active().filter(my_boolean=False).update(my_boolean=None)
You can perform the query above inside your shell. It's better not to include it in your migration files for it may be executed each time you run migrations.
Enter you Django shell by python manage.py shell, import your model, and execute the query above.

Default value for foreign key in Django migrations.AddField

Using migrations, I need to add a new field (a foreign key) to a model. I know it can be done with:
migrations.AddField(
model_name='MyModel',
name='state',
field=models.ForeignKey(null=True, related_name='mymodel_state', to='msqa_common.MyModelState'),
),
However, I don't want my field to be nullable. Instead, I want to use a default value for it, corresponding to the id of MyModelState whose name is "available" (id value might change in different machines). This "available" value of table MyModelState is inserted into the database in a previous migration script, so it does exist.
I guess I should do something like:
migrations.AddField(
model_name='MyModel',
name='state',
field=models.ForeignKey(null=False, default=available_state_id, related_name='mymodel_state', to='msqa_common.MyModelState'),
),
My question: How can I get the available_state_id within my migration script?
You can't do it directly. The recommended way of doing this is to create a migration to add it with null=True, then add a data migration that uses either Python or SQL to update all the existing ones to point to available_state_id, then a third migration that changes it to null=False.
I just had the same issue and stumbled upon this answer, so here is how I did it:
operations = [
# We are forced to create the field as non-nullable before
# assigning each Car to a Brand
migrations.AddField(
model_name="car",
name="brand",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="model.Brand",
),
),
# assign_car_to_brand loops over all my Car objects and sets their
# "brand" field
migrations.RunPython(add_category_to_tags, do_nothing),
# Make the field non-nullable to force all future Car to have a Brand
migrations.AlterField(
model_name="car",
name="brand",
field=models.ForeignKey(
null=False,
on_delete=django.db.models.deletion.PROTECT,
to="model.Brand",
),
preserve_default=False
),
]
Here is a relatively complete example:
Step One
python manage.py makemigrations, set the temporary default value to None
Step Two
Change the genrated migration code to below style
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
def set_default_author_to_blog(apps, schema_editor):
User = apps.get_model("auth", "User")
Blog = apps.get_model("blog", "Blog")
Blog.objects.update(author=User.objects.first())
def revert_set_default_autor_to_blog(apps, schema_editor):
Blog = apps.get_model("blog", "Blog")
Blog.objects.update(author=None)
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0001_auto_20220425_1017'),
]
operations = [
migrations.AddField(
model_name='blog',
name='author',
field=models.ForeignKey(null=True, db_constraint=False, on_delete=django.db.models.deletion.PROTECT,
to='auth.user', verbose_name='Author')
),
migrations.RunPython(set_default_author_to_blog, reverse_code=revert_set_default_autor_to_blog),
migrations.AlterField(
model_name='blog',
name='author',
field=models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.PROTECT,
to='auth.user', verbose_name='Author')
),
]
Step Three
python manage.py migrate

How to remove index varchar_pattern_ops in a django (1.8) migration?

When creating a model with a models.varchar(...) field, a varchar_pattern_ops index is being created.
This is the table generated in postgresql
Table "public.logger_btilog"
Column | Type | Modifiers
------------------+--------------------------+-----------
md5hash | text |
id | integer | not null
Indexes:
"logger_btilog_pkey" PRIMARY KEY, btree (id)
"logger_btilog_md5hash_6454d7bb20588b61_like" btree (md5hash varchar_pattern_ops)
I want to remove that varchar_pattern_ops index in a migration, and add a hash index in that field.
I tried doing this:
# models.py
class Btilog(models.Model):
md5hash = models.TextField(db_index=False)
[...]
And in migration also force adding db_field=False
# 0013_migration.py
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('logger', '0014_btilog_id'),
]
operations = [
# this should remove all indexes for md5hash, but it does not work
migrations.AlterField(
model_name='btilog',
name='md5hash',
field=models.TextField(null=True, blank=True, db_index=False),
),
migrations.RunSQL(
"create index logger_btilog_md5hash_hashindex on logger_btilog using hash(md5hash);",
"drop index logger_btilog_md5hash_hashindex;"
),
]
After running the migrations, this are the indexes in the database
relation | size
--------------------------------------------------------------------+---------
public.logger_btilog | 7185 MB
public.logger_btilog_md5hash_6454d7bb20588b61_like | 1442 MB
public.logger_btilog_md5hash_hashindex | 1024 MB
public.logger_btilog_pkey | 548 MB
Note that public.logger_btilog_md5hash_6454d7bb20588b61_like is the index I want to delete. This index is being added automatically by django, see this
More info on that index
vtfx=# \d logger_btilog_md5hash_6454d7bb20588b61_like
Index "public.logger_btilog_md5hash_6454d7bb20588b61_like"
Column | Type | Definition
---------+------+------------
md5hash | text | md5hash
btree, for table "public.logger_btilog"
Footnote: I'm not confused about the usage of a hash index, I only want to do = (strictrly equal) where searches in md5hash field, then (casually) a hash index would be the fastest and will occupy less space than a btree index (django's default)
Answer update notice:
django < 1.11: Use this answer
django >= 1.11: Use #Cesar Canassa's answer
Ok, I found some info here https://docs.djangoproject.com/en/1.8/_modules/django/db/backends/base/schema/#BaseDatabaseSchemaEditor.alter_field
And made a manual RunPython migration to delete the varchar_pattern_ops index using the SchemaEditor
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import re
def drop_md5hash_varchar_pattern_ops_index(apps, schemaEditor):
# code based on https://docs.djangoproject.com/en/1.8/_modules/django/db/backends/base/schema/#BaseDatabaseSchemaEditor.alter_field
model = apps.get_model("logger", "Btilog")
index_names = schemaEditor._constraint_names(model, index=True)
for index_name in index_names:
if re.search('logger_btilog_md5hash_.+_like', index_name):
print 'dropping index {}'.format(index_name)
schemaEditor.execute(schemaEditor._delete_constraint_sql(schemaEditor.sql_delete_index, model, index_name))
class Migration(migrations.Migration):
dependencies = [
('logger', '0012_auto_20150529_1745'),
]
operations = [
# Remove the annoying index using a hack
migrations.RunPython(
drop_md5hash_varchar_pattern_ops_index
),
]
You can avoid the creation of the varchar_pattern_ops "LIKE" index altogether by using the new Model Meta indexes option that was added in Django 1.11. For example, instead of writing your Model like this:
class MyModel(models.Model):
my_field = models.CharField(max_length=64, db_index=True)
You need to set the index with the Model Meta option:
class MyModel(models.Model):
my_field = models.CharField(max_length=64)
class Meta:
indexes = [
models.Index(fields=['my_field'])
]
By doing so, Django will not create the duplicated index.
I'm using Django 3.0.11 and I had same problem. I don't want _like index for primary key since it's used only for joins and exact matching.
Defining index manually won't help if field is primary_key. So I came up with hacky solution similar to first answer.
from django.db import migrations, models
def drop_pattern_ops_index(schema_editor, model, field):
indexes = schema_editor._model_indexes_sql(model)
for index in indexes:
columns = index.parts["columns"].columns
opclasses = index.parts["columns"].opclasses
if field in columns and len(columns) == 1:
# Django uses varchar_pattern_ops or text_pattern_ops
if "pattern_ops" in opclasses[0]:
drop_sql = schema_editor._delete_index_sql(
model, str(index.parts["name"]).strip('"')
)
# Indexes creation is always deferred in migration
# so we have to execute remove at the end
schema_editor.deferred_sql.extend([drop_sql])
break
def drop_extra_indexes(apps, schema_editor):
model = apps.get_model("com_erp", "TestModel")
drop_pattern_ops_index(schema_editor, model, "code")
class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name="TestModel",
fields=[
("code", models.TextField(primary_key=True, serialize=False)),
("name", models.TextField()),
],
options={"db_table": "test_table",},
),
migrations.RunPython(drop_extra_indexes, migrations.RunPython.noop),
]
Complementing #Cesar Canassa's answer for the cases of a unique field.
To avoid the duplicated indexes you have to drop the unique=True in the field and add this constraint in the Meta class:
class MyModel(models.Model):
my_field = models.CharField(max_length=64)
my_unique_field = models.CharField(max_length=64)
class Meta:
constraints = [
models.UniqueConstraint(
name='unique_field_constraint',
fields=['my_unique_field']
),
indexes = [
models.Index(fields=['my_field'])
]

Assign the value of old column to new column using Django data migration

I got a problem when I wanted to change a column's type from int to char. But I could't do this directly via Django migrations. so I firstly added a new column and wanted to set its value with the old column's, the SQL is like 'update my_app_myqpp set uuid=id'. How can I do this via Django migrations? Thanks!
def migrate_data(apps, schema_editor):
MyApp = apps.get_model('my_app', 'MyApp')
db_alias = schema_editor.connection.alias
results = MyApp.objects.using(db_alias)
for result in results:
result.uuid = str(result.id)
result.save()
class Migration(migrations.Migration):
dependencies = [
('myq_app', '0002_auto_20150205_0501'),
]
operations = [
migrations.AddField(
model_name='myapp',
name='uuid',
field=api.fields.UuidField(auto_created=True, default='0', editable=False, max_length=32, unique=True, verbose_name='UUID'),
preserve_default=True,
),
migrations.RunPython(migrate_data)
]
I also tried
MyApp.objects.raw('UPDATE docker_build_dockerbuild SET uuid=id')
but it seemed migrate_data was not performed.
finally I got my problem solved. The way is simple. I separated the migrations to two steps, one was to just add the new field and the other one was to migrate the data. the reason is that '0002_auto_20150205_0501' didn't include the new field. so 'result.save' didn't update the non-exist column 'uuid' even result.uuid was set a value