From acdba1c6a653bf5c787d3457af95b37708be1e2b Mon Sep 17 00:00:00 2001 From: Jack McCracken <jack.mccracken@shopify.com> Date: Mon, 2 Oct 2017 16:35:13 -0400 Subject: Add a better error message when a "null" Origin header occurs --- .../action_controller/metal/request_forgery_protection.rb | 10 ++++++++++ .../test/controller/request_forgery_protection_test.rb | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index d6cd5fd9e0..b2e6f86eeb 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -414,11 +414,21 @@ module ActionController #:nodoc: allow_forgery_protection end + NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc + The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually + means you have the 'no-referrer' Referrer-Policy header enabled, or that you the request came from a site that + refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the + best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin. + If you cannot change the referrer policy, you can disable origin checking with the + Rails.application.config.action_controller.forgery_protection_origin_check setting. + MSG + # Checks if the request originated from the same origin by looking at the # Origin header. def valid_request_origin? # :doc: if forgery_protection_origin_check # We accept blank origin headers because some user agents don't send it. + raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null" request.origin.nil? || request.origin == request.base_url else true diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index eb3d2f34a8..4822d85bcb 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -446,6 +446,19 @@ module RequestForgeryProtectionTests end end + def test_should_raise_for_post_with_null_origin + forgery_protection_origin_check do + session[:_csrf_token] = @token + @controller.stub :form_authenticity_token, @token do + exception = assert_raises(ActionController::InvalidAuthenticityToken) do + @request.set_header "HTTP_ORIGIN", "null" + post :index, params: { custom_authenticity_token: @token } + end + assert_match "The browser returned a 'null' origin for a request", exception.message + end + end + end + def test_should_block_post_with_origin_checking_and_wrong_origin old_logger = ActionController::Base.logger logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new -- cgit v1.2.3 From 84cad15213ea5447ea0890a4f017b44ad85b901a Mon Sep 17 00:00:00 2001 From: Matthew Draper <matthew@trebex.net> Date: Mon, 27 Nov 2017 23:39:30 +1030 Subject: Drop the before_fork/on_worker_boot advice It's no longer required for Active Record, and other common libraries (dalli, redis-rb) all seem to be fork-proof too. --- .../rails/app/templates/config/puma.rb.tt | 24 +--------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt index 1e19380dcb..a5eccf816b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt @@ -26,31 +26,9 @@ environment ENV.fetch("RAILS_ENV") { "development" } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. If you use this option -# you need to make sure to reconnect any threads in the `on_worker_boot` -# block. +# process behavior so workers use less memory. # # preload_app! -# If you are preloading your application and using Active Record, it's -# recommended that you close any connections to the database before workers -# are forked to prevent connection leakage. -# -# before_fork do -# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) -# end - -# The code in the `on_worker_boot` will be called if you are using -# clustered mode by specifying a number of `workers`. After each worker -# process is booted, this block will be run. If you are using the `preload_app!` -# option, you will want to use this block to reconnect to any threads -# or connections that may have been created at application boot, as Ruby -# cannot share connections between processes. -# -# on_worker_boot do -# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) -# end -# - # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart -- cgit v1.2.3 From 68b2573e33668a4f391c7cf1924c4183e75906e6 Mon Sep 17 00:00:00 2001 From: Michael Coyne <mikeycgto@gmail.com> Date: Sun, 12 Nov 2017 15:19:33 -0500 Subject: Update cookie_store_test to use encrypted cookies This now modernizes these tests to use encrypted cookies instead of using secret_token HMACs. This commit also adds a tests to ensure session cookies with :expires_after set are invalidated and no longer accepted when the time has elapsed. --- .../test/dispatch/session/cookie_store_test.rb | 124 ++++++++++++++------- 1 file changed, 83 insertions(+), 41 deletions(-) diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 2c43a8c29f..7f70961465 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -8,11 +8,13 @@ require "active_support/messages/rotation_configuration" class CookieStoreTest < ActionDispatch::IntegrationTest SessionKey = "_myapp_session" SessionSecret = "b3c631c314c0bbca50c1b2843150fe33" - Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret) + SessionSalt = "authenticated encrypted cookie" + + Generator = ActiveSupport::KeyGenerator.new(SessionSecret, iterations: 1000) Rotations = ActiveSupport::Messages::RotationConfiguration.new - Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, digest: "SHA1") - SignedBar = Verifier.generate(foo: "bar", session_id: SecureRandom.hex(16)) + Encryptor = ActiveSupport::MessageEncryptor.new \ + Generator.generate_key(SessionSalt, 32), cipher: "aes-256-gcm", serializer: Marshal class TestController < ActionController::Base def no_session_access @@ -25,12 +27,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def set_session_value session[:foo] = "bar" - render plain: Rack::Utils.escape(Verifier.generate(session.to_hash)) - end - - def set_session_value_expires_in_five_hours - session[:foo] = "bar" - render plain: Rack::Utils.escape(Verifier.generate(session.to_hash, expires_in: 5.hours)) + render body: nil end def get_session_value @@ -72,19 +69,35 @@ class CookieStoreTest < ActionDispatch::IntegrationTest end end + def parse_cookie_from_header + cookie_matches = headers["Set-Cookie"].match(/#{SessionKey}=([^;]+)/) + cookie_matches && cookie_matches[1] + end + + def assert_session_cookie(cookie_string, contents) + assert_includes headers["Set-Cookie"], cookie_string + + session_value = parse_cookie_from_header + session_data = Encryptor.decrypt_and_verify(Rack::Utils.unescape(session_value)) rescue nil + + assert_not_nil session_data, "session failed to decrypt" + assert_equal session_data.slice(*contents.keys), contents + end + def test_setting_session_value with_test_route_set do get "/set_session_value" + assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" end end def test_getting_session_value with_test_route_set do - cookies[SessionKey] = SignedBar + get "/set_session_value" get "/get_session_value" + assert_response :success assert_equal 'foo: "bar"', response.body end @@ -92,8 +105,9 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_getting_session_id with_test_route_set do - cookies[SessionKey] = SignedBar + get "/set_session_value" get "/persistent_session_id" + assert_response :success assert_equal 32, response.body.size session_id = response.body @@ -106,8 +120,12 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_disregards_tampered_sessions with_test_route_set do - cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--123456780" + encryptor = ActiveSupport::MessageEncryptor.new("A" * 32, cipher: "aes-256-gcm", serializer: Marshal) + + cookies[SessionKey] = encryptor.encrypt_and_sign("foo" => "bar", "session_id" => "abc") + get "/get_session_value" + assert_response :success assert_equal "foo: nil", response.body end @@ -135,19 +153,19 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_does_set_secure_cookies_over_https with_test_route_set(secure: true) do get "/set_session_value", headers: { "HTTPS" => "on" } + assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; secure; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; secure; HttpOnly", "foo" => "bar" end end # {:foo=>#<SessionAutoloadTest::Foo bar:"baz">, :session_id=>"ce8b0752a6ab7c7af3cdb8a80e6b9e46"} - SignedSerializedCookie = "BAh7BzoIZm9vbzodU2Vzc2lvbkF1dG9sb2FkVGVzdDo6Rm9vBjoJQGJhciIIYmF6Og9zZXNzaW9uX2lkIiVjZThiMDc1MmE2YWI3YzdhZjNjZGI4YTgwZTZiOWU0Ng==--2bf3af1ae8bd4e52b9ac2099258ace0c380e601c" + EncryptedSerializedCookie = "9RZ2Fij0qLveUwM4s+CCjGqhpjyUC8jiBIf/AiBr9M3TB8xh2vQZtvSOMfN3uf6oYbbpIDHAcOFIEl69FcW1ozQYeSrCLonYCazoh34ZdYskIQfGwCiSYleVXG1OD9Z4jFqeVArw4Ewm0paOOPLbN1rc6A==--I359v/KWdZ1ok0ey--JFFhuPOY7WUo6tB/eP05Aw==" def test_deserializes_unloaded_classes_on_get_id with_test_route_set do with_autoload_path "session_autoload_test" do - cookies[SessionKey] = SignedSerializedCookie + cookies[SessionKey] = EncryptedSerializedCookie get "/get_session_id" assert_response :success assert_equal "id: ce8b0752a6ab7c7af3cdb8a80e6b9e46", response.body, "should auto-load unloaded class" @@ -158,7 +176,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_deserializes_unloaded_classes_on_get_value with_test_route_set do with_autoload_path "session_autoload_test" do - cookies[SessionKey] = SignedSerializedCookie + cookies[SessionKey] = EncryptedSerializedCookie get "/get_session_value" assert_response :success assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class" @@ -197,8 +215,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest get "/set_session_value" assert_response :success session_payload = response.body - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" get "/call_reset_session" assert_response :success @@ -216,8 +233,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set do get "/set_session_value" assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" get "/get_class_after_reset_session" assert_response :success @@ -239,8 +255,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set do get "/set_session_value" assert_response :success - assert_equal "_myapp_session=#{response.body}; path=/; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; HttpOnly", "foo" => "bar" get "/call_session_clear" assert_response :success @@ -253,7 +268,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_persistent_session_id with_test_route_set do - cookies[SessionKey] = SignedBar + get "/set_session_value" get "/persistent_session_id" assert_response :success assert_equal 32, response.body.size @@ -268,8 +283,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_setting_session_id_to_nil_is_respected with_test_route_set do - cookies[SessionKey] = SignedBar - + get "/set_session_value" get "/get_session_id" sid = response.body assert_equal 36, sid.size @@ -283,31 +297,53 @@ class CookieStoreTest < ActionDispatch::IntegrationTest with_test_route_set(expire_after: 5.hours) do # First request accesses the session time = Time.local(2008, 4, 24) + Time.stub :now, time do expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") - cookies[SessionKey] = SignedBar + get "/set_session_value" - get "/set_session_value_expires_in_five_hours" assert_response :success - - cookie_body = response.body - assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly", - headers["Set-Cookie"] + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" end # Second request does not access the session - time = Time.local(2008, 4, 25) + time = time + 3.hours Time.stub :now, time do expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") - cookies[SessionKey] = SignedBar - get "/no_session_access" + assert_response :success + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" + end + end + end + + def test_session_store_with_expire_after_does_not_accept_expired_session + with_test_route_set(expire_after: 5.hours) do + # First request accesses the session + time = Time.local(2017, 11, 12) + + Time.stub :now, time do + expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000") - assert_equal "_myapp_session=#{cookies[SessionKey]}; path=/; expires=#{expected_expiry}; HttpOnly", - headers["Set-Cookie"] + get "/set_session_value" + get "/get_session_value" + + assert_response :success + assert_equal 'foo: "bar"', response.body + assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar" + end + + # Second request is beyond the expiry time and the session is invalidated + time += 5.hours + 1.minute + + Time.stub :now, time do + get "/get_session_value" + + assert_response :success + assert_equal "foo: nil", response.body end end end @@ -347,8 +383,14 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def get(path, *args) args[0] ||= {} args[0][:headers] ||= {} - args[0][:headers]["action_dispatch.key_generator"] ||= Generator - args[0][:headers]["action_dispatch.cookies_rotations"] ||= Rotations + args[0][:headers].tap do |config| + config["action_dispatch.secret_key_base"] = SessionSecret + config["action_dispatch.authenticated_encrypted_cookie_salt"] = SessionSalt + config["action_dispatch.use_authenticated_cookie_encryption"] = true + + config["action_dispatch.key_generator"] ||= Generator + config["action_dispatch.cookies_rotations"] ||= Rotations + end super(path, *args) end -- cgit v1.2.3 From 5b2e826e6d5a9031133669be03e3b04861c17e4b Mon Sep 17 00:00:00 2001 From: Noah Davis <noah@codeclimate.com> Date: Wed, 29 Nov 2017 23:16:04 -0500 Subject: Keep current Code Climate behavior before upgrade If you merge these changes now (*before Monday, December 4th*), Code Climate will run the same analysis on Rails as it always has on your pull requests. *If you do not merge these changes*, when Code Climate migrates Rails to our new analysis, Code Climate will continue to run Rubocop, but it will ALSO run Code Climate's new maintainability checks (https://codeclimate.com/blog/10-point-technical-debt-assessment). You may want this ... you may not. Just want to make sure you knew the quick option to disable them if it's a problem! --- .codeclimate.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.codeclimate.yml b/.codeclimate.yml index 1f0f067c5b..d59a0780d1 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,3 +1,25 @@ +checks: + argument-count: + enabled: false + complex-logic: + enabled: false + file-lines: + enabled: false + method-complexity: + enabled: false + method-count: + enabled: false + method-lines: + enabled: false + nested-control-flow: + enabled: false + return-statements: + enabled: false + similar-code: + enabled: false + identical-code: + enabled: false + engines: rubocop: enabled: true -- cgit v1.2.3 From b6baf0c88411824ce99f1ad4b9de64fa37ad96ea Mon Sep 17 00:00:00 2001 From: Yauheni Dakuka <yauheni.dakuka@gmail.com> Date: Thu, 30 Nov 2017 09:25:50 +0300 Subject: Cosmetic changes [ci skip] --- guides/source/3_2_release_notes.md | 4 ++-- guides/source/4_1_release_notes.md | 2 +- guides/source/4_2_release_notes.md | 2 +- guides/source/action_view_overview.md | 4 ++-- guides/source/asset_pipeline.md | 8 +++---- guides/source/caching_with_rails.md | 2 +- guides/source/i18n.md | 2 +- guides/source/initialization.md | 2 +- guides/source/testing.md | 2 +- guides/source/upgrading_ruby_on_rails.md | 26 +++++++++++------------ guides/source/working_with_javascript_in_rails.md | 2 +- 11 files changed, 28 insertions(+), 28 deletions(-) diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md index f6571544f9..ae6eb27f35 100644 --- a/guides/source/3_2_release_notes.md +++ b/guides/source/3_2_release_notes.md @@ -36,7 +36,7 @@ TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. * `coffee-rails ~> 3.2.1` * `uglifier >= 1.0.3` -* Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. +* Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. You can start replacing these plugins by extracting them as gems and adding them in your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. * There are a couple of new configuration changes you'd want to add in `config/environments/development.rb`: @@ -156,7 +156,7 @@ Railties will create indexes for `title` and `author` with the latter being a unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively. -* Turn gem has been removed from default Gemfile. +* Turn gem has been removed from default `Gemfile`. * Remove old plugin generator `rails generate plugin` in favor of `rails plugin new` command. diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index 6bf65757ec..2c5e665e33 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -274,7 +274,7 @@ for detailed changes. * The [Spring application preloader](https://github.com/rails/spring) is now installed by default for new applications. It uses the development group of - the Gemfile, so will not be installed in + the `Gemfile`, so will not be installed in production. ([Pull Request](https://github.com/rails/rails/pull/12958)) * `BACKTRACE` environment variable to show unfiltered backtraces for test diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index 036a310ac8..7105df5634 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -368,7 +368,7 @@ Please refer to the [Changelog][railties] for detailed changes. ### Notable changes -* Introduced `web-console` in the default application Gemfile. +* Introduced `web-console` in the default application `Gemfile`. ([Pull Request](https://github.com/rails/rails/pull/11667)) * Added a `required` option to the model generator for associations. diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index c1e02745de..fde2040173 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -149,10 +149,10 @@ end #### Jbuilder [Jbuilder](https://github.com/rails/jbuilder) is a gem that's -maintained by the Rails team and included in the default Rails Gemfile. +maintained by the Rails team and included in the default Rails `Gemfile`. It's similar to Builder, but is used to generate JSON, instead of XML. -If you don't have it, you can add the following to your Gemfile: +If you don't have it, you can add the following to your `Gemfile`: ```ruby gem 'jbuilder' diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 805b0f0d62..e6d5aed135 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -35,7 +35,7 @@ rails new appname --skip-sprockets ``` Rails automatically adds the `sass-rails`, `coffee-rails` and `uglifier` -gems to your Gemfile, which are used by Sprockets for asset compression: +gems to your `Gemfile`, which are used by Sprockets for asset compression: ```ruby gem 'sass-rails' @@ -44,8 +44,8 @@ gem 'coffee-rails' ``` Using the `--skip-sprockets` option will prevent Rails from adding -them to your Gemfile, so if you later want to enable -the asset pipeline you will have to add those gems to your Gemfile. Also, +them to your `Gemfile`, so if you later want to enable +the asset pipeline you will have to add those gems to your `Gemfile`. Also, creating an application with the `--skip-sprockets` option will generate a slightly different `config/application.rb` file, with a require statement for the sprockets railtie that is commented-out. You will have to remove @@ -850,7 +850,7 @@ This mode uses more memory, performs more poorly than the default and is not recommended. If you are deploying a production application to a system without any -pre-existing JavaScript runtimes, you may want to add one to your Gemfile: +pre-existing JavaScript runtimes, you may want to add one to your `Gemfile`: ```ruby group :production do diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 31bc478015..780e69c146 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -32,7 +32,7 @@ Basic Caching This is an introduction to three types of caching techniques: page, action and fragment caching. By default Rails provides fragment caching. In order to use page and action caching you will need to add `actionpack-page_caching` and -`actionpack-action_caching` to your Gemfile. +`actionpack-action_caching` to your `Gemfile`. By default, caching is only enabled in your production environment. To play around with caching locally you'll want to enable caching in your local diff --git a/guides/source/i18n.md b/guides/source/i18n.md index e6aa6181cc..2b545e6b82 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -977,7 +977,7 @@ en: ``` NOTE: In order to use this helper, you need to install [DynamicForm](https://github.com/joelmoss/dynamic_form) -gem by adding this line to your Gemfile: `gem 'dynamic_form'`. +gem by adding this line to your `Gemfile`: `gem 'dynamic_form'`. ### Translations for Action Mailer E-Mail Subjects diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 1541ea38cd..c4f1df487b 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -93,7 +93,7 @@ require 'bundler/setup' # Set up gems listed in the Gemfile. In a standard Rails application, there's a `Gemfile` which declares all dependencies of the application. `config/boot.rb` sets -`ENV['BUNDLE_GEMFILE']` to the location of this file. If the Gemfile +`ENV['BUNDLE_GEMFILE']` to the location of this file. If the `Gemfile` exists, then `bundler/setup` is required. The require is used by Bundler to configure the load path for your Gemfile's dependencies. diff --git a/guides/source/testing.md b/guides/source/testing.md index 8416fd163d..e0a2d281d9 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -645,7 +645,7 @@ system tests should live. If you want to change the default settings you can change what the system tests are "driven by". Say you want to change the driver from Selenium to -Poltergeist. First add the `poltergeist` gem to your Gemfile. Then in your +Poltergeist. First add the `poltergeist` gem to your `Gemfile`. Then in your `application_system_test_case.rb` file do the following: ```ruby diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 9bc87e4bf0..eae73c3e1b 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -45,7 +45,7 @@ TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterp ### The Update Task Rails provides the `app:update` task (`rake rails:update` on 4.2 and earlier). After updating the Rails version -in the Gemfile, run this task. +in the `Gemfile`, run this task. This will help you with the creation of new files and changes of old files in an interactive session. @@ -179,7 +179,7 @@ See [#19034](https://github.com/rails/rails/pull/19034) for more details. `assigns` and `assert_template` have been extracted to the `rails-controller-testing` gem. To continue using these methods in your controller tests, add `gem 'rails-controller-testing'` to -your Gemfile. +your `Gemfile`. If you are using Rspec for testing, please see the extra configuration required in the gem's documentation. @@ -212,7 +212,7 @@ true. `ActiveModel::Serializers::Xml` has been extracted from Rails to the `activemodel-serializers-xml` gem. To continue using XML serialization in your application, add `gem 'activemodel-serializers-xml'` -to your Gemfile. +to your `Gemfile`. ### Removed Support for Legacy `mysql` Database Adapter @@ -278,7 +278,7 @@ You can now just call the dependency once with a wildcard. ### `ActionView::Helpers::RecordTagHelper` moved to external gem (record_tag_helper) -`content_tag_for` and `div_for` have been removed in favor of just using `content_tag`. To continue using the older methods, add the `record_tag_helper` gem to your Gemfile: +`content_tag_for` and `div_for` have been removed in favor of just using `content_tag`. To continue using the older methods, add the `record_tag_helper` gem to your `Gemfile`: ```ruby gem 'record_tag_helper', '~> 1.0' @@ -415,7 +415,7 @@ First, add `gem 'web-console', '~> 2.0'` to the `:development` group in your `Ge ### Responders -`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your Gemfile. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies: +`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your `Gemfile`. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies: ```ruby # app/controllers/users_controller.rb @@ -559,7 +559,7 @@ Read the [gem's readme](https://github.com/rails/rails-html-sanitizer) for more The documentation for `PermitScrubber` and `TargetScrubber` explains how you can gain complete control over when and how elements should be stripped. -If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your Gemfile: +If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your `Gemfile`: ```ruby gem 'rails-deprecated_sanitizer' @@ -617,7 +617,7 @@ migration DSL counterpart. The migration procedure is as follows: -1. remove `gem "foreigner"` from the Gemfile. +1. remove `gem "foreigner"` from the `Gemfile`. 2. run `bundle install`. 3. run `bin/rake db:schema:dump`. 4. make sure that `db/schema.rb` contains every foreign key definition with @@ -769,7 +769,7 @@ and has been removed from Rails. If your application currently depends on MultiJSON directly, you have a few options: -1. Add 'multi_json' to your Gemfile. Note that this might cease to work in the future +1. Add 'multi_json' to your `Gemfile`. Note that this might cease to work in the future 2. Migrate away from MultiJSON by using `obj.to_json`, and `JSON.parse(str)` instead. @@ -810,7 +810,7 @@ part of the rewrite, the following features have been removed from the encoder: If your application depends on one of these features, you can get them back by adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder) -gem to your Gemfile. +gem to your `Gemfile`. #### JSON representation of Time objects @@ -1135,7 +1135,7 @@ full support for the last few changes in the specification. ### Gemfile -Rails 4.0 removed the `assets` group from Gemfile. You'd need to remove that +Rails 4.0 removed the `assets` group from `Gemfile`. You'd need to remove that line from your `Gemfile` when upgrading. You should also update your application file (in `config/application.rb`): @@ -1147,7 +1147,7 @@ Bundler.require(*Rails.groups) ### vendor/plugins -Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must replace any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. +Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must replace any plugins by extracting them to gems and adding them to your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. ### Active Record @@ -1214,7 +1214,7 @@ end ### Active Resource -Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile. +Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your `Gemfile`. ### Active Model @@ -1414,7 +1414,7 @@ config.active_record.mass_assignment_sanitizer = :strict ### vendor/plugins -Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. +Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your `Gemfile`. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`. ### Active Record diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 86746a5ae0..c3dff1772c 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -492,7 +492,7 @@ replace the entire `<body>` of the page with the `<body>` of the response. It will then use PushState to change the URL to the correct one, preserving refresh semantics and giving you pretty URLs. -The only thing you have to do to enable Turbolinks is have it in your Gemfile, +The only thing you have to do to enable Turbolinks is have it in your `Gemfile`, and put `//= require turbolinks` in your JavaScript manifest, which is usually `app/assets/javascripts/application.js`. -- cgit v1.2.3 From 1dca75c2c8f930b58d86cd2216af5e14307b3e53 Mon Sep 17 00:00:00 2001 From: Greg Navis <contact@gregnavis.com> Date: Sat, 13 May 2017 03:09:58 +0200 Subject: Add support for PostgreSQL operator classes to add_index Add support for specifying non-default operator classes in PostgreSQL indexes. An example CREATE INDEX query that becomes possible is: CREATE INDEX users_name ON users USING gist (name gist_trgm_ops); Previously it was possible to specify the `gist` index but not the custom operator class. The `add_index` call for the above query is: add_index :users, :name, using: :gist, opclasses: {name: :gist_trgm_ops} --- activerecord/CHANGELOG.md | 8 ++++++ .../abstract/schema_definitions.rb | 4 ++- .../abstract/schema_statements.rb | 28 +++++++++++++++++-- .../postgresql/schema_statements.rb | 32 ++++++++++++++-------- .../connection_adapters/postgresql_adapter.rb | 17 ++++++++++++ activerecord/lib/active_record/schema_dumper.rb | 1 + .../adapters/postgresql/active_schema_test.rb | 3 ++ .../adapters/postgresql/postgresql_adapter_test.rb | 10 +++---- .../test/cases/adapters/postgresql/schema_test.rb | 32 ++++++++++++++++++++++ 9 files changed, 115 insertions(+), 20 deletions(-) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 60ceffac5e..048a1eeb2f 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,11 @@ +* Add support for PostgreSQL operator classes to `add_index`. + + Example: + + add_index :users, :name, using: :gist, opclass: name: :gist_trgm_ops + + *Greg Navis* + * Don't allow scopes to be defined which conflict with instance methods on `Relation`. Fixes #31120. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index be2f625d74..b66009bdc6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,7 +6,7 @@ module ActiveRecord # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes class IndexDefinition # :nodoc: - attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment + attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclass, :comment def initialize( table, name, @@ -17,6 +17,7 @@ module ActiveRecord where: nil, type: nil, using: nil, + opclass: {}, comment: nil ) @table = table @@ -28,6 +29,7 @@ module ActiveRecord @where = where @type = type @using = using + @opclass = opclass @comment = comment end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 9b7345f7c3..2a335e8f2b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -738,6 +738,29 @@ module ActiveRecord # # Note: only supported by PostgreSQL and MySQL # + # ====== Creating an index with a specific operator class + # + # add_index(:developers, :name, using: 'gist', opclass: :gist_trgm_ops) + # + # generates: + # + # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL + # + # add_index(:developers, [:name, :city], using: 'gist', opclass: { city: :gist_trgm_ops }) + # + # generates: + # + # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL + # + # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops }) + # + # generates: + # + # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL + # + # + # Note: only supported by PostgreSQL + # # ====== Creating an index with a specific type # # add_index(:developers, :name, type: :fulltext) @@ -1120,7 +1143,7 @@ module ActiveRecord def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: column_names = index_column_names(column_name) - options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclass) index_type = options[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" @@ -1186,7 +1209,8 @@ module ActiveRecord quoted_columns end - # Overridden by the MySQL adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths and by + # the PostgreSQL adapter for supporting operator classes. def add_options_for_index_columns(quoted_columns, **options) if supports_index_sort_order? quoted_columns = add_index_sort_order(quoted_columns, options) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 846e721983..15a1dfd102 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -87,10 +87,7 @@ module ActiveRecord result = query(<<-SQL, "SCHEMA") SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, - pg_catalog.obj_description(i.oid, 'pg_class') AS comment, - (SELECT COUNT(*) FROM pg_opclass o - JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c - ON o.oid = c.oid WHERE o.opcdefault = 'f') + pg_catalog.obj_description(i.oid, 'pg_class') AS comment FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid @@ -109,11 +106,10 @@ module ActiveRecord inddef = row[3] oid = row[4] comment = row[5] - opclass = row[6] using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten - if indkey.include?(0) || opclass > 0 + if indkey.include?(0) columns = expressions else columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact @@ -123,10 +119,21 @@ module ActiveRecord AND a.attnum IN (#{indkey.join(",")}) SQL - # add info on sort order for columns (only desc order is explicitly specified, asc is the default) - orders = Hash[ - expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] } - ] + orders = {} + opclasses = {} + + # add info on sort order (only desc order is explicitly specified, asc is the default) + # and non-default opclasses + expressions.scan(/(\w+)(?: (?!DESC)(\w+))?(?: (DESC))?/).each do |column, opclass, desc| + opclasses[column] = opclass if opclass + orders[column] = :desc if desc + end + + # Use a string for the opclass description (instead of a hash) when all columns + # have the same opclass specified. + if columns.count == opclasses.count && opclasses.values.uniq.count == 1 + opclasses = opclasses.values.first + end end IndexDefinition.new( @@ -137,6 +144,7 @@ module ActiveRecord orders: orders, where: where, using: using.to_sym, + opclass: opclasses, comment: comment.presence ) end @@ -458,8 +466,8 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) - execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do + index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) + execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}").tap do execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 7b27f6b7a0..eee61780b7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -388,6 +388,23 @@ module ActiveRecord private + def add_index_opclass(column_names, options = {}) + opclass = + if options[:opclass].is_a?(Hash) + options[:opclass].symbolize_keys + else + Hash.new { |hash, column| hash[column] = options[:opclass].to_s } + end + column_names.each do |name, column| + column << " #{opclass[name]}" if opclass[name].present? + end + end + + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_opclass(quoted_columns, options) + super + end + # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" NUMERIC_VALUE_OUT_OF_RANGE = "22003" diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 66f7d29886..8cb2851557 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -189,6 +189,7 @@ HEADER index_parts << "where: #{index.where.inspect}" if index.where index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index) index_parts << "type: #{index.type.inspect}" if index.type + index_parts << "opclass: #{index.opclass.inspect}" if index.opclass.present? index_parts << "comment: #{index.comment.inspect}" if index.comment index_parts end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 9929237546..99c53dadeb 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -59,6 +59,9 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal expected, add_index(:people, "lower(last_name)", using: type, unique: true) end + expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name" bpchar_pattern_ops)) + assert_equal expected, add_index(:people, :last_name, using: :gist, opclass: { last_name: :bpchar_pattern_ops }) + assert_raise ArgumentError do add_index(:people, :last_name, algorithm: :copy) end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index f199519d86..1951230c8a 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -248,12 +248,12 @@ module ActiveRecord def test_index_with_opclass with_example_table do - @connection.add_index "ex", "data varchar_pattern_ops" - index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } - assert_equal "data varchar_pattern_ops", index.columns + @connection.add_index "ex", "data", opclass: "varchar_pattern_ops" + index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" } + assert_equal ["data"], index.columns - @connection.remove_index "ex", "data varchar_pattern_ops" - assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } + @connection.remove_index "ex", "data" + assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" } end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 5a64da028b..a6ae18a826 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -500,6 +500,38 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase end end +class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "trains" do |t| + t.string :name + t.text :description + end + end + + teardown do + @connection.drop_table "trains", if_exists: true + end + + def test_string_opclass_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name text_pattern_ops, description text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: "text_pattern_ops"/, output) + end + + def test_non_default_opclass_is_dumped + @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name, description text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: \{"description"=>"text_pattern_ops"\}/, output) + end +end + class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection -- cgit v1.2.3 From 96d17ecba8e374cd1ce4db0cee8c0dca057e3ff7 Mon Sep 17 00:00:00 2001 From: Dixit Patel <dixitpatel012@aol.com> Date: Thu, 30 Nov 2017 17:05:48 +0530 Subject: [ci skip] Updated links for uncorn which redirect & added https for nginx link --- guides/source/configuring.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 4bfcc1e21a..fee644d4d4 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -1005,11 +1005,11 @@ Deploying your application using a reverse proxy has definite advantages over tr Many modern web servers can be used as a proxy server to balance third-party elements such as caching servers or application servers. -One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy. +One such application server you can use is [Unicorn](https://bogomips.org/unicorn/) to run behind a reverse proxy. In this case, you would need to configure the proxy server (NGINX, Apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead. -You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it. +You can find more information in the [Unicorn readme](https://bogomips.org/unicorn/README.html) and understand the [philosophy](https://bogomips.org/unicorn/PHILOSOPHY.html) behind it. Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your NGINX config may include: @@ -1037,7 +1037,7 @@ server { } ``` -Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information. +Be sure to read the [NGINX documentation](https://nginx.org/en/docs/) for the most up-to-date information. Rails Environment Settings -- cgit v1.2.3 From eb9ff5fd39ca3af20cb95b723ee622ceef10310d Mon Sep 17 00:00:00 2001 From: Anton Rieder <aried3r@gmail.com> Date: Thu, 30 Nov 2017 16:34:06 +0100 Subject: Move system test dependencies to test group --- .../lib/rails/generators/rails/app/templates/Gemfile.tt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index 61026f5182..e3ed3e7c11 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -41,13 +41,6 @@ gem 'bootsnap', '>= 1.1.0', require: false group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] - <%- if depends_on_system_test? -%> - # Adds support for Capybara system testing and selenium driver - gem 'capybara', '~> 2.15' - gem 'selenium-webdriver' - # Easy installation and use of chromedriver to run system tests with Chrome - gem 'chromedriver-helper' - <%- end -%> end group :development do @@ -70,6 +63,16 @@ group :development do <% end -%> <% end -%> end + +<%- if depends_on_system_test? -%> +group :test do + # Adds support for Capybara system testing and selenium driver + gem 'capybara', '~> 2.15' + gem 'selenium-webdriver' + # Easy installation and use of chromedriver to run system tests with Chrome + gem 'chromedriver-helper' +end +<%- end -%> <% end -%> # Windows does not include zoneinfo files, so bundle the tzinfo-data gem -- cgit v1.2.3 From 0185aae747676e636a52eb079a0a10a6f053fa2c Mon Sep 17 00:00:00 2001 From: eileencodes <eileencodes@gmail.com> Date: Thu, 30 Nov 2017 12:26:33 -0500 Subject: Add changelog entry for 9d6e28 Since this changes a default setting a changelog entry is important. --- actionpack/CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index c8fb34ed52..d120d15770 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,15 @@ +* Changed the default system test screenshot output from `inline` to `simple`. + + `inline` works well for iTerm2 but not everyone uses iTerm2. Some terminals like + Terminal.app ignore the `inline` and output the path to the file since it can't + render the image. Other terminals, like those on Ubuntu, cannot handle the image + inline, but also don't handle it gracefully and instead of outputting the file + path, it dumps binary into the terminal. + + Commit 9d6e28 fixes this by changing the default for screenshot to be `simple`. + + *Eileen M. Uchitelle* + * Register most popular audio/video/font mime types supported by modern browsers. *Guillermo Iguaran* -- cgit v1.2.3 From 0dc3b6535dfb8127f568bb4abc2fb09118ce4b96 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Fri, 1 Dec 2017 11:49:36 +0900 Subject: Maintain raising `RecordNotFound` for class level `update` and` destroy` In 35836019, class level `update` and `destroy` suppressed `RecordNotFound` to ensure returning affected objects. But `RecordNotFound` is a common exception caught by a `rescue_from` handler. So changing the behavior when a typical `params[:id]` is passed has a compatibility problem. The previous behavior should not be changed. Fixes #31301. --- activerecord/lib/active_record/persistence.rb | 13 +++++++++---- activerecord/test/cases/persistence_test.rb | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 4e1b05dbf6..17459f2505 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -99,7 +99,11 @@ module ActiveRecord # for updating all records in a single query. def update(id = :all, attributes) if id.is_a?(Array) - id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }.compact + id.map.with_index { |one_id, idx| + object = find_by(primary_key => one_id) + object.update(attributes[idx]) if object + object + }.compact elsif id == :all all.each { |record| record.update(attributes) } else @@ -112,7 +116,6 @@ module ActiveRecord object.update(attributes) object end - rescue RecordNotFound end # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, @@ -136,11 +139,13 @@ module ActiveRecord # Todo.destroy(todos) def destroy(id) if id.is_a?(Array) - id.map { |one_id| destroy(one_id) }.compact + id.map { |one_id| + object = find_by(primary_key => one_id) + object.destroy if object + }.compact else find(id).destroy end - rescue RecordNotFound end # Deletes the row with a primary key matching the +id+ argument, using a diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index f088c064f5..643032e7cf 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -473,10 +473,18 @@ class PersistenceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) } end - def test_record_not_found_exception + def test_find_raises_record_not_found_exception assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) } end + def test_update_raises_record_not_found_exception + assert_raise(ActiveRecord::RecordNotFound) { Topic.update(99999, approved: true) } + end + + def test_destroy_raises_record_not_found_exception + assert_raise(ActiveRecord::RecordNotFound) { Topic.destroy(99999) } + end + def test_update_all assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'") assert_equal "bulk updated!", Topic.find(1).content @@ -938,7 +946,9 @@ class PersistenceTest < ActiveRecord::TestCase should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world") Topic.find(1).replies << should_not_be_destroyed_reply - assert_nil Topic.where("1=0").scoping { Topic.destroy(1) } + assert_raise(ActiveRecord::RecordNotFound) do + Topic.where("1=0").scoping { Topic.destroy(1) } + end assert_nothing_raised { Topic.find(1) } assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) } -- cgit v1.2.3 From d041a1dcbaced9f44ed976a48312c9f492fffe5e Mon Sep 17 00:00:00 2001 From: George Claghorn <george@basecamp.com> Date: Thu, 30 Nov 2017 23:54:03 -0500 Subject: Add ActiveStorage::Previewer#logger to match ActiveStorage::Analyzer#logger --- activestorage/lib/active_storage/previewer.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/activestorage/lib/active_storage/previewer.rb b/activestorage/lib/active_storage/previewer.rb index ed75bae3b5..3d485988e9 100644 --- a/activestorage/lib/active_storage/previewer.rb +++ b/activestorage/lib/active_storage/previewer.rb @@ -54,5 +54,9 @@ module ActiveStorage IO.popen(argv) { |out| IO.copy_stream(out, to) } to.rewind end + + def logger + ActiveStorage.logger + end end end -- cgit v1.2.3 From 695ec1fef0b7fdb3125a3e2e4364a7f449265c1b Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Fri, 1 Dec 2017 18:21:21 +0900 Subject: Class level `update` and `destroy` checks all the records exist before making changes (#31306) It makes more sense than ignoring invalid IDs. --- activerecord/lib/active_record/persistence.rb | 13 +++------ activerecord/test/cases/persistence_test.rb | 41 ++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 17459f2505..a13b0d0181 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -99,11 +99,9 @@ module ActiveRecord # for updating all records in a single query. def update(id = :all, attributes) if id.is_a?(Array) - id.map.with_index { |one_id, idx| - object = find_by(primary_key => one_id) - object.update(attributes[idx]) if object - object - }.compact + id.map { |one_id| find(one_id) }.each_with_index { |object, idx| + object.update(attributes[idx]) + } elsif id == :all all.each { |record| record.update(attributes) } else @@ -139,10 +137,7 @@ module ActiveRecord # Todo.destroy(todos) def destroy(id) if id.is_a?(Array) - id.map { |one_id| - object = find_by(primary_key => one_id) - object.destroy if object - }.compact + find(id).each(&:destroy) else find(id).destroy end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 643032e7cf..4aea275d51 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -70,7 +70,7 @@ class PersistenceTest < ActiveRecord::TestCase end def test_update_many - topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, nil => {} } + topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } updated = Topic.update(topic_data.keys, topic_data.values) assert_equal [1, 2], updated.map(&:id) @@ -78,10 +78,33 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "2 updated", Topic.find(2).content end + def test_update_many_with_duplicated_ids + updated = Topic.update([1, 1, 2], [ + { "content" => "1 duplicated" }, { "content" => "1 updated" }, { "content" => "2 updated" } + ]) + + assert_equal [1, 1, 2], updated.map(&:id) + assert_equal "1 updated", Topic.find(1).content + assert_equal "2 updated", Topic.find(2).content + end + + def test_update_many_with_invalid_id + topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, 99999 => {} } + + assert_raise(ActiveRecord::RecordNotFound) do + Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) } + end + + assert_not_equal "1 updated", Topic.find(1).content + assert_not_equal "2 updated", Topic.find(2).content + end + def test_class_level_update_is_affected_by_scoping topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } - assert_equal [], Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) } + assert_raise(ActiveRecord::RecordNotFound) do + Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) } + end assert_not_equal "1 updated", Topic.find(1).content assert_not_equal "2 updated", Topic.find(2).content @@ -175,15 +198,25 @@ class PersistenceTest < ActiveRecord::TestCase end def test_destroy_many - clients = Client.all.merge!(order: "id").find([2, 3]) + clients = Client.find([2, 3]) assert_difference("Client.count", -2) do - destroyed = Client.destroy([2, 3, nil]).sort_by(&:id) + destroyed = Client.destroy([2, 3]) assert_equal clients, destroyed assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" end end + def test_destroy_many_with_invalid_id + clients = Client.find([2, 3]) + + assert_raise(ActiveRecord::RecordNotFound) do + Client.destroy([2, 3, 99999]) + end + + assert_equal clients, Client.find([2, 3]) + end + def test_becomes assert_kind_of Reply, topics(:first).becomes(Reply) assert_equal "The First Topic", topics(:first).becomes(Reply).title -- cgit v1.2.3 From 05d1e9e413a87bee5b0c1c200fa9f84dba0e1d15 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Fri, 1 Dec 2017 18:28:31 +0900 Subject: Remove unnecessary scoping --- activerecord/test/cases/persistence_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 4aea275d51..4cc66a2e49 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -92,7 +92,7 @@ class PersistenceTest < ActiveRecord::TestCase topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, 99999 => {} } assert_raise(ActiveRecord::RecordNotFound) do - Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) } + Topic.update(topic_data.keys, topic_data.values) end assert_not_equal "1 updated", Topic.find(1).content -- cgit v1.2.3 From 6b16f6db279bdbd95e8b295c9bab6760a6ff6af0 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Fri, 1 Dec 2017 20:42:04 +0900 Subject: Tweaks CHANGELOGs [ci skip] --- actionview/CHANGELOG.md | 2 +- activerecord/CHANGELOG.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 566e30993b..f42ada0baa 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,7 +1,7 @@ * Add `preload_link_tag` helper This helper that allows to the browser to initiate early fetch of resources - (different to the specified in javascript_include_tag and stylesheet_link_tag). + (different to the specified in `javascript_include_tag` and `stylesheet_link_tag`). Additionally, this sends Early Hints if supported by browser. *Guillermo Iguaran* diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 048a1eeb2f..89a12d4223 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -2,7 +2,7 @@ Example: - add_index :users, :name, using: :gist, opclass: name: :gist_trgm_ops + add_index :users, :name, using: :gist, opclass: { name: :gist_trgm_ops } *Greg Navis* @@ -80,7 +80,7 @@ *bogdanvlviv* * Fixed a bug where column orders for an index weren't written to - db/schema.rb when using the sqlite adapter. + `db/schema.rb` when using the sqlite adapter. Fixes #30902. -- cgit v1.2.3 From 32516e8862946da963e4ba9256553156ca321596 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Fri, 1 Dec 2017 20:48:29 +0900 Subject: Remove unpaired `}` [ci skip] --- .../active_record/connection_adapters/abstract/schema_statements.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 2a335e8f2b..7ae32d0ced 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -752,13 +752,12 @@ module ActiveRecord # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL # - # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops }) + # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops) # # generates: # # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL # - # # Note: only supported by PostgreSQL # # ====== Creating an index with a specific type -- cgit v1.2.3 From 8203482a9ef9bd60d5014745fd7af868d9954b1d Mon Sep 17 00:00:00 2001 From: Travis Hunter <travis.c.hunter@gmail.com> Date: Fri, 20 Jan 2017 16:18:46 -0500 Subject: Add support for invalid foreign keys in Postgres Add validate_constraint and update naming --- .../abstract/schema_definitions.rb | 5 ++ .../abstract/schema_statements.rb | 2 + .../connection_adapters/abstract_adapter.rb | 5 ++ .../postgresql/schema_creation.rb | 12 ++++ .../postgresql/schema_definitions.rb | 13 +++++ .../postgresql/schema_statements.rb | 44 +++++++++++++- .../connection_adapters/postgresql_adapter.rb | 4 ++ .../test/cases/migration/foreign_key_test.rb | 68 ++++++++++++++++++++++ 8 files changed, 152 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index b66009bdc6..88ef8e0819 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -87,6 +87,11 @@ module ActiveRecord options[:primary_key] != default_primary_key end + def validate? + options.fetch(:validate, true) + end + alias validated? validate? + def defined_for?(to_table_ord = nil, to_table: nil, **options) if to_table_ord self.to_table == to_table_ord.to_s diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 7ae32d0ced..09ccf0c193 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -964,6 +964,8 @@ module ActiveRecord # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ # [<tt>:on_update</tt>] # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ + # [<tt>:validate</tt>] + # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+. def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 8993c517a6..fc80d332f9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -272,6 +272,11 @@ module ActiveRecord false end + # Does this adapter support creating invalid constraints? + def supports_validate_constraints? + false + end + # Does this adapter support creating foreign key constraints # in the same statement as creating the table? def supports_foreign_keys_in_create? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb index 59f661da25..8e381a92cf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -5,6 +5,18 @@ module ActiveRecord module PostgreSQL class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: private + def visit_AlterTable(o) + super << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ") + end + + def visit_AddForeignKey(o) + super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? } + end + + def visit_ValidateConstraint(name) + "VALIDATE CONSTRAINT #{quote_column_name(name)}" + end + def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 75622eb304..a3bb66bf3e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -192,6 +192,19 @@ module ActiveRecord class Table < ActiveRecord::ConnectionAdapters::Table include ColumnMethods end + + class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable + attr_reader :constraint_validations + + def initialize(td) + super + @constraint_validations = [] + end + + def validate_constraint(name) + @constraint_validations << name + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 15a1dfd102..38102b2e7a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -507,7 +507,7 @@ module ActiveRecord def foreign_keys(table_name) scope = quoted_scope(table_name) fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") - SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid JOIN pg_class t2 ON c.confrelid = t2.oid @@ -529,6 +529,7 @@ module ActiveRecord options[:on_delete] = extract_foreign_key_action(row["on_delete"]) options[:on_update] = extract_foreign_key_action(row["on_update"]) + options[:validate] = row["valid"] ForeignKeyDefinition.new(table_name, row["to_table"], options) end @@ -589,6 +590,43 @@ module ActiveRecord PostgreSQL::SchemaDumper.create(self, options) end + # Validates the given constraint. + # + # Validates the constraint named +constraint_name+ on +accounts+. + # + # validate_foreign_key :accounts, :constraint_name + def validate_constraint(table_name, constraint_name) + return unless supports_validate_constraints? + + at = create_alter_table table_name + at.validate_constraint constraint_name + + execute schema_creation.accept(at) + end + + # Validates the given foreign key. + # + # Validates the foreign key on +accounts.branch_id+. + # + # validate_foreign_key :accounts, :branches + # + # Validates the foreign key on +accounts.owner_id+. + # + # validate_foreign_key :accounts, column: :owner_id + # + # Validates the foreign key named +special_fk_name+ on the +accounts+ table. + # + # validate_foreign_key :accounts, name: :special_fk_name + # + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. + def validate_foreign_key(from_table, options_or_to_table = {}) + return unless supports_validate_constraints? + + fk_name_to_validate = foreign_key_for!(from_table, options_or_to_table).name + + validate_constraint from_table, fk_name_to_validate + end + private def schema_creation PostgreSQL::SchemaCreation.new(self) @@ -598,6 +636,10 @@ module ActiveRecord PostgreSQL::TableDefinition.new(*args) end + def create_alter_table(name) + PostgreSQL::AlterTable.new create_table_definition(name) + end + def new_column_from_field(table_name, field) column_name, type, default, notnull, oid, fmod, collation, comment = field type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 9397481c4c..68b79f777e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -142,6 +142,10 @@ module ActiveRecord true end + def supports_validate_constraints? + true + end + def supports_views? true end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 499d072de5..079be04946 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -227,6 +227,74 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end end + if ActiveRecord::Base.connection.supports_validate_constraints? + def test_add_invalid_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + refute fk.validated? + end + + def test_validate_foreign_key_infers_column + @connection.add_foreign_key :astronauts, :rockets, validate: false + refute @connection.foreign_keys("astronauts").first.validated? + + @connection.validate_foreign_key :astronauts, :rockets + assert @connection.foreign_keys("astronauts").first.validated? + end + + def test_validate_foreign_key_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + refute @connection.foreign_keys("astronauts").first.validated? + + @connection.validate_foreign_key :astronauts, column: "rocket_id" + assert @connection.foreign_keys("astronauts").first.validated? + end + + def test_validate_foreign_key_by_symbol_column + @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id, validate: false + refute @connection.foreign_keys("astronauts").first.validated? + + @connection.validate_foreign_key :astronauts, column: :rocket_id + assert @connection.foreign_keys("astronauts").first.validated? + end + + def test_validate_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false + refute @connection.foreign_keys("astronauts").first.validated? + + @connection.validate_foreign_key :astronauts, name: "fancy_named_fk" + assert @connection.foreign_keys("astronauts").first.validated? + end + + def test_validate_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.validate_foreign_key :astronauts, :rockets + end + end + + def test_validate_constraint_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false + + @connection.validate_constraint :astronauts, "fancy_named_fk" + assert @connection.foreign_keys("astronauts").first.validated? + end + else + # Foreign key should still be created, but should not be invalid + def test_add_invalid_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert fk.validated? + end + end + def test_schema_dumping @connection.add_foreign_key :astronauts, :rockets output = dump_table_schema "astronauts" -- cgit v1.2.3 From b852ef2660dac36e348865b455fab7fbcc0d2a7f Mon Sep 17 00:00:00 2001 From: George Claghorn <george.claghorn@gmail.com> Date: Fri, 1 Dec 2017 11:07:30 -0500 Subject: Make ASt previewer/analyzer binary paths configurable --- .../lib/active_storage/analyzer/video_analyzer.rb | 4 +++- activestorage/lib/active_storage/engine.rb | 19 ++++++++++++++++++- .../lib/active_storage/previewer/pdf_previewer.rb | 9 ++++++++- .../lib/active_storage/previewer/video_previewer.rb | 4 +++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb index 408b5e58e9..b6fc54d917 100644 --- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb +++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb @@ -19,6 +19,8 @@ module ActiveStorage # This analyzer requires the {ffmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails. You must # install ffmpeg yourself to use this analyzer. class Analyzer::VideoAnalyzer < Analyzer + class_attribute :ffprobe_path, default: "ffprobe" + def self.accept?(blob) blob.video? end @@ -68,7 +70,7 @@ module ActiveStorage end def probe_from(file) - IO.popen([ "ffprobe", "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output| + IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output| JSON.parse(output.read) end rescue Errno::ENOENT diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index 6cf6635c4f..b870e6d4d6 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -15,7 +15,8 @@ module ActiveStorage config.active_storage = ActiveSupport::OrderedOptions.new config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ] - config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ] + config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ] + config.active_storage.paths = ActiveSupport::OrderedOptions.new config.eager_load_namespaces << ActiveStorage @@ -68,5 +69,21 @@ module ActiveStorage end end end + + initializer "active_storage.paths" do + config.after_initialize do |app| + if ffprobe_path = app.config.active_storage.paths.ffprobe + ActiveStorage::Analyzer::VideoAnalyzer.ffprobe_path = ffprobe_path + end + + if ffmpeg_path = app.config.active_storage.paths.ffmpeg + ActiveStorage::Previewer::VideoPreviewer.ffmpeg_path = ffmpeg_path + end + + if mutool_path = app.config.active_storage.paths.mutool + ActiveStorage::Previewer::PDFPreviewer.mutool_path = mutool_path + end + end + end end end diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb index a2f05c74a6..b84aefcc9c 100644 --- a/activestorage/lib/active_storage/previewer/pdf_previewer.rb +++ b/activestorage/lib/active_storage/previewer/pdf_previewer.rb @@ -2,16 +2,23 @@ module ActiveStorage class Previewer::PDFPreviewer < Previewer + class_attribute :mutool_path, default: "mutool" + def self.accept?(blob) blob.content_type == "application/pdf" end def preview download_blob_to_tempfile do |input| - draw "mutool", "draw", "-F", "png", "-o", "-", input.path, "1" do |output| + draw_first_page_from input do |output| yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png" end end end + + private + def draw_first_page_from(file, &block) + draw mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block + end end end diff --git a/activestorage/lib/active_storage/previewer/video_previewer.rb b/activestorage/lib/active_storage/previewer/video_previewer.rb index 49f128d142..5d06e33f44 100644 --- a/activestorage/lib/active_storage/previewer/video_previewer.rb +++ b/activestorage/lib/active_storage/previewer/video_previewer.rb @@ -2,6 +2,8 @@ module ActiveStorage class Previewer::VideoPreviewer < Previewer + class_attribute :ffmpeg_path, default: "ffmpeg" + def self.accept?(blob) blob.video? end @@ -16,7 +18,7 @@ module ActiveStorage private def draw_relevant_frame_from(file, &block) - draw "ffmpeg", "-i", file.path, "-y", "-vcodec", "png", + draw ffmpeg_path, "-i", file.path, "-y", "-vcodec", "png", "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block end end -- cgit v1.2.3 From 70c96b4423abacecb1116184c8ea4d4662b809f8 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" <yuuji.yaginuma@gmail.com> Date: Sat, 2 Dec 2017 11:18:38 +0900 Subject: Fix method name in `validate_constraint` doc [ci skip] --- .../active_record/connection_adapters/postgresql/schema_statements.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 38102b2e7a..af13a9e16f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -594,7 +594,7 @@ module ActiveRecord # # Validates the constraint named +constraint_name+ on +accounts+. # - # validate_foreign_key :accounts, :constraint_name + # validate_constraint :accounts, :constraint_name def validate_constraint(table_name, constraint_name) return unless supports_validate_constraints? -- cgit v1.2.3 From dd6338a0699f2301d4b2fc8653688b4c4183cee5 Mon Sep 17 00:00:00 2001 From: Dinah Shi <dinahshi28@gmail.com> Date: Sat, 2 Dec 2017 14:30:10 +0800 Subject: Extract sql fragment generators for alter table from PostgreSQL adapter --- .../abstract/schema_statements.rb | 16 +++- .../connection_adapters/abstract_mysql_adapter.rb | 36 +++------ .../postgresql/schema_statements.rb | 86 +++++++++++++--------- .../lib/active_record/migration/compatibility.rb | 9 +++ .../test/cases/adapters/mysql2/connection_test.rb | 4 +- .../test/cases/migration/compatibility_test.rb | 17 +++++ 6 files changed, 107 insertions(+), 61 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 9b7345f7c3..f0c6ad63f9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -600,7 +600,7 @@ module ActiveRecord # to provide these in a migration's +change+ method so it can be reverted. # In that case, +type+ and +options+ will be used by #add_column. def remove_column(table_name, column_name, type = nil, options = {}) - execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" + execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}" end # Changes the column's definition according to the new options. @@ -1340,6 +1340,20 @@ module ActiveRecord options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? end + def add_column_for_alter(table_name, column_name, type, options = {}) + td = create_table_definition(table_name) + cd = td.new_column_definition(column_name, type, options) + schema_creation.accept(AddColumnDefinition.new(cd)) + end + + def remove_column_for_alter(table_name, column_name, type = nil, options = {}) + "DROP COLUMN #{quote_column_name(column_name)}" + end + + def remove_columns_for_alter(table_name, *column_names) + column_names.map { |column_name| remove_column_for_alter(table_name, column_name) } + end + def insert_versions_sql(versions) sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 0e552dba95..2926366e10 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -299,7 +299,7 @@ module ActiveRecord def bulk_change_table(table_name, operations) #:nodoc: sqls = operations.flat_map do |command, args| table, arguments = args.shift, args - method = :"#{command}_sql" + method = :"#{command}_for_alter" if respond_to?(method, true) send(method, table, *arguments) @@ -372,11 +372,11 @@ module ActiveRecord end def change_column(table_name, column_name, type, options = {}) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") + execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}") end def rename_column(table_name, column_name, new_column_name) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") + execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_for_alter(table_name, column_name, new_column_name)}") rename_column_indexes(table_name, column_name, new_column_name) end @@ -668,13 +668,7 @@ module ActiveRecord end end - def add_column_sql(table_name, column_name, type, options = {}) - td = create_table_definition(table_name) - cd = td.new_column_definition(column_name, type, options) - schema_creation.accept(AddColumnDefinition.new(cd)) - end - - def change_column_sql(table_name, column_name, type, options = {}) + def change_column_for_alter(table_name, column_name, type, options = {}) column = column_for(table_name, column_name) type ||= column.sql_type @@ -695,7 +689,7 @@ module ActiveRecord schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - def rename_column_sql(table_name, column_name, new_column_name) + def rename_column_for_alter(table_name, column_name, new_column_name) column = column_for(table_name, column_name) options = { default: column.default, @@ -709,31 +703,23 @@ module ActiveRecord schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) end - def remove_column_sql(table_name, column_name, type = nil, options = {}) - "DROP #{quote_column_name(column_name)}" - end - - def remove_columns_sql(table_name, *column_names) - column_names.map { |column_name| remove_column_sql(table_name, column_name) } - end - - def add_index_sql(table_name, column_name, options = {}) + def add_index_for_alter(table_name, column_name, options = {}) index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) index_algorithm[0, 0] = ", " if index_algorithm.present? "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}" end - def remove_index_sql(table_name, options = {}) + def remove_index_for_alter(table_name, options = {}) index_name = index_name_for_remove(table_name, options) "DROP INDEX #{quote_column_name(index_name)}" end - def add_timestamps_sql(table_name, options = {}) - [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)] + def add_timestamps_for_alter(table_name, options = {}) + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] end - def remove_timestamps_sql(table_name, options = {}) - [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + def remove_timestamps_for_alter(table_name, options = {}) + [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)] end # MySQL is too stupid to create a temporary table for use subquery, so we have diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 846e721983..371597031c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -391,51 +391,24 @@ module ActiveRecord end def change_column(table_name, column_name, type, options = {}) #:nodoc: - clear_cache! quoted_table_name = quote_table_name(table_name) - quoted_column_name = quote_column_name(column_name) - sql_type = type_to_sql(type, options) - sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup - if options[:collation] - sql << " COLLATE \"#{options[:collation]}\"" - end - if options[:using] - sql << " USING #{options[:using]}" - elsif options[:cast_as] - cast_as_type = type_to_sql(options[:cast_as], options) - sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" - end - execute sql - - change_column_default(table_name, column_name, options[:default]) if options.key?(:default) - change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) - change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + sqls, procs = change_column_for_alter(table_name, column_name, type, options) + execute "ALTER TABLE #{quoted_table_name} #{sqls.join(", ")}" + procs.each(&:call) end # Changes the default value of a table column. def change_column_default(table_name, column_name, default_or_changes) # :nodoc: - clear_cache! - column = column_for(table_name, column_name) - return unless column - - default = extract_new_default_value(default_or_changes) - alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" - if default.nil? - # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will - # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". - execute alter_column_query % "DROP DEFAULT" - else - execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" - end + execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}" end def change_column_null(table_name, column_name, null, default = nil) #:nodoc: clear_cache! unless null || default.nil? column = column_for(table_name, column_name) - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column + execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL" if column end - execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") + execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}" end # Adds comment for given table column or drops it if +comment+ is a +nil+ @@ -629,6 +602,53 @@ module ActiveRecord end end + def change_column_for_alter(table_name, column_name, type, options = {}) + clear_cache! + quoted_column_name = quote_column_name(column_name) + sql_type = type_to_sql(type, options) + sql = "ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + if options[:using] + sql << " USING #{options[:using]}" + elsif options[:cast_as] + cast_as_type = type_to_sql(options[:cast_as], options) + sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" + end + + sqls = [sql] + procs = [] + sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default) + sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + procs << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment) + + [sqls, procs] + end + + + # Changes the default value of a table column. + def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc: + clear_cache! + column = column_for(table_name, column_name) + return unless column + + default = extract_new_default_value(default_or_changes) + alter_column_query = "ALTER COLUMN #{quote_column_name(column_name)} %s" + if default.nil? + # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will + # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". + alter_column_query % "DROP DEFAULT" + else + alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}" + end + end + + def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc: + clear_cache! + "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL" + end + def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index c979aaf0a0..da307c1723 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -16,6 +16,15 @@ module ActiveRecord V5_2 = Current class V5_1 < V5_2 + if adapter_name == "PostgreSQL" + def change_column(table_name, column_name, type, options = {}) + unless options[:null] || options[:default].nil? + column = connection.send(:column_for, table_name, column_name) + connection.execute("UPDATE #{connection.quote_table_name(table_name)} SET #{connection.quote_column_name(column_name)}=#{connection.quote_default_expression(options[:default], column)} WHERE #{connection.quote_column_name(column_name)} IS NULL") if column + end + connection.change_column(table_name, column_name, type, options) + end + end end class V5_0 < V5_1 diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index e61c70848a..13b4096671 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -174,10 +174,10 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert_equal "SCHEMA", @subscriber.logged[0][1] end - def test_logs_name_rename_column_sql + def test_logs_name_rename_column_for_alter @connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))" @subscriber.logged.clear - @connection.send(:rename_column_sql, "bar_baz", "foo", "foo2") + @connection.send(:rename_column_for_alter, "bar_baz", "foo", "foo2") assert_equal "SCHEMA", @subscriber.logged[0][1] ensure @connection.execute "DROP TABLE `bar_baz`" diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 2fef2f796e..0fd55e6ae4 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -126,6 +126,23 @@ module ActiveRecord end assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message) end + + if current_adapter?(:PostgreSQLAdapter) + class Testing < ActiveRecord::Base + end + + def test_legacy_change_column_with_null_executes_update + migration = Class.new(ActiveRecord::Migration[5.1]) { + def migrate(x) + change_column :testings, :foo, :string, null: false, default: "foobar" + end + }.new + + t = Testing.create! + ActiveRecord::Migrator.new(:up, [migration]).migrate + assert_equal ["foobar"], Testing.all.map(&:foo) + end + end end end end -- cgit v1.2.3 From 99f6722e86c5451e1334db31a18ae2cbaccb551d Mon Sep 17 00:00:00 2001 From: willnet <netwillnet@gmail.com> Date: Sat, 2 Dec 2017 16:35:38 +0900 Subject: [ci skip] Add a missing space before closing curly braces --- guides/source/form_helpers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md index 4ce67df93a..f8ec389b01 100644 --- a/guides/source/form_helpers.md +++ b/guides/source/form_helpers.md @@ -920,7 +920,7 @@ When an association accepts nested attributes `fields_for` renders its block onc ```ruby def new @person = Person.new - 2.times { @person.addresses.build} + 2.times { @person.addresses.build } end ``` -- cgit v1.2.3 From a71bbed76aab9e8a9a6b2da18bcacd8ee32a0dd0 Mon Sep 17 00:00:00 2001 From: claudiob <claudiob@users.noreply.github.com> Date: Sat, 2 Dec 2017 12:00:09 -0800 Subject: Fix typo in test error message With the current code, a failing test shows this error, which is missing the number of times called and has two periods at the end. ``` /railties$ be ruby -Itest test/generators/app_generator_test.rb -n test_active_storage_install Failure: AppGeneratorTest#test_active_storage_install [test/generators/app_generator_test.rb:313]: active_storage:install expected to be called once, but was called times.. Expected: 1 Actual: 2 ``` After the fix, the error message looks correct: ``` /railties$ be ruby -Itest test/generators/app_generator_test.rb -n test_active_storage_install Failure: AppGeneratorTest#test_active_storage_install [test/generators/app_generator_test.rb:313]: active_storage:install expected to be called once, but was called 2 times. Expected: 1 Actual: 2 ``` --- railties/test/generators/app_generator_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 774fd0f315..8d12f4e5a7 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -310,7 +310,7 @@ class AppGeneratorTest < Rails::Generators::TestCase case command when "active_storage:install" @binstub_called += 1 - assert_equal 1, @binstub_called, "active_storage:install expected to be called once, but was called #{@install_called} times." + assert_equal 1, @binstub_called, "active_storage:install expected to be called once, but was called #{@binstub_called} times" end end -- cgit v1.2.3 From 01fb0907b2dafcecf63eedb80408d4cbff5f4d23 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Sun, 3 Dec 2017 04:57:04 +0900 Subject: Refactor `length`, `order`, and `opclass` index options dumping --- .../abstract/schema_definitions.rb | 19 ++++++++++++----- .../abstract/schema_statements.rb | 2 +- .../connection_adapters/abstract_mysql_adapter.rb | 2 +- .../connection_adapters/mysql/schema_statements.rb | 15 ++++++++------ .../postgresql/schema_statements.rb | 16 +++++---------- activerecord/lib/active_record/schema_dumper.rb | 14 ++++++++++--- .../test/cases/adapters/postgresql/schema_test.rb | 6 +++--- activerecord/test/cases/schema_dumper_test.rb | 24 +++++++++++++++++++--- activerecord/test/schema/schema.rb | 2 ++ 9 files changed, 67 insertions(+), 33 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 88ef8e0819..86406d95ad 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,7 +6,7 @@ module ActiveRecord # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes class IndexDefinition # :nodoc: - attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclass, :comment + attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :comment def initialize( table, name, @@ -14,24 +14,33 @@ module ActiveRecord columns = [], lengths: {}, orders: {}, + opclasses: {}, where: nil, type: nil, using: nil, - opclass: {}, comment: nil ) @table = table @name = name @unique = unique @columns = columns - @lengths = lengths - @orders = orders + @lengths = concise_options(lengths) + @orders = concise_options(orders) + @opclasses = concise_options(opclasses) @where = where @type = type @using = using - @opclass = opclass @comment = comment end + + private + def concise_options(options) + if columns.size == options.size && options.values.uniq.size == 1 + options.values.first + else + options + end + end end # Abstract representation of a column definition. Instances of this type diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index fac9e1cca2..efc934c34c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1202,7 +1202,7 @@ module ActiveRecord when Hash order = order.symbolize_keys quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? } - when String + else quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? } end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 143dc6d08d..41c628302d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -611,7 +611,7 @@ module ActiveRecord when Hash length = length.symbolize_keys quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } - when Integer + else quoted_columns.each { |name, column| column << "(#{length})" } end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index a15c7d1787..d5b2c18ee6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -22,23 +22,26 @@ module ActiveRecord index_using = mysql_index_type end - indexes << IndexDefinition.new( + indexes << [ row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, + [], + lengths: {}, + orders: {}, type: index_type, using: index_using, comment: row[:Index_comment].presence - ) + ] end - indexes.last.columns << row[:Column_name] - indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] - indexes.last.orders.merge!(row[:Column_name] => :desc) if row[:Collation] == "D" + indexes.last[-2] << row[:Column_name] + indexes.last[-1][:lengths].merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] + indexes.last[-1][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D" end end - indexes + indexes.map { |index| IndexDefinition.new(*index) } end def remove_column(table_name, column_name, type = nil, options = {}) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 64f08e0ee1..7b4d5711cb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -109,6 +109,9 @@ module ActiveRecord using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten + orders = {} + opclasses = {} + if indkey.include?(0) columns = expressions else @@ -119,21 +122,12 @@ module ActiveRecord AND a.attnum IN (#{indkey.join(",")}) SQL - orders = {} - opclasses = {} - # add info on sort order (only desc order is explicitly specified, asc is the default) # and non-default opclasses expressions.scan(/(\w+)(?: (?!DESC)(\w+))?(?: (DESC))?/).each do |column, opclass, desc| - opclasses[column] = opclass if opclass + opclasses[column] = opclass.to_sym if opclass orders[column] = :desc if desc end - - # Use a string for the opclass description (instead of a hash) when all columns - # have the same opclass specified. - if columns.count == opclasses.count && opclasses.values.uniq.count == 1 - opclasses = opclasses.values.first - end end IndexDefinition.new( @@ -142,9 +136,9 @@ module ActiveRecord unique, columns, orders: orders, + opclasses: opclasses, where: where, using: using.to_sym, - opclass: opclasses, comment: comment.presence ) end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 8cb2851557..16ccba6b6c 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -184,12 +184,12 @@ HEADER "name: #{index.name.inspect}", ] index_parts << "unique: true" if index.unique - index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present? - index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present? + index_parts << "length: #{format_index_parts(index.lengths)}" if index.lengths.present? + index_parts << "order: #{format_index_parts(index.orders)}" if index.orders.present? + index_parts << "opclass: #{format_index_parts(index.opclasses)}" if index.opclasses.present? index_parts << "where: #{index.where.inspect}" if index.where index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index) index_parts << "type: #{index.type.inspect}" if index.type - index_parts << "opclass: #{index.opclass.inspect}" if index.opclass.present? index_parts << "comment: #{index.comment.inspect}" if index.comment index_parts end @@ -232,6 +232,14 @@ HEADER options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ") end + def format_index_parts(options) + if options.is_a?(Hash) + "{ #{format_options(options)} }" + else + options.inspect + end + end + def remove_prefix_and_suffix(table) prefix = Regexp.escape(@options[:table_name_prefix].to_s) suffix = Regexp.escape(@options[:table_name_suffix].to_s) diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index a6ae18a826..1126908761 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -459,7 +459,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal :btree, index_d.using assert_equal :gin, index_e.using - assert_equal :desc, index_d.orders[INDEX_D_COLUMN] + assert_equal :desc, index_d.orders end end @@ -520,7 +520,7 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase output = dump_table_schema "trains" - assert_match(/opclass: "text_pattern_ops"/, output) + assert_match(/opclass: :text_pattern_ops/, output) end def test_non_default_opclass_is_dumped @@ -528,7 +528,7 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase output = dump_table_schema "trains" - assert_match(/opclass: \{"description"=>"text_pattern_ops"\}/, output) + assert_match(/opclass: \{ description: :text_pattern_ops \}/, output) end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index ac5092f1c1..a612ce9bb2 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -177,14 +177,14 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_columns_in_right_order index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip - if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition - elsif current_adapter?(:Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) if ActiveRecord::Base.connection.supports_index_sort_order? assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, order: { rating: :desc }', index_definition else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition end + elsif ActiveRecord::Base.connection.supports_index_sort_order? + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end @@ -199,6 +199,24 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dumps_index_sort_order + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_rating/).first.strip + if ActiveRecord::Base.connection.supports_index_sort_order? + assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating", order: :desc', index_definition + else + assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating"', index_definition + end + end + + def test_schema_dumps_index_length + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_description/).first.strip + if current_adapter?(:Mysql2Adapter) + assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description", length: 10', index_definition + else + assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description"', index_definition + end + end + def test_schema_dump_should_honor_nonstandard_primary_keys output = standard_dump match = output.match(%r{create_table "movies"(.*)do}) diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index a4505a4892..bf66846840 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -205,6 +205,8 @@ ActiveRecord::Schema.define do t.bigint :rating, default: 1 t.integer :account_id t.string :description, default: "" + t.index [:name, :rating], order: :desc + t.index [:name, :description], length: 10 t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)" t.index :name, name: "company_name_index", using: :btree -- cgit v1.2.3 From 3040446cece8e7a6d9e29219e636e13f180a1e03 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Sun, 3 Dec 2017 06:33:56 +0900 Subject: Fix warning: assigned but unused variable - t --- activerecord/test/cases/migration/compatibility_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 0fd55e6ae4..f1defe1a8a 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -138,7 +138,7 @@ module ActiveRecord end }.new - t = Testing.create! + Testing.create! ActiveRecord::Migrator.new(:up, [migration]).migrate assert_equal ["foobar"], Testing.all.map(&:foo) end -- cgit v1.2.3 From 33a3f7123bc4cc49b99b01a40bbfd463b2e73f76 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Sun, 3 Dec 2017 07:08:30 +0900 Subject: Extract duplicated index column options normalization as `options_for_index_columns` And placed `add_options_for_index_columns` in `schema_statements.rb` consistently to ease to find related code. --- .../connection_adapters/abstract/schema_statements.rb | 19 ++++++++++--------- .../connection_adapters/abstract_mysql_adapter.rb | 19 ------------------- .../connection_adapters/mysql/schema_statements.rb | 12 ++++++++++++ .../postgresql/schema_statements.rb | 12 ++++++++++++ .../connection_adapters/postgresql_adapter.rb | 18 ------------------ 5 files changed, 34 insertions(+), 46 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index efc934c34c..4f58b0242c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1197,17 +1197,18 @@ module ActiveRecord end def add_index_sort_order(quoted_columns, **options) - if order = options[:order] - case order - when Hash - order = order.symbolize_keys - quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? } - else - quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? } - end + orders = options_for_index_columns(options[:order]) + quoted_columns.each do |name, column| + column << " #{orders[name].upcase}" if orders[name].present? end + end - quoted_columns + def options_for_index_columns(options) + if options.is_a?(Hash) + options.symbolize_keys + else + Hash.new { |hash, column| hash[column] = options } + end end # Overridden by the MySQL adapter for supporting index lengths and by diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 41c628302d..479131caad 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -605,25 +605,6 @@ module ActiveRecord end end - def add_index_length(quoted_columns, **options) - if length = options[:length] - case length - when Hash - length = length.symbolize_keys - quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } - else - quoted_columns.each { |name, column| column << "(#{length})" } - end - end - - quoted_columns - end - - def add_options_for_index_columns(quoted_columns, **options) - quoted_columns = add_index_length(quoted_columns, options) - super - end - # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index d5b2c18ee6..ce50590651 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -106,6 +106,18 @@ module ActiveRecord super unless specifier == "RESTRICT" end + def add_index_length(quoted_columns, **options) + lengths = options_for_index_columns(options[:length]) + quoted_columns.each do |name, column| + column << "(#{lengths[name]})" if lengths[name].present? + end + end + + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_length(quoted_columns, options) + super + end + def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 7b4d5711cb..2e9e74403b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -693,6 +693,18 @@ module ActiveRecord "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL" end + def add_index_opclass(quoted_columns, **options) + opclasses = options_for_index_columns(options[:opclass]) + quoted_columns.each do |name, column| + column << " #{opclasses[name]}" if opclasses[name].present? + end + end + + def add_options_for_index_columns(quoted_columns, **options) + quoted_columns = add_index_opclass(quoted_columns, options) + super + end + def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 68b79f777e..23fc69d649 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -390,24 +390,6 @@ module ActiveRecord end private - - def add_index_opclass(column_names, options = {}) - opclass = - if options[:opclass].is_a?(Hash) - options[:opclass].symbolize_keys - else - Hash.new { |hash, column| hash[column] = options[:opclass].to_s } - end - column_names.each do |name, column| - column << " #{opclass[name]}" if opclass[name].present? - end - end - - def add_options_for_index_columns(quoted_columns, **options) - quoted_columns = add_index_opclass(quoted_columns, options) - super - end - # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" NUMERIC_VALUE_OUT_OF_RANGE = "22003" -- cgit v1.2.3 From 5358f2b67bd6fb12d708527a4a70dcab65513c5e Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Sun, 3 Dec 2017 07:55:34 +0900 Subject: Fix `test_add_column_with_timestamp_type` failure This test failed due to dirty schema cache. It is needed to call `clear_cache!` when using same named table with different definition. https://travis-ci.org/rails/rails/jobs/310627767#L769-L772 --- activerecord/test/cases/migration/change_schema_test.rb | 13 ++++++------- activerecord/test/cases/migration/compatibility_test.rb | 2 ++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 7b0644e9c0..43aac5da75 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -264,19 +264,18 @@ module ActiveRecord t.column :foo, :timestamp end - klass = Class.new(ActiveRecord::Base) - klass.table_name = "testings" + column = connection.columns(:testings).find { |c| c.name == "foo" } - assert_equal :datetime, klass.columns_hash["foo"].type + assert_equal :datetime, column.type if current_adapter?(:PostgreSQLAdapter) - assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type + assert_equal "timestamp without time zone", column.sql_type elsif current_adapter?(:Mysql2Adapter) - assert_equal "timestamp", klass.columns_hash["foo"].sql_type + assert_equal "timestamp", column.sql_type elsif current_adapter?(:OracleAdapter) - assert_equal "TIMESTAMP(6)", klass.columns_hash["foo"].sql_type + assert_equal "TIMESTAMP(6)", column.sql_type else - assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type + assert_equal klass.connection.type_to_sql("datetime"), column.sql_type end end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index f1defe1a8a..e6ca252369 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -141,6 +141,8 @@ module ActiveRecord Testing.create! ActiveRecord::Migrator.new(:up, [migration]).migrate assert_equal ["foobar"], Testing.all.map(&:foo) + ensure + ActiveRecord::Base.clear_cache! end end end -- cgit v1.2.3 From 32516983a67f10d513370edd67b41ef0e011b175 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Sun, 3 Dec 2017 08:05:32 +0900 Subject: Fix `s/klass.connection/connection/` `klass` has removed in 5358f2b67bd6fb12d708527a4a70dcab65513c5e. --- activerecord/test/cases/migration/change_schema_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 43aac5da75..38a906c8f5 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -275,7 +275,7 @@ module ActiveRecord elsif current_adapter?(:OracleAdapter) assert_equal "TIMESTAMP(6)", column.sql_type else - assert_equal klass.connection.type_to_sql("datetime"), column.sql_type + assert_equal connection.type_to_sql("datetime"), column.sql_type end end -- cgit v1.2.3 From dbee80bca0ef504120219e6c7686437456511060 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" <yuuji.yaginuma@gmail.com> Date: Fri, 1 Dec 2017 18:16:06 +0900 Subject: Make `Migrator.current_version` work without a current database This is necessary in order to make the processing dependent on `Migrator.current_version` work even without database. Context: https://github.com/rails/rails/pull/31135#issuecomment-348404326 --- activerecord/lib/active_record/migration.rb | 10 +++++++++- activerecord/lib/active_record/railtie.rb | 7 +++++-- railties/test/application/rake/dbs_test.rb | 14 ++++++++++++++ railties/test/isolation/abstract_unit.rb | 15 +++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 15e9c09ffb..4d4b0dc67a 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1053,7 +1053,15 @@ module ActiveRecord end end - def current_version(connection = Base.connection) + def current_version(connection = nil) + if connection.nil? + begin + connection = Base.connection + rescue ActiveRecord::NoDatabaseError + return nil + end + end + get_all_versions(connection).max || 0 end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 9ee8425e1b..4538ed6a5f 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -90,12 +90,15 @@ module ActiveRecord filename = File.join(app.config.paths["db"].first, "schema_cache.yml") if File.file?(filename) + current_version = ActiveRecord::Migrator.current_version + next if current_version.nil? + cache = YAML.load(File.read(filename)) - if cache.version == ActiveRecord::Migrator.current_version + if cache.version == current_version connection.schema_cache = cache connection_pool.schema_cache = cache.dup else - warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." + warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}." end end end diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 0235210fdd..2082e9fa9f 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -98,6 +98,20 @@ module ApplicationTests end end + test "db:create works when schema cache exists and database does not exist" do + use_postgresql + + begin + rails %w(db:create db:migrate db:schema:cache:dump) + + rails "db:drop" + rails "db:create" + assert_equal 0, $?.exitstatus + ensure + rails "db:drop" rescue nil + end + end + test "db:drop failure because database does not exist" do output = rails("db:drop:_unsafe", "--trace") assert_match(/does not exist/, output) diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 7522237a38..5b1c06d4e5 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -381,6 +381,21 @@ module TestHelpers $:.reject! { |path| path =~ %r'/(#{to_remove.join('|')})/' } end + + def use_postgresql + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: postgresql + pool: 5 + database: railties_test + development: + <<: *default + test: + <<: *default + YAML + end + end end end -- cgit v1.2.3 From 8c5a7fbefd3cad403e7594d0b6a5488d80d4c98e Mon Sep 17 00:00:00 2001 From: George Claghorn <george.claghorn@gmail.com> Date: Sat, 2 Dec 2017 22:43:28 -0500 Subject: Purge variants with their blobs --- activestorage/app/models/active_storage/blob.rb | 3 ++- activestorage/lib/active_storage/log_subscriber.rb | 4 +++ activestorage/lib/active_storage/service.rb | 9 +++++-- .../service/azure_storage_service.rb | 30 +++++++++++++++++----- .../lib/active_storage/service/disk_service.rb | 22 +++++++++++----- .../lib/active_storage/service/gcs_service.rb | 18 ++++++++----- .../lib/active_storage/service/mirror_service.rb | 5 ++++ .../lib/active_storage/service/s3_service.rb | 20 ++++++++++----- activestorage/test/models/blob_test.rb | 10 +++++++- activestorage/test/service/shared_service_tests.rb | 17 ++++++++++++ 10 files changed, 107 insertions(+), 31 deletions(-) diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 2aa05d665e..acaf22fac1 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -270,7 +270,8 @@ class ActiveStorage::Blob < ActiveRecord::Base # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+ # methods in most circumstances. def delete - service.delete key + service.delete(key) + service.delete_prefixed("variants/#{key}/") if image? end # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted diff --git a/activestorage/lib/active_storage/log_subscriber.rb b/activestorage/lib/active_storage/log_subscriber.rb index 5cbf4bd1a5..a4e148c1a5 100644 --- a/activestorage/lib/active_storage/log_subscriber.rb +++ b/activestorage/lib/active_storage/log_subscriber.rb @@ -18,6 +18,10 @@ module ActiveStorage info event, color("Deleted file from key: #{key_in(event)}", RED) end + def service_delete_prefixed(event) + info event, color("Deleted files by key prefix: #{event.payload[:prefix]}", RED) + end + def service_exist(event) debug event, color("Checked if file exists at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE) end diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb index aa150e4d8a..c8f675db86 100644 --- a/activestorage/lib/active_storage/service.rb +++ b/activestorage/lib/active_storage/service.rb @@ -78,6 +78,11 @@ module ActiveStorage raise NotImplementedError end + # Delete files at keys starting with the +prefix+. + def delete_prefixed(prefix) + raise NotImplementedError + end + # Return +true+ if a file exists at the +key+. def exist?(key) raise NotImplementedError @@ -104,10 +109,10 @@ module ActiveStorage end private - def instrument(operation, key, payload = {}, &block) + def instrument(operation, payload = {}, &block) ActiveSupport::Notifications.instrument( "service_#{operation}.active_storage", - payload.merge(key: key, service: service_name), &block) + payload.merge(service: service_name), &block) end def service_name diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index f3877ad9c9..98de0836de 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -19,7 +19,7 @@ module ActiveStorage end def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do + instrument :upload, key: key, checksum: checksum do begin blobs.create_block_blob(container, key, io, content_md5: checksum) rescue Azure::Core::Http::HTTPError @@ -30,11 +30,11 @@ module ActiveStorage def download(key, &block) if block_given? - instrument :streaming_download, key do + instrument :streaming_download, key: key do stream(key, &block) end else - instrument :download, key do + instrument :download, key: key do _, io = blobs.get_blob(container, key) io.force_encoding(Encoding::BINARY) end @@ -42,7 +42,7 @@ module ActiveStorage end def delete(key) - instrument :delete, key do + instrument :delete, key: key do begin blobs.delete_blob(container, key) rescue Azure::Core::Http::HTTPError @@ -51,8 +51,24 @@ module ActiveStorage end end + def delete_prefixed(prefix) + instrument :delete_all, prefix: prefix do + marker = nil + + loop do + results = blobs.list_blobs(container, prefix: prefix, marker: marker) + + results.each do |blob| + blobs.delete_blob(container, blob.name) + end + + break unless marker = results.continuation_token.presence + end + end + end + def exist?(key) - instrument :exist, key do |payload| + instrument :exist, key: key do |payload| answer = blob_for(key).present? payload[:exist] = answer answer @@ -60,7 +76,7 @@ module ActiveStorage end def url(key, expires_in:, filename:, disposition:, content_type:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| base_url = url_for(key) generated_url = signer.signed_uri( URI(base_url), false, @@ -77,7 +93,7 @@ module ActiveStorage end def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| base_url = url_for(key) generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw", expiry: format_expiry(expires_in)).to_s diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index 52eaba4e7b..a8728c5bc3 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -16,7 +16,7 @@ module ActiveStorage end def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do + instrument :upload, key: key, checksum: checksum do IO.copy_stream(io, make_path_for(key)) ensure_integrity_of(key, checksum) if checksum end @@ -24,7 +24,7 @@ module ActiveStorage def download(key) if block_given? - instrument :streaming_download, key do + instrument :streaming_download, key: key do File.open(path_for(key), "rb") do |file| while data = file.read(64.kilobytes) yield data @@ -32,14 +32,14 @@ module ActiveStorage end end else - instrument :download, key do + instrument :download, key: key do File.binread path_for(key) end end end def delete(key) - instrument :delete, key do + instrument :delete, key: key do begin File.delete path_for(key) rescue Errno::ENOENT @@ -48,8 +48,16 @@ module ActiveStorage end end + def delete_prefixed(prefix) + instrument :delete_prefixed, prefix: prefix do + Dir.glob(path_for("#{prefix}*")).each do |path| + FileUtils.rm_rf(path) + end + end + end + def exist?(key) - instrument :exist, key do |payload| + instrument :exist, key: key do |payload| answer = File.exist? path_for(key) payload[:exist] = answer answer @@ -57,7 +65,7 @@ module ActiveStorage end def url(key, expires_in:, filename:, disposition:, content_type:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key) generated_url = @@ -77,7 +85,7 @@ module ActiveStorage end def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| verified_token_with_expiration = ActiveStorage.verifier.generate( { key: key, diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index fd9916634a..c13ce4786d 100644 --- a/activestorage/lib/active_storage/service/gcs_service.rb +++ b/activestorage/lib/active_storage/service/gcs_service.rb @@ -14,7 +14,7 @@ module ActiveStorage end def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do + instrument :upload, key: key, checksum: checksum do begin bucket.create_file(io, key, md5: checksum) rescue Google::Cloud::InvalidArgumentError @@ -25,7 +25,7 @@ module ActiveStorage # FIXME: Download in chunks when given a block. def download(key) - instrument :download, key do + instrument :download, key: key do io = file_for(key).download io.rewind @@ -38,7 +38,7 @@ module ActiveStorage end def delete(key) - instrument :delete, key do + instrument :delete, key: key do begin file_for(key).delete rescue Google::Cloud::NotFoundError @@ -47,8 +47,14 @@ module ActiveStorage end end + def delete_prefixed(prefix) + instrument :delete_prefixed, prefix: prefix do + bucket.files(prefix: prefix).all(&:delete) + end + end + def exist?(key) - instrument :exist, key do |payload| + instrument :exist, key: key do |payload| answer = file_for(key).exists? payload[:exist] = answer answer @@ -56,7 +62,7 @@ module ActiveStorage end def url(key, expires_in:, filename:, content_type:, disposition:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| generated_url = file_for(key).signed_url expires: expires_in, query: { "response-content-disposition" => content_disposition_with(type: disposition, filename: filename), "response-content-type" => content_type @@ -69,7 +75,7 @@ module ActiveStorage end def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_type: content_type, content_md5: checksum diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb index 39e922f7ab..7eca8ce7f4 100644 --- a/activestorage/lib/active_storage/service/mirror_service.rb +++ b/activestorage/lib/active_storage/service/mirror_service.rb @@ -35,6 +35,11 @@ module ActiveStorage perform_across_services :delete, key end + # Delete files at keys starting with the +prefix+ on all services. + def delete_prefixed(prefix) + perform_across_services :delete_prefixed, prefix + end + private def each_service(&block) [ primary, *mirrors ].each(&block) diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb index 6957119780..c95672f338 100644 --- a/activestorage/lib/active_storage/service/s3_service.rb +++ b/activestorage/lib/active_storage/service/s3_service.rb @@ -17,7 +17,7 @@ module ActiveStorage end def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do + instrument :upload, key: key, checksum: checksum do begin object_for(key).put(upload_options.merge(body: io, content_md5: checksum)) rescue Aws::S3::Errors::BadDigest @@ -28,24 +28,30 @@ module ActiveStorage def download(key, &block) if block_given? - instrument :streaming_download, key do + instrument :streaming_download, key: key do stream(key, &block) end else - instrument :download, key do + instrument :download, key: key do object_for(key).get.body.read.force_encoding(Encoding::BINARY) end end end def delete(key) - instrument :delete, key do + instrument :delete, key: key do object_for(key).delete end end + def delete_prefixed(prefix) + instrument :delete_prefixed, prefix: prefix do + bucket.objects(prefix: prefix).batch_delete! + end + end + def exist?(key) - instrument :exist, key do |payload| + instrument :exist, key: key do |payload| answer = object_for(key).exists? payload[:exist] = answer answer @@ -53,7 +59,7 @@ module ActiveStorage end def url(key, expires_in:, filename:, disposition:, content_type:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| generated_url = object_for(key).presigned_url :get, expires_in: expires_in.to_i, response_content_disposition: content_disposition_with(type: disposition, filename: filename), response_content_type: content_type @@ -65,7 +71,7 @@ module ActiveStorage end def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| + instrument :url, key: key do |payload| generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i, content_type: content_type, content_length: content_length, content_md5: checksum diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index 6e815997ba..f94e65ed77 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -41,13 +41,21 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase end end - test "purge removes from external service" do + test "purge deletes file from external service" do blob = create_blob blob.purge assert_not ActiveStorage::Blob.service.exist?(blob.key) end + test "purge deletes variants from external service" do + blob = create_file_blob + variant = blob.variant(resize: "100>").processed + + blob.purge + assert_not ActiveStorage::Blob.service.exist?(variant.key) + end + private def expected_url_for(blob, disposition: :inline) query_string = { content_type: blob.content_type, disposition: "#{disposition}; #{blob.filename.parameters}" }.to_param diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb index ade91ab89a..ce28c4393a 100644 --- a/activestorage/test/service/shared_service_tests.rb +++ b/activestorage/test/service/shared_service_tests.rb @@ -75,5 +75,22 @@ module ActiveStorage::Service::SharedServiceTests @service.delete SecureRandom.base58(24) end end + + test "deleting by prefix" do + begin + @service.upload("a/a/a", StringIO.new(FIXTURE_DATA)) + @service.upload("a/a/b", StringIO.new(FIXTURE_DATA)) + @service.upload("a/b/a", StringIO.new(FIXTURE_DATA)) + + @service.delete_prefixed("a/a/") + assert_not @service.exist?("a/a/a") + assert_not @service.exist?("a/a/b") + assert @service.exist?("a/b/a") + ensure + @service.delete("a/a/a") + @service.delete("a/a/b") + @service.delete("a/b/a") + end + end end end -- cgit v1.2.3 From d0f5dce492696019ddf409892829f89bee5f45ef Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Sun, 3 Dec 2017 15:00:05 +0900 Subject: `change_column_default` should be executed after type changing If do not execute a type changing first, filling in default value may be failed. ``` % ARCONN=postgresql be ruby -w -Itest test/cases/migration/compatibility_test.rb -n test_legacy_change_column_with_null_executes_update Using postgresql Run options: -n test_legacy_change_column_with_null_executes_update --seed 20459 E Error: ActiveRecord::Migration::CompatibilityTest#test_legacy_change_column_with_null_executes_update: StandardError: An error has occurred, this and all later migrations canceled: PG::StringDataRightTruncation: ERROR: value too long for type character varying(5) : UPDATE "testings" SET "foo"='foobar' WHERE "foo" IS NULL ``` --- .../connection_adapters/postgresql/schema_statements.rb | 15 ++++++++------- .../lib/active_record/migration/compatibility.rb | 17 ++++++++++------- activerecord/test/cases/migration/compatibility_test.rb | 4 ++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 2e9e74403b..bf5fbb30e1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -393,9 +393,9 @@ module ActiveRecord end def change_column(table_name, column_name, type, options = {}) #:nodoc: - quoted_table_name = quote_table_name(table_name) + clear_cache! sqls, procs = change_column_for_alter(table_name, column_name, type, options) - execute "ALTER TABLE #{quoted_table_name} #{sqls.join(", ")}" + execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}" procs.each(&:call) end @@ -646,8 +646,7 @@ module ActiveRecord end end - def change_column_for_alter(table_name, column_name, type, options = {}) - clear_cache! + def change_column_sql(table_name, column_name, type, options = {}) quoted_column_name = quote_column_name(column_name) sql_type = type_to_sql(type, options) sql = "ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup @@ -661,7 +660,11 @@ module ActiveRecord sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" end - sqls = [sql] + sql + end + + def change_column_for_alter(table_name, column_name, type, options = {}) + sqls = [change_column_sql(table_name, column_name, type, options)] procs = [] sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default) sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null) @@ -673,7 +676,6 @@ module ActiveRecord # Changes the default value of a table column. def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc: - clear_cache! column = column_for(table_name, column_name) return unless column @@ -689,7 +691,6 @@ module ActiveRecord end def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc: - clear_cache! "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL" end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index da307c1723..bd8c054c28 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -16,13 +16,16 @@ module ActiveRecord V5_2 = Current class V5_1 < V5_2 - if adapter_name == "PostgreSQL" - def change_column(table_name, column_name, type, options = {}) - unless options[:null] || options[:default].nil? - column = connection.send(:column_for, table_name, column_name) - connection.execute("UPDATE #{connection.quote_table_name(table_name)} SET #{connection.quote_column_name(column_name)}=#{connection.quote_default_expression(options[:default], column)} WHERE #{connection.quote_column_name(column_name)} IS NULL") if column - end - connection.change_column(table_name, column_name, type, options) + def change_column(table_name, column_name, type, options = {}) + if adapter_name == "PostgreSQL" + clear_cache! + sql = connection.send(:change_column_sql, table_name, column_name, type, options) + execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}" + change_column_default(table_name, column_name, options[:default]) if options.key?(:default) + change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) + else + super end end end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index e6ca252369..cc2391f349 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -16,7 +16,7 @@ module ActiveRecord ActiveRecord::Migration.verbose = false connection.create_table :testings do |t| - t.column :foo, :string, limit: 100 + t.column :foo, :string, limit: 5 t.column :bar, :string, limit: 100 end end @@ -134,7 +134,7 @@ module ActiveRecord def test_legacy_change_column_with_null_executes_update migration = Class.new(ActiveRecord::Migration[5.1]) { def migrate(x) - change_column :testings, :foo, :string, null: false, default: "foobar" + change_column :testings, :foo, :string, limit: 10, null: false, default: "foobar" end }.new -- cgit v1.2.3 From 70fa9e9ab7fd89589664ecd7ee367448ef45f9d8 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Sun, 3 Dec 2017 15:28:10 +0900 Subject: Emulate JSON types for SQLite3 adapter (#29664) Actually SQLite3 doesn't have JSON storage class (so it is stored as a TEXT like Date and Time). But emulating JSON types is convinient for making database agnostic migrations. --- .../connection_adapters/abstract/schema_definitions.rb | 1 + .../active_record/connection_adapters/mysql/schema_definitions.rb | 4 ---- .../connection_adapters/postgresql/schema_definitions.rb | 4 ---- .../lib/active_record/connection_adapters/sqlite3_adapter.rb | 7 ++++++- activerecord/test/cases/adapters/sqlite3/json_test.rb | 4 ++-- activerecord/test/cases/json_shared_test_cases.rb | 2 -- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 86406d95ad..0594b4b485 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -220,6 +220,7 @@ module ActiveRecord :decimal, :float, :integer, + :json, :string, :text, :time, diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index da25e4863c..2ed4ad16ae 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -32,10 +32,6 @@ module ActiveRecord args.each { |name| column(name, :longtext, options) } end - def json(*args, **options) - args.each { |name| column(name, :json, options) } - end - def unsigned_integer(*args, **options) args.each { |name| column(name, :unsigned_integer, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index a3bb66bf3e..6047217fcd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -95,10 +95,6 @@ module ActiveRecord args.each { |name| column(name, :int8range, options) } end - def json(*args, **options) - args.each { |name| column(name, :json, options) } - end - def jsonb(*args, **options) args.each { |name| column(name, :jsonb, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index daece2bffd..ff63f63bf7 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -70,7 +70,8 @@ module ActiveRecord time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, - boolean: { name: "boolean" } + boolean: { name: "boolean" }, + json: { name: "json" }, } ## @@ -134,6 +135,10 @@ module ActiveRecord true end + def supports_json? + true + end + def supports_multi_insert? sqlite_version >= "3.7.11" end diff --git a/activerecord/test/cases/adapters/sqlite3/json_test.rb b/activerecord/test/cases/adapters/sqlite3/json_test.rb index 568a524058..6f247fcd22 100644 --- a/activerecord/test/cases/adapters/sqlite3/json_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/json_test.rb @@ -9,8 +9,8 @@ class SQLite3JSONTest < ActiveRecord::SQLite3TestCase def setup super @connection.create_table("json_data_type") do |t| - t.column "payload", :json, default: {} - t.column "settings", :json + t.json "payload", default: {} + t.json "settings" end end diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb index a71485982c..56ec8c8a82 100644 --- a/activerecord/test/cases/json_shared_test_cases.rb +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -30,7 +30,6 @@ module JSONSharedTestCases end def test_change_table_supports_json - skip unless @connection.supports_json? @connection.change_table("json_data_type") do |t| t.public_send column_type, "users" end @@ -41,7 +40,6 @@ module JSONSharedTestCases end def test_schema_dumping - skip unless @connection.supports_json? output = dump_table_schema("json_data_type") assert_match(/t\.#{column_type}\s+"settings"/, output) end -- cgit v1.2.3 From 9b823c02b62a91216ef1ad0c9d4fca095377afb6 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Sun, 3 Dec 2017 15:45:40 +0900 Subject: SQLite3 valid integer value should be 8 bytes (64-bit signed integer) (#28379) This is a regression since Rails 4.2. SQLite3 integer is stored in 1, 2, 3, 4, 6, or 8 bytes depending on the magnitude of the value. Assuming default valid value as 4 bytes caused that actual valid value in INTEGER storage class cannot be stored and existing value cannot be found. https://www.sqlite.org/datatype3.html We should allow valid value in INTEGER storage class in SQLite3 to fix the regression. Fixes #22594. --- .../active_record/connection_adapters/sqlite3_adapter.rb | 15 +++++++++++++++ activerecord/test/cases/base_test.rb | 8 +++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index ff63f63bf7..574a7b4572 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -374,6 +374,10 @@ module ActiveRecord end private + def initialize_type_map(m = type_map) + super + register_class_with_limit m, %r(int)i, SQLite3Integer + end def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") @@ -529,6 +533,17 @@ module ActiveRecord def configure_connection execute("PRAGMA foreign_keys = ON", "SCHEMA") end + + class SQLite3Integer < Type::Integer # :nodoc: + private + def _limit + # INTEGER storage class can be stored 8 bytes value. + # See https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes + limit || 8 + end + end + + ActiveRecord::Type.register(:integer, SQLite3Integer, adapter: :sqlite3) end ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter) end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index d79afa2ee9..3497f6aae4 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -891,11 +891,9 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 2147483648, company.rating end - unless current_adapter?(:SQLite3Adapter) - def test_bignum_pk - company = Company.create!(id: 2147483648, name: "foo") - assert_equal company, Company.find(company.id) - end + def test_bignum_pk + company = Company.create!(id: 2147483648, name: "foo") + assert_equal company, Company.find(company.id) end # TODO: extend defaults tests to other databases! -- cgit v1.2.3 From 7609ca08ce5000689838eeb04ed37084bf364f78 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen <kaspth@gmail.com> Date: Sun, 3 Dec 2017 18:06:29 +0100 Subject: Fix instrumention name: delete_prefixed like the others. --- activestorage/lib/active_storage/service/azure_storage_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index 98de0836de..19b09991b3 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -52,7 +52,7 @@ module ActiveStorage end def delete_prefixed(prefix) - instrument :delete_all, prefix: prefix do + instrument :delete_prefixed, prefix: prefix do marker = nil loop do -- cgit v1.2.3 From c8f014ff1f68bca8deb22f57dd8c7fde9deb7030 Mon Sep 17 00:00:00 2001 From: Kasper Timm Hansen <kaspth@gmail.com> Date: Sun, 3 Dec 2017 19:12:06 +0100 Subject: Embrace the instantiation in loving parens <3 --- actionpack/test/dispatch/session/cookie_store_test.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 7f70961465..e34426a471 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -13,8 +13,9 @@ class CookieStoreTest < ActionDispatch::IntegrationTest Generator = ActiveSupport::KeyGenerator.new(SessionSecret, iterations: 1000) Rotations = ActiveSupport::Messages::RotationConfiguration.new - Encryptor = ActiveSupport::MessageEncryptor.new \ + Encryptor = ActiveSupport::MessageEncryptor.new( Generator.generate_key(SessionSalt, 32), cipher: "aes-256-gcm", serializer: Marshal + ) class TestController < ActionController::Base def no_session_access -- cgit v1.2.3 From 20bb26a428b84ae4f7b09bec284152325b0334f6 Mon Sep 17 00:00:00 2001 From: bogdanvlviv <bogdanvlviv@gmail.com> Date: Mon, 4 Dec 2017 01:18:59 +0200 Subject: Update "Active Record Query Interface" guide [ci skip] - Add missing `LIMIT 1` for some queries - Make some examples of query more readable --- guides/source/active_record_querying.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 3786343fc3..4e28e31a53 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -801,7 +801,7 @@ The SQL that would be executed: SELECT * FROM articles WHERE id > 10 ORDER BY id DESC # Original query without `only` -SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20 +SELECT * FROM articles WHERE id > 10 ORDER BY id DESC LIMIT 20 ``` @@ -820,14 +820,14 @@ Article.find(10).comments.reorder('name') The SQL that would be executed: ```sql -SELECT * FROM articles WHERE id = 10 +SELECT * FROM articles WHERE id = 10 LIMIT 1 SELECT * FROM comments WHERE article_id = 10 ORDER BY name ``` In the case where the `reorder` clause is not used, the SQL executed would be: ```sql -SELECT * FROM articles WHERE id = 10 +SELECT * FROM articles WHERE id = 10 LIMIT 1 SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC ``` @@ -1091,7 +1091,7 @@ This produces: ```sql SELECT articles.* FROM articles - INNER JOIN categories ON articles.category_id = categories.id + INNER JOIN categories ON categories.id = articles.category_id INNER JOIN comments ON comments.article_id = articles.id ``` @@ -1871,14 +1871,14 @@ All calculation methods work directly on a model: ```ruby Client.count -# SELECT count(*) AS count_all FROM clients +# SELECT COUNT(*) FROM clients ``` Or on a relation: ```ruby Client.where(first_name: 'Ryan').count -# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan') +# SELECT COUNT(*) FROM clients WHERE (first_name = 'Ryan') ``` You can also use various finder methods on a relation for performing complex calculations: @@ -1890,9 +1890,9 @@ Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' Which will execute: ```sql -SELECT count(DISTINCT clients.id) AS count_all FROM clients - LEFT OUTER JOIN orders ON orders.client_id = clients.id WHERE - (clients.first_name = 'Ryan' AND orders.status = 'received') +SELECT COUNT(DISTINCT clients.id) FROM clients + LEFT OUTER JOIN orders ON orders.client_id = clients.id + WHERE (clients.first_name = 'Ryan' AND orders.status = 'received') ``` ### Count -- cgit v1.2.3 From 07788c7ad8bad797ec97cba038e37e007f343afa Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Mon, 4 Dec 2017 08:18:31 +0900 Subject: `current_version` should catch `NoDatabaseError` from `get_all_versions` `get_all_versions` doesn't use passed `connection`. So it should be caught `NoDatabaseError` from `SchemaMigration.table_exists?`. --- activerecord/lib/active_record/migration.rb | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4d4b0dc67a..5c10d4ff24 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1045,7 +1045,7 @@ module ActiveRecord new(:up, migrations(migrations_paths), nil) end - def get_all_versions(connection = Base.connection) + def get_all_versions if SchemaMigration.table_exists? SchemaMigration.all_versions.map(&:to_i) else @@ -1054,19 +1054,12 @@ module ActiveRecord end def current_version(connection = nil) - if connection.nil? - begin - connection = Base.connection - rescue ActiveRecord::NoDatabaseError - return nil - end - end - - get_all_versions(connection).max || 0 + get_all_versions.max || 0 + rescue ActiveRecord::NoDatabaseError end - def needs_migration?(connection = Base.connection) - (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0 + def needs_migration?(connection = nil) + (migrations(migrations_paths).collect(&:version) - get_all_versions).size > 0 end def any_migrations? -- cgit v1.2.3 From 915f0e682cecf80080914d0390ce22eb06f41470 Mon Sep 17 00:00:00 2001 From: Tsukuru Tanimichi <info+git@ttanimichi.com> Date: Wed, 29 Nov 2017 18:44:46 +0900 Subject: Add tests for the `--webpack` option We probably don't have any tests for the `--webpack` option. related: #27288 --- railties/test/generators/app_generator_test.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 774fd0f315..c9c85078d4 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -743,6 +743,20 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_webpack_option + command_check = -> command, *_ do + @called ||= 0 + @called += 1 if command == "webpacker:install" + assert_equal 1, @called, "webpacker:install expected to be called once, but was called #{@called} times." + end + + generator([destination_root], webpack: true).stub(:rails_command, command_check) do + quietly { generator.invoke_all } + end + + assert_gem "webpacker" + end + def test_generator_if_skip_turbolinks_is_given run_generator [destination_root, "--skip-turbolinks"] -- cgit v1.2.3 From 649f19cab1d4dd805c915912ede29c86655084cd Mon Sep 17 00:00:00 2001 From: Yoshiyuki Hirano <yhirano@me.com> Date: Tue, 5 Dec 2017 15:25:12 +0900 Subject: Fix example code in ActiveJob::Core [ci skip] 1) It seems that it raise error on example code in `ActiveJob::Core`. Before: ```ruby class DeliverWebhookJob < ActiveJob::Base def serialize super.merge('attempt_number' => (@attempt_number || 0) + 1) end def deserialize(job_data) super @attempt_number = job_data['attempt_number'] end rescue_from(Timeout::Error) do |exception| raise exception if @attempt_number > 5 retry_job(wait: 10) end def perform raise Timeout::Error end end ``` Then it run `DeliverWebhookJob.perform_now` in `rails console`. And raise error: NoMethodError: undefined method `>' for nil:NilClass from /app/jobs/deliver_webhook_job.rb:12:in `block in <class:DeliverWebhookJob>' So I thought it's necessary to fix it. After: ```ruby class DeliverWebhookJob < ActiveJob::Base attr_writer :attempt_number def attempt_number @attempt_number ||= 0 end def serialize super.merge('attempt_number' => attempt_number + 1) end def deserialize(job_data) super self.attempt_number = job_data['attempt_number'] end rescue_from(Timeout::Error) do |exception| raise exception if attempt_number > 5 retry_job(wait: 10) end def perform raise Timeout::Error end end ``` Then it run `DeliverWebhookJob.perform_now` in `rails console`. And it does'nt raise error NoMethodError. 2) Use `Timeout::Error` instead of `TimeoutError` (`TimeoutError` is deprecated). --- activejob/lib/active_job/core.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index c4e12fc518..879746fc01 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -97,17 +97,23 @@ module ActiveJob # ==== Examples # # class DeliverWebhookJob < ActiveJob::Base + # attr_writer :attempt_number + # + # def attempt_number + # @attempt_number ||= 0 + # end + # # def serialize - # super.merge('attempt_number' => (@attempt_number || 0) + 1) + # super.merge('attempt_number' => attempt_number + 1) # end # # def deserialize(job_data) # super - # @attempt_number = job_data['attempt_number'] + # self.attempt_number = job_data['attempt_number'] # end # - # rescue_from(TimeoutError) do |exception| - # raise exception if @attempt_number > 5 + # rescue_from(Timeout::Error) do |exception| + # raise exception if attempt_number > 5 # retry_job(wait: 10) # end # end -- cgit v1.2.3 From 3c442b6df91e291ebbf17f37444414bf5f10fbe6 Mon Sep 17 00:00:00 2001 From: Simon Dawson <spdawson@gmail.com> Date: Tue, 5 Dec 2017 07:13:48 +0000 Subject: Fix CSP copy boolean directives (#31326) Use Object#deep_dup to safely duplicate policy values --- actionpack/lib/action_dispatch/http/content_security_policy.rb | 6 +----- actionpack/test/dispatch/content_security_policy_test.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb index d10d4faf3d..c888a27720 100644 --- a/actionpack/lib/action_dispatch/http/content_security_policy.rb +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -110,7 +110,7 @@ module ActionDispatch #:nodoc: end def initialize_copy(other) - @directives = copy_directives(other.directives) + @directives = other.directives.deep_dup end DIRECTIVES.each do |name, directive| @@ -174,10 +174,6 @@ module ActionDispatch #:nodoc: end private - def copy_directives(directives) - directives.transform_values { |sources| sources.map(&:dup) } - end - def apply_mappings(sources) sources.map do |source| case source diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb index 8a1ac066e8..7c4a65a633 100644 --- a/actionpack/test/dispatch/content_security_policy_test.rb +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -14,6 +14,15 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase assert_equal "script-src 'self';", @policy.build end + def test_dup + @policy.img_src :self + @policy.block_all_mixed_content + @policy.upgrade_insecure_requests + @policy.sandbox + copied = @policy.dup + assert_equal copied.build, @policy.build + end + def test_mappings @policy.script_src :data assert_equal "script-src data:;", @policy.build -- cgit v1.2.3 From 7efb4d23c1022108319add6218ae9d9284936ac5 Mon Sep 17 00:00:00 2001 From: "yuuji.yaginuma" <yuuji.yaginuma@gmail.com> Date: Tue, 5 Dec 2017 18:16:41 +0900 Subject: Add missing require Follow up of 3c442b6df91e291ebbf17f37444414bf5f10fbe6 Without this require, it will fail when run CSP test alone. Ref: https://travis-ci.org/rails/rails/jobs/311715758#L2976 --- actionpack/lib/action_dispatch/http/content_security_policy.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb index c888a27720..4883e23d24 100644 --- a/actionpack/lib/action_dispatch/http/content_security_policy.rb +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/object/deep_dup" + module ActionDispatch #:nodoc: class ContentSecurityPolicy class Middleware -- cgit v1.2.3 From b9fb74514b752df3b707fa420e09dca459a3844f Mon Sep 17 00:00:00 2001 From: Tsukuru Tanimichi <info+git@ttanimichi.com> Date: Tue, 5 Dec 2017 18:41:44 +0900 Subject: Modify `test_webpack_option` --- railties/test/generators/app_generator_test.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index decea77d48..68b31148d6 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -746,11 +746,13 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_webpack_option command_check = -> command, *_ do @called ||= 0 - @called += 1 if command == "webpacker:install" - assert_equal 1, @called, "webpacker:install expected to be called once, but was called #{@called} times." + if command == "webpacker:install" + @called += 1 + assert_equal 1, @called, "webpacker:install expected to be called once, but was called #{@called} times." + end end - generator([destination_root], webpack: true).stub(:rails_command, command_check) do + generator([destination_root], webpack: "webpack").stub(:rails_command, command_check) do quietly { generator.invoke_all } end -- cgit v1.2.3 From 6a11b0c1549e88a7ca32a73764d8b6634dc326e2 Mon Sep 17 00:00:00 2001 From: Tsukuru Tanimichi <info+git@ttanimichi.com> Date: Tue, 5 Dec 2017 18:43:15 +0900 Subject: Add more tests for the `--webpack` option --- railties/test/generators/app_generator_test.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 68b31148d6..96803db838 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -759,6 +759,25 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_gem "webpacker" end + def test_webpack_option_with_js_framework + command_check = -> command, *_ do + case command + when "webpacker:install" + @webpacker ||= 0 + @webpacker += 1 + assert_equal 1, @webpacker, "webpacker:install expected to be called once, but was called #{@webpacker} times." + when "webpacker:install:react" + @react ||= 0 + @react += 1 + assert_equal 1, @react, "webpacker:install:react expected to be called once, but was called #{@react} times." + end + end + + generator([destination_root], webpack: "react").stub(:rails_command, command_check) do + quietly { generator.invoke_all } + end + end + def test_generator_if_skip_turbolinks_is_given run_generator [destination_root, "--skip-turbolinks"] -- cgit v1.2.3 From 6ec0ed67d9afcc666ad0424b10e9903f63e60714 Mon Sep 17 00:00:00 2001 From: Joe Francis <joe@lostapathy.com> Date: Tue, 5 Dec 2017 08:23:30 -0600 Subject: only install ffmpeg and mupdf on activestorage builds These are needed when GEM=ast, thanks @georgeclaghorn --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 290e0b5f2b..fd3b3f9002 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,10 +20,6 @@ addons: sources: - sourceline: "ppa:mc3man/trusty-media" - sourceline: "ppa:ubuntuhandbook1/apps" - packages: - - ffmpeg - - mupdf - - mupdf-tools bundler_args: --without test --jobs 3 --retry 3 before_install: @@ -42,6 +38,7 @@ before_script: # Decodes to e.g. `export VARIABLE=VALUE` - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX0FDQ0VTU19LRVk9YTAzNTM0M2YtZTkyMi00MGIzLWFhM2MtMDZiM2VhNjM1YzQ4") - $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX1VTRVJOQU1FPXJ1YnlvbnJhaWxz") + - if [[ $GEM = *ast* ]] ; then sudo apt-get update && sudo apt-get -y install ffmpeg mupdf mupdf-tools ; fi script: 'ci/travis.rb' -- cgit v1.2.3 From 29d081c47a43cc5d979836541cb73a816c95981d Mon Sep 17 00:00:00 2001 From: Yasuo Honda <yasuo.honda@gmail.com> Date: Tue, 5 Dec 2017 19:16:11 +0000 Subject: Execute `JsonAttributeTest` only if `supports_json?` returns `true` Oracle enhanced adapter does not fully support JSON datatype then `supports_json?` returns `false`. I wanted to skip known failures and errors when tested with Oracle enhanced adapter. --- activerecord/test/cases/json_attribute_test.rb | 46 ++++++++++++++------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb index 63f3c77fc3..a6fd4f34dc 100644 --- a/activerecord/test/cases/json_attribute_test.rb +++ b/activerecord/test/cases/json_attribute_test.rb @@ -3,33 +3,35 @@ require "cases/helper" require "cases/json_shared_test_cases" -class JsonAttributeTest < ActiveRecord::TestCase - include JSONSharedTestCases - self.use_transactional_tests = false +if ActiveRecord::Base.connection.supports_json? + class JsonAttributeTest < ActiveRecord::TestCase + include JSONSharedTestCases + self.use_transactional_tests = false - class JsonDataTypeOnText < ActiveRecord::Base - self.table_name = "json_data_type" + class JsonDataTypeOnText < ActiveRecord::Base + self.table_name = "json_data_type" - attribute :payload, :json - attribute :settings, :json + attribute :payload, :json + attribute :settings, :json - store_accessor :settings, :resolution - end - - def setup - super - @connection.create_table("json_data_type") do |t| - t.text "payload" - t.text "settings" + store_accessor :settings, :resolution end - end - private - def column_type - :text + def setup + super + @connection.create_table("json_data_type") do |t| + t.text "payload" + t.text "settings" + end end - def klass - JsonDataTypeOnText - end + private + def column_type + :text + end + + def klass + JsonDataTypeOnText + end + end end -- cgit v1.2.3 From d2321aacc7451f6b4f154d894adcb76820abba3c Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Wed, 6 Dec 2017 08:30:27 +0900 Subject: Revert "Merge pull request #31341 from yahonda/skip_json_attribute_test" This reverts commit 23226d04f921b79f0077ba38c5a5a923b6d43f89, reversing changes made to 7544cf7603959f25100b21f70b5e70354bed7e45. --- activerecord/test/cases/json_attribute_test.rb | 46 ++++++++++++-------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb index a6fd4f34dc..63f3c77fc3 100644 --- a/activerecord/test/cases/json_attribute_test.rb +++ b/activerecord/test/cases/json_attribute_test.rb @@ -3,35 +3,33 @@ require "cases/helper" require "cases/json_shared_test_cases" -if ActiveRecord::Base.connection.supports_json? - class JsonAttributeTest < ActiveRecord::TestCase - include JSONSharedTestCases - self.use_transactional_tests = false +class JsonAttributeTest < ActiveRecord::TestCase + include JSONSharedTestCases + self.use_transactional_tests = false - class JsonDataTypeOnText < ActiveRecord::Base - self.table_name = "json_data_type" + class JsonDataTypeOnText < ActiveRecord::Base + self.table_name = "json_data_type" - attribute :payload, :json - attribute :settings, :json + attribute :payload, :json + attribute :settings, :json - store_accessor :settings, :resolution - end + store_accessor :settings, :resolution + end - def setup - super - @connection.create_table("json_data_type") do |t| - t.text "payload" - t.text "settings" - end + def setup + super + @connection.create_table("json_data_type") do |t| + t.text "payload" + t.text "settings" end + end - private - def column_type - :text - end + private + def column_type + :text + end - def klass - JsonDataTypeOnText - end - end + def klass + JsonDataTypeOnText + end end -- cgit v1.2.3 From d721344280346f67c7d2dbffa9eaf9341e73673d Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Wed, 6 Dec 2017 08:33:16 +0900 Subject: Use `:string` instead of `:text` for `JsonAttributeTest` Since CLOB data type has many limitations in Oracle SELECT WHERE clause. --- activerecord/test/cases/json_attribute_test.rb | 6 +++--- activerecord/test/cases/json_shared_test_cases.rb | 9 +++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb index 63f3c77fc3..afc39d0420 100644 --- a/activerecord/test/cases/json_attribute_test.rb +++ b/activerecord/test/cases/json_attribute_test.rb @@ -19,14 +19,14 @@ class JsonAttributeTest < ActiveRecord::TestCase def setup super @connection.create_table("json_data_type") do |t| - t.text "payload" - t.text "settings" + t.string "payload" + t.string "settings" end end private def column_type - :text + :string end def klass diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb index 56ec8c8a82..012aaffde1 100644 --- a/activerecord/test/cases/json_shared_test_cases.rb +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -23,7 +23,7 @@ module JSONSharedTestCases def test_column column = klass.columns_hash["payload"] assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type + assert_type_match column_type, column.sql_type type = klass.type_for_attribute("payload") assert_not type.binary? @@ -36,7 +36,7 @@ module JSONSharedTestCases klass.reset_column_information column = klass.columns_hash["users"] assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type + assert_type_match column_type, column.sql_type end def test_schema_dumping @@ -253,4 +253,9 @@ module JSONSharedTestCases def klass JsonDataType end + + def assert_type_match(type, sql_type) + native_type = ActiveRecord::Base.connection.native_database_types[type][:name] + assert_match %r(\A#{native_type}\b), sql_type + end end -- cgit v1.2.3 From 5f7fd1584f131d0afad558eaba6017e08df65892 Mon Sep 17 00:00:00 2001 From: Yoshiyuki Hirano <yhirano@me.com> Date: Wed, 6 Dec 2017 09:09:32 +0900 Subject: Add `assert_in_epsilon` to Testing guide [ci skip] I found `assert_in_epsilon` is not be in "2.4 Available Assertions". So I Added them. MiniTest::Assertions#assert_in_epsilon: https://github.com/seattlerb/minitest/blob/master/lib/minitest/assertions.rb#L204-L210 --- guides/source/testing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/guides/source/testing.md b/guides/source/testing.md index e0a2d281d9..9692f50b6e 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -319,6 +319,8 @@ specify to make your test failure messages clearer. | `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.| | `assert_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.| | `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.| +| `assert_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` have a relative error less than `epsilon`.| +| `assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` don't have a relative error less than `epsilon`.| | `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.| | `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.| | `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.| -- cgit v1.2.3 From d7a121a0d624dd8180556521f0611d4c8e2c648f Mon Sep 17 00:00:00 2001 From: Dominic Cleal <dominic@cleal.org> Date: Tue, 24 Jan 2017 10:55:43 +0000 Subject: Yield array from AC::Parameters#each for block with one arg Matches Hash#each behaviour as used in Rails 4. --- .../lib/action_controller/metal/strong_parameters.rb | 2 +- actionpack/test/controller/parameters/accessors_test.rb | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index ef7c4c4c16..a56ac749f8 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -335,7 +335,7 @@ module ActionController # the same way as <tt>Hash#each_pair</tt>. def each_pair(&block) @parameters.each_pair do |key, value| - yield key, convert_hashes_to_parameters(key, value) + yield [key, convert_hashes_to_parameters(key, value)] end end alias_method :each, :each_pair diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 43cabae7d2..154430d4b0 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -51,6 +51,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase @params.each { |key, value| assert_not(value.permitted?) if key == "person" } end + test "each returns key,value array for block with arity 1" do + @params.each do |arg| + assert_kind_of Array, arg + assert_equal "person", arg[0] + assert_kind_of ActionController::Parameters, arg[1] + end + end + test "each_pair carries permitted status" do @params.permit! @params.each_pair { |key, value| assert(value.permitted?) if key == "person" } @@ -60,6 +68,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase @params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" } end + test "each_pair returns key,value array for block with arity 1" do + @params.each_pair do |arg| + assert_kind_of Array, arg + assert_equal "person", arg[0] + assert_kind_of ActionController::Parameters, arg[1] + end + end + test "empty? returns true when params contains no key/value pairs" do params = ActionController::Parameters.new assert params.empty? -- cgit v1.2.3 From b34838cbe29434212eb81ca1e31bb0ab0765e32a Mon Sep 17 00:00:00 2001 From: Yasuo Honda <yasuo.honda@gmail.com> Date: Wed, 6 Dec 2017 13:09:51 +0000 Subject: Address `ActiveRecord::NotNullViolation: OCIError: ORA-01400` for Oracle database which requires primary key value mentioned in insert statement explicitly. --- activerecord/test/cases/json_shared_test_cases.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb index 012aaffde1..b0c0f2c283 100644 --- a/activerecord/test/cases/json_shared_test_cases.rb +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -66,26 +66,26 @@ module JSONSharedTestCases end def test_rewrite - @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + @connection.execute(insert_statement_per_database('{"k":"v"}')) x = klass.first x.payload = { '"a\'' => "b" } assert x.save! end def test_select - @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + @connection.execute(insert_statement_per_database('{"k":"v"}')) x = klass.first assert_equal({ "k" => "v" }, x.payload) end def test_select_multikey - @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|) + @connection.execute(insert_statement_per_database('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')) x = klass.first assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) end def test_null_json - @connection.execute("insert into json_data_type (payload) VALUES(null)") + @connection.execute(insert_statement_per_database("null")) x = klass.first assert_nil(x.payload) end @@ -107,13 +107,13 @@ module JSONSharedTestCases end def test_select_array_json_value - @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]')) x = klass.first assert_equal(["v0", { "k1" => "v1" }], x.payload) end def test_rewrite_array_json_value - @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]')) x = klass.first x.payload = ["v1", { "k2" => "v2" }, "v3"] assert x.save! @@ -258,4 +258,12 @@ module JSONSharedTestCases native_type = ActiveRecord::Base.connection.native_database_types[type][:name] assert_match %r(\A#{native_type}\b), sql_type end + + def insert_statement_per_database(values) + if current_adapter?(:OracleAdapter) + "insert into json_data_type (id, payload) VALUES (json_data_type_seq.nextval, '#{values}')" + else + "insert into json_data_type (payload) VALUES ('#{values}')" + end + end end -- cgit v1.2.3 From 5ac4f4d2563e7f9c5ffaecce4be4b9e2c5b0c081 Mon Sep 17 00:00:00 2001 From: Ashley Ellis Pierce <anellis12@gmail.com> Date: Mon, 4 Dec 2017 16:44:33 -0500 Subject: Fix sqlite migrations with custom primary keys Previously, if a record was created with a custom primary key, that table could not be migrated using sqlite. While attempting to copy the table, the type of the primary key was ignored. Once that was corrected, copying the indexes would fail because custom primary keys are autoindexed by sqlite by default. To correct that, this skips copying the index if the index name begins with "sqlite_". This is a reserved word that indicates that the index is an internal schema object. SQLite prohibits applications from creating objects whose names begin with "sqlite_", so this string should be safe to use as a check. ref https://www.sqlite.org/fileformat2.html#intschema --- .../connection_adapters/sqlite3_adapter.rb | 6 +++++- .../cases/adapters/sqlite3/sqlite3_adapter_test.rb | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index daece2bffd..3197b522b3 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -395,10 +395,11 @@ module ActiveRecord def copy_table(from, to, options = {}) from_primary_key = primary_key(from) + from_primary_key_column = columns(from).select { |column| column.name == from_primary_key }.first options[:id] = false create_table(to, options) do |definition| @definition = definition - @definition.primary_key(from_primary_key) if from_primary_key.present? + @definition.primary_key(from_primary_key, from_primary_key_column.type) if from_primary_key.present? columns(from).each do |column| column_name = options[:rename] ? (options[:rename][column.name] || @@ -422,6 +423,9 @@ module ActiveRecord def copy_table_indexes(from, to, rename = {}) indexes(from).each do |index| name = index.name + # indexes sqlite creates for internal use start with `sqlite_` and + # don't need to be copied + next if name.starts_with?("sqlite_") if to == "a#{from}" name = "t#{name}" elsif from == "a#{to}" diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 1f057fe5c6..14f4997d5b 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -360,6 +360,24 @@ module ActiveRecord end end + class Barcode < ActiveRecord::Base + end + + def test_existing_records_have_custom_primary_key + connection = Barcode.connection + connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) do |t| + t.text :other_attr + end + code = "214fe0c2-dd47-46df-b53b-66090b3c1d40" + Barcode.create! code: code, other_attr: "xxx" + + connection.change_table "barcodes" do |t| + connection.remove_column("barcodes", "other_attr") + end + + assert_equal code, Barcode.first.id + end + def test_supports_extensions assert_not @conn.supports_extensions?, "does not support extensions" end -- cgit v1.2.3 From faf169eedd10326233eaea55ed76089ede608336 Mon Sep 17 00:00:00 2001 From: Philip Tolton <mrtolton@gmail.com> Date: Wed, 6 Dec 2017 16:01:00 -0500 Subject: Correct routing test spelling mistake. --- actionpack/test/controller/routing_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index f09051b306..71b01c36a7 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -213,7 +213,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal expected, ActiveSupport::JSON.decode(get(u)) end - def test_regexp_precidence + def test_regexp_precedence rs.draw do get "/whois/:domain", constraints: { domain: /\w+\.[\w\.]+/ }, -- cgit v1.2.3 From 2fa29037678874b25ad257dff8974055e25c9384 Mon Sep 17 00:00:00 2001 From: Yoshiyuki Hirano <yhirano@me.com> Date: Thu, 7 Dec 2017 14:46:59 +0900 Subject: [ci skip] Make Todo classes inherit ApplicationRecord Example codes that use `has_many` or `before_create` in `Module::Concerning` look like active record models. So I've made them inherit `ApplicationRecord`. --- activesupport/lib/active_support/core_ext/module/concerning.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb index 370a948eea..800bf213cc 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -22,7 +22,7 @@ class Module # # == Using comments: # - # class Todo + # class Todo < ApplicationRecord # # Other todo implementation # # ... # @@ -42,7 +42,7 @@ class Module # # Noisy syntax. # - # class Todo + # class Todo < ApplicationRecord # # Other todo implementation # # ... # @@ -70,7 +70,7 @@ class Module # increased overhead can be a reasonable tradeoff even if it reduces our # at-a-glance perception of how things work. # - # class Todo + # class Todo < ApplicationRecord # # Other todo implementation # # ... # @@ -82,7 +82,7 @@ class Module # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to # separate bite-sized concerns. # - # class Todo + # class Todo < ApplicationRecord # # Other todo implementation # # ... # @@ -101,7 +101,7 @@ class Module # end # # Todo.ancestors - # # => [Todo, Todo::EventTracking, Object] + # # => [Todo, Todo::EventTracking, ApplicationRecord, Object] # # This small step has some wonderful ripple effects. We can # * grok the behavior of our class in one glance, -- cgit v1.2.3 From 82b974813b28748e5affcff1d8c4ad60ab2971be Mon Sep 17 00:00:00 2001 From: bogdanvlviv <bogdanvlviv@gmail.com> Date: Thu, 7 Dec 2017 20:02:34 +0200 Subject: Add headless firefox driver to System Tests --- actionpack/CHANGELOG.md | 4 ++++ actionpack/lib/action_dispatch/system_test_case.rb | 8 ++++++-- actionpack/lib/action_dispatch/system_testing/driver.rb | 13 ++++++++++++- actionpack/test/abstract_unit.rb | 4 ++++ actionpack/test/dispatch/system_testing/driver_test.rb | 8 ++++++++ .../test/dispatch/system_testing/system_test_case_test.rb | 6 ++++++ guides/source/testing.md | 3 ++- 7 files changed, 42 insertions(+), 4 deletions(-) diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index d120d15770..753dd8589a 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,7 @@ +* Add headless firefox support to System Tests. + + *bogdanvlviv* + * Changed the default system test screenshot output from `inline` to `simple`. `inline` works well for iTerm2 but not everyone uses iTerm2. Some terminals like diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index 7246e01cff..99d0c06751 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -121,11 +121,15 @@ module ActionDispatch # # driven_by :poltergeist # - # driven_by :selenium, using: :firefox + # driven_by :selenium, screen_size: [800, 800] + # + # driven_by :selenium, using: :chrome # # driven_by :selenium, using: :headless_chrome # - # driven_by :selenium, screen_size: [800, 800] + # driven_by :selenium, using: :firefox + # + # driven_by :selenium, using: :headless_firefox def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}) self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options) end diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb index 2687772b4b..280989a146 100644 --- a/actionpack/lib/action_dispatch/system_testing/driver.rb +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -37,6 +37,11 @@ module ActionDispatch browser_options.args << "--headless" browser_options.args << "--disable-gpu" + @options.merge(options: browser_options) + elsif @browser == :headless_firefox + browser_options = Selenium::WebDriver::Firefox::Options.new + browser_options.args << "-headless" + @options.merge(options: browser_options) else @options @@ -44,7 +49,13 @@ module ActionDispatch end def browser - @browser == :headless_chrome ? :chrome : @browser + if @browser == :headless_chrome + :chrome + elsif @browser == :headless_firefox + :firefox + else + @browser + end end def register_selenium(app) diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 5262e85a28..55ad9c245e 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -453,3 +453,7 @@ end class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase driven_by :selenium, using: :headless_chrome end + +class DrivenBySeleniumWithHeadlessFirefox < ActionDispatch::SystemTestCase + driven_by :selenium, using: :headless_firefox +end diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb index 75feae6fe0..fcdaf7fb4c 100644 --- a/actionpack/test/dispatch/system_testing/driver_test.rb +++ b/actionpack/test/dispatch/system_testing/driver_test.rb @@ -25,6 +25,14 @@ class DriverTest < ActiveSupport::TestCase assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) end + test "initializing the driver with a headless firefox" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_firefox, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) + assert_equal :selenium, driver.instance_variable_get(:@name) + assert_equal :headless_firefox, driver.instance_variable_get(:@browser) + assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) + assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) + end + test "initializing the driver with a poltergeist" do driver = ActionDispatch::SystemTesting::Driver.new(:poltergeist, screen_size: [1400, 1400], options: { js_errors: false }) assert_equal :poltergeist, driver.instance_variable_get(:@name) diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb index c6a6aef92b..b078a5abc5 100644 --- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb +++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb @@ -28,6 +28,12 @@ class SetDriverToSeleniumHeadlessChromeTest < DrivenBySeleniumWithHeadlessChrome end end +class SetDriverToSeleniumHeadlessFirefoxTest < DrivenBySeleniumWithHeadlessFirefox + test "uses selenium headless firefox" do + assert_equal :selenium, Capybara.current_driver + end +end + class SetHostTest < DrivenByRackTest test "sets default host" do assert_equal "http://127.0.0.1", Capybara.app_host diff --git a/guides/source/testing.md b/guides/source/testing.md index 9692f50b6e..f28c4c224a 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -673,7 +673,8 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase end ``` -If you want to use a headless browser, you could use Headless Chrome by adding `headless_chrome` in the `:using` argument. +If you want to use a headless browser, you could use Headless Chrome or Headless Firefox by adding +`headless_chrome` or `headless_firefox` in the `:using` argument. ```ruby require "test_helper" -- cgit v1.2.3 From 6a8ce7416d6615a13ee5c4b9f6bcd91cc5adef4d Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Fri, 8 Dec 2017 02:37:02 +0900 Subject: Fix `scope_for_create` to do not lose polymorphic associations This regression was caused at 213796fb due to polymorphic predicates are combined by `Arel::Nodes::And`. But I'd like to keep that combined because it would help inverting polymorphic predicates correctly (e9ba12f7), and we can collect equality nodes regardless of combined by `Arel::Nodes::And` (`a AND (b AND c) AND d` == `a AND b AND c AND d`). This change fixes the regression to collect equality nodes in `Arel::Nodes::And` as well. Fixes #31338. --- .../lib/active_record/relation/where_clause.rb | 18 ++++++++++++++++-- activerecord/test/cases/relation_test.rb | 2 +- activerecord/test/cases/relations_test.rb | 9 +++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 752bb38481..a502713e56 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -47,7 +47,7 @@ module ActiveRecord end def to_h(table_name = nil) - equalities = predicates.grep(Arel::Nodes::Equality) + equalities = equalities(predicates) if table_name equalities = equalities.select do |node| node.left.relation.name == table_name @@ -90,6 +90,20 @@ module ActiveRecord end private + def equalities(predicates) + equalities = [] + + predicates.each do |node| + case node + when Arel::Nodes::Equality + equalities << node + when Arel::Nodes::And + equalities.concat equalities(node.children) + end + end + + equalities + end def predicates_unreferenced_by(other) predicates.reject do |n| @@ -121,7 +135,7 @@ module ActiveRecord end def except_predicates(columns) - self.predicates.reject do |node| + predicates.reject do |node| case node when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index a71d8de521..b424ca91de 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -68,7 +68,7 @@ module ActiveRecord relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) - combine = left.and right + combine = left.or(right) relation.where! combine assert_equal({}, relation.where_values_hash) end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 50ad1d5b26..675aafabda 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1189,6 +1189,15 @@ class RelationTest < ActiveRecord::TestCase assert_equal "hen", hen.name end + def test_create_with_polymorphic_association + author = authors(:david) + post = posts(:welcome) + comment = Comment.where(post: post, author: author).create!(body: "hello") + + assert_equal author, comment.author + assert_equal post, comment.post + end + def test_first_or_create parrot = Bird.where(color: "green").first_or_create(name: "parrot") assert_kind_of Bird, parrot -- cgit v1.2.3 From e8286ee272a3e51daebc198519accd1f6895a8d2 Mon Sep 17 00:00:00 2001 From: George Claghorn <george@basecamp.com> Date: Thu, 7 Dec 2017 15:14:22 -0500 Subject: Fix customizing Content-Type via GCS service URLs --- activestorage/lib/active_storage/service/gcs_service.rb | 8 +++++++- activestorage/test/service/gcs_service_test.rb | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index c13ce4786d..6f6f4105fe 100644 --- a/activestorage/lib/active_storage/service/gcs_service.rb +++ b/activestorage/lib/active_storage/service/gcs_service.rb @@ -16,7 +16,13 @@ module ActiveStorage def upload(key, io, checksum: nil) instrument :upload, key: key, checksum: checksum do begin - bucket.create_file(io, key, md5: checksum) + # The official GCS client library doesn't allow us to create a file with no Content-Type metadata. + # We need the file we create to have no Content-Type so we can control it via the response-content-type + # param in signed URLs. Workaround: let the GCS client create the file with an inferred + # Content-Type (usually "application/octet-stream") then clear it. + bucket.create_file(io, key, md5: checksum).update do |file| + file.content_type = nil + end rescue Google::Cloud::InvalidArgumentError raise ActiveStorage::IntegrityError end diff --git a/activestorage/test/service/gcs_service_test.rb b/activestorage/test/service/gcs_service_test.rb index 1860149da9..7efcd60fb7 100644 --- a/activestorage/test/service/gcs_service_test.rb +++ b/activestorage/test/service/gcs_service_test.rb @@ -35,6 +35,20 @@ if SERVICE_CONFIGURATIONS[:gcs] assert_match(/storage\.googleapis\.com\/.*response-content-disposition=inline.*test\.txt.*response-content-type=text%2Fplain/, @service.url(FIXTURE_KEY, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")) end + + test "signed URL response headers" do + begin + key = SecureRandom.base58(24) + data = "Something else entirely!" + @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data)) + + url = @service.url(key, expires_in: 2.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain") + response = Net::HTTP.get_response(URI(url)) + assert_equal "text/plain", response.header["Content-Type"] + ensure + @service.delete key + end + end end else puts "Skipping GCS Service tests because no GCS configuration was supplied" -- cgit v1.2.3 From 131cc6eab64eeae6c7f508dca4176183144cf3a6 Mon Sep 17 00:00:00 2001 From: Ryuta Kamizono <kamipo@gmail.com> Date: Fri, 8 Dec 2017 10:49:25 +0900 Subject: SQLite: Fix `copy_table` with composite primary keys `connection.primary_key` also return composite primary keys, so `from_primary_key_column` may not be found even if `from_primary_key` is presented. ``` % ARCONN=sqlite3 be ruby -w -Itest test/cases/adapters/sqlite3/sqlite3_adapter_test.rb -n test_copy_table_with_composite_primary_keys Using sqlite3 Run options: -n test_copy_table_with_composite_primary_keys --seed 19041 # Running: E Error: ActiveRecord::ConnectionAdapters::SQLite3AdapterTest#test_copy_table_with_composite_primary_keys: NoMethodError: undefined method `type' for nil:NilClass /path/to/rails/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb:411:in `block in copy_table' ``` This change fixes `copy_table` to do not lose composite primary keys. --- .../connection_adapters/sqlite3_adapter.rb | 10 ++++--- .../cases/adapters/sqlite3/sqlite3_adapter_test.rb | 31 ++++++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 65aba20052..c72db15ce3 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -404,22 +404,24 @@ module ActiveRecord def copy_table(from, to, options = {}) from_primary_key = primary_key(from) - from_primary_key_column = columns(from).select { |column| column.name == from_primary_key }.first options[:id] = false create_table(to, options) do |definition| @definition = definition - @definition.primary_key(from_primary_key, from_primary_key_column.type) if from_primary_key.present? + if from_primary_key.is_a?(Array) + @definition.primary_keys from_primary_key + end columns(from).each do |column| column_name = options[:rename] ? (options[:rename][column.name] || options[:rename][column.name.to_sym] || column.name) : column.name - next if column_name == from_primary_key @definition.column(column_name, column.type, limit: column.limit, default: column.default, precision: column.precision, scale: column.scale, - null: column.null, collation: column.collation) + null: column.null, collation: column.collation, + primary_key: column_name == from_primary_key + ) end yield @definition if block_given? end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 14f4997d5b..1357719422 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -361,21 +361,48 @@ module ActiveRecord end class Barcode < ActiveRecord::Base + self.primary_key = "code" end - def test_existing_records_have_custom_primary_key + def test_copy_table_with_existing_records_have_custom_primary_key connection = Barcode.connection connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) do |t| t.text :other_attr end code = "214fe0c2-dd47-46df-b53b-66090b3c1d40" - Barcode.create! code: code, other_attr: "xxx" + Barcode.create!(code: code, other_attr: "xxx") connection.change_table "barcodes" do |t| connection.remove_column("barcodes", "other_attr") end assert_equal code, Barcode.first.id + ensure + Barcode.reset_column_information + end + + def test_copy_table_with_composite_primary_keys + connection = Barcode.connection + connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t| + t.string :region + t.string :code + t.text :other_attr + end + region = "US" + code = "214fe0c2-dd47-46df-b53b-66090b3c1d40" + Barcode.create!(region: region, code: code, other_attr: "xxx") + + connection.change_table "barcodes" do |t| + connection.remove_column("barcodes", "other_attr") + end + + assert_equal ["region", "code"], connection.primary_keys("barcodes") + + barcode = Barcode.first + assert_equal region, barcode.region + assert_equal code, barcode.code + ensure + Barcode.reset_column_information end def test_supports_extensions -- cgit v1.2.3 From da8e0ba03cbae33857954c0c1a228bd6dae562da Mon Sep 17 00:00:00 2001 From: George Claghorn <george@basecamp.com> Date: Fri, 8 Dec 2017 13:15:04 -0500 Subject: Swap raw video width and height if angle is 90 or 270 degrees --- .../lib/active_storage/analyzer/video_analyzer.rb | 14 +++++++++++++- activestorage/test/analyzer/video_analyzer_test.rb | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb index b6fc54d917..1c144baa37 100644 --- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb +++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb @@ -31,10 +31,18 @@ module ActiveStorage private def width - Integer(video_stream["width"]) if video_stream["width"] + rotated? ? raw_height : raw_width end def height + rotated? ? raw_width : raw_height + end + + def raw_width + Integer(video_stream["width"]) if video_stream["width"] + end + + def raw_height Integer(video_stream["height"]) if video_stream["height"] end @@ -52,6 +60,10 @@ module ActiveStorage end end + def rotated? + angle == 90 || angle == 270 + end + def tags @tags ||= video_stream["tags"] || {} diff --git a/activestorage/test/analyzer/video_analyzer_test.rb b/activestorage/test/analyzer/video_analyzer_test.rb index 4a3c4a8bfc..b3b9c97fe4 100644 --- a/activestorage/test/analyzer/video_analyzer_test.rb +++ b/activestorage/test/analyzer/video_analyzer_test.rb @@ -21,8 +21,8 @@ class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase blob = create_file_blob(filename: "rotated_video.mp4", content_type: "video/mp4") metadata = blob.tap(&:analyze).metadata - assert_equal 640, metadata[:width] - assert_equal 480, metadata[:height] + assert_equal 480, metadata[:width] + assert_equal 640, metadata[:height] assert_equal [4, 3], metadata[:aspect_ratio] assert_equal 5.227975, metadata[:duration] assert_equal 90, metadata[:angle] -- cgit v1.2.3