diff options
-rw-r--r-- | activejob/lib/active_job/exceptions.rb | 17 | ||||
-rw-r--r-- | activejob/test/cases/exceptions_test.rb | 25 | ||||
-rw-r--r-- | activemodel/lib/active_model/attributes.rb | 44 | ||||
-rw-r--r-- | activemodel/test/cases/attributes_test.rb | 14 | ||||
-rw-r--r-- | activerecord/CHANGELOG.md | 41 | ||||
-rw-r--r-- | activerecord/lib/active_record/relation/query_methods.rb | 21 | ||||
-rw-r--r-- | activerecord/lib/active_record/relation/where_clause.rb | 14 | ||||
-rw-r--r-- | activerecord/test/cases/relation/where_clause_test.rb | 2 | ||||
-rw-r--r-- | activerecord/test/cases/relation/where_test.rb | 55 | ||||
-rw-r--r-- | guides/source/6_0_release_notes.md | 1 | ||||
-rw-r--r-- | guides/source/active_record_callbacks.md | 8 | ||||
-rw-r--r-- | guides/source/api_app.md | 9 |
12 files changed, 223 insertions, 28 deletions
diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb index 35c1476368..8e83246303 100644 --- a/activejob/lib/active_job/exceptions.rb +++ b/activejob/lib/active_job/exceptions.rb @@ -49,12 +49,10 @@ module ActiveJob # end def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil) rescue_from(*exceptions) do |error| - # Guard against jobs that were persisted before we started having individual executions counters per retry_on - self.exception_executions ||= {} - self.exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1 + executions = executions_for(exceptions) - if exception_executions[exceptions.to_s] < attempts - retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: exception_executions[exceptions.to_s]), queue: queue, priority: priority, error: error + if executions < attempts + retry_job wait: determine_delay(seconds_or_duration_or_algorithm: wait, executions: executions), queue: queue, priority: priority, error: error else if block_given? instrument :retry_stopped, error: error do @@ -146,5 +144,14 @@ module ActiveJob ActiveSupport::Notifications.instrument("#{name}.active_job", payload, &block) end + + def executions_for(exceptions) + if exception_executions + exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1 + else + # Guard against jobs that were persisted before we started having individual executions counters per retry_on + executions + end + end end end diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb index 840f4d40b5..1f07b7b294 100644 --- a/activejob/test/cases/exceptions_test.rb +++ b/activejob/test/cases/exceptions_test.rb @@ -179,6 +179,31 @@ class ExceptionsTest < ActiveSupport::TestCase assert_equal ["Raised ActiveJob::DeserializationError for the 5 time"], JobBuffer.values end + test "running a job enqueued by AJ 5.2" do + job = RetryJob.new("DefaultsError", 6) + job.exception_executions = nil # This is how jobs from Rails 5.2 will look + + assert_raises DefaultsError do + job.enqueue + end + + assert_equal 5, JobBuffer.values.count + end + + test "running a job enqueued and attempted under AJ 5.2" do + job = RetryJob.new("DefaultsError", 6) + + # Fake 4 previous executions under AJ 5.2 + job.exception_executions = nil + job.executions = 4 + + assert_raises DefaultsError do + job.enqueue + end + + assert_equal ["Raised DefaultsError for the 5th time"], JobBuffer.values + end + private def adapter_skips_scheduling?(queue_adapter) [ diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index b7b2f35bcc..6048ff0113 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -26,6 +26,21 @@ module ActiveModel define_attribute_method(name) end + # Returns an array of attribute names as strings + # + # class Person + # include ActiveModel::Attributes + # + # attribute :name, :string + # attribute :age, :integer + # end + # + # Person.attribute_names + # # => ["name", "age"] + def attribute_names + attribute_types.keys + end + private def define_method_attribute=(name) @@ -65,10 +80,39 @@ module ActiveModel super end + # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. + # + # class Person + # include ActiveModel::Model + # include ActiveModel::Attributes + # + # attribute :name, :string + # attribute :age, :integer + # end + # + # person = Person.new(name: 'Francesco', age: 22) + # person.attributes + # # => {"name"=>"Francesco", "age"=>22} def attributes @attributes.to_hash end + # Returns an array of attribute names as strings + # + # class Person + # include ActiveModel::Attributes + # + # attribute :name, :string + # attribute :age, :integer + # end + # + # person = Person.new + # person.attribute_names + # # => ["name", "age"] + def attribute_names + @attributes.keys + end + private def write_attribute(attr_name, value) diff --git a/activemodel/test/cases/attributes_test.rb b/activemodel/test/cases/attributes_test.rb index 5483fb101d..af0ddcb92f 100644 --- a/activemodel/test/cases/attributes_test.rb +++ b/activemodel/test/cases/attributes_test.rb @@ -67,6 +67,20 @@ module ActiveModel assert_equal expected_attributes, data.attributes end + test "reading attribute names" do + names = [ + "integer_field", + "string_field", + "decimal_field", + "string_with_default", + "date_field", + "boolean_field" + ] + + assert_equal names, ModelForAttributesTest.attribute_names + assert_equal names, ModelForAttributesTest.new.attribute_names + end + test "nonexistent attribute" do assert_raise ActiveModel::UnknownAttributeError do ModelForAttributesTest.new(nonexistent: "nonexistent") diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 485547f036..adc7e754a4 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,44 @@ +* Deprecate `where.not` working as NOR and will be changed to NAND in Rails 6.1. + + ```ruby + all = [treasures(:diamond), treasures(:sapphire), cars(:honda), treasures(:sapphire)] + assert_equal all, PriceEstimate.all.map(&:estimate_of) + ``` + + In Rails 6.0: + + ```ruby + sapphire = treasures(:sapphire) + + nor = all.reject { |e| + e.estimate_of_type == sapphire.class.polymorphic_name + }.reject { |e| + e.estimate_of_id == sapphire.id + } + assert_equal [cars(:honda)], nor + + without_sapphire = PriceEstimate.where.not( + estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id + ) + assert_equal nor, without_sapphire.map(&:estimate_of) + ``` + + In Rails 6.1: + + ```ruby + sapphire = treasures(:sapphire) + + nand = all - [sapphire] + assert_equal [treasures(:diamond), cars(:honda)], nand + + without_sapphire = PriceEstimate.where.not( + estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id + ) + assert_equal nand, without_sapphire.map(&:estimate_of) + ``` + + *Ryuta Kamizono* + * Fix dirty tracking after rollback. Fixes #15018, #30167, #33868. diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 5d3cea6741..f30428d0a5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -41,18 +41,31 @@ module ActiveRecord # # User.where.not(name: %w(Ko1 Nobu)) # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu') - # - # User.where.not(name: "Jon", role: "admin") - # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' def not(opts, *rest) opts = sanitize_forbidden_attributes(opts) where_clause = @scope.send(:where_clause_factory).build(opts, rest) @scope.references!(PredicateBuilder.references(opts)) if Hash === opts - @scope.where_clause += where_clause.invert + + if not_behaves_as_nor?(opts) + ActiveSupport::Deprecation.warn(<<~MSG.squish) + NOT conditions will no longer behave as NOR in Rails 6.1. + To continue using NOR conditions, NOT each conditions manually + (`#{ opts.keys.map { |key| ".where.not(#{key.inspect} => ...)" }.join }`). + MSG + @scope.where_clause += where_clause.invert(:nor) + else + @scope.where_clause += where_clause.invert + end + @scope end + + private + def not_behaves_as_nor?(opts) + opts.is_a?(Hash) && opts.size > 1 + end end FROZEN_EMPTY_ARRAY = [].freeze diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 47728aac30..b91b135867 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -70,7 +70,15 @@ module ActiveRecord predicates == other.predicates end - def invert + def invert(as = :nand) + if predicates.size == 1 + inverted_predicates = [ invert_predicate(predicates.first) ] + elsif as == :nor + inverted_predicates = predicates.map { |node| invert_predicate(node) } + else + inverted_predicates = [ Arel::Nodes::Not.new(ast) ] + end + WhereClause.new(inverted_predicates) end @@ -115,10 +123,6 @@ module ActiveRecord node.respond_to?(:operator) && node.operator == :== end - def inverted_predicates - predicates.map { |node| invert_predicate(node) } - end - def invert_predicate(node) case node when NilClass diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index 0b06cec40b..b26a1a1d80 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -106,7 +106,7 @@ class ActiveRecord::Relation Arel::Nodes::Not.new(random_object) ]) - assert_equal expected, original.invert + assert_equal expected, original.invert(:nor) end test "except removes binary predicates referencing a given column" do diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index b045184d7d..2ecccfe1f3 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -115,13 +115,58 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end - def test_polymorphic_shallow_where_not - treasure = treasures(:sapphire) + def test_where_not_polymorphic_association + sapphire = treasures(:sapphire) - expected = [price_estimates(:diamond), price_estimates(:honda)] - actual = PriceEstimate.where.not(estimate_of: treasure) + all = [treasures(:diamond), sapphire, cars(:honda), sapphire] + assert_equal all, PriceEstimate.all.sort_by(&:id).map(&:estimate_of) - assert_equal expected.sort_by(&:id), actual.sort_by(&:id) + actual = PriceEstimate.where.not(estimate_of: sapphire) + only = PriceEstimate.where(estimate_of: sapphire) + + expected = all - [sapphire] + assert_equal expected, actual.sort_by(&:id).map(&:estimate_of) + assert_equal all - expected, only.sort_by(&:id).map(&:estimate_of) + end + + def test_where_not_polymorphic_id_and_type_as_nand + sapphire = treasures(:sapphire) + + all = [treasures(:diamond), sapphire, cars(:honda), sapphire] + assert_equal all, PriceEstimate.all.sort_by(&:id).map(&:estimate_of) + + actual = PriceEstimate.where.yield_self do |where_chain| + where_chain.stub(:not_behaves_as_nor?, false) do + where_chain.not(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id) + end + end + only = PriceEstimate.where(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id) + + expected = all - [sapphire] + assert_equal expected, actual.sort_by(&:id).map(&:estimate_of) + assert_equal all - expected, only.sort_by(&:id).map(&:estimate_of) + end + + def test_where_not_polymorphic_id_and_type_as_nor_is_deprecated + sapphire = treasures(:sapphire) + + all = [treasures(:diamond), sapphire, cars(:honda), sapphire] + assert_equal all, PriceEstimate.all.sort_by(&:id).map(&:estimate_of) + + message = <<~MSG.squish + NOT conditions will no longer behave as NOR in Rails 6.1. + To continue using NOR conditions, NOT each conditions manually + (`.where.not(:estimate_of_type => ...).where.not(:estimate_of_id => ...)`). + MSG + actual = assert_deprecated(message) do + PriceEstimate.where.not(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id) + end + only = PriceEstimate.where(estimate_of_type: sapphire.class.polymorphic_name, estimate_of_id: sapphire.id) + + expected = all - [sapphire] + # NOT (estimate_of_type = 'Treasure' OR estimate_of_id = sapphire.id) matches only `cars(:honda)` unfortunately. + assert_not_equal expected, actual.sort_by(&:id).map(&:estimate_of) + assert_equal all - expected, only.sort_by(&:id).map(&:estimate_of) end def test_polymorphic_nested_array_where diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md index 8c9f55fc85..180cfa371c 100644 --- a/guides/source/6_0_release_notes.md +++ b/guides/source/6_0_release_notes.md @@ -454,6 +454,7 @@ Please refer to the [Changelog][active-storage] for detailed changes. ### Notable changes * Updating an attached model via `update` or `update!` ala `@user.update!(images: [ … ])` now replaces the existing images instead of merely adding to them. + ([Pull Request](https://github.com/rails/rails/pull/33303)) Active Model ------------ diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 8f54e78224..617f81d37b 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -495,11 +495,3 @@ end >> @user.save => User was saved to database ``` - -To register callbacks for both create and destroy actions, use `after_commit` instead. - -```ruby -class User < ApplicationRecord - after_commit :log_user_saved_to_db, on: [:create, :destroy] -end -``` diff --git a/guides/source/api_app.md b/guides/source/api_app.md index b8b6cb7874..181d39e7e0 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -420,6 +420,15 @@ Some common modules you might want to add: - `ActionController::MimeResponds`: Support for `respond_to`. - `ActionController::Cookies`: Support for `cookies`, which includes support for signed and encrypted cookies. This requires the cookies middleware. +- `ActionController::Caching`: Support view caching for the API controller. Please notice that + you will need to manually specify cache store inside the controller like: + ```ruby + class ApplicationController < ActionController::API + include ::ActionController::Caching + self.cache_store = :mem_cached_store + end + ``` + Rails does *not* pass this configuration automatically. The best place to add a module is in your `ApplicationController`, but you can also add modules to individual controllers. |