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.
- checksums.yaml +15 -0
- data/README.md +15 -3
- data/lib/closure_tree/acts_as_tree.rb +12 -6
- data/lib/closure_tree/digraphs.rb +31 -0
- data/lib/closure_tree/finders.rb +152 -0
- data/lib/closure_tree/hash_tree.rb +59 -0
- data/lib/closure_tree/hierarchy_maintenance.rb +87 -0
- data/lib/closure_tree/model.rb +0 -283
- data/lib/closure_tree/numeric_deterministic_ordering.rb +2 -3
- data/lib/closure_tree/support.rb +17 -129
- data/lib/closure_tree/support_attributes.rb +99 -0
- data/lib/closure_tree/support_flags.rb +41 -0
- data/lib/closure_tree/version.rb +1 -1
- data/spec/label_spec.rb +0 -1
- data/spec/support/models.rb +6 -7
- data/spec/tag_examples.rb +40 -1
- data/spec/user_spec.rb +14 -4
- metadata +161 -167
- data/lib/closure_tree/with_advisory_lock.rb +0 -28
checksums.yaml
ADDED
@@ -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
|
[](http://travis-ci.org/mceachen/closure_tree)
|
9
9
|
[](http://rubygems.org/gems/closure_tree)
|
10
|
+
[](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::
|
25
|
+
include ClosureTree::Finders
|
26
|
+
include ClosureTree::HashTree
|
27
|
+
include ClosureTree::Digraphs
|
20
28
|
|
21
|
-
if _ct.order_option?
|
22
|
-
|
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
|
data/lib/closure_tree/model.rb
CHANGED
@@ -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
|