Commit Graph

2313 Commits

Author SHA1 Message Date
Akira Matsuda
f2a182bf7d Time.utc and Time.local are both public methods 2020-10-02 13:06:59 +09:00
Akira Matsuda
9caf8ddb93 *_ditest and *_digest= are defined as public methods 2020-10-02 13:06:55 +09:00
Trevor John
6fba3c3be0
Allow ActiveModel::Name fields to be overriden 2020-09-23 00:17:54 -04:00
Akira Matsuda
badcaf6763 AR::Base#read_attribute_for_validation is a public_method 2020-09-16 12:15:23 +09:00
lulalala
1fee2cbc50 Rename Error#detail method as details
Plural is more expected.
2020-09-11 17:03:59 +08:00
Eugene Kenny
7af59e16a2 Use transform_values in a few more places
It's faster on Ruby 2.7+ since it avoids rehashing the keys.
2020-09-08 01:34:41 +01:00
Abhay Nikam
714f8c8068 Documents other_than option also accepts the proc or a symbol for numericality validation [skip ci] 2020-08-24 21:27:45 +05:30
Eugene Kenny
0d0eb93b16 Add missing require for Enumerable#index_with
Followup to 0adcec49541aac069600202ed5f83c8ef6f2197e.
2020-08-23 09:17:53 +01:00
Matthew Draper
843898c57a
Merge pull request #22610 from KevinSjoberg/feature/array-member-inclusion
Validate inclusion of each object in an array
2020-08-03 00:09:35 +09:30
Vipul A M
1d6daaaa56
Merge pull request #39735 from lulalala/doc-errors-update
Document ActiveModel errors methods
2020-07-31 08:54:08 +05:30
Ryuta Kamizono
4cc438a1df Extract read_attribute_for_validation for per-validator customization
The model global `read_attribute_for_validation` is not fit for some
validators (specifically for numericality validator).

To allow per-validator customization for attribute value, it extracts
`read_attribute_for_validation` in `EachValidator`.
2020-07-29 18:11:49 +09:00
lulalala
f9518dc972 Document model error methods
[ci skip]
2020-06-29 17:50:08 +08:00
Ryuta Kamizono
f870537c47 Allow ISO 8601 formatted string for fast_string_to_time
Currently `ISO_DATETIME` regexp parser doesn't allow ISO 8601 formatted
string, it fallbacks to slower `Date._parse` in `fallback_string_to_time`.

This makes `ISO_DATETIME` allows ISO 8601 formatted string for
`fast_string_to_time`, it makes ISO 8601 formatted string parsing about
3.5x faster.

```ruby
type = ActiveRecord::Type.lookup(:datetime)
local = Time.now.iso8601
utc = Time.now.utc.iso8601

Benchmark.ips do |x|
  x.report("type.cast(local)") { type.cast(local); type.cast(local) }
  x.report("type.cast(utc)")   { type.cast(utc);   type.cast(utc)   }
end
```

Before:

```
Warming up --------------------------------------
    type.cast(local)     2.198k i/100ms
      type.cast(utc)     2.443k i/100ms
Calculating -------------------------------------
    type.cast(local)     22.371k (± 5.9%) i/s -    112.098k in   5.028419s
      type.cast(utc)     23.359k (± 5.5%) i/s -    117.264k in   5.034913s
```

After:

```
Warming up --------------------------------------
    type.cast(local)     6.918k i/100ms
      type.cast(utc)     9.414k i/100ms
Calculating -------------------------------------
    type.cast(local)     71.468k (± 6.2%) i/s -    359.736k in   5.053612s
      type.cast(utc)     86.258k (± 4.5%) i/s -    433.044k in   5.029979s
```
2020-06-20 10:50:08 +09:00
Ryuta Kamizono
575f4e16a7 Do not use slower public_send in type casting
Before:

```
Warming up --------------------------------------
type.cast(usec=0len)     9.439k i/100ms
type.cast(usec=6len)     9.282k i/100ms
type.cast(usec=3len)     9.111k i/100ms
type.cast(usec=7len)     9.059k i/100ms
Calculating -------------------------------------
type.cast(usec=0len)     90.341k (± 4.2%) i/s -    453.072k in   5.023724s
type.cast(usec=6len)     84.970k (± 3.7%) i/s -    426.972k in   5.031444s
type.cast(usec=3len)     84.043k (± 4.3%) i/s -    428.217k in   5.104155s
type.cast(usec=7len)     83.443k (± 5.3%) i/s -    416.714k in   5.008011s
```

