diff --git a/tests/db_functions/comparison/__init__.py b/tests/db_functions/comparison/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/db_functions/test_cast.py b/tests/db_functions/comparison/test_cast.py similarity index 99% rename from tests/db_functions/test_cast.py rename to tests/db_functions/comparison/test_cast.py index 7cc6bdce0df..ffbb3578351 100644 --- a/tests/db_functions/test_cast.py +++ b/tests/db_functions/comparison/test_cast.py @@ -10,7 +10,7 @@ from django.test import ( TestCase, ignore_warnings, override_settings, skipUnlessDBFeature, ) -from .models import Author +from ..models import Author class CastTests(TestCase): diff --git a/tests/db_functions/comparison/test_coalesce.py b/tests/db_functions/comparison/test_coalesce.py new file mode 100644 index 00000000000..8ba4b01fe6e --- /dev/null +++ b/tests/db_functions/comparison/test_coalesce.py @@ -0,0 +1,72 @@ +from django.db.models import TextField +from django.db.models.functions import Coalesce, Lower +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 CoalesceTests(TestCase): + + def test_basic(self): + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + authors = Author.objects.annotate(display_name=Coalesce('alias', 'name')) + self.assertQuerysetEqual( + authors.order_by('name'), ['smithj', 'Rhonda'], + lambda a: a.display_name + ) + + def test_gt_two_expressions(self): + with self.assertRaisesMessage(ValueError, 'Coalesce must take at least two expressions'): + Author.objects.annotate(display_name=Coalesce('alias')) + + def test_mixed_values(self): + a1 = Author.objects.create(name='John Smith', alias='smithj') + a2 = Author.objects.create(name='Rhonda') + ar1 = Article.objects.create( + title='How to Django', + text=lorem_ipsum, + written=timezone.now(), + ) + ar1.authors.add(a1) + ar1.authors.add(a2) + # mixed Text and Char + article = Article.objects.annotate( + headline=Coalesce('summary', 'text', output_field=TextField()), + ) + self.assertQuerysetEqual( + article.order_by('title'), [lorem_ipsum], + lambda a: a.headline + ) + # mixed Text and Char wrapped + article = Article.objects.annotate( + headline=Coalesce(Lower('summary'), Lower('text'), output_field=TextField()), + ) + self.assertQuerysetEqual( + article.order_by('title'), [lorem_ipsum.lower()], + lambda a: a.headline + ) + + def test_ordering(self): + Author.objects.create(name='John Smith', alias='smithj') + Author.objects.create(name='Rhonda') + authors = Author.objects.order_by(Coalesce('alias', 'name')) + self.assertQuerysetEqual( + authors, ['Rhonda', 'John Smith'], + lambda a: a.name + ) + authors = Author.objects.order_by(Coalesce('alias', 'name').asc()) + self.assertQuerysetEqual( + authors, ['Rhonda', 'John Smith'], + lambda a: a.name + ) + authors = Author.objects.order_by(Coalesce('alias', 'name').desc()) + self.assertQuerysetEqual( + authors, ['John Smith', 'Rhonda'], + lambda a: a.name + ) diff --git a/tests/db_functions/comparison/test_greatest.py b/tests/db_functions/comparison/test_greatest.py new file mode 100644 index 00000000000..ef93d808c23 --- /dev/null +++ b/tests/db_functions/comparison/test_greatest.py @@ -0,0 +1,91 @@ +from datetime import datetime, timedelta +from decimal import Decimal +from unittest import skipIf, skipUnless + +from django.db import connection +from django.db.models.expressions import RawSQL +from django.db.models.functions import Coalesce, Greatest +from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature +from django.utils import timezone + +from ..models import Article, Author, DecimalModel, Fan + + +class GreatestTests(TestCase): + + def test_basic(self): + now = timezone.now() + before = now - timedelta(hours=1) + Article.objects.create(title='Testing with Django', written=before, published=now) + articles = Article.objects.annotate(last_updated=Greatest('written', 'published')) + self.assertEqual(articles.first().last_updated, now) + + @skipUnlessDBFeature('greatest_least_ignores_nulls') + def test_ignores_null(self): + now = timezone.now() + Article.objects.create(title='Testing with Django', written=now) + articles = Article.objects.annotate(last_updated=Greatest('written', 'published')) + self.assertEqual(articles.first().last_updated, now) + + @skipIfDBFeature('greatest_least_ignores_nulls') + def test_propagates_null(self): + Article.objects.create(title='Testing with Django', written=timezone.now()) + articles = Article.objects.annotate(last_updated=Greatest('written', 'published')) + self.assertIsNone(articles.first().last_updated) + + @skipIf(connection.vendor == 'mysql', "This doesn't work on MySQL") + def test_coalesce_workaround(self): + past = datetime(1900, 1, 1) + now = timezone.now() + Article.objects.create(title='Testing with Django', written=now) + articles = Article.objects.annotate( + last_updated=Greatest( + Coalesce('written', past), + Coalesce('published', past), + ), + ) + self.assertEqual(articles.first().last_updated, now) + + @skipUnless(connection.vendor == 'mysql', "MySQL-specific workaround") + def test_coalesce_workaround_mysql(self): + past = datetime(1900, 1, 1) + now = timezone.now() + Article.objects.create(title='Testing with Django', written=now) + past_sql = RawSQL("cast(%s as datetime)", (past,)) + articles = Article.objects.annotate( + last_updated=Greatest( + Coalesce('written', past_sql), + Coalesce('published', past_sql), + ), + ) + self.assertEqual(articles.first().last_updated, now) + + def test_all_null(self): + Article.objects.create(title='Testing with Django', written=timezone.now()) + articles = Article.objects.annotate(last_updated=Greatest('published', 'updated')) + self.assertIsNone(articles.first().last_updated) + + def test_one_expressions(self): + with self.assertRaisesMessage(ValueError, 'Greatest must take at least two expressions'): + Greatest('written') + + def test_related_field(self): + author = Author.objects.create(name='John Smith', age=45) + Fan.objects.create(name='Margaret', age=50, author=author) + authors = Author.objects.annotate(highest_age=Greatest('age', 'fans__age')) + self.assertEqual(authors.first().highest_age, 50) + + def test_update(self): + author = Author.objects.create(name='James Smith', goes_by='Jim') + Author.objects.update(alias=Greatest('name', 'goes_by')) + author.refresh_from_db() + self.assertEqual(author.alias, 'Jim') + + def test_decimal_filter(self): + obj = DecimalModel.objects.create(n1=Decimal('1.1'), n2=Decimal('1.2')) + self.assertCountEqual( + DecimalModel.objects.annotate( + greatest=Greatest('n1', 'n2'), + ).filter(greatest=Decimal('1.2')), + [obj], + ) diff --git a/tests/db_functions/comparison/test_least.py b/tests/db_functions/comparison/test_least.py new file mode 100644 index 00000000000..de2c543f0bf --- /dev/null +++ b/tests/db_functions/comparison/test_least.py @@ -0,0 +1,93 @@ +from datetime import datetime, timedelta +from decimal import Decimal +from unittest import skipIf, skipUnless + +from django.db import connection +from django.db.models.expressions import RawSQL +from django.db.models.functions import Coalesce, Least +from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature +from django.utils import timezone + +from ..models import Article, Author, DecimalModel, Fan + + +class LeastTests(TestCase): + + def test_basic(self): + now = timezone.now() + before = now - timedelta(hours=1) + Article.objects.create(title='Testing with Django', written=before, published=now) + articles = Article.objects.annotate(first_updated=Least('written', 'published')) + self.assertEqual(articles.first().first_updated, before) + + @skipUnlessDBFeature('greatest_least_ignores_nulls') + def test_ignores_null(self): + now = timezone.now() + Article.objects.create(title='Testing with Django', written=now) + articles = Article.objects.annotate( + first_updated=Least('written', 'published'), + ) + self.assertEqual(articles.first().first_updated, now) + + @skipIfDBFeature('greatest_least_ignores_nulls') + def test_propagates_null(self): + Article.objects.create(title='Testing with Django', written=timezone.now()) + articles = Article.objects.annotate(first_updated=Least('written', 'published')) + self.assertIsNone(articles.first().first_updated) + + @skipIf(connection.vendor == 'mysql', "This doesn't work on MySQL") + def test_coalesce_workaround(self): + future = datetime(2100, 1, 1) + now = timezone.now() + Article.objects.create(title='Testing with Django', written=now) + articles = Article.objects.annotate( + last_updated=Least( + Coalesce('written', future), + Coalesce('published', future), + ), + ) + self.assertEqual(articles.first().last_updated, now) + + @skipUnless(connection.vendor == 'mysql', "MySQL-specific workaround") + def test_coalesce_workaround_mysql(self): + future = datetime(2100, 1, 1) + now = timezone.now() + Article.objects.create(title='Testing with Django', written=now) + future_sql = RawSQL("cast(%s as datetime)", (future,)) + articles = Article.objects.annotate( + last_updated=Least( + Coalesce('written', future_sql), + Coalesce('published', future_sql), + ), + ) + self.assertEqual(articles.first().last_updated, now) + + def test_all_null(self): + Article.objects.create(title='Testing with Django', written=timezone.now()) + articles = Article.objects.annotate(first_updated=Least('published', 'updated')) + self.assertIsNone(articles.first().first_updated) + + def test_one_expressions(self): + with self.assertRaisesMessage(ValueError, 'Least must take at least two expressions'): + Least('written') + + def test_related_field(self): + author = Author.objects.create(name='John Smith', age=45) + Fan.objects.create(name='Margaret', age=50, author=author) + authors = Author.objects.annotate(lowest_age=Least('age', 'fans__age')) + self.assertEqual(authors.first().lowest_age, 45) + + def test_update(self): + author = Author.objects.create(name='James Smith', goes_by='Jim') + Author.objects.update(alias=Least('name', 'goes_by')) + author.refresh_from_db() + self.assertEqual(author.alias, 'James Smith') + + def test_decimal_filter(self): + obj = DecimalModel.objects.create(n1=Decimal('1.1'), n2=Decimal('1.2')) + self.assertCountEqual( + DecimalModel.objects.annotate( + least=Least('n1', 'n2'), + ).filter(least=Decimal('1.1')), + [obj], + ) diff --git a/tests/db_functions/tests.py b/tests/db_functions/tests.py index 507d2669b8e..746fa0e1fbd 100644 --- a/tests/db_functions/tests.py +++ b/tests/db_functions/tests.py @@ -1,17 +1,11 @@ from datetime import datetime, timedelta -from decimal import Decimal -from unittest import skipIf, skipUnless -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, Greatest, Least, Length, Lower, Now, Upper, -) -from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature +from django.db.models import CharField, Value as V +from django.db.models.functions import Coalesce, Length, Now, Upper +from django.test import TestCase from django.utils import timezone -from .models import Article, Author, DecimalModel, Fan +from .models import Article, Author lorem_ipsum = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod @@ -20,296 +14,6 @@ lorem_ipsum = """ class FunctionTests(TestCase): - def test_coalesce(self): - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - authors = Author.objects.annotate(display_name=Coalesce('alias', 'name')) - - self.assertQuerysetEqual( - authors.order_by('name'), [ - 'smithj', - 'Rhonda', - ], - lambda a: a.display_name - ) - - with self.assertRaisesMessage(ValueError, 'Coalesce must take at least two expressions'): - Author.objects.annotate(display_name=Coalesce('alias')) - - def test_coalesce_mixed_values(self): - a1 = Author.objects.create(name='John Smith', alias='smithj') - a2 = Author.objects.create(name='Rhonda') - ar1 = Article.objects.create( - title="How to Django", - text=lorem_ipsum, - written=timezone.now(), - ) - ar1.authors.add(a1) - ar1.authors.add(a2) - - # mixed Text and Char - article = Article.objects.annotate( - headline=Coalesce('summary', 'text', output_field=TextField()), - ) - - self.assertQuerysetEqual( - article.order_by('title'), [ - lorem_ipsum, - ], - lambda a: a.headline - ) - - # mixed Text and Char wrapped - article = Article.objects.annotate( - headline=Coalesce(Lower('summary'), Lower('text'), output_field=TextField()), - ) - - self.assertQuerysetEqual( - article.order_by('title'), [ - lorem_ipsum.lower(), - ], - lambda a: a.headline - ) - - def test_coalesce_ordering(self): - Author.objects.create(name='John Smith', alias='smithj') - Author.objects.create(name='Rhonda') - - authors = Author.objects.order_by(Coalesce('alias', 'name')) - self.assertQuerysetEqual( - authors, [ - 'Rhonda', - 'John Smith', - ], - lambda a: a.name - ) - - authors = Author.objects.order_by(Coalesce('alias', 'name').asc()) - self.assertQuerysetEqual( - authors, [ - 'Rhonda', - 'John Smith', - ], - lambda a: a.name - ) - - authors = Author.objects.order_by(Coalesce('alias', 'name').desc()) - self.assertQuerysetEqual( - authors, [ - 'John Smith', - 'Rhonda', - ], - lambda a: a.name - ) - - def test_greatest(self): - now = timezone.now() - before = now - timedelta(hours=1) - - Article.objects.create( - title="Testing with Django", - written=before, - published=now, - ) - - articles = Article.objects.annotate( - last_updated=Greatest('written', 'published'), - ) - self.assertEqual(articles.first().last_updated, now) - - @skipUnlessDBFeature('greatest_least_ignores_nulls') - def test_greatest_ignores_null(self): - now = timezone.now() - - Article.objects.create(title="Testing with Django", written=now) - - articles = Article.objects.annotate( - last_updated=Greatest('written', 'published'), - ) - self.assertEqual(articles.first().last_updated, now) - - @skipIfDBFeature('greatest_least_ignores_nulls') - def test_greatest_propagates_null(self): - now = timezone.now() - - Article.objects.create(title="Testing with Django", written=now) - - articles = Article.objects.annotate( - last_updated=Greatest('written', 'published'), - ) - self.assertIsNone(articles.first().last_updated) - - @skipIf(connection.vendor == 'mysql', "This doesn't work on MySQL") - def test_greatest_coalesce_workaround(self): - past = datetime(1900, 1, 1) - now = timezone.now() - - Article.objects.create(title="Testing with Django", written=now) - - articles = Article.objects.annotate( - last_updated=Greatest( - Coalesce('written', past), - Coalesce('published', past), - ), - ) - self.assertEqual(articles.first().last_updated, now) - - @skipUnless(connection.vendor == 'mysql', "MySQL-specific workaround") - def test_greatest_coalesce_workaround_mysql(self): - past = datetime(1900, 1, 1) - now = timezone.now() - - Article.objects.create(title="Testing with Django", written=now) - - past_sql = RawSQL("cast(%s as datetime)", (past,)) - articles = Article.objects.annotate( - last_updated=Greatest( - Coalesce('written', past_sql), - Coalesce('published', past_sql), - ), - ) - self.assertEqual(articles.first().last_updated, now) - - def test_greatest_all_null(self): - Article.objects.create(title="Testing with Django", written=timezone.now()) - - articles = Article.objects.annotate(last_updated=Greatest('published', 'updated')) - self.assertIsNone(articles.first().last_updated) - - def test_greatest_one_expressions(self): - with self.assertRaisesMessage(ValueError, 'Greatest must take at least two expressions'): - Greatest('written') - - def test_greatest_related_field(self): - author = Author.objects.create(name='John Smith', age=45) - Fan.objects.create(name='Margaret', age=50, author=author) - - authors = Author.objects.annotate( - highest_age=Greatest('age', 'fans__age'), - ) - self.assertEqual(authors.first().highest_age, 50) - - def test_greatest_update(self): - author = Author.objects.create(name='James Smith', goes_by='Jim') - - Author.objects.update(alias=Greatest('name', 'goes_by')) - - author.refresh_from_db() - self.assertEqual(author.alias, 'Jim') - - def test_greatest_decimal_filter(self): - obj = DecimalModel.objects.create(n1=Decimal('1.1'), n2=Decimal('1.2')) - self.assertCountEqual( - DecimalModel.objects.annotate( - greatest=Greatest('n1', 'n2'), - ).filter(greatest=Decimal('1.2')), - [obj], - ) - - def test_least(self): - now = timezone.now() - before = now - timedelta(hours=1) - - Article.objects.create( - title="Testing with Django", - written=before, - published=now, - ) - - articles = Article.objects.annotate( - first_updated=Least('written', 'published'), - ) - self.assertEqual(articles.first().first_updated, before) - - @skipUnlessDBFeature('greatest_least_ignores_nulls') - def test_least_ignores_null(self): - now = timezone.now() - - Article.objects.create(title="Testing with Django", written=now) - - articles = Article.objects.annotate( - first_updated=Least('written', 'published'), - ) - self.assertEqual(articles.first().first_updated, now) - - @skipIfDBFeature('greatest_least_ignores_nulls') - def test_least_propagates_null(self): - now = timezone.now() - - Article.objects.create(title="Testing with Django", written=now) - - articles = Article.objects.annotate( - first_updated=Least('written', 'published'), - ) - self.assertIsNone(articles.first().first_updated) - - @skipIf(connection.vendor == 'mysql', "This doesn't work on MySQL") - def test_least_coalesce_workaround(self): - future = datetime(2100, 1, 1) - now = timezone.now() - - Article.objects.create(title="Testing with Django", written=now) - - articles = Article.objects.annotate( - last_updated=Least( - Coalesce('written', future), - Coalesce('published', future), - ), - ) - self.assertEqual(articles.first().last_updated, now) - - @skipUnless(connection.vendor == 'mysql', "MySQL-specific workaround") - def test_least_coalesce_workaround_mysql(self): - future = datetime(2100, 1, 1) - now = timezone.now() - - Article.objects.create(title="Testing with Django", written=now) - - future_sql = RawSQL("cast(%s as datetime)", (future,)) - articles = Article.objects.annotate( - last_updated=Least( - Coalesce('written', future_sql), - Coalesce('published', future_sql), - ), - ) - self.assertEqual(articles.first().last_updated, now) - - def test_least_all_null(self): - Article.objects.create(title="Testing with Django", written=timezone.now()) - - articles = Article.objects.annotate(first_updated=Least('published', 'updated')) - self.assertIsNone(articles.first().first_updated) - - def test_least_one_expressions(self): - with self.assertRaisesMessage(ValueError, 'Least must take at least two expressions'): - Least('written') - - def test_least_related_field(self): - author = Author.objects.create(name='John Smith', age=45) - Fan.objects.create(name='Margaret', age=50, author=author) - - authors = Author.objects.annotate( - lowest_age=Least('age', 'fans__age'), - ) - self.assertEqual(authors.first().lowest_age, 45) - - def test_least_update(self): - author = Author.objects.create(name='James Smith', goes_by='Jim') - - Author.objects.update(alias=Least('name', 'goes_by')) - - author.refresh_from_db() - self.assertEqual(author.alias, 'James Smith') - - def test_least_decimal_filter(self): - obj = DecimalModel.objects.create(n1=Decimal('1.1'), n2=Decimal('1.2')) - self.assertCountEqual( - DecimalModel.objects.annotate( - least=Least('n1', 'n2'), - ).filter(least=Decimal('1.1')), - [obj], - ) - def test_nested_function_ordering(self): Author.objects.create(name='John Smith') Author.objects.create(name='Rhonda Simpson', alias='ronny')