ancestry 3.0.6 → 4.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.
data/lib/ancestry.rb CHANGED
@@ -4,7 +4,34 @@ require_relative 'ancestry/instance_methods'
4
4
  require_relative 'ancestry/exceptions'
5
5
  require_relative 'ancestry/has_ancestry'
6
6
  require_relative 'ancestry/materialized_path'
7
+ require_relative 'ancestry/materialized_path_pg'
8
+
9
+ I18n.load_path += Dir[File.join(File.expand_path(File.dirname(__FILE__)),
10
+ 'ancestry', 'locales', '*.{rb,yml}').to_s]
7
11
 
8
12
  module Ancestry
9
- ANCESTRY_PATTERN = /\A[0-9]+(\/[0-9]+)*\Z/
13
+ @@default_update_strategy = :ruby
14
+
15
+ # @!default_update_strategy
16
+ # @return [Symbol] the default strategy for updating ancestry
17
+ #
18
+ # The value changes the default way that ancestry is updated for associated records
19
+ #
20
+ # :ruby (default and legacy value)
21
+ #
22
+ # Child records will be loaded into memory and updated. callbacks will get called
23
+ # The callbacks of interest are those that cache values based upon the ancestry value
24
+ #
25
+ # :sql (currently only valid in postgres)
26
+ #
27
+ # Child records are updated in sql and callbacks will not get called.
28
+ # Associated records in memory will have the wrong ancestry value
29
+
30
+ def self.default_update_strategy
31
+ @@default_update_strategy
32
+ end
33
+
34
+ def self.default_update_strategy=(value)
35
+ @@default_update_strategy = value
36
+ end
10
37
  end
@@ -2,7 +2,11 @@ module Ancestry
2
2
  module ClassMethods
3
3
  # Fetch tree node if necessary
4
4
  def to_node object
5
- if object.is_a?(self.ancestry_base_class) then object else unscoped_where{|scope| scope.find object} end
5
+ if object.is_a?(self.ancestry_base_class)
6
+ object
7
+ else
8
+ unscoped_where { |scope| scope.find(object.try(primary_key) || object) }
9
+ end
6
10
  end
7
11
 
8
12
  # Scope on relative depth options
@@ -12,7 +16,7 @@ module Ancestry
12
16
  if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
13
17
  scope.send scope_name, depth + relative_depth
14
18
  else
15
- raise Ancestry::AncestryException.new("Unknown depth option: #{scope_name}.")
19
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_depth_option", scope_name: scope_name))
16
20
  end
17
21
  end
18
22
  end
@@ -23,11 +27,17 @@ module Ancestry
23
27
  if [:rootify, :adopt, :restrict, :destroy].include? orphan_strategy
24
28
  class_variable_set :@@orphan_strategy, orphan_strategy
25
29
  else
26
- raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify,:adopt, :restrict and :destroy.")
30
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.invalid_orphan_strategy"))
27
31
  end
28
32
  end
29
33
 
30
- # Get all nodes and sorting them into an empty hash
34
+
35
+ # these methods arrange an entire subtree into nested hashes for easy navigation after database retrieval
36
+ # the arrange method also works on a scoped class
37
+ # the arrange method takes ActiveRecord find options
38
+ # To order your hashes pass the order to the arrange method instead of to the scope
39
+
40
+ # Get all nodes and sort them into an empty hash
31
41
  def arrange options = {}
32
42
  if (order = options.delete(:order))
33
43
  arrange_nodes self.ancestry_base_class.order(order).where(options)
@@ -50,7 +60,9 @@ module Ancestry
50
60
  end
51
61
  end
52
62
 
53
- # Arrangement to nested array
63
+ # Arrangement to nested array for serialization
64
+ # You can also supply your own serialization logic using blocks
65
+ # also allows you to pass the order just as you can pass it to the arrange method
54
66
  def arrange_serializable options={}, nodes=nil, &block
55
67
  nodes = arrange(options) if nodes.nil?
56
68
  nodes.map do |parent, children|
@@ -63,7 +75,6 @@ module Ancestry
63
75
  end
64
76
 
65
77
  # Pseudo-preordered array of nodes. Children will always follow parents,