After:

```
Warming up --------------------------------------
type.cast(usec=0len)     9.829k i/100ms
type.cast(usec=6len)     9.405k i/100ms
type.cast(usec=3len)     9.315k i/100ms
type.cast(usec=7len)     9.464k i/100ms
Calculating -------------------------------------
type.cast(usec=0len)     92.918k (± 5.3%) i/s -    471.792k in   5.091504s
type.cast(usec=6len)     86.207k (± 4.4%) i/s -    432.630k in   5.027844s
type.cast(usec=3len)     86.060k (± 4.6%) i/s -    437.805k in   5.097409s
type.cast(usec=7len)     87.036k (± 4.3%) i/s -    435.344k in   5.010501s
```
2020-06-20 08:22:45 +09:00
Ryuta Kamizono
e152d3403f Make usec parsing faster
This intends to avoid extra Rational creation for usec parsing
especially in the no usec case.

This makes all usec variations faster.

```ruby
type = ActiveRecord::Type.lookup(:datetime)
time1 = "2020-06-19 19:18:43"
time2 = "2020-06-19 19:18:43.123456"
time3 = "2020-06-19 19:18:43.123"
time4 = "2020-06-19 19:18:43.1234567"

Benchmark.ips do |x|
  x.report("type.cast(usec=0len)") { type.cast(time1); type.cast(time1) }
  x.report("type.cast(usec=6len)") { type.cast(time2); type.cast(time2) }
  x.report("type.cast(usec=3len)") { type.cast(time3); type.cast(time3) }
  x.report("type.cast(usec=7len)") { type.cast(time4); type.cast(time4) }
end
```

Before:

```
Warming up --------------------------------------
type.cast(usec=0len)     7.343k i/100ms
type.cast(usec=6len)     8.993k i/100ms
type.cast(usec=3len)     8.301k i/100ms
type.cast(usec=7len)     8.209k i/100ms
Calculating -------------------------------------
type.cast(usec=0len)     85.825k (± 5.4%) i/s -    433.237k in   5.062319s
type.cast(usec=6len)     82.045k (± 4.3%) i/s -    413.678k in   5.051260s
type.cast(usec=3len)     78.659k (± 4.4%) i/s -    398.448k in   5.074891s
type.cast(usec=7len)     77.477k (± 4.4%) i/s -    394.032k in   5.095355s
```

After:

```
Warming up --------------------------------------
type.cast(usec=0len)     9.439k i/100ms
type.cast(usec=6len)     9.282k i/100ms
type.cast(usec=3len)     9.111k i/100ms
type.cast(usec=7len)     9.059k i/100ms
Calculating -------------------------------------
type.cast(usec=0len)     90.341k (± 4.2%) i/s -    453.072k in   5.023724s
type.cast(usec=6len)     84.970k (± 3.7%) i/s -    426.972k in   5.031444s
type.cast(usec=3len)     84.043k (± 4.3%) i/s -    428.217k in   5.104155s
type.cast(usec=7len)     83.443k (± 5.3%) i/s -    416.714k in   5.008011s
```
2020-06-20 08:08:31 +09:00
Ryuta Kamizono
d2cdf0be67
Merge pull request #39612 from kamipo/faster_attributes
PERF: 45% faster attributes for readonly usage
2020-06-17 20:29:47 +09:00
Ryuta Kamizono
3944fb743a Avoid to use slower define_method for AcceptsMultiparameterTime
This makes `datetime.serialize` about 10% faster.

```ruby
type = ActiveRecord::Type.lookup(:datetime)
time = Time.now.utc

Benchmark.ips do |x|
  x.report("type.serialize(time)") do
    type.serialize(time)
    type.serialize(time)
    type.serialize(time)
    type.serialize(time)
  end
end
```

Before:

```
Warming up --------------------------------------
type.serialize(time)    12.899k i/100ms
Calculating -------------------------------------
type.serialize(time)    131.293k (± 1.6%) i/s -    657.849k in   5.011870s
```

After:

