Commit Graph

2551 Commits

Author SHA1 Message Date
Matthew Draper
bb7f3be138
Revert "Quote binary strings in Arel" 2023-03-15 19:50:50 +10:30
Jonathan Hefner
db8f664b50
Merge pull request #47490 from lazaronixon/has_secure_password_salt
Enhance has_secure_password to also generate a password_salt method

Co-authored-by: Guillermo Iguaran <guilleiguaran@gmail.com>
2023-03-09 14:53:10 -06:00
Nixon
ebe9e575b7 Enhance has_secure_password to also generate a password_salt method 2023-03-09 16:44:10 -03:00
Ole Friis Østergaard
f4242739aa Move SQLite3 blob encoding to ActiveModel 2023-03-09 09:23:00 +00:00
zzak
aa2052ec4c
💅 AM CHANGELOG fixed-width re: #45463, #47569 2023-03-04 08:51:05 +09:00
Rafael Mendonça França
9f60cd8dc7
Merge PR #45463 2023-03-03 22:58:01 +00:00
Petrik
661c995f3b Add class name to ActiveModel::MissingAttributeError error message.
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
2023-03-03 14:31:22 +01:00
joernchen of Phenoelit
2030530865 Fix issue with attr_protected where malformed input could circumvent
protection

Fixes: CVE-2013-0276
2023-02-26 22:20:26 +01:00
Jon Dufresne
da82e587f2 Improve typography of user facing validation messages
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.
2023-02-25 08:21:19 -08:00
zzak
d2af670dba
Remove Copyright years (#47467)
* 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>
2023-02-23 11:38:16 +01:00
Yasuo Honda
f838a74212
Merge pull request #46866 from ghousemohamed/change-year-2022-to-2023 2023-02-13 13:15:43 +09:00
Étienne Barrié
c958bc1342 Remove unparsed :method directive
RDoc doesn't parse this directive since the method is defined below.
It's only useful for metaprogrammed methods.

https://github.com/ruby/rdoc/blob/v6.5.0/lib/rdoc/parser/ruby.rb#L138-L139
2023-02-07 13:15:07 +01:00
zzak
8feec6d452 Tiny knits from AM::Validator docs 2023-02-06 16:39:00 +09:00
zzak
f8544410a2 Use stable guides link for package READMEs
E.g.: These show up here:
https://api.rubyonrails.org/files/actioncable/README_md.html
2023-02-04 09:01:00 +09:00
Jean Boussier
aa7d78d9b1 Improve Rails' Shape friendliness (second pass)
Followup: https://github.com/rails/rails/pull/47023

```
Shape Edges Report
-----------------------------------
snip...
       238  @errors
snip...
       219  @options
snip...
       129  @_request
       128  @type
       125  @virtual_path
       124  @_assigns
       123  @_config
       123  @_controller
       123  @output_buffer
       123  @view_flow
       122  @_default_form_builder
snip...
        89  @_already_called
        75  @validation_context
snip...
        65  @_new_record_before_last_commit
snip...
        58  @_url_options
snip...
```
2023-01-17 13:55:49 +01:00
Rafael Mendonça França
a52596c943
Merge pull request #47022 from zzak/callbacks-links-continued
Fix more links for callbacks
2023-01-16 15:38:27 -05:00
Jean Boussier
fc950324bd Improve Rails' Shape friendliness
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`.
2023-01-16 12:31:37 +01:00
zzak
ae8ca668d8 Fix more links for callbacks
Some of these are linked unnecessarily, while some should be added.
2023-01-16 19:04:26 +09:00
Akira Matsuda
bd11e520a2
Merge pull request #46868 from amatsuda/Time.new_string
Use Ruby 3.2's native ISO 8601-ish String parser to cast String to Time in Active Record
2023-01-11 03:56:25 +09:00
Jonathan Hefner
3eadf057db Fix typos in API docs [ci-skip] 2023-01-08 15:47:20 -06:00
Jonathan Hefner
33557c5dca Indent private methods in code examples [ci-skip]
This matches the indentation used in generated code, such as code from
`railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt`.
2023-01-08 15:47:20 -06:00
zzak
50507abe44
Merge pull request #46856 from zzak/validations-api-default-error-messages
[ci-skip] Document default error messages for some validations
2023-01-08 08:29:52 +09:00
Ghouse Mohamed
e0559d2c1c Change 2022 -> 2023 2023-01-03 13:22:00 +05:30
Akira Matsuda
341b30be2e
Wrap broken Time object with Time.at for Ruby 3.2.0 bug
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
2023-01-02 21:14:35 +09:00
Akira Matsuda
afa47b93a1
Use Ruby 3.2's native ISO 8601-ish String parser for speed and maintainability
Benchmark:

"UTC"
Warming up --------------------------------------
                 new   220.100k i/100ms
                 old   110.278k i/100ms
Calculating -------------------------------------
                 new      2.513M (± 3.0%) i/s -     12.766M in   5.084750s
                 old      1.182M (± 3.3%) i/s -      5.955M in   5.045163s

Comparison:
                 new:  2512925.8 i/s
                 old:  1181629.4 i/s - 2.13x  (± 0.00) slower

"non-UTC"
Warming up --------------------------------------
                 new    88.942k i/100ms
                 old    62.553k i/100ms
Calculating -------------------------------------
                 new    927.745k (± 2.9%) i/s -      4.714M in   5.085328s
                 old    637.047k (± 2.9%) i/s -      3.190M in   5.012170s

Comparison:
                 new:   927744.8 i/s
                 old:   637047.3 i/s - 1.46x  (± 0.00) slower
2023-01-02 15:59:16 +09:00
zzak
b9aaf99701
Fix rdoc syntax for errors#add (#46857) 2022-12-31 10:34:02 +09:00
zzak
9de0d59bb7
Fix rdoc syntax for Kernel.Float (#46860) 2022-12-31 10:33:16 +09:00
zzak
29afcac53b Document default error messages for some validations 2022-12-30 09:56:51 +09:00
Yasuo Honda
40255e4af0
Merge pull request #46397 from shouichi/document-validation-context
Document ActiveModel#validation_context [skip ci]
2022-12-02 08:48:26 +09:00
Shouichi Kamiya
8be20b0deb Document ActiveModel#validation_context [skip ci]
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.
2022-11-29 11:36:59 +09:00
Rafael Mendonça França
5df0f0da7c
Merge pull request #46535 from nashby/type-json
Fix `as_json` call on ActiveModel::Type's child classes.
2022-11-28 14:54:22 -05:00
Étienne Barrié
3d6a7b2faa Initialize deprecators before configuring them
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.
2022-11-28 10:47:26 +01:00
Vasiliy Ermolovich
433bd5995d Raise NoMethodError in ActiveModel::Type::Value#as_json method.
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.
2022-11-25 21:25:06 +01:00
fatkodima
f2f3fa95ec Autoload ActiveModel::ValidationError 2022-11-22 12:36:04 +02:00
Jonathan Hefner
8d9f5257e3 Clarify numeric casting behavior for blank strings [ci-skip]
`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.
2022-11-15 13:57:15 -06:00
Jonathan Hefner
06d37079e8 Document ActiveModel::Type::Time as time of day [ci-skip]
`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.
2022-11-15 13:57:15 -06:00
eileencodes
dbb47d6abd
Revert "Merge pull request #46282 from jonathanhefner/active_model-forgetting_assignment-avoid-value_for_database"
This reverts commit 1f039d8f400e2b9bf76a9c25bd3916eb0aefa0f7, reversing
changes made to be0b5c65a175b6c92514375fc7044efb11cdbe90.

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.
2022-11-08 13:03:01 -05:00
Jonathan Hefner
16ab507a4e
Merge pull request #46401 from jonathanhefner/other-framework-deprecators
Add other framework deprecators
2022-11-03 17:10:05 -05:00
Sébastien Puyet
5e3b9f27ce Fix active model errors add documentation 2022-11-03 17:22:11 +01:00
Jonathan Hefner
74794858c9 Add ActiveModel.deprecator
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`.
2022-11-01 17:39:39 -05:00
Jonathan Hefner
98aa59a7f6 Avoid value_for_database if attribute not updated
After a record is saved, `ActiveModel::Attribute#forgetting_assignment`
is called on each of its attributes.  `forgetting_assignment`, in turn,
calls `ActiveModel::Attribute#value_for_database`.  If an attribute was
not updated, and therefore `value_for_database` was not previously
computed, this will involve an unnecessary serialize (and cast,
depending on type).  And if the attribute was not previously read, it
will also involve an unnecessary deserialize (and cast, depending on
type).

This commit overrides `FromDatabase#forgetting_assignment` to dup the
attribute instead of computing `value_for_database` in the case where
the attribute was not updated.  This can improve the performance for
basic types:

**Benchmark script**

  ```ruby
  # frozen_string_literal: true
  require "benchmark/ips"

  ActiveModel::Attribute.alias_method :baseline_forgetting_assignment, :forgetting_assignment

  {
    big_integer: 123456,
    boolean: 1,
    date: "1999-12-31",
    datetime: "1999-12-31 12:34:56.789011",
    decimal: 123.456,
    float: 123.456,
    immutable_string: "abcdef",
    integer: 123456,
    string: "abcdef",
    time: "1999-12-31 12:34:56.789011",
  }.each do |type_name, value_before_type_cast|
    puts "=== #{type_name} ".ljust(70, "=")

    type = ActiveModel::Type.lookup(type_name)

    Benchmark.ips do |x|
      x.report("before") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.baseline_forgetting_assignment
      end

      x.report("after") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.forgetting_assignment
      end

      x.compare!
    end

    Benchmark.ips do |x|
      x.report("before w/ read") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.value
        attribute.baseline_forgetting_assignment
      end

      x.report("after w/ read") do
        attribute = ActiveModel::Attribute.from_database("x", value_before_type_cast, type)
        attribute.value
        attribute.forgetting_assignment
      end

      x.compare!
    end
  end
  ```

**Results**

  ```
  === big_integer ======================================================
  Warming up --------------------------------------
                before    35.830k i/100ms
                 after    99.520k i/100ms
  Calculating -------------------------------------
                before    356.010k (± 1.0%) i/s -      1.792M in   5.032655s
                 after    989.674k (± 1.2%) i/s -      4.976M in   5.028638s

  Comparison:
                 after:   989674.2 i/s
                before:   356010.4 i/s - 2.78x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    35.030k i/100ms
         after w/ read    56.005k i/100ms
  Calculating -------------------------------------
        before w/ read    349.509k (± 1.3%) i/s -      1.752M in   5.012188s
         after w/ read    558.953k (± 1.2%) i/s -      2.800M in   5.010511s

  Comparison:
         after w/ read:   558953.4 i/s
        before w/ read:   349508.6 i/s - 1.60x  (± 0.00) slower

  === boolean ==========================================================
  Warming up --------------------------------------
                before    31.989k i/100ms
                 after    99.698k i/100ms
  Calculating -------------------------------------
                before    319.969k (± 1.0%) i/s -      1.631M in   5.099207s
                 after    994.759k (± 1.1%) i/s -      4.985M in   5.011775s

  Comparison:
                 after:   994758.6 i/s
                before:   319969.3 i/s - 3.11x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    31.090k i/100ms
         after w/ read    33.306k i/100ms
  Calculating -------------------------------------
        before w/ read    308.859k (± 1.1%) i/s -      1.554M in   5.033639s
         after w/ read    332.726k (± 1.1%) i/s -      1.665M in   5.005669s

  Comparison:
         after w/ read:   332726.2 i/s
        before w/ read:   308858.6 i/s - 1.08x  (± 0.00) slower

  === date =============================================================
  Warming up --------------------------------------
                before    14.466k i/100ms
                 after    98.451k i/100ms
  Calculating -------------------------------------
                before    145.594k (± 1.4%) i/s -    737.766k in   5.068354s
                 after    990.526k (± 1.1%) i/s -      5.021M in   5.069638s

  Comparison:
                 after:   990526.4 i/s
                before:   145593.9 i/s - 6.80x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    14.130k i/100ms
         after w/ read    14.702k i/100ms
  Calculating -------------------------------------
        before w/ read    141.887k (± 1.3%) i/s -    720.630k in   5.079719s
         after w/ read    148.244k (± 1.0%) i/s -    749.802k in   5.058433s

  Comparison:
         after w/ read:   148244.1 i/s
        before w/ read:   141886.8 i/s - 1.04x  (± 0.00) slower

  === datetime =========================================================
  Warming up --------------------------------------
                before     9.484k i/100ms
                 after    97.889k i/100ms
  Calculating -------------------------------------
                before     95.640k (± 1.3%) i/s -    483.684k in   5.058190s
                 after    988.827k (± 1.1%) i/s -      4.992M in   5.049376s

  Comparison:
                 after:   988827.4 i/s
                before:    95639.7 i/s - 10.34x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read     9.440k i/100ms
         after w/ read    10.479k i/100ms
  Calculating -------------------------------------
        before w/ read     94.420k (± 1.1%) i/s -    481.440k in   5.099530s
         after w/ read    105.935k (± 1.0%) i/s -    534.429k in   5.045377s

  Comparison:
         after w/ read:   105935.2 i/s
        before w/ read:    94419.6 i/s - 1.12x  (± 0.00) slower

  === decimal ==========================================================
  Warming up --------------------------------------
                before    12.877k i/100ms
                 after    98.081k i/100ms
  Calculating -------------------------------------
                before    127.627k (± 1.4%) i/s -    643.850k in   5.045749s
                 after    990.178k (± 0.9%) i/s -      5.002M in   5.052175s

  Comparison:
                 after:   990178.0 i/s
                before:   127627.5 i/s - 7.76x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    12.640k i/100ms
         after w/ read    19.739k i/100ms
  Calculating -------------------------------------
        before w/ read    124.933k (± 1.4%) i/s -    632.000k in   5.059694s
         after w/ read    200.110k (± 1.0%) i/s -      1.007M in   5.031169s

  Comparison:
         after w/ read:   200110.3 i/s
        before w/ read:   124932.8 i/s - 1.60x  (± 0.00) slower

  === float ============================================================
  Warming up --------------------------------------
                before    45.424k i/100ms
                 after    99.512k i/100ms
  Calculating -------------------------------------
                before    449.861k (± 1.0%) i/s -      2.271M in   5.049231s
                 after    991.130k (± 1.1%) i/s -      4.976M in   5.020705s

  Comparison:
                 after:   991130.0 i/s
                before:   449861.3 i/s - 2.20x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    43.283k i/100ms
         after w/ read    42.888k i/100ms
  Calculating -------------------------------------
        before w/ read    431.630k (± 1.0%) i/s -      2.164M in   5.014423s
         after w/ read    429.631k (± 1.0%) i/s -      2.187M in   5.091609s

  Comparison:
        before w/ read:   431630.0 i/s
         after w/ read:   429631.0 i/s - same-ish: difference falls within error

  === immutable_string =================================================
  Warming up --------------------------------------
                before    36.820k i/100ms
                 after    97.812k i/100ms
  Calculating -------------------------------------
                before    369.412k (± 1.1%) i/s -      1.878M in   5.083860s
                 after    985.345k (± 1.1%) i/s -      4.988M in   5.063224s

  Comparison:
                 after:   985345.0 i/s
                before:   369411.8 i/s - 2.67x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    36.082k i/100ms
         after w/ read    37.250k i/100ms
  Calculating -------------------------------------
        before w/ read    361.134k (± 1.0%) i/s -      1.840M in   5.096085s
         after w/ read    369.471k (± 0.8%) i/s -      1.862M in   5.041349s

  Comparison:
         after w/ read:   369471.0 i/s
        before w/ read:   361133.6 i/s - 1.02x  (± 0.00) slower

  === integer ==========================================================
  Warming up --------------------------------------
                before    35.763k i/100ms
                 after    98.444k i/100ms
  Calculating -------------------------------------
                before    358.171k (± 1.1%) i/s -      1.824M in   5.092870s
                 after    995.138k (± 1.0%) i/s -      5.021M in   5.045729s

  Comparison:
                 after:   995137.6 i/s
                before:   358171.4 i/s - 2.78x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    34.534k i/100ms
         after w/ read    53.971k i/100ms
  Calculating -------------------------------------
        before w/ read    345.029k (± 1.0%) i/s -      1.727M in   5.005030s
         after w/ read    537.373k (± 0.9%) i/s -      2.699M in   5.022178s

  Comparison:
         after w/ read:   537373.4 i/s
        before w/ read:   345029.3 i/s - 1.56x  (± 0.00) slower

  === string ===========================================================
  Warming up --------------------------------------
                before    33.882k i/100ms
                 after    97.193k i/100ms
  Calculating -------------------------------------
                before    339.174k (± 1.1%) i/s -      1.728M in   5.095272s
                 after    984.544k (± 0.9%) i/s -      4.957M in   5.035057s

  Comparison:
                 after:   984543.5 i/s
                before:   339174.0 i/s - 2.90x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read    32.897k i/100ms
         after w/ read    31.767k i/100ms
  Calculating -------------------------------------
        before w/ read    329.911k (± 0.8%) i/s -      1.678M in   5.085756s
         after w/ read    320.010k (± 0.6%) i/s -      1.620M in   5.062867s

  Comparison:
        before w/ read:   329910.6 i/s
         after w/ read:   320010.1 i/s - 1.03x  (± 0.00) slower

  === time =============================================================
  Warming up --------------------------------------
                before     7.461k i/100ms
                 after    97.467k i/100ms
  Calculating -------------------------------------
                before     74.854k (± 1.5%) i/s -    380.511k in   5.084475s
                 after    986.169k (± 0.9%) i/s -      4.971M in   5.040923s

  Comparison:
                 after:   986168.8 i/s
                before:    74854.5 i/s - 13.17x  (± 0.00) slower

  Warming up --------------------------------------
        before w/ read     7.438k i/100ms
         after w/ read     8.046k i/100ms
  Calculating -------------------------------------
        before w/ read     73.436k (± 1.5%) i/s -    371.900k in   5.065491s
         after w/ read     79.752k (± 1.5%) i/s -    402.300k in   5.045612s

  Comparison:
         after w/ read:    79751.7 i/s
        before w/ read:    73435.8 i/s - 1.09x  (± 0.00) slower
  ```

And, notably, this avoids potentially expensive serialize and
deserialize calls for non-basic types, such as with encrypted
attributes, when the attribute has not been read or assigned:

**Before**

  ```irb
  irb> Post.encrypts :body, encryptor: MyLoggingEncryptor.new

  irb> post = Post.create!(title: "untitled", body: "The Body")
  ! encrypting "The Body"
    TRANSACTION (0.1ms)  begin transaction
    Post Create (0.6ms)  INSERT INTO "posts" ...
  ! decrypted "The Body"
    TRANSACTION (120.2ms)  commit transaction

  irb> post.update!(title: "The Title")
    TRANSACTION (0.1ms)  begin transaction
    Post Update (0.7ms)  UPDATE "posts" ...
  ! decrypted "The Body"
  ! encrypting "The Body"
    TRANSACTION (92.1ms)  commit transaction
  ```

**After**

  ```irb
  irb> Post.encrypts :body, encryptor: MyLoggingEncryptor.new

  irb> post = Post.create!(title: "untitled", body: "The Body")
  ! encrypting "The Body"
    TRANSACTION (0.1ms)  begin transaction
    Post Create (0.7ms)  INSERT INTO "posts" ...
  ! decrypted "The Body"
    TRANSACTION (103.7ms)  commit transaction

  irb> post.update!(title: "The Title")
    TRANSACTION (0.1ms)  begin transaction
    Post Update (0.7ms)  UPDATE "posts" ...
    TRANSACTION (91.2ms)  commit transaction
  ```
2022-10-20 15:39:10 -05:00
Blake Gearin
30edef785f
Update documentation of length validation 2022-10-17 16:11:50 -05:00
Jonathan Hefner
caf9413604
Merge pull request #46231 from jonathanhefner/active_model-memoize-value_for_database
Avoid unnecessary `serialize` calls after save
2022-10-16 17:13:21 -05:00
Jonathan Hefner
28ebf3c81c Avoid double cast in types that only override cast
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
  ```
2022-10-16 16:06:16 -05:00
Jonathan Hefner
5e62c194e5 Avoid unnecessary serialize calls after save
Saving a record calls `ActiveModel::Attribute#value_for_database` on
each of its attributes.  `value_for_database`, in turn, calls
`serialize`.  After a record is successfully saved, its attributes are
reset via `ActiveModel::Attribute#forgetting_assignment`, which also
calls `value_for_database`.  This means attributes are unnecessarily
re-serialized right after they are saved.

This commit memoizes `value_for_database` so that `serialize` is not
called a 2nd time after save.  Because `value` is the single source of
truth and can change in place, the memoization carefully checks for
when `value` differs from the memoized `@value_for_database`.

This yields a small performance increase when saving, and a larger
performance increase when repeatedly reading `value_for_database` for
most types.

**Benchmark script**

  ```ruby
  # frozen_string_literal: true
  require "benchmark/ips"

  ActiveModel::Attribute.subclasses.each { |subclass| subclass.send(:public, :_value_for_database) }

  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

  attribute_set = MyModel.new(VALUES).instance_variable_get(:@attributes)

  def class_name(object) = object.class.name.demodulize

  def mimic_save(attribute)
    puts "=== #{__method__} / #{class_name attribute.type} #{class_name attribute} ".ljust(70, "=")

    Benchmark.ips do |x|
      x.report("before") do
        fresh_copy = attribute.dup
        fresh_copy._value_for_database
        fresh_copy.forgetting_assignment
      end

      x.report("after") do
        fresh_copy = attribute.dup
        fresh_copy.value_for_database
        fresh_copy.forgetting_assignment
      end

      x.compare!
    end
  end

  VALUES.each_key do |name|
    mimic_save(attribute_set[name.to_s].forgetting_assignment)
    mimic_save(attribute_set[name.to_s])
  end

  def get_value_for_database(attribute)
    puts "=== #{__method__} / #{class_name attribute.type} #{class_name attribute} ".ljust(70, "=")

    Benchmark.ips do |x|
      x.report("before") { attribute._value_for_database }
      x.report("after") { attribute.value_for_database }
      x.compare!
    end
  end

  VALUES.each_key do |name|
    get_value_for_database(attribute_set[name.to_s].forgetting_assignment)
    get_value_for_database(attribute_set[name.to_s])
  end
  ```

**`mimic_save` Results**

  ```
  === mimic_save / BigInteger FromDatabase =============================
  Warming up --------------------------------------
                before    24.460k i/100ms
                 after    28.474k i/100ms
  Calculating -------------------------------------
                before    243.390k (± 1.0%) i/s -      1.223M in   5.025334s
                 after    284.497k (± 0.8%) i/s -      1.424M in   5.004566s

  Comparison:
                 after:   284497.1 i/s
                before:   243389.7 i/s - 1.17x  (± 0.00) slower

  === mimic_save / BigInteger FromUser =================================
  Warming up --------------------------------------
                before    58.151k i/100ms
                 after    64.633k i/100ms
  Calculating -------------------------------------
                before    581.268k (± 1.2%) i/s -      2.908M in   5.002814s
                 after    645.165k (± 1.2%) i/s -      3.232M in   5.009752s

  Comparison:
                 after:   645164.8 i/s
                before:   581267.9 i/s - 1.11x  (± 0.00) slower

  === mimic_save / Boolean FromDatabase ================================
  Warming up --------------------------------------
                before    36.771k i/100ms
                 after    38.218k i/100ms
  Calculating -------------------------------------
                before    371.521k (± 1.1%) i/s -      1.875M in   5.048310s
                 after    384.021k (± 0.9%) i/s -      1.949M in   5.075966s

  Comparison:
                 after:   384021.4 i/s
                before:   371520.8 i/s - 1.03x  (± 0.00) slower

  === mimic_save / Boolean FromUser ====================================
  Warming up --------------------------------------
                before    58.738k i/100ms
                 after    63.935k i/100ms
  Calculating -------------------------------------
                before    582.358k (± 0.9%) i/s -      2.937M in   5.043559s
                 after    633.391k (± 0.9%) i/s -      3.197M in   5.047443s

  Comparison:
                 after:   633390.5 i/s
                before:   582358.2 i/s - 1.09x  (± 0.00) slower

  === mimic_save / Date FromDatabase ===================================
  Warming up --------------------------------------
                before    28.242k i/100ms
                 after    31.247k i/100ms
  Calculating -------------------------------------
                before    282.438k (± 1.0%) i/s -      1.412M in   5.000177s
                 after    311.108k (± 1.0%) i/s -      1.562M in   5.022362s

  Comparison:
                 after:   311108.4 i/s
                before:   282437.9 i/s - 1.10x  (± 0.00) slower

  === mimic_save / Date FromUser =======================================
  Warming up --------------------------------------
                before    43.427k i/100ms
                 after    47.354k i/100ms
  Calculating -------------------------------------
                before    431.978k (± 1.3%) i/s -      2.171M in   5.027373s
                 after    470.658k (± 1.3%) i/s -      2.368M in   5.031540s

  Comparison:
                 after:   470658.0 i/s
                before:   431978.2 i/s - 1.09x  (± 0.00) slower

  === mimic_save / DateTime FromDatabase ===============================
  Warming up --------------------------------------
                before    20.997k i/100ms
                 after    24.962k i/100ms
  Calculating -------------------------------------
                before    210.672k (± 0.9%) i/s -      1.071M in   5.083391s
                 after    248.114k (± 0.8%) i/s -      1.248M in   5.030687s

  Comparison:
                 after:   248114.5 i/s
                before:   210671.9 i/s - 1.18x  (± 0.00) slower

  === mimic_save / DateTime FromUser ===================================
  Warming up --------------------------------------
                before    30.406k i/100ms
                 after    45.886k i/100ms
  Calculating -------------------------------------
                before    304.374k (± 0.9%) i/s -      1.551M in   5.095184s
                 after    456.754k (± 1.3%) i/s -      2.294M in   5.023891s

  Comparison:
                 after:   456753.8 i/s
                before:   304374.0 i/s - 1.50x  (± 0.00) slower

  === mimic_save / Decimal FromDatabase ================================
  Warming up --------------------------------------
                before    11.381k i/100ms
                 after    13.632k i/100ms
  Calculating -------------------------------------
                before    112.355k (± 1.4%) i/s -    569.050k in   5.065752s
                 after    135.940k (± 1.5%) i/s -    681.600k in   5.015094s

  Comparison:
                 after:   135939.8 i/s
                before:   112355.1 i/s - 1.21x  (± 0.00) slower

  === mimic_save / Decimal FromUser ====================================
  Warming up --------------------------------------
                before    59.270k i/100ms
                 after    64.668k i/100ms
  Calculating -------------------------------------
                before    595.050k (± 1.3%) i/s -      3.023M in   5.080703s
                 after    644.206k (± 0.9%) i/s -      3.233M in   5.019581s

  Comparison:
                 after:   644205.8 i/s
                before:   595049.7 i/s - 1.08x  (± 0.00) slower

  === mimic_save / Float FromDatabase ==================================
  Warming up --------------------------------------
                before    35.564k i/100ms
                 after    35.632k i/100ms
  Calculating -------------------------------------
                before    355.836k (± 1.5%) i/s -      1.814M in   5.098367s
                 after    361.603k (± 1.1%) i/s -      1.817M in   5.026122s

  Comparison:
                 after:   361603.1 i/s
                before:   355835.7 i/s - same-ish: difference falls within error

  === mimic_save / Float FromUser ======================================
  Warming up --------------------------------------
                before    57.544k i/100ms
                 after    63.450k i/100ms
  Calculating -------------------------------------
                before    572.265k (± 1.1%) i/s -      2.877M in   5.028412s
                 after    631.023k (± 1.1%) i/s -      3.172M in   5.028143s

  Comparison:
                 after:   631022.8 i/s
                before:   572264.9 i/s - 1.10x  (± 0.00) slower

  === mimic_save / ImmutableString FromDatabase ========================
  Warming up --------------------------------------
                before    27.239k i/100ms
                 after    29.235k i/100ms
  Calculating -------------------------------------
                before    272.882k (± 1.1%) i/s -      1.389M in   5.091389s
                 after    292.142k (± 1.1%) i/s -      1.462M in   5.004132s

  Comparison:
                 after:   292142.0 i/s
                before:   272882.0 i/s - 1.07x  (± 0.00) slower

  === mimic_save / ImmutableString FromUser ============================
  Warming up --------------------------------------
                before    44.308k i/100ms
                 after    48.680k i/100ms
  Calculating -------------------------------------
                before    438.869k (± 1.2%) i/s -      2.215M in   5.048665s
                 after    482.455k (± 1.1%) i/s -      2.434M in   5.045670s

  Comparison:
                 after:   482454.7 i/s
                before:   438868.9 i/s - 1.10x  (± 0.00) slower

  === mimic_save / Integer FromDatabase ================================
  Warming up --------------------------------------
                before    25.554k i/100ms
                 after    29.236k i/100ms
  Calculating -------------------------------------
                before    254.308k (± 1.1%) i/s -      1.278M in   5.024863s
                 after    292.265k (± 1.1%) i/s -      1.462M in   5.002250s

  Comparison:
                 after:   292265.3 i/s
                before:   254308.2 i/s - 1.15x  (± 0.00) slower

  === mimic_save / Integer FromUser ====================================
  Warming up --------------------------------------
                before    46.034k i/100ms
                 after    64.028k i/100ms
  Calculating -------------------------------------
                before    458.343k (± 1.2%) i/s -      2.302M in   5.022546s
                 after    636.237k (± 1.1%) i/s -      3.201M in   5.032346s

  Comparison:
                 after:   636237.2 i/s
                before:   458343.4 i/s - 1.39x  (± 0.00) slower

  === mimic_save / String FromDatabase =================================
  Warming up --------------------------------------
                before    25.804k i/100ms
                 after    26.682k i/100ms
  Calculating -------------------------------------
                before    259.941k (± 1.2%) i/s -      1.316M in   5.063398s
                 after    268.140k (± 1.0%) i/s -      1.361M in   5.075435s

  Comparison:
                 after:   268140.2 i/s
                before:   259941.3 i/s - 1.03x  (± 0.00) slower

  === mimic_save / String FromUser =====================================
  Warming up --------------------------------------
                before    40.607k i/100ms
                 after    42.735k i/100ms
  Calculating -------------------------------------
                before    407.731k (± 1.2%) i/s -      2.071M in   5.079973s
                 after    424.659k (± 1.1%) i/s -      2.137M in   5.032247s

  Comparison:
                 after:   424659.2 i/s
                before:   407731.2 i/s - 1.04x  (± 0.00) slower

  === mimic_save / Time FromDatabase ===================================
  Warming up --------------------------------------
                before    21.555k i/100ms
                 after    25.151k i/100ms
  Calculating -------------------------------------
                before    213.479k (± 1.0%) i/s -      1.078M in   5.049047s
                 after    249.833k (± 1.2%) i/s -      1.258M in   5.034246s

  Comparison:
                 after:   249833.1 i/s
                before:   213479.1 i/s - 1.17x  (± 0.00) slower

  === mimic_save / Time FromUser =======================================
  Warming up --------------------------------------
                before    30.226k i/100ms
                 after    45.704k i/100ms
  Calculating -------------------------------------
                before    303.729k (± 1.2%) i/s -      1.542M in   5.076124s
                 after    457.186k (± 0.9%) i/s -      2.331M in   5.098810s

  Comparison:
                 after:   457186.0 i/s
                before:   303729.0 i/s - 1.51x  (± 0.00) slower
  ```

**`get_value_for_database` Results**

  ```
  === get_value_for_database / BigInteger FromDatabase =================
  Warming up --------------------------------------
                before   101.504k i/100ms
                 after   328.924k i/100ms
  Calculating -------------------------------------
                before      1.007M (± 0.7%) i/s -      5.075M in   5.040604s
                 after      3.303M (± 0.6%) i/s -     16.775M in   5.079630s

  Comparison:
                 after:  3302566.7 i/s
                before:  1006908.5 i/s - 3.28x  (± 0.00) slower

  === get_value_for_database / BigInteger FromUser =====================
  Warming up --------------------------------------
                before   282.580k i/100ms
                 after   325.867k i/100ms
  Calculating -------------------------------------
                before      2.840M (± 0.6%) i/s -     14.412M in   5.074481s
                 after      3.329M (± 0.6%) i/s -     16.945M in   5.090498s

  Comparison:
                 after:  3328905.3 i/s
                before:  2840125.6 i/s - 1.17x  (± 0.00) slower

  === get_value_for_database / Boolean FromDatabase ====================
  Warming up --------------------------------------
                before   197.974k i/100ms
                 after   327.017k i/100ms
  Calculating -------------------------------------
                before      1.984M (± 0.8%) i/s -     10.097M in   5.088429s
                 after      3.269M (± 0.7%) i/s -     16.351M in   5.001320s

  Comparison:
                 after:  3269485.0 i/s
                before:  1984376.2 i/s - 1.65x  (± 0.00) slower

  === get_value_for_database / Boolean FromUser ========================
  Warming up --------------------------------------
                before   286.138k i/100ms
                 after   340.681k i/100ms
  Calculating -------------------------------------
                before      2.900M (± 0.7%) i/s -     14.593M in   5.031863s
                 after      3.387M (± 0.6%) i/s -     17.034M in   5.028800s

  Comparison:
                 after:  3387438.6 i/s
                before:  2900285.2 i/s - 1.17x  (± 0.00) slower

  === get_value_for_database / Date FromDatabase =======================
  Warming up --------------------------------------
                before   133.983k i/100ms
                 after   327.549k i/100ms
  Calculating -------------------------------------
                before      1.344M (± 0.7%) i/s -      6.833M in   5.085972s
                 after      3.272M (± 0.7%) i/s -     16.377M in   5.005522s

  Comparison:
                 after:  3272057.0 i/s
                before:  1343591.3 i/s - 2.44x  (± 0.00) slower

  === get_value_for_database / Date FromUser ===========================
  Warming up --------------------------------------
                before   291.156k i/100ms
                 after   336.507k i/100ms
  Calculating -------------------------------------
                before      2.917M (± 1.0%) i/s -     14.849M in   5.090985s
                 after      3.383M (± 0.9%) i/s -     17.162M in   5.073857s

  Comparison:
                 after:  3382717.0 i/s
                before:  2917023.0 i/s - 1.16x  (± 0.00) slower

  === get_value_for_database / DateTime FromDatabase ===================
  Warming up --------------------------------------
                before    75.632k i/100ms
                 after   334.488k i/100ms
  Calculating -------------------------------------
                before    759.512k (± 0.8%) i/s -      3.857M in   5.078867s
                 after      3.363M (± 0.9%) i/s -     17.059M in   5.072516s

  Comparison:
                 after:  3363268.0 i/s
                before:   759512.4 i/s - 4.43x  (± 0.00) slower

  === get_value_for_database / DateTime FromUser =======================
  Warming up --------------------------------------
                before   133.780k i/100ms
                 after   330.351k i/100ms
  Calculating -------------------------------------
                before      1.346M (± 0.8%) i/s -      6.823M in   5.068844s
                 after      3.303M (± 0.9%) i/s -     16.518M in   5.001328s

  Comparison:
                 after:  3302885.8 i/s
                before:  1346115.9 i/s - 2.45x  (± 0.00) slower

  === get_value_for_database / Decimal FromDatabase ====================
  Warming up --------------------------------------
                before    43.500k i/100ms
                 after   329.669k i/100ms
  Calculating -------------------------------------
                before    437.058k (± 1.7%) i/s -      2.218M in   5.077481s
                 after      3.290M (± 0.9%) i/s -     16.483M in   5.010687s

  Comparison:
                 after:  3289905.0 i/s
                before:   437058.2 i/s - 7.53x  (± 0.00) slower

  === get_value_for_database / Decimal FromUser ========================
  Warming up --------------------------------------
                before   288.315k i/100ms
                 after   330.565k i/100ms
  Calculating -------------------------------------
                before      2.886M (± 0.7%) i/s -     14.704M in   5.095872s
                 after      3.309M (± 0.8%) i/s -     16.859M in   5.094675s

  Comparison:
                 after:  3309344.5 i/s
                before:  2885624.4 i/s - 1.15x  (± 0.00) slower

  === get_value_for_database / Float FromDatabase ======================
  Warming up --------------------------------------
                before   187.267k i/100ms
                 after   337.589k i/100ms
  Calculating -------------------------------------
                before      1.888M (± 0.9%) i/s -      9.551M in   5.057695s
                 after      3.350M (± 0.9%) i/s -     16.879M in   5.039205s

  Comparison:
                 after:  3349910.7 i/s
                before:  1888499.4 i/s - 1.77x  (± 0.00) slower

  === get_value_for_database / Float FromUser ==========================
  Warming up --------------------------------------
                before   280.405k i/100ms
                 after   338.447k i/100ms
  Calculating -------------------------------------
                before      2.822M (± 1.0%) i/s -     14.301M in   5.068052s
                 after      3.392M (± 0.8%) i/s -     17.261M in   5.089235s

  Comparison:
                 after:  3391855.1 i/s
                before:  2822015.9 i/s - 1.20x  (± 0.00) slower

  === get_value_for_database / ImmutableString FromDatabase ============
  Warming up --------------------------------------
                before   142.061k i/100ms
                 after   340.814k i/100ms
  Calculating -------------------------------------
                before      1.429M (± 0.9%) i/s -      7.245M in   5.071044s
                 after      3.369M (± 0.8%) i/s -     17.041M in   5.058261s

  Comparison:
                 after:  3369088.0 i/s
                before:  1428830.1 i/s - 2.36x  (± 0.00) slower

  === get_value_for_database / ImmutableString FromUser ================
  Warming up --------------------------------------
                before   285.588k i/100ms
                 after   338.146k i/100ms
  Calculating -------------------------------------
                before      2.890M (± 0.9%) i/s -     14.565M in   5.041037s
                 after      3.369M (± 0.9%) i/s -     16.907M in   5.018268s

  Comparison:
                 after:  3369429.6 i/s
                before:  2889532.4 i/s - 1.17x  (± 0.00) slower

  === get_value_for_database / Integer FromDatabase ====================
  Warming up --------------------------------------
                before   106.915k i/100ms
                 after   334.301k i/100ms
  Calculating -------------------------------------
                before      1.070M (± 1.0%) i/s -      5.453M in   5.098578s
                 after      3.373M (± 0.8%) i/s -     17.049M in   5.054221s

  Comparison:
                 after:  3373498.9 i/s
                before:  1069550.2 i/s - 3.15x  (± 0.00) slower

  === get_value_for_database / Integer FromUser ========================
  Warming up --------------------------------------
                before   175.719k i/100ms
                 after   339.506k i/100ms
  Calculating -------------------------------------
                before      1.760M (± 1.0%) i/s -      8.962M in   5.093648s
                 after      3.368M (± 0.8%) i/s -     16.975M in   5.040973s

  Comparison:
                 after:  3367705.1 i/s
                before:  1759569.2 i/s - 1.91x  (± 0.00) slower

  === get_value_for_database / String FromDatabase =====================
  Warming up --------------------------------------
                before   143.817k i/100ms
                 after   287.976k i/100ms
  Calculating -------------------------------------
                before      1.433M (± 0.9%) i/s -      7.191M in   5.017545s
                 after      2.898M (± 0.9%) i/s -     14.687M in   5.067782s

  Comparison:
                 after:  2898309.6 i/s
                before:  1433247.4 i/s - 2.02x  (± 0.00) slower

  === get_value_for_database / String FromUser =========================
  Warming up --------------------------------------
                before   288.320k i/100ms
                 after   287.449k i/100ms
  Calculating -------------------------------------
                before      2.891M (± 0.7%) i/s -     14.704M in   5.085633s
                 after      2.899M (± 0.6%) i/s -     14.660M in   5.057520s

  Comparison:
                 after:  2898730.4 i/s
                before:  2891484.2 i/s - same-ish: difference falls within error

  === get_value_for_database / Time FromDatabase =======================
  Warming up --------------------------------------
                before    72.976k i/100ms
                 after   335.541k i/100ms
  Calculating -------------------------------------
                before    741.313k (± 0.7%) i/s -      3.722M in   5.020770s
                 after      3.368M (± 0.6%) i/s -     17.113M in   5.080733s

  Comparison:
                 after:  3368266.8 i/s
                before:   741313.5 i/s - 4.54x  (± 0.00) slower

  === get_value_for_database / Time FromUser ===========================
  Warming up --------------------------------------
                before   137.338k i/100ms
                 after   336.559k i/100ms
  Calculating -------------------------------------
                before      1.382M (± 0.6%) i/s -      7.004M in   5.069264s
                 after      3.393M (± 0.5%) i/s -     17.165M in   5.059474s

  Comparison:
                 after:  3392622.1 i/s
                before:  1381763.3 i/s - 2.46x  (± 0.00) slower
  ```

Co-authored-by: Jorge Manrubia <jorge@hey.com>
2022-10-16 16:05:47 -05:00
Jean Boussier
d917896f45 Enable verbose mode in test and report warnings as errors
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.
2022-10-11 09:25:18 +02:00
Jonathan Hefner
8e383fdad6 Avoid double type cast when serializing attributes
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
  ```
2022-09-29 11:34:29 -05:00
Jonathan Hefner
24980fb725 Use actual object instead of Minitest::Mock
Using a `Minitest::Mock` makes these tests brittle.  Therefore, use an
actual object instead.
2022-09-29 11:34:29 -05:00
Aaron Patterson
5abb45d53a Dup and freeze complex types when making query attributes
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
2022-09-19 09:41:39 +02:00
John Bampton
3a32915bbc Fix word case. json -> JSON 2022-09-17 04:11:36 +10:00