66
- # for ordering nodes within a rank provide block, eg. Node.sort_by_ancestry(Node.all) {|a, b| a.rank <=> b.rank}.
67
78
  def sort_by_ancestry(nodes, &block)
68
79
  arranged = nodes if nodes.is_a?(Hash)
69
80
 
@@ -90,6 +101,9 @@ module Ancestry
90
101
  end
91
102
 
92
103
  # Integrity checking
104
+ # compromised tree integrity is unlikely without explicitly setting cyclic parents or invalid ancestry and circumventing validation
105
+ # just in case, raise an AncestryIntegrityException if issues are detected
106
+ # specify :report => :list to return an array of exceptions or :report => :echo to echo any error messages
93
107
  def check_ancestry_integrity! options = {}
94
108
  parents = {}
95
109
  exceptions = [] if options[:report] == :list
@@ -99,20 +113,30 @@ module Ancestry
99
113
  scope.find_each do |node|
100
114
  begin
101
115
  # ... check validity of ancestry column
102
- if !node.valid? and !node.errors[node.class.ancestry_column].blank?
103
- raise Ancestry::AncestryIntegrityException.new("Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_column}.")
116
+ if !node.sane_ancestor_ids?
117
+ raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.invalid_ancestry_column",
118
+ :node_id => node.id,
119
+ :ancestry_column => "#{node.read_attribute node.ancestry_column}"
120
+ ))
104
121
  end
105
122
  # ... check that all ancestors exist
106
123
  node.ancestor_ids.each do |ancestor_id|
107
124
  unless exists? ancestor_id
108
- raise Ancestry::AncestryIntegrityException.new("Reference to non-existent node in node #{node.id}: #{ancestor_id}.")
125
+ raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.reference_nonexistent_node",
126
+ :node_id => node.id,
127
+ :ancestor_id => ancestor_id
128
+ ))
109
129
  end
110
130
  end
111
131
  # ... check that all node parents are consistent with values observed earlier
112
132
  node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
113
133
  parents[node_id] = parent_id unless parents.has_key? node_id
114
134
  unless parents[node_id] == parent_id
115
- raise Ancestry::AncestryIntegrityException.new("Conflicting parent id found in node #{node.id}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}")
135
+ raise Ancestry::AncestryIntegrityException.new(I18n.t("ancestry.conflicting_parent_id",
136
+ :node_id => node_id,
137
+ :parent_id => parent_id || 'nil',
138
+ :expected => parents[node_id] || 'nil'
139
+ ))
116
140
  end
117
141
  end
118
142
  rescue Ancestry::AncestryIntegrityException => integrity_exception
@@ -129,59 +153,59 @@ module Ancestry
129
153
 
130
154
  # Integrity restoration
131
155
  def restore_ancestry_integrity!
132
- parents = {}
156
+ parent_ids = {}
133
157
  # Wrap the whole thing in a transaction ...
134
158
  self.ancestry_base_class.transaction do
135
159
  unscoped_where do |scope|
136
160
  # For each node ...
137
161
  scope.find_each do |node|
138
162
  # ... set its ancestry to nil if invalid
139
- if !node.valid? and !node.errors[node.class.ancestry_column].blank?
163
+ if !node.sane_ancestor_ids?
140
164
  node.without_ancestry_callbacks do
141
- node.update_attribute node.ancestry_column, nil
165
+ node.update_attribute :ancestor_ids, []
142
166
  end
143
167
  end
144
- # ... save parent of this node in parents array if it exists
145
- parents[node.id] = node.parent_id if exists? node.parent_id
168
+ # ... save parent id of this node in parent_ids array if it exists
169
+ parent_ids[node.id] = node.parent_id if exists? node.parent_id
146
170
 
147
171
  # Reset parent id in array to nil if it introduces a cycle
148
- parent = parents[node.id]
149
- until parent.nil? || parent == node.id
150
- parent = parents[parent]
172
+ parent_id = parent_ids[node.id]
173
+ until parent_id.nil? || parent_id == node.id
174
+ parent_id = parent_ids[parent_id]
151
175
  end
152
- parents[node.id] = nil if parent == node.id
176
+ parent_ids[node.id] = nil if parent_id == node.id
153
177
  end
