ancestry 3.0.5 → 3.2.1

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.
@@ -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|
@@ -203,7 +227,7 @@ module Ancestry
203
227
  end
204
228
 
205
229
  ANCESTRY_UNCAST_TYPES = [:string, :uuid, :text].freeze
206
- if ActiveSupport::VERSION::STRING < "4.0"
230
+ if ActiveSupport::VERSION::STRING < "4.2"
207
231
  def primary_key_is_an_integer?
208
232
  if defined?(@primary_key_is_an_integer)
209
233
  @primary_key_is_an_integer
@@ -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,10 +60,27 @@ 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
@@ -100,6 +99,18 @@ module Ancestry
100
99
  return super if defined?(super)
101
100
  has_ancestry(*args)
102
101
  end
102
+
103
+ private
104
+
105
+ def derive_ancestry_pattern(primary_key_format, delimiter = '/')
106
+ primary_key_format ||= '[0-9]+'
107
+
108
+ if primary_key_format.to_s.include?('\A')
109
+ primary_key_format
110
+ else
111
+ /\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
112
+ end
113
+ end
103
114
  end
104
115
  end
105
116
 
@@ -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,81 @@ 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 =
82
+ if ActiveRecord::VERSION::STRING >= '5.1.0'
83
+ saved_change_to_attribute?(self.ancestry_base_class.ancestry_column)
84
+ else
85
+ ancestry_changed?
86
+ end
106
87
 
107
- def ancestor_ids
108
- parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
88
+ return unless changed
89
+
90
+ if parent_id_was = parent_id_before_last_save
91
+ self.class.decrement_counter _counter_cache_column, parent_id_was
92
+ end
93
+
94
+ parent_id && self.class.increment_counter(_counter_cache_column, parent_id)
109
95
  end
110
96
 
111
- def ancestor_conditions
112
- self.ancestry_base_class.ancestor_conditions(self)
97
+ def _counter_cache_column
98
+ self.ancestry_base_class.counter_cache_column.to_s
113
99
  end
114
100
 
115
- def ancestors depth_options = {}
116
- self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
101
+ # Ancestors
102
+
103
+ def ancestors?
104
+ ancestor_ids.present?
117
105
  end
106
+ alias :has_parent? :ancestors?
118
107
 
119
- # deprecate
120
- def ancestor_was_conditions
121
- {primary_key_with_table => ancestor_ids_before_last_save}
108
+ def ancestry_changed?
109
+ column = self.ancestry_base_class.ancestry_column.to_s
110
+ if ActiveRecord::VERSION::STRING >= '5.1.0'
111
+ # These methods return nil if there are no changes.
112
+ # This was fixed in a refactoring in rails 6.0: https://github.com/rails/rails/pull/35933
113
+ !!(will_save_change_to_attribute?(column) || saved_change_to_attribute?(column))
114
+ else
115
+ changed.include?(column)
116
+ end
122
117
  end
123
118
 
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"))
119
+ def sane_ancestor_ids?
120
+ valid? || errors[self.ancestry_base_class.ancestry_column].blank?
127
121
  end
128
122
 
129
- def ancestor_ids_before_last_save
130
- parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
123
+ def ancestors depth_options = {}
124
+ return self.ancestry_base_class.none unless ancestors?
125
+ self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
131
126
  end
132
127
 
133
128
  def path_ids
134
129
  ancestor_ids + [id]
135
130
  end
136
131
 
137
- def path_conditions
138
- self.ancestry_base_class.path_conditions(self)
132
+ def path_ids_in_database
133
+ ancestor_ids_in_database + [id]
139
134
  end
140
135
 
141
136
  def path depth_options = {}
142
- self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where path_conditions
137
+ self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
143
138
  end
144
139
 
145
140
  def depth
@@ -159,7 +154,7 @@ module Ancestry
159
154
  # currently parent= does not work in after save callbacks
160
155
  # assuming that parent hasn't changed
