Attempt to provide backwards compatible YAML deserialization

I should have done this in the first place. We are now serializing an
explicit version so we can make more careful changes in the future. This
will load Active Record objects which were serialized in Rails 4.1.

There will be bugs, as YAML serialization was at least partially broken
back then. There will also be edge cases that we might not be able to
handle, especially if the type of a column has changed.

In addition, we're passing this as `from_database`, since that is
required for serialized columns at minimum. All other types were
serializing the cast value. At a glance, there should be no types for
which this is a problem.

Finally, dirty checking information will be lost on records serialized
in 4.1, so no columns will be marked as changed.
This commit is contained in:
Sean Griffin 2015-03-10 11:35:09 -06:00
parent 08469012e4
commit afc124c3b4
4 changed files with 76 additions and 0 deletions

View File

@ -43,6 +43,7 @@ module ActiveRecord
autoload :Explain
autoload :Inheritance
autoload :Integration
autoload :LegacyYamlAdapter
autoload :Migration
autoload :Migrator, 'active_record/migration'
autoload :ModelSchema

View File

@ -300,6 +300,7 @@ module ActiveRecord
# post.init_with('attributes' => { 'title' => 'hello world' })
# post.title # => 'hello world'
def init_with(coder)
coder = LegacyYamlAdapter.convert(self.class, coder)
@attributes = coder['attributes']
init_internals
@ -370,6 +371,7 @@ module ActiveRecord
coder['raw_attributes'] = attributes_before_type_cast
coder['attributes'] = @attributes
coder['new_record'] = new_record?
coder['active_record_yaml_version'] = 1
end
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+

View File

@ -0,0 +1,30 @@
module ActiveRecord
module LegacyYamlAdapter
def self.convert(klass, coder)
return coder unless coder.is_a?(Psych::Coder)
case coder["active_record_yaml_version"]
when 1 then coder
else
if coder["attributes"].is_a?(AttributeSet)
coder
else
Rails41.convert(klass, coder)
end
end
end
module Rails41
def self.convert(klass, coder)
attributes = klass.attributes_builder
.build_from_database(coder["attributes"])
new_record = coder["attributes"][klass.primary_key].blank?
{
"attributes" => attributes,
"new_record" => new_record,
}
end
end
end
end

View File

@ -83,4 +83,47 @@ class YamlSerializationTest < ActiveRecord::TestCase
assert_equal 5, author.posts_count
assert_equal 5, dumped.posts_count
end
def test_a_yaml_version_is_provided_for_future_backwards_compat
coder = {}
Topic.first.encode_with(coder)
assert coder['active_record_yaml_version']
end
def test_deserializing_rails_41_yaml
yaml = <<-YAML.strip_heredoc
--- !ruby/object:Topic
attributes:
id:
title: The First Topic
author_name: David
author_email_address: david@loudthinking.com
written_on: 2003-07-16 14:28:11.223300000 Z
bonus_time: 2000-01-01 14:28:00.000000000 Z
last_read: 2004-04-15
content: |
---
:omg: :lol
important:
approved: false
replies_count: 1
unique_replies_count: 0
parent_id:
parent_title:
type:
group:
created_at: 2015-03-10 17:05:42.000000000 Z
updated_at: 2015-03-10 17:05:42.000000000 Z
YAML
topic = YAML.load(yaml)
assert topic.new_record?
assert_equal nil, topic.id
assert_equal "The First Topic", topic.title
assert_equal({ omg: :lol }, topic.content)
end
def test_deserializing_rails_42_yaml
end
end