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 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.8
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
- :foreign_key => parent_column_name,
60
- :dependent => closure_tree_options[:dependent]
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
- # Note that options[:limit_depth] defaults to 10. This might be crazy-big, depending on your tree shape.
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
- tree = ActiveSupport::OrderedHash.new
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 hash_tree(options = {})
278
- tree = ActiveSupport::OrderedHash.new
279
- tree[self] = ActiveSupport::OrderedHash.new
280
- id_to_hash = {self.id => tree[self]}
281
- scope = descendants
282
- if options[:limit_depth]
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
- tree
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
- ["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
567
- ct_parent_id,
568
- sibling_node.order_value])
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|
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = "3.6.8" unless defined?(::ClosureTree::VERSION)
2
+ VERSION = "3.6.9" unless defined?(::ClosureTree::VERSION)
3
3
  end
@@ -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
@@ -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.8
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-29 00:00:00.000000000 Z
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: 1831255790364540871
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: 1831255790364540871
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