closure_tree 3.8.1 → 3.8.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -13,9 +13,10 @@ closure_tree has some great features:
13
13
 
14
14
  * __Best-in-class select performance__:
15
15
  * Fetch your whole ancestor lineage in 1 SELECT.
16
- * Grab all your descendants: 1 SELECT.
17
- * Get all your siblings: 1 SELECT.
18
- * Fetch all [7-degrees-of-bacon in a nested hash](#nested-hashes): 1 SELECT.
16
+ * Grab all your descendants in 1 SELECT.
17
+ * Get all your siblings in 1 SELECT.
18
+ * Fetch all [7-degrees-of-bacon in a nested hash](#nested-hashes) in 1 SELECT.
19
+ * [Find a node by path](#find_or_create_by_path) in 1 SELECT.
19
20
  * __Best-in-class mutation performance__:
20
21
  * 2 SQL INSERTs on node creation
21
22
  * 3 SQL INSERT/UPDATEs on node reparenting
@@ -425,6 +426,10 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
425
426
 
426
427
  ## Change log
427
428
 
429
+ ### 3.8.2
430
+
431
+ * find_by_path uses 1 SELECT now. BOOM.
432
+
428
433
  ### 3.8.1
429
434
 
430
435
  * Double-check locking for find_or_create_by_path
@@ -45,6 +45,14 @@ module ClosureTree
45
45
  connection.quote_column_name parent_column_name
46
46
  end
47
47
 
48
+ def quoted_name_column
49
+ connection.quote_column_name name_column
50
+ end
51
+
52
+ def ct_quote(field)
53
+ connection.quote(field)
54
+ end
55
+
48
56
  def order_option
49
57
  closure_tree_options[:order]
50
58
  end
@@ -124,12 +124,9 @@ module ClosureTree
124
124
 
125
125
  # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
126
126
  def find_by_path(path)
127
- path = path.is_a?(Enumerable) ? path.dup : [path]
128
- node = self
129
- while !path.empty? && node
130
- node = node.children.where(name_sym => path.shift).first
131
- end
132
- node
127
+ return self if path.empty?
128
+ parent_constraint = "#{quoted_parent_column_name} = #{ct_quote(id)}"
129
+ ct_class.ct_scoped_to_path(path, parent_constraint).first
133
130
  end
134
131
 
135
132
  # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
@@ -207,13 +204,14 @@ module ClosureTree
207
204
  delete_hierarchy_references unless @was_new_record
208
205
  hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
209
206
  unless root?
210
- connection.execute <<-SQL
211
- INSERT INTO #{quoted_hierarchy_table_name}
212
- (ancestor_id, descendant_id, generations)
213
- SELECT x.ancestor_id, #{ct_quote(id)}, x.generations + 1
214
- FROM #{quoted_hierarchy_table_name} x
215
- WHERE x.descendant_id = #{ct_quote(self.ct_parent_id)}
207
+ sql = <<-SQL
208
+ INSERT INTO #{quoted_hierarchy_table_name}
209
+ (ancestor_id, descendant_id, generations)
210
+ SELECT x.ancestor_id, #{ct_quote(id)}, x.generations + 1
211
+ FROM #{quoted_hierarchy_table_name} x
212
+ WHERE x.descendant_id = #{ct_quote(self.ct_parent_id)}
216
213
  SQL
214
+ connection.execute sql.strip
217
215
  end
218
216
  children.each { |c| c.rebuild! }
219
217
  end
@@ -255,10 +253,6 @@ module ClosureTree
255
253
  end
256
254
  end
257
255
 
258
- def ct_quote(field)
259
- self.class.connection.quote(field)
260
- end
261
-
262
256
  # TODO: _parent_id will be removed in the next major version
263
257
  alias :_parent_id :ct_parent_id
264
258
 
@@ -322,9 +316,22 @@ module ClosureTree
322
316
 
323
317
  # Find the node whose +ancestry_path+ is +path+
324
318
  def find_by_path(path)
325
- subpath = path.dup
326
- root = roots.where(name_sym => subpath.shift).first
327
- root.find_by_path(subpath) if root
319
+ parent_constraint = "#{quoted_parent_column_name} IS NULL"
320
+ ct_scoped_to_path(path, parent_constraint).first
321
+ end
322
+
323
+ def ct_scoped_to_path(path, parent_constraint)
324
+ path = path.is_a?(Enumerable) ? path.dup : [path]
325
+ scope = scoped.where(name_sym => path.last).readonly(false)
326
+ path[0..-2].reverse.each_with_index do |ea, idx|
327
+ subtable = idx == 0 ? quoted_table_name : "p#{idx - 1}"
328
+ scope = scope.joins(<<-SQL)
329
+ INNER JOIN #{quoted_table_name} AS p#{idx} ON p#{idx}.id = #{subtable}.#{parent_column_name}
330
+ SQL
331
+ scope = scope.where("p#{idx}.#{quoted_name_column} = #{ct_quote(ea)}")
332
+ end
333
+ root_table_name = path.size > 1 ? "p#{path.size - 2}" : quoted_table_name
334
+ scope.where("#{root_table_name}.#{parent_constraint}")
328
335
  end
329
336
 
330
337
  # Find or create nodes such that the +ancestry_path+ is +path+
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = "3.8.1" unless defined?(::ClosureTree::VERSION)
2
+ VERSION = "3.8.2" unless defined?(::ClosureTree::VERSION)
3
3
  end
@@ -146,6 +146,81 @@ shared_examples_for "Tag (1)" do
146
146
  end
147
147
 
148
148
  end
149
+
150
+ context "paths" do
151
+ before :each do
152
+ @child = Tag.find_or_create_by_path(%w(grandparent parent child))
153
+ @child.title = "Kid"
154
+ @parent = @child.parent
155
+ @parent.title = "Mom"
156
+ @grandparent = @parent.parent
157
+ @grandparent.title = "Nonnie"
158
+ [@child, @parent, @grandparent].each { |ea| ea.save! }
159
+ end
160
+
161
+ it "should build ancestry path" do
162
+ @child.ancestry_path.should == %w{grandparent parent child}
163
+ @child.ancestry_path(:name).should == %w{grandparent parent child}
164
+ @child.ancestry_path(:title).should == %w{Nonnie Mom Kid}
165
+ end
166
+
167
+ it "should find by path" do
168
+ # class method:
169
+ Tag.find_by_path(%w{grandparent parent child}).should == @child
170
+ # instance method:
171
+ @parent.find_by_path(%w{child}).should == @child
172
+ @grandparent.find_by_path(%w{parent child}).should == @child
173
+ @parent.find_by_path(%w{child larvae}).should be_nil
174
+ end
175
+
176
+ it "finds correctly rooted paths" do
177
+ decoy = Tag.find_or_create_by_path %w(a b c d)
178
+ b_d = Tag.find_or_create_by_path %w(b c d)
179
+ Tag.find_by_path(%w(b c d)).should == b_d
180
+ Tag.find_by_path(%w(c d)).should be_nil
181
+ end
182
+
183
+ it "find_by_path for 1 node" do
184
+ b = Tag.find_or_create_by_path %w(a b)
185
+ b2 = b.root.find_by_path(%w(b))
186
+ b2.should == b
187
+ end
188
+
189
+ it "find_by_path for 2 nodes" do
190
+ c = Tag.find_or_create_by_path %w(a b c)
191
+ c.root.find_by_path(%w(b c)).should == c
192
+ c.root.find_by_path(%w(a c)).should be_nil
193
+ c.root.find_by_path(%w(c)).should be_nil
194
+ end
195
+
196
+ it "find_by_path for 3 nodes" do
197
+ d = Tag.find_or_create_by_path %w(a b c d)
198
+ d.root.find_by_path(%w(b c d)).should == d
199
+ Tag.find_by_path(%w(a b c d)).should == d
200
+ Tag.find_by_path(%w(d)).should be_nil
201
+ end
202
+
203
+ it "should return nil for missing nodes" do
204
+ Tag.find_by_path(%w{missing}).should be_nil
205
+ Tag.find_by_path(%w{grandparent missing}).should be_nil
206
+ Tag.find_by_path(%w{grandparent parent missing}).should be_nil
207
+ Tag.find_by_path(%w{grandparent parent missing child}).should be_nil
208
+ end
209
+
210
+ it "should find or create by path" do
211
+ # class method:
212
+ grandparent = Tag.find_or_create_by_path(%w{grandparent})
213
+ grandparent.should == @grandparent
214
+ child = Tag.find_or_create_by_path(%w{grandparent parent child})
215
+ child.should == @child
216
+ Tag.find_or_create_by_path(%w{events anniversary}).ancestry_path.should == %w{events anniversary}
217
+ a = Tag.find_or_create_by_path(%w{a})
218
+ a.ancestry_path.should == %w{a}
219
+ # instance method:
220
+ a.find_or_create_by_path(%w{b c}).ancestry_path.should == %w{a b c}
221
+ end
222
+ end
223
+
149
224
  end
150
225
 
151
226
  shared_examples_for "Tag (2)" do
@@ -316,43 +391,6 @@ shared_examples_for "Tag (2)" do
316
391
  end
317
392
  end
318
393
 
319
- context "paths" do
320
-
321
- it "should build ancestry path" do
322
- tags(:child).ancestry_path.should == %w{grandparent parent child}
323
- tags(:child).ancestry_path(:name).should == %w{grandparent parent child}
324
- tags(:child).ancestry_path(:title).should == %w{Nonnie Mom Kid}
325
- end
326
-
327
- it "should find by path" do
328
- # class method:
329
- Tag.find_by_path(%w{grandparent parent child}).should == tags(:child)
330
- # instance method:
331
- tags(:parent).find_by_path(%w{child}).should == tags(:child)
332
- tags(:grandparent).find_by_path(%w{parent child}).should == tags(:child)
333
- tags(:parent).find_by_path(%w{child larvae}).should be_nil
334
- end
335
-
336
- it "should return nil for missing nodes" do
337
- Tag.find_by_path(%w{missing}).should be_nil
338
- Tag.find_by_path(%w{grandparent missing}).should be_nil
339
- Tag.find_by_path(%w{grandparent parent missing}).should be_nil
340
- Tag.find_by_path(%w{grandparent parent missing child}).should be_nil
341
- end
342
-
343
- it "should find or create by path" do
344
- # class method:
345
- grandparent = Tag.find_or_create_by_path(%w{grandparent})
346
- grandparent.should == tags(:grandparent)
347
- child = Tag.find_or_create_by_path(%w{grandparent parent child})
348
- child.should == tags(:child)
349
- Tag.find_or_create_by_path(%w{events anniversary}).ancestry_path.should == %w{events anniversary}
350
- a = Tag.find_or_create_by_path(%w{a})
351
- a.ancestry_path.should == %w{a}
352
- # instance method:
353
- a.find_or_create_by_path(%w{b c}).ancestry_path.should == %w{a b c}
354
- end
355
- end
356
394
 
357
395
  def validate_city_tag city
358
396
  tags(:california).children.include?(city).should_not be_nil
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.8.1
4
+ version: 3.8.2
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: 2013-02-25 00:00:00.000000000 Z
12
+ date: 2013-03-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -248,7 +248,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
248
248
  version: '0'
249
249
  segments:
250
250
  - 0
251
- hash: -3183043301926645290
251
+ hash: 625197628069455012
252
252
  required_rubygems_version: !ruby/object:Gem::Requirement
253
253
  none: false
254
254
  requirements:
@@ -257,7 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
257
257
  version: '0'
258
258
  segments:
259
259
  - 0
260
- hash: -3183043301926645290
260
+ hash: 625197628069455012
261
261
  requirements: []
262
262
  rubyforge_project:
263
263
  rubygems_version: 1.8.23