ancestry 3.0.6 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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