1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activerecord/test/cases/attributes_test.rb
Sean Griffin 2f9d88954c Persist user provided default values, even if unchanged
This is a usability change to fix a quirk from our definition of partial
writes. By default, we only persist changed attributes. When creating a
new record, this is assumed that the default values came from the
database. However, if the user provided a default, it will not be
persisted, since we didn't see it as "changed". Since this is a very
specific case, I wanted to isolate it with the other quirks that come
from user provided default values. The number of edge cases which are
presenting themselves are starting to make me wonder if we should just
remove the ability to assign a default, in favor of overriding
`initialize`. For the time being, this is required for the attributes
API to not have confusing behavior.

We had to delete one test, since this actually changes the meaning of
`.changed?` on Active Record models. It now specifically means
`changed_from_database?`. While I think this will make the attributes
API more ergonomic to use, it is a subtle change in definition (though
not a backwards incompatible one). We should probably figure out the
right place to document this. (Feel free to open a PR doing that if
you're reading this).

/cc @rafaelfranca @kirs @senny

This is an alternate implementation of #19921.

Close #19921.

[Sean Griffin & Kir Shatrov]
2015-05-28 16:40:26 -06:00

176 lines
5.5 KiB
Ruby

require 'cases/helper'
class OverloadedType < ActiveRecord::Base
attribute :overloaded_float, :integer
attribute :overloaded_string_with_limit, :string, limit: 50
attribute :non_existent_decimal, :decimal
attribute :string_with_default, :string, default: 'the overloaded default'
end
class ChildOfOverloadedType < OverloadedType
end
class GrandchildOfOverloadedType < ChildOfOverloadedType
attribute :overloaded_float, :float
end
class UnoverloadedType < ActiveRecord::Base
self.table_name = 'overloaded_types'
end
module ActiveRecord
class CustomPropertiesTest < ActiveRecord::TestCase
test "overloading types" do
data = OverloadedType.new
data.overloaded_float = "1.1"
data.unoverloaded_float = "1.1"
assert_equal 1, data.overloaded_float
assert_equal 1.1, data.unoverloaded_float
end
test "overloaded properties save" do
data = OverloadedType.new
data.overloaded_float = "2.2"
data.save!
data.reload
assert_equal 2, data.overloaded_float
assert_kind_of Fixnum, OverloadedType.last.overloaded_float
assert_equal 2.0, UnoverloadedType.last.overloaded_float
assert_kind_of Float, UnoverloadedType.last.overloaded_float
end
test "properties assigned in constructor" do
data = OverloadedType.new(overloaded_float: '3.3')
assert_equal 3, data.overloaded_float
end
test "overloaded properties with limit" do
assert_equal 50, OverloadedType.type_for_attribute('overloaded_string_with_limit').limit
assert_equal 255, UnoverloadedType.type_for_attribute('overloaded_string_with_limit').limit
end
test "nonexistent attribute" do
data = OverloadedType.new(non_existent_decimal: 1)
assert_equal BigDecimal.new(1), data.non_existent_decimal
assert_raise ActiveRecord::UnknownAttributeError do
UnoverloadedType.new(non_existent_decimal: 1)
end
end
test "changing defaults" do
data = OverloadedType.new
unoverloaded_data = UnoverloadedType.new
assert_equal 'the overloaded default', data.string_with_default
assert_equal 'the original default', unoverloaded_data.string_with_default
end
test "defaults are not touched on the columns" do
assert_equal 'the original default', OverloadedType.columns_hash['string_with_default'].default
end
test "children inherit custom properties" do
data = ChildOfOverloadedType.new(overloaded_float: '4.4')
assert_equal 4, data.overloaded_float
end
test "children can override parents" do
data = GrandchildOfOverloadedType.new(overloaded_float: '4.4')
assert_equal 4.4, data.overloaded_float
end
test "overloading properties does not attribute method order" do
attribute_names = OverloadedType.attribute_names
assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), attribute_names
end
test "caches are cleared" do
klass = Class.new(OverloadedType)
assert_equal 6, klass.attribute_types.length
assert_equal 6, klass.column_defaults.length
assert_not klass.attribute_types.include?('wibble')
klass.attribute :wibble, Type::Value.new
assert_equal 7, klass.attribute_types.length
assert_equal 7, klass.column_defaults.length
assert klass.attribute_types.include?('wibble')
end
test "the given default value is cast from user" do
custom_type = Class.new(Type::Value) do
def cast(*)
"from user"
end
def deserialize(*)
"from database"
end
end
klass = Class.new(OverloadedType) do
attribute :wibble, custom_type.new, default: "default"
end
model = klass.new
assert_equal "from user", model.wibble
end
test "procs for default values" do
klass = Class.new(OverloadedType) do
@@counter = 0
attribute :counter, :integer, default: -> { @@counter += 1 }
end
assert_equal 1, klass.new.counter
assert_equal 2, klass.new.counter
end
test "user provided defaults are persisted even if unchanged" do
model = OverloadedType.create!
assert_equal "the overloaded default", model.reload.string_with_default
end
if current_adapter?(:PostgreSQLAdapter)
test "arrays types can be specified" do
klass = Class.new(OverloadedType) do
attribute :my_array, :string, limit: 50, array: true
attribute :my_int_array, :integer, array: true
end
string_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
Type::String.new(limit: 50))
int_array = ConnectionAdapters::PostgreSQL::OID::Array.new(
Type::Integer.new)
refute_equal string_array, int_array
assert_equal string_array, klass.type_for_attribute("my_array")
assert_equal int_array, klass.type_for_attribute("my_int_array")
end
test "range types can be specified" do
klass = Class.new(OverloadedType) do
attribute :my_range, :string, limit: 50, range: true
attribute :my_int_range, :integer, range: true
end
string_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
Type::String.new(limit: 50))
int_range = ConnectionAdapters::PostgreSQL::OID::Range.new(
Type::Integer.new)
refute_equal string_range, int_range
assert_equal string_range, klass.type_for_attribute("my_range")
assert_equal int_range, klass.type_for_attribute("my_int_range")
end
end
end
end