mirror of
https://github.com/django/django.git
synced 2024-12-28 12:06:22 +00:00
[5.0.x] Fixed #34840 -- Avoided casting string base fields on PostgreSQL.
Thanks Alex Vandiver for the report. Regression in09ffc5c121
. Backport of779cd28acb
from main.
This commit is contained in:
parent
fb5dd118e9
commit
2566f92544
@ -154,17 +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",
|
|
||||||
"CICharField",
|
|
||||||
"CIEmailField",
|
|
||||||
"CITextField",
|
|
||||||
):
|
|
||||||
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",
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -376,6 +376,20 @@ class Tests(TestCase):
|
|||||||
"::citext", do.lookup_cast(lookup, internal_type=field_type)
|
"::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):
|
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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user