closure_tree 3.7.2 → 3.7.3

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 CHANGED
@@ -396,6 +396,12 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
396
396
 
397
397
  ## Change log
398
398
 
399
+ ### 3.7.3
400
+
401
+ Due to MySQL's inability to lock rows properly, I've switched to advisory_locks for
402
+ all write paths. This will prevent deadlocks, addressing
403
+ [issue 41](https://github.com/mceachen/closure_tree/issues/41).
404
+
399
405
  ### 3.7.2
400
406
 
401
407
  * Support for UUID primary keys. Addresses
@@ -1,8 +1,9 @@
1
1
  require 'active_support'
2
+ require 'active_record'
2
3
 
3
4
  ActiveSupport.on_load :active_record do
4
- require 'closure_tree/acts_as_tree'
5
5
  require 'with_advisory_lock'
6
+ require 'closure_tree/acts_as_tree'
6
7
 
7
8
  ActiveRecord::Base.send :extend, ClosureTree::ActsAsTree
8
9
  end
@@ -216,20 +216,23 @@ module ClosureTree
216
216
 
217
217
  # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
218
218
  def find_or_create_by_path(path, attributes = {})
219
- subpath = path.is_a?(Enumerable) ? path.dup : [path]
220
- child_name = subpath.shift
221
- return self unless child_name
222
- child = transaction do
223
- lock!
224
- attrs = {name_sym => child_name}
225
- attrs[:type] = self.type if ct_subclass? && ct_has_type?
226
- self.children.where(attrs).first || begin
227
- child = self.class.new(attributes.merge(attrs))
228
- self.children << child
229
- child
219
+ with_advisory_lock("closure_tree") do
220
+ transaction do
221
+ subpath = path.is_a?(Enumerable) ? path.dup : [path]
222
+ child_name = subpath.shift
223
+ return self unless child_name
224
+ child = transaction do
225
+ attrs = {name_sym => child_name}
226
+ attrs[:type] = self.type if ct_subclass? && ct_has_type?
227
+ self.children.where(attrs).first || begin
228
+ child = self.class.new(attributes.merge(attrs))
229
+ self.children << child
230
+ child
231
+ end
232
+ end
233
+ child.find_or_create_by_path(subpath, attributes)
230
234
  end
231
235
  end
232
- child.find_or_create_by_path(subpath, attributes)
233
236
  end
234
237
 
235
238
  def find_all_by_generation(generation_level)
@@ -284,18 +287,22 @@ module ClosureTree
284
287
  end
285
288
 
286
289
  def rebuild!
287
- delete_hierarchy_references unless @was_new_record
288
- hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
289
- unless root?
290
- connection.execute <<-SQL
290
+ with_advisory_lock("closure_tree") do
291
+ transaction do
292
+ delete_hierarchy_references unless @was_new_record
293
+ hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
294
+ unless root?
295
+ connection.execute <<-SQL
291
296
  INSERT INTO #{quoted_hierarchy_table_name}
292
297
  (ancestor_id, descendant_id, generations)
293
298
  SELECT x.ancestor_id, #{ct_quote(id)}, x.generations + 1
294
299
  FROM #{quoted_hierarchy_table_name} x
295
300
  WHERE x.descendant_id = #{ct_quote(self.ct_parent_id)}
296
- SQL
301
+ SQL
302
+ end
303
+ children.each { |c| c.rebuild! }
304
+ end
297
305
  end
298
- children.each { |c| c.rebuild! }
299
306
  end
300
307
 
301
308
  def ct_before_destroy
@@ -351,7 +358,7 @@ module ClosureTree
351
358
  # Rebuilds the hierarchy table based on the parent_id column in the database.
352
359
  # Note that the hierarchy table will be truncated.
353
360
  def rebuild!
354
- with_advisory_lock("closure_tree.#{ct_class}.rebuild") do
361
+ with_advisory_lock("closure_tree") do
355
362
  transaction do
356
363
  hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
357
364
  roots.each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
@@ -371,15 +378,15 @@ module ClosureTree
371
378
  def find_or_create_by_path(path, attributes = {})
372
379
  subpath = path.dup
373
380
  root_name = subpath.shift
374
- root = with_advisory_lock("closure_tree.#{ct_class}.find_or_create(#{root_name})") do
381
+ with_advisory_lock("closure_tree") do
375
382
  transaction do
376
383
  # shenanigans because find_or_create can't infer we want the same class as this:
377
384
  # Note that roots will already be constrained to this subclass (in the case of polymorphism):
378
- roots.where(name_sym => root_name).first ||
379
- create!(attributes.merge(name_sym => root_name))
385
+ root = roots.where(name_sym => root_name).first
386
+ root ||= create!(attributes.merge(name_sym => root_name))
387
+ root.find_or_create_by_path(subpath, attributes)
380
388
  end
381
389
  end
382
- root.find_or_create_by_path(subpath, attributes)
383
390
  end
384
391
 
385
392
  def hash_tree_scope(limit_depth = nil)
@@ -566,29 +573,34 @@ module ClosureTree
566
573
 
567
574
  def add_sibling(sibling_node, use_update_all = true, add_after = true)
568
575
  fail "can't add self as sibling" if self == sibling_node
569
- # issue 18: we need to set the order_value explicitly so subsequent orders will work.
570
- update_attribute(:order_value, 0) if self.order_value.nil?
571
- sibling_node.order_value = self.order_value.to_i + (add_after ? 1 : -1)
572
- # We need to incr the before_siblings to make room for sibling_node:
573
- if use_update_all
574
- col = quoted_order_column(false)
575
- # issue 21: we have to use the base class, so STI doesn't get in the way of only updating the child class instances:
576
- ct_base_class.update_all(
577
- ["#{col} = #{col} #{add_after ? '+' : '-'} 1", "updated_at = now()"],
578
- ["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
579
- ct_parent_id,
580
- sibling_node.order_value])
581
- else
582
- last_value = sibling_node.order_value.to_i
583
- (add_after ? siblings_after : siblings_before.reverse).each do |ea|
584
- last_value += (add_after ? 1 : -1)
585
- ea.order_value = last_value
586
- ea.save!
576
+ # issue 40: we need to lock the parent to prevent deadlocks on parallel sibling additions
577
+ with_advisory_lock("closure_tree") do
578
+ transaction do
579
+ # issue 18: we need to set the order_value explicitly so subsequent orders will work.
580
+ update_attribute(:order_value, 0) if self.order_value.nil?
581
+ sibling_node.order_value = self.order_value.to_i + (add_after ? 1 : -1)
582
+ # We need to incr the before_siblings to make room for sibling_node:
583
+ if use_update_all
584
+ col = quoted_order_column(false)
585
+ # issue 21: we have to use the base class, so STI doesn't get in the way of only updating the child class instances:
586
+ ct_base_class.update_all(
587
+ ["#{col} = #{col} #{add_after ? '+' : '-'} 1", "updated_at = now()"],
588
+ ["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
589
+ ct_parent_id,
590
+ sibling_node.order_value])
591
+ else
592
+ last_value = sibling_node.order_value.to_i
593
+ (add_after ? siblings_after : siblings_before.reverse).each do |ea|
594
+ last_value += (add_after ? 1 : -1)
595
+ ea.order_value = last_value
596
+ ea.save!
597
+ end
598
+ end
599
+ sibling_node.parent = self.parent
600
+ sibling_node.save!
601
+ sibling_node.reload
587
602
  end
588
603
  end
589
- sibling_node.parent = self.parent
590
- sibling_node.save!
591
- sibling_node.reload # <- because siblings_before and siblings_after will have changed.
592
604
  end
593
605
  end
594
- end
606
+ end
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = "3.7.2" unless defined?(::ClosureTree::VERSION)
2
+ VERSION = "3.7.3" unless defined?(::ClosureTree::VERSION)
3
3
  end
@@ -1,10 +1,10 @@
1
- sqlite3:
1
+ sqlite:
2
2
  adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
3
3
  database: spec/sqlite3.db
4
4
  pool: 50
5
5
  timeout: 5000
6
6
 
7
- pg:
7
+ postgresql:
8
8
  adapter: postgresql
9
9
  username: postgres
10
10
  database: closure_tree_test
@@ -11,34 +11,34 @@ end
11
11
 
12
12
  ActiveRecord::Schema.define(:version => 0) do
13
13
 
14
- create_table "nodes", :id => false do |t|
15
- t.string "id"
14
+ create_table "tags", :force => true do |t|
16
15
  t.string "name"
17
- t.string "parent_id"
16
+ t.string "title"
17
+ t.integer "parent_id"
18
+ t.integer "sort_order"
18
19
  t.datetime "created_at"
19
20
  t.datetime "updated_at"
20
21
  end
21
22
 
22
- force_add_index "nodes", [:id], :name => "node_id", :unique => true
23
-
24
- create_table "node_hierarchies", :id => false, :force => true do |t|
25
- t.string "ancestor_id", :null => false
26
- t.string "descendant_id", :null => false
23
+ create_table "tag_hierarchies", :id => false, :force => true do |t|
24
+ t.integer "ancestor_id", :null => false
25
+ t.integer "descendant_id", :null => false
27
26
  t.integer "generations", :null => false
28
27
  end
29
28
 
30
- create_table "tags", :force => true do |t|
29
+ create_table "tags_uuid", :id => false, :force => true do |t|
30
+ t.string "id", :unique => true
31
31
  t.string "name"
32
32
  t.string "title"
33
- t.integer "parent_id"
33
+ t.string "parent_id"
34
34
  t.integer "sort_order"
35
35
  t.datetime "created_at"
36
36
  t.datetime "updated_at"
37
37
  end
38
38
 
39
- create_table "tag_hierarchies", :id => false, :force => true do |t|
40
- t.integer "ancestor_id", :null => false
41
- t.integer "descendant_id", :null => false
39
+ create_table "tag_hierarchies_uuid", :id => false, :force => true do |t|
40
+ t.string "ancestor_id", :null => false
41
+ t.string "descendant_id", :null => false
42
42
  t.integer "generations", :null => false
43
43
  end
44
44
 
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ require 'securerandom'
3
+
4
+ describe "threadhot" do
5
+
6
+ before :each do
7
+ LabelHierarchy.delete_all
8
+ Label.delete_all
9
+ @iterations = 5
10
+ @workers = 8
11
+ end
12
+
13
+ def prepend_sibling_at_even_second(run_at)
14
+ ActiveRecord::Base.connection.reconnect!
15
+ sibling = Label.new(:name => SecureRandom.hex(10))
16
+ target = Label.find(@target.id)
17
+ sleep(run_at - Time.now.to_f)
18
+ target.prepend_sibling sibling
19
+ end
20
+
21
+ def run_workers
22
+ start_time = Time.now.to_i + 2
23
+ @times = @iterations.times.collect { |ea| start_time + (ea * 2) }
24
+ @names = @times.collect { |ea| ea.to_s }
25
+ @threads = @workers.times.collect do
26
+ Thread.new do
27
+ @times.each { |ea| prepend_sibling_at_even_second(ea) }
28
+ end
29
+ end
30
+ @threads.each { |ea| ea.join }
31
+ end
32
+
33
+ it "prepend_sibling on a non-root node doesn't cause deadlocks" do
34
+ @target = Label.find_or_create_by_path %w(root parent)
35
+ run_workers
36
+ children = Label.roots
37
+ uniq_sort_orders = children.collect { |ea| ea.sort_order }.uniq
38
+ children.size.should == uniq_sort_orders.size
39
+
40
+ # The only non-root node should be "root":
41
+ Label.all.select { |ea| ea.root? }.should == [@target.parent]
42
+ end
43
+
44
+ # SQLite doesn't like parallelism, and Rails 3.0 and 3.1 have known threading issues. SKIP.
45
+ end if ((ENV["DB"] != "sqlite3") && (ActiveRecord::VERSION::STRING =~ /^3.2/))
@@ -56,4 +56,4 @@ describe "threadhot" do
56
56
  end
57
57
 
58
58
  # SQLite doesn't like parallelism, and Rails 3.0 and 3.1 have known threading issues. SKIP.
59
- end if ((ENV["DB"] != "sqlite3") && (ActiveRecord::VERSION::STRING =~ /^3.2/))
59
+ end if ((ENV["DB"] != "sqlite") && (ActiveRecord::VERSION::STRING =~ /^3.2/))
@@ -7,11 +7,7 @@ require 'bundler/setup'
7
7
  require 'rspec'
8
8
  require 'logger'
9
9
 
10
- require 'active_support'
11
- require 'active_model'
12
- require 'active_record'
13
10
  require 'action_controller' # rspec-rails needs this :(
14
- require 'with_advisory_lock'
15
11
  require 'closure_tree'
16
12
  require 'tmpdir'
17
13
 
@@ -58,4 +54,4 @@ RSpec.configure do |config|
58
54
  config.after(:all) do
59
55
  FileUtils.remove_entry_secure ENV['FLOCK_DIR']
60
56
  end
61
- end
57
+ end
@@ -1,15 +1,5 @@
1
1
  require 'uuidtools'
2
2
 
3
- class Node < ActiveRecord::Base
4
- acts_as_tree :dependent => :destroy
5
- before_create :generate_uuid
6
- attr_accessible :name
7
-
8
- def generate_uuid
9
- self.id = UUIDTools::UUID.random_create.to_s
10
- end
11
- end
12
-
13
3
  class Tag < ActiveRecord::Base
14
4
  acts_as_tree :dependent => :destroy, :order => "name"
15
5
  before_destroy :add_destroyed_tag
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- shared_examples_for Tag do
3
+ shared_examples_for "Tag (1)" do
4
4
 
5
5
  it "has correct accessible_attributes" do
6
6
  Tag.accessible_attributes.to_a.should =~ %w(parent name)
@@ -146,8 +146,10 @@ shared_examples_for Tag do
146
146
  end
147
147
 
148
148
  end
149
+ end
149
150
 
150
- describe "Tag with fixtures" do
151
+ shared_examples_for "Tag (2)" do
152
+ describe "Tag (2)" do
151
153
 
152
154
  fixtures :tags
153
155
 
@@ -367,7 +369,8 @@ describe Tag do
367
369
  Tag.ancestors.should_not include(ActiveModel::ForbiddenAttributesProtection)
368
370
  end
369
371
  end
370
- it_behaves_like Tag
372
+ it_behaves_like "Tag (1)"
373
+ it_behaves_like "Tag (2)"
371
374
  end
372
375
 
373
376
  describe "Tag with AR whitelisted attributes enabled" do
@@ -380,7 +383,8 @@ describe "Tag with AR whitelisted attributes enabled" do
380
383
  Tag.ancestors.should_not include(ActiveModel::ForbiddenAttributesProtection)
381
384
  end
382
385
  end
383
- it_behaves_like Tag
386
+ it_behaves_like "Tag (1)"
387
+ it_behaves_like "Tag (2)"
384
388
  end
385
389
 
386
390
  # This has to be the last one, because we include strong parameters into Tag
@@ -391,6 +395,29 @@ describe "Tag with strong parameters" do
391
395
  include ActiveModel::ForbiddenAttributesProtection
392
396
  end
393
397
  end
394
- it_behaves_like Tag
398
+ it_behaves_like "Tag (1)"
399
+ it_behaves_like "Tag (2)"
395
400
  end
396
401
 
402
+ describe "Tag with UUID" do
403
+ before(:all) do
404
+ # Change tables
405
+ Tag.table_name = Tag.table_name.gsub('tags', 'tags_uuid')
406
+ Tag.reset_column_information
407
+ TagHierarchy.table_name = TagHierarchy.table_name.gsub('tag_hierarchies', 'tag_hierarchies_uuid')
408
+ TagHierarchy.reset_column_information
409
+
410
+ # We have to reset a few other caches
411
+ Tag.closure_tree_options[:hierarchy_table_name] = 'tag_hierarchies_uuid'
412
+ Tag.reflections.each do |key, ref|
413
+ ref.instance_variable_set('@table_name', nil)
414
+ ref.instance_variable_set('@quoted_table_name', nil)
415
+ ref.options[:order].sub! 'tag_hierarchies', 'tag_hierarchies_uuid' if ref.options[:order]
416
+ end
417
+
418
+ # Add ID
419
+ Tag.before_create { self.id = UUIDTools::UUID.random_create.to_s }
420
+ end
421
+
422
+ it_behaves_like "Tag (1)"
423
+ end
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.7.2
4
+ version: 3.7.3
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-02-10 00:00:00.000000000 Z
12
+ date: 2013-02-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -34,7 +34,7 @@ dependencies:
34
34
  requirements:
35
35
  - - ! '>='
36
36
  - !ruby/object:Gem::Version
37
- version: '0'
37
+ version: 0.0.6
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,7 @@ dependencies:
42
42
  requirements:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
- version: '0'
45
+ version: 0.0.6
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: rake
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -223,7 +223,7 @@ files:
223
223
  - spec/fixtures/tags.yml
224
224
  - spec/hash_tree_spec.rb
225
225
  - spec/label_spec.rb
226
- - spec/node_spec.rb
226
+ - spec/parallel_prepend_sibling_spec.rb
227
227
  - spec/parallel_spec.rb
228
228
  - spec/spec_helper.rb
229
229
  - spec/support/models.rb
@@ -243,7 +243,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
243
243
  version: '0'
244
244
  segments:
245
245
  - 0
246
- hash: -2260215237559696293
246
+ hash: -1398803141248485966
247
247
  required_rubygems_version: !ruby/object:Gem::Requirement
248
248
  none: false
249
249
  requirements:
@@ -252,7 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
252
252
  version: '0'
253
253
  segments:
254
254
  - 0
255
- hash: -2260215237559696293
255
+ hash: -1398803141248485966
256
256
  requirements: []
257
257
  rubyforge_project:
258
258
  rubygems_version: 1.8.23
@@ -267,7 +267,7 @@ test_files:
267
267
  - spec/fixtures/tags.yml
268
268
  - spec/hash_tree_spec.rb
269
269
  - spec/label_spec.rb
270
- - spec/node_spec.rb
270
+ - spec/parallel_prepend_sibling_spec.rb
271
271
  - spec/parallel_spec.rb
272
272
  - spec/spec_helper.rb
273
273
  - spec/support/models.rb
@@ -1,148 +0,0 @@
1
- require 'spec_helper'
2
-
3
- shared_examples_for Node do
4
-
5
- it "has correct accessible_attributes" do
6
- Node.accessible_attributes.to_a.should =~ %w(parent name)
7
- end
8
-
9
- describe "empty db" do
10
-
11
- def nuke_db
12
- NodeHierarchy.delete_all
13
- Node.delete_all
14
- end
15
-
16
- before :each do
17
- nuke_db
18
- end
19
-
20
- context "empty db" do
21
- it "should return no entities" do
22
- Node.roots.should be_empty
23
- Node.leaves.should be_empty
24
- end
25
- end
26
-
27
- context "1 node db" do
28
- it "should return the only entity as a root and leaf" do
29
- a = Node.create!(:name => "a")
30
- Node.roots.should == [a]
31
- Node.leaves.should == [a]
32
- end
33
- end
34
-
35
- context "2 node db" do
36
- it "should return a simple root and leaf" do
37
- root = Node.create!(:name => "root")
38
- leaf = root.add_child(Node.create!(:name => "leaf"))
39
- Node.roots.should == [root]
40
- Node.leaves.should == [leaf]
41
- end
42
- end
43
-
44
- context "3 node collection.create db" do
45
- before :each do
46
- @root = Node.create! :name => "root"
47
- @mid = @root.children.create! :name => "mid"
48
- @leaf = @mid.children.create! :name => "leaf"
49
- end
50
-
51
- it "should create all nodes" do
52
- Node.all.should =~ [@root, @mid, @leaf]
53
- end
54
-
55
- it "should return a root and leaf without middle node" do
56
- Node.roots.should == [@root]
57
- Node.leaves.should == [@leaf]
58
- end
59
-
60
- it "should delete leaves" do
61
- Node.leaves.destroy_all
62
- Node.roots.should == [@root] # untouched
63
- Node.leaves.should == [@mid]
64
- end
65
-
66
- it "should delete everything if you delete the roots" do
67
- Node.roots.destroy_all
68
- Node.all.should be_empty
69
- Node.roots.should be_empty
70
- Node.leaves.should be_empty
71
- end
72
- end
73
-
74
- context "3 node explicit_create db" do
75
- before :each do
76
- @root = Node.create!(:name => "root")
77
- @mid = @root.add_child(Node.create!(:name => "mid"))
78
- @leaf = @mid.add_child(Node.create!(:name => "leaf"))
79
- end
80
-
81
- it "should create all nodes" do
82
- Node.all.should =~ [@root, @mid, @leaf]
83
- end
84
-
85
- it "should return a root and leaf without middle node" do
86
- Node.roots.should == [@root]
87
- Node.leaves.should == [@leaf]
88
- end
89
-
90
- it "should prevent parental loops from torso" do
91
- @mid.children << @root
92
- @root.valid?.should be_false
93
- @mid.reload.children.should == [@leaf]
94
- end
95
-
96
- it "should prevent parental loops from toes" do
97
- @leaf.children << @root
98
- @root.valid?.should be_false
99
- @leaf.reload.children.should be_empty
100
- end
101
-
102
- it "should support re-parenting" do
103
- @root.children << @leaf
104
- Node.leaves.should =~ [@leaf, @mid]
105
- end
106
-
107
- it "cleans up hierarchy references for leaves" do
108
- @leaf.destroy
109
- NodeHierarchy.find_all_by_ancestor_id(@leaf.id).should be_empty
110
- NodeHierarchy.find_all_by_descendant_id(@leaf.id).should be_empty
111
- end
112
-
113
- it "cleans up hierarchy references" do
114
- @mid.destroy
115
- NodeHierarchy.find_all_by_ancestor_id(@mid.id).should be_empty
116
- NodeHierarchy.find_all_by_descendant_id(@mid.id).should be_empty
117
- @root.reload.should be_root
118
- root_hiers = @root.ancestor_hierarchies.to_a
119
- root_hiers.size.should == 1
120
- NodeHierarchy.find_all_by_ancestor_id(@root.id).should == root_hiers
121
- NodeHierarchy.find_all_by_descendant_id(@root.id).should == root_hiers
122
- end
123
- end
124
-
125
- it "performs as the readme says it does" do
126
- grandparent = Node.create(:name => 'Grandparent')
127
- parent = grandparent.children.create(:name => 'Parent')
128
- child1 = Node.create(:name => 'First Child', :parent => parent)
129
- child2 = Node.new(:name => 'Second Child')
130
- parent.children << child2
131
- child3 = Node.new(:name => 'Third Child')
132
- parent.add_child child3
133
- grandparent.self_and_descendants.collect(&:name).should ==
134
- ["Grandparent", "Parent", "First Child", "Second Child", "Third Child"]
135
- child1.ancestry_path.should ==
136
- ["Grandparent", "Parent", "First Child"]
137
- child3.ancestry_path.should ==
138
- ["Grandparent", "Parent", "Third Child"]
139
- d = Node.find_or_create_by_path %w(a b c d)
140
- h = Node.find_or_create_by_path %w(e f g h)
141
- e = h.root
142
- d.add_child(e) # "d.children << e" would work too, of course
143
- h.ancestry_path.should == %w(a b c d e f g h)
144
- end
145
-
146
- end
147
-
148
- end