closure_tree 3.6.8 → 3.6.9
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.
- 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
|