Fixed #29049 -- Added slicing notation to F expressions.

Co-authored-by: Priyansh Saxena <askpriyansh@gmail.com>
Co-authored-by: Niclas Olofsson <n@niclasolofsson.se>
Co-authored-by: David Smith <smithdc@gmail.com>
Co-authored-by: Mariusz Felisiak <felisiak.mariusz@gmail.com>
Co-authored-by: Abhinav Yadav <abhinav.sny.2002@gmail.com>
This commit is contained in:
Nick Pope 2023-12-30 07:24:30 +00:00 committed by GitHub
parent 561e16d6a7
commit 94b6f101f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 256 additions and 4 deletions

View file

@ -106,3 +106,7 @@ class UUIDPK(models.Model):
class UUID(models.Model):
uuid = models.UUIDField(null=True)
uuid_fk = models.ForeignKey(UUIDPK, models.CASCADE, null=True)
class Text(models.Model):
name = models.TextField()

View file

@ -84,6 +84,7 @@ from .models import (
RemoteEmployee,
Result,
SimulationRun,
Text,
Time,
)
@ -205,6 +206,100 @@ class BasicExpressionsTests(TestCase):
],
)
def _test_slicing_of_f_expressions(self, model):
tests = [
(F("name")[:], "Example Inc.", "Example Inc."),
(F("name")[:7], "Example Inc.", "Example"),
(F("name")[:6][:5], "Example", "Examp"), # Nested slicing.
(F("name")[0], "Examp", "E"),
(F("name")[5], "E", ""),
(F("name")[7:], "Foobar Ltd.", "Ltd."),
(F("name")[0:10], "Ltd.", "Ltd."),
(F("name")[2:7], "Test GmbH", "st Gm"),
(F("name")[1:][:3], "st Gm", "t G"),
(F("name")[2:2], "t G", ""),
]
for expression, name, expected in tests:
with self.subTest(expression=expression, name=name, expected=expected):
obj = model.objects.get(name=name)
obj.name = expression
obj.save()
obj.refresh_from_db()
self.assertEqual(obj.name, expected)
def test_slicing_of_f_expressions_charfield(self):
self._test_slicing_of_f_expressions(Company)
def test_slicing_of_f_expressions_textfield(self):
Text.objects.bulk_create(
[Text(name=company.name) for company in Company.objects.all()]
)
self._test_slicing_of_f_expressions(Text)
def test_slicing_of_f_expressions_with_annotate(self):
qs = Company.objects.annotate(
first_three=F("name")[:3],
after_three=F("name")[3:],
random_four=F("name")[2:5],
first_letter_slice=F("name")[:1],
first_letter_index=F("name")[0],
)
tests = [
("first_three", ["Exa", "Foo", "Tes"]),
("after_three", ["mple Inc.", "bar Ltd.", "t GmbH"]),
("random_four", ["amp", "oba", "st "]),
("first_letter_slice", ["E", "F", "T"]),
("first_letter_index", ["E", "F", "T"]),
]
for annotation, expected in tests:
with self.subTest(annotation):
self.assertCountEqual(qs.values_list(annotation, flat=True), expected)
def test_slicing_of_f_expression_with_annotated_expression(self):
qs = Company.objects.annotate(
new_name=Case(
When(based_in_eu=True, then=Concat(Value("EU:"), F("name"))),
default=F("name"),
),
first_two=F("new_name")[:3],
)
self.assertCountEqual(
qs.values_list("first_two", flat=True),
["Exa", "EU:", "Tes"],
)
def test_slicing_of_f_expressions_with_negative_index(self):
msg = "Negative indexing is not supported."
indexes = [slice(0, -4), slice(-4, 0), slice(-4), -5]
for i in indexes:
with self.subTest(i=i), self.assertRaisesMessage(ValueError, msg):
F("name")[i]
def test_slicing_of_f_expressions_with_slice_stop_less_than_slice_start(self):
msg = "Slice stop must be greater than slice start."
with self.assertRaisesMessage(ValueError, msg):
F("name")[4:2]
def test_slicing_of_f_expressions_with_invalid_type(self):
msg = "Argument to slice must be either int or slice instance."
with self.assertRaisesMessage(TypeError, msg):
F("name")["error"]
def test_slicing_of_f_expressions_with_step(self):
msg = "Step argument is not supported."
with self.assertRaisesMessage(ValueError, msg):
F("name")[::4]
def test_slicing_of_f_unsupported_field(self):
msg = "This field does not support slicing."
with self.assertRaisesMessage(NotSupportedError, msg):
Company.objects.update(num_chairs=F("num_chairs")[:4])
def test_slicing_of_outerref(self):
inner = Company.objects.filter(name__startswith=OuterRef("ceo__firstname")[0])
outer = Company.objects.filter(Exists(inner)).values_list("name", flat=True)
self.assertSequenceEqual(outer, ["Foobar Ltd."])
def test_arithmetic(self):
# We can perform arithmetic operations in expressions
# Make sure we have 2 spare chairs
@ -2359,6 +2454,12 @@ class ReprTests(SimpleTestCase):
repr(Func("published", function="TO_CHAR")),
"Func(F(published), function=TO_CHAR)",
)
self.assertEqual(
repr(F("published")[0:2]), "Sliced(F(published), slice(0, 2, None))"
)
self.assertEqual(
repr(OuterRef("name")[1:5]), "Sliced(OuterRef(name), slice(1, 5, None))"
)
self.assertEqual(repr(OrderBy(Value(1))), "OrderBy(Value(1), descending=False)")
self.assertEqual(repr(RawSQL("table.col", [])), "RawSQL(table.col, [])")
self.assertEqual(

View file

@ -10,7 +10,7 @@ from django.core import checks, exceptions, serializers, validators
from django.core.exceptions import FieldError
from django.core.management import call_command
from django.db import IntegrityError, connection, models
from django.db.models.expressions import Exists, OuterRef, RawSQL, Value
from django.db.models.expressions import Exists, F, OuterRef, RawSQL, Value
from django.db.models.functions import Cast, JSONObject, Upper
from django.test import TransactionTestCase, override_settings, skipUnlessDBFeature
from django.test.utils import isolate_apps
@ -594,6 +594,40 @@ class TestQuerying(PostgreSQLTestCase):
[None, [1], [2], [2, 3], [20, 30]],
)
def test_slicing_of_f_expressions(self):
tests = [
(F("field")[:2], [1, 2]),
(F("field")[2:], [3, 4]),
(F("field")[1:3], [2, 3]),
(F("field")[3], [4]),
(F("field")[:3][1:], [2, 3]), # Nested slicing.
(F("field")[:3][1], [2]), # Slice then index.
]
for expression, expected in tests:
with self.subTest(expression=expression, expected=expected):
instance = IntegerArrayModel.objects.create(field=[1, 2, 3, 4])
instance.field = expression
instance.save()
instance.refresh_from_db()
self.assertEqual(instance.field, expected)
def test_slicing_of_f_expressions_with_annotate(self):
IntegerArrayModel.objects.create(field=[1, 2, 3])
annotated = IntegerArrayModel.objects.annotate(
first_two=F("field")[:2],
after_two=F("field")[2:],
random_two=F("field")[1:3],
).get()
self.assertEqual(annotated.first_two, [1, 2])
self.assertEqual(annotated.after_two, [3])
self.assertEqual(annotated.random_two, [2, 3])
def test_slicing_of_f_expressions_with_len(self):
queryset = NullableIntegerArrayModel.objects.annotate(
subarray=F("field")[:1]
).filter(field__len=F("subarray__len"))
self.assertSequenceEqual(queryset, self.objs[:2])
def test_usage_in_subquery(self):
self.assertSequenceEqual(
NullableIntegerArrayModel.objects.filter(