closure_tree 4.1.0 → 4.2.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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MWJmNzNmNzk4OGM2YmJlYWIzYjdkNTE2Yzk2YjZlNWM1ZDgwMTlhYg==
5
+ data.tar.gz: !binary |-
6
+ YjcwMmY4MTcwMTRlZmJmODAwOGViNGMyNzU4NGQ5MGY4ZDU1MjQ5Mg==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ NGRlYjk1MDU4NzdiYzQzNzIzZGQxZTZjMjc1NDA4NzQ0YTAwYTExYTZlM2E4
10
+ YWM0OTY4NzQ2ZmI4NjA2NDc4MWNjN2RlODIwNTY0ZWMwZGJmNzUwNjk0ZmE3
11
+ M2NlYmU1MTk5MzViN2ExZmIwYjBmYTU5N2JhMGVmOGE5N2U3NmQ=
12
+ data.tar.gz: !binary |-
13
+ NmE0NGFjNmZiMDAwM2U5MTc3Y2E3OWIyMGFiNTEzYWI3MTA1MmRlYWI5MzQw
14
+ OGMxNGQwODZmY2ZiNzQwZDhmNGUzYTEwNjNkMTZlOTNjZTE5ZTA2NGNhNTg5
15
+ MTJjMjgxODk1NzdmOWQ1NzhhODA1MWZhYjNlYzUyOGUxNmVlYTk=
data/README.md CHANGED
@@ -7,6 +7,7 @@ and tracking user referrals.
7
7
 
