Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

- Avoid unnecessary prompting for a default value on `ManyToManyField`
removals. (#59)
- Address a ``makemigration`` crash when adding a ``ForeignKey`` with a
callable ``default``. (#60)

1.2.0
=====
Expand Down
18 changes: 17 additions & 1 deletion syzygy/operations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from contextlib import contextmanager

from django.db import models
from django.db.migrations import operations
from django.db.models.fields import NOT_PROVIDED
from django.utils.functional import cached_property
Expand Down Expand Up @@ -201,7 +202,22 @@ def get_pre_add_field_operation(model_name, name, field, preserve_default=True):
"Fields with a db_default don't require a pre-deployment operation."
)
field = field.clone()
field.db_default = field.get_default()
if isinstance(field, models.ForeignKey):
# XXX: Replicate ForeignKey.get_default() logic in a way that
# doesn't require the field references to be pre-emptively
# resolved. This will need to be fixed upstream if we ever
# implement model state schema alterations.
# See https://code.djangoproject.com/ticket/29898
field_default = super(models.ForeignKey, field).get_default()
if (
isinstance(field_default, models.Model)
and field_default._meta.label_lower == field.related_model.lower()
):
target_field = field.to_fields[0] or "pk"
field_default = getattr(field_default, target_field)
else:
field_default = field.get_default()
field.db_default = field_default
operation = operations.AddField(model_name, name, field, preserve_default)
return operation

Expand Down
7 changes: 7 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@

class Foo(models.Model):
pass


class Bar(models.Model):
name = models.CharField(max_length=100, unique=True)

class Meta:
managed = False
34 changes: 31 additions & 3 deletions tests/test_autodetector.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
)
from syzygy.plan import get_migration_stage

from .models import Bar


class AutodetectorTestCase(TestCase):
style = color_style()
Expand Down Expand Up @@ -53,7 +55,7 @@ def get_changes(


class AutodetectorTests(AutodetectorTestCase):
def _test_field_addition(self, field):
def _test_field_addition(self, field, expected_db_default=None):
from_model = ModelState("tests", "Model", [])
to_model = ModelState("tests", "Model", [("field", field)])
changes = self.get_changes([from_model], [to_model])["tests"]
Expand All @@ -64,7 +66,10 @@ def _test_field_addition(self, field):
pre_operation = changes[0].operations[0]
if field_db_default_supported:
self.assertIsInstance(pre_operation, migrations.AddField)
self.assertEqual(pre_operation.field.db_default, field.default)
self.assertEqual(
pre_operation.field.db_default,
expected_db_default or field.get_default(),
)
else:
self.assertIsInstance(pre_operation, AddField)
self.assertEqual(get_migration_stage(changes[1]), Stage.POST_DEPLOY)
Expand All @@ -81,10 +86,33 @@ def test_field_addition(self):
fields = [
models.IntegerField(default=42),
models.IntegerField(null=True, default=42),
models.IntegerField(default=lambda: 42),
# Foreign keys with callable defaults should have their associated
# db_default generated with care.
(models.ForeignKey("tests.Model", models.CASCADE, default=42), 42),
(
models.ForeignKey(
"tests.Bar", models.CASCADE, default=lambda: Bar(id=42)
),
42,
),
(
models.ForeignKey(
"tests.bar",
models.CASCADE,
to_field="name",
default=lambda: Bar(id=123, name="bar"),
),
"bar",
),
]
for field in fields:
if isinstance(field, tuple):
field, expected_db_default = field
else:
expected_db_default = None
with self.subTest(field=field):
self._test_field_addition(field)
self._test_field_addition(field, expected_db_default)

def test_many_to_many_addition(self):
from_model = ModelState("tests", "Model", [])
Expand Down
16 changes: 16 additions & 0 deletions tests/test_migrations/null_field_removal/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,20 @@ class Migration(migrations.Migration):
("bar", models.IntegerField()),
],
),
migrations.CreateModel(
"Bar",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(unique=True)),
],
options={"managed": False},
),
]
Loading