154
178
 
155
179
  # For each node ...
156
180
  scope.find_each do |node|
157
- # ... rebuild ancestry from parents array
158
- ancestry, parent = nil, parents[node.id]
159
- until parent.nil?
160
- ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
181
+ # ... rebuild ancestry from parent_ids array
182
+ ancestor_ids, parent_id = [], parent_ids[node.id]
183
+ until parent_id.nil?
184
+ ancestor_ids, parent_id = [parent_id] + ancestor_ids, parent_ids[parent_id]
161
185
  end
162
186
  node.without_ancestry_callbacks do
163
- node.update_attribute node.ancestry_column, ancestry
187
+ node.update_attribute :ancestor_ids, ancestor_ids
164
188
  end
165
189
  end
166
190
  end
167
191
  end
168
192
  end
169
193
 
170
- # Build ancestry from parent id's for migration purposes
171
- def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
194
+ # Build ancestry from parent ids for migration purposes
195
+ def build_ancestry_from_parent_ids! column=:parent_id, parent_id = nil, ancestor_ids = []
172
196
  unscoped_where do |scope|
173
- scope.where(:parent_id => parent_id).find_each do |node|
197
+ scope.where(column => parent_id).find_each do |node|
174
198
  node.without_ancestry_callbacks do
175
- node.update_attribute ancestry_column, ancestry
199
+ node.update_attribute :ancestor_ids, ancestor_ids
176
200
  end
177
- build_ancestry_from_parent_ids! node.id, if ancestry.nil? then "#{node.id}" else "#{ancestry}/#{node.id}" end
201
+ build_ancestry_from_parent_ids! column, node.id, ancestor_ids + [node.id]
178
202
  end
179
203
  end
180
204
  end
181
205
 
182
206
  # Rebuild depth cache if it got corrupted or if depth caching was just turned on
183
207
  def rebuild_depth_cache!
184
- raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
208
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.cannot_rebuild_depth_cache")) unless respond_to? :depth_cache_column
185
209
 
186
210
  self.ancestry_base_class.transaction do
187
211
  unscoped_where do |scope|
@@ -193,31 +217,15 @@ module Ancestry
193
217
  end
194
218
 
195
219
  def unscoped_where
196
- if ActiveRecord::VERSION::MAJOR < 4
197
- self.ancestry_base_class.unscoped do
198
- yield self.ancestry_base_class
199
- end
200
- else
201
- yield self.ancestry_base_class.unscope(:where)
202
- end
220
+ yield self.ancestry_base_class.default_scoped.unscope(:where)
203
221
  end
204
222
 
205
223
  ANCESTRY_UNCAST_TYPES = [:string, :uuid, :text].freeze
206
- if ActiveSupport::VERSION::STRING < "4.2"
207
- def primary_key_is_an_integer?
208
- if defined?(@primary_key_is_an_integer)
209
- @primary_key_is_an_integer
210
- else
211
- @primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(columns_hash[primary_key.to_s].type)
212
- end
213
- end
214
- else
215
- def primary_key_is_an_integer?
216
- if defined?(@primary_key_is_an_integer)
217
- @primary_key_is_an_integer
218
- else
219
- @primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(type_for_attribute(primary_key).type)
220
- end
224
+ def primary_key_is_an_integer?
225
+ if defined?(@primary_key_is_an_integer)
226
+ @primary_key_is_an_integer
227
+ else
228
+ @primary_key_is_an_integer = !ANCESTRY_UNCAST_TYPES.include?(type_for_attribute(primary_key).type)
221
229
  end
222
230
  end
223
231
  end
@@ -2,14 +2,13 @@ module Ancestry
2
2
  module HasAncestry
3
3
  def has_ancestry options = {}
4
4
  # Check options
5
- raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
5
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.option_must_be_hash")) unless options.is_a? Hash
6
6
  options.each do |key, value|
7
- unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch].include? key
8
- raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
7
+ unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format, :update_strategy].include? key
8
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_option", key: key.inspect, value: value.inspect))
9
9
  end
10
10
  end
11
11
 
12
-
13
12
  # Create ancestry column accessor and set to option or default
