Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Automatically determine primary key column type, and cast to integer …

…only if primary key is an int.
  • Loading branch information...
commit d8cd38683c8f778e11a8357da2f0ad549e6fbc75 1 parent d259c87
Rolf Timmermans authored
View
34 lib/ancestry/instance_methods.rb
@@ -18,7 +18,7 @@ def update_descendants_with_new_ancestry
descendant.update_attribute(
self.base_class.ancestry_column,
descendant.read_attribute(descendant.class.ancestry_column).gsub(
- /^#{self.child_ancestry}/,
+ /^#{self.child_ancestry}/,
if read_attribute(self.class.ancestry_column).blank? then id.to_s else "#{read_attribute self.class.ancestry_column }/#{id}" end
)
)
@@ -55,7 +55,7 @@ def apply_orphan_strategy
end
end
end
-
+
# The ancestry value for this record's children
def child_ancestry
# New records cannot have children
@@ -66,7 +66,7 @@ def child_ancestry
# Ancestors
def ancestor_ids
- read_attribute(self.base_class.ancestry_column).to_s.split('/').map(&:to_i)
+ read_attribute(self.base_class.ancestry_column).to_s.split('/').map { |id| cast_primary_key(id) }
end
def ancestor_conditions
@@ -76,7 +76,7 @@ def ancestor_conditions
def ancestors depth_options = {}
self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.scoped :conditions => ancestor_conditions
end
-
+
def path_ids
ancestor_ids + [id]
end
@@ -88,11 +88,11 @@ def path_conditions
def path depth_options = {}
self.base_class.scope_depth(depth_options, depth).ordered_by_ancestry.scoped :conditions => path_conditions
end
-
+
def depth
ancestor_ids.size
end
-
+
def cache_depth
write_attribute self.base_class.depth_cache_column, depth
end
@@ -181,7 +181,7 @@ def descendants depth_options = {}
def descendant_ids depth_options = {}
descendants(depth_options).all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
end
-
+
# Subtree
def subtree_conditions
["#{self.base_class.primary_key} = ? or #{self.base_class.ancestry_column} like ? or #{self.base_class.ancestry_column} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
@@ -194,20 +194,20 @@ def subtree depth_options = {}
def subtree_ids depth_options = {}
subtree(depth_options).all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
end
-
+
# Callback disabling
def without_ancestry_callbacks
@disable_ancestry_callbacks = true
yield
@disable_ancestry_callbacks = false
end
-
+
def ancestry_callbacks_disabled?
!!@disable_ancestry_callbacks
end
-
+
private
-
+
# Workaround to support Rails 2
def add_error_to_base error
if rails_3
@@ -216,5 +216,17 @@ def add_error_to_base error
errors.add_to_base error
end
end
+
+ def cast_primary_key(key)
+ if primary_key_type == :string
+ key
+ else
+ key.to_i
+ end
+ end
+
+ def primary_key_type
+ @primary_key_type ||= column_for_attribute(self.class.primary_key).type
+ end
end
end
View
13 test/environment.rb
@@ -10,13 +10,15 @@ def self.setup
ActiveRecord::Base.logger
ActiveRecord::Base.establish_connection YAML.load(File.open(File.join(File.dirname(__FILE__), 'database.yml')).read)[ENV['db'] || 'sqlite3']
end
-
+
def self.with_model options = {}
depth = options.delete(:depth) || 0
width = options.delete(:width) || 0
extra_columns = options.delete(:extra_columns)
-
- ActiveRecord::Base.connection.create_table 'test_nodes' do |table|
+ primary_key_type = options.delete(:primary_key_type) || :default
+
+ ActiveRecord::Base.connection.create_table 'test_nodes', :id => (primary_key_type == :default) do |table|
+ table.string :id, :null => false if primary_key_type == :string
table.string options[:ancestry_column] || :ancestry
table.integer options[:depth_cache_column] || :ancestry_depth if options[:cache_depth]
extra_columns.each do |name, type|
@@ -29,6 +31,9 @@ def self.with_model options = {}
(class << model; self; end).send :define_method, :model_name do; Struct.new(:human, :underscore).new 'TestNode', 'test_node'; end
const_set 'TestNode', model
+ if primary_key_type == :string
+ model.before_create { self.id = ActiveSupport::SecureRandom.hex(10) }
+ end
model.send :set_table_name, 'test_nodes'
model.has_ancestry options unless options.delete(:skip_ancestry)
@@ -42,7 +47,7 @@ def self.with_model options = {}
remove_const "TestNode"
end
end
-
+
def self.create_test_nodes model, depth, width, parent = nil
unless depth == 0
Array.new width do
View
153 test/has_ancestry_test.rb
@@ -6,13 +6,13 @@ def test_default_ancestry_column
assert_equal :ancestry, model.ancestry_column
end
end
-
+
def test_non_default_ancestry_column
AncestryTestDatabase.with_model :ancestry_column => :alternative_ancestry do |model|
assert_equal :alternative_ancestry, model.ancestry_column
end
end
-
+
def test_setting_ancestry_column
AncestryTestDatabase.with_model do |model|
model.ancestry_column = :ancestors
@@ -21,19 +21,19 @@ def test_setting_ancestry_column
assert_equal :ancestry, model.ancestry_column
end
end
-
+
def test_default_orphan_strategy
AncestryTestDatabase.with_model do |model|
assert_equal :destroy, model.orphan_strategy
end
end
-
+
def test_non_default_orphan_strategy
AncestryTestDatabase.with_model :orphan_strategy => :rootify do |model|
assert_equal :rootify, model.orphan_strategy
end
end
-
+
def test_setting_orphan_strategy
AncestryTestDatabase.with_model do |model|
model.orphan_strategy = :rootify
@@ -42,7 +42,7 @@ def test_setting_orphan_strategy
assert_equal :destroy, model.orphan_strategy
end
end
-
+
def test_setting_invalid_orphan_strategy
AncestryTestDatabase.with_model do |model|
assert_raise Ancestry::AncestryException do
@@ -50,7 +50,7 @@ def test_setting_invalid_orphan_strategy
end
end
end
-
+
def test_setup_test_nodes
AncestryTestDatabase.with_model :depth => 3, :width => 3 do |model, roots|
assert_equal Array, roots.class
@@ -72,7 +72,7 @@ def test_setup_test_nodes
end
end
end
-
+
def test_tree_navigation
AncestryTestDatabase.with_model :depth => 3, :width => 3 do |model, roots|
roots.each do |lvl0_node, lvl0_children|
@@ -106,7 +106,7 @@ def test_tree_navigation
assert_equal descendants.map(&:id), lvl0_node.descendant_ids
assert_equal descendants, lvl0_node.descendants
assert_equal [lvl0_node] + descendants, lvl0_node.subtree
-
+
lvl0_children.each do |lvl1_node, lvl1_children|
# Ancestors assertions
assert_equal [lvl0_node.id], lvl1_node.ancestor_ids
@@ -138,7 +138,7 @@ def test_tree_navigation
assert_equal descendants.map(&:id), lvl1_node.descendant_ids
assert_equal descendants, lvl1_node.descendants
assert_equal [lvl1_node] + descendants, lvl1_node.subtree
-
+
lvl1_children.each do |lvl2_node, lvl2_children|
# Ancestors assertions
assert_equal [lvl0_node.id, lvl1_node.id], lvl2_node.ancestor_ids
@@ -176,11 +176,40 @@ def test_tree_navigation
end
end
+ def test_ancestors_with_string_primary_keys
+ AncestryTestDatabase.with_model :depth => 3, :width => 3, :primary_key_type => :string, :primary_key_format => /[a-z0-9]+/ do |model, roots|
+ roots.each do |lvl0_node, lvl0_children|
+ # Ancestors assertions
+ assert_equal [], lvl0_node.ancestor_ids
+ assert_equal [], lvl0_node.ancestors
+ assert_equal [lvl0_node.id], lvl0_node.path_ids
+ assert_equal [lvl0_node], lvl0_node.path
+ assert_equal 0, lvl0_node.depth
+ lvl0_children.each do |lvl1_node, lvl1_children|
+ # Ancestors assertions
+ assert_equal [lvl0_node.id], lvl1_node.ancestor_ids
+ assert_equal [lvl0_node], lvl1_node.ancestors
+ assert_equal [lvl0_node.id, lvl1_node.id], lvl1_node.path_ids
+ assert_equal [lvl0_node, lvl1_node], lvl1_node.path
+ assert_equal 1, lvl1_node.depth
+ lvl1_children.each do |lvl2_node, lvl2_children|
+ # Ancestors assertions
+ assert_equal [lvl0_node.id, lvl1_node.id], lvl2_node.ancestor_ids
+ assert_equal [lvl0_node, lvl1_node], lvl2_node.ancestors
+ assert_equal [lvl0_node.id, lvl1_node.id, lvl2_node.id], lvl2_node.path_ids
+ assert_equal [lvl0_node, lvl1_node, lvl2_node], lvl2_node.path
+ assert_equal 2, lvl2_node.depth
+ end
+ end
+ end
+ end
+ end
+
def test_scopes
AncestryTestDatabase.with_model :depth => 3, :width => 3 do |model, roots|
# Roots assertion
assert_equal roots.map(&:first), model.roots.all
-
+
model.all.each do |test_node|
# Assertions for ancestors_of named scope
assert_equal test_node.ancestors.all, model.ancestors_of(test_node).all
@@ -200,7 +229,7 @@ def test_scopes
end
end
end
-
+
def test_ancestry_column_validation
AncestryTestDatabase.with_model do |model|
node = model.create
@@ -226,7 +255,7 @@ def test_ancestry_column_validation
end
end
end
-
+
def test_descendants_move_with_node
AncestryTestDatabase.with_model :depth => 3, :width => 3 do |model, roots|
root1, root2, root3 = roots.map(&:first)
@@ -252,7 +281,7 @@ def test_descendants_move_with_node
end
end
end
-
+
def test_orphan_rootify_strategy
AncestryTestDatabase.with_model :depth => 3, :width => 3 do |model, roots|
model.orphan_strategy = :rootify
@@ -266,7 +295,7 @@ def test_orphan_rootify_strategy
end
end
end
-
+
def test_orphan_destroy_strategy
AncestryTestDatabase.with_model :depth => 3, :width => 3 do |model, roots|
model.orphan_strategy = :destroy
@@ -280,7 +309,7 @@ def test_orphan_destroy_strategy
end
end
end
-
+
def test_orphan_restrict_strategy
AncestryTestDatabase.with_model :depth => 3, :width => 3 do |model, roots|
model.orphan_strategy = :restrict
@@ -293,7 +322,7 @@ def test_orphan_restrict_strategy
end
end
end
-
+
def test_integrity_checking
AncestryTestDatabase.with_model :width => 3, :depth => 3 do |model, roots|
# Check that there are no errors on a valid tree
@@ -302,7 +331,7 @@ def test_integrity_checking
end
assert_equal 0, model.check_ancestry_integrity!(:report => :list).size
end
-
+
AncestryTestDatabase.with_model :width => 3, :depth => 3 do |model, roots|
# Check detection of invalid format for ancestry column
roots.first.first.update_attribute model.ancestry_column, 'invalid_ancestry'
@@ -311,7 +340,7 @@ def test_integrity_checking
end
assert_equal 1, model.check_ancestry_integrity!(:report => :list).size
end
-
+
AncestryTestDatabase.with_model :width => 3, :depth => 3 do |model, roots|
# Check detection of non-existent ancestor
roots.first.first.update_attribute model.ancestry_column, 35
@@ -320,7 +349,7 @@ def test_integrity_checking
end
assert_equal 1, model.check_ancestry_integrity!(:report => :list).size
end
-
+
AncestryTestDatabase.with_model :width => 3, :depth => 3 do |model, roots|
# Check detection of cyclic ancestry
node = roots.first.first
@@ -330,7 +359,7 @@ def test_integrity_checking
end
assert_equal 1, model.check_ancestry_integrity!(:report => :list).size
end
-
+
AncestryTestDatabase.with_model do |model|
# Check detection of conflicting parent id
model.destroy_all
@@ -341,7 +370,7 @@ def test_integrity_checking
assert_equal 1, model.check_ancestry_integrity!(:report => :list).size
end
end
-
+
def assert_integrity_restoration model
assert_raise Ancestry::AncestryIntegrityException do
model.check_ancestry_integrity!
@@ -350,28 +379,28 @@ def assert_integrity_restoration model
assert_nothing_raised do
model.check_ancestry_integrity!
end
- end
-
+ end
+
def test_integrity_restoration
# Check that integrity is restored for invalid format for ancestry column
AncestryTestDatabase.with_model :width => 3, :depth => 3 do |model, roots|
roots.first.first.update_attribute model.ancestry_column, 'invalid_ancestry'
assert_integrity_restoration model
end
-
+
# Check that integrity is restored for non-existent ancestor
AncestryTestDatabase.with_model :width => 3, :depth => 3 do |model, roots|
roots.first.first.update_attribute model.ancestry_column, 35
assert_integrity_restoration model
end
-
+
# Check that integrity is restored for cyclic ancestry
AncestryTestDatabase.with_model :width => 3, :depth => 3 do |model, roots|
node = roots.first.first
node.update_attribute model.ancestry_column, node.id
assert_integrity_restoration model
end
-
+
# Check that integrity is restored for conflicting parent id
AncestryTestDatabase.with_model do |model|
model.destroy_all
@@ -379,7 +408,7 @@ def test_integrity_restoration
assert_integrity_restoration model
end
end
-
+
def test_arrangement
AncestryTestDatabase.with_model :depth => 3, :width => 3 do |model, roots|
id_sorter = Proc.new do |a, b|; a.id <=> b.id; end
@@ -396,26 +425,26 @@ def test_arrangement
end
end
end
-
+
def test_node_creation_though_scope
AncestryTestDatabase.with_model do |model|
node = model.create!
child = node.children.create
- assert_equal node, child.parent
-
+ assert_equal node, child.parent
+
other_child = child.siblings.create!
assert_equal node, other_child.parent
-
+
grandchild = model.children_of(child).new
grandchild.save
assert_equal child, grandchild.parent
-
+
other_grandchild = model.siblings_of(grandchild).new
other_grandchild.save!
assert_equal child, other_grandchild.parent
end
end
-
+
def test_validate_ancestry_exclude_self
AncestryTestDatabase.with_model do |model|
parent = model.create!
@@ -425,7 +454,7 @@ def test_validate_ancestry_exclude_self
end
end
end
-
+
def test_depth_caching
AncestryTestDatabase.with_model :depth => 3, :width => 3, :cache_depth => true, :depth_cache_column => :depth_cache do |model, roots|
roots.each do |lvl0_node, lvl0_children|
@@ -439,7 +468,7 @@ def test_depth_caching
end
end
end
-
+
def test_depth_scopes
AncestryTestDatabase.with_model :depth => 4, :width => 2, :cache_depth => true do |model, roots|
model.before_depth(2).all? { |node| assert node.depth < 2 }
@@ -449,7 +478,7 @@ def test_depth_scopes
model.after_depth(2).all? { |node| assert node.depth > 2 }
end
end
-
+
def test_depth_scopes_unavailable
AncestryTestDatabase.with_model do |model|
assert_raise Ancestry::AncestryException do
@@ -469,7 +498,7 @@ def test_depth_scopes_unavailable
end
end
end
-
+
def test_invalid_has_ancestry_options
assert_raise Ancestry::AncestryException do
Class.new(ActiveRecord::Base).has_ancestry :this_option_doesnt_exist => 42
@@ -478,7 +507,7 @@ def test_invalid_has_ancestry_options
Class.new(ActiveRecord::Base).has_ancestry :not_a_hash
end
end
-
+
def test_build_ancestry_from_parent_ids
AncestryTestDatabase.with_model :skip_ancestry => true, :extra_columns => {:parent_id => :integer} do |model|
[model.create!].each do |parent|
@@ -488,22 +517,22 @@ def test_build_ancestry_from_parent_ids
end
end
end
-
+
# Assert all nodes where created
assert_equal (0..3).map { |n| 5 ** n }.sum, model.count
-
+
model.has_ancestry
model.build_ancestry_from_parent_ids!
-
+
# Assert ancestry integrity
assert_nothing_raised do
model.check_ancestry_integrity!
end
-
+
roots = model.roots.all
# Assert single root node
assert_equal 1, roots.size
-
+
# Assert it has 5 children
roots.each do |parent|
assert_equal 5, parent.children.count
@@ -519,26 +548,26 @@ def test_build_ancestry_from_parent_ids
end
end
end
-
+
def test_rebuild_depth_cache
AncestryTestDatabase.with_model :depth => 3, :width => 3, :cache_depth => true, :depth_cache_column => :depth_cache do |model, roots|
model.connection.execute("update test_nodes set depth_cache = null;")
-
+
# Assert cache was emptied correctly
model.all.each do |test_node|
assert_equal nil, test_node.depth_cache
end
-
+
# Rebuild cache
model.rebuild_depth_cache!
-
+
# Assert cache was rebuild correctly
model.all.each do |test_node|
assert_equal test_node.depth, test_node.depth_cache
end
end
end
-
+
def test_exception_when_rebuilding_depth_cache_for_model_without_depth_caching
AncestryTestDatabase.with_model do |model|
assert_raise Ancestry::AncestryException do
@@ -546,7 +575,7 @@ def test_exception_when_rebuilding_depth_cache_for_model_without_depth_caching
end
end
end
-
+
def test_descendants_with_depth_constraints
AncestryTestDatabase.with_model :depth => 4, :width => 4, :cache_depth => true do |model, roots|
assert_equal 4, model.roots.first.descendants(:before_depth => 2).count
@@ -556,7 +585,7 @@ def test_descendants_with_depth_constraints
assert_equal 64, model.roots.first.descendants(:after_depth => 2).count
end
end
-
+
def test_subtree_with_depth_constraints
AncestryTestDatabase.with_model :depth => 4, :width => 4, :cache_depth => true do |model, roots|
assert_equal 5, model.roots.first.subtree(:before_depth => 2).count
@@ -566,8 +595,8 @@ def test_subtree_with_depth_constraints
assert_equal 64, model.roots.first.subtree(:after_depth => 2).count
end
end
-
-
+
+
def test_ancestors_with_depth_constraints
AncestryTestDatabase.with_model :cache_depth => true do |model|
node1 = model.create!
@@ -576,7 +605,7 @@ def test_ancestors_with_depth_constraints
node4 = node3.children.create!
node5 = node4.children.create!
leaf = node5.children.create!
-
+
assert_equal [node1, node2, node3], leaf.ancestors(:before_depth => -2)
assert_equal [node1, node2, node3, node4], leaf.ancestors(:to_depth => -2)
assert_equal [node4], leaf.ancestors(:at_depth => -2)
@@ -584,7 +613,7 @@ def test_ancestors_with_depth_constraints
assert_equal [node5], leaf.ancestors(:after_depth => -2)
end
end
-
+
def test_path_with_depth_constraints
AncestryTestDatabase.with_model :cache_depth => true do |model|
node1 = model.create!
@@ -593,7 +622,7 @@ def test_path_with_depth_constraints
node4 = node3.children.create!
node5 = node4.children.create!
leaf = node5.children.create!
-
+
assert_equal [node1, node2, node3], leaf.path(:before_depth => -2)
assert_equal [node1, node2, node3, node4], leaf.path(:to_depth => -2)
assert_equal [node4], leaf.path(:at_depth => -2)
@@ -601,7 +630,7 @@ def test_path_with_depth_constraints
assert_equal [node5, leaf], leaf.path(:after_depth => -2)
end
end
-
+
def test_exception_on_unknown_depth_column
AncestryTestDatabase.with_model :cache_depth => true do |model|
assert_raise Ancestry::AncestryException do
@@ -609,20 +638,20 @@ def test_exception_on_unknown_depth_column
end
end
end
-
+
def test_sti_support
AncestryTestDatabase.with_model :extra_columns => {:type => :string} do |model|
subclass1 = Object.const_set 'Subclass1', Class.new(model)
(class << subclass1; self; end).send :define_method, :model_name do; Struct.new(:human, :underscore).new 'Subclass1', 'subclass1'; end
subclass2 = Object.const_set 'Subclass2', Class.new(model)
(class << subclass2; self; end).send :define_method, :model_name do; Struct.new(:human, :underscore).new 'Subclass1', 'subclass1'; end
-
+
node1 = subclass1.create!
node2 = subclass2.create! :parent => node1
node3 = subclass1.create! :parent => node2
node4 = subclass2.create! :parent => node3
node5 = subclass1.create! :parent => node4
-
+
model.all.each do |node|
assert [subclass1, subclass2].include?(node.class)
end
@@ -633,12 +662,12 @@ def test_sti_support
assert_equal [node1.id, node2.id, node3.id, node4.id, node5.id], node5.path.map(&:id)
end
end
-
+
def test_arrange_order_option
AncestryTestDatabase.with_model :width => 3, :depth => 3 do |model, roots|
descending_nodes_lvl0 = model.arrange :order => 'id desc'
ascending_nodes_lvl0 = model.arrange :order => 'id asc'
-
+
descending_nodes_lvl0.keys.zip(ascending_nodes_lvl0.keys.reverse).each do |descending_node, ascending_node|
assert_equal descending_node, ascending_node
descending_nodes_lvl1 = descending_nodes_lvl0[descending_node]
Please sign in to comment.
Something went wrong with that request. Please try again.