diff options
103 files changed, 1010 insertions, 597 deletions
diff --git a/.travis.yml b/.travis.yml index 59f4ed0249..3a774e1564 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ addons: bundler_args: --without test --jobs 3 --retry 3 before_install: - "rm ${BUNDLE_GEMFILE}.lock" - - "gem update --system 2.6.11" + - "gem update --system" - "gem update bundler" - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" @@ -98,17 +98,17 @@ matrix: - "GEM=ar:postgresql POSTGRES=9.2" addons: postgresql: "9.2" - - rvm: jruby-9.1.8.0 + - rvm: jruby-9.1.9.0 jdk: oraclejdk8 env: - "GEM=ap" - - rvm: jruby-9.1.8.0 + - rvm: jruby-9.1.9.0 jdk: oraclejdk8 env: - "GEM=am,amo,aj" allow_failures: - rvm: ruby-head - - rvm: jruby-9.1.8.0 + - rvm: jruby-9.1.9.0 - env: "GEM=ac:integration" fast_finish: true @@ -154,3 +154,7 @@ end gem "ibm_db" if ENV["IBM_DB"] gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem "wdm", ">= 0.1.0", platforms: [:mingw, :mswin, :x64_mingw, :mswin64] + +platforms :ruby_25 do + gem "mathn" +end diff --git a/Gemfile.lock b/Gemfile.lock index ad0ec7be0c..2b8cc2862c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -207,8 +207,9 @@ GEM ruby_dep (~> 1.2) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.4) + mail (2.6.5) mime-types (>= 1.16, < 4) + mathn (0.1.0) metaclass (0.0.4) method_source (0.8.2) mime-types (3.1) @@ -249,10 +250,10 @@ GEM simple_uuid que (0.12.0) racc (1.4.14) - rack (2.0.1) + rack (2.0.3) rack-cache (1.6.1) rack (>= 0.4) - rack-protection (1.5.3) + rack-protection (2.0.0) rack rack-test (0.6.3) rack (>= 1.0) @@ -266,7 +267,7 @@ GEM rb-fsevent (0.9.8) rdoc (5.1.0) redcarpet (3.2.3) - redis (3.3.2) + redis (3.3.3) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) resque (1.27.0) @@ -307,11 +308,11 @@ GEM sequel (4.42.1) serverengine (1.5.11) sigdump (~> 0.2.2) - sidekiq (4.2.9) + sidekiq (5.0.0) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (~> 3.2, >= 3.2.1) + redis (~> 3.3, >= 3.3.3) sigdump (0.2.4) simple_uuid (0.4.0) sinatra (1.0) @@ -396,6 +397,7 @@ DEPENDENCIES kindlerb (~> 1.2.0) libxml-ruby listen (>= 3.0.5, < 3.2) + mathn minitest (< 5.3.4) mocha (~> 0.14) mysql2 (>= 0.4.4) @@ -433,4 +435,4 @@ DEPENDENCIES websocket-client-simple! BUNDLED WITH - 1.14.6 + 1.15.0 diff --git a/actioncable/Rakefile b/actioncable/Rakefile index bda8c7b6c8..0ee1a859e5 100644 --- a/actioncable/Rakefile +++ b/actioncable/Rakefile @@ -65,7 +65,7 @@ namespace :assets do end print "[verify] #{dir} can be required as a module " - stdout, stderr, status = Open3.capture3("node", "--print", "window = {}; require('#{dir}');") + _, stderr, status = Open3.capture3("node", "--print", "window = {}; require('#{dir}');") if status.success? puts "[OK]" else diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb index d2856bc6ae..e689fbf21b 100644 --- a/actioncable/lib/action_cable/remote_connections.rb +++ b/actioncable/lib/action_cable/remote_connections.rb @@ -45,7 +45,7 @@ module ActionCable end # Returns all the identifiers that were applied to this connection. - def identifiers + redefine_method :identifiers do server.connection_identifiers end diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb index cff49c8894..5869eae7fd 100644 --- a/actionmailer/test/caching_test.rb +++ b/actionmailer/test/caching_test.rb @@ -21,10 +21,6 @@ class BaseCachingTest < ActiveSupport::TestCase @mailer.perform_caching = true @mailer.cache_store = @store end - - def test_fragment_cache_key - assert_equal "views/what a key", @mailer.fragment_cache_key("what a key") - end end class FragmentCachingTest < BaseCachingTest @@ -126,7 +122,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest assert_match expected_body, email.body.encoded assert_match expected_body, - @store.read("views/caching/#{template_digest("caching_mailer/fragment_cache")}") + @store.read("views/caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}/caching") end def test_fragment_caching_in_partials @@ -135,7 +131,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest assert_match(expected_body, email.body.encoded) assert_match(expected_body, - @store.read("views/caching/#{template_digest("caching_mailer/_partial")}")) + @store.read("views/caching_mailer/_partial:#{template_digest("caching_mailer/_partial")}/caching")) end def test_skip_fragment_cache_digesting @@ -185,7 +181,7 @@ class FunctionalFragmentCachingTest < BaseCachingTest end assert_equal "caching_mailer", payload[:mailer] - assert_equal "views/caching/#{template_digest("caching_mailer/fragment_cache")}", payload[:key] + assert_equal [ :views, "caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache")}", :caching ], payload[:key] ensure @mailer.enable_fragment_cache_logging = true end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index cc908dcc43..86ea9a7ce6 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,4 +1,17 @@ -* Add `action_controller_api` and `action_controller_base` load hooks to be called in `ActiveSupport.on_load` +* Change the cache key format for fragments to make it easier to debug key churn. The new format is: + + views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123 + ^template path ^template tree digest ^class ^id + + *DHH* + +* Add support for recyclable cache keys with fragment caching. This uses the new versioned entries in the + `ActiveSupport::Cache` stores and relies on the fact that Active Record has split `#cache_key` and `#cache_version` + to support it. + + *DHH* + +* Add `action_controller_api` and `action_controller_base` load hooks to be called in `ActiveSupport.on_load` `ActionController::Base` and `ActionController::API` have differing implementations. This means that the one umbrella hook `action_controller` is not able to address certain situations where a method diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index e7cb6347a2..dc79820a82 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -14,8 +14,16 @@ module AbstractController # expected to provide their own +render+ method, since rendering means # different things depending on the context. class Base + ## + # Returns the body of the HTTP response sent by the controller. attr_internal :response_body + + ## + # Returns the name of the action this controller is processing. attr_internal :action_name + + ## + # Returns the formats that can be processed by the controller. attr_internal :formats include ActiveSupport::Configurable diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index c85b4adba1..14e4a82523 100644 --- a/actionpack/lib/abstract_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -25,7 +25,10 @@ module AbstractController self.fragment_cache_keys = [] - helper_method :fragment_cache_key if respond_to?(:helper_method) + if respond_to?(:helper_method) + helper_method :fragment_cache_key + helper_method :combined_fragment_cache_key + end end module ClassMethods @@ -62,17 +65,36 @@ module AbstractController # with the specified +key+ value. The key is expanded using # ActiveSupport::Cache.expand_cache_key. def fragment_cache_key(key) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Calling fragment_cache_key directly is deprecated and will be removed in Rails 6.0. + All fragment accessors now use the combined_fragment_cache_key method that retains the key as an array, + such that the caching stores can interrogate the parts for cache versions used in + recyclable cache keys. + MSG + head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } tail = key.is_a?(Hash) ? url_for(key).split("://").last : key ActiveSupport::Cache.expand_cache_key([*head, *tail], :views) end + # Given a key (as described in +expire_fragment+), returns + # a key array suitable for use in reading, writing, or expiring a + # cached fragment. All keys begin with <tt>:views</tt>, + # followed by ENV["RAILS_CACHE_ID"] or ENV["RAILS_APP_VERSION"] if set, + # followed by any controller-wide key prefix values, ending + # with the specified +key+ value. + def combined_fragment_cache_key(key) + head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } + tail = key.is_a?(Hash) ? url_for(key).split("://").last : key + [ :views, (ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]), *head, *tail ].compact + end + # Writes +content+ to the location signified by # +key+ (see +expire_fragment+ for acceptable formats). def write_fragment(key, content, options = nil) return content unless cache_configured? - key = fragment_cache_key(key) + key = combined_fragment_cache_key(key) instrument_fragment_cache :write_fragment, key do content = content.to_str cache_store.write(key, content, options) @@ -85,7 +107,7 @@ module AbstractController def read_fragment(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) + key = combined_fragment_cache_key(key) instrument_fragment_cache :read_fragment, key do result = cache_store.read(key, options) result.respond_to?(:html_safe) ? result.html_safe : result @@ -96,7 +118,7 @@ module AbstractController # +key+ exists (see +expire_fragment+ for acceptable formats). def fragment_exist?(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) + key = combined_fragment_cache_key(key) instrument_fragment_cache :exist_fragment?, key do cache_store.exist?(key, options) @@ -123,7 +145,7 @@ module AbstractController # method (or <tt>delete_matched</tt>, for Regexp keys). def expire_fragment(key, options = nil) return unless cache_configured? - key = fragment_cache_key(key) unless key.is_a?(Regexp) + key = combined_fragment_cache_key(key) unless key.is_a?(Regexp) instrument_fragment_cache :expire_fragment, key do if key.is_a?(Regexp) @@ -135,8 +157,7 @@ module AbstractController end def instrument_fragment_cache(name, key) # :nodoc: - payload = instrument_payload(key) - ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", payload) { yield } + ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield } end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index ce4ecf17cc..ba7dec6083 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -1,4 +1,24 @@ module AbstractController + # = Abstract Controller Callbacks + # + # Abstract Controller provides hooks during the life cycle of a controller action. + # Callbacks allow you to trigger logic during this cycle. Available callbacks are: + # + # * <tt>after_action</tt> + # * <tt>append_after_action</tt> + # * <tt>append_around_action</tt> + # * <tt>append_before_action</tt> + # * <tt>around_action</tt> + # * <tt>before_action</tt> + # * <tt>prepend_after_action</tt> + # * <tt>prepend_around_action</tt> + # * <tt>prepend_before_action</tt> + # * <tt>skip_after_action</tt> + # * <tt>skip_around_action</tt> + # * <tt>skip_before_action</tt> + # + # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # module Callbacks extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index d29a5fe68f..d00fcbcd13 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -60,9 +60,9 @@ module ActionController class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{method}(event) return unless logger.info? && ActionController::Base.enable_fragment_cache_logging - key_or_path = event.payload[:key] || event.payload[:path] + key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path]) human_name = #{method.to_s.humanize.inspect} - info("\#{human_name} \#{key_or_path} (\#{event.duration.round(1)}ms)") + info("\#{human_name} \#{key} (\#{event.duration.round(1)}ms)") end METHOD end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 7864f9decd..20330b5091 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -666,8 +666,8 @@ module ActionController # to key. If the key is not found, returns the default value. If the # optional code block is given and the key is not found, pass in the key # and return the result of block. - def delete(key) - convert_value_to_parameters(@parameters.delete(key)) + def delete(key, &block) + convert_value_to_parameters(@parameters.delete(key, &block)) end # Returns a new instance of <tt>ActionController::Parameters</tt> with only diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 61ba052e45..225272d66e 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -27,14 +27,18 @@ module ActionDispatch @tempfile = hash[:tempfile] raise(ArgumentError, ":tempfile is required") unless @tempfile - @original_filename = hash[:filename] - if @original_filename + if hash[:filename] + @original_filename = hash[:filename].dup + begin @original_filename.encode!(Encoding::UTF_8) rescue EncodingError @original_filename.force_encoding(Encoding::UTF_8) end + else + @original_filename = nil end + @content_type = hash[:type] @headers = hash[:head] end diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb index d692f6415c..62f052ced6 100644 --- a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb +++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb @@ -18,14 +18,6 @@ module ActionDispatch @tt = transition_table end - def simulate(string) - ms = memos(string) { return } - MatchData.new(ms) - end - - alias :=~ :simulate - alias :match :simulate - def memos(string) input = StringScanner.new(string) state = [0] diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index e624b4c80d..6d400f3364 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -13,11 +13,13 @@ module ActionDispatch # normalize_path("") # => "/" # normalize_path("/%ab") # => "/%AB" def self.normalize_path(path) + encoding = path.encoding path = "/#{path}" path.squeeze!("/".freeze) path.sub!(%r{/+\Z}, "".freeze) path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase } path = "/" if path == "".freeze + path.force_encoding(encoding) path end diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index fa8d9dc09a..dac5861c4b 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -26,10 +26,6 @@ class FragmentCachingMetalTest < ActionController::TestCase @controller.request = @request @controller.response = @response end - - def test_fragment_cache_key - assert_equal "views/what a key", @controller.fragment_cache_key("what a key") - end end class CachingController < ActionController::Base @@ -43,6 +39,8 @@ class FragmentCachingTestController < CachingController end class FragmentCachingTest < ActionController::TestCase + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + def setup super @store = ActiveSupport::Cache::MemoryStore.new @@ -53,12 +51,25 @@ class FragmentCachingTest < ActionController::TestCase @controller.params = @params @controller.request = @request @controller.response = @response + + @m1v1 = ModelWithKeyAndVersion.new("model/1", "1") + @m1v2 = ModelWithKeyAndVersion.new("model/1", "2") + @m2v1 = ModelWithKeyAndVersion.new("model/2", "1") + @m2v2 = ModelWithKeyAndVersion.new("model/2", "2") end def test_fragment_cache_key - assert_equal "views/what a key", @controller.fragment_cache_key("what a key") - assert_equal "views/test.host/fragment_caching_test/some_action", - @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action") + assert_deprecated do + assert_equal "views/what a key", @controller.fragment_cache_key("what a key") + assert_equal "views/test.host/fragment_caching_test/some_action", + @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action") + end + end + + def test_combined_fragment_cache_key + assert_equal [ :views, "what a key" ], @controller.combined_fragment_cache_key("what a key") + assert_equal [ :views, "test.host/fragment_caching_test/some_action" ], + @controller.combined_fragment_cache_key(controller: "fragment_caching_test", action: "some_action") end def test_read_fragment_with_caching_enabled @@ -72,6 +83,12 @@ class FragmentCachingTest < ActionController::TestCase assert_nil @controller.read_fragment("name") end + def test_read_fragment_with_versioned_model + @controller.write_fragment([ "stuff", @m1v1 ], "hello") + assert_equal "hello", @controller.read_fragment([ "stuff", @m1v1 ]) + assert_nil @controller.read_fragment([ "stuff", @m1v2 ]) + end + def test_fragment_exist_with_caching_enabled @store.write("views/name", "value") assert @controller.fragment_exist?("name") @@ -198,7 +215,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "This bit's fragment cached", - @store.read("views/test.host/functional_caching/fragment_cached/#{template_digest("functional_caching/fragment_cached")}") + @store.read("views/functional_caching/fragment_cached:#{template_digest("functional_caching/fragment_cached")}/fragment") end def test_fragment_caching_in_partials @@ -207,7 +224,7 @@ CACHED assert_match(/Old fragment caching in a partial/, @response.body) assert_match("Old fragment caching in a partial", - @store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial")}")) + @store.read("views/functional_caching/_partial:#{template_digest("functional_caching/_partial")}/test.host/functional_caching/html_fragment_cached_with_partial")) end def test_skipping_fragment_cache_digesting @@ -237,7 +254,7 @@ CACHED assert_match(/Some inline content/, @response.body) assert_match(/Some cached content/, @response.body) assert_match("Some cached content", - @store.read("views/test.host/functional_caching/inline_fragment_cached/#{template_digest("functional_caching/inline_fragment_cached")}")) + @store.read("views/functional_caching/inline_fragment_cached:#{template_digest("functional_caching/inline_fragment_cached")}/test.host/functional_caching/inline_fragment_cached")) end def test_fragment_cache_instrumentation @@ -264,7 +281,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "<p>ERB</p>", - @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}") + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") end def test_xml_formatted_fragment_caching @@ -275,7 +292,7 @@ CACHED assert_equal expected_body, @response.body assert_equal " <p>Builder</p>\n", - @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}") + @store.read("views/functional_caching/formatted_fragment_cached:#{template_digest("functional_caching/formatted_fragment_cached")}/fragment") end def test_fragment_caching_with_variant @@ -286,7 +303,7 @@ CACHED assert_equal expected_body, @response.body assert_equal "<p>PHONE</p>", - @store.read("views/test.host/functional_caching/formatted_fragment_cached_with_variant/#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}") + @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment") end private @@ -412,7 +429,7 @@ class CollectionCacheTest < ActionController::TestCase def test_collection_fetches_cached_views get :index assert_equal 1, @controller.partial_rendered_times - assert_customer_cached "david/1", "david, 1" + assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/david/1") get :index assert_equal 1, @controller.partial_rendered_times @@ -444,14 +461,8 @@ class CollectionCacheTest < ActionController::TestCase def test_caching_with_callable_cache_key get :index_with_callable_cache_key - assert_customer_cached "cached_david", "david, 1" + assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/cached_david") end - - private - def assert_customer_cached(key, content) - assert_match content, - ActionView::PartialRenderer.collection_cache.read("views/#{key}/7c228ab609f0baf0b1f2367469210937") - end end class FragmentCacheKeyTestController < CachingController @@ -470,11 +481,21 @@ class FragmentCacheKeyTest < ActionController::TestCase @controller.cache_store = @store end - def test_fragment_cache_key + def test_combined_fragment_cache_key @controller.account_id = "123" - assert_equal "views/v1/123/what a key", @controller.fragment_cache_key("what a key") + assert_equal [ :views, "v1", "123", "what a key" ], @controller.combined_fragment_cache_key("what a key") @controller.account_id = nil - assert_equal "views/v1//what a key", @controller.fragment_cache_key("what a key") + assert_equal [ :views, "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + end + + def test_combined_fragment_cache_key_with_envs + ENV["RAILS_APP_VERSION"] = "55" + assert_equal [ :views, "55", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + + ENV["RAILS_CACHE_ID"] = "66" + assert_equal [ :views, "66", "v1", "what a key" ], @controller.combined_fragment_cache_key("what a key") + ensure + ENV["RAILS_CACHE_ID"] = ENV["RAILS_APP_VERSION"] = nil end end diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb index e61bbdbe13..2c36f488c6 100644 --- a/actionpack/test/controller/parameters/mutators_test.rb +++ b/actionpack/test/controller/parameters/mutators_test.rb @@ -25,6 +25,27 @@ class ParametersMutatorsTest < ActiveSupport::TestCase assert_not @params.delete(:person).permitted? end + test "delete returns the value when the key is present" do + assert_equal "32", @params[:person].delete(:age) + end + + test "delete removes the entry when the key present" do + @params[:person].delete(:age) + assert_not @params[:person].key?(:age) + end + + test "delete returns nil when the key is not present" do + assert_equal nil, @params[:person].delete(:first_name) + end + + test "delete returns the value of the given block when the key is not present" do + assert_equal "David", @params[:person].delete(:first_name) { "David" } + end + + test "delete yields the key to the given block when the key is not present" do + assert_equal "first_name: David", @params[:person].delete(:first_name) { |k| "#{k}: David" } + end + test "delete_if retains permitted status" do @params.permit! assert @params.delete_if { |k| k == "person" }.permitted? diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index d64917e0d3..32cd78e492 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -4419,7 +4419,7 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest end end - test "invalid UTF-8 encoding returns a 400 Bad Request" do + test "invalid UTF-8 encoding is treated as ASCII 8BIT encode" do with_routing do |set| set.draw do get "/bar/:id", to: redirect("/foo/show/%{id}") @@ -4435,19 +4435,19 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest end get "/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :not_found get "/foo/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :not_found get "/foo/show/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :ok get "/bar/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :redirect get "/foobar/%E2%EF%BF%BD%A6" - assert_response :bad_request + assert_response :ok end end end diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 51680216e4..0074d2a314 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -13,6 +13,12 @@ module ActionDispatch assert_equal "foo", uf.original_filename end + def test_filename_is_different_object + file_str = "foo" + uf = Http::UploadedFile.new(filename: file_str, tempfile: Object.new) + assert_not_equal file_str.object_id , uf.original_filename.object_id + end + def test_filename_should_be_in_utf_8 uf = Http::UploadedFile.new(filename: "foo", tempfile: Object.new) assert_equal "UTF-8", uf.original_filename.encoding.to_s diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb index 9b88fa1f5a..dfcd423978 100644 --- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.html.erb @@ -1,3 +1,3 @@ <body> -<%= cache do %><p>ERB</p><% end %> +<%= cache("fragment") do %><p>ERB</p><% end %> </body> diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder index efdcc28e0f..6599579740 100644 --- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached.xml.builder @@ -1,5 +1,5 @@ xml.body do - cache do + cache("fragment") do xml.p "Builder" end end diff --git a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb index e523b74ae3..abf7017ce6 100644 --- a/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb +++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb @@ -1,3 +1,3 @@ <body> -<%= cache do %><p>PHONE</p><% end %> +<%= cache("fragment") do %><p>PHONE</p><% end %> </body> diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb index fa5e6bd318..1148d83ad7 100644 --- a/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb +++ b/actionpack/test/fixtures/functional_caching/fragment_cached.html.erb @@ -1,3 +1,3 @@ Hello -<%= cache do %>This bit's fragment cached<% end %> +<%= cache "fragment" do %>This bit's fragment cached<% end %> <%= 'Ciao' %> diff --git a/actionpack/test/journey/gtg/transition_table_test.rb b/actionpack/test/journey/gtg/transition_table_test.rb index c7315c0338..889640fdd7 100644 --- a/actionpack/test/journey/gtg/transition_table_test.rb +++ b/actionpack/test/journey/gtg/transition_table_test.rb @@ -35,25 +35,25 @@ module ActionDispatch def test_simulate_gt sim = simulator_for ["/foo", "/bar"] - assert_match sim, "/foo" + assert_match_route sim, "/foo" end def test_simulate_gt_regexp sim = simulator_for [":foo"] - assert_match sim, "foo" + assert_match_route sim, "foo" end def test_simulate_gt_regexp_mix sim = simulator_for ["/get", "/:method/foo"] - assert_match sim, "/get" - assert_match sim, "/get/foo" + assert_match_route sim, "/get" + assert_match_route sim, "/get/foo" end def test_simulate_optional sim = simulator_for ["/foo(/bar)"] - assert_match sim, "/foo" - assert_match sim, "/foo/bar" - assert_no_match sim, "/foo/" + assert_match_route sim, "/foo" + assert_match_route sim, "/foo/bar" + assert_no_match_route sim, "/foo/" end def test_match_data @@ -65,11 +65,11 @@ module ActionDispatch sim = GTG::Simulator.new tt - match = sim.match "/get" - assert_equal [paths.first], match.memos + memos = sim.memos "/get" + assert_equal [paths.first], memos - match = sim.match "/get/foo" - assert_equal [paths.last], match.memos + memos = sim.memos "/get/foo" + assert_equal [paths.last], memos end def test_match_data_ambiguous @@ -86,8 +86,8 @@ module ActionDispatch builder = GTG::Builder.new ast sim = GTG::Simulator.new builder.transition_table - match = sim.match "/articles/new" - assert_equal [paths[1], paths[3]], match.memos + memos = sim.memos "/articles/new" + assert_equal [paths[1], paths[3]], memos end private @@ -109,6 +109,14 @@ module ActionDispatch def simulator_for(paths) GTG::Simulator.new tt(paths) end + + def assert_match_route(simulator, path) + assert simulator.memos(path), "Simulator should match #{path}." + end + + def assert_no_match_route(simulator, path) + assert_not simulator.memos(path) { nil }, "Simulator should not match #{path}." + end end end end diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb index b77bf6628a..74277a4325 100644 --- a/actionpack/test/journey/router/utils_test.rb +++ b/actionpack/test/journey/router/utils_test.rb @@ -31,6 +31,11 @@ module ActionDispatch def test_normalize_path_uppercase assert_equal "/foo%AAbar%AAbaz", Utils.normalize_path("/foo%aabar%aabaz") end + + def test_normalize_path_maintains_string_encoding + path = "/foo%AAbar%AAbaz".b + assert_equal Encoding::ASCII_8BIT, Utils.normalize_path(path).encoding + end end end end diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb index b768553e7a..ff37d85ed8 100644 --- a/actionpack/test/lib/controller/fake_models.rb +++ b/actionpack/test/lib/controller/fake_models.rb @@ -26,6 +26,10 @@ Customer = Struct.new(:name, :id) do def persisted? id.present? end + + def cache_key + "#{name}/#{id}" + end end Post = Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) do diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index d478f4c437..122c42c5bd 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add `:json` type to `auto_discovery_link_tag` to support [JSON Feeds](https://jsonfeed.org/version/1) + + *Mike Gunderloy* + * Update `distance_of_time_in_words` helper to display better error messages for bad input. diff --git a/actionview/Rakefile b/actionview/Rakefile index de588ad25c..4f22ef84c8 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -119,7 +119,7 @@ namespace :assets do class Element {} require('#{dir}') JS - stdout, stderr, status = Open3.capture3("node", "--print", js) + _, stderr, status = Open3.capture3("node", "--print", js) if status.success? puts "[OK]" else diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md index 92f3e8a3b3..399ebc7324 100644 --- a/actionview/app/assets/javascripts/README.md +++ b/actionview/app/assets/javascripts/README.md @@ -39,7 +39,7 @@ Require `rails-ujs` into your application.js manifest. How to run tests ------------ -Run `bundle exec rake ujs:server` first, and then run the web tests by visiting [[http://localhost:4567]] in your browser. +Run `bundle exec rake ujs:server` first, and then run the web tests by visiting http://localhost:4567 in your browser. ## License rails-ujs is released under the [MIT License](MIT-LICENSE). diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 750f96f29e..c21fe782c6 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -122,9 +122,9 @@ module ActionView end # Returns a link tag that browsers and feed readers can use to auto-detect - # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or - # <tt>:atom</tt>. Control the link options in url_for format using the - # +url_options+. You can modify the LINK tag itself in +tag_options+. + # an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default), + # <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format + # using the +url_options+. You can modify the LINK tag itself in +tag_options+. # # ==== Options # @@ -138,6 +138,8 @@ module ActionView # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" /> # auto_discovery_link_tag(:atom) # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" /> + # auto_discovery_link_tag(:json) + # # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" /> # auto_discovery_link_tag(:rss, {action: "feed"}) # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" /> # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"}) @@ -147,8 +149,8 @@ module ActionView # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"}) # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" /> def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) - if !(type == :rss || type == :atom) && tag_options[:type].blank? - raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.") + if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank? + raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.") end tag( diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 15ab7e304f..c3aecadcd6 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -8,10 +8,9 @@ module ActionView # fragments, and so on. This method takes a block that contains # the content you wish to cache. # - # The best way to use this is by doing key-based cache expiration - # on top of a cache store like Memcached that'll automatically - # kick out old entries. For more on key-based expiration, see: - # http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works + # The best way to use this is by doing recyclable key-based cache expiration + # on top of a cache store like Memcached or Redis that'll automatically + # kick out old entries. # # When using this method, you list the cache dependency as the name of the cache, like so: # @@ -23,10 +22,14 @@ module ActionView # This approach will assume that when a new topic is added, you'll touch # the project. The cache key generated from this call will be something like: # - # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9 - # ^class ^id ^updated_at ^template tree digest + # views/template/action.html.erb:7a1156131a6928cb0026877f8b749ac9/projects/123 + # ^template path ^template tree digest ^class ^id # - # The cache is thus automatically bumped whenever the project updated_at is touched. + # This cache key is stable, but it's combined with a cache version derived from the project + # record. When the project updated_at is touched, the #cache_version changes, even + # if the key stays stable. This means that unlike a traditional key-based cache expiration + # approach, you won't be generating cache trash, unused keys, simply because the dependent + # record is updated. # # If your template cache depends on multiple sources (try to avoid this to keep things simple), # you can name all these dependencies as part of an array: @@ -217,10 +220,15 @@ module ActionView def fragment_name_with_digest(name, virtual_path) virtual_path ||= @virtual_path + if virtual_path name = controller.url_for(name).split("://").last if name.is_a?(Hash) - digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies - [ name, digest ] + + if digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies).presence + [ "#{virtual_path}:#{digest}", name ] + else + [ virtual_path, name ] + end else name end diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 1fbe209200..847256ac78 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -38,7 +38,7 @@ module ActionView end def expanded_cache_key(key) - key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) + key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb index 43f7242ee9..fbab512c41 100644 --- a/actionview/test/activerecord/relation_cache_test.rb +++ b/actionview/test/activerecord/relation_cache_test.rb @@ -10,7 +10,7 @@ class RelationCacheTest < ActionView::TestCase def test_cache_relation_other cache(Project.all) { concat("Hello World") } - assert_equal "Hello World", controller.cache_store.read("views/projects-#{Project.count}/") + assert_equal "Hello World", controller.cache_store.read("views/path/projects-#{Project.count}") end def view_cache_dependencies; end diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index b7a993c5c9..6093a4e660 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -53,6 +53,7 @@ class AssetTagHelperTest < ActionView::TestCase %(auto_discovery_link_tag) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:rss)) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:atom)) => %(<link href="http://www.example.com" rel="alternate" title="ATOM" type="application/atom+xml" />), + %(auto_discovery_link_tag(:json)) => %(<link href="http://www.example.com" rel="alternate" title="JSON" type="application/json" />), %(auto_discovery_link_tag(:rss, :action => "feed")) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(<link href="http://localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />), %(auto_discovery_link_tag(:rss, "//localhost/feed")) => %(<link href="//localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />), diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 7f358add7e..584666d54b 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -39,7 +39,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase def set_view_cache_dependencies def @view.view_cache_dependencies; []; end - def @view.fragment_cache_key(*); "ahoy `controller` dependency"; end + def @view.combined_fragment_cache_key(*); "ahoy `controller` dependency"; end end def test_render_file_template diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 3f66ab3ed3..fef78807d1 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -10,8 +10,8 @@ module RenderTestCases @view = Class.new(ActionView::Base) do def view_cache_dependencies; end - def fragment_cache_key(key) - ActiveSupport::Cache.expand_cache_key(key, :views) + def combined_fragment_cache_key(key) + [ :views, key ] end end.new(paths, @assigns) @@ -718,6 +718,6 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase private def cache_key(*names, virtual_path) digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] - @view.fragment_cache_key([ *names, digest ]) + @view.combined_fragment_cache_key([ "#{virtual_path}:#{digest}", *names ]) end end diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 6adfa95dd1..58d903b1c8 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -615,8 +615,8 @@ class UrlHelperTest < ActiveSupport::TestCase def test_mail_to_with_special_characters assert_dom_equal( - %{<a href="mailto:%23%21%24%25%26%27%2A%2B-%2F%3D%3F%5E_%60%7B%7D%7C%7E@example.org">#!$%&'*+-/=?^_`{}|~@example.org</a>}, - mail_to("#!$%&'*+-/=?^_`{}|~@example.org") + %{<a href="mailto:%23%21%24%25%26%27%2A%2B-%2F%3D%3F%5E_%60%7B%7D%7C@example.org">#!$%&'*+-/=?^_`{}|@example.org</a>}, + mail_to("#!$%&'*+-/=?^_`{}|@example.org") ) end diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb index d5b17de8b5..9aebc880a5 100644 --- a/activejob/lib/active_job/callbacks.rb +++ b/activejob/lib/active_job/callbacks.rb @@ -4,7 +4,7 @@ module ActiveJob # = Active Job Callbacks # # Active Job provides hooks during the life cycle of a job. Callbacks allow you - # to trigger logic during the life cycle of a job. Available callbacks are: + # to trigger logic during this cycle. Available callbacks are: # # * <tt>before_enqueue</tt> # * <tt>around_enqueue</tt> @@ -13,6 +13,8 @@ module ActiveJob # * <tt>around_perform</tt> # * <tt>after_perform</tt> # + # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # module Callbacks extend ActiveSupport::Concern include ActiveSupport::Callbacks diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index eac2761433..835e6f7716 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -56,6 +56,9 @@ module ActiveModel # # Would only create the +after_create+ and +before_create+ callback methods in # your class. + # + # NOTE: Calling the same callback multiple times will overwrite previous callback definitions. + # module Callbacks def self.extended(base) #:nodoc: base.class_eval do diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index d460068830..9fcde45167 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -147,6 +147,9 @@ module ActiveModel # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a +true+ or +false+ # value. + # + # NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions. + # def validate(*args, &block) options = args.extract_options! diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 2e059805a2..907dd894cd 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,16 @@ +* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via the new versioned entries + in `ActiveSupport::Cache`. This also means that `ActiveRecord::Base#cache_key` will now return a stable key + that does not include a timestamp any more. + + NOTE: This feature is turned off by default, and `#cache_key` will still return cache keys with timestamps + until you set `ActiveRecord::Base.cache_versioning = true`. That's the setting for all new apps on Rails 5.2+ + + *DHH* + +* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump + + *Rusty Geldmacher*, *Guillermo Iguaran* + * Add type caster to `RuntimeReflection#alias_name` Fixes #28959. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e52c2004f3..7c37132d3a 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1276,7 +1276,7 @@ module ActiveRecord # Scope examples: # has_many :comments, -> { where(author_id: 1) } # has_many :employees, -> { joins(:address) } - # has_many :posts, ->(post) { where("max_post_length > ?", post.length) } + # has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) } # # === Extensions # @@ -1443,7 +1443,7 @@ module ActiveRecord # Scope examples: # has_one :author, -> { where(comment_id: 1) } # has_one :employer, -> { joins(:company) } - # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) } + # has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) } # # === Options # @@ -1573,7 +1573,7 @@ module ActiveRecord # Scope examples: # belongs_to :firm, -> { where(id: 2) } # belongs_to :user, -> { joins(:friends) } - # belongs_to :level, ->(level) { where("game_level > ?", level.current) } + # belongs_to :level, ->(game) { where("game_level > ?", game.current_level) } # # === Options # @@ -1769,9 +1769,8 @@ module ActiveRecord # # Scope examples: # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } - # has_and_belongs_to_many :categories, ->(category) { - # where("default_category = ?", category.name) - # } + # has_and_belongs_to_many :categories, ->(post) { + # where("default_category = ?", post.default_category) # # === Extensions # diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 8995b1e352..643226267c 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -106,12 +106,7 @@ module ActiveRecord def join_constraints(outer_joins, join_type) joins = join_root.children.flat_map { |child| - - if join_type == Arel::Nodes::OuterJoin - make_left_outer_joins join_root, child - else - make_inner_joins join_root, child - end + make_join_constraints(join_root, child, join_type) } joins.concat outer_joins.flat_map { |oj| @@ -175,27 +170,15 @@ module ActiveRecord end def make_outer_joins(parent, child) - tables = table_aliases_for(parent, child) - join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_outer_joins(child, c) } - end - - def make_left_outer_joins(parent, child) - tables = child.tables join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + make_join_constraints(parent, child, join_type, true) end - def make_inner_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::InnerJoin - info = make_constraints parent, child, tables, join_type + def make_join_constraints(parent, child, join_type, aliasing = false) + tables = aliasing ? table_aliases_for(parent, child) : child.tables + info = make_constraints(parent, child, tables, join_type) - [info] + child.children.flat_map { |c| make_inner_joins(child, c) } + [info] + child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } end def table_aliases_for(parent, node) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 61cec5403a..80c9fde5d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -22,10 +22,6 @@ module ActiveRecord @children = children end - def name - reflection.name - end - def match?(other) self.class == other.class end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 607c54e481..ebe92e7878 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -389,7 +389,7 @@ module ActiveRecord autosave = reflection.options[:autosave] # reconstruct the scope now that we know the owner's id - association.reset_scope if association.respond_to?(:reset_scope) + association.reset_scope if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) if autosave diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 8e71b60b29..441237da1e 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -7,12 +7,21 @@ module ActiveRecord included do ## # :singleton-method: - # Indicates the format used to generate the timestamp in the cache key. - # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. + # Indicates the format used to generate the timestamp in the cache key, if + # versioning is off. Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. # # This is +:usec+, by default. class_attribute :cache_timestamp_format, instance_writer: false self.cache_timestamp_format = :usec + + ## + # :singleton-method: + # Indicates whether to use a stable #cache_key method that is accompanied + # by a changing version in the #cache_version method. + # + # This is +false+, by default until Rails 6.0. + class_attribute :cache_versioning, instance_writer: false + self.cache_versioning = false end # Returns a +String+, which Action Pack uses for constructing a URL to this @@ -42,35 +51,66 @@ module ActiveRecord id && id.to_s # Be sure to stringify the id for routes end - # Returns a cache key that can be used to identify this record. + # Returns a stable cache key that can be used to identify this record. # # Product.new.cache_key # => "products/new" - # Product.find(5).cache_key # => "products/5" (updated_at not available) - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + # Product.find(5).cache_key # => "products/5" # - # You can also pass a list of named timestamps, and the newest in the list will be - # used to generate the key: + # If ActiveRecord::Base.cache_versioning is turned off, as it was in Rails 5.1 and earlier, + # the cache key will also include a version. # - # Person.find(5).cache_key(:updated_at, :last_reviewed_at) + # Product.cache_versioning = false + # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) def cache_key(*timestamp_names) if new_record? "#{model_name.cache_key}/new" else - timestamp = if timestamp_names.any? - max_updated_column_timestamp(timestamp_names) + if cache_version && timestamp_names.none? + "#{model_name.cache_key}/#{id}" else - max_updated_column_timestamp - end + timestamp = if timestamp_names.any? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Specifying a timestamp name for #cache_key has been deprecated in favor of + the explicit #cache_version method that can be overwritten. + MSG - if timestamp - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" - else - "#{model_name.cache_key}/#{id}" + max_updated_column_timestamp(timestamp_names) + else + max_updated_column_timestamp + end + + if timestamp + timestamp = timestamp.utc.to_s(cache_timestamp_format) + "#{model_name.cache_key}/#{id}-#{timestamp}" + else + "#{model_name.cache_key}/#{id}" + end end end end + # Returns a cache version that can be used together with the cache key to form + # a recyclable caching scheme. By default, the #updated_at column is used for the + # cache_version, but this method can be overwritten to return something else. + # + # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to + # +false+ (which it is by default until Rails 6.0). + def cache_version + if cache_versioning && timestamp = try(:updated_at) + timestamp.utc.to_s(:usec) + end + end + + # Returns a cache key along with the version. + def cache_key_with_version + if version = cache_version + "#{cache_key}-#{version}" + else + cache_key + end + end + + module ClassMethods # Defines your model's +to_param+ method to generate "pretty" URLs # using +method_name+, which can be any attribute or method that diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 01ecd79b8f..3f39fb84e8 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -458,7 +458,7 @@ module ActiveRecord end unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) - raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" + raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end check_record_limit!(options[:limit], attributes_collection) diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 5775eda5a5..2d5be32266 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -2,7 +2,7 @@ module ActiveRecord # = Active Record \Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :left_joins, :left_outer_joins, :references, + :order, :joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, @@ -269,8 +269,7 @@ module ActiveRecord # Returns true if there are no records. def empty? return @records.empty? if loaded? - - limit_value == 0 || !exists? + !exists? end # Returns true if there are no records. diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index a1459c87c6..1d661fa8ed 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -307,9 +307,11 @@ module ActiveRecord MSG end - return false if !conditions + return false if !conditions || limit_value == 0 + + relation = self unless eager_loading? + relation ||= apply_join_dependency(self, construct_join_dependency(eager_loading: false)) - relation = apply_join_dependency(self, construct_join_dependency(eager_loading: false)) return false if ActiveRecord::NullRelation === relation relation = construct_relation_for_exists(relation, conditions) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1178dec706..79e65baae5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -248,7 +248,7 @@ module ActiveRecord return super() end - raise ArgumentError, "Call this with at least one field" if fields.empty? + raise ArgumentError, "Call `select' with at least one field" if fields.empty? spawn._select!(*fields) end @@ -1100,14 +1100,16 @@ module ActiveRecord end VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, - "asc", "desc", "ASC", "DESC"] # :nodoc: + "asc", "desc", "ASC", "DESC"].to_set # :nodoc: def validate_order_args(args) args.each do |arg| next unless arg.is_a?(Hash) arg.each do |_key, value| - raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \ - "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value) + unless VALID_DIRECTIONS.include?(value) + raise ArgumentError, + "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}" + end end end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 94d63765c9..24b81aabc8 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -11,8 +11,8 @@ module ActiveRecord ## # :singleton-method: # A list of tables which should not be dumped to the schema. - # Acceptable values are strings as well as regexp. - # This setting is only used if ActiveRecord::Base.schema_format == :ruby + # Acceptable values are strings as well as regexp if ActiveRecord::Base.schema_format == :ruby. + # Only strings are accepted if ActiveRecord::Base.schema_format == :sql. cattr_accessor :ignore_tables @@ignore_tables = [] diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 45110a79cd..c53dee43d7 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -71,9 +71,9 @@ module ActiveRecord @tasks[pattern] = task end - register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks) - register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) - register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + register_task(/mysql/, 'ActiveRecord::Tasks::MySQLDatabaseTasks') + register_task(/postgresql/, 'ActiveRecord::Tasks::PostgreSQLDatabaseTasks') + register_task(/sqlite/, 'ActiveRecord::Tasks::SQLiteDatabaseTasks') def db_dir @db_dir ||= Rails.application.config.paths["db"].first @@ -288,11 +288,11 @@ module ActiveRecord private def class_for_adapter(adapter) - key = @tasks.keys.detect { |pattern| adapter[pattern] } - unless key + _key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] } + unless task raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter" end - @tasks[key] + task.is_a?(String) ? task.constantize : task end def each_current_configuration(environment) diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index c05f0a8fbb..541165b3d1 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -60,6 +60,12 @@ module ActiveRecord args.concat(["--routines"]) args.concat(["--skip-comments"]) args.concat(Array(extra_flags)) if extra_flags + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + args += ignore_tables.map { |table| "--ignore-table=#{configuration['database']}.#{table}" } + end + args.concat(["#{configuration['database']}"]) run_cmd("mysqldump", args, "dumping") diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index f1af90c1e8..7f1a768d8b 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -66,6 +66,12 @@ module ActiveRecord "--schema=#{part.strip}" end end + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + args += ignore_tables.flat_map { |table| ["-T", table] } + end + args << configuration["database"] run_cmd("pg_dump", args, "dumping") remove_sql_header_comments(filename) diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 1f756c2979..7043d2f0c8 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -36,9 +36,18 @@ module ActiveRecord end def structure_dump(filename, extra_flags) - dbfile = configuration["database"] - flags = extra_flags.join(" ") if extra_flags - `sqlite3 #{flags} #{dbfile} .schema > #{filename}` + args = [] + args.concat(Array(extra_flags)) if extra_flags + args << configuration["database"] + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + condition = ignore_tables.map { |table| connection.quote_table_name(table) }.join(", ") + args << "SELECT sql FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name" + else + args << ".schema" + end + run_cmd("sqlite3", args, filename) end def structure_load(filename, extra_flags) @@ -56,6 +65,17 @@ module ActiveRecord def root @root end + + def run_cmd(cmd, args, out) + fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out) + end + + def run_cmd_error(cmd, args) + msg = "failed to execute:\n" + msg << "#{cmd} #{args.join(' ')}\n\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 11f4aae5b3..36238dd088 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1363,6 +1363,7 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_nothing_raised do authors(:david).essays.includes(:writer).any? authors(:david).essays.includes(:writer).exists? + authors(:david).essays.includes(:owner).where("name IS NOT NULL").exists? end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 287b3e9ebc..467cc73ecd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -651,20 +651,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end - def test_child_instance_should_be_shared_with_replaced_via_method_parent - face = faces(:confused) - new_man = Man.new - - assert_not_nil face.polymorphic_man - face.polymorphic_man = new_man - - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance" - face.description = "Bongo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance" - new_man.polymorphic_face.description = "Mungo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" - end - def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed new_man = Man.new face = Face.new diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 2c6a38ec35..7b8264e6e8 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -4,15 +4,23 @@ module ActiveRecord class CacheKeyTest < ActiveRecord::TestCase self.use_transactional_tests = false - class CacheMe < ActiveRecord::Base; end + class CacheMe < ActiveRecord::Base + self.cache_versioning = false + end + + class CacheMeWithVersion < ActiveRecord::Base + self.cache_versioning = true + end setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:cache_mes) { |t| t.timestamps } + @connection.create_table(:cache_mes, force: true) { |t| t.timestamps } + @connection.create_table(:cache_me_with_versions, force: true) { |t| t.timestamps } end teardown do @connection.drop_table :cache_mes, if_exists: true + @connection.drop_table :cache_me_with_versions, if_exists: true end test "cache_key format is not too precise" do @@ -21,5 +29,23 @@ module ActiveRecord assert_equal key, record.reload.cache_key end + + test "cache_key has no version when versioning is on" do + record = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{record.id}", record.cache_key + end + + test "cache_version is only there when versioning is on" do + assert CacheMeWithVersion.create.cache_version.present? + assert_not CacheMe.create.cache_version.present? + end + + test "cache_key_with_version always has both key and version" do + r1 = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version + + r2 = CacheMe.create + assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version + end end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 0678bb714f..9104976126 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -168,13 +168,65 @@ class IntegrationTest < ActiveRecord::TestCase end def test_named_timestamps_for_cache_key - owner = owners(:blackbeard) - assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + assert_deprecated do + owner = owners(:blackbeard) + assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + end end def test_cache_key_when_named_timestamp_is_nil - owner = owners(:blackbeard) - owner.happy_at = nil - assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + assert_deprecated do + owner = owners(:blackbeard) + owner.happy_at = nil + assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + end + end + + def test_cache_key_is_stable_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key + + developer.touch + second_key = developer.cache_key + + assert_equal first_key, second_key + ensure + Developer.cache_versioning = false + end + + def test_cache_version_changes_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_version = developer.cache_version + + travel 10.seconds do + developer.touch + end + + second_version = developer.cache_version + + assert_not_equal first_version, second_version + ensure + Developer.cache_versioning = false + end + + def test_cache_key_retains_version_when_custom_timestamp_is_used + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key_with_version + + travel 10.seconds do + developer.touch + end + + second_key = developer.cache_key_with_version + + assert_not_equal first_key, second_key + ensure + Developer.cache_versioning = false end end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index f426333aa5..b80257962c 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -59,19 +59,19 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new assert_equal 0, logger.debugs.length - logger.sql(Event.new(0, sql: "hi mom!")) + logger.sql(Event.new(0.9, sql: "hi mom!")) assert_equal 1, logger.debugs.length - logger.sql(Event.new(0, sql: "hi mom!", name: "foo")) + logger.sql(Event.new(0.9, sql: "hi mom!", name: "foo")) assert_equal 2, logger.debugs.length - logger.sql(Event.new(0, sql: "hi mom!", name: "SCHEMA")) + logger.sql(Event.new(0.9, sql: "hi mom!", name: "SCHEMA")) assert_equal 2, logger.debugs.length end def test_sql_statements_are_not_squeezed logger = TestDebugLogSubscriber.new - logger.sql(Event.new(0, sql: "ruby rails")) + logger.sql(Event.new(0.9, sql: "ruby rails")) assert_match(/ruby rails/, logger.debugs.first) end @@ -87,7 +87,7 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, color_regex| - logger.sql(Event.new(0, sql: verb.to_s)) + logger.sql(Event.new(0.9, sql: verb.to_s)) assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last) end end @@ -96,11 +96,11 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(Event.new(0, sql: verb.to_s)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(Event.new(0, sql: verb.to_s, name: "SQL")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "SQL")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end @@ -108,14 +108,14 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(Event.new(0, sql: verb.to_s, name: "Model Load")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0\.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Load")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(Event.new(0, sql: verb.to_s, name: "Model Exists")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0\.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Exists")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(Event.new(0, sql: verb.to_s, name: "ANY SPECIFIC NAME")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0\.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "ANY SPECIFIC NAME")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end @@ -123,8 +123,8 @@ class LogSubscriberTest < ActiveRecord::TestCase logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| - logger.sql(Event.new(0, sql: "#{verb} WHERE ID IN SELECT")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: "#{verb} WHERE ID IN SELECT")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) end end @@ -138,8 +138,8 @@ class LogSubscriberTest < ActiveRecord::TestCase SELECT ID FROM THINGS ) EOS - logger.sql(Event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end end @@ -151,14 +151,14 @@ class LogSubscriberTest < ActiveRecord::TestCase (SELECT * FROM mytable FOR UPDATE) ss WHERE col1 = 5; EOS - logger.sql(Event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) sql = <<-EOS LOCK TABLE films IN SHARE MODE; EOS - logger.sql(Event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end def test_exists_query_logging diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index b87419d203..5a62cbd3a6 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -752,7 +752,7 @@ module NestedAttributesOnACollectionAssociationTests exception = assert_raise ArgumentError do @pirate.send(association_setter, "foo") end - assert_equal 'Hash or Array expected, got String ("foo")', exception.message + assert_equal %{Hash or Array expected for attribute `#{@association_name}`, got String ("foo")}, exception.message end def test_should_work_with_update_as_well diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 11ef0d8743..dea787c07f 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -36,7 +36,7 @@ module ActiveRecord @relation ||= Relation.new FakeKlass.new("posts"), Post.arel_table, Post.predicate_builder end - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index b85d303a91..33da3d11fc 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -294,6 +294,13 @@ if current_adapter?(:Mysql2Adapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end + def test_structure_dump + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + def test_structure_dump_with_extra_flags filename = "awesome-file.sql" expected_command = ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--noop", "test-db"] @@ -305,6 +312,15 @@ if current_adapter?(:Mysql2Adapter) end end + def test_structure_dump_with_ignore_tables + filename = "awesome-file.sql" + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) + + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" Kernel.expects(:system) diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 512388af6b..a2e968aedf 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -259,6 +259,14 @@ if current_adapter?(:PostgreSQLAdapter) end end + def test_structure_dump_with_ignore_tables + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) + + Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + def test_structure_dump_with_schema_search_path @configuration["schema_search_path"] = "foo,bar" diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 0d917f3f6c..ccb3834fee 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -180,6 +180,9 @@ if current_adapter?(:SQLite3Adapter) "adapter" => "sqlite3", "database" => @database } + + `sqlite3 #{@database} 'CREATE TABLE bar(id INTEGER)'` + `sqlite3 #{@database} 'CREATE TABLE foo(id INTEGER)'` end def test_structure_dump @@ -189,6 +192,23 @@ if current_adapter?(:SQLite3Adapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, "/rails/root" assert File.exist?(dbfile) assert File.exist?(filename) + assert_match(/CREATE TABLE foo/, File.read(filename)) + assert_match(/CREATE TABLE bar/, File.read(filename)) + ensure + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) + end + + def test_structure_dump_with_ignore_tables + dbfile = @database + filename = "awesome-file.sql" + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo"]) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") + assert File.exist?(dbfile) + assert File.exist?(filename) + assert_match(/bar/, File.read(filename)) + assert_no_match(/foo/, File.read(filename)) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 3ce4a0bbab..bae573cf37 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,31 @@ +* Fix implicit coercion calculations with scalars and durations + + Previously calculations where the scalar is first would be converted to a duration + of seconds but this causes issues with dates being converted to times, e.g: + + Time.zone = "Beijing" # => Asia/Shanghai + date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 + 2 * 1.day # => 172800 seconds + date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00 + + Now the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain + the part structure of the duration where possible, e.g: + + Time.zone = "Beijing" # => Asia/Shanghai + date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 + 2 * 1.day # => 2 days + date + 2 * 1.day # => Mon, 22 May 2017 + + Fixes #29160, #28970. + + *Andrew White* + +* Add support for versioned cache entries. This enables the cache stores to recycle cache keys, greatly saving + on storage in cases with frequent churn. Works together with the separation of `#cache_key` and `#cache_version` + in Active Record and its use in Action Pack's fragment caching. + + *DHH* + * Pass gem name and deprecation horizon to deprecation notifications. *Willem van Bergen* diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 4d8c2046e8..44f80dd379 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -88,10 +88,11 @@ module ActiveSupport private def retrieve_cache_key(key) case - when key.respond_to?(:cache_key) then key.cache_key - when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param - when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) - else key.to_param + when key.respond_to?(:cache_key_with_version) then key.cache_key_with_version + when key.respond_to?(:cache_key) then key.cache_key + when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param + when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) + else key.to_param end.to_s end @@ -219,6 +220,10 @@ module ActiveSupport # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes) # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry # + # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt> + # is of the same version. nil is returned on mismatches despite contents. + # This feature is used to support recyclable cache keys. + # # Setting <tt>:race_condition_ttl</tt> is very useful in situations where # a cache entry is used very frequently and is under heavy load. If a # cache expires and due to heavy load several different processes will try @@ -287,6 +292,7 @@ module ActiveSupport instrument(:read, name, options) do |payload| cached_entry = read_entry(key, options) unless options[:force] entry = handle_expired_entry(cached_entry, key, options) + entry = nil if entry && entry.mismatched?(normalize_version(name, options)) payload[:super_operation] = :fetch if payload payload[:hit] = !!entry if payload end @@ -303,21 +309,30 @@ module ActiveSupport end end - # Fetches data from the cache, using the given key. If there is data in + # Reads data from the cache, using the given key. If there is data in # the cache with the given key, then that data is returned. Otherwise, # +nil+ is returned. # + # Note, if data was written with the <tt>:expires_in<tt> or <tt>:version</tt> options, + # both of these conditions are applied before the data is returned. + # # Options are passed to the underlying cache implementation. def read(name, options = nil) options = merged_options(options) - key = normalize_key(name, options) + key = normalize_key(name, options) + version = normalize_version(name, options) + instrument(:read, name, options) do |payload| entry = read_entry(key, options) + if entry if entry.expired? delete_entry(key, options) payload[:hit] = false if payload nil + elsif entry.mismatched?(version) + payload[:hit] = false if payload + nil else payload[:hit] = true if payload entry.value @@ -341,11 +356,15 @@ module ActiveSupport results = {} names.each do |name| - key = normalize_key(name, options) - entry = read_entry(key, options) + key = normalize_key(name, options) + version = normalize_version(name, options) + entry = read_entry(key, options) + if entry if entry.expired? delete_entry(key, options) + elsif entry.mismatched?(version) + # Skip mismatched versions else results[name] = entry.value end @@ -396,7 +415,7 @@ module ActiveSupport options = merged_options(options) instrument(:write, name, options) do - entry = Entry.new(value, options) + entry = Entry.new(value, options.merge(version: normalize_version(name, options))) write_entry(normalize_key(name, options), entry, options) end end @@ -420,7 +439,7 @@ module ActiveSupport instrument(:exist?, name) do entry = read_entry(normalize_key(name, options), options) - (entry && !entry.expired?) || false + (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false end end @@ -517,6 +536,17 @@ module ActiveSupport end end + + # Prefixes a key with the namespace. Namespace and key will be delimited + # with a colon. + def normalize_key(key, options) + key = expanded_key(key) + namespace = options[:namespace] if options + prefix = namespace.is_a?(Proc) ? namespace.call : namespace + key = "#{prefix}:#{key}" if prefix + key + end + # Expands key to be a consistent string value. Invokes +cache_key+ if # object responds to +cache_key+. Otherwise, +to_param+ method will be # called. If the key is a Hash, then keys will be sorted alphabetically. @@ -537,14 +567,16 @@ module ActiveSupport key.to_param end - # Prefixes a key with the namespace. Namespace and key will be delimited - # with a colon. - def normalize_key(key, options) - key = expanded_key(key) - namespace = options[:namespace] if options - prefix = namespace.is_a?(Proc) ? namespace.call : namespace - key = "#{prefix}:#{key}" if prefix - key + def normalize_version(key, options = nil) + (options && options[:version].try(:to_param)) || expanded_version(key) + end + + def expanded_version(key) + case + when key.respond_to?(:cache_version) then key.cache_version.to_param + when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param + when key.respond_to?(:to_a) then expanded_version(key.to_a) + end end def instrument(operation, key, options = nil) @@ -555,6 +587,7 @@ module ActiveSupport ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) } end + def log return unless logger && logger.debug? && !silence? logger.debug(yield) @@ -591,13 +624,16 @@ module ActiveSupport end end - # This class is used to represent cache entries. Cache entries have a value and an optional - # expiration time. The expiration time is used to support the :race_condition_ttl option - # on the cache. + # This class is used to represent cache entries. Cache entries have a value, an optional + # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option + # on the cache. The version is used to support the :version option on the cache for rejecting + # mismatches. # # Since cache entries in most instances will be serialized, the internals of this class are highly optimized # using short instance variable names that are lazily defined. class Entry # :nodoc: + attr_reader :version + DEFAULT_COMPRESS_LIMIT = 16.kilobytes # Creates a new cache entry for the specified value. Options supported are @@ -610,6 +646,7 @@ module ActiveSupport @value = value end + @version = options[:version] @created_at = Time.now.to_f @expires_in = options[:expires_in] @expires_in = @expires_in.to_f if @expires_in @@ -619,6 +656,10 @@ module ActiveSupport compressed? ? uncompress(@value) : @value end + def mismatched?(version) + @version && version && @version != version + end + # Checks if the entry is expired. The +expires_in+ parameter can override # the value set when the entry was created. def expired? diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index e09cee3335..06fa9f67ad 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -97,12 +97,18 @@ module ActiveSupport options = merged_options(options) keys_to_names = Hash[names.map { |name| [normalize_key(name, options), name] }] + raw_values = @data.get_multi(keys_to_names.keys) values = {} + raw_values.each do |key, value| entry = deserialize_entry(value) - values[keys_to_names[key]] = entry.value unless entry.expired? + + unless entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options)) + values[keys_to_names[key]] = entry.value + end end + values end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index 672eb2bb80..91875a56f5 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -115,7 +115,12 @@ module ActiveSupport end def write_entry(key, entry, options) - local_cache.write_entry(key, entry, options) if local_cache + if options[:unless_exist] + local_cache.delete_entry(key, options) if local_cache + else + local_cache.write_entry(key, entry, options) if local_cache + end + super end diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index d4424ed792..39deb2313f 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -37,27 +37,56 @@ module ActiveSupport end def +(other) - calculate(:+, other) + if Duration === other + seconds = value + other.parts[:seconds] + new_parts = other.parts.merge(seconds: seconds) + new_value = value + other.value + + Duration.new(new_value, new_parts) + else + calculate(:+, other) + end end def -(other) - calculate(:-, other) + if Duration === other + seconds = value - other.parts[:seconds] + new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h + new_parts = new_parts.merge(seconds: seconds) + new_value = value - other.value + + Duration.new(new_value, new_parts) + else + calculate(:-, other) + end end def *(other) - calculate(:*, other) + if Duration === other + new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h + new_value = value * other.value + + Duration.new(new_value, new_parts) + else + calculate(:*, other) + end end def /(other) - calculate(:/, other) + if Duration === other + new_parts = other.parts.map { |part, other_value| [part, value / other_value] }.to_h + new_value = new_parts.inject(0) { |total, (part, value)| total + value * Duration::PARTS_IN_SECONDS[part] } + + Duration.new(new_value, new_parts) + else + calculate(:/, other) + end end private def calculate(op, other) if Scalar === other Scalar.new(value.public_send(op, other.value)) - elsif Duration === other - Duration.seconds(value).public_send(op, other) elsif Numeric === other Scalar.new(value.public_send(op, other)) else diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 24053b4fe5..726e1464ad 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -115,7 +115,7 @@ module ActiveSupport # Currently the OpenSSL bindings do not raise an error if auth_tag is # truncated, which would allow an attacker to easily forge it. See # https://github.com/ruby/openssl/issues/63 - raise InvalidMessage if aead_mode? && auth_tag.bytes.length != 16 + raise InvalidMessage if aead_mode? && (auth_tag.nil? || auth_tag.bytes.length != 16) cipher.decrypt cipher.key = @secret diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index ee6592fb5a..12ec8bf1b8 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -84,12 +84,18 @@ module ActiveSupport # end # # Returns the exception if it was handled and +nil+ if it was not. - def rescue_with_handler(exception, object: self) + def rescue_with_handler(exception, object: self, visited_exceptions: []) + visited_exceptions << exception + if handler = handler_for_rescue(exception, object: object) handler.call exception exception elsif exception - rescue_with_handler(exception.cause, object: object) + if visited_exceptions.include?(exception.cause) + nil + else + rescue_with_handler(exception.cause, object: object, visited_exceptions: visited_exceptions) + end end end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index c67ffe69b8..f53b98c73e 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -579,6 +579,93 @@ module CacheStoreBehavior end end +module CacheStoreVersionBehavior + ModelWithKeyAndVersion = Struct.new(:cache_key, :cache_version) + + def test_fetch_with_right_version_should_hit + @cache.fetch("foo", version: 1) { "bar" } + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_fetch_with_wrong_version_should_miss + @cache.fetch("foo", version: 1) { "bar" } + assert_nil @cache.read("foo", version: 2) + end + + def test_read_with_right_version_should_hit + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: 1) + end + + def test_read_with_wrong_version_should_miss + @cache.write("foo", "bar", version: 1) + assert_nil @cache.read("foo", version: 2) + end + + def test_exist_with_right_version_should_be_true + @cache.write("foo", "bar", version: 1) + assert @cache.exist?("foo", version: 1) + end + + def test_exist_with_wrong_version_should_be_false + @cache.write("foo", "bar", version: 1) + assert !@cache.exist?("foo", version: 2) + end + + def test_reading_and_writing_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert_equal "bar", @cache.read(m1v1) + assert_nil @cache.read(m1v2) + end + + def test_reading_and_writing_with_model_supporting_cache_version_using_nested_key + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write([ "something", m1v1 ], "bar") + assert_equal "bar", @cache.read([ "something", m1v1 ]) + assert_nil @cache.read([ "something", m1v2 ]) + end + + def test_fetching_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.fetch(m1v1) { "bar" } + assert_equal "bar", @cache.fetch(m1v1) { "bu" } + assert_equal "bu", @cache.fetch(m1v2) { "bu" } + end + + def test_exist_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m1v2 = ModelWithKeyAndVersion.new("model/1", 2) + + @cache.write(m1v1, "bar") + assert @cache.exist?(m1v1) + assert_not @cache.fetch(m1v2) + end + + def test_fetch_multi_with_model_supporting_cache_version + m1v1 = ModelWithKeyAndVersion.new("model/1", 1) + m2v1 = ModelWithKeyAndVersion.new("model/2", 1) + m2v2 = ModelWithKeyAndVersion.new("model/2", 2) + + first_fetch_values = @cache.fetch_multi(m1v1, m2v1) { |m| m.cache_key } + second_fetch_values = @cache.fetch_multi(m1v1, m2v2) { |m| m.cache_key + " 2nd" } + + assert_equal({ m1v1 => "model/1", m2v1 => "model/2" }, first_fetch_values) + assert_equal({ m1v1 => "model/1", m2v2 => "model/2 2nd" }, second_fetch_values) + end + + def test_version_is_normalized + @cache.write("foo", "bar", version: 1) + assert_equal "bar", @cache.read("foo", version: "1") + end +end + # https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters # The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special # characters like the umlaut in UTF-8. @@ -708,6 +795,14 @@ module LocalCacheBehavior end end + def test_local_cache_of_write_with_unless_exist + @cache.with_local_cache do + @cache.write("foo", "bar") + @cache.write("foo", "baz", unless_exist: true) + assert_equal @peek.read("foo"), @cache.read("foo") + end + end + def test_local_cache_of_delete @cache.with_local_cache do @cache.write("foo", "bar") @@ -814,6 +909,7 @@ class FileStoreTest < ActiveSupport::TestCase end include CacheStoreBehavior + include CacheStoreVersionBehavior include LocalCacheBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior @@ -923,6 +1019,7 @@ class MemoryStoreTest < ActiveSupport::TestCase end include CacheStoreBehavior + include CacheStoreVersionBehavior include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior @@ -1044,6 +1141,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase end include CacheStoreBehavior + include CacheStoreVersionBehavior include LocalCacheBehavior include CacheIncrementDecrementBehavior include EncodedKeyCacheBehavior diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 1648a9b270..3108f24f21 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -337,6 +337,13 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_plus_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: 1, seconds: 10 }, (scalar + 1.day).parts) + assert_equal({ days: -1, seconds: 10 }, (scalar + -1.day).parts) + end + def test_scalar_minus scalar = ActiveSupport::Duration::Scalar.new(10) @@ -349,6 +356,9 @@ class DurationTest < ActiveSupport::TestCase assert_equal 5, scalar - 5.seconds assert_instance_of ActiveSupport::Duration, scalar - 5.seconds + assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts) + assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts) + exception = assert_raises(TypeError) do scalar - "foo" end @@ -356,6 +366,13 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_minus_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: -1, seconds: 10 }, (scalar - 1.day).parts) + assert_equal({ days: 1, seconds: 10 }, (scalar - -1.day).parts) + end + def test_scalar_multiply scalar = ActiveSupport::Duration::Scalar.new(5) @@ -375,6 +392,14 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_multiply_parts + scalar = ActiveSupport::Duration::Scalar.new(1) + assert_equal({ days: 2 }, (scalar * 2.days).parts) + assert_equal(172800, (scalar * 2.days).value) + assert_equal({ days: -2 }, (scalar * -2.days).parts) + assert_equal(-172800, (scalar * -2.days).value) + end + def test_scalar_divide scalar = ActiveSupport::Duration::Scalar.new(10) @@ -394,6 +419,15 @@ class DurationTest < ActiveSupport::TestCase assert_equal "no implicit conversion of String into ActiveSupport::Duration::Scalar", exception.message end + def test_scalar_divide_parts + scalar = ActiveSupport::Duration::Scalar.new(10) + + assert_equal({ days: 2 }, (scalar / 5.days).parts) + assert_equal(172800, (scalar / 5.days).value) + assert_equal({ days: -2 }, (scalar / -5.days).parts) + assert_equal(-172800, (scalar / -5.days).value) + end + def test_twelve_months_equals_one_year assert_equal 12.months, 1.year end diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index 56a436f751..4c3515b5e1 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -86,20 +86,32 @@ class MessageEncryptorTest < ActiveSupport::TestCase assert_equal @data, encryptor.decrypt_and_verify(message) end + def test_aead_mode_with_hmac_cbc_cipher_text + encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") + + assert_aead_not_decrypted(encryptor, "eHdGeExnZEwvMSt3U3dKaFl1WFo0TjVvYzA0eGpjbm5WSkt5MXlsNzhpZ0ZnbWhBWFlQZTRwaXE1bVJCS2oxMDZhYVp2dVN3V0lNZUlWQ3c2eVhQbnhnVjFmeVVubmhRKzF3WnZyWHVNMDg9LS1HSisyakJVSFlPb05ISzRMaXRzcFdBPT0=--831a1d54a3cda8a0658dc668a03dedcbce13b5ca") + end + def test_messing_with_aead_values_causes_failures encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm") text, iv, auth_tag = encryptor.encrypt_and_sign(@data).split("--") - assert_not_decrypted([iv, text, auth_tag] * "--") - assert_not_decrypted([munge(text), iv, auth_tag] * "--") - assert_not_decrypted([text, munge(iv), auth_tag] * "--") - assert_not_decrypted([text, iv, munge(auth_tag)] * "--") - assert_not_decrypted([munge(text), munge(iv), munge(auth_tag)] * "--") - assert_not_decrypted([text, iv] * "--") - assert_not_decrypted([text, iv, auth_tag[0..-2]] * "--") + assert_aead_not_decrypted(encryptor, [iv, text, auth_tag] * "--") + assert_aead_not_decrypted(encryptor, [munge(text), iv, auth_tag] * "--") + assert_aead_not_decrypted(encryptor, [text, munge(iv), auth_tag] * "--") + assert_aead_not_decrypted(encryptor, [text, iv, munge(auth_tag)] * "--") + assert_aead_not_decrypted(encryptor, [munge(text), munge(iv), munge(auth_tag)] * "--") + assert_aead_not_decrypted(encryptor, [text, iv] * "--") + assert_aead_not_decrypted(encryptor, [text, iv, auth_tag[0..-2]] * "--") end private + def assert_aead_not_decrypted(encryptor, value) + assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do + encryptor.decrypt_and_verify(value) + end + end + def assert_not_decrypted(value) assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do @encryptor.decrypt_and_verify(@verifier.generate(value)) diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb index f7eb047d44..3bdd1651e7 100644 --- a/activesupport/test/rescuable_test.rb +++ b/activesupport/test/rescuable_test.rb @@ -43,7 +43,9 @@ class Stargate def dispatch(method) send(method) rescue Exception => e - rescue_with_handler(e) + unless rescue_with_handler(e) + @result = "unhandled" + end end def attack @@ -58,6 +60,26 @@ class Stargate raise MadRonon.new("dex") end + def crash + raise "unhandled RuntimeError" + end + + def looped_crash + ex1 = StandardError.new("error 1") + ex2 = StandardError.new("error 2") + begin + begin + raise ex1 + rescue + # sets the cause on ex2 to be ex1 + raise ex2 + end + rescue + # sets the cause on ex1 to be ex2 + raise ex1 + end + end + def fall_back_to_cause # This exception is the cause and has a handler. ronanize @@ -139,4 +161,14 @@ class RescuableTest < ActiveSupport::TestCase @stargate.dispatch :fall_back_to_cause assert_equal "dex", @stargate.result end + + def test_unhandled_exceptions + @stargate.dispatch(:crash) + assert_equal "unhandled", @stargate.result + end + + def test_rescue_handles_loops_in_exception_cause_chain + @stargate.dispatch :looped_crash + assert_equal "unhandled", @stargate.result + end end diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index 9d43c10be6..7ac3d417a4 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -93,7 +93,7 @@ HTML def github_file_url(file_path) tree = version || edge - root = file_path[%r{(.+)/}, 1] + root = file_path[%r{(\w+)/}, 1] path = \ case root when "abstract_controller", "action_controller", "action_dispatch" diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 5d987264f5..22537f960c 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -715,6 +715,9 @@ end Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to skip this filter only for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place. +NOTE: Calling the same filter multiple times with different options will not work, +since the last filter definition will overwrite the previous ones. + ### After Filters and Around Filters In addition to "before" filters, you can also run filters after an action has been executed, or both before and after. diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index c835adeab6..10412128cc 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -419,7 +419,7 @@ image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png #### auto_discovery_link_tag -Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed. +Returns a link tag that browsers and feed readers can use to auto-detect an RSS, Atom, or JSON feed. ```ruby auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", { title: "RSS Feed" }) # => diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index b58ca61848..443be77934 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -310,6 +310,12 @@ UserMailer.welcome(@user).deliver_now UserMailer.welcome(@user).deliver_later ``` +NOTE: Using the asynchronous queue from a Rake task (for example, to +send an email using `.deliver_later`) will generally not work because Rake will +likely end, causing the in-process thread pool to be deleted, before any/all +of the `.deliver_later` emails are processed. To avoid this problem, use +`.deliver_now` or run a persistent queue in development. + Internationalization -------------------- diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 3c61754982..c3c7367304 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -281,7 +281,7 @@ Methods created with `(module|class)_eval(STRING)` have a comment by their side ```ruby for severity in Severity.constants - class_eval <<-EOT, __FILE__, __LINE__ + class_eval <<-EOT, __FILE__, __LINE__ + 1 def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block) add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block) end # end diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 39f4272b3c..2f2962a3e6 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -141,14 +141,15 @@ NOTE: To help our CI servers you should add [ci skip] to your documentation comm Translating Rails Guides ------------------------ -We are happy to have people volunteer to translate the Rails guides into their own language. -If you want to translate the Rails guides in your own language, follows these steps: +We are happy to have people volunteer to translate the Rails guides. Just follow these steps: -* Fork the project (rails/rails). +* Fork https://github.com/rails/rails. * Add a source folder for your own language, for example: *guides/source/it-IT* for Italian. * Copy the contents of *guides/source* into your own language directory and translate them. * Do NOT translate the HTML files, as they are automatically generated. +Note that translations are not submitted to the Rails repository. As detailed above, your work happens in a fork. This is so because in practice documentation maintenance via patches is only sustainable in English. + To generate the guides in HTML format cd into the *guides* directory then run (eg. for it-IT): ```bash diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index f3ae5a5b28..5553f08456 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -20,16 +20,7 @@ Guide Assumptions This guide is designed for beginners who want to get started with a Rails application from scratch. It does not assume that you have any prior experience -with Rails. However, to get the most out of it, you need to have some -prerequisites installed: - -* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 2.2.2 or newer. -* Right version of [Development Kit](http://rubyinstaller.org/downloads/), if you - are using Windows. -* The [RubyGems](https://rubygems.org) packaging system, which is installed with - Ruby by default. To learn more about RubyGems, please read the - [RubyGems Guides](http://guides.rubygems.org). -* A working installation of the [SQLite3 Database](https://www.sqlite.org). +with Rails. Rails is a web application framework running on the Ruby programming language. If you have no prior experience with Ruby, you will find a very steep learning @@ -86,6 +77,9 @@ your prompt will look something like `c:\source_code>` ### Installing Rails +Before you install Rails, you should check to make sure that your system has the +proper prerequisites installed. These include Ruby and SQLite3. + Open up a command line prompt. On macOS open Terminal.app, on Windows choose "Run" from your Start menu and type 'cmd.exe'. Any commands prefaced with a dollar sign `$` should be run in the command line. Verify that you have a @@ -96,12 +90,19 @@ $ ruby -v ruby 2.3.1p112 ``` +Rails requires Ruby version 2.2.2 or later. If the version number returned is +less than that number, you'll need to install a fresh copy of Ruby. + TIP: A number of tools exist to help you quickly install Ruby and Ruby on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), while macOS users can use [Tokaido](https://github.com/tokaido/tokaidoapp). For more installation methods for most Operating Systems take a look at [ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/). +If you are working on Windows, you should also install the +[Ruby Installer Development Kit](http://rubyinstaller.org/downloads/). + +You will also need an installation of the SQLite3 database. Many popular UNIX-like OSes ship with an acceptable version of SQLite3. On Windows, if you installed Rails through Rails Installer, you already have SQLite installed. Others can find installation instructions @@ -127,7 +128,7 @@ run the following: $ rails --version ``` -If it says something like "Rails 5.1.0", you are ready to continue. +If it says something like "Rails 5.1.1", you are ready to continue. ### Creating the Blog Application @@ -1195,7 +1196,7 @@ it look as follows: This time we point the form to the `update` action, which is not defined yet but will be very soon. -Passing the article object to the method, will automagically create url for submitting the edited article form. +Passing the article object to the method, will automagically create url for submitting the edited article form. This option tells Rails that we want this form to be submitted via the `PATCH` HTTP method which is the HTTP method you're expected to use to **update** resources according to the REST protocol. diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index 48bb3147f3..caa3d21d23 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -768,7 +768,7 @@ WARNING: The asset tag helpers do _not_ verify the existence of the assets at th #### Linking to Feeds with the `auto_discovery_link_tag` -The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS or Atom feeds. It takes the type of the link (`:rss` or `:atom`), a hash of options that are passed through to url_for, and a hash of options for the tag: +The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS, Atom, or JSON feeds. It takes the type of the link (`:rss`, `:atom`, or `:json`), a hash of options that are passed through to url_for, and a hash of options for the tag: ```erb <%= auto_discovery_link_tag(:rss, {action: "feed"}, diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md deleted file mode 100644 index 71efa4b0d0..0000000000 --- a/guides/source/nested_model_forms.md +++ /dev/null @@ -1,230 +0,0 @@ -**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -Rails Nested Model Forms -======================== - -Creating a form for a model _and_ its associations can become quite tedious. Therefore Rails provides helpers to assist in dealing with the complexities of generating these forms _and_ the required CRUD operations to create, update, and destroy associations. - -After reading this guide, you will know: - -* do stuff. - --------------------------------------------------------------------------------- - -NOTE: This guide assumes the user knows how to use the [Rails form helpers](form_helpers.html) in general. Also, it's **not** an API reference. For a complete reference please visit [the Rails API documentation](http://api.rubyonrails.org/). - - -Model setup ------------ - -To be able to use the nested model functionality in your forms, the model will need to support some basic operations. - -First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The `fields_for` form helper will look for this method to decide whether or not a nested model form should be built. - -If the associated object is an array, a form builder will be yielded for each object, else only a single form builder will be yielded. - -Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the `:address` attribute, the `fields_for` form helper will look for a method on the Person instance named `address_attributes=`. - -### ActiveRecord::Base model - -For an ActiveRecord::Base model and association this writer method is commonly defined with the `accepts_nested_attributes_for` class method: - -#### has_one - -```ruby -class Person < ApplicationRecord - has_one :address - accepts_nested_attributes_for :address -end -``` - -#### belongs_to - -```ruby -class Person < ApplicationRecord - belongs_to :firm - accepts_nested_attributes_for :firm -end -``` - -#### has_many / has_and_belongs_to_many - -```ruby -class Person < ApplicationRecord - has_many :projects - accepts_nested_attributes_for :projects -end -``` - -NOTE: For greater detail on associations see [Active Record Associations](association_basics.html). -For a complete reference on associations please visit the API documentation for [ActiveRecord::Associations::ClassMethods](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html). - -### Custom model - -As you might have inflected from this explanation, you _don't_ necessarily need an ActiveRecord::Base model to use this functionality. The following examples are sufficient to enable the nested model form behavior: - -#### Single associated object - -```ruby -class Person - def address - Address.new - end - - def address_attributes=(attributes) - # ... - end -end -``` - -#### Association collection - -```ruby -class Person - def projects - [Project.new, Project.new] - end - - def projects_attributes=(attributes) - # ... - end -end -``` - -NOTE: See (TODO) in the advanced section for more information on how to deal with the CRUD operations in your custom model. - -Views ------ - -### Controller code - -A nested model form will _only_ be built if the associated object(s) exist. This means that for a new model instance you would probably want to build the associated object(s) first. - -Consider the following typical RESTful controller which will prepare a new Person instance and its `address` and `projects` associations before rendering the `new` template: - -```ruby -class PeopleController < ApplicationController - def new - @person = Person.new - @person.build_address - 2.times { @person.projects.build } - end - - def create - @person = Person.new(params[:person]) - if @person.save - # ... - end - end -end -``` - -NOTE: Obviously the instantiation of the associated object(s) can become tedious and not DRY, so you might want to move that into the model itself. ActiveRecord::Base provides an `after_initialize` callback which is a good way to refactor this. - -### Form code - -Now that you have a model instance, with the appropriate methods and associated object(s), you can start building the nested model form. - -#### Standard form - -Start out with a regular RESTful form: - -```erb -<%= form_for @person do |f| %> - <%= f.text_field :name %> -<% end %> -``` - -This will generate the following html: - -```html -<form action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> -</form> -``` - -#### Nested form for a single associated object - -Now add a nested form for the `address` association: - -```erb -<%= form_for @person do |f| %> - <%= f.text_field :name %> - - <%= f.fields_for :address do |af| %> - <%= af.text_field :street %> - <% end %> -<% end %> -``` - -This generates: - -```html -<form action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> - - <input id="person_address_attributes_street" name="person[address_attributes][street]" type="text" /> -</form> -``` - -Notice that `fields_for` recognized the `address` as an association for which a nested model form should be built by the way it has namespaced the `name` attribute. - -When this form is posted the Rails parameter parser will construct a hash like the following: - -```ruby -{ - "person" => { - "name" => "Eloy Duran", - "address_attributes" => { - "street" => "Nieuwe Prinsengracht" - } - } -} -``` - -That's it. The controller will simply pass this hash on to the model from the `create` action. The model will then handle building the `address` association for you and automatically save it when the parent (`person`) is saved. - -#### Nested form for a collection of associated objects - -The form code for an association collection is pretty similar to that of a single associated object: - -```erb -<%= form_for @person do |f| %> - <%= f.text_field :name %> - - <%= f.fields_for :projects do |pf| %> - <%= pf.text_field :name %> - <% end %> -<% end %> -``` - -Which generates: - -```html -<form action="/people" class="new_person" id="new_person" method="post"> - <input id="person_name" name="person[name]" type="text" /> - - <input id="person_projects_attributes_0_name" name="person[projects_attributes][0][name]" type="text" /> - <input id="person_projects_attributes_1_name" name="person[projects_attributes][1][name]" type="text" /> -</form> -``` - -As you can see it has generated 2 `project name` inputs, one for each new `project` that was built in the controller's `new` action. Only this time the `name` attribute of the input contains a digit as an extra namespace. This will be parsed by the Rails parameter parser as: - -```ruby -{ - "person" => { - "name" => "Eloy Duran", - "projects_attributes" => { - "0" => { "name" => "Project 1" }, - "1" => { "name" => "Project 2" } - } - } -} -``` - -You can basically see the `projects_attributes` hash as an array of attribute hashes, one for each model instance. - -NOTE: The reason that `fields_for` constructed a hash instead of an array is that it won't work for any form nested deeper than one level deep. - -TIP: You _can_ however pass an array to the writer method generated by `accepts_nested_attributes_for` if you're using plain Ruby or some other API access. See (TODO) for more info and example. diff --git a/guides/source/profiling.md b/guides/source/profiling.md deleted file mode 100644 index ce093f78ba..0000000000 --- a/guides/source/profiling.md +++ /dev/null @@ -1,16 +0,0 @@ -*DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.** - -A Guide to Profiling Rails Applications -======================================= - -This guide covers built-in mechanisms in Rails for profiling your application. - -After reading this guide, you will know: - -* Rails profiling terminology. -* How to write benchmark tests for your application. -* Other benchmarking approaches and plugins. - --------------------------------------------------------------------------------- - - diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index f25b185fb5..cef8450ee4 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -110,11 +110,12 @@ use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId +use ActionDispatch::RemoteIp +use Sprockets::Rails::QuietAssets use Rails::Rack::Logger use ActionDispatch::ShowExceptions use WebConsole::Middleware use ActionDispatch::DebugExceptions -use ActionDispatch::RemoteIp use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending @@ -124,7 +125,7 @@ use ActionDispatch::Flash use Rack::Head use Rack::ConditionalGet use Rack::ETag -run Rails.application.routes +run MyApp.application.routes ``` The default middlewares shown here (and some others) are each summarized in the [Internal Middlewares](#internal-middleware-stack) section, below. @@ -238,6 +239,14 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#request_id` method. +**`ActionDispatch::RemoteIp`** + +* Checks for IP spoofing attacks. + +**`Sprockets::Rails::QuietAssets`** + +* Suppresses logger output for asset requests. + **`Rails::Rack::Logger`** * Notifies the logs that the request has began. After request is complete, flushes all the logs. @@ -250,10 +259,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol * Responsible for logging exceptions and showing a debugging page in case the request is local. -**`ActionDispatch::RemoteIp`** - -* Checks for IP spoofing attacks. - **`ActionDispatch::Reloader`** * Provides prepare and cleanup callbacks, intended to assist with code reloading during development. diff --git a/guides/source/security.md b/guides/source/security.md index c305350243..1fcb2fc91f 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -796,7 +796,7 @@ In December 2006, 34,000 actual user names and passwords were stolen in a [MySpa INFO: _CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application._ -CSS Injection is explained best by the well-known [MySpace Samy worm](http://namb.la/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. +CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. MySpace blocked many tags, but allowed CSS. So the worm's author put JavaScript into CSS like this: diff --git a/guides/source/testing.md b/guides/source/testing.md index 71fb6fb0e8..c0394f927e 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -610,9 +610,9 @@ For creating Rails system tests, you use the `test/system` directory in your application. Rails provides a generator to create a system test skeleton for you. ```bash -$ bin/rails generate system_test users_create +$ bin/rails generate system_test users invoke test_unit - create test/system/users_creates_test.rb + create test/system/users_test.rb ``` Here's what a freshly-generated system test looks like: @@ -620,11 +620,11 @@ Here's what a freshly-generated system test looks like: ```ruby require "application_system_test_case" -class UsersCreatesTest < ApplicationSystemTestCase +class UsersTest < ApplicationSystemTestCase # test "visiting the index" do - # visit users_creates_url + # visit users_url # - # assert_selector "h1", text: "UsersCreate" + # assert_selector "h1", text: "Users" # end end ``` @@ -658,8 +658,8 @@ end The driver name is a required argument for `driven_by`. The optional arguments that can be passed to `driven_by` are `:using` for the browser (this will only -be used for non-headless drivers like Selenium), and `:screen_size` to change -the size of the screen for screenshots. +be used by Selenium), and `:screen_size` to change the size of the screen for +screenshots. ```ruby require "test_helper" @@ -1406,7 +1406,7 @@ In order to test that your mailer is working as expected, you can use unit tests For the purposes of unit testing a mailer, fixtures are used to provide an example of how the output _should_ look. Because these are example emails, and not Active Record data like the other fixtures, they are kept in their own subdirectory apart from the other fixtures. The name of the directory within `test/fixtures` directly corresponds to the name of the mailer. So, for a mailer named `UserMailer`, the fixtures should reside in `test/fixtures/user_mailer` directory. -When you generated your mailer, the generator creates stub fixtures for each of the mailers actions. If you didn't use the generator, you'll have to create those files yourself. +If you generated your mailer, the generator does not create stub fixtures for the mailers actions. You'll have to create those files yourself as described above. #### The Basic Test Case diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 047aeae37d..290f2a509b 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -250,7 +250,7 @@ Since it's just a `<form>`, all of the information on `form_with` also applies. ### Customize remote elements It is possible to customize the behavior of elements with a `data-remote` -attribute without writing a line of JavaScript. Your can specify extra `data-` +attribute without writing a line of JavaScript. You can specify extra `data-` attributes to accomplish this. #### `data-method` @@ -351,7 +351,7 @@ that have a `data-remote` attribute: NOTE: All handlers bound to these events are always passed the event object as the first argument. The table below describes the extra parameters passed after the -event argument. For exemple, if the extra parameters are listed as `xhr, settings`, +event argument. For example, if the extra parameters are listed as `xhr, settings`, then to access them, you would define your handler with `function(event, xhr, settings)`. | Event name | Extra parameters | Fired | diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index e58086ce93..d93c532c7e 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,9 @@ +* Allow irb options to be passed from `rails console` command. + + Fixes #28988. + + *Yuji Yaginuma* + * Added a shared section to config/database.yml that will be loaded for all environments. *Pierre Schambacher* diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 27c1572357..4dc9a431f6 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -77,9 +77,17 @@ module Rails assets.unknown_asset_fallback = false end + if respond_to?(:action_view) + action_view.form_with_generates_remote_forms = true + end + when "5.2" load_defaults "5.1" + if respond_to?(:active_record) + active_record.cache_versioning = true + end + else raise "Unknown version #{target_version.to_s.inspect}" end diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb index 62e3aa19df..ec58540923 100644 --- a/railties/lib/rails/commands/console/console_command.rb +++ b/railties/lib/rails/commands/console/console_command.rb @@ -73,14 +73,26 @@ module Rails class_option :environment, aliases: "-e", type: :string, desc: "Specifies the environment to run this console under (test/development/production)." + def initialize(args = [], local_options = {}, config = {}) + console_options = [] + + # For the same behavior as OptionParser, leave only options after "--" in ARGV. + termination = local_options.find_index("--") + if termination + console_options = local_options[termination + 1..-1] + local_options = local_options[0...termination] + end + + ARGV.replace(console_options) + super(args, local_options, config) + end + def perform extract_environment_option_from_argument # RAILS_ENV needs to be set before config/application is required. ENV["RAILS_ENV"] = options[:environment] - ARGV.clear # Clear ARGV so IRB doesn't freak. - require_application_and_environment! Rails::Console.start(Rails.application, options) end diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE index c5f8ab72bb..8eb98319d2 100644 --- a/railties/lib/rails/commands/help/USAGE +++ b/railties/lib/rails/commands/help/USAGE @@ -1,13 +1,14 @@ The most common rails commands are: - generate Generate new code (short-cut alias: "g") - console Start the Rails console (short-cut alias: "c") - server Start the Rails server (short-cut alias: "s") - test Run tests (short-cut alias: "t") - dbconsole Start a console for the database specified in config/database.yml - (short-cut alias: "db") + generate Generate new code (short-cut alias: "g") + console Start the Rails console (short-cut alias: "c") + server Start the Rails server (short-cut alias: "s") + test Run tests except system tests (short-cut alias: "t") + test:system Run system tests + dbconsole Start a console for the database specified in config/database.yml + (short-cut alias: "db") <% unless engine? %> - new Create a new Rails application. "rails new my_app" creates a - new application called MyApp in "./my_app" + new Create a new Rails application. "rails new my_app" creates a + new application called MyApp in "./my_app" <% end %> All commands can be run with -h (or --help) for more information. diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 669514b37e..4af84885bc 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -121,7 +121,6 @@ module Rails action_cable_config_exist = File.exist?("config/cable.yml") rack_cors_config_exist = File.exist?("config/initializers/cors.rb") assets_config_exist = File.exist?("config/initializers/assets.rb") - new_framework_defaults_5_1_exist = File.exist?("config/initializers/new_framework_defaults_5_1.rb") config @@ -145,12 +144,6 @@ module Rails unless assets_config_exist remove_file "config/initializers/assets.rb" end - - # Sprockets owns the only new default for 5.1: - # In API-only Applications, we don't want the file. - unless new_framework_defaults_5_1_exist - remove_file "config/initializers/new_framework_defaults_5_1.rb" - end end end @@ -401,7 +394,7 @@ module Rails def delete_new_framework_defaults unless options[:update] - remove_file "config/initializers/new_framework_defaults_5_1.rb" + remove_file "config/initializers/new_framework_defaults_5_2.rb" end end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt deleted file mode 100644 index a0c7f44b60..0000000000 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_1.rb.tt +++ /dev/null @@ -1,16 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.1 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make `form_with` generate non-remote forms. -Rails.application.config.action_view.form_with_generates_remote_forms = false -<%- unless options[:skip_sprockets] -%> - -# Unknown asset fallback will return the path passed in when the given -# asset is not present in the asset pipeline. -# Rails.application.config.assets.unknown_asset_fallback = false -<%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt new file mode 100644 index 0000000000..52c08500d8 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt @@ -0,0 +1,11 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.2 upgrade. +# +# Once upgraded flip defaults one by one to migrate to the new default. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Make Active Record use stable #cache_key alongside new #cache_version method. +# This is needed for recyclable cache keys. +# Rails.application.config.active_record.cache_versioning = true diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb index 9cc3f73a9c..443e743421 100644 --- a/railties/lib/rails/test_unit/railtie.rb +++ b/railties/lib/rails/test_unit/railtie.rb @@ -1,7 +1,7 @@ require "rails/test_unit/line_filtering" if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any? - ENV["RAILS_ENV"] ||= "test" + ENV["RAILS_ENV"] ||= Rake.application.options.show_tasks ? "development" : "test" end module Rails diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index ef19bd7626..33408081f1 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -48,6 +48,7 @@ namespace :test do Minitest.rake_run(["test/controllers", "test/mailers", "test/functional"]) end + desc "Run system tests only" task system: "test:prepare" do $: << "test" Minitest.rake_run(["test/system"]) diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 72f340df34..057d473870 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -136,9 +136,9 @@ class FullStackConsoleTest < ActiveSupport::TestCase assert_output "> " end - def spawn_console + def spawn_console(options) Process.spawn( - "#{app_path}/bin/rails console --sandbox", + "#{app_path}/bin/rails console #{options}", in: @slave, out: @slave, err: @slave ) @@ -146,18 +146,26 @@ class FullStackConsoleTest < ActiveSupport::TestCase end def test_sandbox - spawn_console + spawn_console("--sandbox") write_prompt "Post.count", "=> 0" write_prompt "Post.create" write_prompt "Post.count", "=> 1" @master.puts "quit" - spawn_console + spawn_console("--sandbox") write_prompt "Post.count", "=> 0" write_prompt "Post.transaction { Post.create; raise }" write_prompt "Post.count", "=> 0" @master.puts "quit" end + + def test_environment_option_and_irb_option + spawn_console("test -- --verbose") + + write_prompt "a = 1", "a = 1" + write_prompt "puts Rails.env", "puts Rails.env\r\ntest" + @master.puts "quit" + end end diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb index 6c003e9bcc..6e6996a6ba 100644 --- a/railties/test/application/per_request_digest_cache_test.rb +++ b/railties/test/application/per_request_digest_cache_test.rb @@ -18,6 +18,10 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase class Customer < Struct.new(:name, :id) extend ActiveModel::Naming include ActiveModel::Conversion + + def cache_key + [ name, id ].join("/") + end end RUBY diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index 2edb39c8e8..a19e0f0dd8 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -70,7 +70,6 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase assert_no_file "config/initializers/cookies_serializer.rb" assert_no_file "config/initializers/assets.rb" - assert_no_file "config/initializers/new_framework_defaults_5_1.rb" end def test_app_update_does_not_generate_unnecessary_bin_files diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index ed8eea3243..ff829eb197 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -157,7 +157,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_new_application_doesnt_need_defaults - assert_no_file "config/initializers/new_framework_defaults_5_1.rb" + assert_no_file "config/initializers/new_framework_defaults_5_2.rb" end def test_new_application_load_defaults @@ -203,14 +203,14 @@ class AppGeneratorTest < Rails::Generators::TestCase app_root = File.join(destination_root, "myapp") run_generator [app_root] - assert_no_file "#{app_root}/config/initializers/new_framework_defaults_5_1.rb" + assert_no_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb" stub_rails_application(app_root) do generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, destination_root: app_root, shell: @shell generator.send(:app_const) quietly { generator.send(:update_config_files) } - assert_file "#{app_root}/config/initializers/new_framework_defaults_5_1.rb" + assert_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb" end end |