14
13
  cattr_accessor :ancestry_column
15
14
  self.ancestry_column = options[:ancestry_column] || :ancestry
@@ -28,8 +27,12 @@ module Ancestry
28
27
  # Include dynamic class methods
29
28
  extend Ancestry::ClassMethods
30
29
 
30
+ validates_format_of self.ancestry_column, :with => derive_ancestry_pattern(options[:primary_key_format]), :allow_nil => true
31
31
  extend Ancestry::MaterializedPath
32
32
 
33
+ update_strategy = options[:update_strategy] || Ancestry.default_update_strategy
34
+ include Ancestry::MaterializedPathPg if update_strategy == :sql
35
+
33
36
  # Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
34
37
  cattr_reader :orphan_strategy
35
38
  self.orphan_strategy = options[:orphan_strategy] || :destroy
@@ -37,27 +40,6 @@ module Ancestry
37
40
  # Validate that the ancestor ids don't include own id
38
41
  validate :ancestry_exclude_self
39
42
 
40
- # Named scopes
41
- scope :roots, lambda { where(root_conditions) }
42
- scope :ancestors_of, lambda { |object| where(ancestor_conditions(object)) }
43
- scope :children_of, lambda { |object| where(child_conditions(object)) }
44
- scope :indirects_of, lambda { |object| where(indirect_conditions(object)) }
45
- scope :descendants_of, lambda { |object| where(descendant_conditions(object)) }
46
- scope :subtree_of, lambda { |object| where(subtree_conditions(object)) }
47
- scope :siblings_of, lambda { |object| where(sibling_conditions(object)) }
48
- scope :ordered_by_ancestry, Proc.new { |order|
49
- if %w(mysql mysql2 sqlite sqlite3 postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::MAJOR >= 5
50
- reorder(
51
- Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
52
- order
53
- )
54
- else
55
- reorder(Arel.sql("(CASE WHEN #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)} IS NULL THEN 0 ELSE 1 END), #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}"), order)
56
- end
57
- }
58
- scope :ordered_by_ancestry_and, Proc.new { |order| ordered_by_ancestry(order) }
59
- scope :path_of, lambda { |object| to_node(object).path }
60
-
61
43
  # Update descendants with new ancestry before save
62
44
  before_save :update_descendants_with_new_ancestry
63
45
 
@@ -78,31 +60,55 @@ module Ancestry
78
60
  validates_numericality_of depth_cache_column, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
79
61
  end
80
62
 
63
+ # Create counter cache column accessor and set to option or default
64
+ if options[:counter_cache]
65
+ cattr_accessor :counter_cache_column
66
+
67
+ if options[:counter_cache] == true
68
+ self.counter_cache_column = :children_count
69
+ else
70
+ self.counter_cache_column = options[:counter_cache]
71
+ end
72
+
73
+ after_create :increase_parent_counter_cache, if: :has_parent?
74
+ after_destroy :decrease_parent_counter_cache, if: :has_parent?
75
+ after_update :update_parent_counter_cache
76
+ end
77
+
81
78
  # Create named scopes for depth
82
79
  {:before_depth => '<', :to_depth => '<=', :at_depth => '=', :from_depth => '>=', :after_depth => '>'}.each do |scope_name, operator|
83
80
  scope scope_name, lambda { |depth|
84
- raise Ancestry::AncestryException.new("Named scope '#{scope_name}' is only available when depth caching is enabled.") unless options[:cache_depth]
81
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.named_scope_depth_cache",
82
+ :scope_name => scope_name
83
+ )) unless options[:cache_depth]
85
84
  where("#{depth_cache_column} #{operator} ?", depth)
86
85
  }
87
86
  end
88
87
 
89
88
  after_touch :touch_ancestors_callback
90
89
  after_destroy :touch_ancestors_callback
91
-
92
- if ActiveRecord::VERSION::STRING >= '5.1.0'
93
- after_save :touch_ancestors_callback, if: :saved_changes?
94
- else
95
- after_save :touch_ancestors_callback, if: :changed?
96
- end
90
+ after_save :touch_ancestors_callback, if: :saved_changes?
97
91
  end
98
92
 
99
93
  def acts_as_tree(*args)
