closure_tree 4.1.0 → 4.2.0

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