closure_tree 3.6.8 → 3.6.9
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +8 -2
- data/lib/closure_tree/acts_as_tree.rb +50 -50
- data/lib/closure_tree/version.rb +1 -1
- data/spec/hash_tree_spec.rb +91 -0
- data/spec/tag_spec.rb +0 -22
- metadata +6 -4
data/README.md
CHANGED
@@ -203,6 +203,11 @@ b.hash_tree(:limit_depth => 2)
|
|
203
203
|
=> {b => {c1 => {}, c2 => {}}}
|
204
204
|
```
|
205
205
|
|
206
|
+
**If your tree is large (or might become so), use :limit_depth.**
|
207
|
+
|
208
|
+
Without this option, ```hash_tree``` will load the entire contents of that table into RAM. Your
|
209
|
+
server may not be happy trying to do this.
|
210
|
+
|
206
211
|
HT: [ancestry](https://github.com/stefankroes/ancestry#arrangement) and [elhoyos](https://github.com/mceachen/closure_tree/issues/11)
|
207
212
|
|
208
213
|
### <a id="options"></a>Available options
|
@@ -367,10 +372,11 @@ Closure tree is [tested under every combination](http://travis-ci.org/#!/mceache
|
|
367
372
|
|
368
373
|
## Change log
|
369
374
|
|
370
|
-
### 3.6.
|
375
|
+
### 3.6.9
|
371
376
|
|
372
377
|
* [Don Morrison](https://github.com/elskwid) massaged the [#hash_tree](#nested-hashes) query to
|
373
|
-
be more efficient
|
378
|
+
be more efficient, and found a bug in ```hash_tree```'s query that resulted in duplicate rows,
|
379
|
+
wasting time on the ruby side.
|
374
380
|
|
375
381
|
### 3.6.7
|
376
382
|
|
@@ -56,8 +56,8 @@ module ClosureTree
|
|
56
56
|
|
57
57
|
has_many :children, with_order_option(
|
58
58
|
:class_name => ct_class.to_s,
|
59
|
-
|
60
|
-
|
59
|
+
:foreign_key => parent_column_name,
|
60
|
+
:dependent => closure_tree_options[:dependent]
|
61
61
|
)
|
62
62
|
|
63
63
|
has_many :ancestor_hierarchies,
|
@@ -88,39 +88,10 @@ module ClosureTree
|
|
88
88
|
where(parent_column_name => nil)
|
89
89
|
end
|
90
90
|
|
91
|
-
#
|
91
|
+
# There is no default depth limit. This might be crazy-big, depending
|
92
|
+
# on your tree shape. Hash huge trees at your own peril!
|
92
93
|
def self.hash_tree(options = {})
|
93
|
-
|
94
|
-
id_to_hash = {}
|
95
|
-
limit_depth = (options[:limit_depth] || 10).to_i
|
96
|
-
|
97
|
-
# Simple join with hierarchy for ancestor, descendant, and generation
|
98
|
-
scope = joins(:ancestor_hierarchies)
|
99
|
-
|
100
|
-
# Deepest generation, within limit, for each descendant
|
101
|
-
scope = scope.joins(<<-SQL)
|
102
|
-
INNER JOIN (
|
103
|
-
SELECT
|
104
|
-
#{quoted_hierarchy_table_name}.descendant_id,
|
105
|
-
MAX(#{quoted_hierarchy_table_name}.generations) AS depth
|
106
|
-
FROM #{quoted_hierarchy_table_name}
|
107
|
-
GROUP BY #{quoted_hierarchy_table_name}.descendant_id
|
108
|
-
HAVING MAX(#{quoted_hierarchy_table_name}.generations) <= #{limit_depth - 1}
|
109
|
-
) AS generation_depth
|
110
|
-
ON #{quoted_hierarchy_table_name}.descendant_id = generation_depth.descendant_id
|
111
|
-
SQL
|
112
|
-
|
113
|
-
scope = scope.order(append_order("generation_depth.depth"))
|
114
|
-
|
115
|
-
scope.each do |ea|
|
116
|
-
h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
|
117
|
-
if ea.root?
|
118
|
-
tree[ea] = h
|
119
|
-
else
|
120
|
-
id_to_hash[ea.ct_parent_id][ea] = h
|
121
|
-
end
|
122
|
-
end
|
123
|
-
tree
|
94
|
+
build_hash_tree(hash_tree_scope(options[:limit_depth]))
|
124
95
|
end
|
125
96
|
|
126
97
|
def find_all_by_generation(generation_level)
|
@@ -274,20 +245,17 @@ module ClosureTree
|
|
274
245
|
order_option ? s.order(order_option) : s
|
275
246
|
end
|
276
247
|
|
277
|
-
def
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
limit_depth = options[:limit_depth]
|
284
|
-
return {} if limit_depth <= 0
|
285
|
-
scope = scope.where("generations <= #{limit_depth - 1}")
|
286
|
-
end
|
287
|
-
scope.each do |ea|
|
288
|
-
id_to_hash[ea.ct_parent_id][ea] = (id_to_hash[ea.id] = ActiveSupport::OrderedHash.new)
|
248
|
+
def hash_tree_scope(limit_depth = nil)
|
249
|
+
scope = self_and_descendants
|
250
|
+
if limit_depth
|
251
|
+
scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
|
252
|
+
else
|
253
|
+
scope
|
289
254
|
end
|
290
|
-
|
255
|
+
end
|
256
|
+
|
257
|
+
def hash_tree(options = {})
|
258
|
+
self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
|
291
259
|
end
|
292
260
|
|
293
261
|
def ct_parent_id
|
@@ -401,6 +369,38 @@ module ClosureTree
|
|
401
369
|
end
|
402
370
|
root.find_or_create_by_path(path, attributes)
|
403
371
|
end
|
372
|
+
|
373
|
+
def hash_tree_scope(limit_depth = nil)
|
374
|
+
# Deepest generation, within limit, for each descendant
|
375
|
+
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
|
376
|
+
generation_depth = <<-SQL
|
377
|
+
INNER JOIN (
|
378
|
+
SELECT descendant_id, MAX(generations) as depth
|
379
|
+
FROM #{quoted_hierarchy_table_name}
|
380
|
+
GROUP BY descendant_id
|
381
|
+
#{"HAVING MAX(generations) <= #{limit_depth - 1}" if limit_depth}
|
382
|
+
) AS generation_depth
|
383
|
+
ON #{quoted_table_name}.#{primary_key} = generation_depth.descendant_id
|
384
|
+
SQL
|
385
|
+
scoped.joins(generation_depth).order(append_order("generation_depth.depth"))
|
386
|
+
end
|
387
|
+
|
388
|
+
# Builds nested hash structure using the scope returned from the passed in scope
|
389
|
+
def build_hash_tree(tree_scope)
|
390
|
+
tree = ActiveSupport::OrderedHash.new
|
391
|
+
id_to_hash = {}
|
392
|
+
|
393
|
+
tree_scope.each do |ea|
|
394
|
+
h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
|
395
|
+
if ea.root? || tree.empty? # We're at the top of the tree.
|
396
|
+
tree[ea] = h
|
397
|
+
else
|
398
|
+
id_to_hash[ea.ct_parent_id][ea] = h
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
tree
|
403
|
+
end
|
404
404
|
end
|
405
405
|
end
|
406
406
|
|
@@ -563,9 +563,9 @@ module ClosureTree
|
|
563
563
|
# issue 21: we have to use the base class, so STI doesn't get in the way of only updating the child class instances:
|
564
564
|
ct_base_class.update_all(
|
565
565
|
["#{col} = #{col} #{add_after ? '+' : '-'} 1", "updated_at = now()"],
|
566
|
-
|
567
|
-
|
568
|
-
|
566
|
+
["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
|
567
|
+
ct_parent_id,
|
568
|
+
sibling_node.order_value])
|
569
569
|
else
|
570
570
|
last_value = sibling_node.order_value.to_i
|
571
571
|
(add_after ? siblings_after : siblings_before.reverse).each do |ea|
|
data/lib/closure_tree/version.rb
CHANGED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Tag do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@b = Tag.find_or_create_by_path %w(a b)
|
7
|
+
@a = @b.parent
|
8
|
+
@b2 = Tag.find_or_create_by_path %w(a b2)
|
9
|
+
@d1 = @b.find_or_create_by_path %w(c1 d1)
|
10
|
+
@c1 = @d1.parent
|
11
|
+
@d2 = @b.find_or_create_by_path %w(c2 d2)
|
12
|
+
@c2 = @d2.parent
|
13
|
+
@full_tree = {@a => {@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}, @b2 => {}}}
|
14
|
+
end
|
15
|
+
|
16
|
+
context "#hash_tree" do
|
17
|
+
it "returns {} for depth 0" do
|
18
|
+
Tag.hash_tree(:limit_depth => 0).should == {}
|
19
|
+
end
|
20
|
+
it "limit_depth 1" do
|
21
|
+
Tag.hash_tree(:limit_depth => 1).should == {@a => {}}
|
22
|
+
end
|
23
|
+
it "limit_depth 2" do
|
24
|
+
Tag.hash_tree(:limit_depth => 2).should == {@a => {@b => {}, @b2 => {}}}
|
25
|
+
end
|
26
|
+
it "limit_depth 3" do
|
27
|
+
Tag.hash_tree(:limit_depth => 3).should == {@a => {@b => {@c1 => {}, @c2 => {}}, @b2 => {}}}
|
28
|
+
end
|
29
|
+
it "limit_depth 4" do
|
30
|
+
Tag.hash_tree(:limit_depth => 4).should == @full_tree
|
31
|
+
end
|
32
|
+
it "no limit holdum" do
|
33
|
+
Tag.hash_tree.should == @full_tree
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def assert_no_dupes(scope)
|
38
|
+
# the named scope is complicated enough that an incorrect join could result in unnecessarily
|
39
|
+
# duplicated rows:
|
40
|
+
a = scope.collect { |ea| ea.id }
|
41
|
+
a.should == a.uniq
|
42
|
+
end
|
43
|
+
|
44
|
+
context "#hash_tree_scope" do
|
45
|
+
it "no dupes for any depth" do
|
46
|
+
(0..5).each do |ea|
|
47
|
+
assert_no_dupes(Tag.hash_tree_scope(ea))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
it "no limit holdum" do
|
51
|
+
assert_no_dupes(Tag.hash_tree_scope)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context ".hash_tree_scope" do
|
56
|
+
it "no dupes for any depth" do
|
57
|
+
(0..5).each do |ea|
|
58
|
+
assert_no_dupes(@a.hash_tree_scope(ea))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
it "no limit holdum" do
|
62
|
+
assert_no_dupes(@a.hash_tree_scope)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context ".hash_tree" do
|
67
|
+
before :each do
|
68
|
+
end
|
69
|
+
it "returns {} for depth 0" do
|
70
|
+
@b.hash_tree(:limit_depth => 0).should == {}
|
71
|
+
end
|
72
|
+
it "limit_depth 1" do
|
73
|
+
@b.hash_tree(:limit_depth => 1).should == {@b => {}}
|
74
|
+
end
|
75
|
+
it "limit_depth 2" do
|
76
|
+
@b.hash_tree(:limit_depth => 2).should == {@b => {@c1 => {}, @c2 => {}}}
|
77
|
+
end
|
78
|
+
it "limit_depth 3" do
|
79
|
+
@b.hash_tree(:limit_depth => 3).should == {@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}}
|
80
|
+
end
|
81
|
+
it "no limit holdum from subsubroot" do
|
82
|
+
@c1.hash_tree.should == {@c1 => {@d1 => {}}}
|
83
|
+
end
|
84
|
+
it "no limit holdum from subroot" do
|
85
|
+
@b.hash_tree.should == {@b => {@c1 => {@d1 => {}}, @c2 => {@d2 => {}}}}
|
86
|
+
end
|
87
|
+
it "no limit holdum from root" do
|
88
|
+
@a.hash_tree.should == @full_tree
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/spec/tag_spec.rb
CHANGED
@@ -124,28 +124,6 @@ shared_examples_for Tag do
|
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
127
|
-
it "builds hash_trees properly" do
|
128
|
-
b = Tag.find_or_create_by_path %w(a b)
|
129
|
-
a = b.parent
|
130
|
-
b2 = Tag.find_or_create_by_path %w(a b2)
|
131
|
-
d1 = b.find_or_create_by_path %w(c1 d1)
|
132
|
-
c1 = d1.parent
|
133
|
-
d2 = b.find_or_create_by_path %w(c2 d2)
|
134
|
-
c2 = d2.parent
|
135
|
-
Tag.hash_tree(:limit_depth => 0).should == {}
|
136
|
-
Tag.hash_tree(:limit_depth => 1).should == {a => {}}
|
137
|
-
Tag.hash_tree(:limit_depth => 2).should == {a => {b => {}, b2 => {}}}
|
138
|
-
tree = {a => {b => {c1 => {d1 => {}}, c2 => {d2 => {}}}, b2 => {}}}
|
139
|
-
Tag.hash_tree(:limit_depth => 4).should == tree
|
140
|
-
Tag.hash_tree.should == tree
|
141
|
-
b.hash_tree(:limit_depth => 0).should == {}
|
142
|
-
b.hash_tree(:limit_depth => 1).should == {b => {}}
|
143
|
-
b.hash_tree(:limit_depth => 2).should == {b => {c1 => {}, c2 => {}}}
|
144
|
-
b_tree = {b => {c1 => {d1 => {}}, c2 => {d2 => {}}}}
|
145
|
-
b.hash_tree(:limit_depth => 3).should == b_tree
|
146
|
-
b.hash_tree.should == b_tree
|
147
|
-
end
|
148
|
-
|
149
127
|
it "performs as the readme says it does" do
|
150
128
|
grandparent = Tag.create(:name => 'Grandparent')
|
151
129
|
parent = grandparent.children.create(:name => 'Parent')
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: closure_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.6.
|
4
|
+
version: 3.6.9
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-12-
|
12
|
+
date: 2012-12-31 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -189,6 +189,7 @@ files:
|
|
189
189
|
- spec/db/schema.rb
|
190
190
|
- spec/fixtures/labels.yml
|
191
191
|
- spec/fixtures/tags.yml
|
192
|
+
- spec/hash_tree_spec.rb
|
192
193
|
- spec/label_spec.rb
|
193
194
|
- spec/spec_helper.rb
|
194
195
|
- spec/support/models.rb
|
@@ -208,7 +209,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
208
209
|
version: '0'
|
209
210
|
segments:
|
210
211
|
- 0
|
211
|
-
hash:
|
212
|
+
hash: -2239773870557793017
|
212
213
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
213
214
|
none: false
|
214
215
|
requirements:
|
@@ -217,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
217
218
|
version: '0'
|
218
219
|
segments:
|
219
220
|
- 0
|
220
|
-
hash:
|
221
|
+
hash: -2239773870557793017
|
221
222
|
requirements: []
|
222
223
|
rubyforge_project:
|
223
224
|
rubygems_version: 1.8.23
|
@@ -230,6 +231,7 @@ test_files:
|
|
230
231
|
- spec/db/schema.rb
|
231
232
|
- spec/fixtures/labels.yml
|
232
233
|
- spec/fixtures/tags.yml
|
234
|
+
- spec/hash_tree_spec.rb
|
233
235
|
- spec/label_spec.rb
|
234
236
|
- spec/spec_helper.rb
|
235
237
|
- spec/support/models.rb
|