Run into issues recently with being unable to save models in the admin due to a unique constraint failing validation when it shouldn’t.
This first showed up when I was unable to edit existing objects in the admin as they failed the validation there. However I was able to get them into the db via the ORM directly. So the constraint is raising a false positive on the python side.
Similar posts on here didn’t show up this behaviour. However the documentation on constraints is quite thin so posting on here incase I’m missing something before filing a ticket for it.
Minimal example with test to reproduce:
# models.py
from django.db import models
class Container(models.Model):
name = models.CharField(max_length=10)
class Timestamped(models.Model):
timestamp = models.DateTimeField()
key = models.IntegerField()
container = models.ForeignKey(Container, on_delete=models.CASCADE)
test_label = models.TextField(max_length=50, blank=True)
class Meta:
constraints = [
# triggering validation errors for objects that can actually be saved
models.UniqueConstraint(
models.F("timestamp__date"), "container", "key",
name="%(app_label)s_%(class)s_unique_key_per_date_per_container",
),
]
# tests.py
import datetime as dt
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.test import TestCase
from uniqueconstraintbug.models import Container, Timestamped
class TestUniqueConstraintOnDateTime(TestCase):
@classmethod
def setUpTestData(cls):
cls.ts = dt.datetime(2025,2,13,10,2,0, tzinfo=dt.UTC)
cls.ts_same_date = dt.datetime.combine(cls.ts, dt.time(21,30), tzinfo=dt.UTC)
cls.ts_different_date = cls.ts - dt.timedelta(days=7)
cls.a = Container.objects.create(name='A')
cls.b = Container.objects.create(name='B')
cls.existing = Timestamped.objects.create(timestamp=cls.ts, container=cls.a, key=1)
# other tests verify the constraint is working as expected in other situations but left out here to save space
def test_unexpected_constraint_failures_on_timestamp(self):
new_date = Timestamped(timestamp=self.ts_different_date, container=self.a, key=1, test_label="new_date")
# test will fail here unless we assert the validation error
# even though date is different
with self.assertRaises(ValidationError):
new_date.validate_constraints()
# can save even though validation error raised
new_date.save()
self.assertTrue(new_date.pk)
self.assertNotEqual(new_date.timestamp.date(), self.existing.timestamp.date())
The constraint does work as expected if all three fields are the same, or for different values of key
and container
between instances, so it seems like somewhere along the process something about the timestamp comparisons is going wrong.
Edit
Extra data I forgot to append. Stack trace from failing version of test:
python manage.py test uniqueconstraintbug.tests.TestUniqueConstraintOnDateTime.test_unexpected_constraint_failures_on_timestamp
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_unexpected_constraint_failures_on_timestamp (uniqueconstraintbug.tests.TestUniqueConstraintOnDateTime.test_unexpected_constraint_failures_on_timestamp)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/tom/dev/projects/training_log/uniqueconstraintbug/tests.py", line 51, in test_unexpected_constraint_failures_on_timestamp
new_date.validate_constraints()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/Users/tom/dev/projects/training_log/.venv/lib/python3.13/site-packages/django/db/models/base.py", line 1604, in validate_constraints
raise ValidationError(errors)
django.core.exceptions.ValidationError: {'__all__': ['Constraint “uniqueconstraintbug_timestamped_unique_key_per_date_per_container” is violated.']}
----------------------------------------------------------------------
Ran 1 test in 0.002s
FAILED (errors=1)
Destroying test database for alias 'default'...
Output from sqlmigrate
showing what seems to be correct creation of constraint at DB level in SQLite3:
-- Create model Timestamped
--
CREATE TABLE "uniqueconstraintbug_timestamped" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "timestamp" datetime NOT NULL, "key" integer NOT NULL, "container_id" bigint NOT NULL REFERENCES "uniqueconstraintbug_container" ("id") DEFERRABLE INITIALLY DEFERRED, "test_label" text NOT NULL);
--
CREATE UNIQUE INDEX "uniqueconstraintbug_timestamped_unique_key_per_date_per_container" ON "uniqueconstraintbug_timestamped" ((django_datetime_cast_date("timestamp", 'UTC', 'UTC')), "container_id", "key");
---
Django 5.1.5, Python 3.13.1