diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb index 6034963811..49f1204fa1 100644 --- a/activerecord/lib/active_record/dirty.rb +++ b/activerecord/lib/active_record/dirty.rb @@ -117,14 +117,7 @@ module ActiveRecord # The attribute already has an unsaved change. unless changed_attributes.include?(attr) old = clone_attribute_value(:read_attribute, attr) - - # Remember the original value if it's different. - typecasted = if column = column_for_attribute(attr) - column.type_cast(value) - else - value - end - changed_attributes[attr] = old unless old == typecasted + changed_attributes[attr] = old if field_changed?(attr, old, value) end # Carry on. @@ -138,5 +131,20 @@ module ActiveRecord update_without_dirty end end + + def field_changed?(attr, old, value) + if column = column_for_attribute(attr) + if column.type == :integer && column.null && old.nil? + # For nullable integer columns, NULL gets stored in database for blank (i.e. '') values. + # Hence we don't record it as a change if the value changes from nil to ''. + value = nil if value.blank? + else + value = column.type_cast(value) + end + end + + old != value + end + end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 1266eb5036..62178977f9 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -44,6 +44,16 @@ class DirtyTest < ActiveRecord::TestCase assert_nil pirate.catchphrase_change end + def test_nullable_integer_not_marked_as_changed_if_new_value_is_blank + pirate = Pirate.new + + ["", nil].each do |value| + pirate.parrot_id = value + assert !pirate.parrot_id_changed? + assert_nil pirate.parrot_id_change + end + end + def test_object_should_be_changed_if_any_attribute_is_changed pirate = Pirate.new assert !pirate.changed?