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
  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,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'closure_tree/support_flags'
2
4
  require 'closure_tree/support_attributes'
3
5
  require 'closure_tree/numeric_order_support'
4
6
  require 'closure_tree/active_record_support'
5
7
  require 'closure_tree/hash_tree_support'
6
- require 'with_advisory_lock'
8
+ require 'closure_tree/arel_helpers'
7
9
 
8
10
  # This class and mixins are an effort to reduce the namespace pollution to models that act_as_tree.
9
11
  module ClosureTree
@@ -12,42 +14,40 @@ module ClosureTree
12
14
  include ClosureTree::SupportAttributes
13
15
  include ClosureTree::ActiveRecordSupport
14
16
  include ClosureTree::HashTreeSupport
17
+ include ClosureTree::ArelHelpers
15
18
 
16
- attr_reader :model_class
17
- attr_reader :options
19
+ attr_reader :model_class, :options
18
20
 
19
21
  def initialize(model_class, options)
20
22
  @model_class = model_class
23
+
21
24
  @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
25
+ parent_column_name: 'parent_id',
26
+ dependent: :nullify, # or :destroy or :delete_all -- see the README
27
+ name_column: 'name',
28
+ with_advisory_lock: true, # This will be overridden by adapter support
29
+ numeric_order: false
27
30
  }.merge(options)
28
31
  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
32
+
33
+ return unless order_is_numeric?
34
+
35
+ extend NumericOrderSupport.adapter_for_connection(connection)
32
36
  end
33
37
 
34
38
  def hierarchy_class_for_model
35
39
  parent_class = model_class.module_parent
36
40
  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
41
  model_class_name = model_class.to_s
40
42
  hierarchy_class.class_eval do
41
- include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection
42
43
  belongs_to :ancestor, class_name: model_class_name
43
44
  belongs_to :descendant, class_name: model_class_name
44
- attr_accessible :ancestor, :descendant, :generations if use_attr_accessible
45
45
  def ==(other)
46
46
  self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id
47
47
  end
48
48
  alias :eql? :==
49
49
  def hash
50
- ancestor_id.hash << 31 ^ descendant_id.hash
50
+ (ancestor_id.hash << 31) ^ descendant_id.hash
51
51
  end
52
52
  end
53
53
  hierarchy_class.table_name = hierarchy_table_name
@@ -59,21 +59,19 @@ module ClosureTree
59
59
  # because they may have overridden the table name, which is what we want to be consistent with
60
60
  # in order for the schema to make sense.
61
61
  tablename = options[:hierarchy_table_name] ||
62
- remove_prefix_and_suffix(table_name, model_class).singularize + "_hierarchies"
62
+ "#{remove_prefix_and_suffix(table_name, model_class).singularize}_hierarchies"
63
63
 
64
64
  [model_class.table_name_prefix, tablename, model_class.table_name_suffix].join
65
65
  end
66
66
 
67
67
  def with_order_option(opts)
68
- if order_option?
69
- opts[:order] = [opts[:order], order_by].compact.join(",")
70
- end
68
+ opts[:order] = [opts[:order], order_by].compact.join(',') if order_option?
71
69
  opts
72
70
  end
73
71
 
74
72
  def scope_with_order(scope, additional_order_by = nil)
75
73
  if order_option?
76
- scope.order(*([additional_order_by, order_by].compact))
74
+ scope.order(*[additional_order_by, order_by].compact)
77
75
  else
78
76
  additional_order_by ? scope.order(additional_order_by) : scope
79
77
  end
@@ -81,10 +79,10 @@ module ClosureTree
81
79
 
82
80
  # lambda-ize the order, but don't apply the default order_option
83
81
  def has_many_order_without_option(order_by_opt)
84
- [lambda { order(order_by_opt.call) }]
82
+ [-> { order(order_by_opt.call) }]
85
83
  end
86
84
 
87
- def has_many_order_with_option(order_by_opt=nil)
85
+ def has_many_order_with_option(order_by_opt = nil)
88
86
  order_options = [order_by_opt, order_by].compact
