`define_proxy_call` accepts `call_args` as an argument which impacts
method body generation but it doesn't use `call_args` in the `namespace`
generation which leads to the same cached method to be reused even if
`call_args` differ.
It has never been an issue since `define_proxy_call` was only used to
generate Active Record attribute methods and we were always passing
the same `call_args` per method name.
However, since https://github.com/rails/rails/pull/48533 we are using
`define_proxy_call` to generate alias attribute methods where `call_args`
differ for the same method name which leads to the same cached method
being reused in wrong places.
This commit fixes the issue by making sure `call_args` are being
considered when generating the `namespace` for the method.
When development tools try to load Rails components, they sometimes end up loading files that will error out since a dependency is missing. In these cases, the tooling can catch the error and change its behaviour.
However, since the warning is printed directly to `$stderr`, the tooling cannot catch and suppress it easily, which ends up causing noise in the output of the tool.
This change makes Rails print these warnings using `Kernel#warn` instead, which can be suppressed by the tooling.
This commit changes bodies of methods generated by `alias_attribute`
along with generating these methods lazily.
Previously the body of the `alias_attribute :new_title, :title` was
`def new_title; title; end`. This commit changes it to
`def new_title; attribute("title"); end`.
This allows for `alias_attribute` to be used to alias attributes named
with a reserved names like `id`:
```ruby
class Topic < ActiveRecord::Base
self.primary_key = :title
alias_attribute :id_value, :id
end
Topic.new.id_value # => 1
```
ActiveRecord::Base has a dedicated ActiveSupport load hook. This adds an
additional hook for ActiveModel::Model, so that when ActiveModel is
being used without ActiveRecord, it can still be modified.
Currently it is a bit unclear which dirty methods can be called on
Active Record models. You have to know that methods from ActiveModel::Dirty
are included.
It also unclear if methods can be invoked in the form of
`saved_change_to_name?` unless you read the documentation of the
`saved_change_to_attribute?` method.
By adding an introduction to the module we can show which methods are
defined specifically for Active Record, and how to call them, very
similar to the ActiveModel::Dirty introduction.
Linking to ActiveModel::Dirty makes it's also easier to find methods
defined there.
Active Model generates methods for each attribute to handle dirty
changes. As these methods are generated they can't be found when
searching for them in the API documentation.
We can use the `:method:` directive to document them in the form of
`*_will_change`, `*_previously_was`, etc.
This notation is already used for documenting ActiveModel::Dirty.
An alternative could be documenting the methods as:
`[attribute_name]_will_change`, `[attribute_name]_previously_was`, etc...
But that adds more noise to the method.
In #48106, `Module#deep_dup` was changed to return the module itself
(not a copy) when the module is not anonymous. However, that causes
non-anonymous modules to become frozen via `value.deep_dup.freeze` when
passed to `ActiveModel::Type::Helpers::Mutable#immutable_value`. So,
for example, class attributes can no longer be set on the module.
To prevent such issues, this commit removes the `freeze` from
`immutable_value`. `immutable_value` is only called by
`ActiveRecord::PredicateBuilder#build_bind_attribute`, which only cares
that other code cannot mutate the value, not that the value is actually
frozen.
This deals with a problem introduced in #7743ab95b8e15581f432206245c691434a3993d1a751b9d451170956d59457a9R8
that was preventing query `Class` serialized attributes. Duplicating the original
`Class` argument generates an anonymous class that can't be serialized as YAML.
This change makes query attributes hasheable based on their frozen casted values
to prevent the problem.
This solution is based on an idea by @matthewd from https://github.com/rails/rails/issues/47338#issuecomment-1424402777.
As part of my Railsconf talk I mentioned that attribute_changed? was an
undocumented method. Sage was surprised by this, and suggested that we
should probably be documenting this, since *_changed? methods are part
of the public API of Active Model.
I discussed this further with Rafael at Railsconf and we believe that in
addition to attribute_changed?, we should probably be documenting the
other dispatch targets in ActiveModel::Dirty as well.
Co-authored-by: Sage Griffin <sage@sagetheprogrammer.com>
- Simplify password validation to only check byte size for BCrypt limit (72 bytes)
- Replace specific error messages with a single "is too long" message
- Update test cases to reflect new error message
Co-authored-by: ChatGPT
- Validate password length in both characters and bytes
- Provide user-friendly error message for character length
- Add byte size validation due to BCrypt's 72-byte limit
Co-authored-by: ChatGPT
[Fix#47600]
Most of the other frameworks use a h1(`=`) instead of h2(`==`) for
class/module documentation. Having a h1 will improve SEO and makes
things look more consistent.
This also adds a missing title and escapes a namespace so it won't be
linked.
Currently when opening the main framework pages there is no introduction
to the framework. Instead we only see a whole lot of modules and the
`gem_version` and `version` methods.
By including the READMEs using the `:include:` directive each frameworks
has a nice introduction.
For markdown READMEs we need to add the :markup: directive.
[ci-skip]
Co-authored-by: zzak <zzakscott@gmail.com>
When an attribute is missing the current message is unclear about which
class is missing the attribute, especially when there are multiple
classes that could miss the attribute.
By adding the classs name to the error message it is easier to debug:
```ruby
user = User.first
user.pets.select(:id).first.user_id
=> ActiveModel::MissingAttributeError: missing attribute 'user_id' for Pet
```
This also makes the error message more inline with the
UnknownAttributeError message:
```ruby
=> ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person
```
Co-authored-by: Yasuo Honda <yasuo.honda@gmail.com
With the universal adoption of UTF-8 in browsers, user facing text can
use more optimal Unicode typography. In digital and print design, using
RIGHT SINGLE QUOTATION MARK (U+2019) is normally preferred over
APOSTROPHE (U+0027) in contractions.
For details, see the Unicode Standard Section 6.2:
https://www.unicode.org/versions/Unicode13.0.0/ch06.pdf
> Punctuation Apostrophe. U+2019 right single quotation mark is
> preferred where the character is to represent a punctuation mark, as
> for contractions: “We’ve been here before.” In this latter case,
> U+2019 is also referred to as a punctuation apostrophe.
* Remove Copyright years
* Basecamp is now 37signals... again
Co-authored-by: David Heinemeier Hansson <dhh@hey.com>
---------
Co-authored-by: David Heinemeier Hansson <dhh@hey.com>
Ruby 3.2 significantly changed how instance variables are store.
It now use shapes, and in short, it's important for performance
to define instance variables in a consistent order to limit the
amount of shapes.
Otherwise, the number of shapes will increase past a point where
MRI won't be able to cache instance variable access. The impact
is even more important when YJIT is enabled.
This PR is data driven. I dump the list of Shapes from Shopify's
monolith production environment, and Rails is very present among
the top offenders:
```
Shape Edges Report
-----------------------------------
770 @default_graphql_name
697 @own_fields
661 @to_non_null_type
555 @own_interface_type_memberships
472 @description
389 @errors
348 @oseid
316 @_view_runtime
310 @_db_runtime
292 @visibility
286 @shop
271 @attribute_method_patterns_cache
264 @namespace_for_serializer
254 @locking_column
254 @primary_key
253 @validation_context
244 @quoted_primary_key
238 @access_controls
234 @_trigger_destroy_callback
226 @_trigger_update_callback
224 @finder_needs_type_condition
215 @_committed_already_called
214 @api_type
203 @mutations_before_last_save
202 @access_controls_overrides
201 @options
198 @mutations_from_database
190 @_already_called
183 @name
179 @_request
176 @own_arguments
175 @_assigns
175 @virtual_path
174 @context
173 @_controller
173 @output_buffer
173 @view_flow
172 @_default_form_builder
169 @cache
159 @_touch_record
151 @attribute_names
151 @default_attributes
150 @columns_hash
149 @attribute_types
148 @columns
147 @marked_for_same_origin_verification
146 @schema_loaded
143 @_config
143 @type
141 @column_names
```
All the changes are of similar nature, the goal is to preset the instance
variable to nil when objects are allocated, or when classes are created.
For classes I leverage the `inherited` hook. If the patern becomes common enough
it might make sense to add a helper for this in `ActiveSupport::Concern`.
This matches the indentation used in generated code, such as code from
`railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt`.
Time.new in: 'UTC' creates an internally broken Time object, hence we need to
recreate a valid Time object with the same value.
see: https://bugs.ruby-lang.org/issues/19292
This issue has been fixed on trunk Ruby, so this can be reverted in the future.
Benchmark:
new: after this patch
old: our own Time parsin hack
"UTC"
Warming up --------------------------------------
new 146.681k i/100ms
old 112.821k i/100ms
Calculating -------------------------------------
new 1.542M (± 0.9%) i/s - 7.750M in 5.025003s
old 1.193M (± 2.4%) i/s - 6.040M in 5.067234s
Comparison:
new: 1542382.4 i/s
old: 1192721.1 i/s - 1.29x (± 0.00) slower
"non-UTC"
Warming up --------------------------------------
new 85.220k i/100ms
old 62.541k i/100ms
Calculating -------------------------------------
new 927.019k (± 0.8%) i/s - 4.687M in 5.056394s
old 640.220k (± 1.8%) i/s - 3.252M in 5.081417s
Comparison:
new: 927018.9 i/s
old: 640219.7 i/s - 1.45x (± 0.00) slower
Background:
To run validations except in a certain context, we need to either
1. explicitly pass all validation contexts to the `on` option
2. access undocumented `validation_context`
Option 1 is error-prone. Additionally, adding the opposite behavior of
`on` was rejected https://github.com/rails/rails/pull/30710.
Solution:
Document `ActiveModel#validation_context` to make it clear that the API is
public.
Close https://github.com/rails/rails/issues/46391.
Since engine initializers run later in the process, we need to run this
initializer earlier than the default.
This ensures they're all registered before the environments are loaded.
Right now since we have instance variable called `itself_if_serialize_cast_value_compatible`
assigned to self when we run `as_json` we get stack too deep error because `as_json` calls
`as_json` on every instance variable. And since `@itself_if_serialize_cast_value_compatible` references
to `self` we run into recursion.
And before that we were returning unpredictable data from `as_json` method so it's better to let it to throw
an error and let user know that Value class is not supposed to be converted to json.
`ActiveModel::Type::Decimal`, `ActiveModel::Type::Float`, and
`ActiveModel::Type::Integer` cast blank strings to `nil` rather than
casting them with `to_d`, `to_f`, or `to_i`. This commit clarifies that
behavior in each type's documentation.
`ActiveModel::Type::Time` is meant to represent time of day (without a
date or time zone). This commit fixes the code example to show that the
date and time zone are normalized when parsing a time from a string.
This reverts commit 1f039d8f40, reversing
changes made to be0b5c65a1.
This revert is temporary while we debug some issues with our app. While
we're pretty sure this is caused by code in our application and a
compbination with `activerecord-typed_store`, the failure mode is easy
to miss because it doesn't raise an exception.
We will un-revert after a bit more investigation. We just want to be
confident that the other cases where this could cause issues are fixed
without being blocked from upgrading for the next month or so.
This commit adds `ActiveModel.deprecator`, and adds it to
`Rails.application.deprecators` so that it can be configured via
settings such as `config.active_support.report_deprecations`.
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
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>