1
0
mirror of https://github.com/django/django.git synced 2024-12-23 01:25:58 +00:00

Fixed #34840 -- Avoided casting string base fields on PostgreSQL.

Thanks Alex Vandiver for the report.

Regression in 09ffc5c121.
This commit is contained in:
Mariusz Felisiak 2023-09-22 06:01:11 +02:00 committed by GitHub
parent 78b5c90753
commit 779cd28acb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 78 additions and 12 deletions

View File

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

View File

@ -624,11 +624,15 @@ class IsNull(BuiltinLookup):
raise ValueError( raise ValueError(
"The QuerySet value for an isnull lookup must be True or False." "The QuerySet value for an isnull lookup must be True or False."
) )
if isinstance(self.lhs, Value) and self.lhs.value is None: if isinstance(self.lhs, Value):
if self.rhs: if self.lhs.value is None or (
raise FullResultSet self.lhs.value == ""
and connection.features.interprets_empty_strings_as_nulls
):
result_exception = FullResultSet if self.rhs else EmptyResultSet
else: else:
raise EmptyResultSet result_exception = EmptyResultSet if self.rhs else FullResultSet
raise result_exception
sql, params = self.process_lhs(compiler, connection) sql, params = self.process_lhs(compiler, connection)
if self.rhs: if self.rhs:
return "%s IS NULL" % sql, params 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 * Fixed a regression in Django 4.2.5 where overriding the deprecated
``DEFAULT_FILE_STORAGE`` and ``STATICFILES_STORAGE`` settings in tests caused ``DEFAULT_FILE_STORAGE`` and ``STATICFILES_STORAGE`` settings in tests caused
the main ``STORAGES`` to mutate (:ticket:`34821`). 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

@ -369,6 +369,20 @@ class Tests(TestCase):
with self.subTest(lookup=lookup): with self.subTest(lookup=lookup):
self.assertIn("::text", do.lookup_cast(lookup)) self.assertIn("::text", do.lookup_cast(lookup))
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): def test_correct_extraction_psycopg_version(self):
from django.db.backends.postgresql.base import Database, psycopg_version from django.db.backends.postgresql.base import Database, psycopg_version

View File

@ -995,6 +995,42 @@ class UniqueConstraintTests(TestCase):
exclude={"name"}, 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): def test_name(self):
constraints = get_constraints(UniqueConstraintProduct._meta.db_table) constraints = get_constraints(UniqueConstraintProduct._meta.db_table)
expected_name = "name_color_uniq" expected_name = "name_color_uniq"

View File

@ -1337,6 +1337,16 @@ class LookupTests(TestCase):
with self.assertRaisesMessage(ValueError, msg): with self.assertRaisesMessage(ValueError, msg):
qs.exists() 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): def test_lookup_rhs(self):
product = Product.objects.create(name="GME", qty_target=5000) product = Product.objects.create(name="GME", qty_target=5000)
stock_1 = Stock.objects.create(product=product, short=True, qty_available=180) stock_1 = Stock.objects.create(product=product, short=True, qty_available=180)