closure_tree 8.0.0 → 9.1.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +111 -38
  4. data/bin/rails +15 -0
  5. data/bin/rake +7 -7
  6. data/closure_tree.gemspec +12 -17
  7. data/lib/closure_tree/active_record_support.rb +4 -1
  8. data/lib/closure_tree/arel_helpers.rb +83 -0
  9. data/lib/closure_tree/deterministic_ordering.rb +2 -0
  10. data/lib/closure_tree/digraphs.rb +6 -4
  11. data/lib/closure_tree/finders.rb +103 -54
  12. data/lib/closure_tree/has_closure_tree.rb +5 -4
  13. data/lib/closure_tree/has_closure_tree_root.rb +12 -17
  14. data/lib/closure_tree/hash_tree.rb +2 -1
  15. data/lib/closure_tree/hash_tree_support.rb +38 -13
  16. data/lib/closure_tree/hierarchy_maintenance.rb +19 -26
  17. data/lib/closure_tree/model.rb +29 -29
  18. data/lib/closure_tree/numeric_deterministic_ordering.rb +90 -55
  19. data/lib/closure_tree/numeric_order_support.rb +20 -18
  20. data/lib/closure_tree/support.rb +27 -37
  21. data/lib/closure_tree/support_attributes.rb +31 -5
  22. data/lib/closure_tree/support_flags.rb +2 -12
  23. data/lib/closure_tree/test/matcher.rb +10 -12
  24. data/lib/closure_tree/version.rb +3 -1
  25. data/lib/closure_tree.rb +14 -22
  26. data/lib/generators/closure_tree/migration_generator.rb +6 -4
  27. metadata +17 -99
  28. data/.github/workflows/ci.yml +0 -72
  29. data/.github/workflows/ci_jruby.yml +0 -68
  30. data/.github/workflows/ci_truffleruby.yml +0 -71
  31. data/.github/workflows/release.yml +0 -17
  32. data/.gitignore +0 -17
  33. data/.release-please-manifest.json +0 -1
  34. data/.rspec +0 -1
  35. data/.tool-versions +0 -1
  36. data/.yardopts +0 -3
  37. data/Appraisals +0 -61
  38. data/Gemfile +0 -6
  39. data/Rakefile +0 -32
  40. data/bin/appraisal +0 -29
  41. data/bin/rspec +0 -29
  42. data/lib/closure_tree/configuration.rb +0 -9
  43. data/lib/generators/closure_tree/config_generator.rb +0 -12
  44. data/lib/generators/closure_tree/templates/config.rb +0 -5
  45. data/mktree.rb +0 -38
  46. data/release-please-config.json +0 -4
  47. data/test/closure_tree/cache_invalidation_test.rb +0 -36
  48. data/test/closure_tree/cuisine_type_test.rb +0 -42
  49. data/test/closure_tree/generator_test.rb +0 -49
  50. data/test/closure_tree/has_closure_tree_root_test.rb +0 -80
  51. data/test/closure_tree/hierarchy_maintenance_test.rb +0 -56
  52. data/test/closure_tree/label_test.rb +0 -674
  53. data/test/closure_tree/metal_test.rb +0 -59
  54. data/test/closure_tree/model_test.rb +0 -9
  55. data/test/closure_tree/namespace_type_test.rb +0 -13
  56. data/test/closure_tree/parallel_test.rb +0 -162
  57. data/test/closure_tree/pool_test.rb +0 -33
  58. data/test/closure_tree/support_test.rb +0 -18
  59. data/test/closure_tree/tag_test.rb +0 -8
  60. data/test/closure_tree/user_test.rb +0 -175
  61. data/test/closure_tree/uuid_tag_test.rb +0 -8
  62. data/test/support/query_counter.rb +0 -25
  63. data/test/support/tag_examples.rb +0 -923
  64. data/test/test_helper.rb +0 -99
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/concern'
2
4
 