161
156
  def parent= parent
162
- write_attribute(self.ancestry_base_class.ancestry_column, if parent.nil? then nil else parent.child_ancestry end)
157
+ self.ancestor_ids = parent ? parent.path_ids : []
163
158
  end
164
159
 
165
160
  def parent_id= new_parent_id
@@ -169,15 +164,12 @@ module Ancestry
169
164
  def parent_id
170
165
  ancestor_ids.last if ancestors?
171
166
  end
167
+ alias :parent_id? :ancestors?
172
168
 
173
169
  def parent
174
170
  unscoped_find(parent_id) if ancestors?
175
171
  end
176
172
 
177
- def parent_id?
178
- ancestors?
179
- end
180
-
181
173
  def parent_of?(node)
182
174
  self.id == node.parent_id
183
175
  end
@@ -193,7 +185,7 @@ module Ancestry
193
185
  end
194
186
 
195
187
  def is_root?
196
- read_attribute(self.ancestry_base_class.ancestry_column).blank?
188
+ !ancestors?
197
189
  end
198
190
  alias :root? :is_root?
199
191
 
@@ -203,12 +195,8 @@ module Ancestry
203
195
 
204
196
  # Children
205
197
 
206
- def child_conditions
207
- self.ancestry_base_class.child_conditions(self)
208
- end
209
-
210
198
  def children
211
- self.ancestry_base_class.where child_conditions
199
+ self.ancestry_base_class.children_of(self)
212
200
  end
213
201
 
214
202
  def child_ids
@@ -231,14 +219,11 @@ module Ancestry
231
219
 
232
220
  # Siblings
233
221
 
234
- def sibling_conditions
235
- self.ancestry_base_class.sibling_conditions(self)
236
- end
237
-
238
222
  def siblings
239
- self.ancestry_base_class.where sibling_conditions
223
+ self.ancestry_base_class.siblings_of(self)
240
224
  end
241
225
 
226
+ # NOTE: includes self
242
227
  def sibling_ids
243
228
  siblings.pluck(self.ancestry_base_class.primary_key)
244
229
  end
@@ -254,17 +239,13 @@ module Ancestry
254
239
  alias_method :only_child?, :is_only_child?
255
240
 
256
241
  def sibling_of?(node)
257
- self.ancestry == node.ancestry
242
+ self.ancestor_ids == node.ancestor_ids
258
243
  end
259
244
 
260
245
  # Descendants
261
246
 
262
- def descendant_conditions
263
- self.ancestry_base_class.descendant_conditions(self)
264
- end
265
-
266
247
  def descendants depth_options = {}
267
- self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where descendant_conditions
248
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
268
249
  end
269
250
 
270
251
  def descendant_ids depth_options = {}
@@ -277,12 +258,8 @@ module Ancestry
277
258
 
278
259
  # Indirects
279
260
 
280
- def indirect_conditions
281
- self.ancestry_base_class.indirect_conditions(self)
282
- end
283
-
284
261
  def indirects depth_options = {}
285
- self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where indirect_conditions
262
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
286
263
  end
287
264
 
288
265
  def indirect_ids depth_options = {}
@@ -295,12 +272,8 @@ module Ancestry
295
272
 
296
273
  # Subtree
297
274
 
298
- def subtree_conditions
299
- self.ancestry_base_class.subtree_conditions(self)
300
- end
301
-
302
275
  def subtree depth_options = {}
303
- self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).where subtree_conditions
276
+ self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
304
277
  end
305
278
 
306
279
  def subtree_ids depth_options = {}
@@ -320,17 +293,9 @@ module Ancestry
320
293
  end
321
294
 
322
295
  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
296
  def unscoped_descendants
332
297
  unscoped_where do |scope|
333
- scope.where descendant_conditions
298
+ scope.where self.ancestry_base_class.descendant_conditions(self)
334
299
  end
335
300
  end
336
301