mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
0adcec4954
The extra delegation to `LazyAttributeHash` has non-negligible overhead. Avoiding that delegation makes attributes access about 45% faster for readonly (non-mutation) usage. https://gist.github.com/kamipo/4002c96a02859d8fe6503e26d7be4ad8 Before: ``` IPS Warming up -------------------------------------- attribute access 1.000 i/100ms Calculating ------------------------------------- attribute access 3.444 (± 0.0%) i/s - 18.000 in 5.259030s MEMORY Calculating ------------------------------------- attribute access 38.902M memsize ( 0.000 retained) 350.044k objects ( 0.000 retained) 15.000 strings ( 0.000 retained) ``` After (with `immutable_strings_by_default = true`): ``` IPS Warming up -------------------------------------- attribute access 1.000 i/100ms Calculating ------------------------------------- attribute access 5.066 (±19.7%) i/s - 25.000 in 5.024650s MEMORY Calculating ------------------------------------- attribute access 27.382M memsize ( 0.000 retained) 160.044k objects ( 0.000 retained) 15.000 strings ( 0.000 retained) ```
281 lines
9.6 KiB
Ruby
281 lines
9.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "cases/helper"
|
|
require "active_model/attribute_set"
|
|
|
|
module ActiveModel
|
|
class AttributeSetTest < ActiveModel::TestCase
|
|
test "building a new set from raw attributes" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
|
|
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
|
|
|
|
assert_equal 1, attributes[:foo].value
|
|
assert_equal 2.2, attributes[:bar].value
|
|
assert_equal :foo, attributes[:foo].name
|
|
assert_equal :bar, attributes[:bar].name
|
|
end
|
|
|
|
test "building with custom types" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Float.new)
|
|
attributes = builder.build_from_database({ foo: "3.3", bar: "4.4" }, { bar: Type::Integer.new })
|
|
|
|
assert_equal 3.3, attributes[:foo].value
|
|
assert_equal 4, attributes[:bar].value
|
|
end
|
|
|
|
test "[] returns a null object" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Float.new)
|
|
attributes = builder.build_from_database(foo: "3.3")
|
|
|
|
assert_equal "3.3", attributes[:foo].value_before_type_cast
|
|
assert_nil attributes[:bar].value_before_type_cast
|
|
assert_equal :bar, attributes[:bar].name
|
|
end
|
|
|
|
test "duping creates a new hash, but does not dup the attributes" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
|
|
attributes = builder.build_from_database(foo: 1, bar: "foo")
|
|
|
|
# Ensure the type cast value is cached
|
|
attributes[:foo].value
|
|
attributes[:bar].value
|
|
|
|
duped = attributes.dup
|
|
duped.write_from_database(:foo, 2)
|
|
duped[:bar].value << "bar"
|
|
|
|
assert_equal 1, attributes[:foo].value
|
|
assert_equal 2, duped[:foo].value
|
|
assert_equal "foobar", attributes[:bar].value
|
|
assert_equal "foobar", duped[:bar].value
|
|
end
|
|
|
|
test "deep_duping creates a new hash and dups each attribute" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
|
|
attributes = builder.build_from_database(foo: 1, bar: "foo")
|
|
|
|
# Ensure the type cast value is cached
|
|
attributes[:foo].value
|
|
attributes[:bar].value
|
|
|
|
duped = attributes.deep_dup
|
|
duped.write_from_database(:foo, 2)
|
|
duped[:bar].value << "bar"
|
|
|
|
assert_equal 1, attributes[:foo].value
|
|
assert_equal 2, duped[:foo].value
|
|
assert_equal "foo", attributes[:bar].value
|
|
assert_equal "foobar", duped[:bar].value
|
|
end
|
|
|
|
test "freezing cloned set does not freeze original" do
|
|
attributes = AttributeSet.new({})
|
|
clone = attributes.clone
|
|
|
|
clone.freeze
|
|
|
|
assert_predicate clone, :frozen?
|
|
assert_not_predicate attributes, :frozen?
|
|
end
|
|
|
|
test "to_hash returns a hash of the type cast values" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
|
|
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
|
|
|
|
assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
|
|
assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
|
|
end
|
|
|
|
test "to_hash maintains order" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
|
|
attributes = builder.build_from_database(foo: "2.2", bar: "3.3")
|
|
|
|
attributes[:bar]
|
|
hash = attributes.to_h
|
|
|
|
assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a
|
|
end
|
|
|
|
test "values_before_type_cast" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
|
|
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
|
|
|
|
assert_equal({ foo: "1.1", bar: "2.2" }, attributes.values_before_type_cast)
|
|
end
|
|
|
|
test "known columns are built with uninitialized attributes" do
|
|
attributes = attributes_with_uninitialized_key
|
|
assert_predicate attributes[:foo], :initialized?
|
|
assert_not_predicate attributes[:bar], :initialized?
|
|
end
|
|
|
|
test "uninitialized attributes are not included in the attributes hash" do
|
|
attributes = attributes_with_uninitialized_key
|
|
assert_equal({ foo: 1 }, attributes.to_hash)
|
|
end
|
|
|
|
test "uninitialized attributes are not included in keys" do
|
|
attributes = attributes_with_uninitialized_key
|
|
assert_equal [:foo], attributes.keys
|
|
end
|
|
|
|
test "uninitialized attributes return false for key?" do
|
|
attributes = attributes_with_uninitialized_key
|
|
assert attributes.key?(:foo)
|
|
assert_not attributes.key?(:bar)
|
|
end
|
|
|
|
test "unknown attributes return false for key?" do
|
|
attributes = attributes_with_uninitialized_key
|
|
assert_not attributes.key?(:wibble)
|
|
end
|
|
|
|
test "fetch_value returns the value for the given initialized attribute" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
|
|
attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
|
|
|
|
assert_equal 1, attributes.fetch_value(:foo)
|
|
assert_equal 2.2, attributes.fetch_value(:bar)
|
|
end
|
|
|
|
test "fetch_value returns nil for unknown attributes" do
|
|
attributes = attributes_with_uninitialized_key
|
|
assert_nil attributes.fetch_value(:wibble) { "hello" }
|
|
end
|
|
|
|
test "fetch_value returns nil for unknown attributes when types has a default" do
|
|
types = Hash.new(Type::Value.new)
|
|
builder = AttributeSet::Builder.new(types)
|
|
attributes = builder.build_from_database
|
|
|
|
assert_nil attributes.fetch_value(:wibble) { "hello" }
|
|
end
|
|
|
|
test "fetch_value uses the given block for uninitialized attributes" do
|
|
attributes = attributes_with_uninitialized_key
|
|
value = attributes.fetch_value(:bar) { |n| n.to_s + "!" }
|
|
assert_equal "bar!", value
|
|
end
|
|
|
|
test "fetch_value returns nil for uninitialized attributes if no block is given" do
|
|
attributes = attributes_with_uninitialized_key
|
|
assert_nil attributes.fetch_value(:bar)
|
|
end
|
|
|
|
test "the primary_key is always initialized" do
|
|
defaults = { foo: Attribute.from_user(:foo, nil, nil) }
|
|
builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, defaults)
|
|
attributes = builder.build_from_database
|
|
|
|
assert attributes.key?(:foo)
|
|
assert_equal [:foo], attributes.keys
|
|
assert_predicate attributes[:foo], :initialized?
|
|
end
|
|
|
|
class MyType
|
|
def cast(value)
|
|
return if value.nil?
|
|
value + " from user"
|
|
end
|
|
|
|
def deserialize(value)
|
|
return if value.nil?
|
|
value + " from database"
|
|
end
|
|
|
|
def assert_valid_value(*)
|
|
end
|
|
end
|
|
|
|
test "write_from_database sets the attribute with database typecasting" do
|
|
builder = AttributeSet::Builder.new(foo: MyType.new)
|
|
attributes = builder.build_from_database
|
|
|
|
assert_nil attributes.fetch_value(:foo)
|
|
|
|
attributes.write_from_database(:foo, "value")
|
|
|
|
assert_equal "value from database", attributes.fetch_value(:foo)
|
|
end
|
|
|
|
test "write_from_user sets the attribute with user typecasting" do
|
|
builder = AttributeSet::Builder.new(foo: MyType.new)
|
|
attributes = builder.build_from_database
|
|
|
|
assert_nil attributes.fetch_value(:foo)
|
|
|
|
attributes.write_from_user(:foo, "value")
|
|
|
|
assert_equal "value from user", attributes.fetch_value(:foo)
|
|
end
|
|
|
|
test "freezing doesn't prevent the set from materializing" do
|
|
builder = AttributeSet::Builder.new(foo: Type::String.new)
|
|
attributes = builder.build_from_database(foo: "1")
|
|
|
|
attributes.freeze
|
|
assert_equal({ foo: "1" }, attributes.to_hash)
|
|
end
|
|
|
|
test "marshalling dump/load legacy materialized attribute hash" do
|
|
builder = AttributeSet::Builder.new(foo: Type::String.new)
|
|
|
|
def builder.build_from_database(values = {}, additional_types = {})
|
|
attributes = LazyAttributeHash.new(types, values, additional_types, default_attributes)
|
|
AttributeSet.new(attributes)
|
|
end
|
|
|
|
attributes = builder.build_from_database(foo: "1")
|
|
|
|
attributes.instance_variable_get(:@attributes).instance_eval do
|
|
class << self
|
|
def marshal_dump
|
|
materialize # legacy marshal format before Rails 5.1
|
|
end
|
|
end
|
|
end
|
|
|
|
data = Marshal.dump(attributes)
|
|
attributes = assert_deprecated { Marshal.load(data) }
|
|
assert_equal({ foo: "1" }, attributes.to_hash)
|
|
end
|
|
|
|
test "#accessed_attributes returns only attributes which have been read" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new)
|
|
attributes = builder.build_from_database(foo: "1", bar: "2")
|
|
|
|
assert_equal [], attributes.accessed
|
|
|
|
attributes.fetch_value(:foo)
|
|
|
|
assert_equal [:foo], attributes.accessed
|
|
end
|
|
|
|
test "#map returns a new attribute set with the changes applied" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
|
|
attributes = builder.build_from_database(foo: "1", bar: "2")
|
|
new_attributes = attributes.map do |attr|
|
|
attr.with_cast_value(attr.value + 1)
|
|
end
|
|
|
|
assert_equal 2, new_attributes.fetch_value(:foo)
|
|
assert_equal 3, new_attributes.fetch_value(:bar)
|
|
end
|
|
|
|
test "comparison for equality is correctly implemented" do
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
|
|
attributes = builder.build_from_database(foo: "1", bar: "2")
|
|
attributes2 = builder.build_from_database(foo: "1", bar: "2")
|
|
attributes3 = builder.build_from_database(foo: "2", bar: "2")
|
|
|
|
assert_equal attributes, attributes2
|
|
assert_not_equal attributes2, attributes3
|
|
end
|
|
|
|
private
|
|
def attributes_with_uninitialized_key
|
|
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
|
|
builder.build_from_database(foo: "1.1")
|
|
end
|
|
end
|
|
end
|