-
Notifications
You must be signed in to change notification settings - Fork 239
/
numeric_deterministic_ordering.rb
154 lines (128 loc) · 5.23 KB
/
numeric_deterministic_ordering.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
require 'active_support/concern'
# This module is only included if the order column is an integer.
module ClosureTree
module NumericDeterministicOrdering
extend ActiveSupport::Concern
included do
after_destroy :_ct_reorder_siblings
end
def _ct_reorder_prior_siblings_if_parent_changed
if public_send(:saved_change_to_attribute?, _ct.parent_column_name) && !@was_new_record
was_parent_id = public_send(:attribute_before_last_save, _ct.parent_column_name)
_ct.reorder_with_parent_id(was_parent_id)
end
end
def _ct_reorder_siblings(minimum_sort_order_value = nil)
_ct.reorder_with_parent_id(_ct_parent_id, minimum_sort_order_value)
reload unless destroyed?
end
def _ct_reorder_children(minimum_sort_order_value = nil)
_ct.reorder_with_parent_id(_ct_id, minimum_sort_order_value)
end
def self_and_descendants_preordered
# TODO: raise NotImplementedError if sort_order is not numeric and not null?
join_sql = <<-SQL
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
ON anc_hier.descendant_id = #{_ct.quoted_hierarchy_table_name}.descendant_id
JOIN #{_ct.quoted_table_name} anc
ON anc.#{_ct.quoted_id_column_name} = anc_hier.ancestor_id
JOIN #{_ct.quoted_hierarchy_table_name} depths
ON depths.ancestor_id = #{_ct.quote(self.id)} AND depths.descendant_id = anc.#{_ct.quoted_id_column_name}
SQL
self_and_descendants
.joins(join_sql)
.group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
.reorder(self.class._ct_sum_order_by(self))
end
class_methods do
# If node is nil, order the whole tree.
def _ct_sum_order_by(node = nil)
stats_sql = <<-SQL.squish
SELECT
count(*) as total_descendants,
max(generations) as max_depth
FROM #{_ct.quoted_hierarchy_table_name}
SQL
stats_sql += " WHERE ancestor_id = #{_ct.quote(node.id)}" if node
h = _ct.connection.select_one(stats_sql)
depth_column = node ? 'depths.generations' : 'depths.max_depth'
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
"power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
# We want the NULLs to be first in case we are not ordering roots and they have NULL order.
Arel.sql("SUM(#{node_score}) IS NULL DESC, SUM(#{node_score})")
end
def roots_and_descendants_preordered
if _ct.dont_order_roots
raise ClosureTree::RootOrderingDisabledError.new("Root ordering is disabled on this model")
end
join_sql = <<-SQL.squish
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
ON anc_hier.descendant_id = #{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}
JOIN #{_ct.quoted_table_name} anc
ON anc.#{_ct.quoted_id_column_name} = anc_hier.ancestor_id
JOIN (
SELECT descendant_id, max(generations) AS max_depth
FROM #{_ct.quoted_hierarchy_table_name}
GROUP BY descendant_id
) #{ _ct.t_alias_keyword } depths ON depths.descendant_id = anc.#{_ct.quoted_id_column_name}
SQL
joins(join_sql)
.group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
.reorder(_ct_sum_order_by)
end
end
def append_child(child_node)
add_child(child_node)
end
def prepend_child(child_node)
child_node.order_value = -1
child_node.parent = self
child_node._ct_skip_sort_order_maintenance!
if child_node.save
_ct_reorder_children
child_node.reload
else
child_node
end
end
def append_sibling(sibling_node)
add_sibling(sibling_node, true)
end
def prepend_sibling(sibling_node)
add_sibling(sibling_node, false)
end
def add_sibling(sibling, add_after = true)
fail "can't add self as sibling" if self == sibling
if _ct.dont_order_roots && parent.nil?
raise ClosureTree::RootOrderingDisabledError.new("Root ordering is disabled on this model")
end
# Make sure self isn't dirty, because we're going to call reload:
save
_ct.with_advisory_lock do
prior_sibling_parent = sibling.parent
reorder_from_value = if prior_sibling_parent == self.parent
[self.order_value, sibling.order_value].compact.min
else
self.order_value
end
sibling.order_value = self.order_value
sibling.parent = self.parent
sibling._ct_skip_sort_order_maintenance!
sibling.save # may be a no-op
_ct_reorder_siblings(reorder_from_value)
# The sort order should be correct now except for self and sibling, which may need to flip:
sibling_is_after = self.reload.order_value < sibling.reload.order_value
if add_after != sibling_is_after
# We need to flip the sort orders:
self_ov, sib_ov = self.order_value, sibling.order_value
update_order_value(sib_ov)
sibling.update_order_value(self_ov)
end
if prior_sibling_parent != self.parent
prior_sibling_parent.try(:_ct_reorder_children)
end
sibling
end
end
end
end