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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +111 -38
  4. data/bin/rails +15 -0
  5. data/bin/rake +7 -7
  6. data/closure_tree.gemspec +11 -17
  7. data/lib/closure_tree/active_record_support.rb +4 -1
  8. data/lib/closure_tree/adapter_support.rb +11 -0
  9. data/lib/closure_tree/arel_helpers.rb +83 -0
  10. data/lib/closure_tree/configuration.rb +2 -0
  11. data/lib/closure_tree/deterministic_ordering.rb +2 -0
  12. data/lib/closure_tree/digraphs.rb +6 -4
  13. data/lib/closure_tree/finders.rb +103 -54
  14. data/lib/closure_tree/has_closure_tree.rb +5 -2
  15. data/lib/closure_tree/has_closure_tree_root.rb +12 -17
  16. data/lib/closure_tree/hash_tree.rb +2 -1
  17. data/lib/closure_tree/hash_tree_support.rb +38 -13
  18. data/lib/closure_tree/hierarchy_maintenance.rb +19 -26
  19. data/lib/closure_tree/model.rb +29 -29
  20. data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -55
  21. data/lib/closure_tree/numeric_order_support.rb +20 -18
  22. data/lib/closure_tree/support.rb +29 -32
  23. data/lib/closure_tree/support_attributes.rb +31 -5
  24. data/lib/closure_tree/support_flags.rb +2 -12
  25. data/lib/closure_tree/test/matcher.rb +10 -12
  26. data/lib/closure_tree/version.rb +3 -1
  27. data/lib/closure_tree.rb +22 -2
  28. data/lib/generators/closure_tree/config_generator.rb +3 -1
  29. data/lib/generators/closure_tree/migration_generator.rb +6 -4
  30. data/lib/generators/closure_tree/templates/config.rb +2 -0
  31. metadata +12 -104
  32. data/.github/workflows/ci.yml +0 -72
  33. data/.github/workflows/ci_jruby.yml +0 -68
  34. data/.github/workflows/ci_truffleruby.yml +0 -71
  35. data/.github/workflows/release.yml +0 -17
  36. data/.gitignore +0 -17
  37. data/.release-please-manifest.json +0 -1
  38. data/.rspec +0 -1
  39. data/.tool-versions +0 -1
  40. data/.yardopts +0 -3
  41. data/Appraisals +0 -61
  42. data/Gemfile +0 -6
  43. data/Rakefile +0 -32
  44. data/bin/appraisal +0 -29
  45. data/bin/rspec +0 -29
  46. data/mktree.rb +0 -38
  47. data/release-please-config.json +0 -4
  48. data/test/closure_tree/cache_invalidation_test.rb +0 -36
  49. data/test/closure_tree/cuisine_type_test.rb +0 -42
  50. data/test/closure_tree/generator_test.rb +0 -49
  51. data/test/closure_tree/has_closure_tree_root_test.rb +0 -80
  52. data/test/closure_tree/hierarchy_maintenance_test.rb +0 -56
  53. data/test/closure_tree/label_test.rb +0 -674
  54. data/test/closure_tree/metal_test.rb +0 -59
  55. data/test/closure_tree/model_test.rb +0 -9
  56. data/test/closure_tree/namespace_type_test.rb +0 -13
  57. data/test/closure_tree/parallel_test.rb +0 -162
  58. data/test/closure_tree/pool_test.rb +0 -33
  59. data/test/closure_tree/support_test.rb +0 -18
  60. data/test/closure_tree/tag_test.rb +0 -8
  61. data/test/closure_tree/user_test.rb +0 -175
  62. data/test/closure_tree/uuid_tag_test.rb +0 -8
  63. data/test/support/query_counter.rb +0 -25
  64. data/test/support/tag_examples.rb +0 -923
  65. data/test/test_helper.rb +0 -99