3
5
  # This module is only included if the order column is an integer.
@@ -10,10 +12,10 @@ module ClosureTree
10
12
  end
11
13
 
12
14
  def _ct_reorder_prior_siblings_if_parent_changed
13
- if public_send(:saved_change_to_attribute?, _ct.parent_column_name) && !@was_new_record
14
- was_parent_id = public_send(:attribute_before_last_save, _ct.parent_column_name)
15
- _ct.reorder_with_parent_id(was_parent_id)
16
- end
15
+ return unless saved_change_to_attribute?(_ct.parent_column_name) && !@was_new_record
16
+
17
+ was_parent_id = attribute_before_last_save(_ct.parent_column_name)
18
+ _ct.reorder_with_parent_id(was_parent_id)
17
19
  end
18
20
 
19
21
  def _ct_reorder_siblings(minimum_sort_order_value = nil)
@@ -27,60 +29,94 @@ module ClosureTree
27
29
 
28
30
  def self_and_descendants_preordered
29
31
  # TODO: raise NotImplementedError if sort_order is not numeric and not null?
30
- join_sql = <<-SQL
31
- JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
32
- ON anc_hier.descendant_id = #{_ct.quoted_hierarchy_table_name}.descendant_id
33
- JOIN #{_ct.quoted_table_name} anc
34
- ON anc.#{_ct.quoted_id_column_name} = anc_hier.ancestor_id
35
- JOIN #{_ct.quoted_hierarchy_table_name} depths
36
- ON depths.ancestor_id = #{_ct.quote(self.id)} AND depths.descendant_id = anc.#{_ct.quoted_id_column_name}
37
- SQL
32
+ hierarchy_table = self.class.hierarchy_class.arel_table
33
+ model_table = self.class.arel_table
34
+
35
+ # Create aliased tables for the joins
36
+ anc_hier = _ct.aliased_table(hierarchy_table, 'anc_hier')
37
+ anc = _ct.aliased_table(model_table, 'anc')
38
+ depths = _ct.aliased_table(hierarchy_table, 'depths')
39
+
40
+ # Build the join conditions using Arel
41
+ join_anc_hier = hierarchy_table
42
+ .join(anc_hier)
43
+ .on(anc_hier[:descendant_id].eq(hierarchy_table[:descendant_id]))
44
+
45
+ join_anc = join_anc_hier
46
+ .join(anc)
47
+ .on(anc[self.class.primary_key].eq(anc_hier[:ancestor_id]))
48
+
49
+ join_depths = join_anc
50
+ .join(depths)
51
+ .on(
52
+ depths[:ancestor_id].eq(id)
53
+ .and(depths[:descendant_id].eq(anc[self.class.primary_key]))
54
+ )
38
55
 
39
56
  self_and_descendants
40
- .joins(join_sql)
57
+ .joins(join_depths.join_sources)
41
58
  .group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
42
59
  .reorder(self.class._ct_sum_order_by(self))
43
60
  end
44
61
 
45
62
  class_methods do
46
-
47
63
  # If node is nil, order the whole tree.
48
64
  def _ct_sum_order_by(node = nil)
49
- stats_sql = <<-SQL.squish
50
- SELECT
51
- count(*) as total_descendants,
52
- max(generations) as max_depth
53
- FROM #{_ct.quoted_hierarchy_table_name}
54
- SQL
55
- stats_sql += " WHERE ancestor_id = #{_ct.quote(node.id)}" if node
56
- h = _ct.connection.select_one(stats_sql)
65
+ # Build the stats query using Arel
66
+ hierarchy_table = hierarchy_class.arel_table
67
+
68
+ query = hierarchy_table
69
+ .project(
70
+ Arel.star.count.as('total_descendants'),
71
+ hierarchy_table[:generations].maximum.as('max_depth')
72
+ )
73
+
74
+ query = query.where(hierarchy_table[:ancestor_id].eq(node.id)) if node
75
+
76
+ h = _ct.connection.select_one(query.to_sql)
57
77
 
