Repository: HypothesisWorks/hypothesis

Issue:
Duplicate primary key while creating instances of Django model
I'm trying to create a strategy for testing a Django model, and I'm repeatedly getting integrity errors for a duplicate primary key.

I believe the problem is in the way that Django objects are constructed using `get_or_create` when the primary key is one of the values generated as part of the strategy. We shouldn't pass all the fields to `get_or_create`, we should pass the primary key as a keyword arg and the remaining values as the `defaults` keyword arg. This way only the primary key is used for determining duplicate rows, rather than requiring the values of all fields to match.

This problem hit because my model has both a `OneToOneField` with `primary_key=True` and an `AutoLastModifiedField`. What I think happens is:

- The instance is created and written to DB. The `AutoLastModifiedField` updates the value of `modified` at the last minute.
- Hypothesis creates the same values and tries to create the object with `get_or_create`. However, this currently has to match every field. The existing row doesn't match because the `modified` value has changed, so a new object is created.
- The new object can't be created, because there is a clashing primary key.

```python
    class User(AbstractBaseUser):
        uuid = models.UUIDField(
            # ...
        )

        first_name = models.CharField(max_length=64)
        last_name = models.CharField(max_length=64)

    class Teacher(models.Model):
        user = models.OneToOneField(
            User,
            on_delete=models.CASCADE,
            primary_key=True
        )

        modified = AutoLastModifiedField('modified')
```


Discussion:

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/extra/django/models.py b/hypothesis-python/src/hypothesis/extra/django/models.py
--- a/hypothesis-python/src/hypothesis/extra/django/models.py
+++ b/hypothesis-python/src/hypothesis/extra/django/models.py
@@ -213,6 +213,21 @@ def models(
         raise InvalidArgument(
             u'Missing arguments for mandatory field%s %s for model %s'
             % (u's' if missed else u'', u', '.join(missed), model.__name__))
+
+    for field in result:
+        if model._meta.get_field(field).primary_key:
+            # The primary key is generated as part of the strategy. We
+            # want to find any existing row with this primary key and
+            # overwrite its contents.
+            kwargs = {field: result.pop(field)}
+            kwargs['defaults'] = st.fixed_dictionaries(result)
+            return _models_impl(
+                st.builds(model.objects.update_or_create, **kwargs)
+            )
+
+    # The primary key is not generated as part of the strategy, so we
+    # just match against any row that has the same value for all
+    # fields.
     return _models_impl(st.builds(model.objects.get_or_create, **result))
 
 


Test patch:
diff --git a/hypothesis-python/tests/django/toystore/models.py b/hypothesis-python/tests/django/toystore/models.py
--- a/hypothesis-python/tests/django/toystore/models.py
+++ b/hypothesis-python/tests/django/toystore/models.py
@@ -141,3 +141,21 @@ class RestrictedFields(models.Model):
         validators=[validate_even]
     )
     non_blank_text_field = models.TextField(blank=False)
+
+
+class SelfModifyingField(models.IntegerField):
+    def pre_save(self, model_instance, add):
+        value = getattr(model_instance, self.attname)
+        value += 1
+        setattr(model_instance, self.attname, value)
+        return value
+
+
+class CompanyExtension(models.Model):
+    company = models.OneToOneField(
+        Company,
+        primary_key=True,
+        on_delete=models.CASCADE
+    )
+
+    self_modifying = SelfModifyingField()
diff --git a/hypothesis-python/tests/django/toystore/test_given_models.py b/hypothesis-python/tests/django/toystore/test_given_models.py
--- a/hypothesis-python/tests/django/toystore/test_given_models.py
+++ b/hypothesis-python/tests/django/toystore/test_given_models.py
@@ -24,16 +24,18 @@
 from django.contrib.auth.models import User
 
 from hypothesis import HealthCheck, given, assume, settings
-from hypothesis.errors import InvalidArgument
-from hypothesis.strategies import just, lists
+from hypothesis.errors import InvalidArgument, HypothesisException
+from hypothesis.control import reject
+from hypothesis.strategies import just, lists, binary
 from hypothesis.extra.django import TestCase, TransactionTestCase
 from hypothesis.internal.compat import text_type
 from tests.django.toystore.models import Store, Company, Customer, \
     SelfLoop, Customish, ManyTimes, OddFields, ManyNumerics, \
-    CustomishField, CouldBeCharming, CustomishDefault, RestrictedFields, \
-    MandatoryComputed
+    CustomishField, CouldBeCharming, CompanyExtension, CustomishDefault, \
+    RestrictedFields, MandatoryComputed
 from hypothesis.extra.django.models import models, default_value, \
     add_default_field_mapping
+from hypothesis.internal.conjecture.data import ConjectureData
 
 add_default_field_mapping(CustomishField, just(u'a'))
 
@@ -135,6 +137,27 @@ def test_no_null_in_charfield(self, x):
         # regression test for #1045.  Company just has a convenient CharField.
         assert u'\x00' not in x.name
 
+    @given(binary(min_size=10))
+    def test_foreign_key_primary(self, buf):
+        # Regression test for #1307
+        company_strategy = models(
+            Company,
+            name=just('test')
+        )
+        strategy = models(
+            CompanyExtension,
+            company=company_strategy,
+            self_modifying=just(2)
+        )
+        try:
+            ConjectureData.for_buffer(buf).draw(strategy)
+        except HypothesisException as e:
+            reject()
+        # Draw again with the same buffer. This will cause a duplicate
+        # primary key.
+        ConjectureData.for_buffer(buf).draw(strategy)
+        assert CompanyExtension.objects.all().count() == 1
+
 
 class TestsNeedingRollback(TransactionTestCase):