1
0
mirror of https://github.com/django/django.git synced 2024-12-27 19:46:22 +00:00

[5.0.x] Fixed #34840 -- Avoided casting string base fields on PostgreSQL.

Thanks Alex Vandiver for the report.

Regression in 09ffc5c121.

Backport of 779cd28acb from main.
This commit is contained in:
Mariusz Felisiak 2023-09-22 06:01:11 +02:00
parent fb5dd118e9
commit 2566f92544
6 changed files with 78 additions and 15 deletions

View File

@ -154,17 +154,6 @@ class DatabaseOperations(BaseDatabaseOperations):
def lookup_cast(self, lookup_type, internal_type=None):
lookup = "%s"
if lookup_type == "isnull" and internal_type in (
"CharField",
"EmailField",
"TextField",
"CICharField",
"CIEmailField",
"CITextField",
):
return "%s::text"
# Cast text lookups to text to allow things like filter(x__contains=4)
if lookup_type in (
"iexact",

View File

@ -624,11 +624,15 @@ class IsNull(BuiltinLookup):
raise ValueError(
"The QuerySet value for an isnull lookup must be True or False."
)
if isinstance(self.lhs, Value) and self.lhs.value is None:
if self.rhs:
raise FullResultSet
if isinstance(self.lhs, Value):
if self.lhs.value is None or (
self.lhs.value == ""
and connection.features.interprets_empty_strings_as_nulls
):
result_exception = FullResultSet if self.rhs else EmptyResultSet
else:
raise EmptyResultSet
result_exception = EmptyResultSet if self.rhs else FullResultSet
raise result_exception
sql, params = self.process_lhs(compiler, connection)
if self.rhs:
return "%s IS NULL" % sql, params

View File

@ -12,3 +12,13 @@ Bugfixes
* Fixed a regression in Django 4.2.5 where overriding the deprecated
``DEFAULT_FILE_STORAGE`` and ``STATICFILES_STORAGE`` settings in tests caused
the main ``STORAGES`` to mutate (:ticket:`34821`).
* Fixed a regression in Django 4.2 that caused unnecessary casting of string
based fields (``CharField``, ``EmailField``, ``TextField``, ``CICharField``,
``CIEmailField``, and ``CITextField``) used with the ``__isnull`` lookup on
PostgreSQL. As a consequence, the pre-Django 4.2 indexes didn't match and
were not used by the query planner (:ticket:`34840`).
You may need to recreate indexes propagated to the database with Django
4.2 - 4.2.5 as they contain unnecessary ``::text`` casting that is avoided as
of this release.

View File

@ -376,6 +376,20 @@ class Tests(TestCase):
"::citext", do.lookup_cast(lookup, internal_type=field_type)
)
def test_lookup_cast_isnull_noop(self):
from django.db.backends.postgresql.operations import DatabaseOperations
do = DatabaseOperations(connection=None)
# Using __isnull lookup doesn't require casting.
tests = [
"CharField",
"EmailField",
"TextField",
]
for field_type in tests:
with self.subTest(field_type=field_type):
self.assertEqual(do.lookup_cast("isnull", field_type), "%s")
def test_correct_extraction_psycopg_version(self):
from django.db.backends.postgresql.base import Database, psycopg_version

View File

@ -995,6 +995,42 @@ class UniqueConstraintTests(TestCase):
exclude={"name"},
)
def test_validate_nullable_textfield_with_isnull_true(self):
is_null_constraint = models.UniqueConstraint(
"price",
"discounted_price",
condition=models.Q(unit__isnull=True),
name="uniq_prices_no_unit",
)
is_not_null_constraint = models.UniqueConstraint(
"price",
"discounted_price",
condition=models.Q(unit__isnull=False),
name="uniq_prices_unit",
)
Product.objects.create(price=2, discounted_price=1)
Product.objects.create(price=4, discounted_price=3, unit="ng/mL")
msg = "Constraint “uniq_prices_no_unit” is violated."
with self.assertRaisesMessage(ValidationError, msg):
is_null_constraint.validate(
Product, Product(price=2, discounted_price=1, unit=None)
)
is_null_constraint.validate(
Product, Product(price=2, discounted_price=1, unit="ng/mL")
)
is_null_constraint.validate(Product, Product(price=4, discounted_price=3))
msg = "Constraint “uniq_prices_unit” is violated."
with self.assertRaisesMessage(ValidationError, msg):
is_not_null_constraint.validate(
Product,
Product(price=4, discounted_price=3, unit="μg/mL"),
)
is_not_null_constraint.validate(Product, Product(price=4, discounted_price=3))
is_not_null_constraint.validate(Product, Product(price=2, discounted_price=1))
def test_name(self):
constraints = get_constraints(UniqueConstraintProduct._meta.db_table)
expected_name = "name_color_uniq"

View File

@ -1337,6 +1337,16 @@ class LookupTests(TestCase):
with self.assertRaisesMessage(ValueError, msg):
qs.exists()
def test_isnull_textfield(self):
self.assertSequenceEqual(
Author.objects.filter(bio__isnull=True),
[self.au2],
)
self.assertSequenceEqual(
Author.objects.filter(bio__isnull=False),
[self.au1],
)
def test_lookup_rhs(self):
product = Product.objects.create(name="GME", qty_target=5000)
stock_1 = Stock.objects.create(product=product, short=True, qty_available=180)