@@ -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 = self.children.where(attrs).first || begin
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
- self.children << ea
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
- s = _ct.base_class.joins(<<-SQL.squish)
38
- INNER JOIN (
39
- SELECT descendant_id
40
- FROM #{_ct.quoted_hierarchy_table_name}
41
- WHERE ancestor_id = #{_ct.quote(self.id)}
42
- GROUP BY descendant_id
43
- HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
44
- ) #{ _ct.t_alias_keyword } descendants ON (#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} = descendants.descendant_id)
45
- SQL
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
- s = joins(<<-SQL.squish)
74
- INNER JOIN (
75
- SELECT ancestor_id
76
- FROM #{_ct.quoted_hierarchy_table_name}
77
- GROUP BY ancestor_id
78
- HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = 0
79
- ) #{ _ct.t_alias_keyword } leaves ON (#{_ct.quoted_table_name}.#{primary_key} = leaves.ancestor_id)
80
- SQL
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? ? all : joins(:ancestor_hierarchies).
87
- where("#{_ct.hierarchy_table_name}.ancestor_id" => ancestor_ids).
88
- where("#{_ct.hierarchy_table_name}.generations > 0").
89
- readonly(false)
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? ? all : joins(:descendant_hierarchies).
96
- where("#{_ct.hierarchy_table_name}.descendant_id" => descendant_ids).
97
- where("#{_ct.hierarchy_table_name}.generations > 0").
98
- readonly(false)
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
- .where(descendant_id: descendants)
106
- .group(:ancestor_id)
107
- .having("COUNT(ancestor_id) = #{descendants.count}")
108
- .order(Arel.sql('MIN(generations) ASC'))
109
- .limit(1)
110
- .pluck(:ancestor_id).first
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
- s = joins(<<-SQL.squish)
117
- INNER JOIN (
118
- SELECT #{primary_key} as root_id
119
- FROM #{_ct.quoted_table_name}
120
- WHERE #{_ct.quoted_parent_column_name} IS NULL
121
- ) #{ _ct.t_alias_keyword } roots ON (1 = 1)
122
- INNER JOIN (
123
- SELECT ancestor_id, descendant_id
124
- FROM #{_ct.quoted_hierarchy_table_name}
125
- GROUP BY ancestor_id, descendant_id
126
- HAVING MAX(generations) = #{generation_level.to_i}
127
- ) #{ _ct.t_alias_keyword } descendants ON (
128
- #{_ct.quoted_table_name}.#{primary_key} = descendants.descendant_id
129
- AND roots.root_id = descendants.ancestor_id
130
- )
131
- SQL
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
- return _ct.find_by_large_path(path, attributes, parent_id)
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} #{ _ct.t_alias_keyword } #{next_joined_table}
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
- alias_method :acts_as_tree, :has_closure_tree
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
- options[:class_name] ||= assoc_name.to_s.sub(/\Aroot_/, "").classify
9
- options[:foreign_key] ||= self.name.underscore << "_id"
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 && (args.first == true || args.first == false)
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
- unless reload
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
- if inverse
72
- inverse_assoc = node.association(inverse.name)
73
- inverse_assoc.loaded!
74
- inverse_assoc.target = self
75
- end
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
- # Deepest generation, within limit, for each descendant
5
- # NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
6
- having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
7
- generation_depth = <<-SQL.squish
8
- INNER JOIN (
9
- SELECT descendant_id, MAX(generations) as depth
10
- FROM #{quoted_hierarchy_table_name}
11
- GROUP BY descendant_id
12
- #{having_clause}
13
- ) #{ t_alias_keyword } generation_depth
14
- ON #{quoted_table_name}.#{model_class.primary_key} = generation_depth.descendant_id
15
- SQL
16
- scope_with_order(scope.joins(generation_depth), 'generation_depth.depth')
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
- !new_record? && # don't validate for cycles if we're a new record
25
- changes[_ct.parent_column_name] && # don't validate for cycles if we didn't change our parent
26
- parent.present? && # don't validate if we're root
27
- parent.self_and_ancestors.include?(self) # < this is expensive :\
28
- errors.add(_ct.parent_column_sym, I18n.t('closure_tree.loop_error', default: 'You cannot add an ancestor as a descendant'))
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 public_send(:saved_changes)[_ct.parent_column_name] || @was_new_record
39
- rebuild!
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!(:ancestor => self, :descendant => self, :generations => 0)
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 if !called_by_rebuild
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
- _ct.connection.execute <<-SQL.squish
95
- DELETE FROM #{_ct.quoted_hierarchy_table_name}
96
- WHERE descendant_id IN (
97
- SELECT DISTINCT descendant_id
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
- [:descendant_id, :ancestor_id].each do |foreign_key|
122
- alias_name = foreign_key.to_s.split('_').first + "s"
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]))
@@ -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
- class_name: _ct.model_class.to_s,
11
- foreign_key: _ct.parent_column_name,
12
- inverse_of: :children,
13
- touch: _ct.options[:touch],
14
- optional: true
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
- 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)
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
- class_name: _ct.hierarchy_class_name,
33
- foreign_key: 'descendant_id'
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
- through: :ancestor_hierarchies,
37
- source: :ancestor
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
- class_name: _ct.hierarchy_class_name,
41
- foreign_key: 'ancestor_id'
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
- through: :descendant_hierarchies,
45
- source: :descendant
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
- alias_method :level, :depth
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
- self.ancestors.include? node
150
+ ancestors.include? node
151
151
  end
152
152
 
153
153
  # node is record's parent
154
154
  def child_of?(node)
155
- self.parent == node
155
+ parent == node
156
156
  end
157
157
 
158
158
  # node and record have a same root
159
159
  def family_of?(node)
160
- self.root == node.root
160
+ root == node.root
161
161
  end
162
162
 
163
163
  # Alias for appending to the children collection.