Allow in_bulk() for fields with UniqueConstraint

Previously, in_bulk() required fields to have unique=True. Now, fields with an unconditional UniqueConstraint are also supported, enabling lookups like in_bulk(field_name="slug") for such fields.

Fixes issue where fields unique via UniqueConstraint could not be used with in_bulk().
This commit is contained in:
utkarsh.arya@zomato.com 2025-11-15 22:58:45 +00:00
parent 67f9d076cf
commit 1efb80ab0f
3 changed files with 63 additions and 3 deletions

View file

@ -17,6 +17,7 @@ from django.db import (
) )
from django.db.models import AutoField, DateField, DateTimeField, sql from django.db.models import AutoField, DateField, DateTimeField, sql
from django.db.models.constants import LOOKUP_SEP 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.deletion import Collector
from django.db.models.expressions import Case, Expression, F, Value, When from django.db.models.expressions import Case, Expression, F, Value, When
from django.db.models.functions import Cast, Trunc from django.db.models.functions import Cast, Trunc
@ -689,8 +690,19 @@ class QuerySet:
""" """
assert not self.query.is_sliced, \ assert not self.query.is_sliced, \
"Cannot use 'limit' or 'offset' with in_bulk" "Cannot use 'limit' or 'offset' with in_bulk"
if field_name != 'pk' and not self.model._meta.get_field(field_name).unique: if field_name != 'pk':
raise ValueError("in_bulk()'s field_name must be a unique field but %r isn't." % field_name) # 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 id_list is not None:
if not id_list: if not id_list:
return {} return {}

View file

@ -77,3 +77,13 @@ class AbstractModel(models.Model):
class ChildModel(AbstractModel): class ChildModel(AbstractModel):
pass 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'),
]

View file

@ -7,7 +7,7 @@ from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
from .models import ( from .models import (
ChildModel, Product, UniqueConstraintConditionProduct, ChildModel, Product, UniqueConstraintConditionProduct,
UniqueConstraintProduct, UniqueConstraintInBulkProduct, UniqueConstraintProduct,
) )
@ -238,3 +238,41 @@ class UniqueConstraintTests(TestCase):
def test_condition_must_be_q(self): def test_condition_must_be_q(self):
with self.assertRaisesMessage(ValueError, 'UniqueConstraint.condition must be a Q instance.'): with self.assertRaisesMessage(ValueError, 'UniqueConstraint.condition must be a Q instance.'):
models.UniqueConstraint(name='uniq', fields=['name'], condition='invalid') 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')