Commit Graph

2511 Commits

Author SHA1 Message Date
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
Jean Boussier
26826eb354
Merge pull request #45771 from andrewn617/type-cast-attribute-changed-from-and-to-options
Type cast #attribute_changed? :from and :to options
2022-09-16 12:16:27 +02:00
Yasuo Honda
349a66ebed Drop Rubinius code
Rubinius has not been maintained since May 2020 and based on the
discussion at https://github.com/rails/rails/pull/44984 ,
I think we can remove Rubinius specific code from Rails.
2022-09-15 23:43:51 +09:00
Rafael Mendonça França
46bfabcfd4
Merge pull request #44547 from skipkayhil/fix-incorrect-assertions
fix remaining asserts that should be assert_equal
2022-09-12 20:32:42 -04:00
Yong Bakos
cfca0bfab5 ActiveModel: Fix method name in Callbacks documentation.
:initializer -> :initialize
2022-09-12 11:45:54 -07:00
Hartley McGuire
c62dcf54eb
fix remaining asserts that should be assert_equal
Found using Minitest/AssertWithExpectedArgument.

Also enabled the rule per feedback and fixed 29 additional violations
2022-09-09 19:22:21 -04:00
Fabio Sangiovanni
12f3da0ac3 Fix punctuation in has_secure_password docs. 2022-08-30 19:32:00 +02:00
Jacopo
2f5136a3be Normalize virtual attributes on ActiveRecord::Persistence#becomes
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>
2022-08-24 22:51:57 +02:00
Andrew Novoselac
7519457678 Fixes a defect in ActiveModel::Dirty#attribute_changed? where the :from
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.
2022-08-05 11:52:20 -04:00
Jonathan Hefner
239d6fa67d Use tap in conditionally-required password example [ci-skip] 2022-08-04 11:39:29 -05:00
Jonathan Hefner
f53e3ad938 Add example of conditionally requiring a password [ci-skip]
This example addresses the use case brought up by #45487, which has been
reverted by #45753.
2022-08-03 16:14:08 -05:00
Jonathan Hefner
6020f5331d Revert "Allow passing hash on secure password validations"
This reverts #45487 (8804128ad4cfea228ce97005072b029d0379d80f) until a
better API can be decided upon.
2022-08-03 11:52:30 -05:00
Kevin Jacoby
8804128ad4 Allow passing hash on secure password validations
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.
2022-07-26 10:11:43 -05:00
Jonathan Hefner
71c0c81b47 Add CHANGELOG entry for #43688 [ci-skip] 2022-07-25 12:16:46 -05:00
fatkodima
3822172c00 Fix caching of missed translations 2022-07-15 14:49:11 -05:00
Ryuta Kamizono
c3fd9f1776 Remove reverted CHANGELOG entry [ci-skip]
Follow up to #45553.
2022-07-10 10:02:27 +09:00
Gannon McGibbon
17b4b8fd63 Revert "Provide pattern matching for ActiveModel"
This reverts commit 7e499b25acd5e1bfdd54ca2af66678b0ed05def1.
2022-07-09 00:13:23 -04:00
Jean Boussier
01f0cea3b5 Fix code cache namespacing for proxied attribute methods
Fix: https://github.com/rails/rails/issues/45416

The suffix need to be included in the namespace as it is used to
generate the code.
2022-06-25 09:02:41 +02:00
Shouichi Kamiya
f2695d2dc0 Use const_get to get validator classes
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”)
2022-06-01 17:14:45 +09:00
Jonathan Hefner
0fb166e81c Improve performance for contextual validations
This change reduces retained memory when registering contextual
validations, and reduces overhead when running contextual validations.

Benchmark script:

```ruby
require "benchmark/memory"
require "benchmark/ips"

class Post
  include ActiveModel::Validations

  def foo; end
  def bar; end
end

class Comment
  include ActiveModel::Validations

  def foo; end
  def bar; end
end

Benchmark.memory do |x|
  x.report("warmup") do
    Post.validate :will_not_be_called, on: :warmup
    Comment.validate :will_not_be_called, on: :warmup
  end

  x.report("register validations") do
    Post.validate :foo, on: :create
    Post.validate :bar, on: :update
    Comment.validate :foo, on: :create
    Comment.validate :bar, on: :update
  end
end

post = Post.new

Benchmark.ips do |x|
  x.report("run validations") do
    post.valid?(:create)
  end
end
```

Before:

