diff --git a/django/db/models/query.py b/django/db/models/query.py index d9c9b0db04..15d63745df 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -17,6 +17,7 @@ from django.db import ( ) from django.db.models import AutoField, DateField, DateTimeField, sql from django.db.models.constants import LOOKUP_SEP +from django.db.models.constraints import UniqueConstraint from django.db.models.deletion import Collector from django.db.models.expressions import Case, Expression, F, Value, When from django.db.models.functions import Cast, Trunc @@ -689,8 +690,19 @@ class QuerySet: """ assert not self.query.is_sliced, \ "Cannot use 'limit' or 'offset' with in_bulk" - if field_name != 'pk' and not self.model._meta.get_field(field_name).unique: - raise ValueError("in_bulk()'s field_name must be a unique field but %r isn't." % field_name) + if field_name != 'pk': + # Check if field is unique via field.unique or UniqueConstraint + field = self.model._meta.get_field(field_name) + if not field.unique: + # Check if field has a total UniqueConstraint (single field, no condition) + is_unique_constraint = any( + isinstance(constraint, UniqueConstraint) and + constraint.fields == (field_name,) and + constraint.condition is None + for constraint in self.model._meta.constraints + ) + if not is_unique_constraint: + raise ValueError("in_bulk()'s field_name must be a unique field but %r isn't." % field_name) if id_list is not None: if not id_list: return {} diff --git a/tests/constraints/models.py b/tests/constraints/models.py index 98955498d4..2fccd04ef0 100644 --- a/tests/constraints/models.py +++ b/tests/constraints/models.py @@ -77,3 +77,13 @@ class AbstractModel(models.Model): class ChildModel(AbstractModel): pass + + +class UniqueConstraintInBulkProduct(models.Model): + """Model for testing in_bulk() with UniqueConstraint on a single field.""" + name = models.CharField(max_length=255) + + class Meta: + constraints = [ + models.UniqueConstraint(fields=['name'], name='unique_name'), + ] diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index 85edb51aa7..9d308e8d86 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -7,7 +7,7 @@ from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from .models import ( ChildModel, Product, UniqueConstraintConditionProduct, - UniqueConstraintProduct, + UniqueConstraintInBulkProduct, UniqueConstraintProduct, ) @@ -238,3 +238,41 @@ class UniqueConstraintTests(TestCase): def test_condition_must_be_q(self): with self.assertRaisesMessage(ValueError, 'UniqueConstraint.condition must be a Q instance.'): models.UniqueConstraint(name='uniq', fields=['name'], condition='invalid') + + def test_in_bulk_with_unique_constraint(self): + """Test that in_bulk() works with UniqueConstraint on a single field.""" + # Create test objects + obj1 = UniqueConstraintInBulkProduct.objects.create(name='product1') + obj2 = UniqueConstraintInBulkProduct.objects.create(name='product2') + obj3 = UniqueConstraintInBulkProduct.objects.create(name='product3') + + # Test in_bulk with field_name that has UniqueConstraint + result = UniqueConstraintInBulkProduct.objects.in_bulk( + ['product1', 'product2', 'product3'], + field_name='name' + ) + + self.assertEqual(len(result), 3) + self.assertEqual(result['product1'], obj1) + self.assertEqual(result['product2'], obj2) + self.assertEqual(result['product3'], obj3) + + def test_in_bulk_without_id_list_with_unique_constraint(self): + """Test that in_bulk() works without id_list for UniqueConstraint fields.""" + obj1 = UniqueConstraintInBulkProduct.objects.create(name='product_a') + obj2 = UniqueConstraintInBulkProduct.objects.create(name='product_b') + + result = UniqueConstraintInBulkProduct.objects.in_bulk(field_name='name') + + self.assertEqual(len(result), 2) + self.assertEqual(result['product_a'], obj1) + self.assertEqual(result['product_b'], obj2) + + @skipUnlessDBFeature('supports_partial_indexes') + def test_in_bulk_with_partial_unique_constraint_fails(self): + """Test that in_bulk() fails for fields with conditional UniqueConstraint.""" + UniqueConstraintConditionProduct.objects.create(name='p1') + + msg = "in_bulk()'s field_name must be a unique field but 'name' isn't." + with self.assertRaisesMessage(ValueError, msg): + UniqueConstraintConditionProduct.objects.in_bulk(['p1'], field_name='name')