summaryrefslogtreecommitdiff
path: root/tests/expressions_window
diff options
context:
space:
mode:
authordjango-bot <ops@djangoproject.com>2022-02-03 20:24:19 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-02-07 20:37:05 +0100
commit9c19aff7c7561e3a82978a272ecdaad40dda5c00 (patch)
treef0506b668a013d0063e5fba3dbf4863b466713ba /tests/expressions_window
parentf68fa8b45dfac545cfc4111d4e52804c86db68d3 (diff)
downloaddjango-9c19aff7c7561e3a82978a272ecdaad40dda5c00.tar.gz
Refs #33476 -- Reformatted code with Black.
Diffstat (limited to 'tests/expressions_window')
-rw-r--r--tests/expressions_window/models.py6
-rw-r--r--tests/expressions_window/tests.py1752
2 files changed, 1097 insertions, 661 deletions
diff --git a/tests/expressions_window/models.py b/tests/expressions_window/models.py
index e1cc02323d..631e876e15 100644
--- a/tests/expressions_window/models.py
+++ b/tests/expressions_window/models.py
@@ -11,7 +11,9 @@ class Employee(models.Model):
department = models.CharField(max_length=40, blank=False, null=False)
hire_date = models.DateField(blank=False, null=False)
age = models.IntegerField(blank=False, null=False)
- classification = models.ForeignKey('Classification', on_delete=models.CASCADE, null=True)
+ classification = models.ForeignKey(
+ "Classification", on_delete=models.CASCADE, null=True
+ )
bonus = models.DecimalField(decimal_places=2, max_digits=15, null=True)
@@ -19,4 +21,4 @@ class Detail(models.Model):
value = models.JSONField()
class Meta:
- required_db_features = {'supports_json_field'}
+ required_db_features = {"supports_json_field"}
diff --git a/tests/expressions_window/tests.py b/tests/expressions_window/tests.py
index 5bd31b8191..251fa7b1e5 100644
--- a/tests/expressions_window/tests.py
+++ b/tests/expressions_window/tests.py
@@ -5,100 +5,152 @@ from unittest import mock
from django.core.exceptions import FieldError
from django.db import NotSupportedError, connection
from django.db.models import (
- Avg, BooleanField, Case, F, Func, IntegerField, Max, Min, OuterRef, Q,
- RowRange, Subquery, Sum, Value, ValueRange, When, Window, WindowFrame,
+ Avg,
+ BooleanField,
+ Case,
+ F,
+ Func,
+ IntegerField,
+ Max,
+ Min,
+ OuterRef,
+ Q,
+ RowRange,
+ Subquery,
+ Sum,
+ Value,
+ ValueRange,
+ When,
+ Window,
+ WindowFrame,
)
from django.db.models.fields.json import KeyTextTransform, KeyTransform
from django.db.models.functions import (
- Cast, CumeDist, DenseRank, ExtractYear, FirstValue, Lag, LastValue, Lead,
- NthValue, Ntile, PercentRank, Rank, RowNumber, Upper,
+ Cast,
+ CumeDist,
+ DenseRank,
+ ExtractYear,
+ FirstValue,
+ Lag,
+ LastValue,
+ Lead,
+ NthValue,
+ Ntile,
+ PercentRank,
+ Rank,
+ RowNumber,
+ Upper,
)
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
from .models import Detail, Employee
-@skipUnlessDBFeature('supports_over_clause')
+@skipUnlessDBFeature("supports_over_clause")
class WindowFunctionTests(TestCase):
@classmethod
def setUpTestData(cls):
- Employee.objects.bulk_create([
- Employee(
- name=e[0],
- salary=e[1],
- department=e[2],
- hire_date=e[3],
- age=e[4],
- bonus=Decimal(e[1]) / 400,
- )
- for e in [
- ('Jones', 45000, 'Accounting', datetime.datetime(2005, 11, 1), 20),
- ('Williams', 37000, 'Accounting', datetime.datetime(2009, 6, 1), 20),
- ('Jenson', 45000, 'Accounting', datetime.datetime(2008, 4, 1), 20),
- ('Adams', 50000, 'Accounting', datetime.datetime(2013, 7, 1), 50),
- ('Smith', 55000, 'Sales', datetime.datetime(2007, 6, 1), 30),
- ('Brown', 53000, 'Sales', datetime.datetime(2009, 9, 1), 30),
- ('Johnson', 40000, 'Marketing', datetime.datetime(2012, 3, 1), 30),
- ('Smith', 38000, 'Marketing', datetime.datetime(2009, 10, 1), 20),
- ('Wilkinson', 60000, 'IT', datetime.datetime(2011, 3, 1), 40),
- ('Moore', 34000, 'IT', datetime.datetime(2013, 8, 1), 40),
- ('Miller', 100000, 'Management', datetime.datetime(2005, 6, 1), 40),
- ('Johnson', 80000, 'Management', datetime.datetime(2005, 7, 1), 50),
+ Employee.objects.bulk_create(
+ [
+ Employee(
+ name=e[0],
+ salary=e[1],
+ department=e[2],
+ hire_date=e[3],
+ age=e[4],
+ bonus=Decimal(e[1]) / 400,
+ )
+ for e in [
+ ("Jones", 45000, "Accounting", datetime.datetime(2005, 11, 1), 20),
+ (
+ "Williams",
+ 37000,
+ "Accounting",
+ datetime.datetime(2009, 6, 1),
+ 20,
+ ),
+ ("Jenson", 45000, "Accounting", datetime.datetime(2008, 4, 1), 20),
+ ("Adams", 50000, "Accounting", datetime.datetime(2013, 7, 1), 50),
+ ("Smith", 55000, "Sales", datetime.datetime(2007, 6, 1), 30),
+ ("Brown", 53000, "Sales", datetime.datetime(2009, 9, 1), 30),
+ ("Johnson", 40000, "Marketing", datetime.datetime(2012, 3, 1), 30),
+ ("Smith", 38000, "Marketing", datetime.datetime(2009, 10, 1), 20),
+ ("Wilkinson", 60000, "IT", datetime.datetime(2011, 3, 1), 40),
+ ("Moore", 34000, "IT", datetime.datetime(2013, 8, 1), 40),
+ ("Miller", 100000, "Management", datetime.datetime(2005, 6, 1), 40),
+ ("Johnson", 80000, "Management", datetime.datetime(2005, 7, 1), 50),
+ ]
]
- ])
+ )
def test_dense_rank(self):
tests = [
- ExtractYear(F('hire_date')).asc(),
- F('hire_date__year').asc(),
- 'hire_date__year',
+ ExtractYear(F("hire_date")).asc(),
+ F("hire_date__year").asc(),
+ "hire_date__year",
]
for order_by in tests:
with self.subTest(order_by=order_by):
qs = Employee.objects.annotate(
rank=Window(expression=DenseRank(), order_by=order_by),
)
- self.assertQuerysetEqual(qs, [
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 1),
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), 1),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), 1),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), 2),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 3),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), 4),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), 4),
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), 4),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), 5),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), 6),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), 7),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), 7),
- ], lambda entry: (
- entry.name,
- entry.salary,
- entry.department,
- entry.hire_date,
- entry.rank,
- ), ordered=False)
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 1),
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 1),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 1),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 2),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 3),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 4),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 4),
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 4),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 5),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 6),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 7),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 7),
+ ],
+ lambda entry: (
+ entry.name,
+ entry.salary,
+ entry.department,
+ entry.hire_date,
+ entry.rank,
+ ),
+ ordered=False,
+ )
def test_department_salary(self):
- qs = Employee.objects.annotate(department_sum=Window(
- expression=Sum('salary'),
- partition_by=F('department'),
- order_by=[F('hire_date').asc()],
- )).order_by('department', 'department_sum')
- self.assertQuerysetEqual(qs, [
- ('Jones', 'Accounting', 45000, 45000),
- ('Jenson', 'Accounting', 45000, 90000),
- ('Williams', 'Accounting', 37000, 127000),
- ('Adams', 'Accounting', 50000, 177000),
- ('Wilkinson', 'IT', 60000, 60000),
- ('Moore', 'IT', 34000, 94000),
- ('Miller', 'Management', 100000, 100000),
- ('Johnson', 'Management', 80000, 180000),
- ('Smith', 'Marketing', 38000, 38000),
- ('Johnson', 'Marketing', 40000, 78000),
- ('Smith', 'Sales', 55000, 55000),
- ('Brown', 'Sales', 53000, 108000),
- ], lambda entry: (entry.name, entry.department, entry.salary, entry.department_sum))
+ qs = Employee.objects.annotate(
+ department_sum=Window(
+ expression=Sum("salary"),
+ partition_by=F("department"),
+ order_by=[F("hire_date").asc()],
+ )
+ ).order_by("department", "department_sum")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", "Accounting", 45000, 45000),
+ ("Jenson", "Accounting", 45000, 90000),
+ ("Williams", "Accounting", 37000, 127000),
+ ("Adams", "Accounting", 50000, 177000),
+ ("Wilkinson", "IT", 60000, 60000),
+ ("Moore", "IT", 34000, 94000),
+ ("Miller", "Management", 100000, 100000),
+ ("Johnson", "Management", 80000, 180000),
+ ("Smith", "Marketing", 38000, 38000),
+ ("Johnson", "Marketing", 40000, 78000),
+ ("Smith", "Sales", 55000, 55000),
+ ("Brown", "Sales", 53000, 108000),
+ ],
+ lambda entry: (
+ entry.name,
+ entry.department,
+ entry.salary,
+ entry.department_sum,
+ ),
+ )
def test_rank(self):
"""
@@ -106,24 +158,37 @@ class WindowFunctionTests(TestCase):
are multiple employees hired in different years, this will contain
gaps.
"""
- qs = Employee.objects.annotate(rank=Window(
- expression=Rank(),
- order_by=F('hire_date__year').asc(),
- ))
- self.assertQuerysetEqual(qs, [
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 1),
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), 1),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), 1),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), 4),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 5),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), 6),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), 6),
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), 6),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), 9),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), 10),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), 11),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), 11),
- ], lambda entry: (entry.name, entry.salary, entry.department, entry.hire_date, entry.rank), ordered=False)
+ qs = Employee.objects.annotate(
+ rank=Window(
+ expression=Rank(),
+ order_by=F("hire_date__year").asc(),
+ )
+ )
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 1),
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 1),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 1),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 4),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 5),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 6),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 6),
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 6),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 9),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 10),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 11),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 11),
+ ],
+ lambda entry: (
+ entry.name,
+ entry.salary,
+ entry.department,
+ entry.hire_date,
+ entry.rank,
+ ),
+ ordered=False,
+ )
def test_row_number(self):
"""
@@ -132,24 +197,30 @@ class WindowFunctionTests(TestCase):
Oracle requires an ordering-clause in the Window expression.
"""
- qs = Employee.objects.annotate(row_number=Window(
- expression=RowNumber(),
- order_by=F('pk').asc(),
- )).order_by('pk')
- self.assertQuerysetEqual(qs, [
- ('Jones', 'Accounting', 1),
- ('Williams', 'Accounting', 2),
- ('Jenson', 'Accounting', 3),
- ('Adams', 'Accounting', 4),
- ('Smith', 'Sales', 5),
- ('Brown', 'Sales', 6),
- ('Johnson', 'Marketing', 7),
- ('Smith', 'Marketing', 8),
- ('Wilkinson', 'IT', 9),
- ('Moore', 'IT', 10),
- ('Miller', 'Management', 11),
- ('Johnson', 'Management', 12),
- ], lambda entry: (entry.name, entry.department, entry.row_number))
+ qs = Employee.objects.annotate(
+ row_number=Window(
+ expression=RowNumber(),
+ order_by=F("pk").asc(),
+ )
+ ).order_by("pk")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", "Accounting", 1),
+ ("Williams", "Accounting", 2),
+ ("Jenson", "Accounting", 3),
+ ("Adams", "Accounting", 4),
+ ("Smith", "Sales", 5),
+ ("Brown", "Sales", 6),
+ ("Johnson", "Marketing", 7),
+ ("Smith", "Marketing", 8),
+ ("Wilkinson", "IT", 9),
+ ("Moore", "IT", 10),
+ ("Miller", "Management", 11),
+ ("Johnson", "Management", 12),
+ ],
+ lambda entry: (entry.name, entry.department, entry.row_number),
+ )
def test_row_number_no_ordering(self):
"""
@@ -157,44 +228,61 @@ class WindowFunctionTests(TestCase):
in which the tuples were inserted.
"""
# Add a default ordering for consistent results across databases.
- qs = Employee.objects.annotate(row_number=Window(
- expression=RowNumber(),
- )).order_by('pk')
- self.assertQuerysetEqual(qs, [
- ('Jones', 'Accounting', 1),
- ('Williams', 'Accounting', 2),
- ('Jenson', 'Accounting', 3),
- ('Adams', 'Accounting', 4),
- ('Smith', 'Sales', 5),
- ('Brown', 'Sales', 6),
- ('Johnson', 'Marketing', 7),
- ('Smith', 'Marketing', 8),
- ('Wilkinson', 'IT', 9),
- ('Moore', 'IT', 10),
- ('Miller', 'Management', 11),
- ('Johnson', 'Management', 12),
- ], lambda entry: (entry.name, entry.department, entry.row_number))
+ qs = Employee.objects.annotate(
+ row_number=Window(
+ expression=RowNumber(),
+ )
+ ).order_by("pk")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", "Accounting", 1),
+ ("Williams", "Accounting", 2),
+ ("Jenson", "Accounting", 3),
+ ("Adams", "Accounting", 4),
+ ("Smith", "Sales", 5),
+ ("Brown", "Sales", 6),
+ ("Johnson", "Marketing", 7),
+ ("Smith", "Marketing", 8),
+ ("Wilkinson", "IT", 9),
+ ("Moore", "IT", 10),
+ ("Miller", "Management", 11),
+ ("Johnson", "Management", 12),
+ ],
+ lambda entry: (entry.name, entry.department, entry.row_number),
+ )
def test_avg_salary_department(self):
- qs = Employee.objects.annotate(avg_salary=Window(
- expression=Avg('salary'),
- order_by=F('department').asc(),
- partition_by='department',
- )).order_by('department', '-salary', 'name')
- self.assertQuerysetEqual(qs, [
- ('Adams', 50000, 'Accounting', 44250.00),
- ('Jenson', 45000, 'Accounting', 44250.00),
- ('Jones', 45000, 'Accounting', 44250.00),
- ('Williams', 37000, 'Accounting', 44250.00),
- ('Wilkinson', 60000, 'IT', 47000.00),
- ('Moore', 34000, 'IT', 47000.00),
- ('Miller', 100000, 'Management', 90000.00),
- ('Johnson', 80000, 'Management', 90000.00),
- ('Johnson', 40000, 'Marketing', 39000.00),
- ('Smith', 38000, 'Marketing', 39000.00),
- ('Smith', 55000, 'Sales', 54000.00),
- ('Brown', 53000, 'Sales', 54000.00),
- ], transform=lambda row: (row.name, row.salary, row.department, row.avg_salary))
+ qs = Employee.objects.annotate(
+ avg_salary=Window(
+ expression=Avg("salary"),
+ order_by=F("department").asc(),
+ partition_by="department",
+ )
+ ).order_by("department", "-salary", "name")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Adams", 50000, "Accounting", 44250.00),
+ ("Jenson", 45000, "Accounting", 44250.00),
+ ("Jones", 45000, "Accounting", 44250.00),
+ ("Williams", 37000, "Accounting", 44250.00),
+ ("Wilkinson", 60000, "IT", 47000.00),
+ ("Moore", 34000, "IT", 47000.00),
+ ("Miller", 100000, "Management", 90000.00),
+ ("Johnson", 80000, "Management", 90000.00),
+ ("Johnson", 40000, "Marketing", 39000.00),
+ ("Smith", 38000, "Marketing", 39000.00),
+ ("Smith", 55000, "Sales", 54000.00),
+ ("Brown", 53000, "Sales", 54000.00),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.salary,
+ row.department,
+ row.avg_salary,
+ ),
+ )
def test_lag(self):
"""
@@ -202,206 +290,362 @@ class WindowFunctionTests(TestCase):
highest salary in the employee's department. Return None if the
employee has the lowest salary.
"""
- qs = Employee.objects.annotate(lag=Window(
- expression=Lag(expression='salary', offset=1),
- partition_by=F('department'),
- order_by=[F('salary').asc(), F('name').asc()],
- )).order_by('department', F('salary').asc(), F('name').asc())
- self.assertQuerysetEqual(qs, [
- ('Williams', 37000, 'Accounting', None),
- ('Jenson', 45000, 'Accounting', 37000),
- ('Jones', 45000, 'Accounting', 45000),
- ('Adams', 50000, 'Accounting', 45000),
- ('Moore', 34000, 'IT', None),
- ('Wilkinson', 60000, 'IT', 34000),
- ('Johnson', 80000, 'Management', None),
- ('Miller', 100000, 'Management', 80000),
- ('Smith', 38000, 'Marketing', None),
- ('Johnson', 40000, 'Marketing', 38000),
- ('Brown', 53000, 'Sales', None),
- ('Smith', 55000, 'Sales', 53000),
- ], transform=lambda row: (row.name, row.salary, row.department, row.lag))
+ qs = Employee.objects.annotate(
+ lag=Window(
+ expression=Lag(expression="salary", offset=1),
+ partition_by=F("department"),
+ order_by=[F("salary").asc(), F("name").asc()],
+ )
+ ).order_by("department", F("salary").asc(), F("name").asc())
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Williams", 37000, "Accounting", None),
+ ("Jenson", 45000, "Accounting", 37000),
+ ("Jones", 45000, "Accounting", 45000),
+ ("Adams", 50000, "Accounting", 45000),
+ ("Moore", 34000, "IT", None),
+ ("Wilkinson", 60000, "IT", 34000),
+ ("Johnson", 80000, "Management", None),
+ ("Miller", 100000, "Management", 80000),
+ ("Smith", 38000, "Marketing", None),
+ ("Johnson", 40000, "Marketing", 38000),
+ ("Brown", 53000, "Sales", None),
+ ("Smith", 55000, "Sales", 53000),
+ ],
+ transform=lambda row: (row.name, row.salary, row.department, row.lag),
+ )
def test_lag_decimalfield(self):
- qs = Employee.objects.annotate(lag=Window(
- expression=Lag(expression='bonus', offset=1),
- partition_by=F('department'),
- order_by=[F('bonus').asc(), F('name').asc()],
- )).order_by('department', F('bonus').asc(), F('name').asc())
- self.assertQuerysetEqual(qs, [
- ('Williams', 92.5, 'Accounting', None),
- ('Jenson', 112.5, 'Accounting', 92.5),
- ('Jones', 112.5, 'Accounting', 112.5),
- ('Adams', 125, 'Accounting', 112.5),
- ('Moore', 85, 'IT', None),
- ('Wilkinson', 150, 'IT', 85),
- ('Johnson', 200, 'Management', None),
- ('Miller', 250, 'Management', 200),
- ('Smith', 95, 'Marketing', None),
- ('Johnson', 100, 'Marketing', 95),
- ('Brown', 132.5, 'Sales', None),
- ('Smith', 137.5, 'Sales', 132.5),
- ], transform=lambda row: (row.name, row.bonus, row.department, row.lag))
+ qs = Employee.objects.annotate(
+ lag=Window(
+ expression=Lag(expression="bonus", offset=1),
+ partition_by=F("department"),
+ order_by=[F("bonus").asc(), F("name").asc()],
+ )
+ ).order_by("department", F("bonus").asc(), F("name").asc())
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Williams", 92.5, "Accounting", None),
+ ("Jenson", 112.5, "Accounting", 92.5),
+ ("Jones", 112.5, "Accounting", 112.5),
+ ("Adams", 125, "Accounting", 112.5),
+ ("Moore", 85, "IT", None),
+ ("Wilkinson", 150, "IT", 85),
+ ("Johnson", 200, "Management", None),
+ ("Miller", 250, "Management", 200),
+ ("Smith", 95, "Marketing", None),
+ ("Johnson", 100, "Marketing", 95),
+ ("Brown", 132.5, "Sales", None),
+ ("Smith", 137.5, "Sales", 132.5),
+ ],
+ transform=lambda row: (row.name, row.bonus, row.department, row.lag),
+ )
def test_first_value(self):
- qs = Employee.objects.annotate(first_value=Window(
- expression=FirstValue('salary'),
- partition_by=F('department'),
- order_by=F('hire_date').asc(),
- )).order_by('department', 'hire_date')
- self.assertQuerysetEqual(qs, [
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 45000),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 45000),
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), 45000),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), 45000),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), 60000),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), 60000),
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), 100000),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), 100000),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), 38000),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), 38000),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), 55000),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), 55000),
- ], lambda row: (row.name, row.salary, row.department, row.hire_date, row.first_value))
+ qs = Employee.objects.annotate(
+ first_value=Window(
+ expression=FirstValue("salary"),
+ partition_by=F("department"),
+ order_by=F("hire_date").asc(),
+ )
+ ).order_by("department", "hire_date")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 45000),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 45000),
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 45000),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 45000),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 60000),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 60000),
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 100000),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 100000),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 38000),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 38000),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 55000),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 55000),
+ ],
+ lambda row: (
+ row.name,
+ row.salary,
+ row.department,
+ row.hire_date,
+ row.first_value,
+ ),
+ )
def test_last_value(self):
- qs = Employee.objects.annotate(last_value=Window(
- expression=LastValue('hire_date'),
- partition_by=F('department'),
- order_by=F('hire_date').asc(),
- ))
- self.assertQuerysetEqual(qs, [
- ('Adams', 'Accounting', datetime.date(2013, 7, 1), 50000, datetime.date(2013, 7, 1)),
- ('Jenson', 'Accounting', datetime.date(2008, 4, 1), 45000, datetime.date(2008, 4, 1)),
- ('Jones', 'Accounting', datetime.date(2005, 11, 1), 45000, datetime.date(2005, 11, 1)),
- ('Williams', 'Accounting', datetime.date(2009, 6, 1), 37000, datetime.date(2009, 6, 1)),
- ('Moore', 'IT', datetime.date(2013, 8, 1), 34000, datetime.date(2013, 8, 1)),
- ('Wilkinson', 'IT', datetime.date(2011, 3, 1), 60000, datetime.date(2011, 3, 1)),
- ('Miller', 'Management', datetime.date(2005, 6, 1), 100000, datetime.date(2005, 6, 1)),
- ('Johnson', 'Management', datetime.date(2005, 7, 1), 80000, datetime.date(2005, 7, 1)),
- ('Johnson', 'Marketing', datetime.date(2012, 3, 1), 40000, datetime.date(2012, 3, 1)),
- ('Smith', 'Marketing', datetime.date(2009, 10, 1), 38000, datetime.date(2009, 10, 1)),
- ('Brown', 'Sales', datetime.date(2009, 9, 1), 53000, datetime.date(2009, 9, 1)),
- ('Smith', 'Sales', datetime.date(2007, 6, 1), 55000, datetime.date(2007, 6, 1)),
- ], transform=lambda row: (row.name, row.department, row.hire_date, row.salary, row.last_value), ordered=False)
+ qs = Employee.objects.annotate(
+ last_value=Window(
+ expression=LastValue("hire_date"),
+ partition_by=F("department"),
+ order_by=F("hire_date").asc(),
+ )
+ )
+ self.assertQuerysetEqual(
+ qs,
+ [
+ (
+ "Adams",
+ "Accounting",
+ datetime.date(2013, 7, 1),
+ 50000,
+ datetime.date(2013, 7, 1),
+ ),
+ (
+ "Jenson",
+ "Accounting",
+ datetime.date(2008, 4, 1),
+ 45000,
+ datetime.date(2008, 4, 1),
+ ),
+ (
+ "Jones",
+ "Accounting",
+ datetime.date(2005, 11, 1),
+ 45000,
+ datetime.date(2005, 11, 1),
+ ),
+ (
+ "Williams",
+ "Accounting",
+ datetime.date(2009, 6, 1),
+ 37000,
+ datetime.date(2009, 6, 1),
+ ),
+ (
+ "Moore",
+ "IT",
+ datetime.date(2013, 8, 1),
+ 34000,
+ datetime.date(2013, 8, 1),
+ ),
+ (
+ "Wilkinson",
+ "IT",
+ datetime.date(2011, 3, 1),
+ 60000,
+ datetime.date(2011, 3, 1),
+ ),
+ (
+ "Miller",
+ "Management",
+ datetime.date(2005, 6, 1),
+ 100000,
+ datetime.date(2005, 6, 1),
+ ),
+ (
+ "Johnson",
+ "Management",
+ datetime.date(2005, 7, 1),
+ 80000,
+ datetime.date(2005, 7, 1),
+ ),
+ (
+ "Johnson",
+ "Marketing",
+ datetime.date(2012, 3, 1),
+ 40000,
+ datetime.date(2012, 3, 1),
+ ),
+ (
+ "Smith",
+ "Marketing",
+ datetime.date(2009, 10, 1),
+ 38000,
+ datetime.date(2009, 10, 1),
+ ),
+ (
+ "Brown",
+ "Sales",
+ datetime.date(2009, 9, 1),
+ 53000,
+ datetime.date(2009, 9, 1),
+ ),
+ (
+ "Smith",
+ "Sales",
+ datetime.date(2007, 6, 1),
+ 55000,
+ datetime.date(2007, 6, 1),
+ ),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.department,
+ row.hire_date,
+ row.salary,
+ row.last_value,
+ ),
+ ordered=False,
+ )
def test_function_list_of_values(self):
- qs = Employee.objects.annotate(lead=Window(
- expression=Lead(expression='salary'),
- order_by=[F('hire_date').asc(), F('name').desc()],
- partition_by='department',
- )).values_list('name', 'salary', 'department', 'hire_date', 'lead') \
- .order_by('department', F('hire_date').asc(), F('name').desc())
- self.assertNotIn('GROUP BY', str(qs.query))
- self.assertSequenceEqual(qs, [
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 45000),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 37000),
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), 50000),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), None),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), 34000),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), None),
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), 80000),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), None),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), 40000),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), None),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), 53000),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), None),
- ])
+ qs = (
+ Employee.objects.annotate(
+ lead=Window(
+ expression=Lead(expression="salary"),
+ order_by=[F("hire_date").asc(), F("name").desc()],
+ partition_by="department",
+ )
+ )
+ .values_list("name", "salary", "department", "hire_date", "lead")
+ .order_by("department", F("hire_date").asc(), F("name").desc())
+ )
+ self.assertNotIn("GROUP BY", str(qs.query))
+ self.assertSequenceEqual(
+ qs,
+ [
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 45000),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 37000),
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 50000),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), None),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 34000),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), None),
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 80000),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), None),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 40000),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), None),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 53000),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), None),
+ ],
+ )
def test_min_department(self):
"""An alternative way to specify a query for FirstValue."""
- qs = Employee.objects.annotate(min_salary=Window(
- expression=Min('salary'),
- partition_by=F('department'),
- order_by=[F('salary').asc(), F('name').asc()]
- )).order_by('department', 'salary', 'name')
- self.assertQuerysetEqual(qs, [
- ('Williams', 'Accounting', 37000, 37000),
- ('Jenson', 'Accounting', 45000, 37000),
- ('Jones', 'Accounting', 45000, 37000),
- ('Adams', 'Accounting', 50000, 37000),
- ('Moore', 'IT', 34000, 34000),
- ('Wilkinson', 'IT', 60000, 34000),
- ('Johnson', 'Management', 80000, 80000),
- ('Miller', 'Management', 100000, 80000),
- ('Smith', 'Marketing', 38000, 38000),
- ('Johnson', 'Marketing', 40000, 38000),
- ('Brown', 'Sales', 53000, 53000),
- ('Smith', 'Sales', 55000, 53000),
- ], lambda row: (row.name, row.department, row.salary, row.min_salary))
+ qs = Employee.objects.annotate(
+ min_salary=Window(
+ expression=Min("salary"),
+ partition_by=F("department"),
+ order_by=[F("salary").asc(), F("name").asc()],
+ )
+ ).order_by("department", "salary", "name")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Williams", "Accounting", 37000, 37000),
+ ("Jenson", "Accounting", 45000, 37000),
+ ("Jones", "Accounting", 45000, 37000),
+ ("Adams", "Accounting", 50000, 37000),
+ ("Moore", "IT", 34000, 34000),
+ ("Wilkinson", "IT", 60000, 34000),
+ ("Johnson", "Management", 80000, 80000),
+ ("Miller", "Management", 100000, 80000),
+ ("Smith", "Marketing", 38000, 38000),
+ ("Johnson", "Marketing", 40000, 38000),
+ ("Brown", "Sales", 53000, 53000),
+ ("Smith", "Sales", 55000, 53000),
+ ],
+ lambda row: (row.name, row.department, row.salary, row.min_salary),
+ )
def test_max_per_year(self):
"""
Find the maximum salary awarded in the same year as the
employee was hired, regardless of the department.
"""
- qs = Employee.objects.annotate(max_salary_year=Window(
- expression=Max('salary'),
- order_by=ExtractYear('hire_date').asc(),
- partition_by=ExtractYear('hire_date')
- )).order_by(ExtractYear('hire_date'), 'salary')
- self.assertQuerysetEqual(qs, [
- ('Jones', 'Accounting', 45000, 2005, 100000),
- ('Johnson', 'Management', 80000, 2005, 100000),
- ('Miller', 'Management', 100000, 2005, 100000),
- ('Smith', 'Sales', 55000, 2007, 55000),
- ('Jenson', 'Accounting', 45000, 2008, 45000),
- ('Williams', 'Accounting', 37000, 2009, 53000),
- ('Smith', 'Marketing', 38000, 2009, 53000),
- ('Brown', 'Sales', 53000, 2009, 53000),
- ('Wilkinson', 'IT', 60000, 2011, 60000),
- ('Johnson', 'Marketing', 40000, 2012, 40000),
- ('Moore', 'IT', 34000, 2013, 50000),
- ('Adams', 'Accounting', 50000, 2013, 50000),
- ], lambda row: (row.name, row.department, row.salary, row.hire_date.year, row.max_salary_year))
+ qs = Employee.objects.annotate(
+ max_salary_year=Window(
+ expression=Max("salary"),
+ order_by=ExtractYear("hire_date").asc(),
+ partition_by=ExtractYear("hire_date"),
+ )
+ ).order_by(ExtractYear("hire_date"), "salary")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", "Accounting", 45000, 2005, 100000),
+ ("Johnson", "Management", 80000, 2005, 100000),
+ ("Miller", "Management", 100000, 2005, 100000),
+ ("Smith", "Sales", 55000, 2007, 55000),
+ ("Jenson", "Accounting", 45000, 2008, 45000),
+ ("Williams", "Accounting", 37000, 2009, 53000),
+ ("Smith", "Marketing", 38000, 2009, 53000),
+ ("Brown", "Sales", 53000, 2009, 53000),
+ ("Wilkinson", "IT", 60000, 2011, 60000),
+ ("Johnson", "Marketing", 40000, 2012, 40000),
+ ("Moore", "IT", 34000, 2013, 50000),
+ ("Adams", "Accounting", 50000, 2013, 50000),
+ ],
+ lambda row: (
+ row.name,
+ row.department,
+ row.salary,
+ row.hire_date.year,
+ row.max_salary_year,
+ ),
+ )
def test_cume_dist(self):
"""
Compute the cumulative distribution for the employees based on the
salary in increasing order. Equal to rank/total number of rows (12).
"""
- qs = Employee.objects.annotate(cume_dist=Window(
- expression=CumeDist(),
- order_by=F('salary').asc(),
- )).order_by('salary', 'name')
+ qs = Employee.objects.annotate(
+ cume_dist=Window(
+ expression=CumeDist(),
+ order_by=F("salary").asc(),
+ )
+ ).order_by("salary", "name")
# Round result of cume_dist because Oracle uses greater precision.
- self.assertQuerysetEqual(qs, [
- ('Moore', 'IT', 34000, 0.0833333333),
- ('Williams', 'Accounting', 37000, 0.1666666667),
- ('Smith', 'Marketing', 38000, 0.25),
- ('Johnson', 'Marketing', 40000, 0.3333333333),
- ('Jenson', 'Accounting', 45000, 0.5),
- ('Jones', 'Accounting', 45000, 0.5),
- ('Adams', 'Accounting', 50000, 0.5833333333),
- ('Brown', 'Sales', 53000, 0.6666666667),
- ('Smith', 'Sales', 55000, 0.75),
- ('Wilkinson', 'IT', 60000, 0.8333333333),
- ('Johnson', 'Management', 80000, 0.9166666667),
- ('Miller', 'Management', 100000, 1),
- ], lambda row: (row.name, row.department, row.salary, round(row.cume_dist, 10)))
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Moore", "IT", 34000, 0.0833333333),
+ ("Williams", "Accounting", 37000, 0.1666666667),
+ ("Smith", "Marketing", 38000, 0.25),
+ ("Johnson", "Marketing", 40000, 0.3333333333),
+ ("Jenson", "Accounting", 45000, 0.5),
+ ("Jones", "Accounting", 45000, 0.5),
+ ("Adams", "Accounting", 50000, 0.5833333333),
+ ("Brown", "Sales", 53000, 0.6666666667),
+ ("Smith", "Sales", 55000, 0.75),
+ ("Wilkinson", "IT", 60000, 0.8333333333),
+ ("Johnson", "Management", 80000, 0.9166666667),
+ ("Miller", "Management", 100000, 1),
+ ],
+ lambda row: (
+ row.name,
+ row.department,
+ row.salary,
+ round(row.cume_dist, 10),
+ ),
+ )
def test_nthvalue(self):
qs = Employee.objects.annotate(
- nth_value=Window(expression=NthValue(
- expression='salary', nth=2),
- order_by=[F('hire_date').asc(), F('name').desc()],
- partition_by=F('department'),
- )
- ).order_by('department', 'hire_date', 'name')
- self.assertQuerysetEqual(qs, [
- ('Jones', 'Accounting', datetime.date(2005, 11, 1), 45000, None),
- ('Jenson', 'Accounting', datetime.date(2008, 4, 1), 45000, 45000),
- ('Williams', 'Accounting', datetime.date(2009, 6, 1), 37000, 45000),
- ('Adams', 'Accounting', datetime.date(2013, 7, 1), 50000, 45000),
- ('Wilkinson', 'IT', datetime.date(2011, 3, 1), 60000, None),
- ('Moore', 'IT', datetime.date(2013, 8, 1), 34000, 34000),
- ('Miller', 'Management', datetime.date(2005, 6, 1), 100000, None),
- ('Johnson', 'Management', datetime.date(2005, 7, 1), 80000, 80000),
- ('Smith', 'Marketing', datetime.date(2009, 10, 1), 38000, None),
- ('Johnson', 'Marketing', datetime.date(2012, 3, 1), 40000, 40000),
- ('Smith', 'Sales', datetime.date(2007, 6, 1), 55000, None),
- ('Brown', 'Sales', datetime.date(2009, 9, 1), 53000, 53000),
- ], lambda row: (row.name, row.department, row.hire_date, row.salary, row.nth_value))
+ nth_value=Window(
+ expression=NthValue(expression="salary", nth=2),
+ order_by=[F("hire_date").asc(), F("name").desc()],
+ partition_by=F("department"),
+ )
+ ).order_by("department", "hire_date", "name")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", "Accounting", datetime.date(2005, 11, 1), 45000, None),
+ ("Jenson", "Accounting", datetime.date(2008, 4, 1), 45000, 45000),
+ ("Williams", "Accounting", datetime.date(2009, 6, 1), 37000, 45000),
+ ("Adams", "Accounting", datetime.date(2013, 7, 1), 50000, 45000),
+ ("Wilkinson", "IT", datetime.date(2011, 3, 1), 60000, None),
+ ("Moore", "IT", datetime.date(2013, 8, 1), 34000, 34000),
+ ("Miller", "Management", datetime.date(2005, 6, 1), 100000, None),
+ ("Johnson", "Management", datetime.date(2005, 7, 1), 80000, 80000),
+ ("Smith", "Marketing", datetime.date(2009, 10, 1), 38000, None),
+ ("Johnson", "Marketing", datetime.date(2012, 3, 1), 40000, 40000),
+ ("Smith", "Sales", datetime.date(2007, 6, 1), 55000, None),
+ ("Brown", "Sales", datetime.date(2009, 9, 1), 53000, 53000),
+ ],
+ lambda row: (
+ row.name,
+ row.department,
+ row.hire_date,
+ row.salary,
+ row.nth_value,
+ ),
+ )
def test_lead(self):
"""
@@ -410,61 +654,88 @@ class WindowFunctionTests(TestCase):
ordering clause. No default is provided, so None/NULL should be
returned.
"""
- qs = Employee.objects.annotate(lead=Window(
- expression=Lead(expression='salary'),
- order_by=[F('hire_date').asc(), F('name').desc()],
- partition_by='department',
- )).order_by('department', F('hire_date').asc(), F('name').desc())
- self.assertQuerysetEqual(qs, [
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 45000),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 37000),
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), 50000),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), None),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), 34000),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), None),
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), 80000),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), None),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), 40000),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), None),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), 53000),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), None),
- ], transform=lambda row: (row.name, row.salary, row.department, row.hire_date, row.lead))
+ qs = Employee.objects.annotate(
+ lead=Window(
+ expression=Lead(expression="salary"),
+ order_by=[F("hire_date").asc(), F("name").desc()],
+ partition_by="department",
+ )
+ ).order_by("department", F("hire_date").asc(), F("name").desc())
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 45000),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 37000),
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 50000),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), None),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 34000),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), None),
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 80000),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), None),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 40000),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), None),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 53000),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), None),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.salary,
+ row.department,
+ row.hire_date,
+ row.lead,
+ ),
+ )
def test_lead_offset(self):
"""
Determine what the person hired after someone makes. Due to
ambiguity, the name is also included in the ordering.
"""
- qs = Employee.objects.annotate(lead=Window(
- expression=Lead('salary', offset=2),
- partition_by='department',
- order_by=F('hire_date').asc(),
- ))
- self.assertQuerysetEqual(qs, [
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 37000),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 50000),
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), None),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), None),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), None),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), None),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), None),
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), None),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), None),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), None),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), None),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), None),
- ], transform=lambda row: (row.name, row.salary, row.department, row.hire_date, row.lead),
- ordered=False
- )
-
- @skipUnlessDBFeature('supports_default_in_lead_lag')
+ qs = Employee.objects.annotate(
+ lead=Window(
+ expression=Lead("salary", offset=2),
+ partition_by="department",
+ order_by=F("hire_date").asc(),
+ )
+ )
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 37000),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 50000),
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), None),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), None),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), None),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), None),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), None),
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), None),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), None),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), None),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), None),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), None),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.salary,
+ row.department,
+ row.hire_date,
+ row.lead,
+ ),
+ ordered=False,
+ )
+
+ @skipUnlessDBFeature("supports_default_in_lead_lag")
def test_lead_default(self):
- qs = Employee.objects.annotate(lead_default=Window(
- expression=Lead(expression='salary', offset=5, default=60000),
- partition_by=F('department'),
- order_by=F('department').asc(),
- ))
- self.assertEqual(list(qs.values_list('lead_default', flat=True).distinct()), [60000])
+ qs = Employee.objects.annotate(
+ lead_default=Window(
+ expression=Lead(expression="salary", offset=5, default=60000),
+ partition_by=F("department"),
+ order_by=F("department").asc(),
+ )
+ )
+ self.assertEqual(
+ list(qs.values_list("lead_default", flat=True).distinct()), [60000]
+ )
def test_ntile(self):
"""
@@ -472,84 +743,116 @@ class WindowFunctionTests(TestCase):
based on how high the salary is for them. There are twelve employees
so it divides evenly into four groups.
"""
- qs = Employee.objects.annotate(ntile=Window(
- expression=Ntile(num_buckets=4),
- order_by='-salary',
- )).order_by('ntile', '-salary', 'name')
- self.assertQuerysetEqual(qs, [
- ('Miller', 'Management', 100000, 1),
- ('Johnson', 'Management', 80000, 1),
- ('Wilkinson', 'IT', 60000, 1),
- ('Smith', 'Sales', 55000, 2),
- ('Brown', 'Sales', 53000, 2),
- ('Adams', 'Accounting', 50000, 2),
- ('Jenson', 'Accounting', 45000, 3),
- ('Jones', 'Accounting', 45000, 3),
- ('Johnson', 'Marketing', 40000, 3),
- ('Smith', 'Marketing', 38000, 4),
- ('Williams', 'Accounting', 37000, 4),
- ('Moore', 'IT', 34000, 4),
- ], lambda x: (x.name, x.department, x.salary, x.ntile))
+ qs = Employee.objects.annotate(
+ ntile=Window(
+ expression=Ntile(num_buckets=4),
+ order_by="-salary",
+ )
+ ).order_by("ntile", "-salary", "name")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Miller", "Management", 100000, 1),
+ ("Johnson", "Management", 80000, 1),
+ ("Wilkinson", "IT", 60000, 1),
+ ("Smith", "Sales", 55000, 2),
+ ("Brown", "Sales", 53000, 2),
+ ("Adams", "Accounting", 50000, 2),
+ ("Jenson", "Accounting", 45000, 3),
+ ("Jones", "Accounting", 45000, 3),
+ ("Johnson", "Marketing", 40000, 3),
+ ("Smith", "Marketing", 38000, 4),
+ ("Williams", "Accounting", 37000, 4),
+ ("Moore", "IT", 34000, 4),
+ ],
+ lambda x: (x.name, x.department, x.salary, x.ntile),
+ )
def test_percent_rank(self):
"""
Calculate the percentage rank of the employees across the entire
company based on salary and name (in case of ambiguity).
"""
- qs = Employee.objects.annotate(percent_rank=Window(
- expression=PercentRank(),
- order_by=[F('salary').asc(), F('name').asc()],
- )).order_by('percent_rank')
+ qs = Employee.objects.annotate(
+ percent_rank=Window(
+ expression=PercentRank(),
+ order_by=[F("salary").asc(), F("name").asc()],
+ )
+ ).order_by("percent_rank")
# Round to account for precision differences among databases.
- self.assertQuerysetEqual(qs, [
- ('Moore', 'IT', 34000, 0.0),
- ('Williams', 'Accounting', 37000, 0.0909090909),
- ('Smith', 'Marketing', 38000, 0.1818181818),
- ('Johnson', 'Marketing', 40000, 0.2727272727),
- ('Jenson', 'Accounting', 45000, 0.3636363636),
- ('Jones', 'Accounting', 45000, 0.4545454545),
- ('Adams', 'Accounting', 50000, 0.5454545455),
- ('Brown', 'Sales', 53000, 0.6363636364),
- ('Smith', 'Sales', 55000, 0.7272727273),
- ('Wilkinson', 'IT', 60000, 0.8181818182),
- ('Johnson', 'Management', 80000, 0.9090909091),
- ('Miller', 'Management', 100000, 1.0),
- ], transform=lambda row: (row.name, row.department, row.salary, round(row.percent_rank, 10)))
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Moore", "IT", 34000, 0.0),
+ ("Williams", "Accounting", 37000, 0.0909090909),
+ ("Smith", "Marketing", 38000, 0.1818181818),
+ ("Johnson", "Marketing", 40000, 0.2727272727),
+ ("Jenson", "Accounting", 45000, 0.3636363636),
+ ("Jones", "Accounting", 45000, 0.4545454545),
+ ("Adams", "Accounting", 50000, 0.5454545455),
+ ("Brown", "Sales", 53000, 0.6363636364),
+ ("Smith", "Sales", 55000, 0.7272727273),
+ ("Wilkinson", "IT", 60000, 0.8181818182),
+ ("Johnson", "Management", 80000, 0.9090909091),
+ ("Miller", "Management", 100000, 1.0),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.department,
+ row.salary,
+ round(row.percent_rank, 10),
+ ),
+ )
def test_nth_returns_null(self):
"""
Find the nth row of the data set. None is returned since there are
fewer than 20 rows in the test data.
"""
- qs = Employee.objects.annotate(nth_value=Window(
- expression=NthValue('salary', nth=20),
- order_by=F('salary').asc()
- ))
- self.assertEqual(list(qs.values_list('nth_value', flat=True).distinct()), [None])
+ qs = Employee.objects.annotate(
+ nth_value=Window(
+ expression=NthValue("salary", nth=20), order_by=F("salary").asc()
+ )
+ )
+ self.assertEqual(
+ list(qs.values_list("nth_value", flat=True).distinct()), [None]
+ )
def test_multiple_partitioning(self):
"""
Find the maximum salary for each department for people hired in the
same year.
"""
- qs = Employee.objects.annotate(max=Window(
- expression=Max('salary'),
- partition_by=[F('department'), F('hire_date__year')],
- )).order_by('department', 'hire_date', 'name')
- self.assertQuerysetEqual(qs, [
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 45000),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 45000),
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), 37000),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), 50000),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), 60000),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), 34000),
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), 100000),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), 100000),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), 38000),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), 40000),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), 55000),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), 53000),
- ], transform=lambda row: (row.name, row.salary, row.department, row.hire_date, row.max))
+ qs = Employee.objects.annotate(
+ max=Window(
+ expression=Max("salary"),
+ partition_by=[F("department"), F("hire_date__year")],
+ )
+ ).order_by("department", "hire_date", "name")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 45000),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 45000),
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 37000),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 50000),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 60000),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 34000),
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 100000),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 100000),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 38000),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 40000),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 55000),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 53000),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.salary,
+ row.department,
+ row.hire_date,
+ row.max,
+ ),
+ )
def test_multiple_ordering(self):
"""
@@ -557,110 +860,162 @@ class WindowFunctionTests(TestCase):
If two people were hired on the same date in the same department, the
ordering clause will render a different result for those people.
"""
- qs = Employee.objects.annotate(sum=Window(
- expression=Sum('salary'),
- partition_by='department',
- order_by=[F('hire_date').asc(), F('name').asc()],
- )).order_by('department', 'sum')
- self.assertQuerysetEqual(qs, [
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 45000),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 90000),
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), 127000),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), 177000),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), 60000),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), 94000),
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), 100000),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), 180000),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), 38000),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), 78000),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), 55000),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), 108000),
- ], transform=lambda row: (row.name, row.salary, row.department, row.hire_date, row.sum))
+ qs = Employee.objects.annotate(
+ sum=Window(
+ expression=Sum("salary"),
+ partition_by="department",
+ order_by=[F("hire_date").asc(), F("name").asc()],
+ )
+ ).order_by("department", "sum")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 45000),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 90000),
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 127000),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 177000),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 60000),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 94000),
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 100000),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 180000),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 38000),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 78000),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 55000),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 108000),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.salary,
+ row.department,
+ row.hire_date,
+ row.sum,
+ ),
+ )
def test_related_ordering_with_count(self):
- qs = Employee.objects.annotate(department_sum=Window(
- expression=Sum('salary'),
- partition_by=F('department'),
- order_by=['classification__code'],
- ))
+ qs = Employee.objects.annotate(
+ department_sum=Window(
+ expression=Sum("salary"),
+ partition_by=F("department"),
+ order_by=["classification__code"],
+ )
+ )
self.assertEqual(qs.count(), 12)
- @skipUnlessDBFeature('supports_frame_range_fixed_distance')
+ @skipUnlessDBFeature("supports_frame_range_fixed_distance")
def test_range_n_preceding_and_following(self):
- qs = Employee.objects.annotate(sum=Window(
- expression=Sum('salary'),
- order_by=F('salary').asc(),
- partition_by='department',
- frame=ValueRange(start=-2, end=2),
- ))
- self.assertIn('RANGE BETWEEN 2 PRECEDING AND 2 FOLLOWING', str(qs.query))
- self.assertQuerysetEqual(qs, [
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), 37000),
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 90000),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 90000),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), 50000),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), 53000),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), 55000),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), 40000),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), 38000),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), 60000),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), 34000),
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), 100000),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), 80000),
- ], transform=lambda row: (row.name, row.salary, row.department, row.hire_date, row.sum), ordered=False)
+ qs = Employee.objects.annotate(
+ sum=Window(
+ expression=Sum("salary"),
+ order_by=F("salary").asc(),
+ partition_by="department",
+ frame=ValueRange(start=-2, end=2),
+ )
+ )
+ self.assertIn("RANGE BETWEEN 2 PRECEDING AND 2 FOLLOWING", str(qs.query))
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 37000),
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 90000),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 90000),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 50000),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 53000),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 55000),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 40000),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 38000),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 60000),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 34000),
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 100000),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 80000),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.salary,
+ row.department,
+ row.hire_date,
+ row.sum,
+ ),
+ ordered=False,
+ )
def test_range_unbound(self):
"""A query with RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING."""
- qs = Employee.objects.annotate(sum=Window(
- expression=Sum('salary'),
- partition_by='age',
- order_by=[F('age').asc()],
- frame=ValueRange(start=None, end=None),
- )).order_by('department', 'hire_date', 'name')
- self.assertIn('RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING', str(qs.query))
- self.assertQuerysetEqual(qs, [
- ('Jones', 'Accounting', 45000, datetime.date(2005, 11, 1), 165000),
- ('Jenson', 'Accounting', 45000, datetime.date(2008, 4, 1), 165000),
- ('Williams', 'Accounting', 37000, datetime.date(2009, 6, 1), 165000),
- ('Adams', 'Accounting', 50000, datetime.date(2013, 7, 1), 130000),
- ('Wilkinson', 'IT', 60000, datetime.date(2011, 3, 1), 194000),
- ('Moore', 'IT', 34000, datetime.date(2013, 8, 1), 194000),
- ('Miller', 'Management', 100000, datetime.date(2005, 6, 1), 194000),
- ('Johnson', 'Management', 80000, datetime.date(2005, 7, 1), 130000),
- ('Smith', 'Marketing', 38000, datetime.date(2009, 10, 1), 165000),
- ('Johnson', 'Marketing', 40000, datetime.date(2012, 3, 1), 148000),
- ('Smith', 'Sales', 55000, datetime.date(2007, 6, 1), 148000),
- ('Brown', 'Sales', 53000, datetime.date(2009, 9, 1), 148000)
- ], transform=lambda row: (row.name, row.department, row.salary, row.hire_date, row.sum))
+ qs = Employee.objects.annotate(
+ sum=Window(
+ expression=Sum("salary"),
+ partition_by="age",
+ order_by=[F("age").asc()],
+ frame=ValueRange(start=None, end=None),
+ )
+ ).order_by("department", "hire_date", "name")
+ self.assertIn(
+ "RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING", str(qs.query)
+ )
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Jones", "Accounting", 45000, datetime.date(2005, 11, 1), 165000),
+ ("Jenson", "Accounting", 45000, datetime.date(2008, 4, 1), 165000),
+ ("Williams", "Accounting", 37000, datetime.date(2009, 6, 1), 165000),
+ ("Adams", "Accounting", 50000, datetime.date(2013, 7, 1), 130000),
+ ("Wilkinson", "IT", 60000, datetime.date(2011, 3, 1), 194000),
+ ("Moore", "IT", 34000, datetime.date(2013, 8, 1), 194000),
+ ("Miller", "Management", 100000, datetime.date(2005, 6, 1), 194000),
+ ("Johnson", "Management", 80000, datetime.date(2005, 7, 1), 130000),
+ ("Smith", "Marketing", 38000, datetime.date(2009, 10, 1), 165000),
+ ("Johnson", "Marketing", 40000, datetime.date(2012, 3, 1), 148000),
+ ("Smith", "Sales", 55000, datetime.date(2007, 6, 1), 148000),
+ ("Brown", "Sales", 53000, datetime.date(2009, 9, 1), 148000),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.department,
+ row.salary,
+ row.hire_date,
+ row.sum,
+ ),
+ )
def test_subquery_row_range_rank(self):
qs = Employee.objects.annotate(
highest_avg_salary_date=Subquery(
Employee.objects.filter(
- department=OuterRef('department'),
- ).annotate(
+ department=OuterRef("department"),
+ )
+ .annotate(
avg_salary=Window(
- expression=Avg('salary'),
- order_by=[F('hire_date').asc()],
+ expression=Avg("salary"),
+ order_by=[F("hire_date").asc()],
frame=RowRange(start=-1, end=1),
),
- ).order_by('-avg_salary', 'hire_date').values('hire_date')[:1],
+ )
+ .order_by("-avg_salary", "hire_date")
+ .values("hire_date")[:1],
+ ),
+ ).order_by("department", "name")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Adams", "Accounting", datetime.date(2005, 11, 1)),
+ ("Jenson", "Accounting", datetime.date(2005, 11, 1)),
+ ("Jones", "Accounting", datetime.date(2005, 11, 1)),
+ ("Williams", "Accounting", datetime.date(2005, 11, 1)),
+ ("Moore", "IT", datetime.date(2011, 3, 1)),
+ ("Wilkinson", "IT", datetime.date(2011, 3, 1)),
+ ("Johnson", "Management", datetime.date(2005, 6, 1)),
+ ("Miller", "Management", datetime.date(2005, 6, 1)),
+ ("Johnson", "Marketing", datetime.date(2009, 10, 1)),
+ ("Smith", "Marketing", datetime.date(2009, 10, 1)),
+ ("Brown", "Sales", datetime.date(2007, 6, 1)),
+ ("Smith", "Sales", datetime.date(2007, 6, 1)),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.department,
+ row.highest_avg_salary_date,
),
- ).order_by('department', 'name')
- self.assertQuerysetEqual(qs, [
- ('Adams', 'Accounting', datetime.date(2005, 11, 1)),
- ('Jenson', 'Accounting', datetime.date(2005, 11, 1)),
- ('Jones', 'Accounting', datetime.date(2005, 11, 1)),
- ('Williams', 'Accounting', datetime.date(2005, 11, 1)),
- ('Moore', 'IT', datetime.date(2011, 3, 1)),
- ('Wilkinson', 'IT', datetime.date(2011, 3, 1)),
- ('Johnson', 'Management', datetime.date(2005, 6, 1)),
- ('Miller', 'Management', datetime.date(2005, 6, 1)),
- ('Johnson', 'Marketing', datetime.date(2009, 10, 1)),
- ('Smith', 'Marketing', datetime.date(2009, 10, 1)),
- ('Brown', 'Sales', datetime.date(2007, 6, 1)),
- ('Smith', 'Sales', datetime.date(2007, 6, 1)),
- ], transform=lambda row: (row.name, row.department, row.highest_avg_salary_date))
+ )
def test_row_range_rank(self):
"""
@@ -668,46 +1023,66 @@ class WindowFunctionTests(TestCase):
The resulting sum is the sum of the three next (if they exist) and all
previous rows according to the ordering clause.
"""
- qs = Employee.objects.annotate(sum=Window(
- expression=Sum('salary'),
- order_by=[F('hire_date').asc(), F('name').desc()],
- frame=RowRange(start=None, end=3),
- )).order_by('sum', 'hire_date')
- self.assertIn('ROWS BETWEEN UNBOUNDED PRECEDING AND 3 FOLLOWING', str(qs.query))
- self.assertQuerysetEqual(qs, [
- ('Miller', 100000, 'Management', datetime.date(2005, 6, 1), 280000),
- ('Johnson', 80000, 'Management', datetime.date(2005, 7, 1), 325000),
- ('Jones', 45000, 'Accounting', datetime.date(2005, 11, 1), 362000),
- ('Smith', 55000, 'Sales', datetime.date(2007, 6, 1), 415000),
- ('Jenson', 45000, 'Accounting', datetime.date(2008, 4, 1), 453000),
- ('Williams', 37000, 'Accounting', datetime.date(2009, 6, 1), 513000),
- ('Brown', 53000, 'Sales', datetime.date(2009, 9, 1), 553000),
- ('Smith', 38000, 'Marketing', datetime.date(2009, 10, 1), 603000),
- ('Wilkinson', 60000, 'IT', datetime.date(2011, 3, 1), 637000),
- ('Johnson', 40000, 'Marketing', datetime.date(2012, 3, 1), 637000),
- ('Adams', 50000, 'Accounting', datetime.date(2013, 7, 1), 637000),
- ('Moore', 34000, 'IT', datetime.date(2013, 8, 1), 637000),
- ], transform=lambda row: (row.name, row.salary, row.department, row.hire_date, row.sum))
-
- @skipUnlessDBFeature('can_distinct_on_fields')
+ qs = Employee.objects.annotate(
+ sum=Window(
+ expression=Sum("salary"),
+ order_by=[F("hire_date").asc(), F("name").desc()],
+ frame=RowRange(start=None, end=3),
+ )
+ ).order_by("sum", "hire_date")
+ self.assertIn("ROWS BETWEEN UNBOUNDED PRECEDING AND 3 FOLLOWING", str(qs.query))
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 280000),
+ ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 325000),
+ ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 362000),
+ ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 415000),
+ ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 453000),
+ ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 513000),
+ ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 553000),
+ ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 603000),
+ ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 637000),
+ ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 637000),
+ ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 637000),
+ ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 637000),
+ ],
+ transform=lambda row: (
+ row.name,
+ row.salary,
+ row.department,
+ row.hire_date,
+ row.sum,
+ ),
+ )
+
+ @skipUnlessDBFeature("can_distinct_on_fields")
def test_distinct_window_function(self):
"""
Window functions are not aggregates, and hence a query to filter out
duplicates may be useful.
"""
- qs = Employee.objects.annotate(
- sum=Window(
- expression=Sum('salary'),
- partition_by=ExtractYear('hire_date'),
- order_by=ExtractYear('hire_date')
- ),
- year=ExtractYear('hire_date'),
- ).values('year', 'sum').distinct('year').order_by('year')
+ qs = (
+ Employee.objects.annotate(
+ sum=Window(
+ expression=Sum("salary"),
+ partition_by=ExtractYear("hire_date"),
+ order_by=ExtractYear("hire_date"),
+ ),
+ year=ExtractYear("hire_date"),
+ )
+ .values("year", "sum")
+ .distinct("year")
+ .order_by("year")
+ )
results = [
- {'year': 2005, 'sum': 225000}, {'year': 2007, 'sum': 55000},
- {'year': 2008, 'sum': 45000}, {'year': 2009, 'sum': 128000},
- {'year': 2011, 'sum': 60000}, {'year': 2012, 'sum': 40000},
- {'year': 2013, 'sum': 84000},
+ {"year": 2005, "sum": 225000},
+ {"year": 2007, "sum": 55000},
+ {"year": 2008, "sum": 45000},
+ {"year": 2009, "sum": 128000},
+ {"year": 2011, "sum": 60000},
+ {"year": 2012, "sum": 40000},
+ {"year": 2013, "sum": 84000},
]
for idx, val in zip(range(len(results)), results):
with self.subTest(result=val):
@@ -716,189 +1091,248 @@ class WindowFunctionTests(TestCase):
def test_fail_update(self):
"""Window expressions can't be used in an UPDATE statement."""
msg = (
- 'Window expressions are not allowed in this query (salary=<Window: '
- 'Max(Col(expressions_window_employee, expressions_window.Employee.salary)) '
- 'OVER (PARTITION BY Col(expressions_window_employee, '
- 'expressions_window.Employee.department))>).'
+ "Window expressions are not allowed in this query (salary=<Window: "
+ "Max(Col(expressions_window_employee, expressions_window.Employee.salary)) "
+ "OVER (PARTITION BY Col(expressions_window_employee, "
+ "expressions_window.Employee.department))>)."
)
with self.assertRaisesMessage(FieldError, msg):
- Employee.objects.filter(department='Management').update(
- salary=Window(expression=Max('salary'), partition_by='department'),
+ Employee.objects.filter(department="Management").update(
+ salary=Window(expression=Max("salary"), partition_by="department"),
)
def test_fail_insert(self):
"""Window expressions can't be used in an INSERT statement."""
msg = (
- 'Window expressions are not allowed in this query (salary=<Window: '
- 'Sum(Value(10000), order_by=OrderBy(F(pk), descending=False)) OVER ()'
+ "Window expressions are not allowed in this query (salary=<Window: "
+ "Sum(Value(10000), order_by=OrderBy(F(pk), descending=False)) OVER ()"
)
with self.assertRaisesMessage(FieldError, msg):
Employee.objects.create(
- name='Jameson', department='Management', hire_date=datetime.date(2007, 7, 1),
- salary=Window(expression=Sum(Value(10000), order_by=F('pk').asc())),
+ name="Jameson",
+ department="Management",
+ hire_date=datetime.date(2007, 7, 1),
+ salary=Window(expression=Sum(Value(10000), order_by=F("pk").asc())),
)
def test_window_expression_within_subquery(self):
subquery_qs = Employee.objects.annotate(
- highest=Window(FirstValue('id'), partition_by=F('department'), order_by=F('salary').desc())
- ).values('highest')
+ highest=Window(
+ FirstValue("id"),
+ partition_by=F("department"),
+ order_by=F("salary").desc(),
+ )
+ ).values("highest")
highest_salary = Employee.objects.filter(pk__in=subquery_qs)
- self.assertCountEqual(highest_salary.values('department', 'salary'), [
- {'department': 'Accounting', 'salary': 50000},
- {'department': 'Sales', 'salary': 55000},
- {'department': 'Marketing', 'salary': 40000},
- {'department': 'IT', 'salary': 60000},
- {'department': 'Management', 'salary': 100000}
- ])
-
- @skipUnlessDBFeature('supports_json_field')
+ self.assertCountEqual(
+ highest_salary.values("department", "salary"),
+ [
+ {"department": "Accounting", "salary": 50000},
+ {"department": "Sales", "salary": 55000},
+ {"department": "Marketing", "salary": 40000},
+ {"department": "IT", "salary": 60000},
+ {"department": "Management", "salary": 100000},
+ ],
+ )
+
+ @skipUnlessDBFeature("supports_json_field")
def test_key_transform(self):
- Detail.objects.bulk_create([
- Detail(value={'department': 'IT', 'name': 'Smith', 'salary': 37000}),
- Detail(value={'department': 'IT', 'name': 'Nowak', 'salary': 32000}),
- Detail(value={'department': 'HR', 'name': 'Brown', 'salary': 50000}),
- Detail(value={'department': 'HR', 'name': 'Smith', 'salary': 55000}),
- Detail(value={'department': 'PR', 'name': 'Moore', 'salary': 90000}),
- ])
+ Detail.objects.bulk_create(
+ [
+ Detail(value={"department": "IT", "name": "Smith", "salary": 37000}),
+ Detail(value={"department": "IT", "name": "Nowak", "salary": 32000}),
+ Detail(value={"department": "HR", "name": "Brown", "salary": 50000}),
+ Detail(value={"department": "HR", "name": "Smith", "salary": 55000}),
+ Detail(value={"department": "PR", "name": "Moore", "salary": 90000}),
+ ]
+ )
tests = [
- (KeyTransform('department', 'value'), KeyTransform('name', 'value')),
- (F('value__department'), F('value__name')),
+ (KeyTransform("department", "value"), KeyTransform("name", "value")),
+ (F("value__department"), F("value__name")),
]
for partition_by, order_by in tests:
with self.subTest(partition_by=partition_by, order_by=order_by):
- qs = Detail.objects.annotate(department_sum=Window(
- expression=Sum(Cast(
- KeyTextTransform('salary', 'value'),
- output_field=IntegerField(),
- )),
- partition_by=[partition_by],
- order_by=[order_by],
- )).order_by('value__department', 'department_sum')
- self.assertQuerysetEqual(qs, [
- ('Brown', 'HR', 50000, 50000),
- ('Smith', 'HR', 55000, 105000),
- ('Nowak', 'IT', 32000, 32000),
- ('Smith', 'IT', 37000, 69000),
- ('Moore', 'PR', 90000, 90000),
- ], lambda entry: (
- entry.value['name'],
- entry.value['department'],
- entry.value['salary'],
- entry.department_sum,
- ))
+ qs = Detail.objects.annotate(
+ department_sum=Window(
+ expression=Sum(
+ Cast(
+ KeyTextTransform("salary", "value"),
+ output_field=IntegerField(),
+ )
+ ),
+ partition_by=[partition_by],
+ order_by=[order_by],
+ )
+ ).order_by("value__department", "department_sum")
+ self.assertQuerysetEqual(
+ qs,
+ [
+ ("Brown", "HR", 50000, 50000),
+ ("Smith", "HR", 55000, 105000),
+ ("Nowak", "IT", 32000, 32000),
+ ("Smith", "IT", 37000, 69000),
+ ("Moore", "PR", 90000, 90000),
+ ],
+ lambda entry: (
+ entry.value["name"],
+ entry.value["department"],
+ entry.value["salary"],
+ entry.department_sum,
+ ),
+ )
def test_invalid_start_value_range(self):
msg = "start argument must be a negative integer, zero, or None, but got '3'."
with self.assertRaisesMessage(ValueError, msg):
- list(Employee.objects.annotate(test=Window(
- expression=Sum('salary'),
- order_by=F('hire_date').asc(),
- frame=ValueRange(start=3),
- )))
+ list(
+ Employee.objects.annotate(
+ test=Window(
+ expression=Sum("salary"),
+ order_by=F("hire_date").asc(),
+ frame=ValueRange(start=3),
+ )
+ )
+ )
def test_invalid_end_value_range(self):
msg = "end argument must be a positive integer, zero, or None, but got '-3'."
with self.assertRaisesMessage(ValueError, msg):
- list(Employee.objects.annotate(test=Window(
- expression=Sum('salary'),
- order_by=F('hire_date').asc(),
- frame=ValueRange(end=-3),
- )))
+ list(
+ Employee.objects.annotate(
+ test=Window(
+ expression=Sum("salary"),
+ order_by=F("hire_date").asc(),
+ frame=ValueRange(end=-3),
+ )
+ )
+ )
def test_invalid_type_end_value_range(self):
msg = "end argument must be a positive integer, zero, or None, but got 'a'."
with self.assertRaisesMessage(ValueError, msg):
- list(Employee.objects.annotate(test=Window(
- expression=Sum('salary'),
- order_by=F('hire_date').asc(),
- frame=ValueRange(end='a'),
- )))
+ list(
+ Employee.objects.annotate(
+ test=Window(
+ expression=Sum("salary"),
+ order_by=F("hire_date").asc(),
+ frame=ValueRange(end="a"),
+ )
+ )
+ )
def test_invalid_type_start_value_range(self):
msg = "start argument must be a negative integer, zero, or None, but got 'a'."
with self.assertRaisesMessage(ValueError, msg):
- list(Employee.objects.annotate(test=Window(
- expression=Sum('salary'),
- frame=ValueRange(start='a'),
- )))
+ list(
+ Employee.objects.annotate(
+ test=Window(
+ expression=Sum("salary"),
+ frame=ValueRange(start="a"),
+ )
+ )
+ )
def test_invalid_type_end_row_range(self):
msg = "end argument must be a positive integer, zero, or None, but got 'a'."
with self.assertRaisesMessage(ValueError, msg):
- list(Employee.objects.annotate(test=Window(
- expression=Sum('salary'),
- frame=RowRange(end='a'),
- )))
+ list(
+ Employee.objects.annotate(
+ test=Window(
+ expression=Sum("salary"),
+ frame=RowRange(end="a"),
+ )
+ )
+ )
- @skipUnlessDBFeature('only_supports_unbounded_with_preceding_and_following')
+ @skipUnlessDBFeature("only_supports_unbounded_with_preceding_and_following")
def test_unsupported_range_frame_start(self):
- msg = '%s only supports UNBOUNDED together with PRECEDING and FOLLOWING.' % connection.display_name
+ msg = (
+ "%s only supports UNBOUNDED together with PRECEDING and FOLLOWING."
+ % connection.display_name
+ )
with self.assertRaisesMessage(NotSupportedError, msg):
- list(Employee.objects.annotate(test=Window(
- expression=Sum('salary'),
- order_by=F('hire_date').asc(),
- frame=ValueRange(start=-1),
- )))
+ list(
+ Employee.objects.annotate(
+ test=Window(
+ expression=Sum("salary"),
+ order_by=F("hire_date").asc(),
+ frame=ValueRange(start=-1),
+ )
+ )
+ )
- @skipUnlessDBFeature('only_supports_unbounded_with_preceding_and_following')
+ @skipUnlessDBFeature("only_supports_unbounded_with_preceding_and_following")
def test_unsupported_range_frame_end(self):
- msg = '%s only supports UNBOUNDED together with PRECEDING and FOLLOWING.' % connection.display_name
+ msg = (
+ "%s only supports UNBOUNDED together with PRECEDING and FOLLOWING."
+ % connection.display_name
+ )
with self.assertRaisesMessage(NotSupportedError, msg):
- list(Employee.objects.annotate(test=Window(
- expression=Sum('salary'),
- order_by=F('hire_date').asc(),
- frame=ValueRange(end=1),
- )))
+ list(
+ Employee.objects.annotate(
+ test=Window(
+ expression=Sum("salary"),
+ order_by=F("hire_date").asc(),
+ frame=ValueRange(end=1),
+ )
+ )
+ )
def test_invalid_type_start_row_range(self):
msg = "start argument must be a negative integer, zero, or None, but got 'a'."
with self.assertRaisesMessage(ValueError, msg):
- list(Employee.objects.annotate(test=Window(
- expression=Sum('salary'),
- order_by=F('hire_date').asc(),
- frame=RowRange(start='a'),
- )))
+ list(
+ Employee.objects.annotate(
+ test=Window(
+ expression=Sum("salary"),
+ order_by=F("hire_date").asc(),
+ frame=RowRange(start="a"),
+ )
+ )
+ )
class WindowUnsupportedTests(TestCase):
def test_unsupported_backend(self):
- msg = 'This backend does not support window expressions.'
- with mock.patch.object(connection.features, 'supports_over_clause', False):
+ msg = "This backend does not support window expressions."
+ with mock.patch.object(connection.features, "supports_over_clause", False):
with self.assertRaisesMessage(NotSupportedError, msg):
- Employee.objects.annotate(dense_rank=Window(expression=DenseRank())).get()
+ Employee.objects.annotate(
+ dense_rank=Window(expression=DenseRank())
+ ).get()
class NonQueryWindowTests(SimpleTestCase):
def test_window_repr(self):
self.assertEqual(
- repr(Window(expression=Sum('salary'), partition_by='department')),
- '<Window: Sum(F(salary)) OVER (PARTITION BY F(department))>'
+ repr(Window(expression=Sum("salary"), partition_by="department")),
+ "<Window: Sum(F(salary)) OVER (PARTITION BY F(department))>",
)
self.assertEqual(
- repr(Window(expression=Avg('salary'), order_by=F('department').asc())),
- '<Window: Avg(F(salary)) OVER (OrderByList(OrderBy(F(department), descending=False)))>'
+ repr(Window(expression=Avg("salary"), order_by=F("department").asc())),
+ "<Window: Avg(F(salary)) OVER (OrderByList(OrderBy(F(department), descending=False)))>",
)
def test_window_frame_repr(self):
self.assertEqual(
repr(RowRange(start=-1)),
- '<RowRange: ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING>'
+ "<RowRange: ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING>",
)
self.assertEqual(
repr(ValueRange(start=None, end=1)),
- '<ValueRange: RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING>'
+ "<ValueRange: RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING>",
)
self.assertEqual(
repr(ValueRange(start=0, end=0)),
- '<ValueRange: RANGE BETWEEN CURRENT ROW AND CURRENT ROW>'
+ "<ValueRange: RANGE BETWEEN CURRENT ROW AND CURRENT ROW>",
)
self.assertEqual(
repr(RowRange(start=0, end=0)),
- '<RowRange: ROWS BETWEEN CURRENT ROW AND CURRENT ROW>'
+ "<RowRange: ROWS BETWEEN CURRENT ROW AND CURRENT ROW>",
)
def test_empty_group_by_cols(self):
- window = Window(expression=Sum('pk'))
+ window = Window(expression=Sum("pk"))
self.assertEqual(window.get_group_by_cols(), [])
self.assertFalse(window.contains_aggregate)
@@ -908,49 +1342,49 @@ class NonQueryWindowTests(SimpleTestCase):
def test_frame_window_frame_notimplemented(self):
frame = WindowFrame()
- msg = 'Subclasses must implement window_frame_start_end().'
+ msg = "Subclasses must implement window_frame_start_end()."
with self.assertRaisesMessage(NotImplementedError, msg):
frame.window_frame_start_end(None, None, None)
def test_invalid_filter(self):
- msg = 'Window is disallowed in the filter clause'
+ msg = "Window is disallowed in the filter clause"
qs = Employee.objects.annotate(dense_rank=Window(expression=DenseRank()))
with self.assertRaisesMessage(NotSupportedError, msg):
qs.filter(dense_rank__gte=1)
with self.assertRaisesMessage(NotSupportedError, msg):
- qs.annotate(inc_rank=F('dense_rank') + Value(1)).filter(inc_rank__gte=1)
+ qs.annotate(inc_rank=F("dense_rank") + Value(1)).filter(inc_rank__gte=1)
with self.assertRaisesMessage(NotSupportedError, msg):
- qs.filter(id=F('dense_rank'))
+ qs.filter(id=F("dense_rank"))
with self.assertRaisesMessage(NotSupportedError, msg):
- qs.filter(id=Func('dense_rank', 2, function='div'))
+ qs.filter(id=Func("dense_rank", 2, function="div"))
with self.assertRaisesMessage(NotSupportedError, msg):
- qs.annotate(total=Sum('dense_rank', filter=Q(name='Jones'))).filter(total=1)
+ qs.annotate(total=Sum("dense_rank", filter=Q(name="Jones"))).filter(total=1)
def test_conditional_annotation(self):
qs = Employee.objects.annotate(
dense_rank=Window(expression=DenseRank()),
).annotate(
equal=Case(
- When(id=F('dense_rank'), then=Value(True)),
+ When(id=F("dense_rank"), then=Value(True)),
default=Value(False),
output_field=BooleanField(),
),
)
# The SQL standard disallows referencing window functions in the WHERE
# clause.
- msg = 'Window is disallowed in the filter clause'
+ msg = "Window is disallowed in the filter clause"
with self.assertRaisesMessage(NotSupportedError, msg):
qs.filter(equal=True)
def test_invalid_order_by(self):
msg = (
- 'Window.order_by must be either a string reference to a field, an '
- 'expression, or a list or tuple of them.'
+ "Window.order_by must be either a string reference to a field, an "
+ "expression, or a list or tuple of them."
)
with self.assertRaisesMessage(ValueError, msg):
- Window(expression=Sum('power'), order_by={'-horse'})
+ Window(expression=Sum("power"), order_by={"-horse"})
def test_invalid_source_expression(self):
msg = "Expression 'Upper' isn't compatible with OVER clauses."
with self.assertRaisesMessage(ValueError, msg):
- Window(expression=Upper('name'))
+ Window(expression=Upper("name"))