closure_tree 4.5.0 → 4.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.travis.yml +11 -3
- data/CHANGELOG.md +13 -0
- data/Gemfile +13 -0
- data/README.md +61 -12
- data/closure_tree.gemspec +4 -5
- data/gemfiles/activerecord_3.2.gemfile +12 -0
- data/gemfiles/activerecord_4.0.gemfile +12 -0
- data/gemfiles/activerecord_4.1.gemfile +12 -0
- data/gemfiles/activerecord_edge.gemfile +12 -0
- data/lib/closure_tree/acts_as_tree.rb +2 -9
- data/lib/closure_tree/deterministic_ordering.rb +4 -0
- data/lib/closure_tree/finders.rb +4 -4
- data/lib/closure_tree/hash_tree.rb +1 -1
- data/lib/closure_tree/hierarchy_maintenance.rb +19 -6
- data/lib/closure_tree/model.rb +2 -10
- data/lib/closure_tree/numeric_deterministic_ordering.rb +53 -36
- data/lib/closure_tree/numeric_order_support.rb +20 -13
- data/lib/closure_tree/support.rb +8 -0
- data/lib/closure_tree/support_attributes.rb +10 -0
- data/lib/closure_tree/test/matcher.rb +85 -0
- data/lib/closure_tree/version.rb +1 -1
- data/lib/closure_tree.rb +14 -1
- data/spec/cache_invalidation_spec.rb +39 -0
- data/spec/{support → db}/models.rb +6 -0
- data/spec/db/schema.rb +18 -0
- data/spec/label_spec.rb +171 -46
- data/spec/matcher_spec.rb +32 -0
- data/spec/parallel_spec.rb +85 -49
- data/spec/spec_helper.rb +6 -96
- data/spec/support/database.rb +49 -0
- data/spec/support/database_cleaner.rb +14 -0
- data/spec/support/deprecated/attr_accessible.rb +5 -0
- data/spec/support/hash_monkey_patch.rb +13 -0
- data/spec/support/helpers.rb +8 -0
- data/spec/support/sqlite3_with_advisory_lock.rb +10 -0
- data/tests.sh +7 -2
- metadata +31 -43
- data/spec/parallel_prepend_sibling_spec.rb +0 -42
@@ -13,49 +13,56 @@ module ClosureTree
|
|
13
13
|
end
|
14
14
|
|
15
15
|
module MysqlAdapter
|
16
|
-
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil
|
16
|
+
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
17
17
|
min_where = if minimum_sort_order_value
|
18
18
|
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
|
19
19
|
else
|
20
20
|
""
|
21
21
|
end
|
22
|
-
connection.execute
|
23
|
-
connection.execute <<-SQL
|
22
|
+
connection.execute 'SET @i = 0'
|
23
|
+
connection.execute <<-SQL.strip_heredoc
|
24
24
|
UPDATE #{quoted_table_name}
|
25
|
-
SET #{quoted_order_column} = (@i := @i + 1) + #{minimum_sort_order_value.to_i
|
26
|
-
WHERE #{
|
27
|
-
ORDER BY #{
|
25
|
+
SET #{quoted_order_column} = (@i := @i + 1) + #{minimum_sort_order_value.to_i - 1}
|
26
|
+
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}
|
27
|
+
ORDER BY #{nulls_last_order_by}
|
28
28
|
SQL
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
module PostgreSQLAdapter
|
33
|
-
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil
|
33
|
+
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
34
34
|
min_where = if minimum_sort_order_value
|
35
35
|
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
|
36
36
|
else
|
37
37
|
""
|
38
38
|
end
|
39
|
-
connection.execute <<-SQL
|
39
|
+
connection.execute <<-SQL.strip_heredoc
|
40
40
|
UPDATE #{quoted_table_name}
|
41
|
-
SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i
|
41
|
+
SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i - 1}
|
42
42
|
FROM (
|
43
43
|
SELECT #{quoted_id_column_name} AS id, row_number() OVER(ORDER BY #{order_by}) AS seq
|
44
44
|
FROM #{quoted_table_name}
|
45
|
-
WHERE #{
|
45
|
+
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}
|
46
|
+
) AS t
|
46
47
|
WHERE #{quoted_table_name}.#{quoted_id_column_name} = t.id
|
47
48
|
SQL
|
48
49
|
end
|
50
|
+
|
51
|
+
def rows_updated(result)
|
52
|
+
result.cmd_status.sub(/\AUPDATE /, '').to_i
|
53
|
+
end
|
49
54
|
end
|
50
55
|
|
51
56
|
module GenericAdapter
|
52
|
-
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil
|
53
|
-
scope = model_class.
|
57
|
+
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
58
|
+
scope = model_class.
|
59
|
+
where(parent_column_sym => parent_id).
|
60
|
+
order(nulls_last_order_by)
|
54
61
|
if minimum_sort_order_value
|
55
62
|
scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}")
|
56
63
|
end
|
57
64
|
scope.each_with_index do |ea, idx|
|
58
|
-
ea.
|
65
|
+
ea.update_order_value(idx + minimum_sort_order_value.to_i)
|
59
66
|
end
|
60
67
|
end
|
61
68
|
end
|
data/lib/closure_tree/support.rb
CHANGED
@@ -106,6 +106,14 @@ module ClosureTree
|
|
106
106
|
scope.pluck(model_class.primary_key)
|
107
107
|
end
|
108
108
|
|
109
|
+
def where_eq(column_name, value)
|
110
|
+
if value.nil?
|
111
|
+
"#{connection.quote_column_name(column_name)} IS NULL"
|
112
|
+
else
|
113
|
+
"#{connection.quote_column_name(column_name)} = #{quoted_value(value)}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
109
117
|
def with_advisory_lock(&block)
|
110
118
|
if options[:with_advisory_lock]
|
111
119
|
model_class.with_advisory_lock("closure_tree") do
|
@@ -73,6 +73,16 @@ module ClosureTree
|
|
73
73
|
options[:order]
|
74
74
|
end
|
75
75
|
|
76
|
+
def nulls_last_order_by
|
77
|
+
"-#{quoted_order_column} #{order_by_order(reverse = true)}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def order_by_order(reverse = false)
|
81
|
+
desc = !!(order_by.to_s =~ /DESC\z/)
|
82
|
+
desc = !desc if reverse
|
83
|
+
desc ? 'DESC' : 'ASC'
|
84
|
+
end
|
85
|
+
|
76
86
|
def order_column
|
77
87
|
o = order_by
|
78
88
|
if o.nil?
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module ClosureTree
|
2
|
+
module Test
|
3
|
+
module Matcher
|
4
|
+
def be_a_closure_tree
|
5
|
+
ClosureTree.new
|
6
|
+
end
|
7
|
+
|
8
|
+
class ClosureTree
|
9
|
+
def matches?(subject)
|
10
|
+
@subject = subject
|
11
|
+
# OPTIMIZE
|
12
|
+
if @subject.respond_to?(:_ct)
|
13
|
+
|
14
|
+
unless @subject.column_names.include?(@subject._ct.parent_column_name)
|
15
|
+
@message = "expected #{@subject.class.name} to respond to #{@subject._ct.parent_column_name}"
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
|
19
|
+
# Checking if hierarchy table exists (common error)
|
20
|
+
unless @subject.hierarchy_class.table_exists?
|
21
|
+
@message = "expected #{@subject.class.name}'s hierarchy table '#{@subject.hierarchy_class.table_name}' to exist"
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
|
25
|
+
if @ordered
|
26
|
+
unless @subject._ct.options.include?(:order)
|
27
|
+
@message = "expected #{@subject.class.name} to be an ordered closure tree"
|
28
|
+
return false
|
29
|
+
end
|
30
|
+
unless @subject.column_names.include?(@subject._ct.options[:order].to_s)
|
31
|
+
@message = "expected #{@subject.class.name} to have #{@subject._ct.options[:order]} as column"
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if @with_advisory_lock && !@subject._ct.options[:with_advisory_lock]
|
37
|
+
@message = "expected #{@subject.class.name} to have advisory lock"
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
|
41
|
+
if @without_advisory_lock && @subject._ct.options[:with_advisory_lock]
|
42
|
+
@message = "expected #{@subject.class.name} to not have advisory lock"
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
|
46
|
+
return true
|
47
|
+
end
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def ordered(column = nil)
|
52
|
+
@ordered = 'n ordered'
|
53
|
+
@order_colum = column
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def with_advisory_lock
|
58
|
+
@with_advisory_lock = ' with advisory lock'
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def without_advisory_lock
|
63
|
+
@without_advisory_lock = ' without advisory lock'
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def failure_message
|
68
|
+
@message || "expected #{@subject.class.name} to #{description}"
|
69
|
+
end
|
70
|
+
|
71
|
+
alias_method :failure_message_for_should, :failure_message
|
72
|
+
|
73
|
+
def failure_message_when_negated
|
74
|
+
"expected #{@subject.class.name} not be a closure tree, but it is."
|
75
|
+
end
|
76
|
+
|
77
|
+
alias_method :failure_message_for_should_not, :failure_message_when_negated
|
78
|
+
|
79
|
+
def description
|
80
|
+
"be a#{@ordered} closure tree#{@with_advisory_lock}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/closure_tree/version.rb
CHANGED
data/lib/closure_tree.rb
CHANGED
@@ -1,6 +1,19 @@
|
|
1
1
|
require 'active_support'
|
2
2
|
|
3
|
+
module ClosureTree
|
4
|
+
extend ActiveSupport::Autoload
|
5
|
+
|
6
|
+
autoload :ActsAsTree
|
7
|
+
autoload :Support
|
8
|
+
autoload :HierarchyMaintenance
|
9
|
+
autoload :Model
|
10
|
+
autoload :Finders
|
11
|
+
autoload :HashTree
|
12
|
+
autoload :Digraphs
|
13
|
+
autoload :DeterministicOrdering
|
14
|
+
autoload :NumericDeterministicOrdering
|
15
|
+
end
|
16
|
+
|
3
17
|
ActiveSupport.on_load :active_record do
|
4
|
-
require 'closure_tree/acts_as_tree'
|
5
18
|
ActiveRecord::Base.send :extend, ClosureTree::ActsAsTree
|
6
19
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
describe 'cache invalidation', cache: true do
|
5
|
+
before do
|
6
|
+
Timecop.travel(10.seconds.ago) do
|
7
|
+
#create a long tree with 2 branch
|
8
|
+
@root = MenuItem.create(
|
9
|
+
name: SecureRandom.hex(10)
|
10
|
+
)
|
11
|
+
2.times do
|
12
|
+
parent = @root
|
13
|
+
10.times do
|
14
|
+
parent = parent.children.create(
|
15
|
+
name: SecureRandom.hex(10)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
@first_leaf = MenuItem.leaves.first
|
20
|
+
@second_leaf = MenuItem.leaves.last
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'touch option' do
|
25
|
+
it 'should invalidate cache for all it ancestors' do
|
26
|
+
old_time_stamp = @first_leaf.ancestors.pluck(:updated_at)
|
27
|
+
@first_leaf.touch
|
28
|
+
new_time_stamp = @first_leaf.ancestors.pluck(:updated_at)
|
29
|
+
expect(old_time_stamp).to_not eq(new_time_stamp)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should not invalidate cache for another branch' do
|
33
|
+
old_time_stamp = @second_leaf.updated_at
|
34
|
+
@first_leaf.touch
|
35
|
+
new_time_stamp = @second_leaf.updated_at
|
36
|
+
expect(old_time_stamp).to eq(new_time_stamp)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -4,9 +4,11 @@ class Tag < ActiveRecord::Base
|
|
4
4
|
acts_as_tree :dependent => :destroy, :order => :name
|
5
5
|
before_destroy :add_destroyed_tag
|
6
6
|
attr_accessible :name, :title if _ct.use_attr_accessible?
|
7
|
+
|
7
8
|
def to_s
|
8
9
|
name
|
9
10
|
end
|
11
|
+
|
10
12
|
def add_destroyed_tag
|
11
13
|
# Proof for the tests that the destroy rather than the delete method was called:
|
12
14
|
DestroyedTag.create(:name => name)
|
@@ -99,3 +101,7 @@ class Metal < ActiveRecord::Base
|
|
99
101
|
acts_as_tree :order => 'sort_order'
|
100
102
|
self.inheritance_column = 'metal_type'
|
101
103
|
end
|
104
|
+
|
105
|
+
class MenuItem < ActiveRecord::Base
|
106
|
+
acts_as_tree(touch: true, with_advisory_lock: false)
|
107
|
+
end
|
data/spec/db/schema.rb
CHANGED
@@ -124,4 +124,22 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
124
124
|
|
125
125
|
add_foreign_key(:metal_hierarchies, :metal, :column => 'ancestor_id')
|
126
126
|
add_foreign_key(:metal_hierarchies, :metal, :column => 'descendant_id')
|
127
|
+
|
128
|
+
create_table 'menu_items' do |t|
|
129
|
+
t.string 'name'
|
130
|
+
t.integer 'parent_id'
|
131
|
+
t.timestamps
|
132
|
+
end
|
133
|
+
|
134
|
+
add_foreign_key(:menu_items, :menu_items, :column => 'parent_id')
|
135
|
+
|
136
|
+
create_table 'menu_item_hierarchies', :id => false do |t|
|
137
|
+
t.integer 'ancestor_id', :null => false
|
138
|
+
t.integer 'descendant_id', :null => false
|
139
|
+
t.integer 'generations', :null => false
|
140
|
+
end
|
141
|
+
|
142
|
+
add_foreign_key(:menu_item_hierarchies, :menu_items, :column => 'ancestor_id')
|
143
|
+
add_foreign_key(:menu_item_hierarchies, :menu_items, :column => 'descendant_id')
|
144
|
+
|
127
145
|
end
|
data/spec/label_spec.rb
CHANGED
@@ -44,12 +44,36 @@ end
|
|
44
44
|
describe Label do
|
45
45
|
|
46
46
|
context "destruction" do
|
47
|
-
it "properly destroys descendents" do
|
47
|
+
it "properly destroys descendents created with find_or_create_by_path" do
|
48
48
|
c = Label.find_or_create_by_path %w(a b c)
|
49
49
|
b = c.parent
|
50
50
|
a = c.root
|
51
51
|
a.destroy
|
52
|
-
Label.exists?(id: [a.id,b.id,c.id]).should be_false
|
52
|
+
Label.exists?(id: [a.id, b.id, c.id]).should be_false
|
53
|
+
end
|
54
|
+
|
55
|
+
it "properly destroys descendents created with add_child" do
|
56
|
+
a = Label.create(name: 'a')
|
57
|
+
b = Label.new(name: 'b')
|
58
|
+
a.add_child b
|
59
|
+
c = Label.new(name: 'c')
|
60
|
+
b.add_child c
|
61
|
+
a.destroy
|
62
|
+
Label.exists?(a).should be_false
|
63
|
+
Label.exists?(b).should be_false
|
64
|
+
Label.exists?(c).should be_false
|
65
|
+
end
|
66
|
+
|
67
|
+
it "properly destroys descendents created with <<" do
|
68
|
+
a = Label.create(name: 'a')
|
69
|
+
b = Label.new(name: 'b')
|
70
|
+
a.children << b
|
71
|
+
c = Label.new(name: 'c')
|
72
|
+
b.children << c
|
73
|
+
a.destroy
|
74
|
+
Label.exists?(a).should be_false
|
75
|
+
Label.exists?(b).should be_false
|
76
|
+
Label.exists?(c).should be_false
|
53
77
|
end
|
54
78
|
end
|
55
79
|
|
@@ -131,7 +155,7 @@ describe Label do
|
|
131
155
|
end
|
132
156
|
|
133
157
|
it "returns descendents regardless of subclass" do
|
134
|
-
Label.root.descendants.map{|ea|ea.class.to_s}.uniq.should =~
|
158
|
+
Label.root.descendants.map { |ea| ea.class.to_s }.uniq.should =~
|
135
159
|
%w(Label DateLabel DirectoryLabel EventLabel)
|
136
160
|
end
|
137
161
|
end
|
@@ -199,57 +223,79 @@ describe Label do
|
|
199
223
|
end
|
200
224
|
|
201
225
|
it "self_and_descendants should result in one select" do
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
226
|
+
count_queries do
|
227
|
+
a1_array = @a1.self_and_descendants
|
228
|
+
a1_array.collect { |ea| ea.name }.should == %w(a1 b1 c1 c2 d1 d2)
|
229
|
+
end.should == 1
|
206
230
|
end
|
207
231
|
|
208
232
|
it "self_and_ancestors should result in one select" do
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
233
|
+
count_queries do
|
234
|
+
d1_array = @d1.self_and_ancestors
|
235
|
+
d1_array.collect { |ea| ea.name }.should == %w(d1 c1 b1 a1)
|
236
|
+
end.should == 1
|
213
237
|
end
|
214
238
|
end
|
215
239
|
|
216
240
|
context "deterministically orders with polymorphic siblings" do
|
217
241
|
before :each do
|
218
|
-
@parent = Label.create!(:name =>
|
219
|
-
@a = EventLabel.new(:name =>
|
220
|
-
@b = DirectoryLabel.new(:name => "b")
|
221
|
-
@c = DateLabel.new(:name => "c")
|
222
|
-
@d = Label.new(:name => "d")
|
242
|
+
@parent = Label.create!(:name => 'parent')
|
243
|
+
@a, @b, @c, @d, @e, @f = ('a'..'f').map { |ea| EventLabel.new(:name => ea) }
|
223
244
|
@parent.children << @a
|
224
245
|
@a.append_sibling(@b)
|
225
246
|
@b.append_sibling(@c)
|
226
247
|
@c.append_sibling(@d)
|
248
|
+
@parent.append_sibling(@e)
|
249
|
+
@e.append_sibling(@f)
|
250
|
+
end
|
251
|
+
|
252
|
+
def name_and_order(enum)
|
253
|
+
enum.map { |ea| [ea.name, ea.sort_order] }
|
227
254
|
end
|
228
255
|
|
229
256
|
def children_name_and_order
|
230
|
-
@parent.reload
|
257
|
+
name_and_order(@parent.children(reload = true))
|
258
|
+
end
|
259
|
+
|
260
|
+
def roots_name_and_order
|
261
|
+
name_and_order(Label.roots)
|
231
262
|
end
|
232
263
|
|
233
|
-
it
|
264
|
+
it 'sort_orders properly' do
|
234
265
|
children_name_and_order.should == [['a', 0], ['b', 1], ['c', 2], ['d', 3]]
|
235
266
|
end
|
236
267
|
|
237
|
-
it
|
268
|
+
it 'when inserted before' do
|
238
269
|
@b.append_sibling(@a)
|
239
270
|
children_name_and_order.should == [['b', 0], ['a', 1], ['c', 2], ['d', 3]]
|
240
271
|
end
|
241
272
|
|
242
|
-
it
|
273
|
+
it 'when inserted after' do
|
243
274
|
@a.append_sibling(@c)
|
244
275
|
children_name_and_order.should == [['a', 0], ['c', 1], ['b', 2], ['d', 3]]
|
245
276
|
end
|
277
|
+
|
278
|
+
it 'when inserted before the first' do
|
279
|
+
@a.prepend_sibling(@d)
|
280
|
+
children_name_and_order.should == [['d', 0], ['a', 1], ['b', 2], ['c', 3]]
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'when inserted after the last' do
|
284
|
+
@d.append_sibling(@b)
|
285
|
+
children_name_and_order.should == [['a', 0], ['c', 1], ['d', 2], ['b', 3]]
|
286
|
+
end
|
287
|
+
|
288
|
+
it 'prepends to root nodes' do
|
289
|
+
@parent.prepend_sibling(@f)
|
290
|
+
roots_name_and_order.should == [['f', 0], ['parent', 1], ['e', 2]]
|
291
|
+
end
|
246
292
|
end
|
247
293
|
|
248
|
-
it
|
249
|
-
root = Label.create(:
|
250
|
-
a = Label.
|
251
|
-
b = Label.create(:
|
252
|
-
c = Label.create(:
|
294
|
+
it 'behaves like the readme' do
|
295
|
+
root = Label.create(name: 'root')
|
296
|
+
a = root.append_child(Label.new(name: 'a'))
|
297
|
+
b = Label.create(name: 'b')
|
298
|
+
c = Label.create(name: 'c')
|
253
299
|
|
254
300
|
a.append_sibling(b)
|
255
301
|
a.self_and_siblings.collect(&:name).should == %w(a b)
|
@@ -277,6 +323,30 @@ describe Label do
|
|
277
323
|
d.self_and_siblings.collect(&:sort_order).should == [0, 1, 2, 3]
|
278
324
|
end
|
279
325
|
|
326
|
+
# https://github.com/mceachen/closure_tree/issues/84
|
327
|
+
it "properly appends children with <<" do
|
328
|
+
root = Label.create(:name => "root")
|
329
|
+
a = Label.create(:name => "a", :parent => root)
|
330
|
+
b = Label.create(:name => "b", :parent => root)
|
331
|
+
a.sort_order.should == 0
|
332
|
+
b.sort_order.should == 1
|
333
|
+
#c = Label.create(:name => "c")
|
334
|
+
|
335
|
+
# should the sort_order for roots be set?
|
336
|
+
root.sort_order.should_not be_nil
|
337
|
+
root.sort_order.should == 0
|
338
|
+
|
339
|
+
# sort_order should never be nil on a child.
|
340
|
+
a.sort_order.should_not be_nil
|
341
|
+
a.sort_order.should == 0
|
342
|
+
# Add a child to root at end of children.
|
343
|
+
root.children << b
|
344
|
+
b.parent.should == root
|
345
|
+
a.self_and_siblings.collect(&:name).should == %w(a b)
|
346
|
+
root.reload.children.collect(&:name).should == %w(a b)
|
347
|
+
root.children.collect(&:sort_order).should == [0, 1]
|
348
|
+
end
|
349
|
+
|
280
350
|
context "#add_sibling" do
|
281
351
|
it "should move a node before another node which has an uninitialized sort_order" do
|
282
352
|
f = Label.find_or_create_by_path %w(a b c d e fa)
|
@@ -290,16 +360,16 @@ describe Label do
|
|
290
360
|
f.self_and_siblings.should == [f0, f]
|
291
361
|
end
|
292
362
|
|
363
|
+
let(:f1) { Label.find_or_create_by_path %w(a1 b1 c1 d1 e1 f1) }
|
364
|
+
|
293
365
|
it "should move a node to another tree" do
|
294
|
-
f1 = Label.find_or_create_by_path %w(a1 b1 c1 d1 e1 f1)
|
295
366
|
f2 = Label.find_or_create_by_path %w(a2 b2 c2 d2 e2 f2)
|
296
367
|
f1.add_sibling(f2)
|
297
368
|
f2.ancestry_path.should == %w(a1 b1 c1 d1 e1 f2)
|
298
|
-
f1.parent.children.should == [f1, f2]
|
369
|
+
f1.parent.reload.children.should == [f1, f2]
|
299
370
|
end
|
300
371
|
|
301
372
|
it "should reorder old-parent siblings when a node moves to another tree" do
|
302
|
-
f1 = Label.find_or_create_by_path %w(a1 b1 c1 d1 e1 f1)
|
303
373
|
f2 = Label.find_or_create_by_path %w(a2 b2 c2 d2 e2 f2)
|
304
374
|
f3 = f2.prepend_sibling(Label.new(:name => "f3"))
|
305
375
|
f4 = f2.append_sibling(Label.new(:name => "f4"))
|
@@ -311,32 +381,87 @@ describe Label do
|
|
311
381
|
end
|
312
382
|
end
|
313
383
|
|
384
|
+
context "sort_order must be set" do
|
385
|
+
|
386
|
+
before do
|
387
|
+
@root = Label.create(name: 'root')
|
388
|
+
@a, @b, @c = %w(a b c).map { |n| @root.children.create(name: n) }
|
389
|
+
end
|
390
|
+
|
391
|
+
it 'should set sort_order on roots' do
|
392
|
+
@root.sort_order.should == 0
|
393
|
+
end
|
394
|
+
|
395
|
+
it 'should set sort_order with siblings' do
|
396
|
+
@a.sort_order.should == 0
|
397
|
+
@b.sort_order.should == 1
|
398
|
+
@c.sort_order.should == 2
|
399
|
+
end
|
400
|
+
|
401
|
+
it 'should reset sort_order when a node is moved to another location' do
|
402
|
+
root2 = Label.create(name: 'root2')
|
403
|
+
root2.add_child @b
|
404
|
+
@a.sort_order.should == 0
|
405
|
+
@b.sort_order.should == 0
|
406
|
+
@c.reload.sort_order.should == 1
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
314
410
|
context "destructive reordering" do
|
315
411
|
before :each do
|
316
412
|
# to make sure sort_order isn't affected by additional nodes:
|
317
413
|
create_preorder_tree
|
318
|
-
@root = Label.create(:name =>
|
319
|
-
@a = @root.children.create!(:name =>
|
320
|
-
@b = @a.append_sibling(Label.new(:name =>
|
321
|
-
@c = @b.append_sibling(Label.new(:name =>
|
414
|
+
@root = Label.create(:name => 'root')
|
415
|
+
@a = @root.children.create!(:name => 'a')
|
416
|
+
@b = @a.append_sibling(Label.new(:name => 'b'))
|
417
|
+
@c = @b.append_sibling(Label.new(:name => 'c'))
|
322
418
|
end
|
323
|
-
context "doesn't create sort order gaps
|
324
|
-
it
|
419
|
+
context "doesn't create sort order gaps" do
|
420
|
+
it 'from head' do
|
325
421
|
@a.destroy
|
326
422
|
@root.reload.children.should == [@b, @c]
|
327
423
|
@root.children.map { |ea| ea.sort_order }.should == [0, 1]
|
328
424
|
end
|
329
|
-
it
|
425
|
+
it 'from mid' do
|
330
426
|
@b.destroy
|
331
427
|
@root.reload.children.should == [@a, @c]
|
332
428
|
@root.children.map { |ea| ea.sort_order }.should == [0, 1]
|
333
429
|
end
|
334
|
-
it
|
430
|
+
it 'from tail' do
|
335
431
|
@c.destroy
|
336
432
|
@root.reload.children.should == [@a, @b]
|
337
433
|
@root.children.map { |ea| ea.sort_order }.should == [0, 1]
|
338
434
|
end
|
339
435
|
end
|
436
|
+
|
437
|
+
context 'add_sibling moves descendant nodes' do
|
438
|
+
let(:roots) { (0..10).map { |ea| Label.create(name: ea) } }
|
439
|
+
let(:first_root) { roots.first }
|
440
|
+
let(:last_root) { roots.last }
|
441
|
+
it 'should retain sort orders of descendants when moving to a new parent' do
|
442
|
+
expected_order = ('a'..'z').to_a.shuffle
|
443
|
+
expected_order.map { |ea| first_root.add_child(Label.new(name: ea)) }
|
444
|
+
actual_order = first_root.children(reload = true).pluck(:name)
|
445
|
+
actual_order.should == expected_order
|
446
|
+
last_root.append_child(first_root)
|
447
|
+
last_root.self_and_descendants.pluck(:name).should == %w(10 0) + expected_order
|
448
|
+
end
|
449
|
+
|
450
|
+
it 'should retain sort orders of descendants when moving within the same new parent' do
|
451
|
+
path = ('a'..'z').to_a
|
452
|
+
z = first_root.find_or_create_by_path(path)
|
453
|
+
z_children_names = (100..150).to_a.shuffle.map { |ea| ea.to_s }
|
454
|
+
z_children_names.reverse.each { |ea| z.prepend_child(Label.new(name: ea)) }
|
455
|
+
z.children(reload = true).pluck(:name).should == z_children_names
|
456
|
+
a = first_root.find_by_path(['a'])
|
457
|
+
# move b up to a's level:
|
458
|
+
b = a.children.first
|
459
|
+
a.add_sibling(b)
|
460
|
+
b.parent.should == first_root
|
461
|
+
z.children(reload = true).pluck(:name).should == z_children_names
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
340
465
|
it "shouldn't fail if all children are destroyed" do
|
341
466
|
roots = Label.roots.to_a
|
342
467
|
roots.each { |ea| ea.children.destroy_all }
|
@@ -344,33 +469,33 @@ describe Label do
|
|
344
469
|
end
|
345
470
|
end
|
346
471
|
|
347
|
-
context
|
348
|
-
it
|
472
|
+
context 'descendent destruction' do
|
473
|
+
it 'properly destroys descendents created with add_child' do
|
349
474
|
a = Label.create(name: 'a')
|
350
475
|
b = Label.new(name: 'b')
|
351
476
|
a.add_child b
|
352
477
|
c = Label.new(name: 'c')
|
353
478
|
b.add_child c
|
354
479
|
a.destroy
|
355
|
-
Label.exists?(id: [a.id,b.id,c.id]).should be_false
|
480
|
+
Label.exists?(id: [a.id, b.id, c.id]).should be_false
|
356
481
|
end
|
357
482
|
|
358
|
-
it
|
483
|
+
it 'properly destroys descendents created with <<' do
|
359
484
|
a = Label.create(name: 'a')
|
360
485
|
b = Label.new(name: 'b')
|
361
486
|
a.children << b
|
362
487
|
c = Label.new(name: 'c')
|
363
488
|
b.children << c
|
364
489
|
a.destroy
|
365
|
-
Label.exists?(id: [a.id,b.id,c.id]).should be_false
|
490
|
+
Label.exists?(id: [a.id, b.id, c.id]).should be_false
|
366
491
|
end
|
367
492
|
end
|
368
493
|
|
369
|
-
context
|
370
|
-
it
|
494
|
+
context 'preorder' do
|
495
|
+
it 'returns descendants in proper order' do
|
371
496
|
create_preorder_tree
|
372
497
|
a = Label.root
|
373
|
-
a.name.should ==
|
498
|
+
a.name.should == 'a'
|
374
499
|
expected = ('a'..'r').to_a
|
375
500
|
a.self_and_descendants_preordered.collect { |ea| ea.name }.should == expected
|
376
501
|
Label.roots_and_descendants_preordered.collect { |ea| ea.name }.should == expected
|
@@ -379,11 +504,11 @@ describe Label do
|
|
379
504
|
l.name = "a1"
|
380
505
|
l.sort_order = a.sort_order + 1
|
381
506
|
end
|
382
|
-
create_preorder_tree(
|
507
|
+
create_preorder_tree('1')
|
383
508
|
# Should be no change:
|
384
509
|
a.reload.self_and_descendants_preordered.collect { |ea| ea.name }.should == expected
|
385
510
|
expected += ('a'..'r').collect { |ea| "#{ea}1" }
|
386
511
|
Label.roots_and_descendants_preordered.collect { |ea| ea.name }.should == expected
|
387
512
|
end
|
388
|
-
end unless ENV[
|
513
|
+
end unless ENV['DB'] == 'sqlite' # sqlite doesn't have a power function.
|
389
514
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'ClosureTree::Test::Matcher' do
|
4
|
+
|
5
|
+
describe 'be_a_closure_tree' do
|
6
|
+
it { UUIDTag.should be_a_closure_tree }
|
7
|
+
it { User.should be_a_closure_tree }
|
8
|
+
it { Label.should be_a_closure_tree.ordered }
|
9
|
+
it { Metal.should be_a_closure_tree.ordered(:sort_order) }
|
10
|
+
it { MenuItem.should be_a_closure_tree }
|
11
|
+
it { Contract.should_not be_a_closure_tree }
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'ordered' do
|
15
|
+
it { Label.should be_a_closure_tree.ordered }
|
16
|
+
it { UUIDTag.should be_a_closure_tree.ordered }
|
17
|
+
it { Metal.should be_a_closure_tree.ordered(:sort_order) }
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'advisory_lock' do
|
21
|
+
it 'should use advisory lock' do
|
22
|
+
User.should be_a_closure_tree.with_advisory_lock
|
23
|
+
Label.should be_a_closure_tree.ordered.with_advisory_lock
|
24
|
+
Metal.should be_a_closure_tree.ordered(:sort_order).with_advisory_lock
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should not use advisory lock' do
|
28
|
+
MenuItem.should be_a_closure_tree.without_advisory_lock
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|