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 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