58
78
  depth_column = node ? 'depths.generations' : 'depths.max_depth'
59
79
 
60
- node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
61
- "power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
80
+ node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " \
81
+ "power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
62
82
 
63
83
  # We want the NULLs to be first in case we are not ordering roots and they have NULL order.
64
84
  Arel.sql("SUM(#{node_score}) IS NULL DESC, SUM(#{node_score})")
65
85
  end
66
86
 
67
87
  def roots_and_descendants_preordered
68
- if _ct.dont_order_roots
69
- raise ClosureTree::RootOrderingDisabledError.new("Root ordering is disabled on this model")
70
- end
71
-
72
- join_sql = <<-SQL.squish
73
- JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
74
- ON anc_hier.descendant_id = #{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}
75
- JOIN #{_ct.quoted_table_name} anc
76
- ON anc.#{_ct.quoted_id_column_name} = anc_hier.ancestor_id
77
- JOIN (
78
- SELECT descendant_id, max(generations) AS max_depth
79
- FROM #{_ct.quoted_hierarchy_table_name}
80
- GROUP BY descendant_id
81
- ) #{ _ct.t_alias_keyword } depths ON depths.descendant_id = anc.#{_ct.quoted_id_column_name}
82
- SQL
83
- joins(join_sql)
88
+ raise ClosureTree::RootOrderingDisabledError, 'Root ordering is disabled on this model' if _ct.dont_order_roots
89
+
90
+ hierarchy_table = hierarchy_class.arel_table
91
+ model_table = arel_table
92
+
93
+ # Create aliased tables
94
+ anc_hier = _ct.aliased_table(hierarchy_table, 'anc_hier')
95
+ anc = _ct.aliased_table(model_table, 'anc')
96
+
97
+ # Build the subquery for depths
98
+ depths_subquery = hierarchy_table
99
+ .project(
100
+ hierarchy_table[:descendant_id],
101
+ hierarchy_table[:generations].maximum.as('max_depth')
102
+ )
103
+ .group(hierarchy_table[:descendant_id])
104
+ .as('depths')
105
+
106
+ # Build the join conditions
107
+ join_anc_hier = model_table
108
+ .join(anc_hier)
109
+ .on(anc_hier[:descendant_id].eq(model_table[primary_key]))
110
+
111
+ join_anc = join_anc_hier
112
+ .join(anc)
113
+ .on(anc[primary_key].eq(anc_hier[:ancestor_id]))
114
+
115
+ join_depths = join_anc
116
+ .join(depths_subquery)
117
+ .on(depths_subquery[:descendant_id].eq(anc[primary_key]))
118
+
119
+ joins(join_depths.join_sources)
84
120
  .group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
85
121
  .reorder(_ct_sum_order_by)
86
122
  end
@@ -111,10 +147,10 @@ module ClosureTree
111
147
  end
112
148
 
113
149
  def add_sibling(sibling, add_after = true)
114
- fail "can't add self as sibling" if self == sibling
150
+ raise "can't add self as sibling" if self == sibling
115
151
 
116
152
  if _ct.dont_order_roots && parent.nil?
117
- raise ClosureTree::RootOrderingDisabledError.new("Root ordering is disabled on this model")
153
+ raise ClosureTree::RootOrderingDisabledError, 'Root ordering is disabled on this model'
118
154
  end
119
155
 
120
156
  # Make sure self isn't dirty, because we're going to call reload:
@@ -122,31 +158,30 @@ module ClosureTree
122
158
 
123
159
  _ct.with_advisory_lock do
124
160
  prior_sibling_parent = sibling.parent
