2017-07-09 13:41:28 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
require "cases/helper"
|
2014-05-23 14:24:52 -04:00
|
|
|
|
|
|
|
class OverloadedType < ActiveRecord::Base
|
2015-02-06 13:05:38 -05:00
|
|
|
attribute :overloaded_float, :integer
|
|
|
|
attribute :overloaded_string_with_limit, :string, limit: 50
|
|
|
|
attribute :non_existent_decimal, :decimal
|
2016-08-06 12:26:20 -04:00
|
|
|
attribute :string_with_default, :string, default: "the overloaded default"
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
|
|
|
|
2014-05-28 13:11:57 -04:00
|
|
|
class ChildOfOverloadedType < OverloadedType
|
|
|
|
end
|
|
|
|
|
|
|
|
class GrandchildOfOverloadedType < ChildOfOverloadedType
|
2015-02-06 13:05:38 -05:00
|
|
|
attribute :overloaded_float, :float
|
2014-05-28 13:11:57 -04:00
|
|
|
end
|
|
|
|
|
2014-05-23 14:24:52 -04:00
|
|
|
class UnoverloadedType < ActiveRecord::Base
|
2016-08-06 12:26:20 -04:00
|
|
|
self.table_name = "overloaded_types"
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
module ActiveRecord
|
|
|
|
class CustomPropertiesTest < ActiveRecord::TestCase
|
2014-10-31 18:06:14 -04:00
|
|
|
test "overloading types" do
|
2014-05-23 14:24:52 -04:00
|
|
|
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
|
|
|
|
|
2014-10-31 18:06:14 -04:00
|
|
|
test "overloaded properties save" do
|
2014-05-23 14:24:52 -04:00
|
|
|
data = OverloadedType.new
|
|
|
|
|
|
|
|
data.overloaded_float = "2.2"
|
|
|
|
data.save!
|
|
|
|
data.reload
|
|
|
|
|
|
|
|
assert_equal 2, data.overloaded_float
|
2016-05-17 10:56:08 -04:00
|
|
|
assert_kind_of Integer, OverloadedType.last.overloaded_float
|
2014-05-23 14:24:52 -04:00
|
|
|
assert_equal 2.0, UnoverloadedType.last.overloaded_float
|
2014-05-29 09:55:44 -04:00
|
|
|
assert_kind_of Float, UnoverloadedType.last.overloaded_float
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
|
|
|
|
2014-10-31 18:06:14 -04:00
|
|
|
test "properties assigned in constructor" do
|
2016-08-06 12:26:20 -04:00
|
|
|
data = OverloadedType.new(overloaded_float: "3.3")
|
2014-05-23 14:24:52 -04:00
|
|
|
|
|
|
|
assert_equal 3, data.overloaded_float
|
|
|
|
end
|
|
|
|
|
2014-10-31 18:06:14 -04:00
|
|
|
test "overloaded properties with limit" do
|
2016-08-06 12:26:20 -04:00
|
|
|
assert_equal 50, OverloadedType.type_for_attribute("overloaded_string_with_limit").limit
|
|
|
|
assert_equal 255, UnoverloadedType.type_for_attribute("overloaded_string_with_limit").limit
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
|
|
|
|
2020-07-07 08:15:49 -04:00
|
|
|
test "overloaded default but keeping its own type" do
|
|
|
|
klass = Class.new(UnoverloadedType) do
|
|
|
|
attribute :overloaded_string_with_limit, default: "the overloaded default"
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_equal 255, UnoverloadedType.type_for_attribute("overloaded_string_with_limit").limit
|
|
|
|
assert_equal 255, klass.type_for_attribute("overloaded_string_with_limit").limit
|
|
|
|
|
|
|
|
assert_nil UnoverloadedType.new.overloaded_string_with_limit
|
|
|
|
assert_equal "the overloaded default", klass.new.overloaded_string_with_limit
|
|
|
|
end
|
|
|
|
|
2020-07-26 13:58:45 -04:00
|
|
|
test "attributes with overridden types keep their type when a default value is configured separately" do
|
|
|
|
child = Class.new(OverloadedType) do
|
|
|
|
attribute :overloaded_float, default: "123"
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_equal OverloadedType.type_for_attribute("overloaded_float"), child.type_for_attribute("overloaded_float")
|
|
|
|
assert_equal 123, child.new.overloaded_float
|
|
|
|
end
|
|
|
|
|
2019-03-20 12:20:51 -04:00
|
|
|
test "extra options are forwarded to the type caster constructor" do
|
|
|
|
klass = Class.new(OverloadedType) do
|
2020-07-19 13:01:52 -04:00
|
|
|
attribute :starts_at, :datetime, precision: 3, limit: 2, scale: 1, default: -> { Time.now.utc }
|
2019-03-20 12:20:51 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
starts_at_type = klass.type_for_attribute(:starts_at)
|
2020-07-20 04:55:10 -04:00
|
|
|
|
2019-03-20 12:20:51 -04:00
|
|
|
assert_equal 3, starts_at_type.precision
|
|
|
|
assert_equal 2, starts_at_type.limit
|
|
|
|
assert_equal 1, starts_at_type.scale
|
2020-07-19 13:01:52 -04:00
|
|
|
|
2020-07-20 04:55:10 -04:00
|
|
|
assert_kind_of Type::DateTime, starts_at_type
|
2020-07-19 13:01:52 -04:00
|
|
|
assert_instance_of Time, klass.new.starts_at
|
|
|
|
end
|
|
|
|
|
|
|
|
test "time zone aware attribute" do
|
|
|
|
with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
|
|
|
|
klass = Class.new(OverloadedType) do
|
|
|
|
attribute :starts_at, :datetime, precision: 3, default: -> { Time.now.utc }
|
2020-07-20 04:55:10 -04:00
|
|
|
attribute :ends_at, default: -> { Time.now.utc }
|
2020-07-19 13:01:52 -04:00
|
|
|
end
|
|
|
|
|
2020-07-20 04:55:10 -04:00
|
|
|
starts_at_type = klass.type_for_attribute(:starts_at)
|
|
|
|
ends_at_type = klass.type_for_attribute(:ends_at)
|
|
|
|
|
|
|
|
assert_instance_of AttributeMethods::TimeZoneConversion::TimeZoneConverter, starts_at_type
|
|
|
|
assert_instance_of AttributeMethods::TimeZoneConversion::TimeZoneConverter, ends_at_type
|
|
|
|
assert_kind_of Type::DateTime, starts_at_type.__getobj__
|
|
|
|
assert_kind_of Type::DateTime, ends_at_type.__getobj__
|
2020-07-19 13:01:52 -04:00
|
|
|
assert_instance_of ActiveSupport::TimeWithZone, klass.new.starts_at
|
2020-07-20 04:55:10 -04:00
|
|
|
assert_instance_of ActiveSupport::TimeWithZone, klass.new.ends_at
|
2020-07-19 13:01:52 -04:00
|
|
|
end
|
2019-03-20 12:20:51 -04:00
|
|
|
end
|
|
|
|
|
2014-10-31 18:06:14 -04:00
|
|
|
test "nonexistent attribute" do
|
2014-05-23 14:24:52 -04:00
|
|
|
data = OverloadedType.new(non_existent_decimal: 1)
|
|
|
|
|
Suppress `warning: BigDecimal.new is deprecated` in activerecord
`BigDecimal.new` has been deprecated in BigDecimal 1.3.3
which will be a default for Ruby 2.5.
Refer https://github.com/ruby/bigdecimal/commit/533737338db915b00dc7168c3602e4b462b23503
```
$ cd rails/activerecord/
$ git grep -l BigDecimal.new | grep \.rb | xargs sed -i -e "s/BigDecimal.new/BigDecimal/g"
```
- Changes made only to Active Record. Will apply the same change to
other module once this commit is merged.
- The following deprecation has not been addressed because it has been
reported at `ActiveRecord::Result.new`. `ActiveRecord::Result.ancestors`
did not show `BigDecimal`.
* Not addressed
```ruby
/path/to/rails/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb:34:
warning: BigDecimal.new is deprecated
```
* database_statements.rb:34
```ruby
ActiveRecord::Result.new(result.fields, result.to_a) if result
```
* ActiveRecord::Result.ancestors
```ruby
[ActiveRecord::Result,
Enumerable,
ActiveSupport::ToJsonWithActiveSupportEncoder,
Object,
Metaclass::ObjectMethods,
Mocha::ObjectMethods,
PP::ObjectMixin,
ActiveSupport::Dependencies::Loadable,
ActiveSupport::Tryable,
JSON::Ext::Generator::GeneratorMethods::Object,
Kernel,
BasicObject]
```
This commit has been tested with these Ruby and BigDecimal versions
- ruby 2.5 and bigdecimal 1.3.3
```
$ ruby -v
ruby 2.5.0dev (2017-12-14 trunk 61217) [x86_64-linux]
$ gem list |grep bigdecimal
bigdecimal (default: 1.3.3, default: 1.3.2)
```
- ruby 2.4 and bigdecimal 1.3.0
```
$ ruby -v
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-linux-gnu]
$ gem list |grep bigdecimal
bigdecimal (default: 1.3.0)
```
- ruby 2.3 and bigdecimal 1.2.8
```
$ ruby -v
ruby 2.3.5p376 (2017-09-14 revision 59905) [x86_64-linux]
$ gem list |grep -i bigdecimal
bigdecimal (1.2.8)
```
- ruby 2.2 and bigdecimal 1.2.6
```
$ ruby -v
ruby 2.2.8p477 (2017-09-14 revision 59906) [x86_64-linux]
$ gem list |grep bigdecimal
bigdecimal (1.2.6)
```
2017-12-13 14:46:46 -05:00
|
|
|
assert_equal BigDecimal(1), data.non_existent_decimal
|
2015-02-25 09:14:43 -05:00
|
|
|
assert_raise ActiveRecord::UnknownAttributeError do
|
2014-05-23 14:24:52 -04:00
|
|
|
UnoverloadedType.new(non_existent_decimal: 1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-05-11 13:09:34 -04:00
|
|
|
test "model with nonexistent attribute with default value can be saved" do
|
|
|
|
klass = Class.new(OverloadedType) do
|
2016-08-06 12:26:20 -04:00
|
|
|
attribute :non_existent_string_with_default, :string, default: "nonexistent"
|
2016-05-11 13:09:34 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
model = klass.new
|
|
|
|
assert model.save
|
|
|
|
end
|
|
|
|
|
2014-10-31 18:06:14 -04:00
|
|
|
test "changing defaults" do
|
2014-05-23 14:24:52 -04:00
|
|
|
data = OverloadedType.new
|
|
|
|
unoverloaded_data = UnoverloadedType.new
|
|
|
|
|
2016-08-06 12:26:20 -04:00
|
|
|
assert_equal "the overloaded default", data.string_with_default
|
|
|
|
assert_equal "the original default", unoverloaded_data.string_with_default
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
2014-05-28 13:11:57 -04:00
|
|
|
|
2014-10-31 18:06:14 -04:00
|
|
|
test "defaults are not touched on the columns" do
|
2016-08-06 12:26:20 -04:00
|
|
|
assert_equal "the original default", OverloadedType.columns_hash["string_with_default"].default
|
2014-10-31 18:06:14 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
test "children inherit custom properties" do
|
2016-08-06 12:26:20 -04:00
|
|
|
data = ChildOfOverloadedType.new(overloaded_float: "4.4")
|
2014-05-28 13:11:57 -04:00
|
|
|
|
|
|
|
assert_equal 4, data.overloaded_float
|
|
|
|
end
|
|
|
|
|
2014-10-31 18:06:14 -04:00
|
|
|
test "children can override parents" do
|
2016-08-06 12:26:20 -04:00
|
|
|
data = GrandchildOfOverloadedType.new(overloaded_float: "4.4")
|
2014-05-28 13:11:57 -04:00
|
|
|
|
|
|
|
assert_equal 4.4, data.overloaded_float
|
|
|
|
end
|
2014-05-28 13:32:00 -04:00
|
|
|
|
Attribute assignment and type casting has nothing to do with columns
It's finally finished!!!!!!! The reason the Attributes API was kept
private in 4.2 was due to some publicly visible implementation details.
It was previously implemented by overloading `columns` and
`columns_hash`, to make them return column objects which were modified
with the attribute information.
This meant that those methods LIED! We didn't change the database
schema. We changed the attribute information on the class. That is
wrong! It should be the other way around, where schema loading just
calls the attributes API for you. And now it does!
Yes, this means that there is nothing that happens in automatic schema
loading that you couldn't manually do yourself. (There's still some
funky cases where we hit the connection adapter that I need to handle,
before we can turn off automatic schema detection entirely.)
There were a few weird test failures caused by this that had to be
fixed. The main source came from the fact that the attribute methods are
now defined in terms of `attribute_names`, which has a clause like
`return [] unless table_exists?`. I don't *think* this is an issue,
since the only place this caused failures were in a fake adapter which
didn't override `table_exists?`.
Additionally, there were a few cases where tests were failing because a
migration was run, but the model was not reloaded. I'm not sure why
these started failing from this change, I might need to clear an
additional cache in `reload_schema_from_cache`. Again, since this is not
normal usage, and it's expected that `reset_column_information` will be
called after the table is modified, I don't think it's a problem.
Still, test failures that were unrelated to the change are worrying, and
I need to dig into them further.
Finally, I spent a lot of time debugging issues with the mutex used in
`define_attribute_methods`. I think we can just remove that method
entirely, and define the attribute methods *manually* in the call to
`define_attribute`, which would simplify the code *tremendously*.
Ok. now to make this damn thing public, and work on moving it up to
Active Model.
2015-01-30 16:03:36 -05:00
|
|
|
test "overloading properties does not attribute method order" do
|
|
|
|
attribute_names = OverloadedType.attribute_names
|
Add a setting to specify that all string columns should be immutable
In Rails 4.2 we introduced mutation detection, to remove the need to
call `attribute_will_change!` after modifying a field. One side effect
of that change was that we needed to enforce that the
`_before_type_cast` form of a value is a different object than the post
type cast value, if the value is mutable. That contract is really only
relevant for strings, but it meant we needed to dup them.
In Rails 5 we added the `ImmutableString` type, to allow people to opt
out of this duping in places where the memory usage was causing
problems, and they don't need to mutate that field.
This takes that a step further, and adds a class-level setting to
specify whether strings should be frozen by default or not. The default
value of this setting is `false` (strings are mutable), and I do not
plan on changing that.
While I think that immutable strings by default would be a reasonable
default for new applications, I do not think that we would be able to
document the value of this setting in a place that people will be
looking when they can't figure out why some string is frozen.
Realistically, the number of applications where this setting is relevant
is fairly small, so I don't think it would make sense to ever enable it
by default.
2017-07-25 11:37:05 -04:00
|
|
|
expected = OverloadedType.column_names + ["non_existent_decimal"]
|
|
|
|
assert_equal expected, attribute_names
|
2014-05-28 13:32:00 -04:00
|
|
|
end
|
2014-05-30 16:51:27 -04:00
|
|
|
|
2014-10-31 18:06:14 -04:00
|
|
|
test "caches are cleared" do
|
2014-05-30 16:51:27 -04:00
|
|
|
klass = Class.new(OverloadedType)
|
Add a setting to specify that all string columns should be immutable
In Rails 4.2 we introduced mutation detection, to remove the need to
call `attribute_will_change!` after modifying a field. One side effect
of that change was that we needed to enforce that the
`_before_type_cast` form of a value is a different object than the post
type cast value, if the value is mutable. That contract is really only
relevant for strings, but it meant we needed to dup them.
In Rails 5 we added the `ImmutableString` type, to allow people to opt
out of this duping in places where the memory usage was causing
problems, and they don't need to mutate that field.
This takes that a step further, and adds a class-level setting to
specify whether strings should be frozen by default or not. The default
value of this setting is `false` (strings are mutable), and I do not
plan on changing that.
While I think that immutable strings by default would be a reasonable
default for new applications, I do not think that we would be able to
document the value of this setting in a place that people will be
looking when they can't figure out why some string is frozen.
Realistically, the number of applications where this setting is relevant
is fairly small, so I don't think it would make sense to ever enable it
by default.
2017-07-25 11:37:05 -04:00
|
|
|
column_count = klass.columns.length
|
2014-05-30 16:51:27 -04:00
|
|
|
|
Add a setting to specify that all string columns should be immutable
In Rails 4.2 we introduced mutation detection, to remove the need to
call `attribute_will_change!` after modifying a field. One side effect
of that change was that we needed to enforce that the
`_before_type_cast` form of a value is a different object than the post
type cast value, if the value is mutable. That contract is really only
relevant for strings, but it meant we needed to dup them.
In Rails 5 we added the `ImmutableString` type, to allow people to opt
out of this duping in places where the memory usage was causing
problems, and they don't need to mutate that field.
This takes that a step further, and adds a class-level setting to
specify whether strings should be frozen by default or not. The default
value of this setting is `false` (strings are mutable), and I do not
plan on changing that.
While I think that immutable strings by default would be a reasonable
default for new applications, I do not think that we would be able to
document the value of this setting in a place that people will be
looking when they can't figure out why some string is frozen.
Realistically, the number of applications where this setting is relevant
is fairly small, so I don't think it would make sense to ever enable it
by default.
2017-07-25 11:37:05 -04:00
|
|
|
assert_equal column_count + 1, klass.attribute_types.length
|
|
|
|
assert_equal column_count + 1, klass.column_defaults.length
|
|
|
|
assert_equal column_count + 1, klass.attribute_names.length
|
2016-08-06 12:26:20 -04:00
|
|
|
assert_not klass.attribute_types.include?("wibble")
|
2014-05-30 16:51:27 -04:00
|
|
|
|
2014-06-07 09:09:45 -04:00
|
|
|
klass.attribute :wibble, Type::Value.new
|
2014-05-30 16:51:27 -04:00
|
|
|
|
Add a setting to specify that all string columns should be immutable
In Rails 4.2 we introduced mutation detection, to remove the need to
call `attribute_will_change!` after modifying a field. One side effect
of that change was that we needed to enforce that the
`_before_type_cast` form of a value is a different object than the post
type cast value, if the value is mutable. That contract is really only
relevant for strings, but it meant we needed to dup them.
In Rails 5 we added the `ImmutableString` type, to allow people to opt
out of this duping in places where the memory usage was causing
problems, and they don't need to mutate that field.
This takes that a step further, and adds a class-level setting to
specify whether strings should be frozen by default or not. The default
value of this setting is `false` (strings are mutable), and I do not
plan on changing that.
While I think that immutable strings by default would be a reasonable
default for new applications, I do not think that we would be able to
document the value of this setting in a place that people will be
looking when they can't figure out why some string is frozen.
Realistically, the number of applications where this setting is relevant
is fairly small, so I don't think it would make sense to ever enable it
by default.
2017-07-25 11:37:05 -04:00
|
|
|
assert_equal column_count + 2, klass.attribute_types.length
|
|
|
|
assert_equal column_count + 2, klass.column_defaults.length
|
|
|
|
assert_equal column_count + 2, klass.attribute_names.length
|
2016-09-16 12:44:05 -04:00
|
|
|
assert_includes klass.attribute_types, "wibble"
|
Attribute assignment and type casting has nothing to do with columns
It's finally finished!!!!!!! The reason the Attributes API was kept
private in 4.2 was due to some publicly visible implementation details.
It was previously implemented by overloading `columns` and
`columns_hash`, to make them return column objects which were modified
with the attribute information.
This meant that those methods LIED! We didn't change the database
schema. We changed the attribute information on the class. That is
wrong! It should be the other way around, where schema loading just
calls the attributes API for you. And now it does!
Yes, this means that there is nothing that happens in automatic schema
loading that you couldn't manually do yourself. (There's still some
funky cases where we hit the connection adapter that I need to handle,
before we can turn off automatic schema detection entirely.)
There were a few weird test failures caused by this that had to be
fixed. The main source came from the fact that the attribute methods are
now defined in terms of `attribute_names`, which has a clause like
`return [] unless table_exists?`. I don't *think* this is an issue,
since the only place this caused failures were in a fake adapter which
didn't override `table_exists?`.
Additionally, there were a few cases where tests were failing because a
migration was run, but the model was not reloaded. I'm not sure why
these started failing from this change, I might need to clear an
additional cache in `reload_schema_from_cache`. Again, since this is not
normal usage, and it's expected that `reset_column_information` will be
called after the table is modified, I don't think it's a problem.
Still, test failures that were unrelated to the change are worrying, and
I need to dig into them further.
Finally, I spent a lot of time debugging issues with the mutex used in
`define_attribute_methods`. I think we can just remove that method
entirely, and define the attribute methods *manually* in the call to
`define_attribute`, which would simplify the code *tremendously*.
Ok. now to make this damn thing public, and work on moving it up to
Active Model.
2015-01-30 16:03:36 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
test "the given default value is cast from user" do
|
|
|
|
custom_type = Class.new(Type::Value) do
|
2015-02-17 15:39:42 -05:00
|
|
|
def cast(*)
|
Attribute assignment and type casting has nothing to do with columns
It's finally finished!!!!!!! The reason the Attributes API was kept
private in 4.2 was due to some publicly visible implementation details.
It was previously implemented by overloading `columns` and
`columns_hash`, to make them return column objects which were modified
with the attribute information.
This meant that those methods LIED! We didn't change the database
schema. We changed the attribute information on the class. That is
wrong! It should be the other way around, where schema loading just
calls the attributes API for you. And now it does!
Yes, this means that there is nothing that happens in automatic schema
loading that you couldn't manually do yourself. (There's still some
funky cases where we hit the connection adapter that I need to handle,
before we can turn off automatic schema detection entirely.)
There were a few weird test failures caused by this that had to be
fixed. The main source came from the fact that the attribute methods are
now defined in terms of `attribute_names`, which has a clause like
`return [] unless table_exists?`. I don't *think* this is an issue,
since the only place this caused failures were in a fake adapter which
didn't override `table_exists?`.
Additionally, there were a few cases where tests were failing because a
migration was run, but the model was not reloaded. I'm not sure why
these started failing from this change, I might need to clear an
additional cache in `reload_schema_from_cache`. Again, since this is not
normal usage, and it's expected that `reset_column_information` will be
called after the table is modified, I don't think it's a problem.
Still, test failures that were unrelated to the change are worrying, and
I need to dig into them further.
Finally, I spent a lot of time debugging issues with the mutex used in
`define_attribute_methods`. I think we can just remove that method
entirely, and define the attribute methods *manually* in the call to
`define_attribute`, which would simplify the code *tremendously*.
Ok. now to make this damn thing public, and work on moving it up to
Active Model.
2015-01-30 16:03:36 -05:00
|
|
|
"from user"
|
|
|
|
end
|
|
|
|
|
2015-02-17 13:29:51 -05:00
|
|
|
def deserialize(*)
|
Attribute assignment and type casting has nothing to do with columns
It's finally finished!!!!!!! The reason the Attributes API was kept
private in 4.2 was due to some publicly visible implementation details.
It was previously implemented by overloading `columns` and
`columns_hash`, to make them return column objects which were modified
with the attribute information.
This meant that those methods LIED! We didn't change the database
schema. We changed the attribute information on the class. That is
wrong! It should be the other way around, where schema loading just
calls the attributes API for you. And now it does!
Yes, this means that there is nothing that happens in automatic schema
loading that you couldn't manually do yourself. (There's still some
funky cases where we hit the connection adapter that I need to handle,
before we can turn off automatic schema detection entirely.)
There were a few weird test failures caused by this that had to be
fixed. The main source came from the fact that the attribute methods are
now defined in terms of `attribute_names`, which has a clause like
`return [] unless table_exists?`. I don't *think* this is an issue,
since the only place this caused failures were in a fake adapter which
didn't override `table_exists?`.
Additionally, there were a few cases where tests were failing because a
migration was run, but the model was not reloaded. I'm not sure why
these started failing from this change, I might need to clear an
additional cache in `reload_schema_from_cache`. Again, since this is not
normal usage, and it's expected that `reset_column_information` will be
called after the table is modified, I don't think it's a problem.
Still, test failures that were unrelated to the change are worrying, and
I need to dig into them further.
Finally, I spent a lot of time debugging issues with the mutex used in
`define_attribute_methods`. I think we can just remove that method
entirely, and define the attribute methods *manually* in the call to
`define_attribute`, which would simplify the code *tremendously*.
Ok. now to make this damn thing public, and work on moving it up to
Active Model.
2015-01-30 16:03:36 -05:00
|
|
|
"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
|
2014-05-30 16:51:27 -04:00
|
|
|
end
|
2015-02-06 13:05:38 -05:00
|
|
|
|
2015-05-28 18:26:49 -04:00
|
|
|
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
|
|
|
|
|
2018-09-19 23:58:43 -04:00
|
|
|
test "procs for default values are evaluated even after column_defaults is called" do
|
|
|
|
klass = Class.new(OverloadedType) do
|
|
|
|
@@counter = 0
|
|
|
|
attribute :counter, :integer, default: -> { @@counter += 1 }
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_equal 1, klass.new.counter
|
|
|
|
|
|
|
|
# column_defaults will increment the counter since the proc is called
|
|
|
|
klass.column_defaults
|
|
|
|
|
|
|
|
assert_equal 3, klass.new.counter
|
|
|
|
end
|
|
|
|
|
2016-03-24 16:54:02 -04:00
|
|
|
test "procs are memoized before type casting" do
|
|
|
|
klass = Class.new(OverloadedType) do
|
|
|
|
@@counter = 0
|
|
|
|
attribute :counter, :integer, default: -> { @@counter += 1 }
|
|
|
|
end
|
|
|
|
|
|
|
|
model = klass.new
|
|
|
|
assert_equal 1, model.counter_before_type_cast
|
|
|
|
assert_equal 1, model.counter_before_type_cast
|
|
|
|
end
|
|
|
|
|
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 18:33:38 -04:00
|
|
|
test "user provided defaults are persisted even if unchanged" do
|
|
|
|
model = OverloadedType.create!
|
|
|
|
|
|
|
|
assert_equal "the overloaded default", model.reload.string_with_default
|
|
|
|
end
|
|
|
|
|
2015-02-06 13:05:38 -05:00
|
|
|
if current_adapter?(:PostgreSQLAdapter)
|
2015-07-20 11:22:30 -04:00
|
|
|
test "array types can be specified" do
|
2015-02-06 13:05:38 -05:00
|
|
|
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)
|
2015-08-13 03:20:39 -04:00
|
|
|
assert_not_equal string_array, int_array
|
2015-02-06 13:05:38 -05:00
|
|
|
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)
|
2015-08-13 03:20:39 -04:00
|
|
|
assert_not_equal string_range, int_range
|
2015-02-06 13:05:38 -05:00
|
|
|
assert_equal string_range, klass.type_for_attribute("my_range")
|
|
|
|
assert_equal int_range, klass.type_for_attribute("my_int_range")
|
|
|
|
end
|
|
|
|
end
|
2015-11-07 10:17:25 -05:00
|
|
|
|
|
|
|
test "attributes added after subclasses load are inherited" do
|
|
|
|
parent = Class.new(ActiveRecord::Base) do
|
|
|
|
self.table_name = "topics"
|
|
|
|
end
|
|
|
|
|
|
|
|
child = Class.new(parent)
|
|
|
|
child.new # => force a schema load
|
|
|
|
|
|
|
|
parent.attribute(:foo, Type::Value.new)
|
|
|
|
|
|
|
|
assert_equal(:bar, child.new(foo: :bar).foo)
|
|
|
|
end
|
2016-07-25 11:06:04 -04:00
|
|
|
|
|
|
|
test "attributes not backed by database columns are not dirty when unchanged" do
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate OverloadedType.new, :non_existent_decimal_changed?
|
2016-07-25 11:06:04 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
test "attributes not backed by database columns are always initialized" do
|
|
|
|
OverloadedType.create!
|
2019-06-14 19:19:31 -04:00
|
|
|
model = OverloadedType.last
|
2016-07-25 11:06:04 -04:00
|
|
|
|
|
|
|
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!
|
2019-06-14 19:19:31 -04:00
|
|
|
model = child.last
|
2016-07-25 11:06:04 -04:00
|
|
|
|
|
|
|
assert_equal 123, model.non_existent_decimal
|
|
|
|
end
|
|
|
|
|
2020-07-26 13:58:45 -04:00
|
|
|
test "attributes not backed by database columns keep their type when a default value is configured separately" do
|
|
|
|
child = Class.new(OverloadedType) do
|
|
|
|
attribute :non_existent_decimal, default: "123"
|
|
|
|
end
|
|
|
|
|
|
|
|
assert_equal OverloadedType.type_for_attribute("non_existent_decimal"), child.type_for_attribute("non_existent_decimal")
|
|
|
|
assert_equal 123, child.new.non_existent_decimal
|
|
|
|
end
|
|
|
|
|
2016-07-25 11:06:04 -04:00
|
|
|
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!
|
2019-06-14 19:19:31 -04:00
|
|
|
model = child.last
|
2016-07-25 11:06:04 -04:00
|
|
|
|
|
|
|
assert_equal "lol", model.foo
|
|
|
|
|
|
|
|
model.foo << "asdf"
|
|
|
|
assert_equal "lolasdf", model.foo
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_predicate model, :foo_changed?
|
2016-07-25 11:06:04 -04:00
|
|
|
|
|
|
|
model.reload
|
|
|
|
assert_equal "lol", model.foo
|
|
|
|
|
|
|
|
model.foo = "lol"
|
2018-01-25 18:14:09 -05:00
|
|
|
assert_not_predicate model, :changed?
|
2016-07-25 11:06:04 -04:00
|
|
|
end
|
2016-08-31 14:46:40 -04:00
|
|
|
|
|
|
|
test "attributes not backed by database columns appear in inspect" do
|
|
|
|
inspection = OverloadedType.new.inspect
|
|
|
|
|
2016-09-16 12:44:05 -04:00
|
|
|
assert_includes inspection, "non_existent_decimal"
|
2016-08-31 14:46:40 -04:00
|
|
|
end
|
2016-11-30 12:47:31 -05:00
|
|
|
|
|
|
|
test "attributes do not require a type" do
|
|
|
|
klass = Class.new(OverloadedType) do
|
|
|
|
attribute :no_type
|
|
|
|
end
|
|
|
|
assert_equal 1, klass.new(no_type: 1).no_type
|
|
|
|
assert_equal "foo", klass.new(no_type: "foo").no_type
|
|
|
|
end
|
Add a setting to specify that all string columns should be immutable
In Rails 4.2 we introduced mutation detection, to remove the need to
call `attribute_will_change!` after modifying a field. One side effect
of that change was that we needed to enforce that the
`_before_type_cast` form of a value is a different object than the post
type cast value, if the value is mutable. That contract is really only
relevant for strings, but it meant we needed to dup them.
In Rails 5 we added the `ImmutableString` type, to allow people to opt
out of this duping in places where the memory usage was causing
problems, and they don't need to mutate that field.
This takes that a step further, and adds a class-level setting to
specify whether strings should be frozen by default or not. The default
value of this setting is `false` (strings are mutable), and I do not
plan on changing that.
While I think that immutable strings by default would be a reasonable
default for new applications, I do not think that we would be able to
document the value of this setting in a place that people will be
looking when they can't figure out why some string is frozen.
Realistically, the number of applications where this setting is relevant
is fairly small, so I don't think it would make sense to ever enable it
by default.
2017-07-25 11:37:05 -04:00
|
|
|
|
|
|
|
test "immutable_strings_by_default changes schema inference for string columns" do
|
|
|
|
with_immutable_strings do
|
|
|
|
OverloadedType.reset_column_information
|
|
|
|
immutable_string_type = Type.lookup(:immutable_string).class
|
|
|
|
assert_instance_of immutable_string_type, OverloadedType.type_for_attribute("inferred_string")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "immutable_strings_by_default retains limit information" do
|
|
|
|
with_immutable_strings do
|
|
|
|
OverloadedType.reset_column_information
|
|
|
|
assert_equal 255, OverloadedType.type_for_attribute("inferred_string").limit
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "immutable_strings_by_default does not affect `attribute :foo, :string`" do
|
|
|
|
with_immutable_strings do
|
|
|
|
OverloadedType.reset_column_information
|
|
|
|
default_string_type = Type.lookup(:string).class
|
|
|
|
assert_instance_of default_string_type, OverloadedType.type_for_attribute("string_with_default")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-03 13:03:02 -04:00
|
|
|
test "serialize boolean for both string types" do
|
|
|
|
default_string_type = Type.lookup(:string)
|
|
|
|
immutable_string_type = Type.lookup(:immutable_string)
|
|
|
|
assert_equal default_string_type.serialize(true), immutable_string_type.serialize(true)
|
|
|
|
assert_equal default_string_type.serialize(false), immutable_string_type.serialize(false)
|
|
|
|
end
|
|
|
|
|
Add a setting to specify that all string columns should be immutable
In Rails 4.2 we introduced mutation detection, to remove the need to
call `attribute_will_change!` after modifying a field. One side effect
of that change was that we needed to enforce that the
`_before_type_cast` form of a value is a different object than the post
type cast value, if the value is mutable. That contract is really only
relevant for strings, but it meant we needed to dup them.
In Rails 5 we added the `ImmutableString` type, to allow people to opt
out of this duping in places where the memory usage was causing
problems, and they don't need to mutate that field.
This takes that a step further, and adds a class-level setting to
specify whether strings should be frozen by default or not. The default
value of this setting is `false` (strings are mutable), and I do not
plan on changing that.
While I think that immutable strings by default would be a reasonable
default for new applications, I do not think that we would be able to
document the value of this setting in a place that people will be
looking when they can't figure out why some string is frozen.
Realistically, the number of applications where this setting is relevant
is fairly small, so I don't think it would make sense to ever enable it
by default.
2017-07-25 11:37:05 -04:00
|
|
|
private
|
|
|
|
def with_immutable_strings
|
|
|
|
old_value = ActiveRecord::Base.immutable_strings_by_default
|
|
|
|
ActiveRecord::Base.immutable_strings_by_default = true
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
ActiveRecord::Base.immutable_strings_by_default = old_value
|
|
|
|
end
|
2014-05-23 14:24:52 -04:00
|
|
|
end
|
|
|
|
end
|