closure_tree 4.5.0 → 4.6.0

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.
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