```
Warming up --------------------------------------
type.serialize(time)    14.603k i/100ms
Calculating -------------------------------------
type.serialize(time)    145.941k (± 1.1%) i/s -    730.150k in   5.003639s
```
2020-06-17 19:45:00 +09:00
Ryuta Kamizono
0adcec4954 PERF: Avoid extra delegation to LazyAttributeHash
The extra delegation to `LazyAttributeHash` has non-negligible overhead.

Avoiding that delegation makes attributes access about 45% faster for
readonly (non-mutation) usage.

https://gist.github.com/kamipo/4002c96a02859d8fe6503e26d7be4ad8

Before:

```
IPS
Warming up --------------------------------------
    attribute access     1.000  i/100ms
Calculating -------------------------------------
    attribute access      3.444  (± 0.0%) i/s -     18.000  in   5.259030s
MEMORY
Calculating -------------------------------------
    attribute access    38.902M memsize (     0.000  retained)
                       350.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```

After (with `immutable_strings_by_default = true`):

```
IPS
Warming up --------------------------------------
    attribute access     1.000  i/100ms
Calculating -------------------------------------
    attribute access      5.066  (±19.7%) i/s -     25.000  in   5.024650s
MEMORY
Calculating -------------------------------------
    attribute access    27.382M memsize (     0.000  retained)
                       160.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```
2020-06-15 09:26:24 +09:00
Ryuta Kamizono
5090a64aa3 Lazy allocate @forced_changes
It is almost no longer used in Active Record.
2020-06-14 13:50:07 +09:00
Ryuta Kamizono
528b62e386 Address to false negative for Performance/DeletePrefix,DeleteSuffix
Follow up to c07dff72278fb7f2a3c4c71212a0773a2b25c790.

Actually it is not the cop's fault, but we mistakenly use `^`, `$`, and
`\Z` in much places, the cop doesn't correct those conservatively.

I've checked all those usage and replaced all safe ones.
2020-06-14 13:04:47 +09:00
Ryuta Kamizono
ab8b12eaf6 PERF: 35% faster attributes for readonly usage
Instantiating attributes hash from raw database values is one of the
slower part of attributes.

Why that is necessary is to detect mutations. In other words, that isn't
necessary until mutations are happened.

`LazyAttributeHash` which was introduced at 0f29c21 is to instantiate
attribute lazily until first accessing the attribute (i.e.
`Model.find(1)` isn't slow yet, but `Model.find(1).attr_name` is still
slow).

This introduces `LazyAttributeSet` to instantiate attribute more lazily,
it doesn't instantiate attribute until first assigning/dirty checking
the attribute (i.e. `Model.find(1).attr_name` is no longer slow).

It makes attributes access about 35% faster for readonly (non-mutation)
usage.

https://gist.github.com/kamipo/4002c96a02859d8fe6503e26d7be4ad8

Before:

```
IPS
Warming up --------------------------------------
    attribute access     1.000  i/100ms
Calculating -------------------------------------
    attribute access      3.444  (± 0.0%) i/s -     18.000  in   5.259030s
MEMORY
Calculating -------------------------------------
    attribute access    38.902M memsize (     0.000  retained)
                       350.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```

After (with `immutable_strings_by_default = true`):

```
IPS
Warming up --------------------------------------
    attribute access     1.000  i/100ms
Calculating -------------------------------------
    attribute access      4.652  (±21.5%) i/s -     23.000  in   5.034853s
MEMORY
Calculating -------------------------------------
    attribute access    27.782M memsize (     0.000  retained)
                       170.044k objects (     0.000  retained)
                        15.000  strings (     0.000  retained)
```
2020-06-13 16:01:08 +09:00
Ryuta Kamizono
19839ff46c Fix string type cast with boolean serialization for MySQL
And benchmark with this branch for immutable string type:

```ruby
ActiveRecord::Schema.define do
  create_table :users, force: true do |t|
    t.string :name
    t.string :fast_name
  end
end

class User < ActiveRecord::Base
  attribute :fast_name, :immutable_string
end

user = User.new

Benchmark.ips do |x|
  x.report("user.name") do
    user.name = "foo"
    user.name_changed?
  end
  x.report("user.fast_name") do
    user.fast_name = "foo"
    user.fast_name_changed?
  end
end
```