```
Calculating -------------------------------------
              warmup     3.320k memsize (     1.552k retained)
                        48.000  objects (    21.000  retained)
                         0.000  strings (     0.000  retained)
register validations     5.856k memsize (     2.456k retained)
                        92.000  objects (    33.000  retained)
                         0.000  strings (     0.000  retained)
Warming up --------------------------------------
     run validations     6.232k i/100ms
Calculating -------------------------------------
     run validations     61.826k (± 0.7%) i/s -    311.600k in   5.040176s
```

After:

```
Calculating -------------------------------------
              warmup     3.960k memsize (     1.400k retained)
                        51.000  objects (    17.000  retained)
                         0.000  strings (     0.000  retained)
register validations     6.368k memsize (     1.384k retained)
                        94.000  objects (    21.000  retained)
                         0.000  strings (     0.000  retained)
Warming up --------------------------------------
     run validations     6.998k i/100ms
Calculating -------------------------------------
     run validations     70.741k (± 0.6%) i/s -    356.898k in   5.045347s
```
2022-05-27 15:37:11 -05:00
Jonathan Hefner
5389c56292 Dup options in validates_with
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>
2022-05-26 15:08:00 -05:00
fatkodima
91cd5cbc93 Support infinite ranges for LengthValidators :in/:within options 2022-05-20 20:41:18 +03:00
Bo Jeanes
28e40a6405 Add beginless range support to clusivity 2022-05-20 07:46:01 +10:00
fatkodima
00b269ec8d Make validators accept lambdas without record argument 2022-05-18 19:53:29 +03:00
Kevin Newton
7e499b25ac
Provide pattern matching for ActiveModel
It would be nice to be able to pattern match against ActiveModel (and
transitively ActiveRecord). If you want to check multiple attributes
with conditions, it's nice to be able use the pattern matching syntax.

For example:

```ruby
case Current.user
in { superuser: true }
  "Thanks for logging in. You are a superuser."
in { admin: true, name: }
  "Thanks for logging in, admin #{name}!"
in { name: }
  "Welcome, #{name}!"
end
```
2022-05-11 12:47:59 -04:00
fatkodima
0f4b030c44 Fix casting long strings to Date, Time or DateTime 2022-05-11 16:40:31 +03:00
Ryuta Kamizono
714fd07fd9 All intermediate delegation methods should preserve kwargs flag
Since 0456826180,
`foo(*caller_args)` method call delegation no longer preserve kwargs
flag.

Fixes #44846.
2022-04-06 15:32:54 +09:00
Petrik
8e91abbea1 Remove confusing validations comments [ci-skip]
All validations are called on `#save` by default.
The Absence, Acceptance, Presence validators don't do anything special,
so there is no reason to mention they are called on `#save` by default.

Instead we can mention all validations are called in `#save` by default
in ActiveRecord::Validations.

Co-authored-by: Gannon McGibbon <gannon@hey.com>
2022-03-29 16:04:20 +02:00
Kevin Dew
322e62842c
Remove override of ActiveModel#attribute_names
In https://github.com/rails/rails/pull/43036 an optimisation was applied
to ActiveModel#Serialization to speed up the generation of a
serialized_hash by avoiding loading the subjects attributes by using an
attribute_names method. A fallback method,
ActiveModel::Serialization#attribute_names` was added as #attribute_names
isn't part of the public API of ActiveModel.

Unfortunately, this fallback method happens to override the ActiveRecord
method (as ActiveModel::Serialization is a later mixin than
ActiveRecord::AttributeMethods), so this change didn't actually provide
an optimisation - the full attribute data was loaded as per [1]

This change also, in our case, produced some severe performance issues
as it introduced an N+1 query in a situation where we had one gem,
Globalize [2], which adds in dynamic attributes that are loaded by a query;
and another gem, Transitions [3], that checks attribute names at
initialization. The combination of these meant that for every model that
was initialized an extra query would run - no matter what includes or
eager_load steps were in place. This rapidly hindered our applications'
performance and meant we had to rollback the Rails 7 upgrade.

Following rafaelfranca's suggestion [4] this adds a
`attribute_names_for_serialization` method to Serialization modules in
ActiveRecord and ActiveModel. This allows the ActiveRecord one to
override the ActiveModel fallback and thus be optimised.

Some basic benchmarks of this follow - they use code from
https://github.com/ollietreend/rails-demo and have some pretty large
arrays set as serialized attributes [5] to demonstrate impacts.

Loading attribute names:

Rails 7.0.2.3

```
> Benchmark.ms { Widget.all.map(&:attribute_names) }
  Widget Load (131.1ms)  SELECT "widgets".* FROM "widgets"
=> 20108.852999983355
```

This patch

```
> Benchmark.ms { Widget.all.map(&:attribute_names) }
  Widget Load (144.0ms)  SELECT "widgets".* FROM "widgets"
=> 237.96699999365956
```

