"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.
This commit adds documentation to the constants and methods that are
part of Active Model's Attributes API. So far this API has been hidden
with the :nodoc: flag since its inception in Active Record and
subsequent move to Active Model (#30920 and #30985); as the API matures
and gets ready for public usage, visible documentation for its endpoints
becomes necessary.
The classes and modules being documented and publicized by this commit
are the main `Attributes` module, the `Type` namespace, and all the
standard attribute type classes included in the current API, which users
will be able to extend and replicate to suit their customization needs.
Some private modules are also receiving documetation, although they will
continue using the :nodoc: flag. Those are `Attribute` and
`AttributeSet`. Although they remain private I found useful to add some
comments to describe their responsibilities.
Ruby 3.1 introduced an optimization to string interpolation for some
core classes in b08dacfea3.
But since we override `to_s` in some of those core classes to add behavior
like `to_s(:db)`, all Rails applications will not be able to take advantage
of that improvement.
Since we can use the `to_formatted_s` alias for the Rails specific behavior
it is best for us to deprecate the `to_s` core extension and allow Rails
applications to get the proformace improvement.
This commit starts removing all the `to_s(:db)` calls inside the framework
so we can deprecate the core extension in the next commit.
Prior to this change, attempting to merge an `ActiveModel::Error` instance
with itself would result in an endless loop where `ActiveModel::NestedError`s
would continue to be imported on the instance until interrupted. Though the
merging of identical objects is less likely to happen in practice, this method
should still be able to handle such a case gracefully. This change ensures
that instances attempting to merge on themselves return early rather than
hanging indefinitely.
Addresses https://github.com/rails/rails/issues/43737
The bulk of the optimization is to generate code rather than use
`define_method` with a closure.
```
Warming up --------------------------------------
original 207.468k i/100ms
code-generator 340.849k i/100ms
Calculating -------------------------------------
original 2.127M (± 1.1%) i/s - 10.788M in 5.073860s
code-generator 3.426M (± 0.9%) i/s - 17.383M in 5.073965s
Comparison:
code-generator: 3426241.0 i/s
original: 2126539.2 i/s - 1.61x (± 0.00) slower
```
```ruby
require 'benchmark/ips'
require 'active_support/all'
class Original < ActiveSupport::CurrentAttributes
attribute :foo
end
class CodeGen < ActiveSupport::CurrentAttributes
class << self
def attribute(*names)
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
names.each do |name|
owner.define_cached_method(name, namespace: :current_attributes) do |batch|
batch <<
"def #{name}" <<
"attributes[:#{name}]" <<
"end"
end
owner.define_cached_method("#{name}=", namespace: :current_attributes) do |batch|
batch <<
"def #{name}=(value)" <<
"attributes[:#{name}] = value" <<
"end"
end
end
end
ActiveSupport::CodeGenerator.batch(singleton_class, __FILE__, __LINE__) do |owner|
names.each do |name|
owner.define_cached_method(name, namespace: :current_attributes_delegation) do |batch|
batch <<
"def #{name}" <<
"instance.#{name}" <<
"end"
end
owner.define_cached_method("#{name}=", namespace: :current_attributes_delegation) do |batch|
batch <<
"def #{name}=(value)" <<
"instance.#{name} = value" <<
"end"
end
end
end
end
end
attribute :foo
end
Benchmark.ips do |x|
x.report('original') { Original.foo }
x.report('code-generator') { CodeGen.foo }
x.compare!
end
```
Along the same lines as ccb3cb573b, this commit removes unnecessary
references to mental health.
As in that commit, I think many of these are more descriptive than what
we had before.
The commit changes only tests and documentation.
Currently `ActiveModel::Model` is defined as the minimum API to talk
with Action Pack and Action View.
Its name suggests it can be included to create Active Record type
models, but for creating models it's probably too minimal. For example
it's very common to include ActiveModel::Attributes as well.
By moving `ActiveModel::Model`'s implementation to a new
`ActiveModel::API` we keep a definition of the minimum API to talk with
Action Pack and Action View.
For `ActiveModel::Model` we only need to include `ActiveModel::API`.
This will allow adding more funcationality to `ActiveModel::Model` while
keeping backwards compatibility.
Co-authored-by: Nathaniel Watts <1141717+thewatts@users.noreply.github.com>
for ActiveRecord objects, it will force the entire attributes hash to
be constructed, which may include expensive deserialization.
ActiveModel already has a simple attribute_names method defined which
returns attributes.keys, exactly like the code we're replacing.
ActiveRecord overrides that method, and returns the names of the
attributes _without_ having to deserialize any values.
this change can save a lot of CPU if you have a serialized column
on a model that you often load from the DB because it's not worth
the effort to customize the SELECT on every relation, but
also rarely expose in any JSON serializations (i.e. have a default
except option for it).
* Without the change the new test fails like this:
Failure:
ActiveModel::TypeTest#test_registering_a_new_type [test/cases/type_test.rb:21]:
Expected: #<struct args={}>
Actual: #<struct args=nil>
* (*args, **kwargs)-delegation is not correct on Ruby 2.7 unless the
target always accepts keyword arguments (not the case for `Struct.new(:args).new`).
See https://eregon.me/blog/2021/02/13/correct-delegation-in-ruby-2-27-3.html
* Without the change the new test fails like this:
Failure:
ActiveModel::Type::RegistryTest#test_a_class_can_be_registered_for_a_symbol [test/cases/type/registry_test.rb:16]:
Expected: [{}, {}]
Actual: [nil, nil]
* (*args, **kwargs)-delegation is not correct on Ruby 2.7 unless the
target always accepts keyword arguments (not the case for `Array.new`).
See https://eregon.me/blog/2021/02/13/correct-delegation-in-ruby-2-27-3.html
Ruby master ships with Psych 4.0.0 which makes `YAML.load`
defaults to safe mode (https://github.com/ruby/psych/pull/487).
However since these YAML files are trustworthy sources
we can parse them with `unsafe_load`.
`codespell` works with a small custom dictionary and seems to find perhaps more spelling mistakes than `misspell` which really only fixes commonly misspelled English words.
Not all spell checkers can check all file types and most spell checkers can't find all the errors.
https://github.com/codespell-project/codespellhttps://pypi.org/project/codespell/
Somehow it isn't caused on CI, but it consistently causes on locally.
```
% bin/test -w
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:85: warning: method redefined; discarding old validates_each
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:85: warning: previous definition of validates_each was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:89: warning: already initialized constant ActiveModel::Validations::ClassMethods::VALID_OPTIONS_FOR_VALIDATE
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:89: warning: previous definition of VALID_OPTIONS_FOR_VALIDATE was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:152: warning: method redefined; discarding old validate
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:152: warning: previous definition of validate was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:192: warning: method redefined; discarding old validators
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:192: warning: previous definition of validators was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:234: warning: method redefined; discarding old clear_validators!
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:234: warning: previous definition of clear_validators! was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:254: warning: method redefined; discarding old validators_on
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:254: warning: previous definition of validators_on was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:270: warning: method redefined; discarding old attribute_method?
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:270: warning: previous definition of attribute_method? was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:275: warning: method redefined; discarding old inherited
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:275: warning: previous definition of inherited was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:283: warning: method redefined; discarding old initialize_dup
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:283: warning: previous definition of initialize_dup was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:301: warning: method redefined; discarding old errors
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:301: warning: previous definition of errors was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:373: warning: method redefined; discarding old invalid?
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:373: warning: previous definition of invalid? was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:382: warning: method redefined; discarding old validate!
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:382: warning: previous definition of validate! was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:405: warning: method redefined; discarding old run_validations!
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:405: warning: previous definition of run_validations! was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:410: warning: method redefined; discarding old raise_validation_error
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:410: warning: previous definition of raise_validation_error was here
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:426: warning: method redefined; discarding old model
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:428: warning: method redefined; discarding old initialize
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:428: warning: previous definition of initialize was here
Traceback (most recent call last):
23: from bin/test:5:in `<main>'
22: from bin/test:5:in `require_relative'
21: from /Users/kamipo/src/github.com/rails/rails/tools/test.rb:18:in `<top (required)>'
20: from /Users/kamipo/src/github.com/rails/rails/railties/lib/rails/test_unit/runner.rb:40:in `run'
19: from /Users/kamipo/src/github.com/rails/rails/railties/lib/rails/test_unit/runner.rb:52:in `load_tests'
18: from /Users/kamipo/src/github.com/rails/rails/railties/lib/rails/test_unit/runner.rb:52:in `each'
17: from /Users/kamipo/src/github.com/rails/rails/railties/lib/rails/test_unit/runner.rb:52:in `block in load_tests'
16: from /Users/kamipo/src/github.com/rails/rails/railties/lib/rails/test_unit/runner.rb:52:in `require'
15: from /Users/kamipo/src/github.com/rails/rails/activemodel/test/cases/attributes_dirty_test.rb:5:in `<top (required)>'
14: from /Users/kamipo/src/github.com/rails/rails/activemodel/test/cases/attributes_dirty_test.rb:6:in `<class:AttributesDirtyTest>'
13: from /Users/kamipo/src/github.com/rails/rails/activemodel/test/cases/attributes_dirty_test.rb:7:in `<class:DirtyModel>'
12: from /Users/kamipo/src/github.com/rails/rails/activemodel/test/cases/attributes_dirty_test.rb:7:in `require'
11: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/model.rb:3:in `<top (required)>'
10: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/model.rb:59:in `<module:ActiveModel>'
9: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/model.rb:62:in `<module:Model>'
8: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/model.rb:62:in `require'
7: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:436:in `<top (required)>'
6: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:436:in `each'
5: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:436:in `block in <top (required)>'
4: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations.rb:436:in `require'
3: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations/numericality.rb:5:in `<top (required)>'
2: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations/numericality.rb:6:in `<module:ActiveModel>'
1: from /Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations/numericality.rb:7:in `<module:Validations>'
/Users/kamipo/src/github.com/rails/rails/activemodel/lib/active_model/validations/numericality.rb:8:in `<class:NumericalityValidator>': uninitialized constant ActiveModel::Validations::NumericalityValidator::Comparability (NameError)
```
This saves some array allocations from avoiding `*args`, as well
as makes the Method object `arity` and `parameters` correct.
e.g. before this patch, ArgumentError would be confusing:
```ruby
>> model.name_was(1)
ArgumentError: wrong number of arguments (given 2, expected 1)
```
ActiveRecord::Type::Registry doesn't need to inherit from
ActiveModel::Type::Registry, and it makes both classes more simple.
Co-authored-by: Adrianna Chang <adrianna.chang@shopify.com>
Since Ruby 2.7 `self.some_private_method` works fine.
So now that Ruby 2.7 is the minimal supported version,
`define_proxy_call` can always prepend `self.`
I used `type.cast(value)` to emulate unchecked serialized value in
`unboundable?`, since `RangeError` was raised only for the integer type,
so the emulation works enough for the integer type.
But since #41516, Enum types are also not always serializable, so
`type.cast(value)` may also be called for the enum types.
I've delegated `type.cast(value)` to the subtype if an unknown label is
passed to work the emulation even on Enum types in 3b6461b. But it is
strange to delegate to the subtype for the emulation only if an unknown
label is passed.
Instead of using `type.cast(value)` for the emulation, extend
`serializable?` to get unchecked serialized value if the value is not
serializable.
Since a `BindParam` object always has an attribute object as the value
in the Active Record usage, so `_insert_record`/`_update_record` could
be passed attribute set instead of wrapping casted value by a query
attribute.
RDoc Markup does not support backticks the way Markdown does to mark up
inline code. Additionally, `<tt>` must be used to mark up inline code
that includes spaces or certain punctuation characters (e.g. quotes).
These methods have changed in Ruby 2.5 to be more akin to grep:
https://bugs.ruby-lang.org/issues/11286
Using classes seems to be faster (and a bit more expressive) than iterating over
the collection items:
```
Warming up --------------------------------------
#all? with class 504.000 i/100ms
#all? with proc 189.000 i/100ms
Calculating -------------------------------------
#all? with class 4.960k (± 1.6%) i/s - 25.200k in 5.082049s
#all? with proc 1.874k (± 2.8%) i/s - 9.450k in 5.047866s
Comparison:
#all? with class: 4959.9 i/s
#all? with proc: 1873.8 i/s - 2.65x (± 0.00) slower
```
Benchmark script:
```ruby
require "minitest/autorun"
require "benchmark/ips"
class BugTest < Minitest::Test
def test_enumerators_with_classes
arr = (1..10000).to_a << nil
assert_equal arr.all?(Integer), arr.all? { |v| v.is_a?(Integer) }
Benchmark.ips do |x|
x.report("#all? with class") do
arr.all?(Integer)
end
x.report("#all? with proc") do
arr.all? { |v| v.is_a?(Integer) }
end
x.compare!
end
end
end
```
We allow for compare validations in NumericalityValidator, but these
only work on numbers. There are various comparisons people may want
to validate, from dates to strings, to custom comparisons.
```
validates_comparison_of :end_date, greater_than: :start_date
```
Refactor NumericalityValidator to share module Comparison with ComparabilityValidator
* Move creating the option_value into a reusable module
* Separate COMPARE_CHECKS which support compare functions and accept values
* Move odd/even checks to NUMBER_CHECKS as they can only be run on numbers
If unknow type is given for attribute (`attribute :foo, :unknown`),
unknown type error isn't raised on the definition time but runtime.
It should be raised on the definition time.
f72f743 introduces truncate(scale) in the Numericality validator.
This behaviour conflicts with AR decimal type conversion,
which uses round(scale) instead.
Changes the Numericality validator in order to use
round(scale) for consistency.
The ability has lost due to reverted #39321 in #41049.
We should allow updating with dirty locking value to work the documented
usage, but if casted value has no difference (i.e. regarded as no dirty),
identify the object by the original (uninitialized default) value.
In some cases, the framework was mutating the :if option of callbacks.
Since #38323, those options are frozen, so the framework could raise
exception when trying to mutate those options if they were being resued
with method like `with_options`.
It is a regression for 4cc438a1df75e4c230f19cafe9258dbab969cd27.
`NumericalityValidator` basically takes the value before typecasting,
but `allow_nil` should work for the typecasted value for the
compatibility.
Fixes#40750.
This reverts commit d93a5d385e5bc2392a1f47dc2885e353898b62e1.
Revert "Revert "Remove unused internal methods in ActiveModel::Attributes""
This reverts commit 2d7967204e7f7d5ba846b0a6ed51088c7a7db365.
Reason: read_attribute was added in 6.1 as a performance optimization
and it is not needed anymore and write_attribute only existed to make
possible to call something that is not `attribute=` with send. We don't
need those methods internally and since they were never part of the
public API we can remove them.
There are validation cases in which the human_attribute_name depends on
other fields of the base class.
For instance, an Address model that depends on the selected country to
localize the attribute name to be shown in error messages. E.g. the
:address1 and :address2 attributes can be displayed as very different
strings depending on whether the address is in the US or in Japan.
The model global `read_attribute_for_validation` is not fit for some
validators (specifically for numericality validator).
To allow per-validator customization for attribute value, it extracts
`read_attribute_for_validation` in `EachValidator`.
This makes `datetime.serialize` about 10% faster.
```ruby
type = ActiveRecord::Type.lookup(:datetime)
time = Time.now.utc
Benchmark.ips do |x|
x.report("type.serialize(time)") do
type.serialize(time)
type.serialize(time)
type.serialize(time)
type.serialize(time)
end
end
```
Before:
```
Warming up --------------------------------------
type.serialize(time) 12.899k i/100ms
Calculating -------------------------------------
type.serialize(time) 131.293k (± 1.6%) i/s - 657.849k in 5.011870s
```
After:
```
Warming up --------------------------------------
type.serialize(time) 14.603k i/100ms
Calculating -------------------------------------
type.serialize(time) 145.941k (± 1.1%) i/s - 730.150k in 5.003639s
```
Follow up to c07dff72278fb7f2a3c4c71212a0773a2b25c790.
Actually it is not the cop's fault, but we mistakenly use `^`, `$`, and
`\Z` in much places, the cop doesn't correct those conservatively.
I've checked all those usage and replaced all safe ones.
Instantiating attributes hash from raw database values is one of the
slower part of attributes.
Why that is necessary is to detect mutations. In other words, that isn't
necessary until mutations are happened.
`LazyAttributeHash` which was introduced at 0f29c21 is to instantiate
attribute lazily until first accessing the attribute (i.e.
`Model.find(1)` isn't slow yet, but `Model.find(1).attr_name` is still
slow).
This introduces `LazyAttributeSet` to instantiate attribute more lazily,
it doesn't instantiate attribute until first assigning/dirty checking
the attribute (i.e. `Model.find(1).attr_name` is no longer slow).
It makes attributes access about 35% 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 4.652 (±21.5%) i/s - 23.000 in 5.034853s
MEMORY
Calculating -------------------------------------
attribute access 27.782M memsize ( 0.000 retained)
170.044k objects ( 0.000 retained)
15.000 strings ( 0.000 retained)
```
And benchmark with this branch for immutable string type:
```ruby
ActiveRecord::Schema.define do
create_table :users, force: true do |t|
t.string :name
t.string :fast_name
end
end
class User < ActiveRecord::Base
attribute :fast_name, :immutable_string
end
user = User.new
Benchmark.ips do |x|
x.report("user.name") do
user.name = "foo"
user.name_changed?
end
x.report("user.fast_name") do
user.fast_name = "foo"
user.fast_name_changed?
end
end
```
```
Warming up --------------------------------------
user.name 34.811k i/100ms
user.fast_name 39.505k i/100ms
Calculating -------------------------------------
user.name 343.864k (± 3.6%) i/s - 1.741M in 5.068576s
user.fast_name 384.033k (± 2.7%) i/s - 1.936M in 5.044425s
```
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.
Delegating to just one line method is to not be worth it.
Avoiding the delegation makes `read_attribute` about 15% faster.
```ruby
ActiveRecord::Schema.define do
create_table :users, force: true do |t|
t.string :name
end
end
class User < ActiveRecord::Base
def fast_read_attribute(attr_name, &block)
name = attr_name.to_s
name = self.class.attribute_aliases[name] || name
name = @primary_key if name == "id" && @primary_key
@attributes.fetch_value(name, &block)
end
end
user = User.create!(name: "user name")
Benchmark.ips do |x|
x.report("read_attribute('id')") { user.read_attribute('id') }
x.report("read_attribute('name')") { user.read_attribute('name') }
x.report("fast_read_attribute('id')") { user.fast_read_attribute('id') }
x.report("fast_read_attribute('name')") { user.fast_read_attribute('name') }
end
```
```
Warming up --------------------------------------
read_attribute('id') 165.744k i/100ms
read_attribute('name')
162.229k i/100ms
fast_read_attribute('id')
192.543k i/100ms
fast_read_attribute('name')
191.209k i/100ms
Calculating -------------------------------------
read_attribute('id') 1.648M (± 1.7%) i/s - 8.287M in 5.030170s
read_attribute('name')
1.636M (± 3.9%) i/s - 8.274M in 5.065356s
fast_read_attribute('id')
1.918M (± 1.8%) i/s - 9.627M in 5.021271s
fast_read_attribute('name')
1.928M (± 0.9%) i/s - 9.752M in 5.058820s
```
Redundant `to_s` has a few overhead. Especially private methods are not
intend to be passed user input directly so it should be passed always
string.
Removing redundant `to_s` makes attribute methods about 10% faster.
```ruby
ActiveRecord::Schema.define do
create_table :users, force: true do |t|
end
end
class User < ActiveRecord::Base
def fast_read_attribute(attr_name, &block)
@attributes.fetch_value(attr_name, &block)
end
end
user = User.create!
Benchmark.ips do |x|
x.report("user._read_attribute('id')") { user._read_attribute("id") }
x.report("user.fast_read_attribute('id')") { user.fast_read_attribute("id") }
end
```
```
Warming up --------------------------------------
user._read_attribute('id')
272.151k i/100ms
user.fast_read_attribute('id')
283.518k i/100ms
Calculating -------------------------------------
user._read_attribute('id')
2.699M (± 1.3%) i/s - 13.608M in 5.042846s
user.fast_read_attribute('id')
2.988M (± 1.2%) i/s - 15.026M in 5.029056s
```
For now, `increment` with aliased attribute does work, but `increment!`
with aliased attribute does not work, due to `clear_attribute_change` is
not aware of attribute aliases.
We sometimes partially updates specific attributes in dirties, at that
time it relies on `clear_attribute_change` to clear partially updated
attribute dirties. If `clear_attribute_change` is not attribute method
unlike others, we need to resolve attribute aliases manually only for
`clear_attribute_change`, it is a little inconvinient for me.
From another point of view, we have `restore_attributes`,
`restore_attribute!`, `clear_attribute_changes`, and
`clear_attribute_change`. Despite almost similar features
`restore_attribute!` is an attribute method but `clear_attribute_change`
is not.
Given the above, I'd like to promote `clear_attribute_change` as
attribute methods to fix issues caused by the inconsisteny.
This reverts 8538dfdc084555673d18cfc3479ebef09f325c9c, which broke the
activemodel-serializers-xml gem.
We can still get most of the benefit by applying the optimisation from
7b3919774252f99e55e6b6ec370aafc42adca2b2 to empty hashes as well as nil.
This has the additional benefit of retaining the optimisation when the
user passes an empty options hash.
Since we're checking `serializable?` in the new `HomogeneousIn`
`serialize` will no longer raise an exception. We implemented
`unchecked_serialize` to avoid raising in these cases, but with some of
our refactoring we no longer need it.
I discovered this while trying to fix a query in our application that
was not properly serializing binary columns. I discovered that in at
least 2 of our active model types we were not calling the correct
serialization. Since `serialize` wasn't aliased to `unchecked_serialize`
in `ActiveModel::Type::Binary` and `ActiveModel::Type::Boolean` (I
didn't check others but pretty sure all the AM Types are broken) the SQL
was being treated as a `String` and not the correct type.
This caused Rails to incorrectly query by string values. This is
problematic for columns storing binary data like our emoji columns at
GitHub. The test added here is an example of how the Binary type was
broken previously. The SQL should be using the hex values, not the
string value of "🥦" or other emoji.
We still have the problem `unchecked_serialize` was supposed to fix -
that `serialize` shouldn't validate data, just convert it. We'll be
fixing that in a followup PR so for now we should use `serialize` so we
know all the values are going through the right serialization for their
SQL.
Since #31827, marshalling attributes hash format is changed to improve
performance because materializing lazy attribute hash is too expensive.
In that time, we had kept an ability to load from legacy attributes
format, since that performance improvement is backported to 5-1-stable
and 5-0-stable.
Now all supported versions will dump attributes as new format, the
backward compatibity should no longer be needed.
Rails has a monkey patch on `range.cover?` that is slower than Ruby's
`range.cover?`. We don't need Range support in this case because the SQL
creates a `BETWEEN` not an `IN` statement.
A coworker at GitHub found a few months back that if we used
`santitize_sql` over `where` when we knew the values going into `where`
it was a lot faster than `where`.
This PR adds a new Arel node type called `HomogenousIn` that will be
used when Rails knows the values are all homogenous and can therefore
pick a faster codepath. This new codepath skips some of the required
processing by `where` to make `wheres` with homogenous arrays faster
without requiring the application author to know when to use which query
type.
Using our benchmark code:
```ruby
ids = (1..1000).each.map do |n|
Post.create!.id
end
Benchmark.ips do |x|
x.report("where with ids") do
Post.where(id: ids).to_a
end
x.report("where with sanitize") do
Post.where(ActiveRecord::Base.sanitize_sql(["id IN (?)", ids])).to_a
end
x.compare!
end
```
Before this PR comparing where with a list of IDs to santitize sql:
```
Warming up --------------------------------------
where with ids 11.000 i/100ms
where with sanitize 17.000 i/100ms
Calculating -------------------------------------
where with ids 115.733 (± 4.3%) i/s - 583.000 in 5.045828s
where with sanitize 174.231 (± 4.0%) i/s - 884.000 in 5.081495s
Comparison:
where with sanitize: 174.2 i/s
where with ids: 115.7 i/s - 1.51x slower
```
After this PR comparing where with a list of IDs to santitize sql:
```
Warming up --------------------------------------
where with ids 16.000 i/100ms
where with sanitize 19.000 i/100ms
Calculating -------------------------------------
where with ids 158.293 (± 6.3%) i/s - 800.000 in 5.072208s
where with sanitize 169.141 (± 3.5%) i/s - 855.000 in 5.060878s
Comparison:
where with sanitize: 169.1 i/s
where with ids: 158.3 i/s - same-ish: difference falls within error
```
Co-authored-by: Aaron Patterson <aaron.patterson@gmail.com>
This deprecation is useless since the result is still an Error object
and there is no way to fix the code to remove the deprecation.
Let's just accept as a breaking change.
This method replaces the `keys` method on `errors` as a way to get the error attribute names from the errors object without treating `errors` like a hash.
This message more clearly communicates how to access the attribute and message keys that you would expect to get when using the previous API.
before you might iterate over errors like,
errors.each do |attribute, message|
# My error code here
end
This message helps the user find the methods on error that match the previous API.
Mark private constants
Display alternative for deprecation removal warning
Annotate Error's attributes
More emphasis on adding an error instead of message
Rewrite scaffold template using new errors API
Set first and last with behavior change deprecation
Update more doc and example
Add inspect for easier debugging
This reverts commit 4e105385d046e2aeab16955943df97c5eefa3a6f, reversing
changes made to 62b43839098bbbbfc4be789128d33dc0612f1ab3.
The change in Ruby that made those changes required was reverted in
8852fa8760
LazilyDefineAttributes only needs to stay in the model's ancestors long
enough to define accessors for acceptance attributes, after that it is
not needed anymore. To optimize this, we eagerly define attributes as
soon as either +method_missing+ or +respond_to_missing?+ is called, then
immediately remove those methods from the module so that future calls
will incur no performance penalty.
I've also removed the call to +define_attribute_methods+ here, which
is no longer be necessary to avoid an infinite loop.
reload is an ActiveRecord method, not an ActiveModel one.
clear_changes_information is a public method so it should be tested
directly, not through a method defined for on the DirtyModel test class.