Follow-up to #44625.
In #44625, the `SerializeCastValue` module was added to allow types to
avoid a redundant call to `cast` when serializing a value for the
database. Because it introduced a new method (`serialize_cast_value`)
that was not part of the `ActiveModel::Type::Value` contract, it was
designed to be opt-in. Furthermore, to guard against incompatible
`serialize` and `serialize_cast_value` implementations in types that
override `serialize` but (unintentionally) inherit `serialize_cast_value`,
types were required to explicitly include the `SerializeCastValue`
module to activate the optimization. i.e. It was not sufficient just to
have `SerializeCastValue` in the ancestor chain.
The `SerializeCastValue` module is not part of the public API, and there
are no plans to change that, which meant user-created custom types could
not benefit from this optimization.
This commit changes the opt-in condition such that it is sufficient for
the owner of the `serialize_cast_value` method to be the same or below
the owner of the `serialize` method in the ancestor chain. This means
a user-created type that only overrides `cast`, **not** `serialize`,
will now benefit from the optimization. For example, a type like:
```ruby
class DowncasedString < ActiveModel::Type::String
def cast(value)
super&.downcase
end
end
```
As demonstrated in the benchmark below, this commit does not change the
current performance of the built-in Active Model types. However, for a
simple custom type like `DowncasedString`, the performance of
`value_for_database` is twice as fast. For types with more expensive
`cast` operations, the improvement may be greater.
**Benchmark**
```ruby
# frozen_string_literal: true
require "benchmark/ips"
require "active_model"
class DowncasedString < ActiveModel::Type::String
def cast(value)
super&.downcase
end
end
ActiveModel::Type.register(:downcased_string, DowncasedString)
VALUES = {
my_big_integer: "123456",
my_boolean: "true",
my_date: "1999-12-31",
my_datetime: "1999-12-31 12:34:56 UTC",
my_decimal: "123.456",
my_float: "123.456",
my_immutable_string: "abcdef",
my_integer: "123456",
my_string: "abcdef",
my_time: "1999-12-31T12:34:56.789-10:00",
my_downcased_string: "AbcDef",
}
TYPES = VALUES.to_h { |name, value| [name, name.to_s.delete_prefix("my_").to_sym] }
class MyModel
include ActiveModel::API
include ActiveModel::Attributes
TYPES.each do |name, type|
attribute name, type
end
end
attribute_set = MyModel.new(VALUES).instance_variable_get(:@attributes)
TYPES.each do |name, type|
attribute = attribute_set[name.to_s]
Benchmark.ips do |x|
x.report(type.to_s) { attribute.value_for_database }
end
end
```
**Before**
```
big_integer 2.986M (± 1.2%) i/s - 15.161M in 5.078972s
boolean 2.980M (± 1.1%) i/s - 15.074M in 5.059456s
date 2.960M (± 1.1%) i/s - 14.831M in 5.011355s
datetime 1.368M (± 0.9%) i/s - 6.964M in 5.092074s
decimal 2.930M (± 1.2%) i/s - 14.911M in 5.089048s
float 2.932M (± 1.3%) i/s - 14.713M in 5.018512s
immutable_string 3.013M (± 1.3%) i/s - 15.239M in 5.058085s
integer 1.603M (± 0.8%) i/s - 8.096M in 5.052046s
string 2.977M (± 1.1%) i/s - 15.168M in 5.094874s
time 1.338M (± 0.9%) i/s - 6.699M in 5.006046s
downcased_string 1.394M (± 0.9%) i/s - 7.034M in 5.046972s
```
**After**
```
big_integer 3.016M (± 1.0%) i/s - 15.238M in 5.053005s
boolean 2.965M (± 1.3%) i/s - 15.037M in 5.071921s
date 2.924M (± 1.0%) i/s - 14.754M in 5.046294s
datetime 1.435M (± 0.9%) i/s - 7.295M in 5.082498s
decimal 2.950M (± 0.9%) i/s - 14.800M in 5.017225s
float 2.964M (± 0.9%) i/s - 14.987M in 5.056405s
immutable_string 2.907M (± 1.4%) i/s - 14.677M in 5.049194s
integer 1.638M (± 0.9%) i/s - 8.227M in 5.022401s
string 2.971M (± 1.0%) i/s - 14.891M in 5.011709s
time 1.454M (± 0.9%) i/s - 7.384M in 5.079993s
downcased_string 2.939M (± 0.9%) i/s - 14.872M in 5.061100s
```
We recently let a few very easy to avoid warnings get merged.
The root cause is that locally the test suite doesn't run in
verbose mode unless you explictly pass `-w`.
On CI warnings are enabled, but there is no reason to look at the
build output unless something is failing. And even if one wanted
to do that, that would be particularly work intensive since warnings
may be specific to a Ruby version etc.
Because of this I believe we should:
- Always run the test suite with warnings enabled.
- Raise an error if a warning is unexpected.
We've been using this pattern for a long time at Shopify both in private
and public repositories.
Most model attribute types try to cast a given value before serializing
it. This allows uncast values to be passed to finder methods and still
be serialized appropriately. However, when persisting a model, this
cast is unnecessary because the value will already have been cast by
`ActiveModel::Attribute#value`.
To eliminate the overhead of a 2nd cast, this commit introduces a
`ActiveModel::Type::SerializeCastValue` module. Types can include this
module, and their `serialize_cast_value` method will be called instead
of `serialize` when serializing an already-cast value.
To preserve existing behavior of any user types that subclass Rails'
types, `serialize_after_cast` will only be called if the type itself
(not a superclass) includes `ActiveModel::Type::SerializeCastValue`.
This also applies to type decorators implemented via `DelegateClass`.
Benchmark script:
```ruby
require "active_model"
require "benchmark/ips"
class ActiveModel::Attribute
alias baseline_value_for_database value_for_database
end
VALUES = {
my_big_integer: "123456",
my_boolean: "true",
my_date: "1999-12-31",
my_datetime: "1999-12-31 12:34:56 UTC",
my_decimal: "123.456",
my_float: "123.456",
my_immutable_string: "abcdef",
my_integer: "123456",
my_string: "abcdef",
my_time: "1999-12-31T12:34:56.789-10:00",
}
TYPES = VALUES.to_h { |name, value| [name, name.to_s.delete_prefix("my_").to_sym] }
class MyModel
include ActiveModel::API
include ActiveModel::Attributes
TYPES.each do |name, type|
attribute name, type
end
end
TYPES.each do |name, type|
$attribute_set ||= MyModel.new(VALUES).instance_variable_get(:@attributes)
attribute = $attribute_set[name.to_s]
puts "=" * 72
Benchmark.ips do |x|
x.report("#{type} before") { attribute.baseline_value_for_database }
x.report("#{type} after") { attribute.value_for_database }
x.compare!
end
end
```
Benchmark results:
```
========================================================================
Warming up --------------------------------------
big_integer before 100.417k i/100ms
big_integer after 260.375k i/100ms
Calculating -------------------------------------
big_integer before 1.005M (± 1.0%) i/s - 5.121M in 5.096498s
big_integer after 2.630M (± 1.0%) i/s - 13.279M in 5.050387s
Comparison:
big_integer after: 2629583.6 i/s
big_integer before: 1004961.2 i/s - 2.62x (± 0.00) slower
========================================================================
Warming up --------------------------------------
boolean before 230.663k i/100ms
boolean after 299.262k i/100ms
Calculating -------------------------------------
boolean before 2.313M (± 0.7%) i/s - 11.764M in 5.085925s
boolean after 3.037M (± 0.6%) i/s - 15.262M in 5.026280s
Comparison:
boolean after: 3036640.8 i/s
boolean before: 2313127.8 i/s - 1.31x (± 0.00) slower
========================================================================
Warming up --------------------------------------
date before 148.821k i/100ms
date after 298.939k i/100ms
Calculating -------------------------------------
date before 1.486M (± 0.6%) i/s - 7.441M in 5.006091s
date after 2.963M (± 0.8%) i/s - 14.947M in 5.045651s
Comparison:
date after: 2962535.3 i/s
date before: 1486459.4 i/s - 1.99x (± 0.00) slower
========================================================================
Warming up --------------------------------------
datetime before 92.818k i/100ms
datetime after 136.710k i/100ms
Calculating -------------------------------------
datetime before 920.236k (± 0.6%) i/s - 4.641M in 5.043355s
datetime after 1.366M (± 0.8%) i/s - 6.836M in 5.003307s
Comparison:
datetime after: 1366294.1 i/s
datetime before: 920236.1 i/s - 1.48x (± 0.00) slower
========================================================================
Warming up --------------------------------------
decimal before 50.194k i/100ms
decimal after 298.674k i/100ms
Calculating -------------------------------------
decimal before 494.141k (± 1.4%) i/s - 2.510M in 5.079995s
decimal after 3.015M (± 1.0%) i/s - 15.232M in 5.052929s
Comparison:
decimal after: 3014901.3 i/s
decimal before: 494141.2 i/s - 6.10x (± 0.00) slower
========================================================================
Warming up --------------------------------------
float before 217.547k i/100ms
float after 298.106k i/100ms
Calculating -------------------------------------
float before 2.157M (± 0.8%) i/s - 10.877M in 5.043292s
float after 2.991M (± 0.6%) i/s - 15.203M in 5.082806s
Comparison:
float after: 2991262.8 i/s
float before: 2156940.2 i/s - 1.39x (± 0.00) slower
========================================================================
Warming up --------------------------------------
immutable_string before
163.287k i/100ms
immutable_string after
298.245k i/100ms
Calculating -------------------------------------
immutable_string before
1.652M (± 0.7%) i/s - 8.328M in 5.040855s
immutable_string after
3.022M (± 0.9%) i/s - 15.210M in 5.033151s
Comparison:
immutable_string after: 3022313.3 i/s
immutable_string before: 1652121.7 i/s - 1.83x (± 0.00) slower
========================================================================
Warming up --------------------------------------
integer before 115.383k i/100ms
integer after 159.702k i/100ms
Calculating -------------------------------------
integer before 1.132M (± 0.8%) i/s - 5.769M in 5.095041s
integer after 1.641M (± 0.5%) i/s - 8.305M in 5.061893s
Comparison:
integer after: 1640635.8 i/s
integer before: 1132381.5 i/s - 1.45x (± 0.00) slower
========================================================================
Warming up --------------------------------------
string before 163.061k i/100ms
string after 299.885k i/100ms
Calculating -------------------------------------
string before 1.659M (± 0.7%) i/s - 8.316M in 5.012609s
string after 2.999M (± 0.6%) i/s - 15.294M in 5.100008s
Comparison:
string after: 2998956.0 i/s
string before: 1659115.6 i/s - 1.81x (± 0.00) slower
========================================================================
Warming up --------------------------------------
time before 98.250k i/100ms
time after 133.463k i/100ms
Calculating -------------------------------------
time before 987.771k (± 0.7%) i/s - 5.011M in 5.073023s
time after 1.330M (± 0.5%) i/s - 6.673M in 5.016573s
Comparison:
time after: 1330253.9 i/s
time before: 987771.0 i/s - 1.35x (± 0.00) slower
```
This avoids problems when complex data structures are mutated _after_
being handed to ActiveRecord for processing. For example false hits in
the query cache.
Fixes#46044
Rubinius has not been maintained since May 2020 and based on the
discussion at https://github.com/rails/rails/pull/44984 ,
I think we can remove Rubinius specific code from Rails.
When source and target classes have a different set of attributes adapts
attributes such that the extra attributes from target are added.
Fixes#41195
Co-authored-by: SampsonCrowley <sampsonsprojects@gmail.com>
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
and :to options are not being type cast. For example, for an enum
attribute, attribute_changed? should handle a String, Symbol or Integer
for the :from and :to options.
I was running into a case where I didn't want to just disabled the
validations and add my own. In fact, I would very much like to keep the
default validation but just de-activate it on some scenario:
e.g. Inviting a user without having to set a password for them yet so
they can add it themselves later when they receive an email invitation
to finish setting up their account.
My understanding of the validations flag originally intended was to
just disabled them and if you needed something more custom, you could
run your own validations instead.
This would be an acceptable solution, but it would add more code to my
controller. Instead validations can receive a `Hash` wich is then use to
apply validations rules to `validate`.
This is just a suggestion, I am not sure if there is a need, and I am
aware this PR is probably far from perfect. Any feedback welcome.
EDIT: implemented changes as per feedback.
Though we needed to use constantize for ruby < 2.0, ruby >= 2.0
const_get can handle namespace.
> https://docs.ruby-lang.org/en/2.0.0/NEWS.html
> Module#const_get accepts a qualified constant string, e.g. Object.const_get(“Foo::Bar::Baz”)
Some validators, such as validators that inherit from `EachValidator`,
mutate the options they receive. This can cause problems when passing
multiple validators and options to `validates_with`. This can also be a
problem if a validator deletes standard options such as `:if` and `:on`,
because the validation callback would then not receive them.
This commit modifies `validates_with` to `dup` options before passing
them to validators, thus preventing these issues.
Fixes#44460.
Closes#44476.
Co-authored-by: Dieter Späth <dieter.spaeth@lanes-planes.com>
It would be nice to be able to pattern match against ActiveModel (and
transitively ActiveRecord). If you want to check multiple attributes
with conditions, it's nice to be able use the pattern matching syntax.
For example:
```ruby
case Current.user
in { superuser: true }
"Thanks for logging in. You are a superuser."
in { admin: true, name: }
"Thanks for logging in, admin #{name}!"
in { name: }
"Welcome, #{name}!"
end
```
All validations are called on `#save` by default.
The Absence, Acceptance, Presence validators don't do anything special,
so there is no reason to mention they are called on `#save` by default.
Instead we can mention all validations are called in `#save` by default
in ActiveRecord::Validations.
Co-authored-by: Gannon McGibbon <gannon@hey.com>
In https://github.com/rails/rails/pull/43036 an optimisation was applied
to ActiveModel#Serialization to speed up the generation of a
serialized_hash by avoiding loading the subjects attributes by using an
attribute_names method. A fallback method,
ActiveModel::Serialization#attribute_names` was added as #attribute_names
isn't part of the public API of ActiveModel.
Unfortunately, this fallback method happens to override the ActiveRecord
method (as ActiveModel::Serialization is a later mixin than
ActiveRecord::AttributeMethods), so this change didn't actually provide
an optimisation - the full attribute data was loaded as per [1]
This change also, in our case, produced some severe performance issues
as it introduced an N+1 query in a situation where we had one gem,
Globalize [2], which adds in dynamic attributes that are loaded by a query;
and another gem, Transitions [3], that checks attribute names at
initialization. The combination of these meant that for every model that
was initialized an extra query would run - no matter what includes or
eager_load steps were in place. This rapidly hindered our applications'
performance and meant we had to rollback the Rails 7 upgrade.
Following rafaelfranca's suggestion [4] this adds a
`attribute_names_for_serialization` method to Serialization modules in
ActiveRecord and ActiveModel. This allows the ActiveRecord one to
override the ActiveModel fallback and thus be optimised.
Some basic benchmarks of this follow - they use code from
https://github.com/ollietreend/rails-demo and have some pretty large
arrays set as serialized attributes [5] to demonstrate impacts.
Loading attribute names:
Rails 7.0.2.3
```
> Benchmark.ms { Widget.all.map(&:attribute_names) }
Widget Load (131.1ms) SELECT "widgets".* FROM "widgets"
=> 20108.852999983355
```
This patch
```
> Benchmark.ms { Widget.all.map(&:attribute_names) }
Widget Load (144.0ms) SELECT "widgets".* FROM "widgets"
=> 237.96699999365956
```
Using serializable_hash:
Rails 7.0.2.3
```
> widgets = Widget.all.to_a; Benchmark.ms { widgets.map { |w| w.serializable_hash(only: []) } }
Widget Load (133.3ms) SELECT "widgets".* FROM "widgets"
=> 22071.45000001765
```
This patch
```
> widgets = Widget.all.to_a; Benchmark.ms { widgets.map { |w| w.serializable_hash(only: []) } }
Widget Load (83.5ms) SELECT "widgets".* FROM "widgets"
=> 67.9039999959059
```
[1]: eeb2cfb686/activemodel/lib/active_model/serialization.rb (L151-L154)
[2]: https://github.com/globalize/globalize
[3]: https://github.com/troessner/transitions
[4]: https://github.com/rails/rails/pull/44770#pullrequestreview-922209612
[5]: 525f88887b/db/seeds.rb
As a step toward sharing more code between Active Model and Active
Record, this commit factors an `ActiveModel::AttributeRegistration`
module out of `ActiveModel::Attributes`. This module is marked as
`nodoc` and is for internal use only.
Additionally, this commit adds thorough test coverage of attribute
registration and inheritance. (`activemodel/test/cases/attributes_test.rb`
does already test some of this behavior, but it is focused on high-level
functionality.)
This enhances `has_secure_password` to define a `password_challenge`
accessor and the appropriate validation. When `password_challenge` is
set, the validation checks that it matches the currently *persisted*
`password_digest` (i.e. `password_digest_was`).
This allows a password challenge to be implemented with the same ease as
a password confirmation, re-using the same error handling logic in the
view, as well as the controller. For example, in the controller,
instead of:
```ruby
password_params = params.require(:password).permit(
:password_challenge,
:password,
:password_confirmation,
)
password_challenge = password_params.delete(:password_challenge)
@password_challenge_failed = !current_user.authenticate(password_challenge)
if !@password_challenge_failed && current_user.update(password_params)
# ...
end
```
One could write:
```ruby
password_params = params.require(:password).permit(
:password_challenge,
:password,
:password_confirmation,
).with_defaults(password_challenge: "")
if current_user.update(password_params)
# ...
end
```
And, in the view, instead of checking `@password_challenge_failed`, one
could render a password challenge error in the same manner as other form
field errors, including utilizing `config.action_view.field_error_proc`.
RDoc will automatically format and link API references as long as they
are not already marked up as inline code.
This commit removes markup from various API references so that those
references will link to the relevant API docs.
"Overwrite" means "destructively replace", and is more suitable when,
for example, talking about writing data to a location.
"Override" means "supersede", and is more suitable when, for example,
talking about redifining methods in a subclass.