Using serializable_hash:

Rails 7.0.2.3

```
> widgets = Widget.all.to_a; Benchmark.ms { widgets.map { |w| w.serializable_hash(only: []) } }
  Widget Load (133.3ms)  SELECT "widgets".* FROM "widgets"
=> 22071.45000001765
```

This patch

```
> widgets = Widget.all.to_a; Benchmark.ms { widgets.map { |w| w.serializable_hash(only: []) } }
  Widget Load (83.5ms)  SELECT "widgets".* FROM "widgets"
=> 67.9039999959059
```

[1]: eeb2cfb686/activemodel/lib/active_model/serialization.rb (L151-L154)
[2]: https://github.com/globalize/globalize
[3]: https://github.com/troessner/transitions
[4]: https://github.com/rails/rails/pull/44770#pullrequestreview-922209612
[5]: 525f88887b/db/seeds.rb
2022-03-26 10:31:11 +00:00
Aaron Patterson
4be60c9cbd
Merge pull request #44664 from jonathanhefner/active_model-attribute_registration
Factor out `ActiveModel::AttributeRegistration`
2022-03-17 10:24:11 -07:00
Rafael Mendonça França
5c1bd20f0d
Merge pull request #44693 from ghousemohamed/fix-docs-related-gem-versions
Fix `#version` method docs and some typos [ci-skip]
2022-03-15 16:28:07 -04:00
Ghouse Mohamed
6ee6cb554b Fix #version docs and some typos 2022-03-16 01:48:37 +05:30
Jonathan Hefner
608cbfae36 Factor out ActiveModel::AttributeRegistration
As a step toward sharing more code between Active Model and Active
Record, this commit factors an `ActiveModel::AttributeRegistration`
module out of `ActiveModel::Attributes`.  This module is marked as
`nodoc` and is for internal use only.

Additionally, this commit adds thorough test coverage of attribute
registration and inheritance.  (`activemodel/test/cases/attributes_test.rb`
does already test some of this behavior, but it is focused on high-level
functionality.)
2022-03-11 22:15:14 -06:00
Jonathan Hefner
13cfc27c7b Support password challenge via has_secure_password
This enhances `has_secure_password` to define a `password_challenge`
accessor and the appropriate validation.  When `password_challenge` is
set, the validation checks that it matches the currently *persisted*
`password_digest` (i.e. `password_digest_was`).

This allows a password challenge to be implemented with the same ease as
a password confirmation, re-using the same error handling logic in the
view, as well as the controller.  For example, in the controller,
instead of:

```ruby
password_params = params.require(:password).permit(
  :password_challenge,
  :password,
  :password_confirmation,
)

password_challenge = password_params.delete(:password_challenge)
@password_challenge_failed = !current_user.authenticate(password_challenge)

if !@password_challenge_failed && current_user.update(password_params)
  # ...
end
```

One could write:

```ruby
password_params = params.require(:password).permit(
  :password_challenge,
  :password,
  :password_confirmation,
).with_defaults(password_challenge: "")

if current_user.update(password_params)
  # ...
end
```

And, in the view, instead of checking `@password_challenge_failed`, one
could render a password challenge error in the same manner as other form
field errors, including utilizing `config.action_view.field_error_proc`.
2022-02-28 12:17:02 -06:00
Rafael Mendonça França
269037a765
Merge pull request #44501 from ghousemohamed/fix-inconsistency-in-activemodel-testcases
Fixed inconsistencies in ActiveModel test cases
2022-02-25 14:53:02 -05:00
Jonathan Hefner
de5a4b3ab8 Add ActiveModel::Access
This ports `ActiveRecord::Base#slice` and `#values_at` to a new
`ActiveModel::Access` module, which is included in `ActiveModel::Model`
by default.
2022-02-24 13:52:21 -06:00
Ghouse Mohamed
22fe46b40f Fixed inconsistencies in ActiveModel test cases 2022-02-23 01:30:26 +05:30
Jonathan Hefner
a199aaedb8 Cross-link API docs [ci-skip]
RDoc will automatically format and link API references as long as they
are not already marked up as inline code.

This commit removes markup from various API references so that those
references will link to the relevant API docs.
2022-02-21 11:45:25 -06:00
Jonathan Hefner
9dbf7a58a2 Fix formatting of parameters doc [ci-skip] 2022-02-21 11:11:11 -06:00
Jonathan Hefner
e37adfed4e Add Oxford commas [ci-skip] 2022-02-21 11:11:11 -06:00
Jonathan Hefner
0d3effc97e Replace "overwrite" with "override" [ci-skip]
"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.
2022-02-21 11:11:11 -06:00