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.
- 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
|
[![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::
|
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
|