100
94
  return super if defined?(super)
101
95
  has_ancestry(*args)
102
96
  end
97
+
98
+ private
99
+
100
+ def derive_ancestry_pattern(primary_key_format, delimiter = '/')
101
+ primary_key_format ||= '[0-9]+'
102
+
103
+ if primary_key_format.to_s.include?('\A')
104
+ primary_key_format
105
+ else
106
+ /\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
107
+ end
108
+ end
103
109
  end
104
110
  end
105
111
 
106
112
  ActiveSupport.on_load :active_record do
107
- send :extend, Ancestry::HasAncestry
113
+ extend Ancestry::HasAncestry
108
114
  end
@@ -1,11 +1,8 @@
1
1
  module Ancestry
2
2
  module InstanceMethods
3
- BEFORE_LAST_SAVE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_before_last_save' : '_was'
4
- IN_DATABASE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_in_database' : '_was'
5
-
6
3
  # Validate that the ancestors don't include itself
7
4
  def ancestry_exclude_self
8
- errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.") if ancestor_ids.include? self.id
5
+ errors.add(:base, I18n.t("ancestry.exclude_self", class_name: self.class.name.humanize)) if ancestor_ids.include? self.id
9
6
  end
10
7
 
11
8
  # Update descendants with new ancestry (before save)
@@ -16,15 +13,8 @@ module Ancestry
16
13
  unscoped_descendants.each do |descendant|
17
14
  # ... replace old ancestry with new ancestry
18
15
  descendant.without_ancestry_callbacks do
19
- descendant.update_attribute(
20
- self.ancestry_base_class.ancestry_column,
21
- descendant.read_attribute(descendant.class.ancestry_column).gsub(
22
- # child_ancestry_was
23
- /^#{self.child_ancestry}/,
24
- # future child_ancestry
25
- if ancestors? then "#{read_attribute self.class.ancestry_column }/#{id}" else id.to_s end
26
- )
27
- )
16
+ new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_in_database)
17
+ descendant.update_attribute(:ancestor_ids, new_ancestor_ids)
28
18
  end
29
19
  end
30
20
  end
@@ -37,13 +27,7 @@ module Ancestry
37
27
  when :rootify # make all children root if orphan strategy is rootify
38
28
  unscoped_descendants.each do |descendant|
39
29
  descendant.without_ancestry_callbacks do
40
- new_ancestry = if descendant.ancestry == child_ancestry
41
- nil
42
- else
43
- # child_ancestry did not change so child_ancestry_was will work here
44
- descendant.ancestry.gsub(/^#{child_ancestry}\//, '')
45
- end
46
- descendant.update_attribute descendant.class.ancestry_column, new_ancestry
30
+ descendant.update_attribute :ancestor_ids, descendant.ancestor_ids - path_ids
47
31
  end
48
32
  end
49
33
  when :destroy # destroy all descendants if orphan strategy is destroy
@@ -55,14 +39,11 @@ module Ancestry
55
39
  when :adopt # make child elements of this node, child of its parent
56
40
  descendants.each do |descendant|
57
41
  descendant.without_ancestry_callbacks do
58
- new_ancestry = descendant.ancestor_ids.delete_if { |x| x == self.id }.join("/")
59
- # check for empty string if it's then set to nil
60
- new_ancestry = nil if new_ancestry.empty?
61
- descendant.update_attribute descendant.class.ancestry_column, new_ancestry || nil
42
+ descendant.update_attribute :ancestor_ids, (descendant.ancestor_ids.delete_if { |x| x == self.id })
62
43
  end
63
44
  end
64
45
  when :restrict # throw an exception if it has children
65
- raise Ancestry::AncestryException.new('Cannot delete record because it has descendants.') unless is_childless?
46
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.cannot_delete_descendants")) unless is_childless?
66
47
  end
67
48
  end
68
49
  end
@@ -79,67 +60,72 @@ module Ancestry
79
60
  end
80
61
  end
81
62
 
82
- # The ancestry value for this record's children (before save)
83
- # This is technically child_ancestry_was
84
- def child_ancestry
85
- # New records cannot have children
86
- raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
87
-
88
- if self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}").blank?
89
- id.to_s
90
- else
91
- "#{self.send "#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"}/#{id}"
92
- end
63
+ # Counter Cache
64
+ def increase_parent_counter_cache
65
+ self.class.increment_counter _counter_cache_column, parent_id
93
66
  end
