closure_tree 8.0.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +111 -38
- data/bin/rails +15 -0
- data/bin/rake +7 -7
- data/closure_tree.gemspec +11 -17
- data/lib/closure_tree/active_record_support.rb +4 -1
- data/lib/closure_tree/adapter_support.rb +11 -0
- data/lib/closure_tree/arel_helpers.rb +83 -0
- data/lib/closure_tree/configuration.rb +2 -0
- data/lib/closure_tree/deterministic_ordering.rb +2 -0
- data/lib/closure_tree/digraphs.rb +6 -4
- data/lib/closure_tree/finders.rb +103 -54
- data/lib/closure_tree/has_closure_tree.rb +5 -2
- data/lib/closure_tree/has_closure_tree_root.rb +12 -17
- data/lib/closure_tree/hash_tree.rb +2 -1
- data/lib/closure_tree/hash_tree_support.rb +38 -13
- data/lib/closure_tree/hierarchy_maintenance.rb +19 -26
- data/lib/closure_tree/model.rb +29 -29
- data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -55
- data/lib/closure_tree/numeric_order_support.rb +20 -18
- data/lib/closure_tree/support.rb +29 -32
- data/lib/closure_tree/support_attributes.rb +31 -5
- data/lib/closure_tree/support_flags.rb +2 -12
- data/lib/closure_tree/test/matcher.rb +10 -12
- data/lib/closure_tree/version.rb +3 -1
- data/lib/closure_tree.rb +22 -2
- data/lib/generators/closure_tree/config_generator.rb +3 -1
- data/lib/generators/closure_tree/migration_generator.rb +6 -4
- data/lib/generators/closure_tree/templates/config.rb +2 -0
- metadata +12 -104
- data/.github/workflows/ci.yml +0 -72
- data/.github/workflows/ci_jruby.yml +0 -68
- data/.github/workflows/ci_truffleruby.yml +0 -71
- data/.github/workflows/release.yml +0 -17
- data/.gitignore +0 -17
- data/.release-please-manifest.json +0 -1
- data/.rspec +0 -1
- data/.tool-versions +0 -1
- data/.yardopts +0 -3
- data/Appraisals +0 -61
- data/Gemfile +0 -6
- data/Rakefile +0 -32
- data/bin/appraisal +0 -29
- data/bin/rspec +0 -29
- data/mktree.rb +0 -38
- data/release-please-config.json +0 -4
- data/test/closure_tree/cache_invalidation_test.rb +0 -36
- data/test/closure_tree/cuisine_type_test.rb +0 -42
- data/test/closure_tree/generator_test.rb +0 -49
- data/test/closure_tree/has_closure_tree_root_test.rb +0 -80
- data/test/closure_tree/hierarchy_maintenance_test.rb +0 -56
- data/test/closure_tree/label_test.rb +0 -674
- data/test/closure_tree/metal_test.rb +0 -59
- data/test/closure_tree/model_test.rb +0 -9
- data/test/closure_tree/namespace_type_test.rb +0 -13
- data/test/closure_tree/parallel_test.rb +0 -162
- data/test/closure_tree/pool_test.rb +0 -33
- data/test/closure_tree/support_test.rb +0 -18
- data/test/closure_tree/tag_test.rb +0 -8
- data/test/closure_tree/user_test.rb +0 -175
- data/test/closure_tree/uuid_tag_test.rb +0 -8
- data/test/support/query_counter.rb +0 -25
- data/test/support/tag_examples.rb +0 -923
- data/test/test_helper.rb +0 -99
data/lib/closure_tree/finders.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module Finders
|
3
5
|
extend ActiveSupport::Concern
|
@@ -5,6 +7,7 @@ module ClosureTree
|
|
5
7
|
# Find a descendant node whose +ancestry_path+ will be ```self.ancestry_path + path```
|
6
8
|
def find_by_path(path, attributes = {})
|
7
9
|
return self if path.empty?
|
10
|
+
|
8
11
|
self.class.find_by_path(path, attributes, id)
|
9
12
|
end
|
10
13
|
|
@@ -20,13 +23,13 @@ module ClosureTree
|
|
20
23
|
_ct.with_advisory_lock do
|
21
24
|
# shenanigans because children.create is bound to the superclass
|
22
25
|
# (in the case of polymorphism):
|
23
|
-
child =
|
26
|
+
child = children.where(attrs).first || begin
|
24
27
|
# Support STI creation by using base_class:
|
25
28
|
_ct.create(self.class, attrs).tap do |ea|
|
26
29
|
# We know that there isn't a cycle, because we just created it, and
|
27
30
|
# cycle detection is expensive when the node is deep.
|
28
31
|
ea._ct_skip_cycle_detection!
|
29
|
-
|
32
|
+
children << ea
|
30
33
|
end
|
31
34
|
end
|
32
35
|
child.find_or_create_by_path(subpath, attributes)
|
@@ -34,15 +37,24 @@ module ClosureTree
|
|
34
37
|
end
|
35
38
|
|
36
39
|
def find_all_by_generation(generation_level)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
40
|
+
hierarchy_table = self.class.hierarchy_class.arel_table
|
41
|
+
model_table = self.class.arel_table
|
42
|
+
|
43
|
+
# Build the subquery
|
44
|
+
descendants_subquery = hierarchy_table
|
45
|
+
.project(hierarchy_table[:descendant_id])
|
46
|
+
.where(hierarchy_table[:ancestor_id].eq(id))
|
47
|
+
.group(hierarchy_table[:descendant_id])
|
48
|
+
.having(hierarchy_table[:generations].maximum.eq(generation_level.to_i))
|
49
|
+
.as('descendants')
|
50
|
+
|
51
|
+
# Build the join
|
52
|
+
join_source = model_table
|
53
|
+
.join(descendants_subquery)
|
54
|
+
.on(model_table[_ct.base_class.primary_key].eq(descendants_subquery[:descendant_id]))
|
55
|
+
.join_sources
|
56
|
+
|
57
|
+
s = _ct.base_class.joins(join_source)
|
46
58
|
_ct.scope_with_order(s)
|
47
59
|
end
|
48
60
|
|
@@ -51,7 +63,6 @@ module ClosureTree
|
|
51
63
|
end
|
52
64
|
|
53
65
|
class_methods do
|
54
|
-
|
55
66
|
def without_instance(instance)
|
56
67
|
if instance.new_record?
|
57
68
|
all
|
@@ -70,87 +81,125 @@ module ClosureTree
|
|
70
81
|
end
|
71
82
|
|
72
83
|
def leaves
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
84
|
+
hierarchy_table = hierarchy_class.arel_table
|
85
|
+
model_table = arel_table
|
86
|
+
|
87
|
+
# Build the subquery for leaves (nodes with no children)
|
88
|
+
leaves_subquery = hierarchy_table
|
89
|
+
.project(hierarchy_table[:ancestor_id])
|
90
|
+
.group(hierarchy_table[:ancestor_id])
|
91
|
+
.having(hierarchy_table[:generations].maximum.eq(0))
|
92
|
+
.as('leaves')
|
93
|
+
|
94
|
+
# Build the join
|
95
|
+
join_source = model_table
|
96
|
+
.join(leaves_subquery)
|
97
|
+
.on(model_table[primary_key].eq(leaves_subquery[:ancestor_id]))
|
98
|
+
.join_sources
|
99
|
+
|
100
|
+
s = joins(join_source)
|
81
101
|
_ct.scope_with_order(s.readonly(false))
|
82
102
|
end
|
83
103
|
|
84
104
|
def with_ancestor(*ancestors)
|
85
105
|
ancestor_ids = ancestors.map { |ea| ea.is_a?(ActiveRecord::Base) ? ea._ct_id : ea }
|
86
|
-
scope = ancestor_ids.blank?
|
87
|
-
|
88
|
-
|
89
|
-
|
106
|
+
scope = if ancestor_ids.blank?
|
107
|
+
all
|
108
|
+
else
|
109
|
+
joins(:ancestor_hierarchies)
|
110
|
+
.where("#{_ct.hierarchy_table_name}.ancestor_id" => ancestor_ids)
|
111
|
+
.where("#{_ct.hierarchy_table_name}.generations > 0")
|
112
|
+
.readonly(false)
|
113
|
+
end
|
90
114
|
_ct.scope_with_order(scope)
|
91
115
|
end
|
92
116
|
|
93
117
|
def with_descendant(*descendants)
|
94
118
|
descendant_ids = descendants.map { |ea| ea.is_a?(ActiveRecord::Base) ? ea._ct_id : ea }
|
95
|
-
scope = descendant_ids.blank?
|
96
|
-
|
97
|
-
|
98
|
-
|
119
|
+
scope = if descendant_ids.blank?
|
120
|
+
all
|
121
|
+
else
|
122
|
+
joins(:descendant_hierarchies)
|
123
|
+
.where("#{_ct.hierarchy_table_name}.descendant_id" => descendant_ids)
|
124
|
+
.where("#{_ct.hierarchy_table_name}.generations > 0")
|
125
|
+
.readonly(false)
|
126
|
+
end
|
99
127
|
_ct.scope_with_order(scope)
|
100
128
|
end
|
101
129
|
|
102
130
|
def lowest_common_ancestor(*descendants)
|
103
131
|
descendants = descendants.first if descendants.length == 1 && descendants.first.respond_to?(:each)
|
104
132
|
ancestor_id = hierarchy_class
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
133
|
+
.where(descendant_id: descendants)
|
134
|
+
.group(:ancestor_id)
|
135
|
+
.having("COUNT(ancestor_id) = #{descendants.count}")
|
136
|
+
.order(Arel.sql('MIN(generations) ASC'))
|
137
|
+
.limit(1)
|
138
|
+
.pluck(:ancestor_id).first
|
111
139
|
|
112
140
|
find_by(primary_key => ancestor_id) if ancestor_id
|
113
141
|
end
|
114
142
|
|
115
143
|
def find_all_by_generation(generation_level)
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
144
|
+
hierarchy_table = hierarchy_class.arel_table
|
145
|
+
model_table = arel_table
|
146
|
+
|
147
|
+
# Build the roots subquery
|
148
|
+
roots_subquery = model_table
|
149
|
+
.project(model_table[primary_key].as('root_id'))
|
150
|
+
.where(model_table[_ct.parent_column_sym].eq(nil))
|
151
|
+
.as('roots')
|
152
|
+
|
153
|
+
# Build the descendants subquery
|
154
|
+
descendants_subquery = hierarchy_table
|
155
|
+
.project(
|
156
|
+
hierarchy_table[:ancestor_id],
|
157
|
+
hierarchy_table[:descendant_id]
|
158
|
+
)
|
159
|
+
.group(hierarchy_table[:ancestor_id], hierarchy_table[:descendant_id])
|
160
|
+
.having(hierarchy_table[:generations].maximum.eq(generation_level.to_i))
|
161
|
+
.as('descendants')
|
162
|
+
|
163
|
+
# Build the joins
|
164
|
+
# Note: We intentionally use a cartesian product join (CROSS JOIN) here.
|
165
|
+
# This allows us to find all nodes at a specific generation level across all root nodes.
|
166
|
+
# The 1=1 condition creates this cartesian product in a database-agnostic way.
|
167
|
+
join_roots = model_table
|
168
|
+
.join(roots_subquery)
|
169
|
+
.on(Arel.sql('1 = 1'))
|
170
|
+
|
171
|
+
join_descendants = join_roots
|
172
|
+
.join(descendants_subquery)
|
173
|
+
.on(
|
174
|
+
model_table[primary_key].eq(descendants_subquery[:descendant_id])
|
175
|
+
.and(roots_subquery[:root_id].eq(descendants_subquery[:ancestor_id]))
|
176
|
+
)
|
177
|
+
|
178
|
+
s = joins(join_descendants.join_sources)
|
132
179
|
_ct.scope_with_order(s)
|
133
180
|
end
|
134
181
|
|
135
182
|
# Find the node whose +ancestry_path+ is +path+
|
136
183
|
def find_by_path(path, attributes = {}, parent_id = nil)
|
137
184
|
return nil if path.blank?
|
185
|
+
|
138
186
|
path = _ct.build_ancestry_attr_path(path, attributes)
|
139
|
-
if path.size > _ct.max_join_tables
|
140
|
-
|
141
|
-
end
|
187
|
+
return _ct.find_by_large_path(path, attributes, parent_id) if path.size > _ct.max_join_tables
|
188
|
+
|
142
189
|
scope = where(path.pop)
|
143
190
|
last_joined_table = _ct.table_name
|
191
|
+
|
144
192
|
path.reverse.each_with_index do |ea, idx|
|
145
193
|
next_joined_table = "p#{idx}"
|
146
194
|
scope = scope.joins(<<-SQL.squish)
|
147
|
-
INNER JOIN #{_ct.quoted_table_name} #{
|
195
|
+
INNER JOIN #{_ct.quoted_table_name} #{_ct.t_alias_keyword} #{next_joined_table}
|
148
196
|
ON #{next_joined_table}.#{_ct.quoted_id_column_name} =
|
149
197
|
#{connection.quote_table_name(last_joined_table)}.#{_ct.quoted_parent_column_name}
|
150
198
|
SQL
|
151
199
|
scope = _ct.scoped_attributes(scope, ea, next_joined_table)
|
152
200
|
last_joined_table = next_joined_table
|
153
201
|
end
|
202
|
+
|
154
203
|
scope.where("#{last_joined_table}.#{_ct.parent_column_name}" => parent_id).readonly(false).first
|
155
204
|
end
|
156
205
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module HasClosureTree
|
3
5
|
def has_closure_tree(options = {})
|
@@ -11,7 +13,8 @@ module ClosureTree
|
|
11
13
|
:dont_order_roots,
|
12
14
|
:numeric_order,
|
13
15
|
:touch,
|
14
|
-
:with_advisory_lock
|
16
|
+
:with_advisory_lock,
|
17
|
+
:advisory_lock_name
|
15
18
|
)
|
16
19
|
|
17
20
|
class_attribute :_ct
|
@@ -37,6 +40,6 @@ module ClosureTree
|
|
37
40
|
raise e unless ClosureTree.configuration.database_less
|
38
41
|
end
|
39
42
|
|
40
|
-
|
43
|
+
alias acts_as_tree has_closure_tree
|
41
44
|
end
|
42
45
|
end
|
@@ -1,38 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
class MultipleRootError < StandardError; end
|
3
5
|
class RootOrderingDisabledError < StandardError; end
|
4
6
|
|
5
7
|
module HasClosureTreeRoot
|
6
|
-
|
7
8
|
def has_closure_tree_root(assoc_name, options = {})
|
8
|
-
|
9
|
-
options[:foreign_key] ||=
|
9
|
+
options[:class_name] ||= assoc_name.to_s.sub(/\Aroot_/, '').classify
|
10
|
+
options[:foreign_key] ||= name.underscore << '_id'
|
10
11
|
|
11
12
|
has_one assoc_name, -> { where(parent: nil) }, **options
|
12
13
|
|
13
14
|
# Fetches the association, eager loading all children and given associations
|
14
15
|
define_method("#{assoc_name}_including_tree") do |*args|
|
15
16
|
reload = false
|
16
|
-
reload = args.shift if args &&
|
17
|
+
reload = args.shift if args && [true, false].include?(args.first)
|
17
18
|
assoc_map = args
|
18
19
|
assoc_map = [nil] if assoc_map.blank?
|
19
20
|
|
20
21
|
# Memoize
|
21
22
|
@closure_tree_roots ||= {}
|
22
23
|
@closure_tree_roots[assoc_name] ||= {}
|
23
|
-
|
24
|
-
if @closure_tree_roots[assoc_name].has_key?(assoc_map)
|
25
|
-
return @closure_tree_roots[assoc_name][assoc_map]
|
26
|
-
end
|
27
|
-
end
|
24
|
+
return @closure_tree_roots[assoc_name][assoc_map] if !reload && @closure_tree_roots[assoc_name].key?(assoc_map)
|
28
25
|
|
29
26
|
roots = options[:class_name].constantize.where(parent: nil, options[:foreign_key] => id).to_a
|
30
27
|
|
31
28
|
return nil if roots.empty?
|
32
29
|
|
33
|
-
if roots.size > 1
|
34
|
-
raise MultipleRootError.new("#{self.class.name}: has_closure_tree_root requires a single root")
|
35
|
-
end
|
30
|
+
raise MultipleRootError, "#{self.class.name}: has_closure_tree_root requires a single root" if roots.size > 1
|
36
31
|
|
37
32
|
temp_root = roots.first
|
38
33
|
root = nil
|
@@ -68,11 +63,11 @@ module ClosureTree
|
|
68
63
|
end
|
69
64
|
|
70
65
|
# Pre-assign inverse association back to this class, if it exists on target class.
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
66
|
+
next unless inverse
|
67
|
+
|
68
|
+
inverse_assoc = node.association(inverse.name)
|
69
|
+
inverse_assoc.loaded!
|
70
|
+
inverse_assoc.target = self
|
76
71
|
end
|
77
72
|
|
78
73
|
@closure_tree_roots[assoc_name][assoc_map] = root
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module HashTree
|
3
5
|
extend ActiveSupport::Concern
|
@@ -7,7 +9,6 @@ module ClosureTree
|
|
7
9
|
end
|
8
10
|
|
9
11
|
class_methods do
|
10
|
-
|
11
12
|
# There is no default depth limit. This might be crazy-big, depending
|
12
13
|
# on your tree shape. Hash huge trees at your own peril!
|
13
14
|
def hash_tree(options = {})
|
@@ -1,19 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ClosureTree
|
2
4
|
module HashTreeSupport
|
3
5
|
def default_tree_scope(scope, limit_depth = nil)
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
6
|
+
# Deepest generation, within limit, for each descendant
|
7
|
+
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
|
8
|
+
|
9
|
+
# Get the hierarchy table for the scope's model class
|
10
|
+
hierarchy_table_arel = if scope.respond_to?(:hierarchy_class)
|
11
|
+
scope.hierarchy_class.arel_table
|
12
|
+
elsif scope.klass.respond_to?(:hierarchy_class)
|
13
|
+
scope.klass.hierarchy_class.arel_table
|
14
|
+
else
|
15
|
+
hierarchy_table
|
16
|
+
end
|
17
|
+
|
18
|
+
model_table_arel = scope.klass.arel_table
|
19
|
+
|
20
|
+
# Build the subquery using Arel
|
21
|
+
subquery = hierarchy_table_arel
|
22
|
+
.project(
|
23
|
+
hierarchy_table_arel[:descendant_id],
|
24
|
+
hierarchy_table_arel[:generations].maximum.as('depth')
|
25
|
+
)
|
26
|
+
.group(hierarchy_table_arel[:descendant_id])
|
27
|
+
|
28
|
+
# Add HAVING clause if limit_depth is specified
|
29
|
+
subquery = subquery.having(hierarchy_table_arel[:generations].maximum.lteq(limit_depth - 1)) if limit_depth
|
30
|
+
|
31
|
+
generation_depth_alias = subquery.as('generation_depth')
|
32
|
+
|
33
|
+
# Build the join
|
34
|
+
join_condition = model_table_arel[scope.klass.primary_key].eq(generation_depth_alias[:descendant_id])
|
35
|
+
|
36
|
+
join_source = model_table_arel
|
37
|
+
.join(generation_depth_alias)
|
38
|
+
.on(join_condition)
|
39
|
+
.join_sources
|
40
|
+
|
41
|
+
scope_with_order(scope.joins(join_source), 'generation_depth.depth')
|
17
42
|
end
|
18
43
|
|
19
44
|
def hash_tree(tree_scope, limit_depth = nil)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/concern'
|
2
4
|
|
3
5
|
module ClosureTree
|
@@ -21,11 +23,12 @@ module ClosureTree
|
|
21
23
|
|
22
24
|
def _ct_validate
|
23
25
|
if !(defined? @_ct_skip_cycle_detection) &&
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
errors.add(_ct.parent_column_sym,
|
26
|
+
!new_record? && # don't validate for cycles if we're a new record
|
27
|
+
changes[_ct.parent_column_name] && # don't validate for cycles if we didn't change our parent
|
28
|
+
parent.present? && # don't validate if we're root
|
29
|
+
parent.self_and_ancestors.include?(self) # < this is expensive :\
|
30
|
+
errors.add(_ct.parent_column_sym,
|
31
|
+
I18n.t('closure_tree.loop_error', default: 'You cannot add an ancestor as a descendant'))
|
29
32
|
end
|
30
33
|
end
|
31
34
|
|
@@ -35,10 +38,8 @@ module ClosureTree
|
|
35
38
|
end
|
36
39
|
|
37
40
|
def _ct_after_save
|
38
|
-
if
|
39
|
-
|
40
|
-
end
|
41
|
-
if public_send(:saved_changes)[_ct.parent_column_name] && !@was_new_record
|
41
|
+
rebuild! if saved_changes[_ct.parent_column_name] || @was_new_record
|
42
|
+
if saved_changes[_ct.parent_column_name] && !@was_new_record
|
42
43
|
# Resetting the ancestral collections addresses
|
43
44
|
# https://github.com/mceachen/closure_tree/issues/68
|
44
45
|
ancestor_hierarchies.reload
|
@@ -52,9 +53,7 @@ module ClosureTree
|
|
52
53
|
def _ct_before_destroy
|
53
54
|
_ct.with_advisory_lock do
|
54
55
|
delete_hierarchy_references
|
55
|
-
if _ct.options[:dependent] == :nullify
|
56
|
-
self.class.find(self.id).children.find_each { |c| c.rebuild! }
|
57
|
-
end
|
56
|
+
self.class.find(id).children.find_each(&:rebuild!) if _ct.options[:dependent] == :nullify
|
58
57
|
end
|
59
58
|
true # don't prevent destruction
|
60
59
|
end
|
@@ -62,7 +61,7 @@ module ClosureTree
|
|
62
61
|
def rebuild!(called_by_rebuild = false)
|
63
62
|
_ct.with_advisory_lock do
|
64
63
|
delete_hierarchy_references unless (defined? @was_new_record) && @was_new_record
|
65
|
-
hierarchy_class.create!(:
|
64
|
+
hierarchy_class.create!(ancestor: self, descendant: self, generations: 0)
|
66
65
|
unless root?
|
67
66
|
_ct.connection.execute <<-SQL.squish
|
68
67
|
INSERT INTO #{_ct.quoted_hierarchy_table_name}
|
@@ -76,7 +75,7 @@ module ClosureTree
|
|
76
75
|
if _ct.order_is_numeric? && !@_ct_skip_sort_order_maintenance
|
77
76
|
_ct_reorder_prior_siblings_if_parent_changed
|
78
77
|
# Prevent double-reordering of siblings:
|
79
|
-
_ct_reorder_siblings
|
78
|
+
_ct_reorder_siblings unless called_by_rebuild
|
80
79
|
end
|
81
80
|
|
82
81
|
children.find_each { |c| c.rebuild!(true) }
|
@@ -91,16 +90,10 @@ module ClosureTree
|
|
91
90
|
# It shouldn't affect performance of postgresql.
|
92
91
|
# See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
|
93
92
|
# Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
FROM (SELECT descendant_id
|
99
|
-
FROM #{_ct.quoted_hierarchy_table_name}
|
100
|
-
WHERE ancestor_id = #{_ct.quote(id)}
|
101
|
-
OR descendant_id = #{_ct.quote(id)}
|
102
|
-
) #{ _ct.t_alias_keyword } x )
|
103
|
-
SQL
|
93
|
+
|
94
|
+
hierarchy_table = hierarchy_class.arel_table
|
95
|
+
delete_query = _ct.build_hierarchy_delete_query(hierarchy_table, id)
|
96
|
+
_ct.connection.execute(delete_query.to_sql)
|
104
97
|
end
|
105
98
|
end
|
106
99
|
|
@@ -118,8 +111,8 @@ module ClosureTree
|
|
118
111
|
def cleanup!
|
119
112
|
hierarchy_table = hierarchy_class.arel_table
|
120
113
|
|
121
|
-
[
|
122
|
-
alias_name = foreign_key.to_s.split('_').first
|
114
|
+
%i[descendant_id ancestor_id].each do |foreign_key|
|
115
|
+
alias_name = "#{foreign_key.to_s.split('_').first}s"
|
123
116
|
alias_table = Arel::Table.new(table_name).alias(alias_name)
|
124
117
|
arel_join = hierarchy_table.join(alias_table, Arel::Nodes::OuterJoin)
|
125
118
|
.on(alias_table[primary_key].eq(hierarchy_table[foreign_key]))
|
data/lib/closure_tree/model.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/concern'
|
2
4
|
|
3
5
|
module ClosureTree
|
@@ -5,44 +7,42 @@ module ClosureTree
|
|
5
7
|
extend ActiveSupport::Concern
|
6
8
|
|
7
9
|
included do
|
8
|
-
|
9
10
|
belongs_to :parent, nil,
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
class_name: _ct.model_class.to_s,
|
12
|
+
foreign_key: _ct.parent_column_name,
|
13
|
+
inverse_of: :children,
|
14
|
+
touch: _ct.options[:touch],
|
15
|
+
optional: true
|
15
16
|
|
16
17
|
order_by_generations = -> { Arel.sql("#{_ct.quoted_hierarchy_table_name}.generations ASC") }
|
17
18
|
|
18
|
-
has_many :children, *_ct.has_many_order_with_option,
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
_ct.hash_tree(@association.owner.descendants, limit_depth ? limit_depth + 1 : nil)
|
28
|
-
end
|
19
|
+
has_many :children, *_ct.has_many_order_with_option, class_name: _ct.model_class.to_s,
|
20
|
+
foreign_key: _ct.parent_column_name,
|
21
|
+
dependent: _ct.options[:dependent],
|
22
|
+
inverse_of: :parent do
|
23
|
+
# We have to redefine hash_tree because the activerecord relation is already scoped to parent_id.
|
24
|
+
def hash_tree(options = {})
|
25
|
+
# we want limit_depth + 1 because we don't do self_and_descendants.
|
26
|
+
limit_depth = options[:limit_depth]
|
27
|
+
_ct.hash_tree(@association.owner.descendants, limit_depth ? limit_depth + 1 : nil)
|
29
28
|
end
|
29
|
+
end
|
30
30
|
|
31
31
|
has_many :ancestor_hierarchies, *_ct.has_many_order_without_option(order_by_generations),
|
32
|
-
|
33
|
-
|
32
|
+
class_name: _ct.hierarchy_class_name,
|
33
|
+
foreign_key: 'descendant_id'
|
34
34
|
|
35
35
|
has_many :self_and_ancestors, *_ct.has_many_order_without_option(order_by_generations),
|
36
|
-
|
37
|
-
|
36
|
+
through: :ancestor_hierarchies,
|
37
|
+
source: :ancestor
|
38
38
|
|
39
39
|
has_many :descendant_hierarchies, *_ct.has_many_order_without_option(order_by_generations),
|
40
|
-
|
41
|
-
|
40
|
+
class_name: _ct.hierarchy_class_name,
|
41
|
+
foreign_key: 'ancestor_id'
|
42
42
|
|
43
43
|
has_many :self_and_descendants, *_ct.has_many_order_with_option(order_by_generations),
|
44
|
-
|
45
|
-
|
44
|
+
through: :descendant_hierarchies,
|
45
|
+
source: :descendant
|
46
46
|
end
|
47
47
|
|
48
48
|
# Delegate to the Support instance on the class:
|
@@ -80,7 +80,7 @@ module ClosureTree
|
|
80
80
|
ancestor_hierarchies.size - 1
|
81
81
|
end
|
82
82
|
|
83
|
-
|
83
|
+
alias level depth
|
84
84
|
|
85
85
|
# enumerable of ancestors, immediate parent is first, root is last.
|
86
86
|
def ancestors
|
@@ -147,17 +147,17 @@ module ClosureTree
|
|
147
147
|
|
148
148
|
# node is record's ancestor
|
149
149
|
def descendant_of?(node)
|
150
|
-
|
150
|
+
ancestors.include? node
|
151
151
|
end
|
152
152
|
|
153
153
|
# node is record's parent
|
154
154
|
def child_of?(node)
|
155
|
-
|
155
|
+
parent == node
|
156
156
|
end
|
157
157
|
|
158
158
|
# node and record have a same root
|
159
159
|
def family_of?(node)
|
160
|
-
|
160
|
+
root == node.root
|
161
161
|
end
|
162
162
|
|
163
163
|
# Alias for appending to the children collection.
|