closure_tree 3.7.2 → 3.7.3

Sign up to get free protection for your applications and to get access to all the features.
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