```
Warming up --------------------------------------
           user.name    34.811k i/100ms
      user.fast_name    39.505k i/100ms
Calculating -------------------------------------
           user.name    343.864k (± 3.6%) i/s -      1.741M in   5.068576s
      user.fast_name    384.033k (± 2.7%) i/s -      1.936M in   5.044425s
```
2020-06-13 07:50:12 +09:00
Sean Griffin
332c3364b6 Add a setting to specify that all string columns should be immutable
In Rails 4.2 we introduced mutation detection, to remove the need to
call `attribute_will_change!` after modifying a field. One side effect
of that change was that we needed to enforce that the
`_before_type_cast` form of a value is a different object than the post
type cast value, if the value is mutable. That contract is really only
relevant for strings, but it meant we needed to dup them.

In Rails 5 we added the `ImmutableString` type, to allow people to opt
out of this duping in places where the memory usage was causing
problems, and they don't need to mutate that field.

This takes that a step further, and adds a class-level setting to
specify whether strings should be frozen by default or not. The default
value of this setting is `false` (strings are mutable), and I do not
plan on changing that.

While I think that immutable strings by default would be a reasonable
default for new applications, I do not think that we would be able to
document the value of this setting in a place that people will be
looking when they can't figure out why some string is frozen.
Realistically, the number of applications where this setting is relevant
is fairly small, so I don't think it would make sense to ever enable it
by default.
2020-06-11 15:07:06 +09:00
Ryuta Kamizono
27a1ca2bfe PERF: 15% faster attribute access
Delegating to just one line method is to not be worth it.
Avoiding the delegation makes `read_attribute` about 15% faster.

```ruby
ActiveRecord::Schema.define do
  create_table :users, force: true do |t|
    t.string :name
  end
end

class User < ActiveRecord::Base
  def fast_read_attribute(attr_name, &block)
    name = attr_name.to_s
    name = self.class.attribute_aliases[name] || name

    name = @primary_key if name == "id" && @primary_key
    @attributes.fetch_value(name, &block)
  end
end

user = User.create!(name: "user name")

Benchmark.ips do |x|
  x.report("read_attribute('id')") { user.read_attribute('id') }
  x.report("read_attribute('name')") { user.read_attribute('name') }
  x.report("fast_read_attribute('id')") { user.fast_read_attribute('id') }
  x.report("fast_read_attribute('name')") { user.fast_read_attribute('name') }
end
```

```
Warming up --------------------------------------
read_attribute('id')   165.744k i/100ms
read_attribute('name')
                       162.229k i/100ms
fast_read_attribute('id')
                       192.543k i/100ms
fast_read_attribute('name')
                       191.209k i/100ms
Calculating -------------------------------------
read_attribute('id')      1.648M (± 1.7%) i/s -      8.287M in   5.030170s
read_attribute('name')
                          1.636M (± 3.9%) i/s -      8.274M in   5.065356s
fast_read_attribute('id')
                          1.918M (± 1.8%) i/s -      9.627M in   5.021271s
fast_read_attribute('name')
                          1.928M (± 0.9%) i/s -      9.752M in   5.058820s
```
2020-06-05 09:12:21 +09:00
Ryuta Kamizono
7834363bbf Avoid redundant to_s in internal attribute API
Redundant `to_s` has a few overhead. Especially private methods are not
intend to be passed user input directly so it should be passed always
string.

Removing redundant `to_s` makes attribute methods about 10% faster.

```ruby
ActiveRecord::Schema.define do
  create_table :users, force: true do |t|
  end
end

class User < ActiveRecord::Base
  def fast_read_attribute(attr_name, &block)
    @attributes.fetch_value(attr_name, &block)
  end
end

user = User.create!

Benchmark.ips do |x|
  x.report("user._read_attribute('id')") { user._read_attribute("id") }
  x.report("user.fast_read_attribute('id')") { user.fast_read_attribute("id") }
end
```

```
Warming up --------------------------------------
user._read_attribute('id')
                       272.151k i/100ms
user.fast_read_attribute('id')
                       283.518k i/100ms
Calculating -------------------------------------
user._read_attribute('id')
                          2.699M (± 1.3%) i/s -     13.608M in   5.042846s
user.fast_read_attribute('id')
                          2.988M (± 1.2%) i/s -     15.026M in   5.029056s
```
2020-06-04 09:02:53 +09:00
Ryuta Kamizono
6f1bf2a3dd Promote clear_attribute_change as attribute methods
For now, `increment` with aliased attribute does work, but `increment!`
with aliased attribute does not work, due to `clear_attribute_change` is
not aware of attribute aliases.