125
- reorder_from_value = if prior_sibling_parent == self.parent
126
- [self.order_value, sibling.order_value].compact.min
127
- else
128
- self.order_value
129
- end
130
-
131
- sibling.order_value = self.order_value
132
- sibling.parent = self.parent
161
+ reorder_from_value = if prior_sibling_parent == parent
162
+ [order_value, sibling.order_value].compact.min
163
+ else
164
+ order_value
165
+ end
166
+
167
+ sibling.order_value = order_value
168
+ sibling.parent = parent
133
169
  sibling._ct_skip_sort_order_maintenance!
134
170
  sibling.save # may be a no-op
135
171
 
136
172
  _ct_reorder_siblings(reorder_from_value)
137
173
 
138
174
  # The sort order should be correct now except for self and sibling, which may need to flip:
139
- sibling_is_after = self.reload.order_value < sibling.reload.order_value
175
+ sibling_is_after = reload.order_value < sibling.reload.order_value
140
176
  if add_after != sibling_is_after
141
177
  # We need to flip the sort orders:
142
- self_ov, sib_ov = self.order_value, sibling.order_value
178
+ self_ov = order_value
179
+ sib_ov = sibling.order_value
143
180
  update_order_value(sib_ov)
144
181
  sibling.update_order_value(self_ov)
145
182
  end
146
183
 
147
- if prior_sibling_parent != self.parent
148
- prior_sibling_parent.try(:_ct_reorder_children)
149
- end
184
+ prior_sibling_parent.try(:_ct_reorder_children) if prior_sibling_parent != parent
150
185
  sibling
151
186
  end
152
187
  end
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClosureTree
2
4
  module NumericOrderSupport
3
-
4
5
  def self.adapter_for_connection(connection)
5
- das = WithAdvisoryLock::DatabaseAdapterSupport.new(connection)
6
- if das.postgresql?
6
+ adapter_name = connection.adapter_name.downcase
7
+ if adapter_name.include?('postgresql') || adapter_name.include?('postgis')
7
8
  ::ClosureTree::NumericOrderSupport::PostgreSQLAdapter
8
- elsif das.mysql?
9
+ elsif adapter_name.include?('mysql') || adapter_name.include?('trilogy')
9
10
  ::ClosureTree::NumericOrderSupport::MysqlAdapter
10
11
  else
11
12
  ::ClosureTree::NumericOrderSupport::GenericAdapter
@@ -15,11 +16,12 @@ module ClosureTree
15
16
  module MysqlAdapter
16
17
  def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
17
18
  return if parent_id.nil? && dont_order_roots
19
+
18
20
  min_where = if minimum_sort_order_value
19
- "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
20
- else
21
- ""
22
- end
21
+ "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
22
+ else
23
+ ''
24
+ end
23
25
  connection.execute 'SET @i = 0'
24
26
  connection.execute <<-SQL.squish
25
27
  UPDATE #{quoted_table_name}
@@ -33,11 +35,12 @@ module ClosureTree
33
35
  module PostgreSQLAdapter
34
36
  def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
35
37
  return if parent_id.nil? && dont_order_roots
38
+
36
39
  min_where = if minimum_sort_order_value
37
- "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
38
- else
39
- ""
40
- end
40
+ "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
41
+ else
42
+ ''
43
+ end
41
44
  connection.execute <<-SQL.squish
42
45
  UPDATE #{quoted_table_name}
43
46
  SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i - 1}
@@ -59,12 +62,11 @@ module ClosureTree
59
62
  module GenericAdapter
60
63
  def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
61
64
  return if parent_id.nil? && dont_order_roots
62
- scope = model_class.
63
- where(parent_column_sym => parent_id).
64
- order(nulls_last_order_by)
65
- if minimum_sort_order_value
66
- scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}")
67
- end
65
+
66
+ scope = model_class
67
+ .where(parent_column_sym => parent_id)
68
+ .order(nulls_last_order_by)
69
+ scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}") if minimum_sort_order_value
68
70
  scope.each_with_index do |ea, idx|
69
71
  ea.update_order_value(idx + minimum_sort_order_value.to_i)
