1
1
from 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
4
4
from django .utils import timezone
5
5
6
6
from simple_history .utils import (
@@ -29,13 +29,11 @@ def __init__(self, *args, **kwargs):
29
29
self ._as_of = None
30
30
self ._pk_attr = self .model .instance_type ._meta .pk .attname
31
31
32
- def as_instances (self ):
32
+ def as_instances (self ) -> "HistoricalQuerySet" :
33
33
"""
34
34
Return a queryset that generates instances instead of historical records.
35
35
Queries against the resulting queryset will translate `pk` into the
36
36
primary key field of the original type.
37
-
38
- Returns a queryset.
39
37
"""
40
38
if not self ._as_instances :
41
39
result = self .exclude (history_type = "-" )
@@ -44,7 +42,7 @@ def as_instances(self):
44
42
result = self ._clone ()
45
43
return result
46
44
47
- def filter (self , * args , ** kwargs ):
45
+ def filter (self , * args , ** kwargs ) -> "HistoricalQuerySet" :
48
46
"""
49
47
If a `pk` filter arrives and the queryset is returning instances
50
48
then the caller actually wants to filter based on the original
@@ -55,43 +53,26 @@ def filter(self, *args, **kwargs):
55
53
kwargs [self ._pk_attr ] = kwargs .pop ("pk" )
56
54
return super ().filter (* args , ** kwargs )
57
55
58
- def latest_of_each (self ):
56
+ def latest_of_each (self ) -> "HistoricalQuerySet" :
59
57
"""
60
58
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.
64
60
"""
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 ))
93
74
94
- def _select_related_history_tracked_objs (self ):
75
+ def _select_related_history_tracked_objs (self ) -> "HistoricalQuerySet" :
95
76
"""
96
77
A convenience method that calls ``select_related()`` with all the names of
97
78
the model's history-tracked ``ForeignKey`` fields.
@@ -103,18 +84,18 @@ def _select_related_history_tracked_objs(self):
103
84
]
104
85
return self .select_related (* field_names )
105
86
106
- def _clone (self ):
87
+ def _clone (self ) -> "HistoricalQuerySet" :
107
88
c = super ()._clone ()
108
89
c ._as_instances = self ._as_instances
109
90
c ._as_of = self ._as_of
110
91
c ._pk_attr = self ._pk_attr
111
92
return c
112
93
113
- def _fetch_all (self ):
94
+ def _fetch_all (self ) -> None :
114
95
super ()._fetch_all ()
115
96
self ._instanceize ()
116
97
117
- def _instanceize (self ):
98
+ def _instanceize (self ) -> None :
118
99
"""
119
100
Convert the result cache to instances if possible and it has not already been
120
101
done. If a query extracts `.values(...)` then the result cache will not contain
0 commit comments