1
1
from django .contrib .postgres .indexes import GistIndex
2
2
from django .db import models
3
+ from django .db .models import functions as f
3
4
from django .db .transaction import atomic
4
5
from django .utils .crypto import get_random_string
5
6
@@ -14,6 +15,9 @@ class LtreeConcat(models.Func):
14
15
class Subpath (models .Func ):
15
16
function = 'subpath'
16
17
18
+ class Text2Ltree (models .Func ):
19
+ function = 'text2ltree'
20
+
17
21
18
22
class TreeNode (models .Model ):
19
23
__old_tree_path = None
@@ -22,6 +26,7 @@ class TreeNode(models.Model):
22
26
class Meta :
23
27
abstract = True
24
28
indexes = (GistIndex (fields = ['tree_path' ], name = 'tree_path_idx' ),)
29
+ ordering = ('tree_path' ,)
25
30
26
31
def __init__ (self , * args , parent = None , ** kwargs ):
27
32
if parent is not None :
@@ -45,31 +50,40 @@ def parent(self, new_parent):
45
50
self .tree_path = [* new_parent .tree_path , get_random_string (length = 32 )]
46
51
47
52
def save (self , * args , ** kwargs ): # pylint: disable=arguments-differ
53
+ tree_path_needs_refresh = False
48
54
if not self .tree_path :
55
+ tree_path_needs_refresh = True
49
56
# Ensure that we have a tree_path set. We set a random one at this point,
50
57
# because we don't know whether this node will become the parent of other
51
58
# nodes down the track.
52
- self .tree_path = [get_random_string (length = 32 )]
59
+ largest_ltree = models .Subquery (self .__class__ .objects .order_by ('-tree_path' ).values ('tree_path' )[:1 ])
60
+ largest_ltree_root_num = f .Cast (f .Cast (Subpath (largest_ltree , 0 , 1 ), models .CharField ()), models .BigIntegerField ())
61
+ self .tree_path = Text2Ltree (f .Cast (f .Coalesce (largest_ltree_root_num , - 2 ** 32 ) + (2 ** 32 ), models .CharField ()))
53
62
54
63
# If we haven't changed the parent, save as normal.
55
64
if self .__old_tree_path is None :
56
- return super ().save (* args , ** kwargs )
65
+ rv = super ().save (* args , ** kwargs )
57
66
58
67
# If we have, use a transaction to avoid other contexts seeing the intermediate
59
68
# state where our descendants aren't connected to us.
60
- with atomic ():
61
- rv = super ().save (* args , ** kwargs )
62
- # Move all of our descendants along with us, by substituting our old ltree
63
- # prefix with our new one, in every descendant that has that prefix.
64
- self .__class__ .objects .filter (
65
- tree_path__descendant_of = self .__old_tree_path
66
- ).update (
67
- tree_path = LtreeConcat (
68
- models .Value ('.' .join (self .tree_path )),
69
- Subpath (models .F ('tree_path' ), len (self .__old_tree_path )),
69
+ else :
70
+ with atomic ():
71
+ rv = super ().save (* args , ** kwargs )
72
+ # Move all of our descendants along with us, by substituting our old ltree
73
+ # prefix with our new one, in every descendant that has that prefix.
74
+ self .__class__ .objects .filter (
75
+ tree_path__descendant_of = self .__old_tree_path
76
+ ).update (
77
+ tree_path = LtreeConcat (
78
+ models .Value ('.' .join (self .tree_path )),
79
+ Subpath (models .F ('tree_path' ), len (self .__old_tree_path )),
80
+ )
70
81
)
71
- )
72
- return rv
82
+
83
+ if tree_path_needs_refresh :
84
+ self .refresh_from_db (fields = ('tree_path' ,))
85
+
86
+ return rv
73
87
74
88
@property
75
89
def ancestors (self ):
0 commit comments