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 +8 -3
- data/lib/closure_tree/columns.rb +8 -0
- data/lib/closure_tree/model.rb +26 -19
- data/lib/closure_tree/version.rb +1 -1
- data/spec/tag_spec.rb +75 -37
- metadata +4 -4
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
|
17
|
-
* Get all your siblings
|
18
|
-
* Fetch all [7-degrees-of-bacon in a nested hash](#nested-hashes)
|
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
|
data/lib/closure_tree/columns.rb
CHANGED
@@ -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
|
data/lib/closure_tree/model.rb
CHANGED
@@ -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
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
326
|
-
|
327
|
-
|
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+
|
data/lib/closure_tree/version.rb
CHANGED
data/spec/tag_spec.rb
CHANGED
@@ -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.
|
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-
|
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:
|
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:
|
260
|
+
hash: 625197628069455012
|
261
261
|
requirements: []
|
262
262
|
rubyforge_project:
|
263
263
|
rubygems_version: 1.8.23
|