We sometimes partially updates specific attributes in dirties, at that
time it relies on `clear_attribute_change` to clear partially updated
attribute dirties. If `clear_attribute_change` is not attribute method
unlike others, we need to resolve attribute aliases manually only for
`clear_attribute_change`, it is a little inconvinient for me.

From another point of view, we have `restore_attributes`,
`restore_attribute!`, `clear_attribute_changes`, and
`clear_attribute_change`. Despite almost similar features
`restore_attribute!` is an attribute method but `clear_attribute_change`
is not.

Given the above, I'd like to promote `clear_attribute_change` as
attribute methods to fix issues caused by the inconsisteny.
2020-06-04 04:17:17 +09:00
Aaron Patterson
35fe9bc0aa
Merge pull request #39477 from p8/improve-inspect
Make custom inspect methods more consistent
2020-06-03 10:43:35 -07:00
Ryuta Kamizono
4dbbba4a97 Don't call ruby2_keywords for user supplied block 2020-06-04 02:19:30 +09:00
Petrik
74cb9a6f38 Make inspect look more like regular Object#inspect
Move the # outside the < > just like regular Object#inspect
2020-05-29 21:53:35 +02:00
Ryuta Kamizono
c65864cdca Prefer no allocation start/end_with? over String#[] == 2020-05-29 10:20:13 +09:00
Eugene Kenny
7e14c16cc0 Optimise serializable_hash when options are empty
This reverts 8538dfdc084555673d18cfc3479ebef09f325c9c, which broke the
activemodel-serializers-xml gem.

We can still get most of the benefit by applying the optimisation from
7b3919774252f99e55e6b6ec370aafc42adca2b2 to empty hashes as well as nil.
This has the additional benefit of retaining the optimisation when the
user passes an empty options hash.
2020-05-22 00:04:31 +01:00
Eugene Kenny
8538dfdc08 Reduce allocations in to_json's include option
Since 7b3919774252f99e55e6b6ec370aafc42adca2b2, `serializable_hash`
allocates fewer objects when options is nil rather than empty.
2020-05-19 22:00:00 +01:00
Ryuta Kamizono
fba016c7a3 Fix update with dirty locking column to not match latest object accidentally
Related #32163.

We should not identify an object by dirty value. If do so, accidentally
matches latest object even though it is a stale object.
2020-05-18 09:46:09 +09:00
fatkodima
7b39197742 Avoid allocating extra hash and arrays when as_json is called without options
Co-authored-by: Eugene Kenny <elkenny@gmail.com>
2020-05-17 22:54:55 +03:00
eileencodes
6833bf4d10
Remove implementation of unchecked_serialize
Since we're checking `serializable?` in the new `HomogeneousIn`
`serialize` will no longer raise an exception. We implemented
`unchecked_serialize` to avoid raising in these cases, but with some of
our refactoring we no longer need it.

