11from django .conf import settings
2- from django .db import connection , models
3- from django .db .models import OuterRef , QuerySet , Subquery
2+ from django .db import models
3+ from django .db .models import Exists , OuterRef , Q , QuerySet
44from django .utils import timezone
55
66from simple_history .utils import (
@@ -29,13 +29,11 @@ def __init__(self, *args, **kwargs):
2929 self ._as_of = None
3030 self ._pk_attr = self .model .instance_type ._meta .pk .attname
3131
32- def as_instances (self ):
32+ def as_instances (self ) -> "HistoricalQuerySet" :
3333 """
3434 Return a queryset that generates instances instead of historical records.
3535 Queries against the resulting queryset will translate `pk` into the
3636 primary key field of the original type.
37-
38- Returns a queryset.
3937 """
4038 if not self ._as_instances :
4139 result = self .exclude (history_type = "-" )
@@ -44,7 +42,7 @@ def as_instances(self):
4442 result = self ._clone ()
4543 return result
4644
47- def filter (self , * args , ** kwargs ):
45+ def filter (self , * args , ** kwargs ) -> "HistoricalQuerySet" :
4846 """
4947 If a `pk` filter arrives and the queryset is returning instances
5048 then the caller actually wants to filter based on the original
@@ -55,43 +53,26 @@ def filter(self, *args, **kwargs):
5553 kwargs [self ._pk_attr ] = kwargs .pop ("pk" )
5654 return super ().filter (* args , ** kwargs )
5755
58- def latest_of_each (self ):
56+ def latest_of_each (self ) -> "HistoricalQuerySet" :
5957 """
6058 Ensures results in the queryset are the latest historical record for each
61- primary key. Deletions are not removed.
62-
63- Returns a queryset.
59+ primary key. This includes deletion records.
6460 """
65- # If using MySQL, need to get a list of IDs in memory and then use them for the
66- # second query.
67- # Does mean two loops through the DB to get the full set, but still a speed
68- # improvement.
69- backend = connection .vendor
70- if backend == "mysql" :
71- history_ids = {}
72- for item in self .order_by ("-history_date" , "-pk" ):
73- if getattr (item , self ._pk_attr ) not in history_ids :
74- history_ids [getattr (item , self ._pk_attr )] = item .pk
75- latest_historics = self .filter (history_id__in = history_ids .values ())
76- elif backend == "postgresql" :
77- latest_pk_attr_historic_ids = (
78- self .order_by (self ._pk_attr , "-history_date" , "-pk" )
79- .distinct (self ._pk_attr )
80- .values_list ("pk" , flat = True )
81- )
82- latest_historics = self .filter (history_id__in = latest_pk_attr_historic_ids )
83- else :
84- latest_pk_attr_historic_ids = (
85- self .filter (** {self ._pk_attr : OuterRef (self ._pk_attr )})
86- .order_by ("-history_date" , "-pk" )
87- .values ("pk" )[:1 ]
88- )
89- latest_historics = self .filter (
90- history_id__in = Subquery (latest_pk_attr_historic_ids )
91- )
92- return latest_historics
61+ # Subquery for finding the records that belong to the same history-tracked
62+ # object as the record from the outer query (identified by `_pk_attr`),
63+ # and that have a later `history_date` than the outer record.
64+ # The very latest record of a history-tracked object should be excluded from
65+ # this query - which will make it included in the `~Exists` query below.
66+ later_records = self .filter (
67+ Q (** {self ._pk_attr : OuterRef (self ._pk_attr )}),
68+ Q (history_date__gt = OuterRef ("history_date" )),
69+ )
70+
71+ # Filter the records to only include those for which the `later_records`
72+ # subquery does not return any results.
73+ return self .filter (~ Exists (later_records ))
9374
94- def _select_related_history_tracked_objs (self ):
75+ def _select_related_history_tracked_objs (self ) -> "HistoricalQuerySet" :
9576 """
9677 A convenience method that calls ``select_related()`` with all the names of
9778 the model's history-tracked ``ForeignKey`` fields.
@@ -103,18 +84,18 @@ def _select_related_history_tracked_objs(self):
10384 ]
10485 return self .select_related (* field_names )
10586
106- def _clone (self ):
87+ def _clone (self ) -> "HistoricalQuerySet" :
10788 c = super ()._clone ()
10889 c ._as_instances = self ._as_instances
10990 c ._as_of = self ._as_of
11091 c ._pk_attr = self ._pk_attr
11192 return c
11293
113- def _fetch_all (self ):
94+ def _fetch_all (self ) -> None :
11495 super ()._fetch_all ()
11596 self ._instanceize ()
11697
117- def _instanceize (self ):
98+ def _instanceize (self ) -> None :
11899 """
119100 Convert the result cache to instances if possible and it has not already been
120101 done. If a query extracts `.values(...)` then the result cache will not contain
0 commit comments