8
8
  [![Build Status](https://secure.travis-ci.org/mceachen/closure_tree.png?branch=master)](http://travis-ci.org/mceachen/closure_tree)
9
9
  [![Gem Version](https://badge.fury.io/rb/closure_tree.png)](http://rubygems.org/gems/closure_tree)
10
+ [![Code Climate](https://codeclimate.com/github/mceachen/closure_tree.png)](https://codeclimate.com/github/mceachen/closure_tree)
10
11
 
11
12
  Substantially more efficient than
12
13
  [ancestry](https://github.com/stefankroes/ancestry) and
@@ -156,7 +157,8 @@ Ancestry paths may be built using any column in your model. The default
156
157
  column is ```name```, which can be changed with the :name_column option
157
158
  provided to ```acts_as_tree```.
158
159
 
159
- Note that any other AR fields can be set with the second, optional ```attributes``` argument.
160
+ Note that any other AR fields can be set with the second, optional ```attributes``` argument,
161
+ and as of version 4.2.0, these attributes are added to the where clause as selection criteria.
160
162
 
161
163
  ```ruby
162
164
  child = Tag.find_or_create_by_path(%w{home chuck Photos"}, {:tag_type => "File"})
@@ -252,9 +254,10 @@ When you include ```acts_as_tree``` in your model, you can provide a hash to ove
252
254
  * ```Tag.roots``` returns all root nodes
253
255
  * ```Tag.leaves``` returns all leaf nodes
254
256
  * ```Tag.hash_tree``` returns an [ordered, nested hash](#nested-hashes) that can be depth-limited.
255
- * ```Tag.find_by_path(path)``` returns the node whose name path is ```path```. See (#find_or_create_by_path).
256
- * ```Tag.find_or_create_by_path(path)``` returns the node whose name path is ```path```, and will create the node if it doesn't exist already.See (#find_or_create_by_path).
257
+ * ```Tag.find_by_path(path, attributes)``` returns the node whose name path is ```path```. See (#find_or_create_by_path).
258
+ * ```Tag.find_or_create_by_path(path, attributes)``` returns the node whose name path is ```path```, and will create the node if it doesn't exist already.See (#find_or_create_by_path).
257
259
  * ```Tag.find_all_by_generation(generation_level)``` returns the descendant nodes who are ```generation_level``` away from a root. ```Tag.find_all_by_generation(0)``` is equivalent to ```Tag.roots```.
260
+ * ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose ancestor is in the given list.
258
261
 
259
262
  ### Instance methods
260
263
 
@@ -468,6 +471,15 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
468
471
 
469
472
  ## Change log
470
473
 
474
+ ### 4.2.0
475
+
476
+ * Added ```with_ancestor(*ancestors)```. Thanks for the idea, [Matt](https://github.com/mgornick)!
477
+ * Applied [Leonel Galan](https://github.com/leonelgalan)'s fix for Strong Attribute support
478
+ * ```find_or_create_by``` now uses passed-in attributes as both selection and creation criteria.
479
+ Thanks for the help, [Judd Blair](https://github.com/juddblair)!
480
+ **Please note that this changes prior behavior—test your code with this new version!**
481
+ * ```ct_advisory_lock``` was moved into the ```_ct``` support class, to reduce model method pollution
482
+
471
483
  ### 4.1.0
472
484
 
473
485
  * Added support for Rails 4.0.0.rc1 and Ruby 2.0.0 (while maintaining backward compatibility with Rails 3, BOOYA)
@@ -1,8 +1,12 @@
1
+ require 'with_advisory_lock'
1
2
  require 'closure_tree/support'
3
+ require 'closure_tree/hierarchy_maintenance'
2
4
  require 'closure_tree/model'
5
+ require 'closure_tree/finders'
6
+ require 'closure_tree/hash_tree'
7
+ require 'closure_tree/digraphs'
3
8
  require 'closure_tree/deterministic_ordering'
4
9
  require 'closure_tree/numeric_deterministic_ordering'
5
- require 'closure_tree/with_advisory_lock'
6
10
 
7
11
  module ClosureTree
8
12
  module ActsAsTree
@@ -15,13 +19,15 @@ module ClosureTree
15
19
  class_attribute :hierarchy_class
16
20
  self.hierarchy_class = _ct.hierarchy_class_for_model
17
21
 
22
+ # tests fail if you include Model before HierarchyMaintenance wtf
23
+ include ClosureTree::HierarchyMaintenance
18
24
  include ClosureTree::Model
19
- include ClosureTree::WithAdvisoryLock
25
+ include ClosureTree::Finders
26
+ include ClosureTree::HashTree
27
+ include ClosureTree::Digraphs
20
28
 
21
- if _ct.order_option?
22
- include ClosureTree::DeterministicOrdering
23
- include ClosureTree::DeterministicNumericOrdering if _ct.order_is_numeric?
24
- end
29
+ include ClosureTree::DeterministicOrdering if _ct.order_option?
30
+ include ClosureTree::NumericDeterministicOrdering if _ct.order_is_numeric?
25
31
  end
26
32
  end
27
33
  end
@@ -0,0 +1,31 @@
1
+ module ClosureTree
2
+ module Digraphs
3
+ extend ActiveSupport::Concern
4
+
5
+ def to_dot_digraph
6
+ self.class.to_dot_digraph(self_and_descendants)
7
+ end
8
+
9
+ # override this method in your model class if you want a different digraph label.
10
+ def to_digraph_label
11
+ _ct.has_name? ? read_attribute(_ct.name_column) : to_s
12
+ end
13
+
14
+ module ClassMethods
15
+ # Renders the given scope as a DOT digraph, suitable for rendering by Graphviz
16
+ def to_dot_digraph(tree_scope)
17
+ id_to_instance = tree_scope.inject({}) { |h, ea| h[ea.id] = ea; h }
18
+ output = StringIO.new
19
+ output << "digraph G {\n"
20
+ tree_scope.each do |ea|
21
+ if id_to_instance.has_key? ea._ct_parent_id
22
+ output << " #{ea._ct_parent_id} -> #{ea._ct_id}\n"
23
+ end
24
+ output << " #{ea._ct_id} [label=\"#{ea.to_digraph_label}\"]\n"
25
+ end
26
+ output << "}\n"
27
+ output.string
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,152 @@
1
+ module ClosureTree
2
+ module Finders
3
+ extend ActiveSupport::Concern
4
+
5
+ # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
6
+ def find_by_path(path, attributes = {})
7
+ return self if path.empty?
8
+ self.class.find_by_path(path, attributes, id)
9
+ end
10
+
11
+ # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
12
+ def find_or_create_by_path(path, attributes = {}, find_before_lock = true)
13
+ attributes[:type] ||= self.type if _ct.subclass? && _ct.has_type?
14
+ (find_before_lock && find_by_path(path, attributes)) || begin
15
+ _ct.with_advisory_lock do
16
+ subpath = path.is_a?(Enumerable) ? path.dup : [path]
17
+ child_name = subpath.shift
18
+ return self unless child_name
19
+ child = transaction do
20
+ attrs = attributes.merge(_ct.name_sym => child_name)
21
+ # shenanigans because children.create is bound to the superclass
22
+ # (in the case of polymorphism):
23
+ self.children.where(attrs).first || begin
24
+ self.class.new(attrs).tap { |ea| self.children << ea }
25
+ end
26
+ end
27
+ child.find_or_create_by_path(subpath, attributes, false)
28
+ end
29
+ end
30
+ end
31
+
32
+ def find_all_by_generation(generation_level)
33
+ s = _ct.base_class.joins(<<-SQL)
34
+ INNER JOIN (
35
+ SELECT descendant_id
36
+ FROM #{_ct.quoted_hierarchy_table_name}
37
+ WHERE ancestor_id = #{_ct.quote(self.id)}
38
+ GROUP BY 1
39
+ HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
40
+ ) AS descendants ON (#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} = descendants.descendant_id)
41
+ SQL
42
+ _ct.scope_with_order(s)
43
+ end
44
+
45
+ def without_self(scope)
46
+ scope.without(self)
47
+ end
48
+
49
+ module ClassMethods
50
+
51
+ def without(instance)
52
+ if instance.new_record?
53
+ all
54
+ else
55
+ where(["#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name} != ?", instance.id])
56
+ end
57
+ end
58
+
59
+ def roots
60
+ _ct.scope_with_order(where(_ct.parent_column_name => nil))
61
+ end
62
+
63
+ # Returns an arbitrary node that has no parents.
64
+ def root
65
+ roots.first
66
+ end
67
+
68
+ def leaves
69
+ s = joins(<<-SQL)
70
+ INNER JOIN (
71
+ SELECT ancestor_id
72
+ FROM #{_ct.quoted_hierarchy_table_name}
73
+ GROUP BY 1
74
+ HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = 0
75
+ ) AS leaves ON (#{_ct.quoted_table_name}.#{primary_key} = leaves.ancestor_id)
76
+ SQL
77
+ _ct.scope_with_order(s.readonly(false))
78
+ end
79
+
80
+ def with_ancestor(*ancestors)
81
+ ancestor_ids = ancestors.map { |ea| ea.is_a?(ActiveRecord::Base) ? ea._ct_id : ea }
82
+ scope = ancestor_ids.blank? ? scoped : joins(:ancestor_hierarchies).
83
+ where("#{_ct.hierarchy_table_name}.ancestor_id" => ancestor_ids).
84
+ where("#{_ct.hierarchy_table_name}.generations > 0").
85
+ readonly(false)
86
+ _ct.scope_with_order(scope)
87
+ end
88
+
89
+ def find_all_by_generation(generation_level)
90
+ s = joins(<<-SQL)
91
+ INNER JOIN (
92
+ SELECT #{primary_key} as root_id
93
+ FROM #{_ct.quoted_table_name}
94
+ WHERE #{_ct.quoted_parent_column_name} IS NULL
95
+ ) AS roots ON (1 = 1)
96
+ INNER JOIN (
97
+ SELECT ancestor_id, descendant_id
98
+ FROM #{_ct.quoted_hierarchy_table_name}
99
+ GROUP BY 1, 2
100
+ HAVING MAX(generations) = #{generation_level.to_i}
101
+ ) AS descendants ON (
102
+ #{_ct.quoted_table_name}.#{primary_key} = descendants.descendant_id
103
+ AND roots.root_id = descendants.ancestor_id
104
+ )
105
+ SQL
106
+ _ct.scope_with_order(s)
107
+ end
108
+
109
+ def ct_scoped_attributes(scope, attributes, target_table = table_name)
110
+ attributes.inject(scope) do |scope, pair|
111
+ scope.where("#{target_table}.#{pair.first}" => pair.last)
112
+ end
113
+ end
114
+
115
+ # Find the node whose +ancestry_path+ is +path+
116
+ def find_by_path(path, attributes = {}, parent_id = nil)
117
+ path = path.is_a?(Enumerable) ? path.dup : [path]
118
+ scope = where(_ct.name_sym => path.pop).readonly(false)
119
+ scope = ct_scoped_attributes(scope, attributes)
120
+ last_joined_table = _ct.table_name
121
+ path.reverse.each_with_index do |ea, idx|
122
+ next_joined_table = "p#{idx}"
123
+ scope = scope.joins(<<-SQL)
124
+ INNER JOIN #{_ct.quoted_table_name} AS #{next_joined_table}
125
+ ON #{next_joined_table}.#{_ct.quoted_id_column_name} =
126
+ #{connection.quote_table_name(last_joined_table)}.#{_ct.quoted_parent_column_name}
127
+ SQL
128
+ scope = scope.where("#{next_joined_table}.#{_ct.name_column}" => ea)
129
+ scope = ct_scoped_attributes(scope, attributes, next_joined_table)
130
+ last_joined_table = next_joined_table
131
+ end
132
+ scope = scope.where("#{last_joined_table}.#{_ct.parent_column_name}" => parent_id)
133
+ scope.first
134
+ end
135
+
136
+ # Find or create nodes such that the +ancestry_path+ is +path+
137
+ def find_or_create_by_path(path, attributes = {})
138
+ find_by_path(path, attributes) || begin
139
+ subpath = path.dup
140
+ root_name = subpath.shift
141
+ _ct.with_advisory_lock do
142
+ # shenanigans because find_or_create can't infer that we want the same class as this:
143
+ # Note that roots will already be constrained to this subclass (in the case of polymorphism):
144
+ attrs = attributes.merge(_ct.name_sym => root_name)
145
+ root = roots.where(attrs).first || roots.create!(attrs)
146
+ root.find_or_create_by_path(subpath, attributes)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,59 @@
1
+ module ClosureTree
2
+ module HashTree
3
+ extend ActiveSupport::Concern
4
+
5
+ def hash_tree_scope(limit_depth = nil)
6
+ scope = self_and_descendants
7
+ if limit_depth
8
+ scope.where("#{_ct.quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
9
+ else
10
+ scope
11
+ end
12
+ end
13
+
14
+ def hash_tree(options = {})
15
+ self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ # There is no default depth limit. This might be crazy-big, depending
21
+ # on your tree shape. Hash huge trees at your own peril!
22
+ def hash_tree(options = {})
23
+ build_hash_tree(hash_tree_scope(options[:limit_depth]))
24
+ end
25
+
26
+ def hash_tree_scope(limit_depth = nil)
27
+ # Deepest generation, within limit, for each descendant
28
+ # NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
29
+ having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
30
+ generation_depth = <<-SQL
31
+ INNER JOIN (
32
+ SELECT descendant_id, MAX(generations) as depth
33
+ FROM #{_ct.quoted_hierarchy_table_name}
34
+ GROUP BY descendant_id
35
+ #{having_clause}
36
+ ) AS generation_depth
37
+ ON #{_ct.quoted_table_name}.#{primary_key} = generation_depth.descendant_id
38
+ SQL
39
+ _ct.scope_with_order(joins(generation_depth), "generation_depth.depth")
40
+ end
41
+
42
+ # Builds nested hash structure using the scope returned from the passed in scope
43
+ def build_hash_tree(tree_scope)
44
+ tree = ActiveSupport::OrderedHash.new
45
+ id_to_hash = {}
46
+
47
+ tree_scope.each do |ea|
48
+ h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
49
+ if ea.root? || tree.empty? # We're at the top of the tree.
50
+ tree[ea] = h
51
+ else
52
+ id_to_hash[ea._ct_parent_id][ea] = h
53
+ end
54
+ end
55
+ tree
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,87 @@
1
+ require 'active_support/concern'
2
+
3
+ module ClosureTree
4
+ module HierarchyMaintenance
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ validate :_ct_validate
9
+ before_save :_ct_before_save
10
+ after_save :_ct_after_save
11
+ before_destroy :_ct_before_destroy
12
+ end
13
+
14
+ def _ct_validate
15
+ if changes[_ct.parent_column_name] &&
16
+ parent.present? &&
17
+ parent.self_and_ancestors.include?(self)
18
+ errors.add(_ct.parent_column_sym, "You cannot add an ancestor as a descendant")
19
+ end
20
+ end
21
+
22
+ def _ct_before_save
23
+ @was_new_record = new_record?
24
+ true # don't cancel the save
25
+ end
26
+
27
+ def _ct_after_save
28
+ rebuild! if changes[_ct.parent_column_name] || @was_new_record
29
+ @was_new_record = false # we aren't new anymore.
30
+ true # don't cancel anything.
31
+ end
32
+
33
+ def _ct_before_destroy
34
+ delete_hierarchy_references
35
+ if _ct.options[:dependent] == :nullify
36
+ self.class.find(self.id).children.each { |c| c.rebuild! }
37
+ end
38
+ true # don't prevent destruction
39
+ end
40
+
41
+ def rebuild!
42
+ _ct.with_advisory_lock do
43
+ delete_hierarchy_references unless @was_new_record
44
+ hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
45
+ unless root?
46
+ _ct.connection.execute <<-SQL
47
+ INSERT INTO #{_ct.quoted_hierarchy_table_name}
48
+ (ancestor_id, descendant_id, generations)
49
+ SELECT x.ancestor_id, #{_ct.quote(_ct_id)}, x.generations + 1
50
+ FROM #{_ct.quoted_hierarchy_table_name} x
51
+ WHERE x.descendant_id = #{_ct.quote(_ct_parent_id)}
52
+ SQL
53
+ end
54
+ children.each { |c| c.rebuild! }
55
+ end
56
+ end
57
+
58
+ def delete_hierarchy_references
59
+ # The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
60
+ # It shouldn't affect performance of postgresql.
61
+ # See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
62
+ # Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
63
+ _ct.connection.execute <<-SQL
64
+ DELETE FROM #{_ct.quoted_hierarchy_table_name}
65
+ WHERE descendant_id IN (
66
+ SELECT DISTINCT descendant_id
67
+ FROM (SELECT descendant_id
68
+ FROM #{_ct.quoted_hierarchy_table_name}
69
+ WHERE ancestor_id = #{_ct.quote(id)}
70
+ ) AS x )
71
+ OR descendant_id = #{_ct.quote(id)}
72
+ SQL
73
+ end
74
+
75
+ module ClassMethods
76
+ # Rebuilds the hierarchy table based on the parent_id column in the database.
77
+ # Note that the hierarchy table will be truncated.
78
+ def rebuild!
79
+ _ct.with_advisory_lock do
80
+ hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
81
+ roots.each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
82
+ end
83
+ nil
84
+ end
85
+ end
86
+ end
87
+ end
@@ -5,11 +5,6 @@ module ClosureTree
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- validate :_ct_validate
9
- before_save :_ct_before_save
10
- after_save :_ct_after_save
11
- before_destroy :_ct_before_destroy
12
-
13
8
  belongs_to :parent,
14
9
  :class_name => _ct.model_class.to_s,
15
10
  :foreign_key => _ct.parent_column_name
@@ -45,14 +40,6 @@ module ClosureTree
45
40
  :through => :descendant_hierarchies,
46
41
  :source => :descendant,
47
42
  :order => order_by_generations)
48
-
49
- scope :without, lambda { |instance|
50
- if instance.new_record?
51
- all
52
- else
53
- where(["#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} != ?", instance.id])
54
- end
55
- }
56
43
  end
57
44
 
58
45
  # Delegate to the Support instance on the class:
@@ -136,65 +123,6 @@ module ClosureTree
136
123
  child_node
137
124
  end
138
125
 
139
- # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
140
- def find_by_path(path)
141
- return self if path.empty?
142
- parent_constraint = "#{_ct.quoted_parent_column_name} = #{_ct.quote(id)}"
143
- self.class.ct_scoped_to_path(path, parent_constraint).first
144
- end
145
-
146
- # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
147
- def find_or_create_by_path(path, attributes = {}, find_before_lock = true)
148
- (find_before_lock && find_by_path(path)) || begin
149
- ct_with_advisory_lock do
150
- subpath = path.is_a?(Enumerable) ? path.dup : [path]
151
- child_name = subpath.shift
152
- return self unless child_name
153
- child = transaction do
154
- attrs = {_ct.name_sym => child_name}
155
- attrs[:type] = self.type if _ct.subclass? && _ct.has_type?
156
- self.children.where(attrs).first || begin
157
- child = self.class.new(attributes.merge(attrs))
158
- self.children << child
159
- child
160
- end
161
- end
162
- child.find_or_create_by_path(subpath, attributes, false)
163
- end
164
- end
165
- end
166
-
167
- def find_all_by_generation(generation_level)
168
- s = _ct.base_class.joins(<<-SQL)
169
- INNER JOIN (
170
- SELECT descendant_id
171
- FROM #{_ct.quoted_hierarchy_table_name}
172
- WHERE ancestor_id = #{_ct.quote(self.id)}
173
- GROUP BY 1
174
- HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
175
- ) AS descendants ON (#{_ct.quoted_table_name}.#{_ct.base_class.primary_key} = descendants.descendant_id)
176
- SQL
177
- _ct.scope_with_order(s)
178
- end
179
-
180
- def hash_tree_scope(limit_depth = nil)
181
- scope = self_and_descendants
182
- if limit_depth
183
- scope.where("#{_ct.quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
184
- else
185
- scope
186
- end
187
- end
188
-
189
- def hash_tree(options = {})
190
- self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
191
- end
192
-
193
- # override this method in your model class if you want a different digraph label.
194
- def to_digraph_label
195
- _ct.has_name? ? read_attribute(_ct.name_column) : to_s
196
- end
197
-
198
126
  def _ct_parent_id
199
127
  read_attribute(_ct.parent_column_sym)
200
128
  end
@@ -202,216 +130,5 @@ module ClosureTree
202
130
  def _ct_id
203
131
  read_attribute(_ct.model_class.primary_key)
204
132
  end
205
-
206
- def _ct_validate
207
- if changes[_ct.parent_column_name] &&
208
- parent.present? &&
209
- parent.self_and_ancestors.include?(self)
210
- errors.add(_ct.parent_column_sym, "You cannot add an ancestor as a descendant")
211
- end
212
- end
213
-
214
- def _ct_before_save
215
- @was_new_record = new_record?
216
- true # don't cancel the save
217
- end
218
-
219
- def _ct_after_save
220
- rebuild! if changes[_ct.parent_column_name] || @was_new_record
221
- @was_new_record = false # we aren't new anymore.
222
- true # don't cancel anything.
223
- end
224
-
225
- def rebuild!
226
- ct_with_advisory_lock do
227
- delete_hierarchy_references unless @was_new_record
228
- hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
229
- unless root?
230
- sql = <<-SQL
231
- INSERT INTO #{_ct.quoted_hierarchy_table_name}
232
- (ancestor_id, descendant_id, generations)
233
- SELECT x.ancestor_id, #{_ct.quote(_ct_id)}, x.generations + 1
234
- FROM #{_ct.quoted_hierarchy_table_name} x
235
- WHERE x.descendant_id = #{_ct.quote(_ct_parent_id)}
236
- SQL
237
- _ct.connection.execute sql.strip
238
- end
239
- children.each { |c| c.rebuild! }
240
- end
241
- end
242
-
243
- def _ct_before_destroy
244
- delete_hierarchy_references
245
- if _ct.options[:dependent] == :nullify
246
- children.each { |c| c.rebuild! }
247
- end
248
- end
249
-
250
- def delete_hierarchy_references
251
- # The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
252
- # It shouldn't affect performance of postgresql.
253
- # See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
254
- # Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
255
- _ct.connection.execute <<-SQL
256
- DELETE FROM #{_ct.quoted_hierarchy_table_name}
257
- WHERE descendant_id IN (
258
- SELECT DISTINCT descendant_id
259
- FROM (SELECT descendant_id
260
- FROM #{_ct.quoted_hierarchy_table_name}
261
- WHERE ancestor_id = #{_ct.quote(id)}
262
- ) AS x )
263
- OR descendant_id = #{_ct.quote(id)}
264
- SQL
265
- end
266
-
267
- def without_self(scope)
268
- scope.without(self)
269
- end
270
-
271
- def to_dot_digraph
272
- self.class.to_dot_digraph(self_and_descendants)
273
- end
274
-
275
- module ClassMethods
276
- def roots
277
- _ct.scope_with_order(where(_ct.parent_column_name => nil))
278
- end
279
-
280
- # Returns an arbitrary node that has no parents.
281
- def root
282
- roots.first
283
- end
284
-
285
- # There is no default depth limit. This might be crazy-big, depending
286
- # on your tree shape. Hash huge trees at your own peril!
287
- def hash_tree(options = {})
288
- build_hash_tree(hash_tree_scope(options[:limit_depth]))
289
- end
290
-
291
- def leaves
292
- s = joins(<<-SQL)
293
- INNER JOIN (
294
- SELECT ancestor_id
295
- FROM #{_ct.quoted_hierarchy_table_name}
296
- GROUP BY 1
297
- HAVING MAX(#{_ct.quoted_hierarchy_table_name}.generations) = 0
298
- ) AS leaves ON (#{_ct.quoted_table_name}.#{primary_key} = leaves.ancestor_id)
299
- SQL
300
- _ct.scope_with_order(s.readonly(false))
301
- end
302
-
303
- # Rebuilds the hierarchy table based on the parent_id column in the database.
304
- # Note that the hierarchy table will be truncated.
305
- def rebuild!
306
- ct_with_advisory_lock do
307
- hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
308
- roots.each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
309
- end
310
- nil
311
- end
312
-
313
- def find_all_by_generation(generation_level)
314
- s = joins(<<-SQL)
315
- INNER JOIN (
316
- SELECT #{primary_key} as root_id
317
- FROM #{_ct.quoted_table_name}
318
- WHERE #{_ct.quoted_parent_column_name} IS NULL
319
- ) AS roots ON (1 = 1)
320
- INNER JOIN (
321
- SELECT ancestor_id, descendant_id
322
- FROM #{_ct.quoted_hierarchy_table_name}
323
- GROUP BY 1, 2
324
- HAVING MAX(generations) = #{generation_level.to_i}
325
- ) AS descendants ON (
326
- #{_ct.quoted_table_name}.#{primary_key} = descendants.descendant_id
327
- AND roots.root_id = descendants.ancestor_id
328
- )
329
- SQL
330
- _ct.scope_with_order(s)
331
- end
332
-
333
- # Find the node whose +ancestry_path+ is +path+
334
- def find_by_path(path)
335
- parent_constraint = "#{_ct.quoted_parent_column_name} IS NULL"
336
- ct_scoped_to_path(path, parent_constraint).first
337
- end
338
-
339
- def ct_scoped_to_path(path, parent_constraint)
340
- path = path.is_a?(Enumerable) ? path.dup : [path]
341
- scope = where(_ct.name_sym => path.last).readonly(false)
342
- path[0..-2].reverse.each_with_index do |ea, idx|
343
- subtable = idx == 0 ? _ct.quoted_table_name : "p#{idx - 1}"
344
- scope = scope.joins(<<-SQL)
345
- INNER JOIN #{_ct.quoted_table_name} AS p#{idx}
346
- ON p#{idx}.#{_ct.quoted_id_column_name} = #{subtable}.#{_ct.parent_column_name}
347
- SQL
348
- scope = scope.where("p#{idx}.#{_ct.quoted_name_column} = #{_ct.quote(ea)}")
349
- end
350
- root_table_name = path.size > 1 ? "p#{path.size - 2}" : _ct.quoted_table_name
351
- scope.where("#{root_table_name}.#{parent_constraint}")
352
- end
353
-
354
- # Find or create nodes such that the +ancestry_path+ is +path+
355
- def find_or_create_by_path(path, attributes = {})
356
- find_by_path(path) || begin
357
- subpath = path.dup
358
- root_name = subpath.shift
359
- ct_with_advisory_lock do
360
- # shenanigans because find_or_create can't infer we want the same class as this:
361
- # Note that roots will already be constrained to this subclass (in the case of polymorphism):
362
- root = roots.where(_ct.name_sym => root_name).first
363
- root ||= create!(attributes.merge(_ct.name_sym => root_name))
364
- root.find_or_create_by_path(subpath, attributes)
365
- end
366
- end
367
- end
368
-
369
- def hash_tree_scope(limit_depth = nil)
370
- # Deepest generation, within limit, for each descendant
371
- # NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
372
- having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
373
- generation_depth = <<-SQL
374
- INNER JOIN (
375
- SELECT descendant_id, MAX(generations) as depth
376
- FROM #{_ct.quoted_hierarchy_table_name}
377
- GROUP BY descendant_id
378
- #{having_clause}
379
- ) AS generation_depth
380
- ON #{_ct.quoted_table_name}.#{primary_key} = generation_depth.descendant_id
381
- SQL
382
- _ct.scope_with_order(joins(generation_depth), "generation_depth.depth")
383
- end
384
-
385
- # Builds nested hash structure using the scope returned from the passed in scope
386
- def build_hash_tree(tree_scope)
387
- tree = ActiveSupport::OrderedHash.new
388
- id_to_hash = {}
389
-
390
- tree_scope.each do |ea|
391
- h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
392
- if ea.root? || tree.empty? # We're at the top of the tree.
393
- tree[ea] = h
394
- else
395
- id_to_hash[ea._ct_parent_id][ea] = h
396
- end
397
- end
398
- tree
399
- end
400
-
401
- # Renders the given scope as a DOT digraph, suitable for rendering by Graphviz
402
- def to_dot_digraph(tree_scope)
403
- id_to_instance = tree_scope.inject({}) { |h, ea| h[ea.id] = ea; h }
404
- output = StringIO.new
405
- output << "digraph G {\n"
406
- tree_scope.each do |ea|
407
- if id_to_instance.has_key? ea._ct_parent_id
408
- output << " #{ea._ct_parent_id} -> #{ea._ct_id}\n"
409
- end
410
- output << " #{ea._ct_id} [label=\"#{ea.to_digraph_label}\"]\n"
411
- end
412
- output << "}\n"
413
- output.string
414
- end
415
- end
416
133
  end
417
134
  end