89
87
  [lambda {
90
88
  order_options = order_options.map { |o| o.is_a?(Proc) ? o.call : o }
@@ -105,9 +103,9 @@ module ClosureTree
105
103
  end
106
104
 
107
105
  def with_advisory_lock(&block)
108
- if options[:with_advisory_lock]
106
+ if options[:with_advisory_lock] && connection.supports_advisory_locks? && model_class.respond_to?(:with_advisory_lock)
109
107
  model_class.with_advisory_lock(advisory_lock_name) do
110
- transaction { yield }
108
+ transaction(&block)
111
109
  end
112
110
  else
113
111
  yield
@@ -119,7 +117,7 @@ module ClosureTree
119
117
  unless path.first.is_a?(Hash)
120
118
  if subclass? && has_inheritance_column?
121
119
  attributes = attributes.with_indifferent_access
122
- attributes[inheritance_column] ||= self.sti_name
120
+ attributes[inheritance_column] ||= sti_name
123
121
  end
124
122
  path = path.map { |ea| attributes.merge(name_column => ea) }
125
123
  end
@@ -127,11 +125,9 @@ module ClosureTree
127
125
  end
128
126
 
129
127
  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
- ]
128
+ table_prefixed_attributes = attributes.transform_keys do |column_name|
129
+ "#{target_table}.#{column_name}"
130
+ end
135
131
  scope.where(table_prefixed_attributes)
136
132
  end
137
133
 
@@ -146,6 +142,7 @@ module ClosureTree
146
142
  path.in_groups(max_join_tables, false).each do |subpath|
147
143
  child = model_class.find_by_path(subpath, attributes, next_parent_id)
148
144
  return nil if child.nil?
145
+
149
146
  next_parent_id = child._ct_id
150
147
  end
151
148
  child
@@ -164,7 +161,7 @@ module ClosureTree
164
161
  end
165
162
 
166
163
  def create!(model_class, attributes)
167
- create(model_class, attributes).tap { |ea| ea.save! }
164
+ create(model_class, attributes).tap(&:save!)
168
165
  end
169
166
  end
170
167
  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.0.0')
3
5
  end
data/lib/closure_tree.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
 
3
5
  module ClosureTree
@@ -14,6 +16,7 @@ module ClosureTree
14
16
  autoload :DeterministicOrdering
15
17
  autoload :NumericDeterministicOrdering
16
18
  autoload :Configuration
19
+ autoload :AdapterSupport
17
20
 
18
21
  def self.configure
19
22
  yield configuration
@@ -25,6 +28,23 @@ module ClosureTree
25
28
  end
26
29
 
27
30
  ActiveSupport.on_load :active_record do
28
- ActiveRecord::Base.send :extend, ClosureTree::HasClosureTree
29
- ActiveRecord::Base.send :extend, ClosureTree::HasClosureTreeRoot
31
+ ActiveRecord::Base.extend ClosureTree::HasClosureTree
32
+ ActiveRecord::Base.extend ClosureTree::HasClosureTreeRoot
33
+ end
34
+
35
+ # Adapter injection for different database types
36
+ ActiveSupport.on_load :active_record_postgresqladapter do
37
+ prepend ClosureTree::AdapterSupport
38
+ end
39
+
40
+ ActiveSupport.on_load :active_record_mysql2adapter do
41
+ prepend ClosureTree::AdapterSupport
42
+ end
43
+
44
+ ActiveSupport.on_load :active_record_trilogyadapter do
45
+ prepend ClosureTree::AdapterSupport
46
+ end
47
+
48
+ ActiveSupport.on_load :active_record_sqlite3adapter do
49
+ prepend ClosureTree::AdapterSupport
30
50
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ClosureTree
2
4
  module Generators # :nodoc:
3
5
  class ConfigGenerator < Rails::Generators::Base # :nodoc:
4
- source_root File.expand_path('../templates', __FILE__)
6
+ source_root File.expand_path('templates', __dir__)
5
7
  desc 'Install closure tree config.'
6
8
 
7
9
  def config
@@ -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