94
67
 
95
- # Ancestors
68
+ def decrease_parent_counter_cache
69
+ # @_trigger_destroy_callback comes from activerecord, which makes sure only once decrement when concurrent deletion.
70
+ # but @_trigger_destroy_callback began after rails@5.1.0.alpha.
71
+ # https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/persistence.rb#L340
72
+ # https://github.com/rails/rails/pull/14735
73
+ # https://github.com/rails/rails/pull/27248
74
+ return if defined?(@_trigger_destroy_callback) && !@_trigger_destroy_callback
75
+ return if ancestry_callbacks_disabled?
96
76
 
97
- def ancestors?
98
- # ancestor_ids.present?
99
- read_attribute(self.ancestry_base_class.ancestry_column).present?
77
+ self.class.decrement_counter _counter_cache_column, parent_id
100
78
  end
101
- alias :has_parent? :ancestors?
102
79
 
103
- def ancestry_changed?
104
- changed.include?(self.ancestry_base_class.ancestry_column.to_s)
105
- end
80
+ def update_parent_counter_cache
81
+ changed = saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)
106
82
 
107
- def ancestor_ids
108
- parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
83
+ return unless changed
84
+
85
+ if parent_id_was = parent_id_before_last_save
86
+ self.class.decrement_counter _counter_cache_column, parent_id_was
87
+ end
88
+
89
+ parent_id && self.class.increment_counter(_counter_cache_column, parent_id)
109
90
  end
110
91
 
111
- def ancestor_conditions
112
- self.ancestry_base_class.ancestor_conditions(self)
92
+ def _counter_cache_column
93
+ self.ancestry_base_class.counter_cache_column.to_s
113
94
  end
114
95
 
115
- def ancestors depth_options = {}
116
- self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
96
+ # Ancestors
97
+
98
+ def has_parent?
99
+ ancestor_ids.present?
117
100
  end
101
+ alias :ancestors? :has_parent?
118
102
 
119
- # deprecate
120
- def ancestor_was_conditions
121
- {primary_key_with_table => ancestor_ids_before_last_save}
103
+ def ancestry_changed?
104
+ column = self.ancestry_base_class.ancestry_column.to_s
105
+ # These methods return nil if there are no changes.
106
+ # This was fixed in a refactoring in rails 6.0: https://github.com/rails/rails/pull/35933
107
+ !!(will_save_change_to_attribute?(column) || saved_change_to_attribute?(column))
122
108
  end
123
109
 
124
- # deprecated - probably don't want to use anymore
125
- def ancestor_ids_was
126
- parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}_was"))
110
+ def sane_ancestor_ids?
111
+ valid? || errors[self.ancestry_base_class.ancestry_column].blank?
127
112
  end
128
113
 
129
- def ancestor_ids_before_last_save
130
- parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
114
+ def ancestors depth_options = {}
115
+ return self.ancestry_base_class.none unless has_parent?
116
+ self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
131
117
  end
132
118
 
133
119
  def path_ids
134
120
  ancestor_ids + [id]
135
121
  end
136
122
 
137
- def path_conditions
138
- self.ancestry_base_class.path_conditions(self)
123
+ def path_ids_in_database
124
+ ancestor_ids_in_database + [id]
139
125
  end
140
126
 
141
127
  def path depth_options = {}
142
- self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where path_conditions
128
+ self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
143
129
  end
144
130
 
145
131
  def depth
@@ -159,7 +145,7 @@ module Ancestry
159
145
  # currently parent= does not work in after save callbacks
160
146
  # assuming that parent hasn't changed
161
147
  def parent= parent
162
- write_attribute(self.ancestry_base_class.ancestry_column, if parent.nil? then nil else parent.child_ancestry end)
148
+ self.ancestor_ids = parent ? parent.path_ids : []
163
149
  end
164
150
 
165
151
  def parent_id= new_parent_id
@@ -167,15 +153,12 @@ module Ancestry
167
153
  end
