6
6
7
7
from .fields import LtreeField
8
8
9
+ GAP = 1_000_000_000
10
+
9
11
10
12
class LtreeConcat (models .Func ):
11
13
arg_joiner = '||'
@@ -20,8 +22,8 @@ class Text2Ltree(models.Func):
20
22
21
23
22
24
class TreeNode (models .Model ):
23
- __old_tree_path = None
24
- tree_path = LtreeField ()
25
+ __new_parent = None
26
+ tree_path = LtreeField (unique = True )
25
27
26
28
class Meta :
27
29
abstract = True
@@ -30,14 +32,15 @@ class Meta:
30
32
31
33
def __init__ (self , * args , parent = None , ** kwargs ):
32
34
if parent is not None :
33
- kwargs ['tree_path' ] = [* parent .tree_path ,
34
- get_random_string (length = 32 )]
35
+ self .__new_parent = parent
35
36
super ().__init__ (* args , ** kwargs )
36
37
37
38
@property
38
39
def parent (self ):
40
+ if self .__new_parent is not None :
41
+ return self .__new_parent
39
42
parent_path = self .tree_path [:-
40
- 1 ] # pylint: disable=unsubscriptable-object
43
+ 1 ] # pylint: disable=unsubscriptable-object
41
44
return self .__class__ .objects .get (tree_path = parent_path )
42
45
43
46
@parent .setter
@@ -46,22 +49,54 @@ def parent(self, new_parent):
46
49
raise ValueError (
47
50
"Parent node must be saved before receiving children" )
48
51
# Replace our tree_path with a new one that has our new parent's
49
- self .__old_tree_path = self .tree_path
50
- self .tree_path = [* new_parent .tree_path , get_random_string (length = 32 )]
52
+ self .__new_parent = new_parent
53
+
54
+ def __next_tree_path_qx (self , prefix = None ):
55
+ if prefix is None :
56
+ prefix = []
57
+
58
+ # These are all the siblings of the target position, in reverse tree order.
59
+ # If we don't have a prefix, this will be all root nodes.
60
+ sibling_queryset = self .__class__ .objects .filter (tree_path__matches_lquery = [* prefix , '*{1}' ]).order_by ('-tree_path' )
61
+ # This query expression is the full ltree of the last sibling by tree order.
62
+ last_sibling_tree_path = models .Subquery (sibling_queryset .values ('tree_path' )[:1 ])
63
+
64
+ # Django doesn't allow the use of column references in an INSERT statement,
65
+ # because it makes the assumption that they refer to columns in the
66
+ # to-be-inserted row, the values for which aren't yet known.
67
+ # Unfortunately, this means we can't use a subquery that refers to column
68
+ # values anywhere internally, even though the columns it refers to are subquery
69
+ # result columns. To get around this, we override the contains_column_references
70
+ # property on the subquery with a static False, so that Django's check doesn't
71
+ # cross the subquery boundary.
72
+ last_sibling_tree_path .contains_column_references = False
73
+
74
+ # This query expression is the rightmost component of that ltree. The double
75
+ # cast is because PostgreSQL doesn't let you cast directly from ltree to bigint.
76
+ last_sibling_last_value = f .Cast (f .Cast (Subpath (last_sibling_tree_path , - 1 ), models .CharField ()), models .BigIntegerField ())
77
+ # This query expression is an ltree containing that value, plus GAP, or just
78
+ # GAP if there is no existing siblings. Again, we need to double cast.
79
+ new_last_value = Text2Ltree (f .Cast (f .Coalesce (last_sibling_last_value , 0 ) + (GAP ), models .CharField ()))
80
+
81
+ # If we have a prefix, we prepend that to the resulting ltree.
82
+ if not prefix :
83
+ return new_last_value
84
+ return LtreeConcat (models .Value ('.' .join (prefix )), new_last_value )
51
85
52
86
def save (self , * args , ** kwargs ): # pylint: disable=arguments-differ
53
87
tree_path_needs_refresh = False
88
+ old_tree_path = None
89
+
90
+ if self .__new_parent is not None :
91
+ tree_path_needs_refresh = True
92
+ old_tree_path = self .tree_path or None
93
+ self .tree_path = self .__next_tree_path_qx (self .__new_parent .tree_path )
54
94
if not self .tree_path :
55
95
tree_path_needs_refresh = True
56
- # Ensure that we have a tree_path set. We set a random one at this point,
57
- # because we don't know whether this node will become the parent of other
58
- # nodes down the track.
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 ()))
96
+ self .tree_path = self .__next_tree_path_qx ()
62
97
63
98
# If we haven't changed the parent, save as normal.
64
- if self . __old_tree_path is None :
99
+ if old_tree_path is None :
65
100
rv = super ().save (* args , ** kwargs )
66
101
67
102
# If we have, use a transaction to avoid other contexts seeing the intermediate
@@ -71,18 +106,21 @@ def save(self, *args, **kwargs): # pylint: disable=arguments-differ
71
106
rv = super ().save (* args , ** kwargs )
72
107
# Move all of our descendants along with us, by substituting our old ltree
73
108
# prefix with our new one, in every descendant that has that prefix.
109
+ self .refresh_from_db (fields = ('tree_path' ,))
110
+ tree_path_needs_refresh = False
74
111
self .__class__ .objects .filter (
75
- tree_path__descendant_of = self . __old_tree_path
112
+ tree_path__descendant_of = old_tree_path
76
113
).update (
77
114
tree_path = LtreeConcat (
78
115
models .Value ('.' .join (self .tree_path )),
79
- Subpath (models .F ('tree_path' ), len (self . __old_tree_path )),
116
+ Subpath (models .F ('tree_path' ), len (old_tree_path )),
80
117
)
81
118
)
82
119
83
120
if tree_path_needs_refresh :
84
121
self .refresh_from_db (fields = ('tree_path' ,))
85
122
123
+ print ('for object {!r}, old_tree_path is {!r}, tree_path is {!r}' .format (self , old_tree_path , self .tree_path ))
86
124
return rv
87
125
88
126
@property
0 commit comments