70
72
  end
@@ -1,9 +1,4 @@
1
- require 'closure_tree/support_flags'
2
- require 'closure_tree/support_attributes'
3
- require 'closure_tree/numeric_order_support'
4
- require 'closure_tree/active_record_support'
5
- require 'closure_tree/hash_tree_support'
6
- require 'with_advisory_lock'
1
+ # frozen_string_literal: true
7
2
 
8
3
  # This class and mixins are an effort to reduce the namespace pollution to models that act_as_tree.
9
4
  module ClosureTree
@@ -12,42 +7,40 @@ module ClosureTree
12
7
  include ClosureTree::SupportAttributes
13
8
  include ClosureTree::ActiveRecordSupport
14
9
  include ClosureTree::HashTreeSupport
10
+ include ClosureTree::ArelHelpers
15
11
 
16
- attr_reader :model_class
17
- attr_reader :options
12
+ attr_reader :model_class, :options
18
13
 
19
14
  def initialize(model_class, options)
20
15
  @model_class = model_class
16
+
21
17
  @options = {
22
- :parent_column_name => 'parent_id',
23
- :dependent => :nullify, # or :destroy or :delete_all -- see the README
24
- :name_column => 'name',
25
- :with_advisory_lock => true,
26
- :numeric_order => false
18
+ parent_column_name: 'parent_id',
19
+ dependent: :nullify, # or :destroy or :delete_all -- see the README
20
+ name_column: 'name',
21
+ with_advisory_lock: true, # This will be overridden by adapter support
22
+ numeric_order: false
27
23
  }.merge(options)
28
24
  raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
29
- if order_is_numeric?
30
- extend NumericOrderSupport.adapter_for_connection(connection)
31
- end
25
+
26
+ return unless order_is_numeric?
27
+
28
+ extend NumericOrderSupport.adapter_for_connection(connection)
32
29
  end
33
30
 
34
31
  def hierarchy_class_for_model
35
32
  parent_class = model_class.module_parent
36
33
  hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(model_class.superclass))
37
- use_attr_accessible = use_attr_accessible?
38
- include_forbidden_attributes_protection = include_forbidden_attributes_protection?
39
34
  model_class_name = model_class.to_s
40
35
  hierarchy_class.class_eval do
41
- include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection
42
36
  belongs_to :ancestor, class_name: model_class_name
43
37
  belongs_to :descendant, class_name: model_class_name
44
- attr_accessible :ancestor, :descendant, :generations if use_attr_accessible
45
38
  def ==(other)
46
39
  self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
47
40
  end
48
41
  alias :eql? :==
49
42
  def hash
50
- ancestor_id.hash << 31 ^ descendant_id.hash
43
+ (ancestor_id.hash << 31) ^ descendant_id.hash
51
44
  end
52
45
  end
53
46
  hierarchy_class.table_name = hierarchy_table_name
@@ -59,21 +52,19 @@ module ClosureTree
59
52
  # because they may have overridden the table name, which is what we want to be consistent with
60
53
  # in order for the schema to make sense.
61
54
  tablename = options[:hierarchy_table_name] ||
62
- remove_prefix_and_suffix(table_name, model_class).singularize + "_hierarchies"
55
+ "#{remove_prefix_and_suffix(table_name, model_class).singularize}_hierarchies"
63
56
 
64
57
  [model_class.table_name_prefix, tablename, model_class.table_name_suffix].join
65
58
  end
66
59
 
67
60
  def with_order_option(opts)
68
- if order_option?
69
- opts[:order] = [opts[:order], order_by].compact.join(",")
70
- end
61
+ opts[:order] = [opts[:order], order_by].compact.join(',') if order_option?
71
62
  opts
72
63
  end
73
64
 
74
65
  def scope_with_order(scope, additional_order_by = nil)
75
66
  if order_option?
