Skip to content

Commit

Permalink
Merge pull request #25949 from sgrif/sg-backport-f0ddf87e4bfcfcb861b0…
Browse files Browse the repository at this point in the history
…a9dca32edb733668bd30

Backport f0ddf87
  • Loading branch information
sgrif committed Jul 27, 2016
2 parents b68f6f1 + ae2e451 commit e7ec1e3
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 11 deletions.
5 changes: 5 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,8 @@
* Virtual attributes will no longer raise when read on models loaded from the
database

*Sean Griffin*

* Fixes multi-parameter attributes conversion with invalid params.

*Hiroyuki Ishii*
Expand Down
4 changes: 2 additions & 2 deletions activerecord/lib/active_record/attribute.rb
Expand Up @@ -132,7 +132,7 @@ def original_value_for_database
end

def _original_value_for_database
value_for_database
type.serialize(original_value)
end

class FromDatabase < Attribute # :nodoc:
Expand Down Expand Up @@ -160,7 +160,7 @@ def type_cast(value)
value
end

def changed_in_place_from?(old_value)
def changed_in_place?
false
end
end
Expand Down
2 changes: 2 additions & 0 deletions activerecord/lib/active_record/attribute_set.rb
Expand Up @@ -2,6 +2,8 @@

module ActiveRecord
class AttributeSet # :nodoc:
delegate :fetch, to: :attributes

def initialize(attributes)
@attributes = attributes
end
Expand Down
36 changes: 29 additions & 7 deletions activerecord/lib/active_record/attribute_set/builder.rb
Expand Up @@ -3,33 +3,35 @@
module ActiveRecord
class AttributeSet # :nodoc:
class Builder # :nodoc:
attr_reader :types, :always_initialized
attr_reader :types, :always_initialized, :default

def initialize(types, always_initialized = nil)
def initialize(types, always_initialized = nil, &default)
@types = types
@always_initialized = always_initialized
@default = default
end

def build_from_database(values = {}, additional_types = {})
if always_initialized && !values.key?(always_initialized)
values[always_initialized] = nil
end

attributes = LazyAttributeHash.new(types, values, additional_types)
attributes = LazyAttributeHash.new(types, values, additional_types, &default)
AttributeSet.new(attributes)
end
end
end

class LazyAttributeHash # :nodoc:
delegate :transform_values, :each_key, to: :materialize
delegate :transform_values, :each_key, :fetch, to: :materialize

def initialize(types, values, additional_types)
def initialize(types, values, additional_types, &default)
@types = types
@values = values
@additional_types = additional_types
@materialized = false
@delegate_hash = {}
@default = default || proc {}
end

def key?(key)
Expand Down Expand Up @@ -76,9 +78,29 @@ def ==(other)
end
end

def marshal_dump
materialize
end

def marshal_load(delegate_hash)
@delegate_hash = delegate_hash
@types = {}
@values = {}
@additional_types = {}
@materialized = true
end

def encode_with(coder)
coder["delegate_hash"] = materialize
end

def init_with(coder)
marshal_load(coder["delegate_hash"])
end

protected

attr_reader :types, :values, :additional_types, :delegate_hash
attr_reader :types, :values, :additional_types, :delegate_hash, :default

def materialize
unless @materialized
Expand All @@ -101,7 +123,7 @@ def assign_default_value(name)
if value_present
delegate_hash[name] = Attribute.from_database(name, value, type)
elsif types.key?(name)
delegate_hash[name] = Attribute.uninitialized(name, type)
delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/attributes.rb
Expand Up @@ -253,7 +253,7 @@ def define_default_attribute(name, value, type, from_user:)
name,
value,
type,
_default_attributes[name],
_default_attributes.fetch(name.to_s) { nil },
)
else
default_attribute = Attribute.from_database(name, value, type)
Expand Down
6 changes: 5 additions & 1 deletion activerecord/lib/active_record/model_schema.rb
Expand Up @@ -249,7 +249,11 @@ def table_exists?
end

def attributes_builder # :nodoc:
@attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key)
@attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) do |name|
unless columns_hash.key?(name)
_default_attributes[name].dup
end
end
end

def columns_hash # :nodoc:
Expand Down
44 changes: 44 additions & 0 deletions activerecord/test/cases/attributes_test.rb
Expand Up @@ -205,5 +205,49 @@ def deserialize(*)

assert_equal(:bar, child.new(foo: :bar).foo)
end

test "attributes not backed by database columns are not dirty when unchanged" do
refute OverloadedType.new.non_existent_decimal_changed?
end

test "attributes not backed by database columns are always initialized" do
OverloadedType.create!
model = OverloadedType.first

assert_nil model.non_existent_decimal
model.non_existent_decimal = "123"
assert_equal 123, model.non_existent_decimal
end

test "attributes not backed by database columns return the default on models loaded from database" do
child = Class.new(OverloadedType) do
attribute :non_existent_decimal, :decimal, default: 123
end
child.create!
model = child.first

assert_equal 123, model.non_existent_decimal
end

test "attributes not backed by database columns properly interact with mutation and dirty" do
child = Class.new(ActiveRecord::Base) do
self.table_name = "topics"
attribute :foo, :string, default: "lol"
end
child.create!
model = child.first

assert_equal "lol", model.foo

model.foo << "asdf"
assert_equal "lolasdf", model.foo
assert model.foo_changed?

model.reload
assert_equal "lol", model.foo

model.foo = "lol"
refute model.changed?
end
end
end

0 comments on commit e7ec1e3

Please sign in to comment.