168
154
 
169
155
  def parent_id
170
- ancestor_ids.last if ancestors?
156
+ ancestor_ids.last if has_parent?
171
157
  end
158
+ alias :parent_id? :ancestors?
172
159
 
173
160
  def parent
174
- unscoped_find(parent_id) if ancestors?
175
- end
176
-
177
- def parent_id?
178
- ancestors?
161
+ unscoped_find(parent_id) if has_parent?
179
162
  end
180
163
 
181
164
  def parent_of?(node)
@@ -185,15 +168,15 @@ module Ancestry
185
168
  # Root
186
169
 
187
170
  def root_id
188
- ancestors? ? ancestor_ids.first : id
171
+ has_parent? ? ancestor_ids.first : id
189
172
  end
190
173
 
191
174
  def root
192
- ancestors? ? unscoped_find(root_id) : self
175
+ has_parent? ? unscoped_find(root_id) : self
193
176
  end
194
177
 
195
178
  def is_root?
196
- read_attribute(self.ancestry_base_class.ancestry_column).blank?
179
+ !has_parent?
197
180
  end
198
181
  alias :root? :is_root?
199
182
 
@@ -203,12 +186,8 @@ module Ancestry
203
186
 
204
187
  # Children
205
188
 
206
- def child_conditions
207
- self.ancestry_base_class.child_conditions(self)
208
- end
209
-
210
189
  def children
211
- self.ancestry_base_class.where child_conditions
190
+ self.ancestry_base_class.children_of(self)
212
191
  end
213
192
 
214
193
  def child_ids
@@ -231,14 +210,11 @@ module Ancestry
231
210
 
232
211
  # Siblings
233
212
 
234
- def sibling_conditions
235
- self.ancestry_base_class.sibling_conditions(self)
236
- end
237
-
238
213
  def siblings
239
- self.ancestry_base_class.where sibling_conditions
214
+ self.ancestry_base_class.siblings_of(self)
240
215
  end
241
216
 
217
+ # NOTE: includes self
242
218
  def sibling_ids
243
219
  siblings.pluck(self.ancestry_base_class.primary_key)
244
220
  end
@@ -254,17 +230,13 @@ module Ancestry
254
230
  alias_method :only_child?, :is_only_child?
255
231
 
256
232
  def sibling_of?(node)
257
- self.ancestry == node.ancestry
233
+ self.ancestor_ids == node.ancestor_ids
258
234
  end
259
235
 
260
236
  # Descendants
261
237
 
262
- def descendant_conditions
263
- self.ancestry_base_class.descendant_conditions(self)
264
- end
265
-
266
238
  def descendants depth_options = {}
267
- self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where descendant_conditions
239
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
268
240
  end
269
241
 
270
242
  def descendant_ids depth_options = {}
@@ -277,12 +249,8 @@ module Ancestry
277
249
 
278
250
  # Indirects
279
251
 
280
- def indirect_conditions
281
- self.ancestry_base_class.indirect_conditions(self)
282
- end
283
-
284
252
  def indirects depth_options = {}
285
- self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where indirect_conditions
253
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
286
254
  end
287
255
 
288
256
  def indirect_ids depth_options = {}
@@ -295,12 +263,8 @@ module Ancestry
295
263
 
296
264
  # Subtree
297
265
 
298
- def subtree_conditions
299
- self.ancestry_base_class.subtree_conditions(self)
300
- end
301
-
302
266
  def subtree depth_options = {}
303
- self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where subtree_conditions
267
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
304
268
  end
305
269
 
306
270
  def subtree_ids depth_options = {}
@@ -320,17 +284,9 @@ module Ancestry
320
284
  end
321
285
 
322
286
  private
323
- ANCESTRY_DELIMITER = '/'.freeze
324
-
325
- def parse_ancestry_column obj
326
- return [] unless obj
327
- obj_ids = obj.split(ANCESTRY_DELIMITER)
328
- self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
329
- end
330
-
331
287
  def unscoped_descendants
332
288
  unscoped_where do |scope|
333
- scope.where descendant_conditions
289
+ scope.where self.ancestry_base_class.descendant_conditions(self)
334
290
  end
335
291
  end
336
292