I discovered this while trying to fix a query in our application that
was not properly serializing binary columns. I discovered that in at
least 2 of our active model types we were not calling the correct
serialization. Since `serialize` wasn't aliased to `unchecked_serialize`
in `ActiveModel::Type::Binary` and `ActiveModel::Type::Boolean` (I
didn't check others but pretty sure all the AM Types are broken) the SQL
was being treated as a `String` and not the correct type.

This caused Rails to incorrectly query by string values. This is
problematic for columns storing binary data like our emoji columns at
GitHub. The test added here is an example of how the Binary type was
broken previously. The SQL should be using the hex values, not the
string value of "🥦" or other emoji.

We still have the problem `unchecked_serialize` was supposed to fix -
that `serialize` shouldn't validate data, just convert it. We'll be
fixing that in a followup PR so for now we should use `serialize` so we
know all the values are going through the right serialization for their
SQL.
2020-05-12 13:37:22 -04:00
Akira Matsuda
1edf103b52 undef_method can take varargs 2020-05-08 23:58:01 +09:00
Ryuta Kamizono
12c2f44316 Remove redundant squish for single line message 2020-05-06 15:08:06 +09:00
eileencodes
70ddb8a704
Merge branch 'fix-array-builder-wheres' 2020-05-05 12:57:14 -04:00
Ryuta Kamizono
199e4e96d6 Attributes can be dup-ed 2020-05-06 00:40:48 +09:00
Ryuta Kamizono
776f2abc5a Type::Value#initialize doesn't take positional arguments
Follow up to 257d8f97921a234425817404eab662f590d2d2b3.
2020-05-05 20:40:37 +09:00
Ryuta Kamizono
f93b04afab Deprecate marshalling load from legacy attributes format
Since #31827, marshalling attributes hash format is changed to improve
performance because materializing lazy attribute hash is too expensive.

In that time, we had kept an ability to load from legacy attributes
format, since that performance improvement is backported to 5-1-stable
and 5-0-stable.

Now all supported versions will dump attributes as new format, the
backward compatibity should no longer be needed.
2020-05-02 15:49:09 +09:00
eileencodes
272f2f081a
Use range.member? over range.cover?
Rails has a monkey patch on `range.cover?` that is slower than Ruby's
`range.cover?`. We don't need Range support in this case because the SQL
creates a `BETWEEN` not an `IN` statement.
2020-05-01 15:12:05 -04:00
eileencodes
72fd0bae59
Perf: Improve performance of where when using an array of values
A coworker at GitHub found a few months back that if we used
`santitize_sql` over `where` when we knew the values going into `where`
it was a lot faster than `where`.

This PR adds a new Arel node type called `HomogenousIn` that will be
used when Rails knows the values are all homogenous and can therefore
pick a faster codepath. This new codepath skips some of the required
processing by `where` to make `wheres` with homogenous arrays faster
without requiring the application author to know when to use which query
type.

Using our benchmark code:

```ruby
ids = (1..1000).each.map do |n|
  Post.create!.id
end

Benchmark.ips do |x|
  x.report("where with ids") do
    Post.where(id: ids).to_a
  end

  x.report("where with sanitize") do
    Post.where(ActiveRecord::Base.sanitize_sql(["id IN (?)", ids])).to_a
  end

  x.compare!
end
```

Before this PR comparing where with a list of IDs to santitize sql:

```
Warming up --------------------------------------
      where with ids    11.000  i/100ms
 where with sanitize    17.000  i/100ms

Calculating -------------------------------------
      where with ids    115.733  (± 4.3%) i/s -    583.000  in   5.045828s
 where with sanitize    174.231  (± 4.0%) i/s -    884.000  in   5.081495s

Comparison:
 where with sanitize:      174.2 i/s
      where with ids:      115.7 i/s - 1.51x  slower
```

After this PR comparing where with a list of IDs to santitize sql:

```
Warming up --------------------------------------
      where with ids    16.000  i/100ms
 where with sanitize    19.000  i/100ms

Calculating -------------------------------------
      where with ids    158.293  (± 6.3%) i/s -    800.000  in   5.072208s
 where with sanitize    169.141  (± 3.5%) i/s -    855.000  in   5.060878s

Comparison:
 where with sanitize:      169.1 i/s
      where with ids:      158.3 i/s - same-ish: difference falls within error
```

Co-authored-by: Aaron Patterson <aaron.patterson@gmail.com>
2020-05-01 15:12:05 -04:00
Jean Boussier
d12418a776 Also batch attribute readers and writers 2020-05-01 12:21:16 +02:00
Jean Boussier
9e8bbf6fc9 Batch attribute methods definition in a single module_eval 2020-04-30 18:09:48 +02:00
Adam Hess
096f2d1f83 Reference updated errors attribute names method in deprecation warning 2020-04-22 09:08:42 -07:00
nimish
b3c308dd20 Reject signed hexadecimal numbers while validating numericality 2020-04-22 12:15:57 +00:00
Ryuta Kamizono
6f2126c760 Fixup CHANGELOGs [ci skip] 2020-04-15 21:23:24 +09:00
Rafael Mendonça França
a4deb63798
No need to deprecate Errors#first
This deprecation is useless since the result is still an Error object
and there is no way to fix the code to remove the deprecation.

Let's just accept as a breaking change.
2020-04-13 19:07:59 -04:00
Rafael França
6f6f4e40ab
Merge pull request #36125 from lulalala/doc-for-model-errors
Document update for ActiveModel#errors
2020-04-13 14:11:55 -04:00