76
- scope.order(*([additional_order_by, order_by].compact))
67
+ scope.order(*[additional_order_by, order_by].compact)
77
68
  else
78
69
  additional_order_by ? scope.order(additional_order_by) : scope
79
70
  end
@@ -81,10 +72,10 @@ module ClosureTree
81
72
 
82
73
  # lambda-ize the order, but don't apply the default order_option
83
74
  def has_many_order_without_option(order_by_opt)
84
- [lambda { order(order_by_opt.call) }]
75
+ [-> { order(order_by_opt.call) }]
85
76
  end
86
77
 
87
- def has_many_order_with_option(order_by_opt=nil)
78
+ def has_many_order_with_option(order_by_opt = nil)
88
79
  order_options = [order_by_opt, order_by].compact
89
80
  [lambda {
90
81
  order_options = order_options.map { |o| o.is_a?(Proc) ? o.call : o }
@@ -105,9 +96,9 @@ module ClosureTree
105
96
  end
106
97
 
107
98
  def with_advisory_lock(&block)
108
- if options[:with_advisory_lock]
99
+ if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(:with_advisory_lock)
109
100
  model_class.with_advisory_lock(advisory_lock_name) do
110
- transaction { yield }
101
+ transaction(&block)
111
102
  end
112
103
  else
113
104
  yield
@@ -119,7 +110,7 @@ module ClosureTree
119
110
  unless path.first.is_a?(Hash)
120
111
  if subclass? && has_inheritance_column?
121
112
  attributes = attributes.with_indifferent_access
122
- attributes[inheritance_column] ||= self.sti_name
113
+ attributes[inheritance_column] ||= sti_name
123
114
  end
124
115
  path = path.map { |ea| attributes.merge(name_column => ea) }
125
116
  end
@@ -127,11 +118,9 @@ module ClosureTree
127
118
  end
128
119
 
129
120
  def scoped_attributes(scope, attributes, target_table = model_class.table_name)
130
- table_prefixed_attributes = Hash[
131
- attributes.map do |column_name, column_value|
132
- ["#{target_table}.#{column_name}", column_value]
133
- end
134
- ]
121
+ table_prefixed_attributes = attributes.transform_keys do |column_name|
122
+ "#{target_table}.#{column_name}"
123
+ end
135
124
  scope.where(table_prefixed_attributes)
136
125
  end
137
126
 
@@ -146,6 +135,7 @@ module ClosureTree
146
135
  path.in_groups(max_join_tables, false).each do |subpath|
147
136
  child = model_class.find_by_path(subpath, attributes, next_parent_id)
148
137
  return nil if child.nil?
138
+
149
139
  next_parent_id = child._ct_id
150
140
  end
151
141
  child
@@ -164,7 +154,7 @@ module ClosureTree
164
154
  end
165
155
 
166
156
  def create!(model_class, attributes)
167
- create(model_class, attributes).tap { |ea| ea.save! }
157
+ create(model_class, attributes).tap(&:save!)
168
158
  end
169
159
  end
170
160
  end
@@ -1,11 +1,37 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
4
+ require 'zlib'
5
+
2
6
  module ClosureTree
3
7
  module SupportAttributes
4
8
  extend Forwardable
5
9
  def_delegators :model_class, :connection, :transaction, :table_name, :base_class, :inheritance_column, :column_names
6
10
 
7
11
  def advisory_lock_name
8
- Digest::SHA1.hexdigest("ClosureTree::#{base_class.name}")[0..32]
12
+ # Allow customization via options or instance method
13
+ if options[:advisory_lock_name]
14
+ case options[:advisory_lock_name]
15
+ when Proc
16
+ # Allow dynamic generation via proc
17
+ options[:advisory_lock_name].call(base_class)
18
+ when Symbol
19
+ # Allow delegation to a model method
20
+ if model_class.respond_to?(options[:advisory_lock_name])
21
+ model_class.send(options[:advisory_lock_name])
22
+ else
23
+ raise ArgumentError, "Model #{model_class} does not respond to #{options[:advisory_lock_name]}"
24
+ end
25
+ else
26
+ # Use static string value
27
+ options[:advisory_lock_name].to_s
28
+ end
29
+ else
30
+ # Default: Use CRC32 for a shorter, consistent hash
31
+ # This gives us 8 hex characters which is plenty for uniqueness
32
+ # and leaves room for prefixes
33
+ "ct_#{Zlib.crc32(base_class.name.to_s).to_s(16)}"
34
+ end
9
35
  end
10
36
 
11
37
  def quoted_table_name
@@ -17,7 +43,7 @@ module ClosureTree
17
43
  end
18
44
 
19
45
  def hierarchy_class_name
20
- options[:hierarchy_class_name] || model_class.to_s + "Hierarchy"
46
+ options[:hierarchy_class_name] || "#{model_class}Hierarchy"
21
47
  end
22
48
 
23
49
  def primary_key_column
@@ -84,7 +110,7 @@ module ClosureTree
84
110
  end
85
111
 
86
112
  def order_by_order(reverse = false)
87
- desc = !!(order_by.to_s =~ /DESC\z/)
113
+ desc = !(order_by.to_s =~ /DESC\z/).nil?
88
114
  desc = !desc if reverse
89
115
  desc ? 'DESC' : 'ASC'
90
116
  end
@@ -111,13 +137,13 @@ module ClosureTree
111
137
 
112
138
  def quoted_order_column(include_table_name = true)
113
139
  require_order_column
114
- prefix = include_table_name ? "#{quoted_table_name}." : ""
140
+ prefix = include_table_name ? "#{quoted_table_name}." : ''
115
141
  "#{prefix}#{connection.quote_column_name(order_column)}"
116
142
  end
117
143
 
118
144
  # table_name alias keyword , like "AS". When used on table name alias, Oracle Database don't support used 'AS'
119
145
  def t_alias_keyword
120
- (ActiveRecord::Base.connection.adapter_name.to_sym == :OracleEnhanced) ? "" : "AS"
146
+ ActiveRecord::Base.connection.adapter_name.to_sym == :OracleEnhanced ? '' : 'AS'
121
147
  end
122
148
  end
123
149
  end
@@ -1,17 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClosureTree
2
4
  module SupportFlags
3
-
4
- def use_attr_accessible?
5
- defined?(ActiveModel::MassAssignmentSecurity) &&
6
- model_class.respond_to?(:accessible_attributes) &&
7
- ! model_class.accessible_attributes.empty?
8
- end
9
-
10
- def include_forbidden_attributes_protection?
11
- defined?(ActiveModel::ForbiddenAttributesProtection) &&
12
- model_class.ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
13
- end
14
-
15
5
  def order_option?
16
6
  order_by.present?
17
7
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'closure_tree'
2
4
 
3
5
  module ClosureTree
@@ -19,13 +21,13 @@ module ClosureTree
19
21
  end
20
22
 
21
23
  # Checking if hierarchy table exists (common error)
22
- unless @subject.hierarchy_class.table_exists?
24
+ unless @subject.hierarchy_class.table_exists?
23
25
  @message = "expected #{@subject.name}'s hierarchy table '#{@subject.hierarchy_class.table_name}' to exist"
24
26
  return false
25
27
  end
26
28
 
27
29
  if @ordered
28
- unless @subject._ct.options.include?(:order)
30
+ unless @subject._ct.options.include?(:order)
29
31
  @message = "expected #{@subject.name} to be an ordered closure tree"
30
32
  return false
31
33
  end
@@ -36,13 +38,13 @@ module ClosureTree
36
38
  end
37
39
 
38
40
  if @with_advisory_lock && !@subject._ct.options[:with_advisory_lock]
39
- @message = "expected #{@subject.name} to have advisory lock"
40
- return false
41
+ @message = "expected #{@subject.name} to have advisory lock"
42
+ return false
41
43
  end
42
44
 
43
45
  if @without_advisory_lock && @subject._ct.options[:with_advisory_lock]
44
- @message = "expected #{@subject.name} to not have advisory lock"
45
- return false
46
+ @message = "expected #{@subject.name} to not have advisory lock"
47
+ return false
46
48
  end
47
49
 
48
50
  return true
@@ -70,13 +72,13 @@ module ClosureTree
70
72
  @message || "expected #{@subject.name} to #{description}"
71
73
  end
72
74
 
73
- alias_method :failure_message_for_should, :failure_message
75
+ alias failure_message_for_should failure_message
74
76
 
75
77
  def failure_message_when_negated
76
78
  "expected #{@subject.name} not be a closure tree, but it is."
77
79
  end
78
80
 
79
- alias_method :failure_message_for_should_not, :failure_message_when_negated
81
+ alias failure_message_for_should_not failure_message_when_negated
80
82
 
81
83
  def description
82
84
  "be a#{@ordered} closure tree#{@with_advisory_lock}"
@@ -85,7 +87,3 @@ module ClosureTree
85
87
  end
86
88
  end
87
89
  end
88
-
89
- RSpec.configure do |c|
90
- c.include ClosureTree::Test::Matcher, type: :model
91
- end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClosureTree
2
- VERSION = Gem::Version.new('8.0.0')
4
+ VERSION = Gem::Version.new('9.1.0')
3
5
  end
data/lib/closure_tree.rb CHANGED
@@ -1,30 +1,22 @@
1
- require 'active_record'
1
+ # frozen_string_literal: true
2
2
 
3
- module ClosureTree
4
- extend ActiveSupport::Autoload
3
+ require 'active_record'
4
+ require 'zeitwerk'
5
5
 
6
- autoload :HasClosureTree
7
- autoload :HasClosureTreeRoot
8
- autoload :Support
9
- autoload :HierarchyMaintenance
10
- autoload :Model
11
- autoload :Finders
12
- autoload :HashTree
13
- autoload :Digraphs
14
- autoload :DeterministicOrdering
15
- autoload :NumericDeterministicOrdering
16
- autoload :Configuration
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.ignore("#{__dir__}/generators")
8
+ loader.setup
17
9
 
10
+ module ClosureTree
18
11
  def self.configure
19
- yield configuration
20
- end
21
-
22
- def self.configuration
23
- @configuration ||= Configuration.new
12
+ ActiveSupport::Deprecation.new.warn(
13
+ 'ClosureTree.configure is deprecated and will be removed in a future version. ' \
14
+ 'Configuration is no longer needed.'
15
+ )
16
+ yield if block_given?
24
17
  end
25
18
  end
26
19
 
27
- ActiveSupport.on_load :active_record do
28
- ActiveRecord::Base.send :extend, ClosureTree::HasClosureTree
29
- ActiveRecord::Base.send :extend, ClosureTree::HasClosureTreeRoot
20
+ ActiveSupport.on_load(:active_record) do
21
+ extend ClosureTree::HasClosureTree, ClosureTree::HasClosureTreeRoot
30
22
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'closure_tree/active_record_support'
2
4
  require 'forwardable'
3
5
  require 'rails/generators'
@@ -36,10 +38,10 @@ module ClosureTree
36
38
 
37
39
  def ct
38
40
  @ct ||= if target_class.respond_to?(:_ct)
39
- target_class._ct
40
- else
41
- fail "Please RTFM and add the `has_closure_tree` (or `acts_as_tree`) annotation to #{class_name} before creating the migration."
42
- end
41
+ target_class._ct
42
+ else
43
+ raise "Please RTFM and add the `has_closure_tree` (or `acts_as_tree`) annotation to #{class_name} before creating the migration."
44
+ end
43
45
  end
44
46
 
45
47
  def migration_version