closure_tree 3.8.1 → 3.8.2
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 -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
|