diff --git a/tests/db_functions/tests.py b/tests/db_functions/tests.py index 4331e02ef62..507d2669b8e 100644 --- a/tests/db_functions/tests.py +++ b/tests/db_functions/tests.py @@ -6,8 +6,7 @@ from django.db import connection from django.db.models import CharField, TextField, Value as V from django.db.models.expressions import RawSQL from django.db.models.functions import ( - Coalesce, Concat, ConcatPair, Greatest, Least, Length, Lower, Now, - StrIndex, Substr, Upper, + Coalesce, Greatest, Least, Length, Lower, Now, Upper, ) from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature from django.utils import timezone @@ -311,214 +310,6 @@ class FunctionTests(TestCase): [obj], ) - def test_concat(self): - Author.objects.create(name='Jayden') - Author.objects.create(name='John Smith', alias='smithj', goes_by='John') - Author.objects.create(name='Margaret', goes_by='Maggie') - Author.objects.create(name='Rhonda', alias='adnohR') - - authors = Author.objects.annotate(joined=Concat('alias', 'goes_by')) - self.assertQuerysetEqual( - authors.order_by('name'), [ - '', - 'smithjJohn', - 'Maggie', - 'adnohR', - ], - lambda a: a.joined - ) - - with self.assertRaisesMessage(ValueError, 'Concat must take at least two expressions'): - Author.objects.annotate(joined=Concat('alias')) - - def test_concat_many(self): - Author.objects.create(name='Jayden') - Author.objects.create(name='John Smith', alias='smithj', goes_by='John') - Author.objects.create(name='Margaret', goes_by='Maggie') - Author.objects.create(name='Rhonda', alias='adnohR') - - authors = Author.objects.annotate( - joined=Concat('name', V(' ('), 'goes_by', V(')'), output_field=CharField()), - ) - - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'Jayden ()', - 'John Smith (John)', - 'Margaret (Maggie)', - 'Rhonda ()', - ], - lambda a: a.joined - ) - - def test_concat_mixed_char_text(self): - Article.objects.create(title='The Title', text=lorem_ipsum, written=timezone.now()) - article = Article.objects.annotate( - title_text=Concat('title', V(' - '), 'text', output_field=TextField()), - ).get(title='The Title') - self.assertEqual(article.title + ' - ' + article.text, article.title_text) - - # wrap the concat in something else to ensure that we're still - # getting text rather than bytes - article = Article.objects.annotate( - title_text=Upper(Concat('title', V(' - '), 'text', output_field=TextField())), - ).get(title='The Title') - expected = article.title + ' - ' + article.text - self.assertEqual(expected.upper(), article.title_text) - - @skipUnless(connection.vendor == 'sqlite', "sqlite specific implementation detail.") - def test_concat_coalesce_idempotent(self): - pair = ConcatPair(V('a'), V('b')) - # Check nodes counts - self.assertEqual(len(list(pair.flatten())), 3) - self.assertEqual(len(list(pair.coalesce().flatten())), 7) # + 2 Coalesce + 2 Value() - self.assertEqual(len(list(pair.flatten())), 3) - - def test_concat_sql_generation_idempotency(self): - qs = Article.objects.annotate(description=Concat('title', V(': '), 'summary')) - # Multiple compilations should not alter the generated query. - self.assertEqual(str(qs.query), str(qs.all().query)) - - def test_lower(self): - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - authors = Author.objects.annotate(lower_name=Lower('name')) - - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'john smith', - 'rhonda', - ], - lambda a: a.lower_name - ) - - Author.objects.update(name=Lower('name')) - self.assertQuerysetEqual( - authors.order_by('name'), [ - ('john smith', 'john smith'), - ('rhonda', 'rhonda'), - ], - lambda a: (a.lower_name, a.name) - ) - - with self.assertRaisesMessage(TypeError, "'Lower' takes exactly 1 argument (2 given)"): - Author.objects.update(name=Lower('name', 'name')) - - def test_upper(self): - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - authors = Author.objects.annotate(upper_name=Upper('name')) - - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'JOHN SMITH', - 'RHONDA', - ], - lambda a: a.upper_name - ) - - Author.objects.update(name=Upper('name')) - self.assertQuerysetEqual( - authors.order_by('name'), [ - ('JOHN SMITH', 'JOHN SMITH'), - ('RHONDA', 'RHONDA'), - ], - lambda a: (a.upper_name, a.name) - ) - - def test_length(self): - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - authors = Author.objects.annotate( - name_length=Length('name'), - alias_length=Length('alias')) - - self.assertQuerysetEqual( - authors.order_by('name'), [ - (10, 6), - (6, None), - ], - lambda a: (a.name_length, a.alias_length) - ) - - self.assertEqual(authors.filter(alias_length__lte=Length('name')).count(), 1) - - def test_length_ordering(self): - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='John Smith', alias='smithj1') - Author.objects.create(name='Rhonda', alias='ronny') - - authors = Author.objects.order_by(Length('name'), Length('alias')) - - self.assertQuerysetEqual( - authors, [ - ('Rhonda', 'ronny'), - ('John Smith', 'smithj'), - ('John Smith', 'smithj1'), - ], - lambda a: (a.name, a.alias) - ) - - def test_substr(self): - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - authors = Author.objects.annotate(name_part=Substr('name', 5, 3)) - - self.assertQuerysetEqual( - authors.order_by('name'), [ - ' Sm', - 'da', - ], - lambda a: a.name_part - ) - - authors = Author.objects.annotate(name_part=Substr('name', 2)) - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'ohn Smith', - 'honda', - ], - lambda a: a.name_part - ) - - # if alias is null, set to first 5 lower characters of the name - Author.objects.filter(alias__isnull=True).update( - alias=Lower(Substr('name', 1, 5)), - ) - - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'smithj', - 'rhond', - ], - lambda a: a.alias - ) - - def test_substr_start(self): - Author.objects.create(name='John Smith', alias='smithj') - a = Author.objects.annotate( - name_part_1=Substr('name', 1), - name_part_2=Substr('name', 2), - ).get(alias='smithj') - - self.assertEqual(a.name_part_1[1:], a.name_part_2) - - with self.assertRaisesMessage(ValueError, "'pos' must be greater than 0"): - Author.objects.annotate(raises=Substr('name', 0)) - - def test_substr_with_expressions(self): - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - substr = Substr(Upper('name'), StrIndex('name', V('h')), 5, output_field=CharField()) - authors = Author.objects.annotate(name_part=substr) - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'HN SM', - 'HONDA', - ], - lambda a: a.name_part - ) - def test_nested_function_ordering(self): Author.objects.create(name='John Smith') Author.objects.create(name='Rhonda Simpson', alias='ronny') @@ -578,51 +369,6 @@ class FunctionTests(TestCase): lambda a: a.title ) - def test_length_transform(self): - try: - CharField.register_lookup(Length) - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - authors = Author.objects.filter(name__length__gt=7) - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'John Smith', - ], - lambda a: a.name - ) - finally: - CharField._unregister_lookup(Length) - - def test_lower_transform(self): - try: - CharField.register_lookup(Lower) - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - authors = Author.objects.filter(name__lower__exact='john smith') - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'John Smith', - ], - lambda a: a.name - ) - finally: - CharField._unregister_lookup(Lower) - - def test_upper_transform(self): - try: - CharField.register_lookup(Upper) - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - authors = Author.objects.filter(name__upper__exact='JOHN SMITH') - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'John Smith', - ], - lambda a: a.name - ) - finally: - CharField._unregister_lookup(Upper) - def test_func_transform_bilateral(self): class UpperBilateral(Upper): bilateral = True diff --git a/tests/db_functions/text/__init__.py b/tests/db_functions/text/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/db_functions/test_chr.py b/tests/db_functions/text/test_chr.py similarity index 98% rename from tests/db_functions/test_chr.py rename to tests/db_functions/text/test_chr.py index 0b4b0cc77cd..d48793457ad 100644 --- a/tests/db_functions/test_chr.py +++ b/tests/db_functions/text/test_chr.py @@ -2,7 +2,7 @@ from django.db.models import IntegerField from django.db.models.functions import Chr, Left, Ord from django.test import TestCase -from .models import Author +from ..models import Author class ChrTests(TestCase): diff --git a/tests/db_functions/text/test_concat.py b/tests/db_functions/text/test_concat.py new file mode 100644 index 00000000000..9850b2fd0d0 --- /dev/null +++ b/tests/db_functions/text/test_concat.py @@ -0,0 +1,81 @@ +from unittest import skipUnless + +from django.db import connection +from django.db.models import CharField, TextField, Value as V +from django.db.models.functions import Concat, ConcatPair, Upper +from django.test import TestCase +from django.utils import timezone + +from ..models import Article, Author + +lorem_ipsum = """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua.""" + + +class ConcatTests(TestCase): + + def test_basic(self): + Author.objects.create(name='Jayden') + Author.objects.create(name='John Smith', alias='smithj', goes_by='John') + Author.objects.create(name='Margaret', goes_by='Maggie') + Author.objects.create(name='Rhonda', alias='adnohR') + authors = Author.objects.annotate(joined=Concat('alias', 'goes_by')) + self.assertQuerysetEqual( + authors.order_by('name'), [ + '', + 'smithjJohn', + 'Maggie', + 'adnohR', + ], + lambda a: a.joined + ) + + def test_gt_two_expressions(self): + with self.assertRaisesMessage(ValueError, 'Concat must take at least two expressions'): + Author.objects.annotate(joined=Concat('alias')) + + def test_many(self): + Author.objects.create(name='Jayden') + Author.objects.create(name='John Smith', alias='smithj', goes_by='John') + Author.objects.create(name='Margaret', goes_by='Maggie') + Author.objects.create(name='Rhonda', alias='adnohR') + authors = Author.objects.annotate( + joined=Concat('name', V(' ('), 'goes_by', V(')'), output_field=CharField()), + ) + self.assertQuerysetEqual( + authors.order_by('name'), [ + 'Jayden ()', + 'John Smith (John)', + 'Margaret (Maggie)', + 'Rhonda ()', + ], + lambda a: a.joined + ) + + def test_mixed_char_text(self): + Article.objects.create(title='The Title', text=lorem_ipsum, written=timezone.now()) + article = Article.objects.annotate( + title_text=Concat('title', V(' - '), 'text', output_field=TextField()), + ).get(title='The Title') + self.assertEqual(article.title + ' - ' + article.text, article.title_text) + # Wrap the concat in something else to ensure that text is returned + # rather than bytes. + article = Article.objects.annotate( + title_text=Upper(Concat('title', V(' - '), 'text', output_field=TextField())), + ).get(title='The Title') + expected = article.title + ' - ' + article.text + self.assertEqual(expected.upper(), article.title_text) + + @skipUnless(connection.vendor == 'sqlite', "sqlite specific implementation detail.") + def test_coalesce_idempotent(self): + pair = ConcatPair(V('a'), V('b')) + # Check nodes counts + self.assertEqual(len(list(pair.flatten())), 3) + self.assertEqual(len(list(pair.coalesce().flatten())), 7) # + 2 Coalesce + 2 Value() + self.assertEqual(len(list(pair.flatten())), 3) + + def test_sql_generation_idempotency(self): + qs = Article.objects.annotate(description=Concat('title', V(': '), 'summary')) + # Multiple compilations should not alter the generated query. + self.assertEqual(str(qs.query), str(qs.all().query)) diff --git a/tests/db_functions/test_left.py b/tests/db_functions/text/test_left.py similarity index 97% rename from tests/db_functions/test_left.py rename to tests/db_functions/text/test_left.py index f853ac21ac1..5bb3d6c4fad 100644 --- a/tests/db_functions/test_left.py +++ b/tests/db_functions/text/test_left.py @@ -2,7 +2,7 @@ from django.db.models import CharField, Value from django.db.models.functions import Left, Lower from django.test import TestCase -from .models import Author +from ..models import Author class LeftTests(TestCase): diff --git a/tests/db_functions/text/test_length.py b/tests/db_functions/text/test_length.py new file mode 100644 index 00000000000..8fbe6887aa4 --- /dev/null +++ b/tests/db_functions/text/test_length.py @@ -0,0 +1,48 @@ +from django.db.models import CharField +from django.db.models.functions import Length +from django.test import TestCase + +from ..models import Author + + +class LengthTests(TestCase): + + def test_basic(self): + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + authors = Author.objects.annotate( + name_length=Length('name'), + alias_length=Length('alias'), + ) + self.assertQuerysetEqual( + authors.order_by('name'), [(10, 6), (6, None)], + lambda a: (a.name_length, a.alias_length) + ) + self.assertEqual(authors.filter(alias_length__lte=Length('name')).count(), 1) + + def test_ordering(self): + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='John Smith', alias='smithj1') + Author.objects.create(name='Rhonda', alias='ronny') + authors = Author.objects.order_by(Length('name'), Length('alias')) + self.assertQuerysetEqual( + authors, [ + ('Rhonda', 'ronny'), + ('John Smith', 'smithj'), + ('John Smith', 'smithj1'), + ], + lambda a: (a.name, a.alias) + ) + + def test_transform(self): + try: + CharField.register_lookup(Length) + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + authors = Author.objects.filter(name__length__gt=7) + self.assertQuerysetEqual( + authors.order_by('name'), ['John Smith'], + lambda a: a.name + ) + finally: + CharField._unregister_lookup(Length) diff --git a/tests/db_functions/text/test_lower.py b/tests/db_functions/text/test_lower.py new file mode 100644 index 00000000000..f438682f1d1 --- /dev/null +++ b/tests/db_functions/text/test_lower.py @@ -0,0 +1,42 @@ +from django.db.models import CharField +from django.db.models.functions import Lower +from django.test import TestCase + +from ..models import Author + + +class LowerTests(TestCase): + + def test_basic(self): + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + authors = Author.objects.annotate(lower_name=Lower('name')) + self.assertQuerysetEqual( + authors.order_by('name'), ['john smith', 'rhonda'], + lambda a: a.lower_name + ) + Author.objects.update(name=Lower('name')) + self.assertQuerysetEqual( + authors.order_by('name'), [ + ('john smith', 'john smith'), + ('rhonda', 'rhonda'), + ], + lambda a: (a.lower_name, a.name) + ) + + def test_num_args(self): + with self.assertRaisesMessage(TypeError, "'Lower' takes exactly 1 argument (2 given)"): + Author.objects.update(name=Lower('name', 'name')) + + def test_transform(self): + try: + CharField.register_lookup(Lower) + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + authors = Author.objects.filter(name__lower__exact='john smith') + self.assertQuerysetEqual( + authors.order_by('name'), ['John Smith'], + lambda a: a.name + ) + finally: + CharField._unregister_lookup(Lower) diff --git a/tests/db_functions/test_ord.py b/tests/db_functions/text/test_ord.py similarity index 97% rename from tests/db_functions/test_ord.py rename to tests/db_functions/text/test_ord.py index 93ec38b0761..c4ad730f6ca 100644 --- a/tests/db_functions/test_ord.py +++ b/tests/db_functions/text/test_ord.py @@ -2,7 +2,7 @@ from django.db.models import CharField, Value from django.db.models.functions import Left, Ord from django.test import TestCase -from .models import Author +from ..models import Author class OrdTests(TestCase): diff --git a/tests/db_functions/test_pad.py b/tests/db_functions/text/test_pad.py similarity index 98% rename from tests/db_functions/test_pad.py rename to tests/db_functions/text/test_pad.py index 1c2caf06b51..2cec280b4d2 100644 --- a/tests/db_functions/test_pad.py +++ b/tests/db_functions/text/test_pad.py @@ -2,7 +2,7 @@ from django.db.models import CharField, Value from django.db.models.functions import Length, LPad, RPad from django.test import TestCase -from .models import Author +from ..models import Author class PadTests(TestCase): diff --git a/tests/db_functions/test_repeat.py b/tests/db_functions/text/test_repeat.py similarity index 97% rename from tests/db_functions/test_repeat.py rename to tests/db_functions/text/test_repeat.py index d3f294c409d..f45544d97e4 100644 --- a/tests/db_functions/test_repeat.py +++ b/tests/db_functions/text/test_repeat.py @@ -2,7 +2,7 @@ from django.db.models import CharField, Value from django.db.models.functions import Length, Repeat from django.test import TestCase -from .models import Author +from ..models import Author class RepeatTests(TestCase): diff --git a/tests/db_functions/test_replace.py b/tests/db_functions/text/test_replace.py similarity index 98% rename from tests/db_functions/test_replace.py rename to tests/db_functions/text/test_replace.py index 91a1749d70d..ae87781b8c2 100644 --- a/tests/db_functions/test_replace.py +++ b/tests/db_functions/text/test_replace.py @@ -2,7 +2,7 @@ from django.db.models import F, Value from django.db.models.functions import Concat, Replace from django.test import TestCase -from .models import Author +from ..models import Author class ReplaceTests(TestCase): diff --git a/tests/db_functions/test_right.py b/tests/db_functions/text/test_right.py similarity index 97% rename from tests/db_functions/test_right.py rename to tests/db_functions/text/test_right.py index b75bfd5155b..6dcbcc18f5d 100644 --- a/tests/db_functions/test_right.py +++ b/tests/db_functions/text/test_right.py @@ -2,7 +2,7 @@ from django.db.models import CharField, Value from django.db.models.functions import Lower, Right from django.test import TestCase -from .models import Author +from ..models import Author class RightTests(TestCase): diff --git a/tests/db_functions/test_strindex.py b/tests/db_functions/text/test_strindex.py similarity index 98% rename from tests/db_functions/test_strindex.py rename to tests/db_functions/text/test_strindex.py index 32a153bcbcb..1670df00fd5 100644 --- a/tests/db_functions/test_strindex.py +++ b/tests/db_functions/text/test_strindex.py @@ -3,7 +3,7 @@ from django.db.models.functions import StrIndex from django.test import TestCase from django.utils import timezone -from .models import Article, Author +from ..models import Article, Author class StrIndexTests(TestCase): diff --git a/tests/db_functions/text/test_substr.py b/tests/db_functions/text/test_substr.py new file mode 100644 index 00000000000..5cc12c02881 --- /dev/null +++ b/tests/db_functions/text/test_substr.py @@ -0,0 +1,53 @@ +from django.db.models import CharField, Value as V +from django.db.models.functions import Lower, StrIndex, Substr, Upper +from django.test import TestCase + +from ..models import Author + + +class SubstrTests(TestCase): + + def test_basic(self): + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + authors = Author.objects.annotate(name_part=Substr('name', 5, 3)) + self.assertQuerysetEqual( + authors.order_by('name'), [' Sm', 'da'], + lambda a: a.name_part + ) + authors = Author.objects.annotate(name_part=Substr('name', 2)) + self.assertQuerysetEqual( + authors.order_by('name'), ['ohn Smith', 'honda'], + lambda a: a.name_part + ) + # If alias is null, set to first 5 lower characters of the name. + Author.objects.filter(alias__isnull=True).update( + alias=Lower(Substr('name', 1, 5)), + ) + self.assertQuerysetEqual( + authors.order_by('name'), ['smithj', 'rhond'], + lambda a: a.alias + ) + + def test_start(self): + Author.objects.create(name='John Smith', alias='smithj') + a = Author.objects.annotate( + name_part_1=Substr('name', 1), + name_part_2=Substr('name', 2), + ).get(alias='smithj') + + self.assertEqual(a.name_part_1[1:], a.name_part_2) + + def test_pos_gt_zero(self): + with self.assertRaisesMessage(ValueError, "'pos' must be greater than 0"): + Author.objects.annotate(raises=Substr('name', 0)) + + def test_expressions(self): + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + substr = Substr(Upper('name'), StrIndex('name', V('h')), 5, output_field=CharField()) + authors = Author.objects.annotate(name_part=substr) + self.assertQuerysetEqual( + authors.order_by('name'), ['HN SM', 'HONDA'], + lambda a: a.name_part + ) diff --git a/tests/db_functions/test_trim.py b/tests/db_functions/text/test_trim.py similarity index 98% rename from tests/db_functions/test_trim.py rename to tests/db_functions/text/test_trim.py index 687d1522d31..3144aef028e 100644 --- a/tests/db_functions/test_trim.py +++ b/tests/db_functions/text/test_trim.py @@ -2,7 +2,7 @@ from django.db.models import CharField from django.db.models.functions import LTrim, RTrim, Trim from django.test import TestCase -from .models import Author +from ..models import Author class TrimTests(TestCase): diff --git a/tests/db_functions/text/test_upper.py b/tests/db_functions/text/test_upper.py new file mode 100644 index 00000000000..091e815d6aa --- /dev/null +++ b/tests/db_functions/text/test_upper.py @@ -0,0 +1,43 @@ +from django.db.models import CharField +from django.db.models.functions import Upper +from django.test import TestCase + +from ..models import Author + + +class UpperTests(TestCase): + + def test_basic(self): + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + authors = Author.objects.annotate(upper_name=Upper('name')) + self.assertQuerysetEqual( + authors.order_by('name'), [ + 'JOHN SMITH', + 'RHONDA', + ], + lambda a: a.upper_name + ) + Author.objects.update(name=Upper('name')) + self.assertQuerysetEqual( + authors.order_by('name'), [ + ('JOHN SMITH', 'JOHN SMITH'), + ('RHONDA', 'RHONDA'), + ], + lambda a: (a.upper_name, a.name) + ) + + def test_transform(self): + try: + CharField.register_lookup(Upper) + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + authors = Author.objects.filter(name__upper__exact='JOHN SMITH') + self.assertQuerysetEqual( + authors.order_by('name'), [ + 'John Smith', + ], + lambda a: a.name + ) + finally: + CharField._unregister_lookup(Upper)