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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.travis.yml +11 -3
  4. data/CHANGELOG.md +13 -0
  5. data/Gemfile +13 -0
  6. data/README.md +61 -12
  7. data/closure_tree.gemspec +4 -5
  8. data/gemfiles/activerecord_3.2.gemfile +12 -0
  9. data/gemfiles/activerecord_4.0.gemfile +12 -0
  10. data/gemfiles/activerecord_4.1.gemfile +12 -0
  11. data/gemfiles/activerecord_edge.gemfile +12 -0
  12. data/lib/closure_tree/acts_as_tree.rb +2 -9
  13. data/lib/closure_tree/deterministic_ordering.rb +4 -0
  14. data/lib/closure_tree/finders.rb +4 -4
  15. data/lib/closure_tree/hash_tree.rb +1 -1
  16. data/lib/closure_tree/hierarchy_maintenance.rb +19 -6
  17. data/lib/closure_tree/model.rb +2 -10
  18. data/lib/closure_tree/numeric_deterministic_ordering.rb +53 -36
  19. data/lib/closure_tree/numeric_order_support.rb +20 -13
  20. data/lib/closure_tree/support.rb +8 -0
  21. data/lib/closure_tree/support_attributes.rb +10 -0
  22. data/lib/closure_tree/test/matcher.rb +85 -0
  23. data/lib/closure_tree/version.rb +1 -1
  24. data/lib/closure_tree.rb +14 -1
  25. data/spec/cache_invalidation_spec.rb +39 -0
  26. data/spec/{support → db}/models.rb +6 -0
  27. data/spec/db/schema.rb +18 -0
  28. data/spec/label_spec.rb +171 -46
  29. data/spec/matcher_spec.rb +32 -0
  30. data/spec/parallel_spec.rb +85 -49
  31. data/spec/spec_helper.rb +6 -96
  32. data/spec/support/database.rb +49 -0
  33. data/spec/support/database_cleaner.rb +14 -0
  34. data/spec/support/deprecated/attr_accessible.rb +5 -0
  35. data/spec/support/hash_monkey_patch.rb +13 -0
  36. data/spec/support/helpers.rb +8 -0
  37. data/spec/support/sqlite3_with_advisory_lock.rb +10 -0
  38. data/tests.sh +7 -2
  39. metadata +31 -43
  40. 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, delta = 0)
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 "SET @i = 0"
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 + delta - 1}
26
- WHERE #{quoted_parent_column_name} = #{quoted_value(parent_id)} #{min_where}
27
- ORDER BY #{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, delta = 0)
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 + delta - 1}
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 #{quoted_parent_column_name} = #{quoted_value(parent_id)} #{min_where}) AS t
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, delta = 0)
53
- scope = model_class.where(parent_column_sym => parent_id)
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.update_attribute(order_column_sym, idx + minimum_sort_order_value.to_i + delta)
65
+ ea.update_order_value(idx + minimum_sort_order_value.to_i)
59
66
  end
60
67
  end
61
68
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = Gem::Version.new('4.5.0') unless defined?(::ClosureTree::VERSION)
2
+ VERSION = Gem::Version.new('4.6.0') unless defined?(::ClosureTree::VERSION)
3
3
  end
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
- DB_QUERIES.clear
203
- a1_array = @a1.self_and_descendants
204
- a1_array.collect { |ea| ea.name }.should == %w(a1 b1 c1 c2 d1 d2)
205
- DB_QUERIES.size.should == 1
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
- DB_QUERIES.clear
210
- d1_array = @d1.self_and_ancestors
211
- d1_array.collect { |ea| ea.name }.should == %w(d1 c1 b1 a1)
212
- DB_QUERIES.size.should == 1
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 => "parent")
219
- @a = EventLabel.new(:name => "a")
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.children.map { |ea| [ea.name, ea.sort_order] }
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 "sort_orders properly" do
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 "when inserted before" do
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 "when inserted after" do
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 "behaves like the readme" do
249
- root = Label.create(:name => "root")
250
- a = Label.create(:name => "a", :parent => root)
251
- b = Label.create(:name => "b")
252
- c = Label.create(:name => "c")
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 => "root")
319
- @a = @root.children.create!(:name => "a")
320
- @b = @a.append_sibling(Label.new(:name => "b"))
321
- @c = @b.append_sibling(Label.new(:name => "c"))
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 from" do
324
- it "from head" do
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 "from mid" do
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 "from tail" do
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 "descendent destruction" do
348
- it "properly destroys descendents created with add_child" do
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 "properly destroys descendents created with <<" do
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 "preorder" do
370
- it "returns descendants in proper order" do
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 == "a"
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("1")
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["DB"] == "sqlite"
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