12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036 |
- import datetime
- from decimal import Decimal
- from unittest import mock
- from django.core.exceptions import FieldError
- from django.db import NotSupportedError, connection
- from django.db.models import (
- Avg,
- Case,
- Count,
- F,
- IntegerField,
- Max,
- Min,
- OuterRef,
- Q,
- RowRange,
- Subquery,
- Sum,
- Value,
- ValueRange,
- When,
- Window,
- WindowFrame,
- WindowFrameExclusion,
- )
- 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,
- )
- from django.db.models.lookups import Exact
- from django.test import SimpleTestCase, TestCase, skipIfDBFeature, skipUnlessDBFeature
- from django.test.utils import CaptureQueriesContext
- from .models import Classification, Detail, Employee, PastEmployeeDepartment
- @skipUnlessDBFeature("supports_over_clause")
- class WindowFunctionTests(TestCase):
- @classmethod
- def setUpTestData(cls):
- classification = Classification.objects.create()
- 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,
- classification=classification,
- )
- 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),
- ]
- ]
- )
- employees = list(Employee.objects.order_by("pk"))
- PastEmployeeDepartment.objects.bulk_create(
- [
- PastEmployeeDepartment(employee=employees[6], department="Sales"),
- PastEmployeeDepartment(employee=employees[10], department="IT"),
- ]
- )
- def test_dense_rank(self):
- tests = [
- 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,
- )
- 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,
- ),
- )
- def test_rank(self):
- """
- Rank the employees based on the year they're were hired. Since there
- 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,
- )
- def test_row_number(self):
- """
- The row number window function computes the number based on the order
- in which the tuples were inserted. Depending on the backend,
- 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),
- )
- def test_row_number_no_ordering(self):
- """
- The row number window function computes the number based on the order
- in which the tuples were inserted.
- """
-
- 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,
- ),
- )
- def test_lag(self):
- """
- Compute the difference between an employee's salary and the next
- 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),
- )
- 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),
- )
- def test_order_by_decimalfield(self):
- qs = Employee.objects.annotate(
- rank=Window(expression=Rank(), order_by="bonus")
- ).order_by("-bonus", "id")
- self.assertQuerySetEqual(
- qs,
- [
- ("Miller", 250.0, 12),
- ("Johnson", 200.0, 11),
- ("Wilkinson", 150.0, 10),
- ("Smith", 137.5, 9),
- ("Brown", 132.5, 8),
- ("Adams", 125.0, 7),
- ("Jones", 112.5, 5),
- ("Jenson", 112.5, 5),
- ("Johnson", 100.0, 4),
- ("Smith", 95.0, 3),
- ("Williams", 92.5, 2),
- ("Moore", 85.0, 1),
- ],
- transform=lambda row: (row.name, float(row.bonus), row.rank),
- )
- 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,
- ),
- )
- 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,
- )
- 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),
- ],
- )
- 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),
- )
- 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,
- ),
- )
- 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")
-
- 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,
- ),
- )
- def test_lead(self):
- """
- Determine what the next person hired in the same department makes.
- Because the dataset is ambiguous, the name is also part of the
- 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,
- ),
- )
- 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")
- 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]
- )
- def test_ntile(self):
- """
- Compute the group for each of the employees across the entire company,
- 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),
- )
- 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")
-
- 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]
- )
- 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")],
- ),
- past_department_count=Count("past_departments"),
- ).order_by("department", "hire_date", "name")
- self.assertQuerySetEqual(
- qs,
- [
- ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 45000, 0),
- ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 45000, 0),
- ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 37000, 0),
- ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 50000, 0),
- ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 60000, 0),
- ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 34000, 0),
- ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 100000, 1),
- ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 100000, 0),
- ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 38000, 0),
- ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 40000, 1),
- ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 55000, 0),
- ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 53000, 0),
- ],
- transform=lambda row: (
- row.name,
- row.salary,
- row.department,
- row.hire_date,
- row.max,
- row.past_department_count,
- ),
- )
- def test_multiple_ordering(self):
- """
- Accumulate the salaries over the departments based on hire_date.
- 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,
- ),
- )
- def test_empty_ordering(self):
- """
- Explicit empty ordering makes little sense but it is something that
- was historically allowed.
- """
- qs = Employee.objects.annotate(
- sum=Window(
- expression=Sum("salary"),
- partition_by="department",
- order_by=[],
- )
- ).order_by("department", "sum")
- self.assertEqual(len(qs), 12)
- 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"],
- )
- )
- self.assertEqual(qs.count(), 12)
- def test_filter(self):
- qs = Employee.objects.annotate(
- department_salary_rank=Window(
- Rank(), partition_by="department", order_by="-salary"
- ),
- department_avg_age_diff=(
- Window(Avg("age"), partition_by="department") - F("age")
- ),
- ).order_by("department", "name")
-
- self.assertQuerySetEqual(
- qs.filter(department_salary_rank=1),
- ["Adams", "Wilkinson", "Miller", "Johnson", "Smith"],
- lambda employee: employee.name,
- )
-
- self.assertQuerySetEqual(
- qs.filter(department_avg_age_diff__gt=0),
- ["Jenson", "Jones", "Williams", "Miller", "Smith"],
- lambda employee: employee.name,
- )
-
- self.assertQuerySetEqual(
- qs.filter(department_salary_rank=1, department_avg_age_diff__gt=0),
- ["Miller"],
- lambda employee: employee.name,
- )
-
- self.assertQuerySetEqual(
- qs.filter(Q(department_salary_rank=1) | Q(department_avg_age_diff__gt=0)),
- [
- "Adams",
- "Jenson",
- "Jones",
- "Williams",
- "Wilkinson",
- "Miller",
- "Johnson",
- "Smith",
- "Smith",
- ],
- lambda employee: employee.name,
- )
- def test_filter_conditional_annotation(self):
- qs = (
- Employee.objects.annotate(
- rank=Window(Rank(), partition_by="department", order_by="-salary"),
- case_first_rank=Case(
- When(rank=1, then=True),
- default=False,
- ),
- q_first_rank=Q(rank=1),
- )
- .order_by("name")
- .values_list("name", flat=True)
- )
- for annotation in ["case_first_rank", "q_first_rank"]:
- with self.subTest(annotation=annotation):
- self.assertSequenceEqual(
- qs.filter(**{annotation: True}),
- ["Adams", "Johnson", "Miller", "Smith", "Wilkinson"],
- )
- def test_filter_conditional_expression(self):
- qs = (
- Employee.objects.filter(
- Exact(Window(Rank(), partition_by="department", order_by="-salary"), 1)
- )
- .order_by("name")
- .values_list("name", flat=True)
- )
- self.assertSequenceEqual(
- qs, ["Adams", "Johnson", "Miller", "Smith", "Wilkinson"]
- )
- def test_filter_column_ref_rhs(self):
- qs = (
- Employee.objects.annotate(
- max_dept_salary=Window(Max("salary"), partition_by="department")
- )
- .filter(max_dept_salary=F("salary"))
- .order_by("name")
- .values_list("name", flat=True)
- )
- self.assertSequenceEqual(
- qs, ["Adams", "Johnson", "Miller", "Smith", "Wilkinson"]
- )
- def test_filter_values(self):
- qs = (
- Employee.objects.annotate(
- department_salary_rank=Window(
- Rank(), partition_by="department", order_by="-salary"
- ),
- )
- .order_by("department", "name")
- .values_list(Upper("name"), flat=True)
- )
- self.assertSequenceEqual(
- qs.filter(department_salary_rank=1),
- ["ADAMS", "WILKINSON", "MILLER", "JOHNSON", "SMITH"],
- )
- def test_filter_alias(self):
- qs = Employee.objects.alias(
- department_avg_age_diff=(
- Window(Avg("age"), partition_by="department") - F("age")
- ),
- ).order_by("department", "name")
- self.assertQuerySetEqual(
- qs.filter(department_avg_age_diff__gt=0),
- ["Jenson", "Jones", "Williams", "Miller", "Smith"],
- lambda employee: employee.name,
- )
- def test_filter_select_related(self):
- qs = (
- Employee.objects.alias(
- department_avg_age_diff=(
- Window(Avg("age"), partition_by="department") - F("age")
- ),
- )
- .select_related("classification")
- .filter(department_avg_age_diff__gt=0)
- .order_by("department", "name")
- )
- self.assertQuerySetEqual(
- qs,
- ["Jenson", "Jones", "Williams", "Miller", "Smith"],
- lambda employee: employee.name,
- )
- with self.assertNumQueries(0):
- qs[0].classification
- def test_exclude(self):
- qs = Employee.objects.annotate(
- department_salary_rank=Window(
- Rank(), partition_by="department", order_by="-salary"
- ),
- department_avg_age_diff=(
- Window(Avg("age"), partition_by="department") - F("age")
- ),
- ).order_by("department", "name")
-
- self.assertQuerySetEqual(
- qs.exclude(department_salary_rank__gt=1),
- ["Adams", "Wilkinson", "Miller", "Johnson", "Smith"],
- lambda employee: employee.name,
- )
-
- self.assertQuerySetEqual(
- qs.exclude(department_avg_age_diff__lte=0),
- ["Jenson", "Jones", "Williams", "Miller", "Smith"],
- lambda employee: employee.name,
- )
-
- self.assertQuerySetEqual(
- qs.exclude(
- Q(department_salary_rank__gt=1) | Q(department_avg_age_diff__lte=0)
- ),
- ["Miller"],
- lambda employee: employee.name,
- )
-
- self.assertQuerySetEqual(
- qs.exclude(department_salary_rank__gt=1, department_avg_age_diff__lte=0),
- [
- "Adams",
- "Jenson",
- "Jones",
- "Williams",
- "Wilkinson",
- "Miller",
- "Johnson",
- "Smith",
- "Smith",
- ],
- lambda employee: employee.name,
- )
- def test_heterogeneous_filter(self):
- qs = (
- Employee.objects.annotate(
- department_salary_rank=Window(
- Rank(), partition_by="department", order_by="-salary"
- ),
- )
- .order_by("name")
- .values_list("name", flat=True)
- )
-
-
- self.assertSequenceEqual(
- qs.filter(
- department_salary_rank=1, department__in=["Accounting", "Management"]
- ),
- ["Adams", "Miller"],
- )
- self.assertSequenceEqual(
- qs.filter(
- Q(department_salary_rank=1)
- | Q(department__in=["Accounting", "Management"])
- ),
- [
- "Adams",
- "Jenson",
- "Johnson",
- "Johnson",
- "Jones",
- "Miller",
- "Smith",
- "Wilkinson",
- "Williams",
- ],
- )
-
-
- qs = qs.annotate(past_department_count=Count("past_departments"))
- self.assertSequenceEqual(
- qs.filter(department_salary_rank=1, past_department_count__gte=1),
- ["Johnson", "Miller"],
- )
- self.assertSequenceEqual(
- qs.filter(Q(department_salary_rank=1) | Q(past_department_count__gte=1)),
- ["Adams", "Johnson", "Miller", "Smith", "Wilkinson"],
- )
- def test_limited_filter(self):
- """
- A query filtering against a window function have its limit applied
- after window filtering takes place.
- """
- self.assertQuerySetEqual(
- Employee.objects.annotate(
- department_salary_rank=Window(
- Rank(), partition_by="department", order_by="-salary"
- )
- )
- .filter(department_salary_rank=1)
- .order_by("department")[0:3],
- ["Adams", "Wilkinson", "Miller"],
- lambda employee: employee.name,
- )
- def test_filter_count(self):
- with CaptureQueriesContext(connection) as ctx:
- self.assertEqual(
- Employee.objects.annotate(
- department_salary_rank=Window(
- Rank(), partition_by="department", order_by="-salary"
- )
- )
- .filter(department_salary_rank=1)
- .count(),
- 5,
- )
- self.assertEqual(len(ctx.captured_queries), 1)
- sql = ctx.captured_queries[0]["sql"].lower()
- self.assertEqual(sql.count("select"), 3)
- self.assertNotIn("group by", sql)
- @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,
- )
- @skipUnlessDBFeature(
- "supports_frame_exclusion", "supports_frame_range_fixed_distance"
- )
- def test_range_exclude_current(self):
- qs = Employee.objects.annotate(
- sum=Window(
- expression=Sum("salary"),
- order_by=F("salary").asc(),
- partition_by="department",
- frame=ValueRange(end=2, exclusion=WindowFrameExclusion.CURRENT_ROW),
- )
- ).order_by("department", "salary")
- self.assertIn(
- "RANGE BETWEEN UNBOUNDED PRECEDING AND 2 FOLLOWING EXCLUDE CURRENT ROW",
- str(qs.query),
- )
- self.assertQuerySetEqual(
- qs,
- [
- ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), None),
- ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 82000),
- ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 82000),
- ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 127000),
- ("Moore", 34000, "IT", datetime.date(2013, 8, 1), None),
- ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 34000),
- ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), None),
- ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 80000),
- ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), None),
- ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 38000),
- ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), None),
- ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 53000),
- ],
- transform=lambda row: (
- row.name,
- row.salary,
- row.department,
- row.hire_date,
- row.sum,
- ),
- )
- 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,
- ),
- )
- def test_subquery_row_range_rank(self):
- qs = Employee.objects.annotate(
- highest_avg_salary_date=Subquery(
- Employee.objects.filter(
- department=OuterRef("department"),
- )
- .annotate(
- avg_salary=Window(
- 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("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,
- ),
- )
- @skipUnlessDBFeature("supports_frame_exclusion")
- def test_row_range_rank_exclude_current_row(self):
- qs = Employee.objects.annotate(
- avg_salary_cohort=Window(
- expression=Avg("salary"),
- order_by=[F("hire_date").asc(), F("name").desc()],
- frame=RowRange(
- start=-1, end=1, exclusion=WindowFrameExclusion.CURRENT_ROW
- ),
- )
- ).order_by("hire_date")
- self.assertIn(
- "ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE CURRENT ROW",
- str(qs.query),
- )
- self.assertQuerySetEqual(
- qs,
- [
- ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 80000),
- ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 72500),
- ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 67500),
- ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 45000),
- ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 46000),
- ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 49000),
- ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 37500),
- ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 56500),
- ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 39000),
- ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 55000),
- ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 37000),
- ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 50000),
- ],
- transform=lambda row: (
- row.name,
- row.salary,
- row.department,
- row.hire_date,
- row.avg_salary_cohort,
- ),
- )
- @skipUnlessDBFeature("supports_frame_exclusion")
- def test_row_range_rank_exclude_group(self):
- qs = Employee.objects.annotate(
- avg_salary_cohort=Window(
- expression=Avg("salary"),
- order_by=[F("hire_date").asc(), F("name").desc()],
- frame=RowRange(start=-1, end=1, exclusion=WindowFrameExclusion.GROUP),
- )
- ).order_by("hire_date")
- self.assertIn(
- "ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE GROUP",
- str(qs.query),
- )
- self.assertQuerySetEqual(
- qs,
- [
- ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 80000),
- ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 72500),
- ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 67500),
- ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 45000),
- ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 46000),
- ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 49000),
- ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 37500),
- ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 56500),
- ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 39000),
- ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 55000),
- ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 37000),
- ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 50000),
- ],
- transform=lambda row: (
- row.name,
- row.salary,
- row.department,
- row.hire_date,
- row.avg_salary_cohort,
- ),
- )
- @skipUnlessDBFeature("supports_frame_exclusion")
- def test_row_range_rank_exclude_ties(self):
- qs = Employee.objects.annotate(
- sum_salary_cohort=Window(
- expression=Sum("salary"),
- order_by=[F("hire_date").asc(), F("name").desc()],
- frame=RowRange(start=-1, end=1, exclusion=WindowFrameExclusion.TIES),
- )
- ).order_by("hire_date")
- self.assertIn(
- "ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE TIES",
- str(qs.query),
- )
- self.assertQuerySetEqual(
- qs,
- [
- ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 180000),
- ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 225000),
- ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 180000),
- ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 145000),
- ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 137000),
- ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 135000),
- ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 128000),
- ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 151000),
- ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 138000),
- ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 150000),
- ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 124000),
- ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 84000),
- ],
- transform=lambda row: (
- row.name,
- row.salary,
- row.department,
- row.hire_date,
- row.sum_salary_cohort,
- ),
- )
- @skipUnlessDBFeature("supports_frame_exclusion")
- def test_row_range_rank_exclude_no_others(self):
- qs = Employee.objects.annotate(
- sum_salary_cohort=Window(
- expression=Sum("salary"),
- order_by=[F("hire_date").asc(), F("name").desc()],
- frame=RowRange(
- start=-1, end=1, exclusion=WindowFrameExclusion.NO_OTHERS
- ),
- )
- ).order_by("hire_date")
- self.assertIn(
- "ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE NO OTHERS",
- str(qs.query),
- )
- self.assertQuerySetEqual(
- qs,
- [
- ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 180000),
- ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 225000),
- ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 180000),
- ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 145000),
- ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 137000),
- ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 135000),
- ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 128000),
- ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 151000),
- ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 138000),
- ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 150000),
- ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 124000),
- ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 84000),
- ],
- transform=lambda row: (
- row.name,
- row.salary,
- row.department,
- row.hire_date,
- row.sum_salary_cohort,
- ),
- )
- @skipIfDBFeature("supports_frame_exclusion")
- def test_unsupported_frame_exclusion_raises_error(self):
- msg = "This backend does not support window frame exclusions."
- with self.assertRaisesMessage(NotSupportedError, msg):
- list(
- Employee.objects.annotate(
- avg_salary_cohort=Window(
- expression=Avg("salary"),
- order_by=[F("hire_date").asc(), F("name").desc()],
- frame=RowRange(
- start=-1, end=1, exclusion=WindowFrameExclusion.CURRENT_ROW
- ),
- )
- )
- )
- @skipUnlessDBFeature("supports_frame_exclusion")
- def test_invalid_frame_exclusion_value_raises_error(self):
- msg = "RowRange.exclusion must be a WindowFrameExclusion instance."
- with self.assertRaisesMessage(TypeError, msg):
- Employee.objects.annotate(
- avg_salary_cohort=Window(
- expression=Avg("salary"),
- order_by=[F("hire_date").asc(), F("name").desc()],
- frame=RowRange(start=-1, end=1, exclusion="RUBBISH"),
- )
- )
- def test_row_range_rank(self):
- """
- A query with ROWS BETWEEN UNBOUNDED PRECEDING AND 3 FOLLOWING.
- 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,
- ),
- )
- def test_row_range_both_preceding(self):
- """
- A query with ROWS BETWEEN 2 PRECEDING AND 1 PRECEDING.
- The resulting sum is the sum of the previous two (if they exist) 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=-2, end=-1),
- )
- ).order_by("hire_date")
- self.assertIn("ROWS BETWEEN 2 PRECEDING AND 1 PRECEDING", str(qs.query))
- self.assertQuerySetEqual(
- qs,
- [
- ("Miller", 100000, "Management", datetime.date(2005, 6, 1), None),
- ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 100000),
- ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 180000),
- ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 125000),
- ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 100000),
- ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 100000),
- ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 82000),
- ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 90000),
- ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 91000),
- ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 98000),
- ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 100000),
- ("Moore", 34000, "IT", datetime.date(2013, 8, 1), 90000),
- ],
- transform=lambda row: (
- row.name,
- row.salary,
- row.department,
- row.hire_date,
- row.sum,
- ),
- )
- def test_row_range_both_following(self):
- """
- A query with ROWS BETWEEN 1 FOLLOWING AND 2 FOLLOWING.
- The resulting sum is the sum of the following two (if they exist) 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=1, end=2),
- )
- ).order_by("hire_date")
- self.assertIn("ROWS BETWEEN 1 FOLLOWING AND 2 FOLLOWING", str(qs.query))
- self.assertQuerySetEqual(
- qs,
- [
- ("Miller", 100000, "Management", datetime.date(2005, 6, 1), 125000),
- ("Johnson", 80000, "Management", datetime.date(2005, 7, 1), 100000),
- ("Jones", 45000, "Accounting", datetime.date(2005, 11, 1), 100000),
- ("Smith", 55000, "Sales", datetime.date(2007, 6, 1), 82000),
- ("Jenson", 45000, "Accounting", datetime.date(2008, 4, 1), 90000),
- ("Williams", 37000, "Accounting", datetime.date(2009, 6, 1), 91000),
- ("Brown", 53000, "Sales", datetime.date(2009, 9, 1), 98000),
- ("Smith", 38000, "Marketing", datetime.date(2009, 10, 1), 100000),
- ("Wilkinson", 60000, "IT", datetime.date(2011, 3, 1), 90000),
- ("Johnson", 40000, "Marketing", datetime.date(2012, 3, 1), 84000),
- ("Adams", 50000, "Accounting", datetime.date(2013, 7, 1), 34000),
- ("Moore", 34000, "IT", datetime.date(2013, 8, 1), None),
- ],
- 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"),
- )
- .filter(sum__gte=45000)
- .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": 2013, "sum": 84000},
- ]
- for idx, val in zip(range(len(results)), results):
- with self.subTest(result=val):
- self.assertEqual(qs[idx], val)
- 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))>)."
- )
- with self.assertRaisesMessage(FieldError, msg):
- 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)) 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))),
- )
- 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_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")
- 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}),
- ]
- )
- tests = [
- (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,
- ),
- )
- 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),
- )
- )
- )
- 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),
- )
- )
- )
- def test_invalid_start_end_value_for_row_range(self):
- msg = "start cannot be greater than end."
- with self.assertRaisesMessage(ValueError, msg):
- list(
- Employee.objects.annotate(
- test=Window(
- expression=Sum("salary"),
- order_by=F("hire_date").asc(),
- frame=RowRange(start=4, 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"),
- )
- )
- )
- 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"),
- )
- )
- )
- def test_invalid_type_end_row_range(self):
- msg = "end argument must be an 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"),
- )
- )
- )
- @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
- )
- with self.assertRaisesMessage(NotSupportedError, msg):
- 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")
- def test_unsupported_range_frame_end(self):
- 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),
- )
- )
- )
- def test_invalid_type_start_row_range(self):
- msg = "start argument must be an 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"),
- )
- )
- )
- def test_invalid_filter(self):
- msg = (
- "Heterogeneous disjunctive predicates against window functions are not "
- "implemented when performing conditional aggregation."
- )
- qs = Employee.objects.annotate(
- window=Window(Rank()),
- past_dept_cnt=Count("past_departments"),
- )
- with self.assertRaisesMessage(NotImplementedError, msg):
- list(qs.filter(Q(window=1) | Q(department="Accounting")))
- with self.assertRaisesMessage(NotImplementedError, msg):
- list(qs.exclude(window=1, department="Accounting"))
- 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):
- with self.assertRaisesMessage(NotSupportedError, msg):
- Employee.objects.annotate(
- dense_rank=Window(expression=DenseRank())
- ).get()
- def test_filter_subquery(self):
- qs = Employee.objects.annotate(
- department_salary_rank=Window(
- Rank(), partition_by="department", order_by="-salary"
- )
- )
- msg = (
- "Referencing outer query window expression is not supported: "
- "department_salary_rank."
- )
- with self.assertRaisesMessage(NotSupportedError, msg):
- qs.annotate(
- employee_name=Subquery(
- Employee.objects.filter(
- age=OuterRef("department_salary_rank")
- ).values("name")[:1]
- )
- )
- 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))>",
- )
- self.assertEqual(
- repr(Window(expression=Avg("salary"), order_by=F("department").asc())),
- "<Window: Avg(F(salary)) OVER (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>",
- )
- self.assertEqual(
- repr(ValueRange(start=None, end=1)),
- "<ValueRange: RANGE BETWEEN UNBOUNDED PRECEDING AND 1 FOLLOWING>",
- )
- self.assertEqual(
- repr(ValueRange(start=0, end=0)),
- "<ValueRange: RANGE BETWEEN CURRENT ROW AND CURRENT ROW>",
- )
- self.assertEqual(
- repr(RowRange(start=0, end=0)),
- "<RowRange: ROWS BETWEEN CURRENT ROW AND CURRENT ROW>",
- )
- self.assertEqual(
- repr(RowRange(start=-2, end=-1)),
- "<RowRange: ROWS BETWEEN 2 PRECEDING AND 1 PRECEDING>",
- )
- self.assertEqual(
- repr(RowRange(start=1, end=2)),
- "<RowRange: ROWS BETWEEN 1 FOLLOWING AND 2 FOLLOWING>",
- )
- self.assertEqual(
- repr(RowRange(start=1, end=2, exclusion=WindowFrameExclusion.CURRENT_ROW)),
- "<RowRange: ROWS BETWEEN 1 FOLLOWING AND 2 FOLLOWING EXCLUDE CURRENT ROW>",
- )
- def test_window_frame_exclusion_repr(self):
- self.assertEqual(repr(WindowFrameExclusion.TIES), "WindowFrameExclusion.TIES")
- def test_empty_group_by_cols(self):
- window = Window(expression=Sum("pk"))
- self.assertEqual(window.get_group_by_cols(), [])
- self.assertFalse(window.contains_aggregate)
- def test_frame_empty_group_by_cols(self):
- frame = WindowFrame()
- self.assertEqual(frame.get_group_by_cols(), [])
- def test_frame_window_frame_notimplemented(self):
- frame = WindowFrame()
- msg = "Subclasses must implement window_frame_start_end()."
- with self.assertRaisesMessage(NotImplementedError, msg):
- frame.window_frame_start_end(None, None, None)
- 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 not {'-horse'}."
- )
- with self.assertRaisesMessage(ValueError, msg):
- 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"))
|