diff options
186 files changed, 2788 insertions, 1561 deletions
diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml index c8033d8c30..d73b2e3362 100644 --- a/.github/autolabeler.yml +++ b/.github/autolabeler.yml @@ -4,8 +4,6 @@ actionmailer: - "actionmailer/**/*" actionpack: - "actionpack/**/*" -routing: - - "actionpack/**/*routing*" actionview: - "actionview/**/*" activejob: @@ -14,12 +12,6 @@ activemodel: - "activemodel/**/*" activerecord: - "activerecord/**/*" -MySQL: - - "activerecord/**/*mysql*" -PostgreSQL: - - "activerecord/**/*postgresql*" -enum: - - "activerecord/**/*enum*" activestorage: - "activestorage/**/*" activesupport: @@ -28,19 +20,5 @@ rails-ujs: - "actionview/app/assets/javascripts/rails-ujs*/*" railties: - "railties/**/*" -engines: - - "railties/lib/rails/engine/**/*" - - "railties/test/railties/**/*engine*" docs: - "guides/**/*" -asset pipeline: - - "guides/source/asset_pipeline.md" -ci issues: - - "ci/**/*" -security: - - "**/*security*" - - "**/*secure*" - - "**/*sanitize*" - - "**/*sanitization*" -i18n: - - "**/*i18n*" diff --git a/.gitignore b/.gitignore index d44875ed9a..35440cb54f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ node_modules/ package-lock.json pkg/ /tmp/ -/yarn.lock
\ No newline at end of file +/yarn-error.log +/yarn.lock diff --git a/.rubocop.yml b/.rubocop.yml index 33791588cb..615a229432 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,6 +8,7 @@ AllCops: - '**/vendor/**/*' - 'actionpack/lib/action_dispatch/journey/parser.rb' - 'railties/test/fixtures/tmp/**/*' + - 'node_modules/**/*' Performance: Exclude: diff --git a/.travis.yml b/.travis.yml index a4f5fa045d..3db4e9cd94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,13 +31,11 @@ addons: - postgresql-10 - postgresql-client-10 -bundler_args: --without test --jobs 3 --retry 3 +bundler_args: --jobs 3 --retry 3 before_install: - "rm ${BUNDLE_GEMFILE}.lock" - "travis_retry gem update --system" - "travis_retry gem install bundler" - - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" - - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - "[[ -z $encrypted_0fb9444d0374_key && -z $encrypted_0fb9444d0374_iv ]] || openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d" - "[[ $GEM != 'av:ujs' ]] || nvm install node" - "[[ $GEM != 'av:ujs' ]] || node --version" @@ -101,23 +99,44 @@ matrix: - rvm: 2.5.1 env: "GEM=av:ujs" - rvm: 2.4.4 + sudo: required env: "GEM=aj:integration" services: - memcached - redis-server - rabbitmq + before_install: + - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" + - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - rvm: 2.5.1 + sudo: required env: "GEM=aj:integration" services: - memcached - redis-server - rabbitmq + before_install: + - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" + - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - rvm: ruby-head + sudo: required env: "GEM=aj:integration" services: - memcached - redis-server - rabbitmq + before_install: + - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf + - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" + - "sudo service postgresql restart 10" + - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" + - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - rvm: 2.4.4 env: "GEM=ar:mysql2" sudo: required @@ -99,6 +99,7 @@ instance_eval File.read local_gemfile if File.exist? local_gemfile group :test do gem "minitest-bisect" + gem "minitest-retry" platforms :mri do gem "stackprof" diff --git a/Gemfile.lock b/Gemfile.lock index 727489b50a..1321436814 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -306,7 +306,7 @@ GEM loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.0) + mail (2.7.1) mini_mime (>= 0.1.1) marcel (0.3.3) mimemagic (~> 0.3.2) @@ -323,6 +323,8 @@ GEM minitest-bisect (1.4.0) minitest-server (~> 1.0) path_expander (~> 1.0) + minitest-retry (0.1.9) + minitest (>= 5.0) minitest-server (1.0.5) minitest (~> 5.0) mono_logger (1.1.0) @@ -339,12 +341,12 @@ GEM mysql2 (0.5.2-x86-mingw32) nio4r (2.3.1) nio4r (2.3.1-java) - nokogiri (1.8.4) + nokogiri (1.8.5) mini_portile2 (~> 2.3.0) - nokogiri (1.8.4-java) - nokogiri (1.8.4-x64-mingw32) + nokogiri (1.8.5-java) + nokogiri (1.8.5-x64-mingw32) mini_portile2 (~> 2.3.0) - nokogiri (1.8.4-x86-mingw32) + nokogiri (1.8.5-x86-mingw32) mini_portile2 (~> 2.3.0) os (1.0.0) parallel (1.12.1) @@ -538,6 +540,7 @@ DEPENDENCIES libxml-ruby listen (>= 3.0.5, < 3.2) minitest-bisect + minitest-retry mysql2 (>= 0.4.10) nokogiri (>= 1.8.1) pg (>= 0.18.0) @@ -576,4 +579,4 @@ DEPENDENCIES websocket-client-simple! BUNDLED WITH - 1.16.5 + 1.16.6 diff --git a/actioncable/README.md b/actioncable/README.md index d6893dbab1..a2783d6f45 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -442,7 +442,7 @@ Beware that currently, the cable server will _not_ auto-reload any changes in th We'll get all this abstracted properly when the framework is integrated into Rails. -The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication). +The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this [article](https://greg.molnar.io/blog/actioncable-devise-authentication/). ## Dependencies diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt index 5da1ce2dce..0cfcf74919 100644 --- a/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt +++ b/actioncable/lib/rails/generators/channel/templates/javascript/index.js.tt @@ -1,5 +1,5 @@ -// Load all the channels within this directory and all subdirectories. +// Load all the channels within this directory and all subdirectories. // Channel files must be named *_channel.js. -const channels = require.context('.', true, /\_channel\.js$/) +const channels = require.context('.', true, /_channel\.js$/) channels.keys().forEach(channels) diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 1aa7485d3e..db6fc7ee9c 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,14 @@ +* Allow ActionMailer classes to configure the parameterized delivery job + Example: + ``` + class MyMailer < ApplicationMailer + self.parameterized_delivery_job = MyCustomDeliveryJob + ... + end + ``` + + *Luke Pearce* + * `ActionDispatch::IntegrationTest` includes `ActionMailer::TestHelper` module by default. *Ricardo Díaz* diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 02e5ac2a3e..509d859ac3 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -462,6 +462,7 @@ module ActionMailer helper ActionMailer::MailHelper class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob + class_attribute :parameterized_delivery_job, default: ::ActionMailer::Parameterized::DeliveryJob class_attribute :default_params, default: { mime_version: "1.0", charset: "UTF-8", diff --git a/actionmailer/lib/action_mailer/parameterized.rb b/actionmailer/lib/action_mailer/parameterized.rb index 5e768e7106..0fe417affe 100644 --- a/actionmailer/lib/action_mailer/parameterized.rb +++ b/actionmailer/lib/action_mailer/parameterized.rb @@ -140,7 +140,8 @@ module ActionMailer super else args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args - ActionMailer::Parameterized::DeliveryJob.set(options).perform_later(*args) + job = @mailer_class.parameterized_delivery_job + job.set(options).perform_later(*args) end end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 7a1a505398..cbbee9fae8 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -152,9 +152,9 @@ class BaseTest < ActiveSupport::TestCase assert_equal(2, email.parts.length) assert_equal("multipart/mixed", email.mime_type) assert_equal("text/html", email.parts[0].mime_type) - assert_equal("Attachment with content", email.parts[0].body.encoded) + assert_equal("Attachment with content", email.parts[0].decoded) assert_equal("application/pdf", email.parts[1].mime_type) - assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) + assert_equal("This is test File content", email.parts[1].decoded) end test "adds the given :body as part" do @@ -162,9 +162,9 @@ class BaseTest < ActiveSupport::TestCase assert_equal(2, email.parts.length) assert_equal("multipart/mixed", email.mime_type) assert_equal("text/plain", email.parts[0].mime_type) - assert_equal("I'm the eggman", email.parts[0].body.encoded) + assert_equal("I'm the eggman", email.parts[0].decoded) assert_equal("application/pdf", email.parts[1].mime_type) - assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) + assert_equal("This is test File content", email.parts[1].decoded) end test "can embed an inline attachment" do diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb index ec6c5e9e67..d3b34fc3e3 100644 --- a/actionmailer/test/parameterized_test.rb +++ b/actionmailer/test/parameterized_test.rb @@ -53,4 +53,17 @@ class ParameterizedTest < ActiveSupport::TestCase invitation = mailer.method(:anything) end end + + test "should enqueue a parameterized request with the correct delivery job" do + old_delivery_job = ParamsMailer.parameterized_delivery_job + ParamsMailer.parameterized_delivery_job = ParameterizedDummyJob + + assert_performed_with(job: ParameterizedDummyJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do + @mail.deliver_later + end + + ParamsMailer.parameterized_delivery_job = old_delivery_job + end + + class ParameterizedDummyJob < ActionMailer::Parameterized::DeliveryJob; end end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index c972c64766..5554d4e6b8 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,38 @@ +* Use request object for context if there's no controller + + There is no controller instance when using a redirect route or a + mounted rack application so pass the request object as the context + when resolving dynamic CSP sources in this scenario. + + Fixes #34200. + + *Andrew White* + +* Apply mapping to symbols returned from dynamic CSP sources + + Previously if a dynamic source returned a symbol such as :self it + would be converted to a string implicity, e.g: + + policy.default_src -> { :self } + + would generate the header: + + Content-Security-Policy: default-src self + + and now it generates: + + Content-Security-Policy: default-src 'self' + + *Andrew White* + +* Add `ActionController::Parameters#each_value`. + + *Lukáš Zapletal* + +* Deprecate `ActionDispatch::Http::ParameterFilter` in favor of `ActiveSupport::ParameterFilter`. + + *Yoshiyuki Kinjo* + * Remove undocumented `params` option from `url_for` helper. *Ilkka Oksanen* diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb index b6e5631a4e..fbd93705ed 100644 --- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb +++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb @@ -7,7 +7,7 @@ module AbstractController Module.new do define_method(:inherited) do |klass| super(klass) - if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } + if namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } klass.include(namespace.railtie_routes_url_helpers(include_path_helpers)) else klass.include(routes.url_helpers(include_path_helpers)) diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index 6de1fb2c19..afbd38e7fe 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -18,14 +18,17 @@ module ActionController def process_action(event) info do - payload = event.payload + payload = event.payload additions = ActionController::Base.log_process_action(payload) - status = payload[:status] + if status.nil? && payload[:exception].present? exception_class_name = payload[:exception].first status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) end + + additions << "Allocations: #{event.allocations}" + message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" message << " (#{additions.join(" | ")})" unless additions.empty? message << "\n\n" if defined?(Rails.env) && Rails.env.development? diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index c1272ce667..04922b0715 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -352,7 +352,7 @@ module ActionController # the same way as <tt>Hash#each_value</tt>. def each_value(&block) @parameters.each_pair do |key, value| - yield [convert_hashes_to_parameters(key, value)] + yield convert_hashes_to_parameters(key, value) end end diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb index 50953e32b5..b1e5a28be5 100644 --- a/actionpack/lib/action_dispatch/http/content_security_policy.rb +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -22,7 +22,8 @@ module ActionDispatch #:nodoc: if policy = request.content_security_policy nonce = request.content_security_policy_nonce - headers[header_name(request)] = policy.build(request.controller_instance, nonce) + context = request.controller_instance || request + headers[header_name(request)] = policy.build(context, nonce) end response @@ -257,7 +258,8 @@ module ActionDispatch #:nodoc: if context.nil? raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}" else - context.instance_exec(&source) + resolved = context.instance_exec(&source) + resolved.is_a?(Symbol) ? apply_mapping(resolved) : resolved end else raise RuntimeError, "Unexpected content security policy source: #{source.inspect}" diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index ec012ad02d..9f8f178402 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "action_dispatch/http/parameter_filter" +require "active_support/parameter_filter" module ActionDispatch module Http @@ -28,8 +28,8 @@ module ActionDispatch # => reverses the value to all keys matching /secret/i module FilterParameters ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc: - NULL_PARAM_FILTER = ParameterFilter.new # :nodoc: - NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc: + NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc: + NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc: def initialize super @@ -69,7 +69,7 @@ module ActionDispatch end def parameter_filter_for(filters) # :doc: - ParameterFilter.new(filters) + ActiveSupport::ParameterFilter.new(filters) end KV_RE = "[^&;=]+" diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index dd74695229..c3e0ea3c89 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# -*- frozen-string-literal: true -*- - require "singleton" require "active_support/core_ext/string/starts_ends_with" diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb index 6689092859..ddeb3d81e2 100644 --- a/actionpack/lib/action_dispatch/http/parameter_filter.rb +++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb @@ -1,87 +1,12 @@ # frozen_string_literal: true -require "active_support/core_ext/object/duplicable" -require "active_support/core_ext/array/extract" +require "active_support/deprecation/constant_accessor" +require "active_support/parameter_filter" module ActionDispatch module Http - class ParameterFilter - FILTERED = "[FILTERED]" # :nodoc: - - def initialize(filters = []) - @filters = filters - end - - def filter(params) - compiled_filter.call(params) - end - - private - - def compiled_filter - @compiled_filter ||= CompiledFilter.compile(@filters) - end - - class CompiledFilter # :nodoc: - def self.compile(filters) - return lambda { |params| params.dup } if filters.empty? - - strings, regexps, blocks = [], [], [] - - filters.each do |item| - case item - when Proc - blocks << item - when Regexp - regexps << item - else - strings << Regexp.escape(item.to_s) - end - end - - deep_regexps = regexps.extract! { |r| r.to_s.include?("\\.") } - deep_strings = strings.extract! { |s| s.include?("\\.") } - - regexps << Regexp.new(strings.join("|"), true) unless strings.empty? - deep_regexps << Regexp.new(deep_strings.join("|"), true) unless deep_strings.empty? - - new regexps, deep_regexps, blocks - end - - attr_reader :regexps, :deep_regexps, :blocks - - def initialize(regexps, deep_regexps, blocks) - @regexps = regexps - @deep_regexps = deep_regexps.any? ? deep_regexps : nil - @blocks = blocks - end - - def call(params, parents = [], original_params = params) - filtered_params = params.class.new - - params.each do |key, value| - parents.push(key) if deep_regexps - if regexps.any? { |r| key =~ r } - value = FILTERED - elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| joined =~ r } - value = FILTERED - elsif value.is_a?(Hash) - value = call(value, parents, original_params) - elsif value.is_a?(Array) - value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v } - elsif blocks.any? - key = key.dup if key.duplicable? - value = value.dup if value.duplicable? - blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) } - end - parents.pop if deep_regexps - - filtered_params[key] = value - end - - filtered_params - end - end - end + include ActiveSupport::Deprecation::DeprecatedConstantAccessor + deprecate_constant "ParameterFilter", "ActiveSupport::ParameterFilter", + message: "ActionDispatch::Http::ParameterFilter is deprecated and will be removed from Rails 6.1. Use ActiveSupport::ParameterFilter instead." end end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index fedf622e90..2ae75b0da8 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -378,8 +378,6 @@ module ActionDispatch @disable_clear_and_finalize = false @finalized = false @env_key = "ROUTES_#{object_id}_SCRIPT_NAME" - @url_helpers = nil - @deferred_classes = [] @set = Journey::Routes.new @router = Journey::Router.new @set @@ -435,25 +433,6 @@ module ActionDispatch end private :eval_block - def include_helpers(klass, include_path_helpers) - if @finalized - include_helpers_now klass, include_path_helpers - else - @deferred_classes << [klass, include_path_helpers] - end - end - - def include_helpers_now(klass, include_path_helpers) - namespace = klass.module_parents.detect { |m| m.respond_to?(:railtie_include_helpers) } - - if namespace && namespace.railtie_namespace.routes != self - namespace.railtie_include_helpers(klass, include_path_helpers) - else - klass.include(url_helpers(include_path_helpers)) - end - end - private :include_helpers_now - def finalize! return if @finalized @append.each { |blk| eval_block(blk) } diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 9f1fb3d042..7789e654d5 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -77,11 +77,15 @@ class ParametersAccessorsTest < ActiveSupport::TestCase test "each_value carries permitted status" do @params.permit! - @params["person"].each_value { |value| assert(value.permitted?) if value == 32 } + @params.each_value do |value| + assert_predicate(value, :permitted?) + end end test "each_value carries unpermitted status" do - @params["person"].each_value { |value| assert_not(value.permitted?) if value == 32 } + @params.each_value do |value| + assert_not_predicate(value, :permitted?) + end end test "each_key converts to hash for permitted" do diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb index 13ad22b5c5..c8c885f35c 100644 --- a/actionpack/test/dispatch/content_security_policy_test.rb +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -260,12 +260,13 @@ class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationT ROUTES.draw do scope module: "default_content_security_policy_integration_test" do get "/", to: "policy#index" + get "/redirect", to: redirect("/") end end POLICY = ActionDispatch::ContentSecurityPolicy.new do |p| - p.default_src :self - p.script_src :https + p.default_src -> { :self } + p.script_src -> { :https } end class PolicyConfigMiddleware @@ -295,14 +296,19 @@ class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationT def test_adds_nonce_to_script_src_content_security_policy_only_once get "/" get "/" + assert_response :success + assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='" + end + + def test_redirect_works_with_dynamic_sources + get "/redirect" + assert_response :redirect assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='" end private def assert_policy(expected, report_only: false) - assert_response :success - if report_only expected_header = "Content-Security-Policy-Report-Only" unexpected_header = "Content-Security-Policy" diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index c7b68e5266..9d1246b3a4 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -1059,47 +1059,9 @@ class RequestParameters < BaseRequestTest end class RequestParameterFilter < BaseRequestTest - test "process parameter filter" do - test_hashes = [ - [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'], - [{ "foo" => "bar" }, { "foo" => "[FILTERED]" }, %w'foo'], - [{ "foo" => "bar", "bar" => "foo" }, { "foo" => "[FILTERED]", "bar" => "foo" }, %w'foo baz'], - [{ "foo" => "bar", "baz" => "foo" }, { "foo" => "[FILTERED]", "baz" => "[FILTERED]" }, %w'foo baz'], - [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => "[FILTERED]", "bar" => "foo" } }, %w'fo'], - [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => "[FILTERED]" }, %w'f banana'], - [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => "[FILTERED]", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'], - [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => "[FILTERED]" }, "1"] }, [/foo/]]] - - test_hashes.each do |before_filter, after_filter, filter_words| - parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) - assert_equal after_filter, parameter_filter.filter(before_filter) - - filter_words << "blah" - filter_words << lambda { |key, value| - value.reverse! if key =~ /bargain/ - } - filter_words << lambda { |key, value, original_params| - value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello" - } - - parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) - before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } } - after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]", "hello" => "world!" } } } - - assert_equal after_filter, parameter_filter.filter(before_filter) - end - end - - test "parameter filter should maintain hash with indifferent access" do - test_hashes = [ - [{ "foo" => "bar" }.with_indifferent_access, ["blah"]], - [{ "foo" => "bar" }.with_indifferent_access, []] - ] - - test_hashes.each do |before_filter, filter_words| - parameter_filter = ActionDispatch::Http::ParameterFilter.new(filter_words) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, - parameter_filter.filter(before_filter) + test "parameter filter is deprecated" do + assert_deprecated do + ActionDispatch::Http::ParameterFilter.new(["blah"]) end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 82add522df..fd8bae0cdf 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,21 @@ +* Add allocations to template rendering instrumentation. + + Adds the allocations for template and partial rendering to the server output on render. + + ``` + Rendered posts/_form.html.erb (Duration: 7.1ms | Allocations: 6004) + Rendered posts/new.html.erb within layouts/application (Duration: 8.3ms | Allocations: 6654) + Completed 200 OK in 858ms (Views: 848.4ms | ActiveRecord: 0.4ms | Allocations: 1539564) + ``` + + *Eileen M. Uchitelle*, *Aaron Patterson* + +* Respect the `only_path` option passed to `url_for` when the options are passed in as an array + + Fixes #33237. + + *Joel Ambass* + * Deprecate calling private model methods from view helpers. For example, in methods like `options_from_collection_for_select` diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index db07b9d7fb..227f025385 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -18,7 +18,7 @@ module ActionView info do message = +" Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" + message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" end end @@ -26,7 +26,7 @@ module ActionView info do message = +" Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" + message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil? message end @@ -37,7 +37,7 @@ module ActionView info do " Rendered collection of #{from_rails_root(identifier)}" \ - " #{render_count(event.payload)} (#{event.duration.round(1)}ms)" + " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" end end diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index fd563f34a9..f8ea3aa770 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -84,25 +84,24 @@ module ActionView super(only_path: _generate_paths_by_default) when Hash options = options.symbolize_keys - unless options.key?(:only_path) - options[:only_path] = only_path?(options[:host]) - end + ensure_only_path_option(options) super(options) when ActionController::Parameters - unless options.key?(:only_path) - options[:only_path] = only_path?(options[:host]) - end + ensure_only_path_option(options) super(options) when :back _back_url when Array components = options.dup - if _generate_paths_by_default - polymorphic_path(components, components.extract_options!) + options = components.extract_options! + ensure_only_path_option(options) + + if options[:only_path] + polymorphic_path(components, options) else - polymorphic_url(components, components.extract_options!) + polymorphic_url(components, options) end else method = _generate_paths_by_default ? :path : :url @@ -138,8 +137,10 @@ module ActionView true end - def only_path?(host) - _generate_paths_by_default unless host + def ensure_only_path_option(options) + unless options.key?(:only_path) + options[:only_path] = _generate_paths_by_default unless options[:host] + end end end end diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb index 7cbd3aaf89..563044f11e 100644 --- a/actionview/test/activerecord/controller_runtime_test.rb +++ b/actionview/test/activerecord/controller_runtime_test.rb @@ -68,7 +68,7 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase wait assert_equal 2, @logger.logged(:info).size - assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[1]) + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[1]) end def test_runtime_reset_before_requests @@ -77,20 +77,20 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase wait assert_equal 2, @logger.logged(:info).size - assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0\.0ms\)/, @logger.logged(:info)[1]) + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[1]) end def test_log_with_active_record_when_post post :create wait - assert_match(/ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[2]) + assert_match(/ActiveRecord: ([1-9][\d.]+)ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[2]) end def test_log_with_active_record_when_redirecting get :redirect wait assert_equal 3, @logger.logged(:info).size - assert_match(/\(ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[2]) + assert_match(/\(ActiveRecord: [\d.]+ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[2]) end def test_include_time_query_time_after_rendering @@ -98,6 +98,6 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase wait assert_equal 2, @logger.logged(:info).size - assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[1]) + assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms \| Allocations: [\d.]+\)/, @logger.logged(:info)[1]) end end diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 7f4fd25573..9fcf80bb24 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -129,14 +129,14 @@ class AVLogSubscriberTest < ActiveSupport::TestCase wait *, cached_inner, uncached_outer = @logger.logged(:info) assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, cached_inner) - assert_match(/Rendered test\/_nested_cached_customer\.erb \(.*?ms\)$/, uncached_outer) + assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| Allocations: .*?\)$/, uncached_outer) # Second render hits the cache for the _cached_customer partial. Outer template's log shouldn't be affected. @view.render(partial: "test/nested_cached_customer", locals: { cached_customer: Customer.new("Stan") }) wait *, cached_inner, uncached_outer = @logger.logged(:info) assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache hit\]/, cached_inner) - assert_match(/Rendered test\/_nested_cached_customer\.erb \(.*?ms\)$/, uncached_outer) + assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| Allocations: .*?\)$/, uncached_outer) end end diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 6db9eb3be1..1ab28e4749 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -75,6 +75,15 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal "javascript:history.back()", url_for(:back) end + def test_url_for_with_array_defaults_to_only_path_true + assert_equal "/other", url_for([:other, { controller: "foo" }]) + end + + def test_url_for_with_array_and_only_path_set_to_false + default_url_options[:host] = "http://example.com" + assert_equal "http://example.com/other", url_for([:other, { controller: "foo", only_path: false }]) + end + def test_to_form_params_with_hash assert_equal( [{ name: "name", value: "David" }, { name: "nationality", value: "Danish" }], diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index 61d402cfca..62bb5861bb 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -6,35 +6,33 @@ module ActiveJob module Core extend ActiveSupport::Concern - included do - # Job arguments - attr_accessor :arguments - attr_writer :serialized_arguments + # Job arguments + attr_accessor :arguments + attr_writer :serialized_arguments - # Timestamp when the job should be performed - attr_accessor :scheduled_at + # Timestamp when the job should be performed + attr_accessor :scheduled_at - # Job Identifier - attr_accessor :job_id + # Job Identifier + attr_accessor :job_id - # Queue in which the job will reside. - attr_writer :queue_name + # Queue in which the job will reside. + attr_writer :queue_name - # Priority that the job will have (lower is more priority). - attr_writer :priority + # Priority that the job will have (lower is more priority). + attr_writer :priority - # ID optionally provided by adapter - attr_accessor :provider_job_id + # ID optionally provided by adapter + attr_accessor :provider_job_id - # Number of times this job has been executed (which increments on every retry, like after an exception). - attr_accessor :executions + # Number of times this job has been executed (which increments on every retry, like after an exception). + attr_accessor :executions - # I18n.locale to be used during the job. - attr_accessor :locale + # I18n.locale to be used during the job. + attr_accessor :locale - # Timezone to be used during the job. - attr_accessor :timezone - end + # Timezone to be used during the job. + attr_accessor :timezone # These methods will be included into any Active Job object, adding # helpers for de/serialization and creation of job instances. diff --git a/activejob/lib/active_job/queue_adapters/async_adapter.rb b/activejob/lib/active_job/queue_adapters/async_adapter.rb index ebf6f384e3..53a7e3d53e 100644 --- a/activejob/lib/active_job/queue_adapters/async_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/async_adapter.rb @@ -31,7 +31,7 @@ module ActiveJob # jobs. Since jobs share a single thread pool, long-running jobs will block # short-lived jobs. Fine for dev/test; bad for production. class AsyncAdapter - # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options. + # See {Concurrent::ThreadPoolExecutor}[https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/ThreadPoolExecutor.html] for executor options. def initialize(**executor_options) @scheduler = Scheduler.new(**executor_options) end diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 261b68d4f8..229855c5d9 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -197,7 +197,7 @@ module ActiveJob # assert_performed_jobs 2 # end # - # If a block is passed, that block should cause the specified number of + # If a block is passed, asserts that the block will cause the specified number of # jobs to be performed. # # def test_jobs_again @@ -279,7 +279,7 @@ module ActiveJob # end # end # - # If a block is passed, that block should not cause any job to be performed. + # If a block is passed, asserts that the block will not cause any job to be performed. # # def test_jobs_again # assert_no_performed_jobs do @@ -347,7 +347,7 @@ module ActiveJob # end # # - # If a block is passed, that block should cause the job to be + # If a block is passed, asserts that the block will cause the job to be # enqueued with the given arguments. # # def test_assert_enqueued_with diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb index eb179011d9..1163ae8178 100644 --- a/activejob/test/support/integration/adapters/backburner.rb +++ b/activejob/test/support/integration/adapters/backburner.rb @@ -8,7 +8,8 @@ module BackburnerJobsManager end unless can_run? puts "Cannot run integration tests for backburner. To be able to run integration tests for backburner you need to install and start beanstalkd.\n" - exit + status = ENV["CI"] ? false : true + exit status end end diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb index 2e7d327b37..f231e5e12d 100644 --- a/activejob/test/support/integration/adapters/que.rb +++ b/activejob/test/support/integration/adapters/que.rb @@ -32,7 +32,8 @@ module QueJobsManager rescue Sequel::DatabaseConnectionError puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n" - exit + status = ENV["CI"] ? false : true + exit status end def stop_workers diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb index dbbdc12b9d..2b5375461a 100644 --- a/activejob/test/support/integration/adapters/queue_classic.rb +++ b/activejob/test/support/integration/adapters/queue_classic.rb @@ -30,7 +30,8 @@ module QueueClassicJobsManager rescue PG::ConnectionBad puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n" - exit + status = ENV["CI"] ? false : true + exit status end def stop_workers diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb index 965e6e2e6c..eb8d4cc2d5 100644 --- a/activejob/test/support/integration/adapters/sneakers.rb +++ b/activejob/test/support/integration/adapters/sneakers.rb @@ -1,19 +1,8 @@ # frozen_string_literal: true require "sneakers/runner" -require "sneakers/publisher" require "timeout" -module Sneakers - class Publisher - def safe_ensure_connected - @mutex.synchronize do - ensure_connection! unless connected? - end - end - end -end - module SneakersJobsManager def setup ActiveJob::Base.queue_adapter = :sneakers @@ -29,7 +18,8 @@ module SneakersJobsManager log: Rails.root.join("log/sneakers.log").to_s unless can_run? puts "Cannot run integration tests for sneakers. To be able to run integration tests for sneakers you need to install and start rabbitmq.\n" - exit + status = ENV["CI"] ? false : true + exit status end end @@ -79,7 +69,7 @@ module SneakersJobsManager def bunny_publisher @bunny_publisher ||= begin p = ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper.send(:publisher) - p.safe_ensure_connected + p.ensure_connection! p end end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 1ad9071cc2..d8352343e9 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -474,5 +474,43 @@ module ActiveModel def _read_attribute(attr) __send__(attr) end + + module AttrNames # :nodoc: + DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/ + + # We want to generate the methods via module_eval rather than + # define_method, because define_method is slower on dispatch. + # Evaluating many similar methods may use more memory as the instruction + # sequences are duplicated and cached (in MRI). define_method may + # be slower on dispatch, but if you're careful about the closure + # created, then define_method will consume much less memory. + # + # But sometimes the database might return columns with + # characters that are not allowed in normal method names (like + # 'my_column(omg)'. So to work around this we first define with + # the __temp__ identifier, and then use alias method to rename + # it to what we want. + # + # We are also defining a constant to hold the frozen string of + # the attribute name. Using a constant means that we do not have + # to allocate an object on each call to the attribute method. + # Making it frozen means that it doesn't get duped when used to + # key the @attributes in read_attribute. + def self.define_attribute_accessor_method(mod, attr_name, writer: false) + method_name = "#{attr_name}#{'=' if writer}" + if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name) + yield method_name, "'#{attr_name}'.freeze" + else + safe_name = attr_name.unpack1("h*") + const_name = "ATTR_#{safe_name}" + const_set(const_name, attr_name) unless const_defined?(const_name) + temp_method_name = "__temp__#{safe_name}#{'=' if writer}" + attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}" + yield temp_method_name, attr_name_expr + mod.send(:alias_method, method_name, temp_method_name) + mod.send(:undef_method, temp_method_name) + end + end + end end end diff --git a/activemodel/lib/active_model/attributes.rb b/activemodel/lib/active_model/attributes.rb index 41fe5168f3..c3a446098c 100644 --- a/activemodel/lib/active_model/attributes.rb +++ b/activemodel/lib/active_model/attributes.rb @@ -29,17 +29,16 @@ module ActiveModel private def define_method_attribute=(name) - safe_name = name.unpack1("h*") - ActiveModel::AttributeMethods::AttrNames.set_name_cache safe_name, name - - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - name = ::ActiveModel::AttributeMethods::AttrNames::ATTR_#{safe_name} - write_attribute(name, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR + ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( + generated_attribute_methods, name, writer: true, + ) do |temp_method_name, attr_name_expr| + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{temp_method_name}(value) + name = #{attr_name_expr} + write_attribute(name, value) + end + RUBY + end end NO_DEFAULT_PROVIDED = Object.new # :nodoc: @@ -97,15 +96,4 @@ module ActiveModel write_attribute(attribute_name, value) end end - - module AttributeMethods #:nodoc: - AttrNames = Module.new { - def self.set_name_cache(name, value) - const_name = "ATTR_#{name}" - unless const_defined? const_name - const_set const_name, -value - end - end - } - end end diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb index 9641bf45ee..d48598376e 100644 --- a/activemodel/lib/active_model/type/date_time.rb +++ b/activemodel/lib/active_model/type/date_time.rb @@ -39,9 +39,9 @@ module ActiveModel end def value_from_multiparameter_assignment(values_hash) - missing_parameter = (1..3).detect { |key| !values_hash.key?(key) } - if missing_parameter - raise ArgumentError, missing_parameter + missing_parameters = (1..3).select { |key| !values_hash.key?(key) } + if missing_parameters.any? + raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}" end super end diff --git a/activemodel/test/cases/type/date_time_test.rb b/activemodel/test/cases/type/date_time_test.rb index 60f62becc2..74b47d1b4d 100644 --- a/activemodel/test/cases/type/date_time_test.rb +++ b/activemodel/test/cases/type/date_time_test.rb @@ -25,6 +25,17 @@ module ActiveModel end end + def test_hash_to_time + type = Type::DateTime.new + assert_equal ::Time.utc(2018, 10, 15, 0, 0, 0), type.cast(1 => 2018, 2 => 10, 3 => 15) + end + + def test_hash_with_wrong_keys + type = Type::DateTime.new + error = assert_raises(ArgumentError) { type.cast(a: 1) } + assert_equal "Provided hash {:a=>1} doesn't contain necessary keys: [1, 2, 3]", error.message + end + private def with_timezone_config(default:) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 96fd6a62c6..97b7ad93d1 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,73 @@ +* Fix collection cache key with limit and custom select to avoid ambiguous timestamp column error. + + Fixes #33056. + + *Federico Martinez* + +* Add basic API for connection switching to support multiple databases. + + 1) Adds a `connects_to` method for models to connect to multiple databases. Example: + + ``` + class AnimalsModel < ApplicationRecord + self.abstract_class = true + + connects_to database: { writing: :animals_primary, reading: :animals_replica } + end + + class Dog < AnimalsModel + # connected to both the animals_primary db for writing and the animals_replica for reading + end + ``` + + 2) Adds a `connected_to` block method for switching connection roles or connecting to + a database that the model didn't connect to. Connecting to the database in this block is + useful when you have another defined connection, for example `slow_replica` that you don't + want to connect to by default but need in the console, or a specific code block. + + ``` + ActiveRecord::Base.connected_to(role: :reading) do + Dog.first # finds dog from replica connected to AnimalsBase + Book.first # doesn't have a reading connection, will raise an error + end + ``` + + ``` + ActiveRecord::Base.connected_to(database: :slow_replica) do + SlowReplicaModel.first # if the db config has a slow_replica configuration this will be used to do the lookup, otherwise this will throw an exception + end + ``` + + *Eileen M. Uchitelle* + +* Enum raises on invalid definition values + + When defining a Hash enum it can be easy to use [] instead of {}. This + commit checks that only valid definition values are provided, those can + be a Hash, an array of Symbols or an array of Strings. Otherwise it + raises an ArgumentError. + + Fixes #33961 + + *Alberto Almagro* + +* Reloading associations now clears the Query Cache like `Persistence#reload` does. + + ``` + class Post < ActiveRecord::Base + has_one :category + belongs_to :author + has_many :comments + end + + # Each of the following will now clear the query cache. + post.reload_category + post.reload_author + post.comments.reload + ``` + + *Christophe Maximin* + * Added `index` option for `change_table` migration helpers. With this change you can create indexes while adding new columns into the existing tables. diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 403667fb70..4c538ef2bd 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -31,9 +31,9 @@ module ActiveRecord private def exec_queries - super do |r| - @association.set_inverse_instance r - yield r if block_given? + super do |record| + @association.set_inverse_instance_from_queries(record) + yield record if block_given? end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 44596f4424..bf4942aac8 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -40,7 +40,9 @@ module ActiveRecord end # Reloads the \target and returns +self+ on success. - def reload + # The QueryCache is cleared if +force+ is true. + def reload(force = false) + klass.connection.clear_query_cache if force && klass reset reset_scope load_target @@ -79,18 +81,6 @@ module ActiveRecord target_scope.merge!(association_scope) end - # The scope for this association. - # - # Note that the association_scope is merged into the target_scope only when the - # scope method is called. This is because at that point the call may be surrounded - # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which - # actually gets built. - def association_scope - if klass - @association_scope ||= AssociationScope.scope(self) - end - end - def reset_scope @association_scope = nil end @@ -103,6 +93,13 @@ module ActiveRecord record end + def set_inverse_instance_from_queries(record) + if inverse = inverse_association_for(record) + inverse.inversed_from_queries(owner) + end + record + end + # Remove the inverse association, if possible def remove_inverse_instance(record) if inverse = inverse_association_for(record) @@ -114,6 +111,7 @@ module ActiveRecord self.target = record @inversed = !!record end + alias :inversed_from_queries :inversed_from # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. @@ -121,12 +119,6 @@ module ActiveRecord reflection.klass end - # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the - # through association's scope) - def target_scope - AssociationRelation.create(klass, self).merge!(klass.all) - end - def extensions extensions = klass.default_extensions | reflection.extensions @@ -187,6 +179,24 @@ module ActiveRecord end private + # The scope for this association. + # + # Note that the association_scope is merged into the target_scope only when the + # scope method is called. This is because at that point the call may be surrounded + # by scope.scoping { ... } or unscoped { ... } etc, which affects the scope which + # actually gets built. + def association_scope + if klass + @association_scope ||= AssociationScope.scope(self) + end + end + + # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the + # through association's scope) + def target_scope + AssociationRelation.create(klass, self).merge!(klass.all) + end + def scope_for_create scope.scope_for_create end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 9a30198b95..811fbfd927 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -1088,7 +1088,7 @@ module ActiveRecord # person.pets.reload # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload - proxy_association.reload + proxy_association.reload(true) reset_scope end @@ -1125,7 +1125,7 @@ module ActiveRecord SpawnMethods, ].flat_map { |klass| klass.public_instance_methods(false) - } - self.public_instance_methods(false) - [:select] + [:scoping] + } - self.public_instance_methods(false) - [:select] + [:scoping, :values] delegate(*delegate_methods, to: :scope) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 617956c768..f84ac65fa2 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -21,20 +21,6 @@ module ActiveRecord super end - def concat_records(records) - ensure_not_nested - - records = super(records, true) - - if owner.new_record? && records - records.flatten.each do |record| - build_through_record(record) - end - end - - records - end - def insert_record(record, validate = true, raise = false) ensure_not_nested @@ -48,6 +34,20 @@ module ActiveRecord end private + def concat_records(records) + ensure_not_nested + + records = super(records, true) + + if owner.new_record? && records + records.flatten.each do |record| + build_through_record(record) + end + end + + records + end + # The through record (built with build_record) is temporarily cached # so that it may be reused if insert_record is subsequently called. # diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index cfab16a745..8e50cce102 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -26,7 +26,7 @@ module ActiveRecord # Implements the reload reader method, e.g. foo.reload_bar for # Foo.has_one :bar def force_reload_reader - klass.uncached { reload } + reload(true) target end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 221ebea8ea..eeb88e4b7a 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -22,15 +22,6 @@ module ActiveRecord delegate :column_for_attribute, to: :class end - AttrNames = Module.new { - def self.set_name_cache(name, value) - const_name = "ATTR_#{name}" - unless const_defined? const_name - const_set const_name, -value - end - end - } - RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) class GeneratedAttributeMethods < Module #:nodoc: @@ -270,21 +261,14 @@ module ActiveRecord def respond_to?(name, include_private = false) return false unless super - case name - when :to_partial_path - name = "to_partial_path" - when :to_model - name = "to_model" - else - name = name.to_s - end - # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@attributes) && self.class.column_names.include?(name) - return has_attribute?(name) + if defined?(@attributes) + if name = self.class.symbol_column_to_string(name.to_sym) + return has_attribute?(name) + end end true diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 903fe86e04..6e1275e990 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -8,42 +8,19 @@ module ActiveRecord module ClassMethods # :nodoc: private - # We want to generate the methods via module_eval rather than - # define_method, because define_method is slower on dispatch. - # Evaluating many similar methods may use more memory as the instruction - # sequences are duplicated and cached (in MRI). define_method may - # be slower on dispatch, but if you're careful about the closure - # created, then define_method will consume much less memory. - # - # But sometimes the database might return columns with - # characters that are not allowed in normal method names (like - # 'my_column(omg)'. So to work around this we first define with - # the __temp__ identifier, and then use alias method to rename - # it to what we want. - # - # We are also defining a constant to hold the frozen string of - # the attribute name. Using a constant means that we do not have - # to allocate an object on each call to the attribute method. - # Making it frozen means that it doesn't get duped when used to - # key the @attributes in read_attribute. def define_method_attribute(name) - safe_name = name.unpack1("h*") - temp_method = "__temp__#{safe_name}" - - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{temp_method} - #{sync_with_transaction_state} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - STR - - generated_attribute_methods.module_eval do - alias_method name, temp_method - undef_method temp_method + ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( + generated_attribute_methods, name + ) do |temp_method_name, attr_name_expr| + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{temp_method_name} + #{sync_with_transaction_state} + name = #{attr_name_expr} + _read_attribute(name) { |n| missing_attribute(n, caller) } + end + RUBY end end end @@ -52,10 +29,9 @@ module ActiveRecord # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) - name = if self.class.attribute_alias?(attr_name) - self.class.attribute_alias(attr_name).to_s - else - attr_name.to_s + name = attr_name.to_s + if self.class.attribute_alias?(name) + name = self.class.attribute_alias(name) end primary_key = self.class.primary_key diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 62743bc9d8..455e67e19b 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -13,19 +13,19 @@ module ActiveRecord private def define_method_attribute=(name) - safe_name = name.unpack1("h*") - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - #{sync_with_transaction_state} - _write_attribute(name, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR + ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( + generated_attribute_methods, name, writer: true, + ) do |temp_method_name, attr_name_expr| + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{temp_method_name}(value) + name = #{attr_name_expr} + #{sync_with_transaction_state} + _write_attribute(name, value) + end + RUBY + end end end @@ -33,10 +33,9 @@ module ActiveRecord # specified +value+. Empty strings for Integer and Float columns are # turned into +nil+. def write_attribute(attr_name, value) - name = if self.class.attribute_alias?(attr_name) - self.class.attribute_alias(attr_name).to_s - else - attr_name.to_s + name = attr_name.to_s + if self.class.attribute_alias?(name) + name = self.class.attribute_alias(name) end primary_key = self.class.primary_key diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 61581b0451..4b6db8a96c 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -20,9 +20,9 @@ module ActiveRecord select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" if collection.has_limit_or_offset? - query = collection.select(column) + query = collection.select("#{column} AS collection_cache_key_timestamp") subquery_alias = "subquery_for_cache_key" - subquery_column = "#{subquery_alias}.#{timestamp_column}" + subquery_column = "#{subquery_alias}.collection_cache_key_timestamp" subquery = query.arel.as(subquery_alias) arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column) else 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 015204c056..70607fda5a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -348,8 +348,8 @@ module ActiveRecord # # create_table :taggings do |t| # t.references :tag, index: { name: 'index_taggings_on_tag_id' } - # t.references :tagger, polymorphic: true, index: true - # t.references :taggable, polymorphic: { default: 'Photo' } + # t.references :tagger, polymorphic: true + # t.references :taggable, polymorphic: { default: 'Photo' }, index: false # end def column(name, type, options = {}) name = name.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 723d8c318d..8a7020a799 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -846,17 +846,17 @@ module ActiveRecord # [<tt>:null</tt>] # Whether the column allows nulls. Defaults to true. # - # ====== Create a user_id bigint column + # ====== Create a user_id bigint column without a index # - # add_reference(:products, :user) + # add_reference(:products, :user, index: false) # # ====== Create a user_id string column # # add_reference(:products, :user, type: :string) # - # ====== Create supplier_id, supplier_type columns and appropriate index + # ====== Create supplier_id, supplier_type columns # - # add_reference(:products, :supplier, polymorphic: true, index: true) + # add_reference(:products, :supplier, polymorphic: true) # # ====== Create a supplier_id column with a unique index # @@ -884,7 +884,7 @@ module ActiveRecord # # ====== Remove the reference # - # remove_reference(:products, :user, index: true) + # remove_reference(:products, :user, index: false) # # ====== Remove polymorphic reference # @@ -892,7 +892,7 @@ module ActiveRecord # # ====== Remove the reference with a foreign key # - # remove_reference(:products, :user, index: true, foreign_key: true) + # remove_reference(:products, :user, foreign_key: true) # def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options) if foreign_key diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index fa10f18cb7..0fe868478c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -125,6 +125,8 @@ module ActiveRecord @advisory_locks_enabled = self.class.type_cast_config_to_boolean( config.fetch(:advisory_locks, true) ) + + check_version end def replica? @@ -502,6 +504,9 @@ module ActiveRecord end private + def check_version + end + def type_map @type_map ||= Type::TypeMap.new.tap do |mapping| initialize_type_map(mapping) 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 d40f38fb77..0f73641369 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -54,10 +54,6 @@ module ActiveRecord super(connection, logger, config) @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - - if version < "5.5.8" - raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8." - end end def version #:nodoc: @@ -535,6 +531,12 @@ module ActiveRecord end private + def check_version + if version < "5.5.8" + raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8." + end + end + def combine_multi_statements(total_sql) total_sql.each_with_object([]) do |sql, total_sql_chunks| previous_packet = total_sql_chunks.last @@ -622,6 +624,8 @@ module ActiveRecord # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 + ER_NO_REFERENCED_ROW = 1216 + ER_ROW_IS_REFERENCED = 1217 ER_DO_NOT_HAVE_DEFAULT = 1364 ER_ROW_IS_REFERENCED_2 = 1451 ER_NO_REFERENCED_ROW_2 = 1452 @@ -638,7 +642,7 @@ module ActiveRecord case error_number(exception) when ER_DUP_ENTRY RecordNotUnique.new(message) - when ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 + when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 InvalidForeignKey.new(message) when ER_CANNOT_ADD_FOREIGN mismatched_foreign_key(message) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index bc6eb11572..a11a786ec7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -5,7 +5,7 @@ gem "pg", ">= 0.18", "< 2.0" require "pg" # Use async_exec instead of exec_params on pg versions before 1.1 -class ::PG::Connection +class ::PG::Connection # :nodoc: unless self.public_method_defined?(:async_exec_params) remove_method :exec_params alias exec_params async_exec @@ -43,9 +43,14 @@ module ActiveRecord valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl] conn_params.slice!(*valid_conn_param_keys) - # The postgres drivers don't allow the creation of an unconnected PG::Connection object, - # so just pass a nil connection object for the time being. - ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config) + conn = PG.connect(conn_params) + ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config) + rescue ::PG::Error => error + if error.message.include?("does not exist") + raise ActiveRecord::NoDatabaseError + else + raise + end end end @@ -220,15 +225,11 @@ module ActiveRecord @local_tz = nil @max_identifier_length = nil - connect + configure_connection add_pg_encoders @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config[:statement_limit]) - if postgresql_version < 90100 - raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." - end - add_pg_decoders @type_map = Type::HashLookupTypeMap.new @@ -410,6 +411,12 @@ module ActiveRecord end private + def check_version + if postgresql_version < 90100 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." + end + end + # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" NUMERIC_VALUE_OUT_OF_RANGE = "22003" @@ -699,12 +706,6 @@ module ActiveRecord def connect @connection = PG.connect(@connection_parameters) configure_connection - rescue ::PG::Error => error - if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError - else - raise - end end # Configures the encoding, verbosity, schema search path, and time zone of the connection. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 81882f6cc1..e0355a316b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -105,11 +105,6 @@ module ActiveRecord @active = true @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - - if sqlite_version < "3.8.0" - raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." - end - configure_connection end @@ -401,6 +396,12 @@ module ActiveRecord end private + def check_version + if sqlite_version < "3.8.0" + raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." + end + end + def initialize_type_map(m = type_map) super register_class_with_limit m, %r(int)i, SQLite3Integer diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 18114f9e1c..5141271db9 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -47,6 +47,92 @@ module ActiveRecord # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. def establish_connection(config_or_env = nil) + config_hash = resolve_config_for_connection(config_or_env) + connection_handler.establish_connection(config_hash) + end + + # Connects a model to the databases specified. The +database+ keyword + # takes a hash consisting of a +role+ and a +database_key+. + # + # This will create a connection handler for switching between connections, + # look up the config hash using the +database_key+ and finally + # establishes a connection to that config. + # + # class AnimalsModel < ApplicationRecord + # self.abstract_class = true + # + # connects_to database: { writing: :primary, reading: :primary_replica } + # end + # + # Returns an array of established connections. + def connects_to(database: {}) + connections = [] + + database.each do |role, database_key| + config_hash = resolve_config_for_connection(database_key) + handler = lookup_connection_handler(role.to_sym) + + connections << handler.establish_connection(config_hash) + end + + connections + end + + # Connects to a database or role (ex writing, reading, or another + # custom role) for the duration of the block. + # + # If a role is passed, Active Record will look up the connection + # based on the requested role: + # + # ActiveRecord::Base.connected_to(role: :writing) do + # Dog.create! # creates dog using dog connection + # end + # + # ActiveRecord::Base.connected_to(role: :reading) do + # Dog.create! # throws exception because we're on a replica + # end + # + # ActiveRecord::Base.connected_to(role: :unknown_ode) do + # # raises exception due to non-existent role + # end + # + # For cases where you may want to connect to a database outside of the model, + # you can use +connected_to+ with a +database+ argument. The +database+ argument + # expects a symbol that corresponds to the database key in your config. + # + # This will connect to a new database for the queries inside the block. + # + # ActiveRecord::Base.connected_to(database: :animals_slow_replica) do + # Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+ + # end + def connected_to(database: nil, role: nil, &blk) + if database && role + raise ArgumentError, "connected_to can only accept a database or role argument, but not both arguments." + elsif database + config_hash = resolve_config_for_connection(database) + handler = lookup_connection_handler(database.to_sym) + + with_handler(database.to_sym) do + handler.establish_connection(config_hash) + return yield + end + elsif role + with_handler(role.to_sym, &blk) + else + raise ArgumentError, "must provide a `database` or a `role`." + end + end + + def lookup_connection_handler(handler_key) # :nodoc: + connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new + end + + def with_handler(handler_key, &blk) # :nodoc: + handler = lookup_connection_handler(handler_key) + swap_connection_handler(handler, &blk) + end + + def resolve_config_for_connection(config_or_env) # :nodoc: raise "Anonymous class is not allowed." unless name config_or_env ||= DEFAULT_ENV.call.to_sym @@ -57,7 +143,7 @@ module ActiveRecord config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys config_hash[:name] = pool_name - connection_handler.establish_connection(config_hash) + config_hash end # Returns the connection currently associated with the class. This can @@ -118,5 +204,14 @@ module ActiveRecord delegate :clear_active_connections!, :clear_reloadable_connections!, :clear_all_connections!, :flush_idle_connections!, to: :connection_handler + + private + + def swap_connection_handler(handler, &blk) # :nodoc: + old_handler, ActiveRecord::Base.connection_handler = ActiveRecord::Base.connection_handler, handler + yield + ensure + ActiveRecord::Base.connection_handler = old_handler + end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 392602bc0f..0718688863 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -124,6 +124,8 @@ module ActiveRecord mattr_accessor :belongs_to_required_by_default, instance_accessor: false + mattr_accessor :connection_handlers, instance_accessor: false, default: {} + class_attribute :default_connection_handler, instance_writer: false self.filter_attributes = [] @@ -137,6 +139,7 @@ module ActiveRecord end self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new + self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } end module ClassMethods @@ -341,34 +344,22 @@ module ActiveRecord # post = Post.allocate # post.init_with(coder) # post.title # => 'hello world' - def init_with(coder) + def init_with(coder, &block) coder = LegacyYamlAdapter.convert(self.class, coder) - @attributes = self.class.yaml_encoder.decode(coder) - - init_internals - - @new_record = coder["new_record"] - - self.class.define_attribute_methods - - yield self if block_given? - - _run_find_callbacks - _run_initialize_callbacks - - self + attributes = self.class.yaml_encoder.decode(coder) + init_with_attributes(attributes, coder["new_record"], &block) end ## - # Initializer used for instantiating objects that have been read from the - # database. +attributes+ should be an attributes object, and unlike the + # Initialize an empty model object from +attributes+. + # +attributes+ should be an attributes object, and unlike the # `initialize` method, no assignment calls are made per attribute. # # :nodoc: - def init_from_db(attributes) + def init_with_attributes(attributes, new_record = false) init_internals - @new_record = false + @new_record = new_record @attributes = attributes self.class.define_attribute_methods diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index fa1589511e..30cb0a27e7 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -29,7 +29,7 @@ module ActiveRecord # configs for all environments. # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults # to +nil+. - # <tt>include_replicas:</tt> Determines whether to include replicas in the + # <tt>include_replicas:</tt> Determines whether to include replicas in # the returned list. Most of the time we're only iterating over the write # connection (i.e. migrations don't need to run for the write and read connection). # Defaults to +false+. diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 23ecb24542..3d97e4e513 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -150,6 +150,7 @@ module ActiveRecord enum_prefix = definitions.delete(:_prefix) enum_suffix = definitions.delete(:_suffix) definitions.each do |name, values| + assert_valid_enum_definition_values(values) # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new name = name.to_s @@ -210,10 +211,20 @@ module ActiveRecord end end + def assert_valid_enum_definition_values(values) + unless values.is_a?(Hash) || values.all? { |v| v.is_a?(Symbol) } || values.all? { |v| v.is_a?(String) } + error_message = <<~MSG + Enum values #{values} must be either a hash, an array of symbols, or an array of strings. + MSG + raise ArgumentError, error_message + end + end + ENUM_CONFLICT_MESSAGE = \ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \ "this will generate a %{type} method \"%{method}\", which is already defined " \ "by %{source}." + private_constant :ENUM_CONFLICT_MESSAGE def detect_enum_conflict!(enum_name, method_name, klass_method = false) if klass_method && dangerous_class_method?(method_name) diff --git a/activerecord/lib/active_record/fixture_set/model_metadata.rb b/activerecord/lib/active_record/fixture_set/model_metadata.rb new file mode 100644 index 0000000000..edc03939c8 --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/model_metadata.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module ActiveRecord + class FixtureSet + class ModelMetadata # :nodoc: + def initialize(model_class, table_name) + @model_class = model_class + @table_name = table_name + end + + def primary_key_name + @primary_key_name ||= @model_class && @model_class.primary_key + end + + def primary_key_type + @primary_key_type ||= @model_class && @model_class.type_for_attribute(@model_class.primary_key).type + end + + def has_primary_key_column? + @has_primary_key_column ||= primary_key_name && + @model_class.columns.any? { |col| col.name == primary_key_name } + end + + def timestamp_column_names + @timestamp_column_names ||= + %w(created_at created_on updated_at updated_on) & column_names + end + + def inheritance_column_name + @inheritance_column_name ||= @model_class && @model_class.inheritance_column + end + + private + + def column_names + @column_names ||= @model_class.connection.columns(@table_name).collect(&:name) + end + end + end +end diff --git a/activerecord/lib/active_record/fixture_set/table_row.rb b/activerecord/lib/active_record/fixture_set/table_row.rb new file mode 100644 index 0000000000..5f72c1df38 --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/table_row.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module ActiveRecord + class FixtureSet + class TableRow # :nodoc: + def initialize(fixture, table_rows:, label:, now:) + @table_rows = table_rows + @label = label + @now = now + @row = fixture.to_hash + fill_row_model_attributes + end + + def to_hash + @row + end + + private + + def model_metadata + @table_rows.model_metadata + end + + def model_class + @table_rows.model_class + end + + def fill_row_model_attributes + return unless model_class + fill_timestamps + interpolate_label + generate_primary_key + resolve_enums + @table_rows.resolve_sti_reflections(@row) + end + + def reflection_class + @reflection_class ||= if @row.include?(model_metadata.inheritance_column_name) + @row[model_metadata.inheritance_column_name].constantize rescue model_class + else + model_class + end + end + + def fill_timestamps + # fill in timestamp columns if they aren't specified and the model is set to record_timestamps + if model_class.record_timestamps + model_metadata.timestamp_column_names.each do |c_name| + @row[c_name] = @now unless @row.key?(c_name) + end + end + end + + def interpolate_label + # interpolate the fixture label + @row.each do |key, value| + @row[key] = value.gsub("$LABEL", @label.to_s) if value.is_a?(String) + end + end + + def generate_primary_key + # generate a primary key if necessary + if model_metadata.has_primary_key_column? && !@row.include?(model_metadata.primary_key_name) + @row[model_metadata.primary_key_name] = ActiveRecord::FixtureSet.identify( + @label, model_metadata.primary_key_type + ) + end + end + + def resolve_enums + model_class.defined_enums.each do |name, values| + if @row.include?(name) + @row[name] = values.fetch(@row[name], @row[name]) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/fixture_set/table_rows.rb b/activerecord/lib/active_record/fixture_set/table_rows.rb new file mode 100644 index 0000000000..e8335a2e10 --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/table_rows.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require "active_record/fixture_set/table_row" +require "active_record/fixture_set/model_metadata" + +module ActiveRecord + class FixtureSet + class TableRows # :nodoc: + class ReflectionProxy # :nodoc: + def initialize(association) + @association = association + end + + def join_table + @association.join_table + end + + def name + @association.name + end + + def primary_key_type + @association.klass.type_for_attribute(@association.klass.primary_key).type + end + end + + class HasManyThroughProxy < ReflectionProxy # :nodoc: + def rhs_key + @association.foreign_key + end + + def lhs_key + @association.through_reflection.foreign_key + end + + def join_table + @association.through_reflection.table_name + end + end + + def initialize(table_name, model_class:, fixtures:, config:) + @table_name = table_name + @model_class = model_class + + # track any join tables we need to insert later + @tables = Hash.new { |h, table| h[table] = [] } + + build_table_rows_from(fixtures, config) + end + + attr_reader :table_name, :model_class + + def to_hash + @tables.transform_values { |rows| rows.map(&:to_hash) } + end + + def model_metadata + @model_metadata ||= ModelMetadata.new(model_class, table_name) + end + + def resolve_sti_reflections(row) + # If STI is used, find the correct subclass for association reflection + reflection_class = reflection_class_for(row) + + reflection_class._reflections.each_value do |association| + case association.macro + when :belongs_to + # Do not replace association name with association foreign key if they are named the same + fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s + + if association.name.to_s != fk_name && value = row.delete(association.name.to_s) + if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + row[association.foreign_type] = $1 + end + + fk_type = reflection_class.type_for_attribute(fk_name).type + row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) + end + when :has_many + if association.options[:through] + add_join_records(row, HasManyThroughProxy.new(association)) + end + end + end + end + + private + + def build_table_rows_from(fixtures, config) + now = config.default_timezone == :utc ? Time.now.utc : Time.now + + @tables[table_name] = fixtures.map do |label, fixture| + TableRow.new( + fixture, + table_rows: self, + label: label, + now: now, + ) + end + end + + def reflection_class_for(row) + if row.include?(model_metadata.inheritance_column_name) + row[model_metadata.inheritance_column_name].constantize rescue model_class + else + model_class + end + end + + def add_join_records(row, association) + # This is the case when the join table has no fixtures file + if (targets = row.delete(association.name.to_s)) + table_name = association.join_table + column_type = association.primary_key_type + lhs_key = association.lhs_key + rhs_key = association.rhs_key + + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) + joins = targets.map do |target| + { lhs_key => row[model_metadata.primary_key_name], + rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } + end + @tables[table_name].concat(joins) + end + end + end + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index e697c30bb6..b090c76a38 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -8,6 +8,7 @@ require "active_support/dependencies" require "active_support/core_ext/digest/uuid" require "active_record/fixture_set/file" require "active_record/fixture_set/render_context" +require "active_record/fixture_set/table_rows" require "active_record/test_fixtures" require "active_record/errors" @@ -442,60 +443,6 @@ module ActiveRecord @@all_cached_fixtures = Hash.new { |h, k| h[k] = {} } - def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: - config.pluralize_table_names ? - fixture_set_name.singularize.camelize : - fixture_set_name.camelize - end - - def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: - "#{ config.table_name_prefix }"\ - "#{ fixture_set_name.tr('/', '_') }"\ - "#{ config.table_name_suffix }".to_sym - end - - def self.reset_cache - @@all_cached_fixtures.clear - end - - def self.cache_for_connection(connection) - @@all_cached_fixtures[connection] - end - - def self.fixture_is_cached?(connection, table_name) - cache_for_connection(connection)[table_name] - end - - def self.cached_fixtures(connection, keys_to_fetch = nil) - if keys_to_fetch - cache_for_connection(connection).values_at(*keys_to_fetch) - else - cache_for_connection(connection).values - end - end - - def self.cache_fixtures(connection, fixtures_map) - cache_for_connection(connection).update(fixtures_map) - end - - def self.instantiate_fixtures(object, fixture_set, load_instances = true) - if load_instances - fixture_set.each do |fixture_name, fixture| - begin - object.instance_variable_set "@#{fixture_name}", fixture.find - rescue FixtureClassNotFound - nil - end - end - end - end - - def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each_value do |fixture_set| - instantiate_fixtures(object, fixture_set, load_instances) - end - end - cattr_accessor :all_loaded_fixtures, default: {} class ClassCache @@ -504,14 +451,16 @@ module ActiveRecord @config = config # Remove string values that aren't constants or subclasses of AR - @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) } + @class_names.delete_if do |klass_name, klass| + !insert_class(@class_names, klass_name, klass) + end end def [](fs_name) - @class_names.fetch(fs_name) { + @class_names.fetch(fs_name) do klass = default_fixture_model(fs_name, @config).safe_constantize insert_class(@class_names, fs_name, klass) - } + end end private @@ -530,38 +479,129 @@ module ActiveRecord end end - def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) - fixture_set_names = Array(fixture_set_names).map(&:to_s) - class_names = ClassCache.new class_names, config + class << self + def default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + config.pluralize_table_names ? + fixture_set_name.singularize.camelize : + fixture_set_name.camelize + end + + def default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + "#{ config.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ config.table_name_suffix }".to_sym + end + + def reset_cache + @@all_cached_fixtures.clear + end - # FIXME: Apparently JK uses this. - connection = block_given? ? yield : ActiveRecord::Base.connection + def cache_for_connection(connection) + @@all_cached_fixtures[connection] + end - files_to_read = fixture_set_names.reject { |fs_name| - fixture_is_cached?(connection, fs_name) - } + def fixture_is_cached?(connection, table_name) + cache_for_connection(connection)[table_name] + end - unless files_to_read.empty? - fixtures_map = {} + def cached_fixtures(connection, keys_to_fetch = nil) + if keys_to_fetch + cache_for_connection(connection).values_at(*keys_to_fetch) + else + cache_for_connection(connection).values + end + end + + def cache_fixtures(connection, fixtures_map) + cache_for_connection(connection).update(fixtures_map) + end + + def instantiate_fixtures(object, fixture_set, load_instances = true) + return unless load_instances + fixture_set.each do |fixture_name, fixture| + begin + object.instance_variable_set "@#{fixture_name}", fixture.find + rescue FixtureClassNotFound + nil + end + end + end + + def instantiate_all_loaded_fixtures(object, load_instances = true) + all_loaded_fixtures.each_value do |fixture_set| + instantiate_fixtures(object, fixture_set, load_instances) + end + end + + def create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) + fixture_set_names = Array(fixture_set_names).map(&:to_s) + class_names = ClassCache.new class_names, config + + # FIXME: Apparently JK uses this. + connection = block_given? ? yield : ActiveRecord::Base.connection + + fixture_files_to_read = fixture_set_names.reject do |fs_name| + fixture_is_cached?(connection, fs_name) + end + + if fixture_files_to_read.any? + fixtures_map = read_and_insert( + fixtures_directory, + fixture_files_to_read, + class_names, + connection, + ) + cache_fixtures(connection, fixtures_map) + end + cached_fixtures(connection, fixture_set_names) + end + + # Returns a consistent, platform-independent identifier for +label+. + # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. + def identify(label, column_type = :integer) + if column_type == :uuid + Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) + else + Zlib.crc32(label.to_s) % MAX_ID + end + end - fixture_sets = files_to_read.map do |fs_name| - klass = class_names[fs_name] - conn = klass ? klass.connection : connection - fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new + # Superclass for the evaluation contexts used by ERB fixtures. + def context_class + @context_class ||= Class.new + end + + private + + def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc: + fixtures_map = {} + fixture_sets = fixture_files.map do |fixture_set_name| + klass = class_names[fixture_set_name] + conn = klass&.connection || connection + fixtures_map[fixture_set_name] = new( # ActiveRecord::FixtureSet.new conn, - fs_name, + fixture_set_name, klass, - ::File.join(fixtures_directory, fs_name)) + ::File.join(fixtures_directory, fixture_set_name) + ) end + update_all_loaded_fixtures(fixtures_map) - update_all_loaded_fixtures fixtures_map - fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection } + insert(fixture_sets, connection) + + fixtures_map + end + + def insert(fixture_sets, connection) # :nodoc: + fixture_sets_by_connection = fixture_sets.group_by do |fixture_set| + fixture_set.model_class&.connection || connection + end fixture_sets_by_connection.each do |conn, set| table_rows_for_connection = Hash.new { |h, k| h[k] = [] } - set.each do |fs| - fs.table_rows.each do |table, rows| + set.each do |fixture_set| + fixture_set.table_rows.each do |table, rows| table_rows_for_connection[table].unshift(*rows) end end @@ -572,31 +612,13 @@ module ActiveRecord set.each { |fs| conn.reset_pk_sequence!(fs.table_name) } end end - - cache_fixtures(connection, fixtures_map) end - cached_fixtures(connection, fixture_set_names) - end - # Returns a consistent, platform-independent identifier for +label+. - # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. - def self.identify(label, column_type = :integer) - if column_type == :uuid - Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) - else - Zlib.crc32(label.to_s) % MAX_ID + def update_all_loaded_fixtures(fixtures_map) # :nodoc: + all_loaded_fixtures.update(fixtures_map) end end - # Superclass for the evaluation contexts used by ERB fixtures. - def self.context_class - @context_class ||= Class.new - end - - def self.update_all_loaded_fixtures(fixtures_map) # :nodoc: - all_loaded_fixtures.update(fixtures_map) - end - attr_reader :table_name, :name, :fixtures, :model_class, :config def initialize(connection, name, class_name, path, config = ActiveRecord::Base) @@ -634,152 +656,18 @@ module ActiveRecord # Returns a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows - now = config.default_timezone == :utc ? Time.now.utc : Time.now - # allow a standard key to be used for doing defaults in YAML fixtures.delete("DEFAULTS") - # track any join tables we need to insert later - rows = Hash.new { |h, table| h[table] = [] } - - rows[table_name] = fixtures.map do |label, fixture| - row = fixture.to_hash - - if model_class - # fill in timestamp columns if they aren't specified and the model is set to record_timestamps - if model_class.record_timestamps - timestamp_column_names.each do |c_name| - row[c_name] = now unless row.key?(c_name) - end - end - - # interpolate the fixture label - row.each do |key, value| - row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String) - end - - # generate a primary key if necessary - if has_primary_key_column? && !row.include?(primary_key_name) - row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type) - end - - # Resolve enums - model_class.defined_enums.each do |name, values| - if row.include?(name) - row[name] = values.fetch(row[name], row[name]) - end - end - - # If STI is used, find the correct subclass for association reflection - reflection_class = - if row.include?(inheritance_column_name) - row[inheritance_column_name].constantize rescue model_class - else - model_class - end - - reflection_class._reflections.each_value do |association| - case association.macro - when :belongs_to - # Do not replace association name with association foreign key if they are named the same - fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s - - if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - # support polymorphic belongs_to as "label (Type)" - row[association.foreign_type] = $1 - end - - fk_type = reflection_class.type_for_attribute(fk_name).type - row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) - end - when :has_many - if association.options[:through] - add_join_records(rows, row, HasManyThroughProxy.new(association)) - end - end - end - end - - row - end - rows - end - - class ReflectionProxy # :nodoc: - def initialize(association) - @association = association - end - - def join_table - @association.join_table - end - - def name - @association.name - end - - def primary_key_type - @association.klass.type_for_attribute(@association.klass.primary_key).type - end - end - - class HasManyThroughProxy < ReflectionProxy # :nodoc: - def rhs_key - @association.foreign_key - end - - def lhs_key - @association.through_reflection.foreign_key - end - - def join_table - @association.through_reflection.table_name - end + TableRows.new( + table_name, + model_class: model_class, + fixtures: fixtures, + config: config, + ).to_hash end private - def primary_key_name - @primary_key_name ||= model_class && model_class.primary_key - end - - def primary_key_type - @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type - end - - def add_join_records(rows, row, association) - # This is the case when the join table has no fixtures file - if (targets = row.delete(association.name.to_s)) - table_name = association.join_table - column_type = association.primary_key_type - lhs_key = association.lhs_key - rhs_key = association.rhs_key - - targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - rows[table_name].concat targets.map { |target| - { lhs_key => row[primary_key_name], - rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } - } - end - end - - def has_primary_key_column? - @has_primary_key_column ||= primary_key_name && - model_class.columns.any? { |c| c.name == primary_key_name } - end - - def timestamp_column_names - @timestamp_column_names ||= - %w(created_at created_on updated_at updated_on) & column_names - end - - def inheritance_column_name - @inheritance_column_name ||= model_class && model_class.inheritance_column - end - - def column_names - @column_names ||= @connection.columns(@table_name).collect(&:name) - end def model_class=(class_name) if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @@ -843,12 +731,9 @@ module ActiveRecord alias :to_hash :fixture def find - if model_class - model_class.unscoped do - model_class.find(fixture[model_class.primary_key]) - end - else - raise FixtureClassNotFound, "No class attached to find." + raise FixtureClassNotFound, "No class attached to find." unless model_class + model_class.unscoped do + model_class.find(fixture[model_class.primary_key]) end end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 6cf26a9792..456689ec6d 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -60,7 +60,7 @@ module ActiveRecord # the cache key will also include a version. # # Product.cache_versioning = false - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + # Product.find(5).cache_key # => "products/5-20071224150000" (updated_at available) def cache_key(*timestamp_names) if new_record? "#{model_name.cache_key}/new" diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 5a4a1fb969..c50a420432 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -388,6 +388,11 @@ module ActiveRecord @column_names ||= columns.map(&:name) end + def symbol_column_to_string(name_symbol) # :nodoc: + @symbol_column_to_string_name_hash ||= column_names.index_by(&:to_sym) + @symbol_column_to_string_name_hash[name_symbol] + end + # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", # and columns used for single table inheritance have been removed. def content_columns @@ -477,6 +482,7 @@ module ActiveRecord def reload_schema_from_cache @arel_table = nil @column_names = nil + @symbol_column_to_string_name_hash = nil @attribute_types = nil @content_columns = nil @default_attributes = nil diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 50767ee93f..8b9098df6c 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -426,7 +426,7 @@ module ActiveRecord existing_record.assign_attributes(assignable_attributes) association(association_name).initialize_attributes(existing_record) else - method = "build_#{association_name}" + method = :"build_#{association_name}" if respond_to?(method) send(method, assignable_attributes) else diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 6eb2bfb452..45ceb1e3ad 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -209,7 +209,7 @@ module ActiveRecord # new instance of the class. Accepts only keys as strings. def instantiate_instance_of(klass, attributes, column_types = {}, &block) attributes = klass.attributes_builder.build_from_database(attributes, column_types) - klass.allocate.init_from_db(attributes, &block) + klass.allocate.init_with_attributes(attributes, &block) end # Called by +instantiate+ to decide which class to use for a new @@ -758,6 +758,8 @@ module ActiveRecord @_association_destroy_exception = nil end + # The name of the method used to touch a +belongs_to+ association when the + # +:touch+ option is used. def belongs_to_touch_method :touch end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index be5ef350a9..1c7ceb4981 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -149,18 +149,21 @@ db_namespace = namespace :db do desc "Display status of migrations" task status: :load_config do - unless ActiveRecord::SchemaMigration.table_exists? - abort "Schema migrations table does not exist yet." + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate_status end + end - # output - puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" - puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" - puts "-" * 50 - ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| - puts "#{status.center(8)} #{version.ljust(14)} #{name}" + namespace :status do + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Display status of migrations for #{spec_name} database" + task spec_name => :load_config do + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate_status + end end - puts end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 8f657840f5..383dc1bf4b 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -17,6 +17,7 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } + include_relation_methods(delegate) mangled_name = klass.name.gsub("::", "_") const_set mangled_name, delegate private_constant mangled_name @@ -29,6 +30,35 @@ module ActiveRecord child_class.initialize_relation_delegate_cache super end + + protected + def include_relation_methods(delegate) + superclass.include_relation_methods(delegate) unless base_class? + delegate.include generated_relation_methods + end + + private + def generated_relation_methods + @generated_relation_methods ||= Module.new.tap do |mod| + mod_name = "GeneratedRelationMethods" + const_set mod_name, mod + private_constant mod_name + end + end + + def generate_relation_method(method) + if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) + generated_relation_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + scoping { klass.#{method}(*args, &block) } + end + RUBY + else + generated_relation_methods.send(:define_method, method) do |*args, &block| + scoping { klass.public_send(method, *args, &block) } + end + end + end end extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 5170d38fa8..eb80aab701 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -909,21 +909,18 @@ module ActiveRecord @arel ||= build_arel(aliases) end - # Returns a relation value with a given name - def get_value(name) # :nodoc: - @values.fetch(name, DEFAULT_VALUES[name]) - end - - protected + private + # Returns a relation value with a given name + def get_value(name) + @values.fetch(name, DEFAULT_VALUES[name]) + end # Sets the relation value with the given name - def set_value(name, value) # :nodoc: + def set_value(name, value) assert_mutability! @values[name] = value end - private - def assert_mutability! raise ImmutableRelation if @loaded raise ImmutableRelation if defined?(@arel) && @arel @@ -1188,8 +1185,9 @@ module ActiveRecord STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references] def structurally_incompatible_values_for_or(other) + values = other.values STRUCTURAL_OR_METHODS.reject do |method| - get_value(method) == other.get_value(method) + get_value(method) == values.fetch(method, DEFAULT_VALUES[method]) end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 573d97b819..d5cc5db97e 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -191,6 +191,8 @@ module ActiveRecord scope end end + + generate_relation_method(name) end private diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 5e29085aff..974d7a1c0a 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -197,6 +197,21 @@ module ActiveRecord Migration.verbose = verbose_was end + def migrate_status + unless ActiveRecord::SchemaMigration.table_exists? + Kernel.abort "Schema migrations table does not exist yet." + end + + # output + puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" + puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" + puts "-" * 50 + ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| + puts "#{status.center(8)} #{version.ljust(14)} #{name}" + end + puts + end + def check_target_version if target_version && !(Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"])) raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`" diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb index 9d9294fecc..32f6705d04 100644 --- a/activerecord/lib/arel/visitors/mysql.rb +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -9,20 +9,20 @@ module Arel # :nodoc: all collector << "( " end - collector = case o.left - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.left, collector, true - else - visit o.left, collector + case o.left + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.left, collector, true + else + visit o.left, collector end collector << " UNION " - collector = case o.right - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.right, collector, true - else - visit o.right, collector + case o.right + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.right, collector, true + else + visit o.right, collector end if suppress_parens @@ -75,14 +75,7 @@ module Arel # :nodoc: all o end end - - def prepare_delete_statement(o) - if o.offset || has_join_sources?(o) - super - else - o - end - end + alias :prepare_delete_statement :prepare_update_statement # MySQL is too stupid to create a temporary table for use subquery, so we have # to give it some prompting in the form of a subsubquery. diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 7c0f6c2e97..7ce26884a5 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -76,10 +76,18 @@ module Arel # :nodoc: all def visit_Arel_Nodes_DeleteStatement(o, collector) o = prepare_delete_statement(o) - collector << "DELETE FROM " + if has_join_sources?(o) + collector << "DELETE " + visit o.relation.left, collector + collector << " FROM " + else + collector << "DELETE FROM " + end collector = visit o.relation, collector - collect_where_for(o, collector) + collect_nodes_for o.wheres, collector, " WHERE ", " AND " + collect_nodes_for o.orders, collector, " ORDER BY " + maybe_visit o.limit, collector end def visit_Arel_Nodes_UpdateStatement(o, collector) @@ -87,12 +95,11 @@ module Arel # :nodoc: all collector << "UPDATE " collector = visit o.relation, collector - unless o.values.empty? - collector << " SET " - collector = inject_join o.values, collector, ", " - end + collect_nodes_for o.values, collector, " SET " - collect_where_for(o, collector) + collect_nodes_for o.wheres, collector, " WHERE ", " AND " + collect_nodes_for o.orders, collector, " ORDER BY " + maybe_visit o.limit, collector end def visit_Arel_Nodes_InsertStatement(o, collector) @@ -225,10 +232,7 @@ module Arel # :nodoc: all collect_nodes_for o.wheres, collector, WHERE, AND collect_nodes_for o.groups, collector, GROUP_BY - unless o.havings.empty? - collector << " HAVING " - inject_join o.havings, collector, AND - end + collect_nodes_for o.havings, collector, " HAVING ", AND collect_nodes_for o.windows, collector, WINDOW collector @@ -237,11 +241,7 @@ module Arel # :nodoc: all def collect_nodes_for(nodes, collector, spacer, connector = COMMA) unless nodes.empty? collector << spacer - len = nodes.length - 1 - nodes.each_with_index do |x, i| - collector = visit(x, collector) - collector << connector unless len == i - end + inject_join nodes, collector, connector end end @@ -296,10 +296,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_Window(o, collector) collector << "(" - if o.partitions.any? - collector << "PARTITION BY " - collector = inject_join o.partitions, collector, ", " - end + collect_nodes_for o.partitions, collector, "PARTITION BY " if o.orders.any? collector << SPACE if o.partitions.any? @@ -830,20 +827,6 @@ module Arel # :nodoc: all stmt end - def collect_where_for(o, collector) - unless o.wheres.empty? - collector << " WHERE " - collector = inject_join o.wheres, collector, " AND " - end - - unless o.orders.empty? - collector << " ORDER BY " - collector = inject_join o.orders, collector, ", " - end - - maybe_visit o.limit, collector - end - def infix_value(o, collector, value) collector = visit o.left, collector collector << value diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 1fca1be181..93dd427951 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -367,6 +367,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal "ODEGY", odegy_account.reload_firm.name end + def test_reload_the_belonging_object_with_query_cache + odegy_account_id = accounts(:odegy_account).id + + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + odegy_account = Account.find(odegy_account_id) + + # Populate the cache with a second query + odegy_account.firm + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the firm again, populating the cache with a query + assert_queries(1) { odegy_account.reload_firm } + + # This query is not cached anymore, so it should make a real SQL query + assert_queries(1) { Account.find(odegy_account_id) } + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + def test_natural_assignment_to_nil client = Client.find(3) client.firm = nil @@ -694,7 +718,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase line_item = LineItem.create! Invoice.create!(line_items: [line_item]) - assert_queries(0) { line_item.save } + assert_no_queries { line_item.save } end def test_belongs_to_with_touch_option_on_destroy @@ -789,7 +813,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_dont_find_target_when_foreign_key_is_null tagging = taggings(:thinking_general) - assert_queries(0) { tagging.super_tag } + assert_no_queries { tagging.super_tag } end def test_dont_find_target_when_saving_foreign_key_after_stale_association_loaded diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index c5b2b77bd4..525ad3197a 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -92,7 +92,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase def test_include_query res = ShapeExpression.all.merge!(includes: [ :shape, { paint: :non_poly } ]).to_a assert_equal NUM_SHAPE_EXPRESSIONS, res.size - assert_queries(0) do + assert_no_queries do res.each do |se| assert_not_nil se.paint.non_poly, "this is the association that was loading incorrectly before the change" assert_not_nil se.shape, "just making sure other associations still work" diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 79b3b4a6ad..39034746c9 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1346,7 +1346,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_joins_with_includes_should_preload_via_joins post = assert_queries(1) { Post.includes(:comments).joins(:comments).order("posts.id desc").to_a.first } - assert_queries(0) do + assert_no_queries do assert_not_equal 0, post.comments.to_a.count end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 482302055d..515eb65d37 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -25,6 +25,8 @@ require "models/user" require "models/member" require "models/membership" require "models/sponsor" +require "models/lesson" +require "models/student" require "models/country" require "models/treaty" require "models/vertex" @@ -275,7 +277,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_habtm_saving_multiple_relationships new_project = Project.new("name" => "Grimetime") amount_of_developers = 4 - developers = (0...amount_of_developers).collect { |i| Developer.create(name: "JME #{i}") }.reverse + developers = (0...amount_of_developers).reverse_each.map { |i| Developer.create(name: "JME #{i}") } new_project.developer_ids = [developers[0].id, developers[1].id] new_project.developers_with_callback_ids = [developers[2].id, developers[3].id] @@ -310,7 +312,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_build devel = Developer.find(1) - proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") } + + # Load schema information so we don't query below if running just this test. + Project.define_attribute_methods + + proj = assert_no_queries { devel.projects.build("name" => "Projekt") } assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj @@ -325,7 +331,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build devel = Developer.find(1) - proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") } + + # Load schema information so we don't query below if running just this test. + Project.define_attribute_methods + + proj = assert_no_queries { devel.projects.new("name" => "Projekt") } assert_not_predicate devel.projects, :loaded? assert_equal devel.projects.last, proj @@ -546,7 +556,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_predicate project.developers, :loaded? assert_includes project.developers, developer end @@ -741,7 +751,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations developer = developers(:david) developer.projects.reload - assert_queries(0) do + assert_no_queries do developer.project_ids developer.project_ids end @@ -772,6 +782,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort end + def test_singular_ids_are_reloaded_after_collection_concat + student = Student.create(name: "Alberto Almagro") + student.lesson_ids + + lesson = Lesson.create(name: "DSI") + student.lessons << lesson + + assert_includes student.lesson_ids, lesson.id + end + def test_scoped_find_on_through_association_doesnt_return_read_only_records tag = Post.find(1).tags.find_by_name("General") @@ -859,7 +879,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], projects assert_equal [], projects.where(title: "omg") assert_equal [], projects.pluck(:title) diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 0b44515e00..d13e1a86e9 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -458,7 +458,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_finder_method_with_dirty_target company = companies(:first_firm) new_clients = [] - assert_no_queries(ignore_none: false) do + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + assert_no_queries do new_clients << company.clients_of_firm.build(name: "Another Client") new_clients << company.clients_of_firm.build(name: "Another Client II") new_clients << company.clients_of_firm.build(name: "Another Client III") @@ -478,7 +482,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_finder_bang_method_with_dirty_target company = companies(:first_firm) new_clients = [] - assert_no_queries(ignore_none: false) do + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + assert_no_queries do new_clients << company.clients_of_firm.build(name: "Another Client") new_clients << company.clients_of_firm.build(name: "Another Client II") new_clients << company.clients_of_firm.build(name: "Another Client III") @@ -821,6 +829,48 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_not_same original_object, collection.first, "Expected #first after #reload to return a new object" end + def test_reload_with_query_cache + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + firm = Firm.first + # Populate the cache with a second query + firm.clients.load + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the clients again, populating the cache with a query + assert_queries(1) { firm.clients.reload } + # This query is cached, so it shouldn't make a real SQL query + assert_queries(0) { firm.clients.load } + + assert_equal 1, connection.query_cache.size + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + + def test_reloading_unloaded_associations_with_query_cache + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + firm = Firm.create!(name: "firm name") + client = firm.clients.create!(name: "client name") + firm.clients.to_a # add request to cache + + connection.uncached do + client.update!(name: "new client name") + end + + firm = Firm.find(firm.id) + + assert_equal [client.name], firm.clients.reload.map(&:name) + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + def test_find_all_with_include_and_conditions assert_nothing_raised do Developer.all.merge!(joins: :audit_logs, where: { "audit_logs.message" => nil, :name => "Smith" }).to_a @@ -955,8 +1005,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_adding_to_new_record - assert_no_queries(ignore_none: false) do - firm = Firm.new + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + firm = Firm.new + assert_no_queries do firm.clients_of_firm.concat(Client.new("name" => "Natural Company")) end end @@ -970,7 +1023,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name @@ -980,7 +1037,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name @@ -1037,7 +1098,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many company = companies(:first_firm) - new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_clients = assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } assert_equal 2, new_clients.size end @@ -1049,10 +1114,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_without_loading_association first_topic = topics(:first) - Reply.column_names assert_equal 1, first_topic.replies.length + # Load schema information so we don't query below if running just this test. + Reply.define_attribute_methods + assert_no_queries do first_topic.replies.build(title: "Not saved", content: "Superstars") assert_equal 2, first_topic.replies.size @@ -1063,7 +1130,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_via_block company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert_not_predicate company.clients_of_firm, :loaded? assert_equal "Another Client", new_client.name @@ -1073,7 +1144,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many_via_block company = companies(:first_firm) - new_clients = assert_no_queries(ignore_none: false) do + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_clients = assert_no_queries do company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" end @@ -1086,8 +1161,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_without_loading_association first_firm = companies(:first_firm) - Firm.column_names - Client.column_names assert_equal 2, first_firm.clients_of_firm.size first_firm.clients_of_firm.reset @@ -1266,7 +1339,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_empty_with_counter_cache post = posts(:welcome) - assert_queries(0) do + assert_no_queries do assert_not_empty post.comments end end @@ -1364,8 +1437,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transaction_when_deleting_new_record - assert_no_queries(ignore_none: false) do - firm = Firm.new + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + firm = Firm.new + assert_no_queries do client = Client.new("name" => "New Client") firm.clients_of_firm << client firm.clients_of_firm.destroy(client) @@ -1800,7 +1876,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients = [] firm.save - assert_queries(0, ignore_none: true) do + assert_no_queries do firm.clients = [] end @@ -1822,8 +1898,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_replacing_on_new_record - assert_no_queries(ignore_none: false) do - firm = Firm.new + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + firm = Firm.new + assert_no_queries do firm.clients_of_firm = [Client.new("name" => "New Client")] end end @@ -1835,7 +1914,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations company = companies(:first_firm) company.clients.reload - assert_queries(0) do + assert_no_queries do company.client_ids company.client_ids end @@ -1862,11 +1941,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_get_ids_for_association_on_new_record_does_not_try_to_find_records - Company.columns # Load schema information so we don't query below - Contract.columns # if running just this test. + # Load schema information so we don't query below if running just this test. + companies(:first_client).contract_ids company = Company.new - assert_queries(0) do + assert_no_queries do company.contract_ids end @@ -1972,7 +2051,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.load_target assert_predicate firm.clients, :loaded? - assert_no_queries(ignore_none: false) do + assert_no_queries do firm.clients.first assert_equal 2, firm.clients.first(2).size firm.clients.last @@ -2354,6 +2433,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_collection_association_with_private_kernel_method firm = companies(:first_firm) assert_equal [accounts(:signals37)], firm.accounts.open + assert_equal [accounts(:signals37)], firm.accounts.available + end + + def test_association_with_or_doesnt_set_inverse_instance_key + firm = companies(:first_firm) + accounts = firm.accounts.or(Account.where(firm_id: nil)).order(:id) + assert_equal [firm.id, nil], accounts.map(&:firm_id) + end + + def test_association_with_rewhere_doesnt_set_inverse_instance_key + firm = companies(:first_firm) + accounts = firm.accounts.rewhere(firm_id: [firm.id, nil]).order(:id) + assert_equal [firm.id, nil], accounts.map(&:firm_id) end test "first_or_initialize adds the record to the association" do @@ -2385,7 +2477,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "has many associations on new records use null relations" do post = Post.new - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], post.comments assert_equal [], post.comments.where(body: "omg") assert_equal [], post.comments.pluck(:body) diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 442f4a93d4..7b405c74c4 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -274,7 +274,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_queries(1) { posts(:thinking) } new_person = nil # so block binding catches it - assert_queries(0) do + # Load schema information so we don't query below if running just this test. + Person.define_attribute_methods + + assert_no_queries do new_person = Person.new first_name: "bob" end @@ -294,7 +297,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_associate_new_by_building assert_queries(1) { posts(:thinking) } - assert_queries(0) do + # Load schema information so we don't query below if running just this test. + Person.define_attribute_methods + + assert_no_queries do posts(:thinking).people.build(first_name: "Bob") posts(:thinking).people.new(first_name: "Ted") end @@ -571,10 +577,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts(:welcome).people = [people(:david)] end - assert_queries(0) { + assert_no_queries do assert_includes posts(:welcome).people, people(:david) assert_not_includes posts(:welcome).people, people(:michael) - } + end assert_includes posts(:welcome).reload.people.reload, people(:david) assert_not_includes posts(:welcome).reload.people.reload, people(:michael) @@ -698,7 +704,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts(:welcome).people.clear end - assert_queries(0) do + assert_no_queries do assert_empty posts(:welcome).people end @@ -788,7 +794,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations person = people(:michael) person.posts.reload - assert_queries(0) do + assert_no_queries do person.post_ids person.post_ids end @@ -1198,7 +1204,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_associations_on_new_records_use_null_relations person = Person.new - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], person.posts assert_equal [], person.posts.where(body: "omg") assert_equal [], person.posts.pluck(:body) diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 9eea34d2b9..adfb3ce072 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -37,10 +37,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_has_one_cache_nils firm = companies(:another_firm) assert_queries(1) { assert_nil firm.account } - assert_queries(0) { assert_nil firm.account } + assert_no_queries { assert_nil firm.account } - firms = Firm.all.merge!(includes: :account).to_a - assert_queries(0) { firms.each(&:account) } + firms = Firm.includes(:account).to_a + assert_no_queries { firms.each(&:account) } end def test_with_select @@ -231,9 +231,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end def test_build_association_dont_create_transaction - assert_no_queries(ignore_none: false) { - Firm.new.build_account - } + # Load schema information so we don't query below if running just this test. + Account.define_attribute_methods + + firm = Firm.new + assert_no_queries do + firm.build_account + end end def test_building_the_associated_object_with_implicit_sti_base_class @@ -329,6 +333,29 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal 80, odegy.reload_account.credit_limit end + def test_reload_association_with_query_cache + odegy_id = companies(:odegy).id + + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + odegy = Company.find(odegy_id) + # Populate the cache with a second query + odegy.account + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the account again, populating the cache with a query + assert_queries(1) { odegy.reload_account } + + # This query is not cached anymore, so it should make a real SQL query + assert_queries(1) { Company.find(odegy_id) } + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 03ed1c1d47..5821744530 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -137,7 +137,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_has_one_through_with_has_one_source_reflection_preload members = assert_queries(4) { Member.includes(:nested_sponsors).to_a } mustache = sponsors(:moustache_club_sponsor_for_groucho) - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [mustache], members.first.nested_sponsors end end @@ -196,7 +196,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase # postgresql test if randomly executed then executes "SHOW max_identifier_length". Hence # the need to ignore certain predefined sqls that deal with system calls. - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id) end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index db3a58eba9..88df0eed55 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -642,7 +642,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_before_save company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" @@ -653,7 +657,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_before_save company = companies(:first_firm) - assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } company.name += "-changed" assert_queries(3) { assert company.save } @@ -662,7 +670,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_via_block_before_save company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } } + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert_not_predicate company.clients_of_firm, :loaded? company.name += "-changed" @@ -673,7 +685,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_via_block_before_save company = companies(:first_firm) - assert_no_queries(ignore_none: false) do + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + assert_no_queries do company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" end @@ -1100,7 +1116,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase assert @pirate.save Pirate.transaction do - assert_queries(0) do + assert_no_queries do assert @pirate.save end end @@ -1181,12 +1197,12 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_changed_for_autosave_should_handle_cycles @ship.pirate = @pirate - assert_queries(0) { @ship.save! } + assert_no_queries { @ship.save! } @parrot = @pirate.parrots.create(name: "some_name") @parrot.name = "changed_name" assert_queries(1) { @ship.save! } - assert_queries(0) { @ship.save! } + assert_no_queries { @ship.save! } end def test_should_automatically_save_bang_the_associated_model diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index a5d908344a..483383257b 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -42,6 +42,20 @@ module ActiveRecord assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end + test "cache_key for relation with custom select and limit" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5) + developers_with_select = developers.select("developers.*") + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers_with_select.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers_with_select.cache_key + + assert_equal ActiveSupport::Digest.hexdigest(developers_with_select.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + test "cache_key for loaded relation" do developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load last_developer_timestamp = developers.first.updated_at @@ -91,12 +105,12 @@ module ActiveRecord developers = Developer.where(name: "David") assert_queries(1) { developers.cache_key } - assert_queries(0) { developers.cache_key } + assert_no_queries { developers.cache_key } end test "it doesn't trigger any query if the relation is already loaded" do developers = Developer.where(name: "David").load - assert_queries(0) { developers.cache_key } + assert_no_queries { developers.cache_key } end test "relation cache_key changes when the sql query changes" do diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 5e3447efde..51d0cc3d12 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -170,6 +170,11 @@ module ActiveRecord ActiveRecord::Base.configurations = config ActiveRecord::Base.configurations.configs_for.each do |db_config| assert_instance_of ActiveRecord::DatabaseConfigurations::HashConfig, db_config + assert_instance_of String, db_config.env_name + assert_instance_of String, db_config.spec_name + db_config.config.keys.each do |key| + assert_instance_of String, key + end end ensure ActiveRecord::Base.configurations = @prev_configs diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb new file mode 100644 index 0000000000..d4e8cbee81 --- /dev/null +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/person" + +module ActiveRecord + module ConnectionAdapters + class ConnectionHandlersMultiDbTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + fixtures :people + + def setup + @handlers = { writing: ConnectionHandler.new, reading: ConnectionHandler.new } + @rw_handler = @handlers[:writing] + @ro_handler = @handlers[:reading] + @spec_name = "primary" + @rw_pool = @handlers[:writing].establish_connection(ActiveRecord::Base.configurations["arunit"]) + @ro_pool = @handlers[:reading].establish_connection(ActiveRecord::Base.configurations["arunit"]) + end + + def teardown + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + + class MultiConnectionTestModel < ActiveRecord::Base + end + + def test_multiple_connection_handlers_works_in_a_threaded_environment + tf_writing = Tempfile.open "test_writing" + tf_reading = Tempfile.open "test_reading" + + MultiConnectionTestModel.connects_to database: { writing: { database: tf_writing.path, adapter: "sqlite3" }, reading: { database: tf_reading.path, adapter: "sqlite3" } } + + MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))") + MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('writing')") + + ActiveRecord::Base.connected_to(role: :reading) do + MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))") + MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('reading')") + end + + read_latch = Concurrent::CountDownLatch.new + write_latch = Concurrent::CountDownLatch.new + + MultiConnectionTestModel.connection + + thread = Thread.new do + MultiConnectionTestModel.connection + + write_latch.wait + assert_equal "writing", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1") + read_latch.count_down + end + + ActiveRecord::Base.connected_to(role: :reading) do + write_latch.count_down + assert_equal "reading", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1") + read_latch.wait + end + + thread.join + ensure + tf_reading.close + tf_reading.unlink + tf_writing.close + tf_writing.unlink + end + + unless in_memory_db? + def test_establish_connection_using_3_levels_config + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to(database: { writing: :primary, reading: :readonly }) + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_switching_connections_via_handler + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to(database: { writing: :primary, reading: :readonly }) + + ActiveRecord::Base.connected_to(role: :reading) do + @ro_handler = ActiveRecord::Base.connection_handler + assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:reading] + end + + ActiveRecord::Base.connected_to(role: :writing) do + assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] + assert_not_equal @ro_handler, ActiveRecord::Base.connection_handler + end + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_connects_to_with_single_configuration + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to database: { writing: :development } + + assert_equal 1, ActiveRecord::Base.connection_handlers.size + assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + end + + def test_connects_to_using_top_level_key_in_two_level_config + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly } + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + end + end + + def test_connection_pools + assert_equal([@rw_pool], @handlers[:writing].connection_pools) + assert_equal([@ro_pool], @handlers[:reading].connection_pools) + end + + def test_retrieve_connection + assert @rw_handler.retrieve_connection(@spec_name) + assert @ro_handler.retrieve_connection(@spec_name) + end + + def test_active_connections? + assert_not_predicate @rw_handler, :active_connections? + assert_not_predicate @ro_handler, :active_connections? + + assert @rw_handler.retrieve_connection(@spec_name) + assert @ro_handler.retrieve_connection(@spec_name) + + assert_predicate @rw_handler, :active_connections? + assert_predicate @ro_handler, :active_connections? + + @rw_handler.clear_active_connections! + assert_not_predicate @rw_handler, :active_connections? + + @ro_handler.clear_active_connections! + assert_not_predicate @ro_handler, :active_connections? + end + + def test_retrieve_connection_pool + assert_not_nil @rw_handler.retrieve_connection_pool(@spec_name) + assert_not_nil @ro_handler.retrieve_connection_pool(@spec_name) + end + + def test_retrieve_connection_pool_with_invalid_id + assert_nil @rw_handler.retrieve_connection_pool("foo") + assert_nil @ro_handler.retrieve_connection_pool("foo") + end + end + end +end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index b1ebd20d6b..dfd74bfcb4 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -336,7 +336,7 @@ class DirtyTest < ActiveRecord::TestCase end with_partial_writes Pirate, true do - assert_queries(0) { 2.times { pirate.save! } } + assert_no_queries { 2.times { pirate.save! } } assert_equal old_updated_on, pirate.reload.updated_on assert_queries(1) { pirate.catchphrase = "bar"; pirate.save! } @@ -355,7 +355,7 @@ class DirtyTest < ActiveRecord::TestCase old_lock_version = person.lock_version with_partial_writes Person, true do - assert_queries(0) { 2.times { person.save! } } + assert_no_queries { 2.times { person.save! } } assert_equal old_lock_version, person.reload.lock_version assert_queries(1) { person.first_name = "bar"; person.save! } diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index d5a1d11e12..b4593ccdf2 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -265,6 +265,17 @@ class EnumTest < ActiveRecord::TestCase assert_equal "published", @book.status end + test "invalid definition values raise an ArgumentError" do + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [proposed: 1, written: 2, published: 3] + end + end + + assert_match(/must be either a hash, an array of symbols, or an array of strings./, e.message) + end + test "reserved enum names" do klass = Class.new(ActiveRecord::Base) do self.table_name = "books" diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 36cd63c4d4..5687afbc71 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -157,18 +157,40 @@ class IntegrationTest < ActiveRecord::TestCase skip("Subsecond precision is not supported") unless subsecond_precision_supported? dev = Developer.first key = dev.cache_key - dev.touch + travel_to dev.updated_at + 0.000001 do + dev.touch + end assert_not_equal key, dev.cache_key end def test_cache_key_format_is_not_too_precise - skip("Subsecond precision is not supported") unless subsecond_precision_supported? dev = Developer.first dev.touch key = dev.cache_key assert_equal key, dev.reload.cache_key end + def test_cache_version_format_is_precise_enough + skip("Subsecond precision is not supported") unless subsecond_precision_supported? + with_cache_versioning do + dev = Developer.first + version = dev.cache_version.to_param + travel_to Developer.first.updated_at + 0.000001 do + dev.touch + end + assert_not_equal version, dev.cache_version.to_param + end + end + + def test_cache_version_format_is_not_too_precise + with_cache_versioning do + dev = Developer.first + dev.touch + key = dev.cache_version.to_param + assert_equal key, dev.reload.cache_version.to_param + end + end + def test_named_timestamps_for_cache_key assert_deprecated do owner = owners(:blackbeard) @@ -185,50 +207,52 @@ class IntegrationTest < ActiveRecord::TestCase end def test_cache_key_is_stable_with_versioning_on - Developer.cache_versioning = true - - developer = Developer.first - first_key = developer.cache_key + with_cache_versioning do + developer = Developer.first + first_key = developer.cache_key - developer.touch - second_key = developer.cache_key + developer.touch + second_key = developer.cache_key - assert_equal first_key, second_key - ensure - Developer.cache_versioning = false + assert_equal first_key, second_key + end end def test_cache_version_changes_with_versioning_on - Developer.cache_versioning = true - - developer = Developer.first - first_version = developer.cache_version + with_cache_versioning do + developer = Developer.first + first_version = developer.cache_version - travel 10.seconds do - developer.touch - end + travel 10.seconds do + developer.touch + end - second_version = developer.cache_version + second_version = developer.cache_version - assert_not_equal first_version, second_version - ensure - Developer.cache_versioning = false + assert_not_equal first_version, second_version + end end def test_cache_key_retains_version_when_custom_timestamp_is_used - Developer.cache_versioning = true + with_cache_versioning do + developer = Developer.first + first_key = developer.cache_key_with_version - developer = Developer.first - first_key = developer.cache_key_with_version + travel 10.seconds do + developer.touch + end - travel 10.seconds do - developer.touch - end + second_key = developer.cache_key_with_version - second_key = developer.cache_key_with_version + assert_not_equal first_key, second_key + end + end - assert_not_equal first_key, second_key + def with_cache_versioning(value = true) + @old_cache_versioning = ActiveRecord::Base.cache_versioning + ActiveRecord::Base.cache_versioning = value + yield ensure - Developer.cache_versioning = false + ActiveRecord::Base.cache_versioning = @old_cache_versioning end end diff --git a/activerecord/test/cases/null_relation_test.rb b/activerecord/test/cases/null_relation_test.rb index 17527568f8..ee96ea1af6 100644 --- a/activerecord/test/cases/null_relation_test.rb +++ b/activerecord/test/cases/null_relation_test.rb @@ -10,26 +10,27 @@ class NullRelationTest < ActiveRecord::TestCase fixtures :posts, :comments def test_none - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], Developer.none assert_equal [], Developer.all.none end end def test_none_chainable - assert_no_queries(ignore_none: false) do + Developer.send(:load_schema) + assert_no_queries do assert_equal [], Developer.none.where(name: "David") end end def test_none_chainable_to_existing_scope_extension_method - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal 1, Topic.anonymous_extension.none.one end end def test_none_chained_to_methods_firing_queries_straight_to_db - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal [], Developer.none.pluck(:id, :name) assert_equal 0, Developer.none.delete_all assert_equal 0, Developer.none.update_all(name: "David") @@ -39,7 +40,7 @@ class NullRelationTest < ActiveRecord::TestCase end def test_null_relation_content_size_methods - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal 0, Developer.none.size assert_equal 0, Developer.none.count assert_equal true, Developer.none.empty? @@ -61,7 +62,7 @@ class NullRelationTest < ActiveRecord::TestCase [:count, :sum].each do |method| define_method "test_null_relation_#{method}" do - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_equal 0, Comment.none.public_send(method, :id) assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id) end @@ -70,7 +71,7 @@ class NullRelationTest < ActiveRecord::TestCase [:average, :minimum, :maximum].each do |method| define_method "test_null_relation_#{method}" do - assert_no_queries(ignore_none: false) do + assert_no_queries do assert_nil Comment.none.public_send(method, :id) assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id) end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 8073cabae6..4830ff2b5f 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -446,19 +446,17 @@ class PersistenceTest < ActiveRecord::TestCase end def test_update_attribute_does_not_run_sql_if_attribute_is_not_changed - klass = Class.new(Topic) do - def self.name; "Topic"; end - end - topic = klass.create(title: "Another New Topic") - assert_queries(0) do + topic = Topic.create(title: "Another New Topic") + assert_no_queries do assert topic.update_attribute(:title, "Another New Topic") end end def test_update_does_not_run_sql_if_record_has_not_changed topic = Topic.create(title: "Another New Topic") - assert_queries(0) { assert topic.update(title: "Another New Topic") } - assert_queries(0) { assert topic.update(title: "Another New Topic") } + assert_no_queries do + assert topic.update(title: "Another New Topic") + end end def test_delete diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 3eb4e04cb7..565190c476 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -190,7 +190,7 @@ class QueryCacheTest < ActiveRecord::TestCase Task.cache do assert_queries(2) { Task.find(1); Task.find(2) } end - assert_queries(0) { Task.find(1); Task.find(1); Task.find(2) } + assert_no_queries { Task.find(1); Task.find(1); Task.find(2) } end end @@ -372,7 +372,7 @@ class QueryCacheTest < ActiveRecord::TestCase end # Check that if the same query is run again, no queries are executed - assert_queries(0) do + assert_no_queries do assert_equal 0, Post.where(title: "test").to_a.count end @@ -427,8 +427,9 @@ class QueryCacheTest < ActiveRecord::TestCase # Clear places where type information is cached Task.reset_column_information Task.initialize_find_by_cache + Task.define_attribute_methods - assert_queries(0) do + assert_no_queries do Task.find(1) end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 9914a61033..e471ee8039 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1812,6 +1812,16 @@ class RelationTest < ActiveRecord::TestCase assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",") end + def test_relation_with_private_kernel_method + accounts = Account.all + assert_equal [accounts(:signals37)], accounts.open + assert_equal [accounts(:signals37)], accounts.available + + sub_accounts = SubAccount.all + assert_equal [accounts(:signals37)], sub_accounts.open + assert_equal [accounts(:signals37)], sub_accounts.available + end + test "#skip_query_cache!" do Post.cache do assert_queries(1) do diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index d674bd562f..3fd1813d64 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -731,7 +731,7 @@ module ActiveRecord end if current_adapter?(:SQLite3Adapter) && !in_memory_db? - class DatabaseTasksMigrateTest < ActiveRecord::TestCase + class DatabaseTasksMigrationTestCase < ActiveRecord::TestCase self.use_transactional_tests = false # Use a memory db here to avoid having to rollback at the end @@ -751,7 +751,9 @@ module ActiveRecord @conn.release_connection if @conn ActiveRecord::Base.establish_connection :arunit end + end + class DatabaseTasksMigrateTest < DatabaseTasksMigrationTestCase def test_migrate_set_and_unset_verbose_and_version_env_vars verbose, version = ENV["VERBOSE"], ENV["VERSION"] ENV["VERSION"] = "2" @@ -812,6 +814,26 @@ module ActiveRecord end end end + + class DatabaseTasksMigrateStatusTest < DatabaseTasksMigrationTestCase + def test_migrate_status_table + ActiveRecord::SchemaMigration.create_table + output = capture_migration_status + assert_match(/database: :memory:/, output) + assert_match(/down 001 Valid people have last names/, output) + assert_match(/down 002 We need reminders/, output) + assert_match(/down 003 Innocent jointable/, output) + ActiveRecord::SchemaMigration.drop_table + end + + private + + def capture_migration_status + capture(:stdout) do + ActiveRecord::Tasks::DatabaseTasks.migrate_status + end + end + end end class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 925a4609a2..cd3d5ed7d1 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -100,7 +100,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_later_dont_hit_the_db invoice = Invoice.create! - assert_queries(0) do + assert_no_queries do invoice.touch_later end end diff --git a/activerecord/test/models/account.rb b/activerecord/test/models/account.rb index 0c3cd45a81..639e395743 100644 --- a/activerecord/test/models/account.rb +++ b/activerecord/test/models/account.rb @@ -11,9 +11,8 @@ class Account < ActiveRecord::Base end # Test private kernel method through collection proxy using has_many. - def self.open - where("firm_name = ?", "37signals") - end + scope :open, -> { where("firm_name = ?", "37signals") } + scope :available, -> { open } before_destroy do |account| if account.firm @@ -32,3 +31,11 @@ class Account < ActiveRecord::Base "Sir, yes sir!" end end + +class SubAccount < Account + def self.instantiate_instance_of(klass, attributes, column_types = {}, &block) + klass = superclass + super + end + private_class_method :instantiate_instance_of +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 528585fb75..710a75ad30 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -297,8 +297,6 @@ end class FakeKlass extend ActiveRecord::Delegation::DelegateCache - inherited self - class << self def connection Post.connection @@ -335,5 +333,11 @@ class FakeKlass def predicate_builder Post.predicate_builder end + + def base_class? + true + end end + + inherited self end diff --git a/activestorage/README.md b/activestorage/README.md index b677721d95..bd31f0ea58 100644 --- a/activestorage/README.md +++ b/activestorage/README.md @@ -16,6 +16,8 @@ A key difference to how Active Storage works compared to other attachment soluti Run `rails active_storage:install` to copy over active_storage migrations. +NOTE: If the task cannot be found, verify that `require "active_storage/engine"` is present in `config/application.rb`. + ## Examples One attachment: diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index 7bd641ab9a..99982202dd 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -61,6 +61,6 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController end def acceptable_content?(token) - token[:content_type] == request.content_type && token[:content_length] == request.content_length + token[:content_type] == request.content_mime_type && token[:content_length] == request.content_length end end diff --git a/activestorage/test/controllers/disk_controller_test.rb b/activestorage/test/controllers/disk_controller_test.rb index 4bc61d13f3..7b5e989699 100644 --- a/activestorage/test/controllers/disk_controller_test.rb +++ b/activestorage/test/controllers/disk_controller_test.rb @@ -67,6 +67,16 @@ class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest assert_not blob.service.exist?(blob.key) end + test "directly uploading blob with different but equivalent content type" do + data = "Something else entirely!" + blob = create_blob_before_direct_upload( + byte_size: data.size, checksum: Digest::MD5.base64digest(data), content_type: "application/x-gzip") + + put blob.service_url_for_direct_upload, params: data, headers: { "Content-Type" => "application/x-gzip" } + assert_response :no_content + assert_equal data, blob.download + end + test "directly uploading blob with mismatched content length" do data = "Something else entirely!" blob = create_blob_before_direct_upload byte_size: data.size - 1, checksum: Digest::MD5.base64digest(data) diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 586ed28693..a53e4eb5a2 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,35 @@ +* Deprecate `ActiveSupport::Multibyte::Unicode#pack_graphemes(array)` and `ActiveSuppport::Multibyte::Unicode#unpack_graphemes(string)` + in favor of `array.flatten.pack("U*")` and `string.scan(/\X/).map(&:codepoints)`, respectively. + + *Francesco Rodríguez* + +* Deprecate `ActiveSupport::Multibyte::Chars.consumes?` in favor of `String#is_utf8?`. + + *Francesco Rodríguez* + +* Fix duration being rounded to a full second. + ``` + time = DateTime.parse("2018-1-1") + time += 0.51.seconds + ``` + Will now correctly add 0.51 second and not 1 full second. + + *Edouard Chin* + +* Deprecate `ActiveSupport::Multibyte::Unicode#normalize` and `ActiveSuppport::Multibyte::Chars#normalize` + in favor of `String#unicode_normalize` + + *Francesco Rodríguez* + +* Deprecate `ActiveSupport::Multibyte::Unicode#downcase/upcase/swapcase` in favor of + `String#downcase/upcase/swapcase`. + + *Francesco Rodríguez* + +* Add `ActiveSupport::ParameterFilter`. + + *Yoshiyuki Kinjo* + * Rename `Module#parent`, `Module#parents`, and `Module#parent_name` to `module_parent`, `module_parents`, and `module_parent_name`. diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 222ef2b515..e8518645d9 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -411,8 +411,6 @@ module ActiveSupport # to the cache. If you do not want to write the cache when the cache is # not found, use #read_multi. # - # Options are passed to the underlying cache implementation. - # # Returns a hash with the data for each of the names. For example: # # cache.write("bim", "bam") @@ -422,6 +420,17 @@ module ActiveSupport # # => { "bim" => "bam", # # "unknown_key" => "Fallback value for key: unknown_key" } # + # Options are passed to the underlying cache implementation. For example: + # + # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key| + # "buzz" + # end + # # => {"fizz"=>"buzz"} + # cache.read("fizz") + # # => "buzz" + # sleep(6) + # cache.read("fizz") + # # => nil def fetch_multi(*names) raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given? @@ -647,7 +656,7 @@ module ActiveSupport if key.size > 1 key = key.collect { |element| expanded_key(element) } else - key = key.first + key = expanded_key(key.first) end when Hash key = key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" } diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index 6159e45230..9acf674c40 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -2,7 +2,6 @@ require "active_support/concern" require "active_support/ordered_options" -require "active_support/core_ext/array/extract_options" module ActiveSupport # Configurable provides a <tt>config</tt> method to store and retrieve @@ -105,9 +104,7 @@ module ActiveSupport # end # # User.hair_colors # => [:brown, :black, :blonde, :red] - def config_accessor(*names) #:doc: - options = names.extract_options! - + def config_accessor(*names, instance_reader: true, instance_writer: true, instance_accessor: true) # :doc: names.each do |name| raise NameError.new("invalid config attribute name") unless /\A[_A-Za-z]\w*\z/.match?(name) @@ -117,9 +114,9 @@ module ActiveSupport singleton_class.class_eval reader, __FILE__, reader_line singleton_class.class_eval writer, __FILE__, writer_line - unless options[:instance_accessor] == false - class_eval reader, __FILE__, reader_line unless options[:instance_reader] == false - class_eval writer, __FILE__, writer_line unless options[:instance_writer] == false + if instance_accessor + class_eval reader, __FILE__, reader_line if instance_reader + class_eval writer, __FILE__, writer_line if instance_writer end send("#{name}=", yield) if block_given? end diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index e61b23f842..bc670c3e76 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -110,7 +110,7 @@ class DateTime # instance time. Do not use this method in combination with x.months, use # months_since instead! def since(seconds) - self + Rational(seconds.round, 86400) + self + Rational(seconds, 86400) end alias :in :since diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 281eaa7d5f..5850e0193f 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/array/extract_options" - # Extends the module object with class/module and instance accessors for # class/module attributes, just like the native attr* accessors for instance # attributes. diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb index b1788a000b..cb996e9e6d 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/array/extract_options" - # Extends the module object with class/module and instance accessors for # class/module attributes, just like the native attr* accessors for instance # attributes, but does so on a per-thread basis. @@ -35,9 +33,7 @@ class Module # end # # Current.new.user # => NoMethodError - def thread_mattr_reader(*syms) # :nodoc: - options = syms.extract_options! - + def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true) # :nodoc: syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym) @@ -49,7 +45,7 @@ class Module end EOS - unless options[:instance_reader] == false || options[:instance_accessor] == false + if instance_reader && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym} self.class.#{sym} @@ -78,8 +74,7 @@ class Module # end # # Current.new.user = "DHH" # => NoMethodError - def thread_mattr_writer(*syms) # :nodoc: - options = syms.extract_options! + def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true) # :nodoc: syms.each do |sym| raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym) @@ -91,7 +86,7 @@ class Module end EOS - unless options[:instance_writer] == false || options[:instance_accessor] == false + if instance_writer && instance_accessor class_eval(<<-EOS, __FILE__, __LINE__ + 1) def #{sym}=(obj) self.class.#{sym} = obj @@ -141,9 +136,9 @@ class Module # # Current.new.user = "DHH" # => NoMethodError # Current.new.user # => NoMethodError - def thread_mattr_accessor(*syms) - thread_mattr_reader(*syms) - thread_mattr_writer(*syms) + def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true) + thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor) + thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor) end alias :thread_cattr_accessor :thread_mattr_accessor end diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 7fcd0d0311..8acad6164a 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -4,127 +4,129 @@ require "active_support/core_ext/big_decimal/conversions" require "active_support/number_helper" require "active_support/core_ext/module/deprecation" -module ActiveSupport::NumericWithFormat - # Provides options for converting numbers into formatted strings. - # Options are provided for phone numbers, currency, percentage, - # precision, positional notation, file size and pretty printing. - # - # ==== Options - # - # For details on which formats use which options, see ActiveSupport::NumberHelper - # - # ==== Examples - # - # Phone Numbers: - # 5551234.to_s(:phone) # => "555-1234" - # 1235551234.to_s(:phone) # => "123-555-1234" - # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234" - # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234" - # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555" - # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234" - # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.') - # # => "+1.123.555.1234 x 1343" - # - # Currency: - # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50" - # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51" - # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506" - # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €" - # -1234567890.50.to_s(:currency, negative_format: '(%u%n)') - # # => "($1,234,567,890.50)" - # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '') - # # => "£1234567890,50" - # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u') - # # => "1234567890,50 £" - # - # Percentage: - # 100.to_s(:percentage) # => "100.000%" - # 100.to_s(:percentage, precision: 0) # => "100%" - # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%" - # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%" - # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%" - # 100.to_s(:percentage, format: '%n %') # => "100.000 %" - # - # Delimited: - # 12345678.to_s(:delimited) # => "12,345,678" - # 12345678.05.to_s(:delimited) # => "12,345,678.05" - # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678" - # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678" - # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05" - # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05" - # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',') - # # => "98 765 432,98" - # - # Rounded: - # 111.2345.to_s(:rounded) # => "111.235" - # 111.2345.to_s(:rounded, precision: 2) # => "111.23" - # 13.to_s(:rounded, precision: 5) # => "13.00000" - # 389.32314.to_s(:rounded, precision: 0) # => "389" - # 111.2345.to_s(:rounded, significant: true) # => "111" - # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100" - # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000" - # 111.234.to_s(:rounded, locale: :fr) # => "111,234" - # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true) - # # => "13" - # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3" - # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.') - # # => "1.111,23" - # - # Human-friendly size in Bytes: - # 123.to_s(:human_size) # => "123 Bytes" - # 1234.to_s(:human_size) # => "1.21 KB" - # 12345.to_s(:human_size) # => "12.1 KB" - # 1234567.to_s(:human_size) # => "1.18 MB" - # 1234567890.to_s(:human_size) # => "1.15 GB" - # 1234567890123.to_s(:human_size) # => "1.12 TB" - # 1234567890123456.to_s(:human_size) # => "1.1 PB" - # 1234567890123456789.to_s(:human_size) # => "1.07 EB" - # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB" - # 483989.to_s(:human_size, precision: 2) # => "470 KB" - # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB" - # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB" - # 524288000.to_s(:human_size, precision: 5) # => "500 MB" - # - # Human-friendly format: - # 123.to_s(:human) # => "123" - # 1234.to_s(:human) # => "1.23 Thousand" - # 12345.to_s(:human) # => "12.3 Thousand" - # 1234567.to_s(:human) # => "1.23 Million" - # 1234567890.to_s(:human) # => "1.23 Billion" - # 1234567890123.to_s(:human) # => "1.23 Trillion" - # 1234567890123456.to_s(:human) # => "1.23 Quadrillion" - # 1234567890123456789.to_s(:human) # => "1230 Quadrillion" - # 489939.to_s(:human, precision: 2) # => "490 Thousand" - # 489939.to_s(:human, precision: 4) # => "489.9 Thousand" - # 1234567.to_s(:human, precision: 4, - # significant: false) # => "1.2346 Million" - # 1234567.to_s(:human, precision: 1, - # separator: ',', - # significant: false) # => "1,2 Million" - def to_s(format = nil, options = nil) - case format - when nil - super() - when Integer, String - super(format) - when :phone - ActiveSupport::NumberHelper.number_to_phone(self, options || {}) - when :currency - ActiveSupport::NumberHelper.number_to_currency(self, options || {}) - when :percentage - ActiveSupport::NumberHelper.number_to_percentage(self, options || {}) - when :delimited - ActiveSupport::NumberHelper.number_to_delimited(self, options || {}) - when :rounded - ActiveSupport::NumberHelper.number_to_rounded(self, options || {}) - when :human - ActiveSupport::NumberHelper.number_to_human(self, options || {}) - when :human_size - ActiveSupport::NumberHelper.number_to_human_size(self, options || {}) - when Symbol - super() - else - super(format) +module ActiveSupport + module NumericWithFormat + # Provides options for converting numbers into formatted strings. + # Options are provided for phone numbers, currency, percentage, + # precision, positional notation, file size and pretty printing. + # + # ==== Options + # + # For details on which formats use which options, see ActiveSupport::NumberHelper + # + # ==== Examples + # + # Phone Numbers: + # 5551234.to_s(:phone) # => "555-1234" + # 1235551234.to_s(:phone) # => "123-555-1234" + # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234" + # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234" + # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555" + # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234" + # 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.') + # # => "+1.123.555.1234 x 1343" + # + # Currency: + # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50" + # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51" + # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506" + # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €" + # -1234567890.50.to_s(:currency, negative_format: '(%u%n)') + # # => "($1,234,567,890.50)" + # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '') + # # => "£1234567890,50" + # 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u') + # # => "1234567890,50 £" + # + # Percentage: + # 100.to_s(:percentage) # => "100.000%" + # 100.to_s(:percentage, precision: 0) # => "100%" + # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%" + # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%" + # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%" + # 100.to_s(:percentage, format: '%n %') # => "100.000 %" + # + # Delimited: + # 12345678.to_s(:delimited) # => "12,345,678" + # 12345678.05.to_s(:delimited) # => "12,345,678.05" + # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678" + # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678" + # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05" + # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05" + # 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',') + # # => "98 765 432,98" + # + # Rounded: + # 111.2345.to_s(:rounded) # => "111.235" + # 111.2345.to_s(:rounded, precision: 2) # => "111.23" + # 13.to_s(:rounded, precision: 5) # => "13.00000" + # 389.32314.to_s(:rounded, precision: 0) # => "389" + # 111.2345.to_s(:rounded, significant: true) # => "111" + # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100" + # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000" + # 111.234.to_s(:rounded, locale: :fr) # => "111,234" + # 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true) + # # => "13" + # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3" + # 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.') + # # => "1.111,23" + # + # Human-friendly size in Bytes: + # 123.to_s(:human_size) # => "123 Bytes" + # 1234.to_s(:human_size) # => "1.21 KB" + # 12345.to_s(:human_size) # => "12.1 KB" + # 1234567.to_s(:human_size) # => "1.18 MB" + # 1234567890.to_s(:human_size) # => "1.15 GB" + # 1234567890123.to_s(:human_size) # => "1.12 TB" + # 1234567890123456.to_s(:human_size) # => "1.1 PB" + # 1234567890123456789.to_s(:human_size) # => "1.07 EB" + # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB" + # 483989.to_s(:human_size, precision: 2) # => "470 KB" + # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB" + # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB" + # 524288000.to_s(:human_size, precision: 5) # => "500 MB" + # + # Human-friendly format: + # 123.to_s(:human) # => "123" + # 1234.to_s(:human) # => "1.23 Thousand" + # 12345.to_s(:human) # => "12.3 Thousand" + # 1234567.to_s(:human) # => "1.23 Million" + # 1234567890.to_s(:human) # => "1.23 Billion" + # 1234567890123.to_s(:human) # => "1.23 Trillion" + # 1234567890123456.to_s(:human) # => "1.23 Quadrillion" + # 1234567890123456789.to_s(:human) # => "1230 Quadrillion" + # 489939.to_s(:human, precision: 2) # => "490 Thousand" + # 489939.to_s(:human, precision: 4) # => "489.9 Thousand" + # 1234567.to_s(:human, precision: 4, + # significant: false) # => "1.2346 Million" + # 1234567.to_s(:human, precision: 1, + # separator: ',', + # significant: false) # => "1,2 Million" + def to_s(format = nil, options = nil) + case format + when nil + super() + when Integer, String + super(format) + when :phone + ActiveSupport::NumberHelper.number_to_phone(self, options || {}) + when :currency + ActiveSupport::NumberHelper.number_to_currency(self, options || {}) + when :percentage + ActiveSupport::NumberHelper.number_to_percentage(self, options || {}) + when :delimited + ActiveSupport::NumberHelper.number_to_delimited(self, options || {}) + when :rounded + ActiveSupport::NumberHelper.number_to_rounded(self, options || {}) + when :human + ActiveSupport::NumberHelper.number_to_human(self, options || {}) + when :human_size + ActiveSupport::NumberHelper.number_to_human_size(self, options || {}) + when Symbol + super() + else + super(format) + end end end end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index aa6896af32..ef8a1f476d 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -143,14 +143,14 @@ class NilClass # # With +try+ # @person.try(:children).try(:first).try(:name) - def try(*args) + def try(method_name = nil, *args) nil end # Calling +try!+ on +nil+ always returns +nil+. # # nil.try!(:name) # => nil - def try!(*args) + def try!(method_name = nil, *args) nil end end diff --git a/activesupport/lib/active_support/core_ext/range/conversions.rb b/activesupport/lib/active_support/core_ext/range/conversions.rb index 8832fbcb3c..024e32db40 100644 --- a/activesupport/lib/active_support/core_ext/range/conversions.rb +++ b/activesupport/lib/active_support/core_ext/range/conversions.rb @@ -1,39 +1,41 @@ # frozen_string_literal: true -module ActiveSupport::RangeWithFormat - RANGE_FORMATS = { - db: -> (start, stop) do - case start - when String then "BETWEEN '#{start}' AND '#{stop}'" +module ActiveSupport + module RangeWithFormat + RANGE_FORMATS = { + db: -> (start, stop) do + case start + when String then "BETWEEN '#{start}' AND '#{stop}'" + else + "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" + end + end + } + + # Convert range to a formatted string. See RANGE_FORMATS for predefined formats. + # + # range = (1..100) # => 1..100 + # + # range.to_s # => "1..100" + # range.to_s(:db) # => "BETWEEN '1' AND '100'" + # + # == Adding your own range formats to to_s + # You can add your own formats to the Range::RANGE_FORMATS hash. + # Use the format name as the hash key and a Proc instance. + # + # # config/initializers/range_formats.rb + # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" } + def to_s(format = :default) + if formatter = RANGE_FORMATS[format] + formatter.call(first, last) else - "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" + super() end end - } - # Convert range to a formatted string. See RANGE_FORMATS for predefined formats. - # - # range = (1..100) # => 1..100 - # - # range.to_s # => "1..100" - # range.to_s(:db) # => "BETWEEN '1' AND '100'" - # - # == Adding your own range formats to to_s - # You can add your own formats to the Range::RANGE_FORMATS hash. - # Use the format name as the hash key and a Proc instance. - # - # # config/initializers/range_formats.rb - # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" } - def to_s(format = :default) - if formatter = RANGE_FORMATS[format] - formatter.call(first, last) - else - super() - end + alias_method :to_default_s, :to_s + alias_method :to_formatted_s, :to_s end - - alias_method :to_default_s, :to_s - alias_method :to_formatted_s, :to_s end Range.prepend(ActiveSupport::RangeWithFormat) diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 4e6d8e4585..3145ff87a1 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/callbacks" + module ActiveSupport # Abstract super class that provides a thread-isolated attributes singleton, which resets automatically # before and after each request. This allows you to keep all the per-request attributes easily diff --git a/activesupport/lib/active_support/encrypted_configuration.rb b/activesupport/lib/active_support/encrypted_configuration.rb index 3c6da10548..cc1d026737 100644 --- a/activesupport/lib/active_support/encrypted_configuration.rb +++ b/activesupport/lib/active_support/encrypted_configuration.rb @@ -39,7 +39,7 @@ module ActiveSupport end def deserialize(config) - config.present? ? YAML.load(config, content_path) : {} + YAML.load(config).presence || {} end end end diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb index f48c586cad..ca810db584 100644 --- a/activesupport/lib/active_support/execution_wrapper.rb +++ b/activesupport/lib/active_support/execution_wrapper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/callbacks" +require "concurrent/hash" module ActiveSupport class ExecutionWrapper diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index dbc8b8a2fa..0d2a17970f 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -62,9 +62,9 @@ module ActiveSupport raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String) I18n.transliterate( - ActiveSupport::Multibyte::Unicode.normalize( - ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), - replacement: replacement) + ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc), + replacement: replacement + ) end # Replaces special characters in a string so that it may be used as part of diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 499a206f49..a1e23aeaca 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -17,7 +17,7 @@ module ActiveSupport #:nodoc: # through the +mb_chars+ method. Methods which would normally return a # String object now return a Chars object so methods can be chained. # - # 'The Perfect String '.mb_chars.downcase.strip.normalize + # 'The Perfect String '.mb_chars.downcase.strip # # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string"> # # Chars objects are perfectly interchangeable with String objects as long as @@ -76,6 +76,11 @@ module ActiveSupport #:nodoc: # Returns +true+ when the proxy class can handle the string. Returns # +false+ otherwise. def self.consumes?(string) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Chars.consumes? is deprecated and will be + removed from Rails 6.1. Use string.is_utf8? instead. + MSG + string.encoding == Encoding::UTF_8 end @@ -108,7 +113,7 @@ module ActiveSupport #:nodoc: # # 'Café'.mb_chars.reverse.to_s # => 'éfaC' def reverse - chars(Unicode.unpack_graphemes(@wrapped_string).reverse.flatten.pack("U*")) + chars(@wrapped_string.scan(/\X/).reverse.join) end # Limits the byte size of the string to a number of bytes without breaking @@ -117,35 +122,7 @@ module ActiveSupport #:nodoc: # # 'こんにちは'.mb_chars.limit(7).to_s # => "こん" def limit(limit) - slice(0...translate_offset(limit)) - end - - # Converts characters in the string to uppercase. - # - # 'Laurent, où sont les tests ?'.mb_chars.upcase.to_s # => "LAURENT, OÙ SONT LES TESTS ?" - def upcase - chars Unicode.upcase(@wrapped_string) - end - - # Converts characters in the string to lowercase. - # - # 'VĚDA A VÝZKUM'.mb_chars.downcase.to_s # => "věda a výzkum" - def downcase - chars Unicode.downcase(@wrapped_string) - end - - # Converts characters in the string to the opposite case. - # - # 'El Cañón'.mb_chars.swapcase.to_s # => "eL cAÑÓN" - def swapcase - chars Unicode.swapcase(@wrapped_string) - end - - # Converts the first character to uppercase and the remainder to lowercase. - # - # 'über'.mb_chars.capitalize.to_s # => "Über" - def capitalize - (slice(0) || chars("")).upcase + (slice(1..-1) || chars("")).downcase + truncate_bytes(limit, omission: nil) end # Capitalizes the first letter of every word, when possible. @@ -153,7 +130,7 @@ module ActiveSupport #:nodoc: # "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró" # "日本語".mb_chars.titleize.to_s # => "日本語" def titleize - chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1) }) + chars(downcase.to_s.gsub(/\b('?\S)/u) { $1.upcase }) end alias_method :titlecase, :titleize @@ -165,7 +142,24 @@ module ActiveSupport #:nodoc: # <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. Default is # ActiveSupport::Multibyte::Unicode.default_normalization_form def normalize(form = nil) - chars(Unicode.normalize(@wrapped_string, form)) + form ||= Unicode.default_normalization_form + + # See https://www.unicode.org/reports/tr15, Table 1 + if alias_form = Unicode::NORMALIZATION_FORM_ALIASES[form] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Chars#normalize is deprecated and will be + removed from Rails 6.1. Use #unicode_normalize(:#{alias_form}) instead. + MSG + + send(:unicode_normalize, alias_form) + else + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Chars#normalize is deprecated and will be + removed from Rails 6.1. Use #unicode_normalize instead. + MSG + + raise ArgumentError, "#{form} is not a valid normalization variant", caller + end end # Performs canonical decomposition on all the characters. @@ -189,7 +183,7 @@ module ActiveSupport #:nodoc: # 'क्षि'.mb_chars.length # => 4 # 'क्षि'.mb_chars.grapheme_length # => 3 def grapheme_length - Unicode.unpack_graphemes(@wrapped_string).length + @wrapped_string.scan(/\X/).length end # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent @@ -205,7 +199,7 @@ module ActiveSupport #:nodoc: to_s.as_json(options) end - %w(capitalize downcase reverse tidy_bytes upcase).each do |method| + %w(reverse tidy_bytes).each do |method| define_method("#{method}!") do |*args| @wrapped_string = send(method, *args).to_s self @@ -214,18 +208,6 @@ module ActiveSupport #:nodoc: private - def translate_offset(byte_offset) - return nil if byte_offset.nil? - return 0 if @wrapped_string == "" - - begin - @wrapped_string.byteslice(0...byte_offset).unpack("U*").length - rescue ArgumentError - byte_offset -= 1 - retry - end - end - def chars(string) self.class.new(string) end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 4f0e1165ef..ce8ecece69 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -6,10 +6,17 @@ module ActiveSupport extend self # A list of all available normalization forms. - # See http://www.unicode.org/reports/tr15/tr15-29.html for more + # See https://www.unicode.org/reports/tr15/tr15-29.html for more # information about normalization. NORMALIZATION_FORMS = [:c, :kc, :d, :kd] + NORMALIZATION_FORM_ALIASES = { # :nodoc: + c: :nfc, + d: :nfd, + kc: :nfkc, + kd: :nfkd + } + # The Unicode version that is supported by the implementation UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"] @@ -27,6 +34,11 @@ module ActiveSupport # Unicode.unpack_graphemes('क्षि') # => [[2325, 2381], [2359], [2367]] # Unicode.unpack_graphemes('Café') # => [[67], [97], [102], [233]] def unpack_graphemes(string) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#unpack_graphemes is deprecated and will be + removed from Rails 6.1. Use string.scan(/\X/).map(&:codepoints) instead. + MSG + string.scan(/\X/).map(&:codepoints) end @@ -34,6 +46,11 @@ module ActiveSupport # # Unicode.pack_graphemes(Unicode.unpack_graphemes('क्षि')) # => 'क्षि' def pack_graphemes(unpacked) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#pack_graphemes is deprecated and will be + removed from Rails 6.1. Use array.flatten.pack("U*") instead. + MSG + unpacked.flatten.pack("U*") end @@ -100,31 +117,34 @@ module ActiveSupport # Default is ActiveSupport::Multibyte::Unicode.default_normalization_form. def normalize(string, form = nil) form ||= @default_normalization_form - # See http://www.unicode.org/reports/tr15, Table 1 - case form - when :d - string.unicode_normalize(:nfd) - when :c - string.unicode_normalize(:nfc) - when :kd - string.unicode_normalize(:nfkd) - when :kc - string.unicode_normalize(:nfkc) + + # See https://www.unicode.org/reports/tr15, Table 1 + if alias_form = NORMALIZATION_FORM_ALIASES[form] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be + removed from Rails 6.1. Use String#unicode_normalize(:#{alias_form}) instead. + MSG + + string.unicode_normalize(alias_form) else + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode#normalize is deprecated and will be + removed from Rails 6.1. Use String#unicode_normalize instead. + MSG + raise ArgumentError, "#{form} is not a valid normalization variant", caller end end - def downcase(string) - string.downcase - end + %w(downcase upcase swapcase).each do |method| + define_method(method) do |string| + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport::Multibyte::Unicode##{method} is deprecated and + will be removed from Rails 6.1. Use String methods directly. + MSG - def upcase(string) - string.upcase - end - - def swapcase(string) - string.swapcase + string.send(method) + end end private diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 6207de8094..01cc363e2b 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -34,7 +34,7 @@ module ActiveSupport # name # => String, name of the event (such as 'render' from above) # start # => Time, when the instrumented block started execution # finish # => Time, when the instrumented block ended execution - # id # => String, unique ID for this notification + # id # => String, unique ID for the instrumenter that fired the event # payload # => Hash, the payload # end # @@ -59,7 +59,7 @@ module ActiveSupport # event.payload # => { extra: :information } # # The block in the <tt>subscribe</tt> call gets the name of the event, start - # timestamp, end timestamp, a string with a unique identifier for that event + # timestamp, end timestamp, a string with a unique identifier for that event's instrumenter # (something like "535801666f04d0298cd6"), and a hash with the payload, in # that order. # @@ -171,6 +171,24 @@ module ActiveSupport end end + # Subscribe to a given event name with the passed +block+. + # + # You can subscribe to events by passing a String to match exact event + # names, or by passing a Regexp to match all events that match a pattern. + # + # ActiveSupport::Notifications.subscribe(/render/) do |*args| + # ... + # end + # + # The +block+ will receive five parameters with information about the event: + # + # ActiveSupport::Notifications.subscribe('render') do |name, start, finish, id, payload| + # name # => String, name of the event (such as 'render' from above) + # start # => Time, when the instrumented block started execution + # finish # => Time, when the instrumented block ended execution + # id # => String, unique ID for the instrumenter that fired the event + # payload # => Hash, the payload + # end def subscribe(*args, &block) notifier.subscribe(*args, &block) end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index c75ad52b0c..d19a2f64d4 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/dependencies/autoload" + module ActiveSupport module NumberHelper extend ActiveSupport::Autoload diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb index aef5b62aed..0e8ae82dd5 100644 --- a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToCurrencyConverter < NumberConverter # :nodoc: diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb index 05427fef53..467a580a2e 100644 --- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToDelimitedConverter < NumberConverter #:nodoc: diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb index 908f788ee3..494408fc01 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToHumanConverter < NumberConverter # :nodoc: diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb index 0c72096b72..91262fa656 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToHumanSizeConverter < NumberConverter #:nodoc: diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb index 6618ecffd5..0c2e190f8a 100644 --- a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToPercentageConverter < NumberConverter # :nodoc: diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb index 96410f4995..d5e72981b4 100644 --- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToPhoneConverter < NumberConverter #:nodoc: diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb index 0ee5ef92dd..6ceb9a572e 100644 --- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/number_helper/number_converter" + module ActiveSupport module NumberHelper class NumberToRoundedConverter < NumberConverter # :nodoc: diff --git a/activesupport/lib/active_support/parameter_filter.rb b/activesupport/lib/active_support/parameter_filter.rb new file mode 100644 index 0000000000..59945e9daa --- /dev/null +++ b/activesupport/lib/active_support/parameter_filter.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/duplicable" +require "active_support/core_ext/array/extract" + +module ActiveSupport + # +ParameterFilter+ allows you to specify keys for sensitive data from + # hash-like object and replace corresponding value. Filtering only certain + # sub-keys from a hash is possible by using the dot notation: + # 'credit_card.number'. If a proc is given, each key and value of a hash and + # all sub-hashes are passed to it, where the value or the key can be replaced + # using String#replace or similar methods. + # + # ActiveSupport::ParameterFilter.new([:password]) + # => replaces the value to all keys matching /password/i with "[FILTERED]" + # + # ActiveSupport::ParameterFilter.new([:foo, "bar"]) + # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" + # + # ActiveSupport::ParameterFilter.new(["credit_card.code"]) + # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not + # change { file: { code: "xxxx"} } + # + # ActiveSupport::ParameterFilter.new([-> (k, v) do + # v.reverse! if k =~ /secret/i + # end]) + # => reverses the value to all keys matching /secret/i + class ParameterFilter + FILTERED = "[FILTERED]" # :nodoc: + + def initialize(filters = []) + @filters = filters + end + + def filter(params) + compiled_filter.call(params) + end + + private + + def compiled_filter + @compiled_filter ||= CompiledFilter.compile(@filters) + end + + class CompiledFilter # :nodoc: + def self.compile(filters) + return lambda { |params| params.dup } if filters.empty? + + strings, regexps, blocks = [], [], [] + + filters.each do |item| + case item + when Proc + blocks << item + when Regexp + regexps << item + else + strings << Regexp.escape(item.to_s) + end + end + + deep_regexps = regexps.extract! { |r| r.to_s.include?("\\.") } + deep_strings = strings.extract! { |s| s.include?("\\.") } + + regexps << Regexp.new(strings.join("|"), true) unless strings.empty? + deep_regexps << Regexp.new(deep_strings.join("|"), true) unless deep_strings.empty? + + new regexps, deep_regexps, blocks + end + + attr_reader :regexps, :deep_regexps, :blocks + + def initialize(regexps, deep_regexps, blocks) + @regexps = regexps + @deep_regexps = deep_regexps.any? ? deep_regexps : nil + @blocks = blocks + end + + def call(params, parents = [], original_params = params) + filtered_params = params.class.new + + params.each do |key, value| + parents.push(key) if deep_regexps + if regexps.any? { |r| key =~ r } + value = FILTERED + elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| joined =~ r } + value = FILTERED + elsif value.is_a?(Hash) + value = call(value, parents, original_params) + elsif value.is_a?(Array) + value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v } + elsif blocks.any? + key = key.dup if key.duplicable? + value = value.dup if value.duplicable? + blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) } + end + parents.pop if deep_regexps + + filtered_params[key] = value + end + + filtered_params + end + end + end +end diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 9562149f8d..f3e902f9dd 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -79,12 +79,12 @@ module ActiveSupport end def start(name, id, payload) - e = ActiveSupport::Notifications::Event.new(name, nil, nil, id, payload) - e.start! + event = ActiveSupport::Notifications::Event.new(name, nil, nil, id, payload) + event.start! parent = event_stack.last - parent << e if parent + parent << event if parent - event_stack.push e + event_stack.push event end def finish(name, id, payload) diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb index 30735eb0eb..9f54b1e7de 100644 --- a/activesupport/test/cache/behaviors/cache_store_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -290,6 +290,55 @@ module CacheStoreBehavior assert_equal "bar", @cache.read("fu/foo") end + InstanceTest = Struct.new(:name, :id) do + def cache_key + "#{name}/#{id}" + end + + def to_param + "hello" + end + end + + def test_array_with_single_instance_as_cache_key_uses_cache_key_method + test_instance_one = InstanceTest.new("test", 1) + test_instance_two = InstanceTest.new("test", 2) + + @cache.write([test_instance_one], "one") + @cache.write([test_instance_two], "two") + + assert_equal "one", @cache.read([test_instance_one]) + assert_equal "two", @cache.read([test_instance_two]) + end + + def test_array_with_multiple_instances_as_cache_key_uses_cache_key_method + test_instance_one = InstanceTest.new("test", 1) + test_instance_two = InstanceTest.new("test", 2) + test_instance_three = InstanceTest.new("test", 3) + + @cache.write([test_instance_one, test_instance_three], "one") + @cache.write([test_instance_two, test_instance_three], "two") + + assert_equal "one", @cache.read([test_instance_one, test_instance_three]) + assert_equal "two", @cache.read([test_instance_two, test_instance_three]) + end + + def test_format_of_expanded_key_for_single_instance + test_instance_one = InstanceTest.new("test", 1) + + expanded_key = @cache.send(:expanded_key, test_instance_one) + + assert_equal expanded_key, test_instance_one.cache_key + end + + def test_format_of_expanded_key_for_single_instance_in_array + test_instance_one = InstanceTest.new("test", 1) + + expanded_key = @cache.send(:expanded_key, [test_instance_one]) + + assert_equal expanded_key, test_instance_one.cache_key + end + def test_hash_as_cache_key @cache.write({ foo: 1, fu: 2 }, "bar") assert_equal "bar", @cache.read("foo=1/fu=2") diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 894fb80cba..f9f6b21c9b 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -152,8 +152,8 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert_equal DateTime.civil(2005, 2, 22, 11, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(3600) assert_equal DateTime.civil(2005, 2, 24, 10, 10, 10), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2) assert_equal DateTime.civil(2005, 2, 24, 11, 10, 35), DateTime.civil(2005, 2, 22, 10, 10, 10).since(86400 * 2 + 3600 + 25) - assert_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333) - assert_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667) + assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 11), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.333) + assert_not_equal DateTime.civil(2005, 2, 22, 10, 10, 12), DateTime.civil(2005, 2, 22, 10, 10, 10).since(1.667) end def test_change diff --git a/activesupport/test/encrypted_configuration_test.rb b/activesupport/test/encrypted_configuration_test.rb index 93ccf457de..387d6e1c1f 100644 --- a/activesupport/test/encrypted_configuration_test.rb +++ b/activesupport/test/encrypted_configuration_test.rb @@ -42,6 +42,12 @@ class EncryptedConfigurationTest < ActiveSupport::TestCase assert @credentials.something[:good] end + test "reading comment-only configuration" do + @credentials.write("# comment") + + assert_equal @credentials.config, {} + end + test "change configuration by key file" do @credentials.write({ something: { good: true } }.to_yaml) @credentials.change do |config_file| diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index ebc5df14dd..8062873386 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -157,6 +157,16 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal({ "foo" => "hello" }, JSON.parse(json)) end + def test_struct_to_json_with_options_nested + klass = Struct.new(:foo, :bar) + struct = klass.new "hello", "world" + parent_struct = klass.new struct, "world" + json = parent_struct.to_json only: [:foo] + + assert_equal({ "foo" => { "foo" => "hello" } }, JSON.parse(json)) + end + + def test_hash_should_pass_encoding_options_to_children_in_as_json person = { name: "John", diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 11c4822748..f87099566b 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -73,9 +73,15 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_consumes_utf8_strings - assert @proxy_class.consumes?(UNICODE_STRING) - assert @proxy_class.consumes?(ASCII_STRING) - assert_not @proxy_class.consumes?(BYTE_STRING) + ActiveSupport::Deprecation.silence do + assert @proxy_class.consumes?(UNICODE_STRING) + assert @proxy_class.consumes?(ASCII_STRING) + assert_not @proxy_class.consumes?(BYTE_STRING) + end + end + + def test_consumes_is_deprecated + assert_deprecated { @proxy_class.consumes?(UNICODE_STRING) } end def test_concatenation_should_return_a_proxy_class_instance @@ -165,7 +171,9 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase assert chars("").upcase.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").downcase.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").capitalize.kind_of?(ActiveSupport::Multibyte.proxy_class) - assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class) + ActiveSupport::Deprecation.silence do + assert chars("").normalize.kind_of?(ActiveSupport::Multibyte.proxy_class) + end assert chars("").decompose.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").compose.kind_of?(ActiveSupport::Multibyte.proxy_class) assert chars("").tidy_bytes.kind_of?(ActiveSupport::Multibyte.proxy_class) @@ -383,10 +391,12 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase def test_reverse_should_work_with_normalized_strings str = "bös" reversed_str = "söb" - assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse - assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse - assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse - assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse + ActiveSupport::Deprecation.silence do + assert_equal chars(reversed_str).normalize(:kc), chars(str).normalize(:kc).reverse + assert_equal chars(reversed_str).normalize(:c), chars(str).normalize(:c).reverse + assert_equal chars(reversed_str).normalize(:d), chars(str).normalize(:d).reverse + assert_equal chars(reversed_str).normalize(:kd), chars(str).normalize(:kd).reverse + end assert_equal chars(reversed_str).decompose, chars(str).decompose.reverse assert_equal chars(reversed_str).compose, chars(str).compose.reverse end @@ -477,7 +487,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase def test_method_works_for_proxyed_methods assert_equal "ll", "hello".mb_chars.method(:slice).call(2..3) # Defined on Chars - chars = "hello".mb_chars + chars = +"hello".mb_chars assert_equal "Hello", chars.method(:capitalize!).call # Defined on Chars assert_equal "Hello", chars assert_equal "jello", "hello".mb_chars.method(:gsub).call(/h/, "j") # Defined on String @@ -568,7 +578,9 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase def test_composition_exclusion_is_set_up_properly # Normalization of DEVANAGARI LETTER QA breaks when composition exclusion isn't used correctly qa = [0x915, 0x93c].pack("U*") - assert_equal qa, chars(qa).normalize(:c) + ActiveSupport::Deprecation.silence do + assert_equal qa, chars(qa).normalize(:c) + end end # Test for the Public Review Issue #29, bad explanation of composition might lead to a @@ -578,17 +590,21 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase [0x0B47, 0x0300, 0x0B3E], [0x1100, 0x0300, 0x1161] ].map { |c| c.pack("U*") }.each do |c| - assert_equal_codepoints c, chars(c).normalize(:c) + ActiveSupport::Deprecation.silence do + assert_equal_codepoints c, chars(c).normalize(:c) + end end end def test_normalization_shouldnt_strip_null_bytes null_byte_str = "Test\0test" - assert_equal null_byte_str, chars(null_byte_str).normalize(:kc) - assert_equal null_byte_str, chars(null_byte_str).normalize(:c) - assert_equal null_byte_str, chars(null_byte_str).normalize(:d) - assert_equal null_byte_str, chars(null_byte_str).normalize(:kd) + ActiveSupport::Deprecation.silence do + assert_equal null_byte_str, chars(null_byte_str).normalize(:kc) + assert_equal null_byte_str, chars(null_byte_str).normalize(:c) + assert_equal null_byte_str, chars(null_byte_str).normalize(:d) + assert_equal null_byte_str, chars(null_byte_str).normalize(:kd) + end assert_equal null_byte_str, chars(null_byte_str).decompose assert_equal null_byte_str, chars(null_byte_str).compose end @@ -601,11 +617,13 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase 323 # COMBINING DOT BELOW ].pack("U*") - assert_equal_codepoints "", chars("").normalize - assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s - assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s - assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s - assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s + ActiveSupport::Deprecation.silence do + assert_equal_codepoints "", chars("").normalize + assert_equal_codepoints [44, 105, 106, 328, 323].pack("U*"), chars(comp_str).normalize(:kc).to_s + assert_equal_codepoints [44, 307, 328, 323].pack("U*"), chars(comp_str).normalize(:c).to_s + assert_equal_codepoints [44, 307, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:d).to_s + assert_equal_codepoints [44, 105, 106, 110, 780, 78, 769].pack("U*"), chars(comp_str).normalize(:kd).to_s + end end def test_should_compute_grapheme_length @@ -719,6 +737,41 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase assert_equal BYTE_STRING.dup.mb_chars.class, ActiveSupport::Multibyte::Chars end + def test_unicode_normalize_deprecation + # String#unicode_normalize default form is `:nfc`, and + # different than Multibyte::Unicode default, `:nkfc`. + # Deprecation should suggest the right form if no params + # are given and default is used. + assert_deprecated(/unicode_normalize\(:nfkc\)/) do + ActiveSupport::Multibyte::Unicode.normalize("") + end + + assert_deprecated(/unicode_normalize\(:nfd\)/) do + ActiveSupport::Multibyte::Unicode.normalize("", :d) + end + end + + def test_chars_normalize_deprecation + # String#unicode_normalize default form is `:nfc`, and + # different than Multibyte::Unicode default, `:nkfc`. + # Deprecation should suggest the right form if no params + # are given and default is used. + assert_deprecated(/unicode_normalize\(:nfkc\)/) do + "".mb_chars.normalize + end + + assert_deprecated(/unicode_normalize\(:nfc\)/) { "".mb_chars.normalize(:c) } + assert_deprecated(/unicode_normalize\(:nfd\)/) { "".mb_chars.normalize(:d) } + assert_deprecated(/unicode_normalize\(:nfkc\)/) { "".mb_chars.normalize(:kc) } + assert_deprecated(/unicode_normalize\(:nfkd\)/) { "".mb_chars.normalize(:kd) } + end + + def test_unicode_deprecations + assert_deprecated { ActiveSupport::Multibyte::Unicode.downcase("") } + assert_deprecated { ActiveSupport::Multibyte::Unicode.upcase("") } + assert_deprecated { ActiveSupport::Multibyte::Unicode.swapcase("") } + end + private def string_from_classes(classes) @@ -732,21 +785,3 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase end.pack("U*") end end - -class MultibyteInternalsTest < ActiveSupport::TestCase - include MultibyteTestHelpers - - test "Chars translates a character offset to a byte offset" do - example = chars("Puisque c'était son erreur, il m'a aidé") - [ - [0, 0], - [3, 3], - [12, 11], - [14, 13], - [41, 39] - ].each do |byte_offset, character_offset| - assert_equal character_offset, example.send(:translate_offset, byte_offset), - "Expected byte offset #{byte_offset} to translate to #{character_offset}" - end - end -end diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index a704505fc6..b19689723f 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -18,64 +18,72 @@ class MultibyteConformanceTest < ActiveSupport::TestCase end def test_normalizations_C - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols - # CONFORMANCE: - # 1. The following invariants must be true for all conformant implementations - # - # NFC - # c2 == NFC(c1) == NFC(c2) == NFC(c3) - assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" - # - # c4 == NFC(c4) == NFC(c5) - assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + # CONFORMANCE: + # 1. The following invariants must be true for all conformant implementations + # + # NFC + # c2 == NFC(c1) == NFC(c2) == NFC(c3) + assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" + # + # c4 == NFC(c4) == NFC(c5) + assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + end end end def test_normalizations_D - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols - # - # NFD - # c3 == NFD(c1) == NFD(c2) == NFD(c3) - assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" - # c5 == NFD(c4) == NFD(c5) - assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + # + # NFD + # c3 == NFD(c1) == NFD(c2) == NFD(c3) + assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" + # c5 == NFD(c4) == NFD(c5) + assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + end end end def test_normalizations_KC - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKC - # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) - assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKC + # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) + assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + end end end def test_normalizations_KD - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKD - # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) - assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKD + # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) + assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + end end end diff --git a/activesupport/test/multibyte_grapheme_break_conformance_test.rb b/activesupport/test/multibyte_grapheme_break_conformance_test.rb index 61b171a8d4..97963279af 100644 --- a/activesupport/test/multibyte_grapheme_break_conformance_test.rb +++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb @@ -17,10 +17,12 @@ class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase end def test_breaks - each_line_of_break_tests do |*cols| - *clusters, comment = *cols - packed = ActiveSupport::Multibyte::Unicode.pack_graphemes(clusters) - assert_equal clusters, ActiveSupport::Multibyte::Unicode.unpack_graphemes(packed), comment + ActiveSupport::Deprecation.silence do + each_line_of_break_tests do |*cols| + *clusters, comment = *cols + packed = ActiveSupport::Multibyte::Unicode.pack_graphemes(clusters) + assert_equal clusters, ActiveSupport::Multibyte::Unicode.unpack_graphemes(packed), comment + end end end diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb index 3674ea44f3..82edf69294 100644 --- a/activesupport/test/multibyte_normalization_conformance_test.rb +++ b/activesupport/test/multibyte_normalization_conformance_test.rb @@ -18,64 +18,72 @@ class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase end def test_normalizations_C - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols - # CONFORMANCE: - # 1. The following invariants must be true for all conformant implementations - # - # NFC - # c2 == NFC(c1) == NFC(c2) == NFC(c3) - assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" - assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" - # - # c4 == NFC(c4) == NFC(c5) - assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + # CONFORMANCE: + # 1. The following invariants must be true for all conformant implementations + # + # NFC + # c2 == NFC(c1) == NFC(c2) == NFC(c3) + assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" + # + # c4 == NFC(c4) == NFC(c5) + assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + end end end def test_normalizations_D - each_line_of_norm_tests do |*cols| - col1, col2, col3, col4, col5, comment = *cols - # - # NFD - # c3 == NFD(c1) == NFD(c2) == NFD(c3) - assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" - assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" - # c5 == NFD(c4) == NFD(c5) - assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + # + # NFD + # c3 == NFD(c1) == NFD(c2) == NFD(c3) + assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" + # c5 == NFD(c4) == NFD(c5) + assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + end end end def test_normalizations_KC - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKC - # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) - assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" - assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKC + # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) + assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + end end end def test_normalizations_KD - each_line_of_norm_tests do | *cols | - col1, col2, col3, col4, col5, comment = *cols - # - # NFKD - # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) - assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" - assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + ActiveSupport::Deprecation.silence do + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKD + # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) + assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + end end end diff --git a/activesupport/test/parameter_filter_test.rb b/activesupport/test/parameter_filter_test.rb new file mode 100644 index 0000000000..3403a3188b --- /dev/null +++ b/activesupport/test/parameter_filter_test.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "abstract_unit" +require "active_support/core_ext/hash" +require "active_support/parameter_filter" + +class ParameterFilterTest < ActiveSupport::TestCase + test "process parameter filter" do + test_hashes = [ + [{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'], + [{ "foo" => "bar" }, { "foo" => "[FILTERED]" }, %w'foo'], + [{ "foo" => "bar", "bar" => "foo" }, { "foo" => "[FILTERED]", "bar" => "foo" }, %w'foo baz'], + [{ "foo" => "bar", "baz" => "foo" }, { "foo" => "[FILTERED]", "baz" => "[FILTERED]" }, %w'foo baz'], + [{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => "[FILTERED]", "bar" => "foo" } }, %w'fo'], + [{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => "[FILTERED]" }, %w'f banana'], + [{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => "[FILTERED]", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'], + [{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => "[FILTERED]" }, "1"] }, [/foo/]]] + + test_hashes.each do |before_filter, after_filter, filter_words| + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words) + assert_equal after_filter, parameter_filter.filter(before_filter) + + filter_words << "blah" + filter_words << lambda { |key, value| + value.reverse! if key =~ /bargain/ + } + filter_words << lambda { |key, value, original_params| + value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello" + } + + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words) + before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } } + after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]", "hello" => "world!" } } } + + assert_equal after_filter, parameter_filter.filter(before_filter) + end + end + + test "parameter filter should maintain hash with indifferent access" do + test_hashes = [ + [{ "foo" => "bar" }.with_indifferent_access, ["blah"]], + [{ "foo" => "bar" }.with_indifferent_access, []] + ] + + test_hashes.each do |before_filter, filter_words| + parameter_filter = ActiveSupport::ParameterFilter.new(filter_words) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, + parameter_filter.filter(before_filter) + end + end +end diff --git a/guides/assets/images/rails_guides_logo_1x.png b/guides/assets/images/rails_guides_logo_1x.png Binary files differnew file mode 100644 index 0000000000..8c6810c312 --- /dev/null +++ b/guides/assets/images/rails_guides_logo_1x.png diff --git a/guides/assets/images/rails_guides_logo_2x.png b/guides/assets/images/rails_guides_logo_2x.png Binary files differnew file mode 100644 index 0000000000..accc6bbfa4 --- /dev/null +++ b/guides/assets/images/rails_guides_logo_2x.png diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css index 2657a84a91..bdc3e21977 100644 --- a/guides/assets/stylesheets/main.css +++ b/guides/assets/stylesheets/main.css @@ -401,14 +401,10 @@ a, a:link, a:visited { } #guides { - width: 27em; + width: 37em; display: block; background: #980905; border-radius: 1em; - -webkit-border-radius: 1em; - -moz-border-radius: 1em; - -webkit-box-shadow: 0.25em 0.25em 1em rgba(0,0,0,0.25); - -moz-box-shadow: rgba(0,0,0,0.25) 0.25em 0.25em 1em; color: #f1938c; padding: 1.5em 2em; position: absolute; @@ -422,17 +418,44 @@ a, a:link, a:visited { display: block !important; } -#guides dt, #guides dd { +.guides-section dt, .guides-section dd { font-weight: normal; font-size: 0.722em; margin: 0; padding: 0; } -#guides dt {padding:0; margin: 0.5em 0 0;} -#guides a {color: #FFF; background: none !important; text-decoration: none;} -#guides a:hover {text-decoration: underline;} -#guides .L, #guides .R {float: left; width: 50%; margin: 0; padding: 0;} -#guides .R {float: right;} +.guides-section dt { + margin: 0.5em 0 0; + padding:0; +} +#guides a { + background: none !important; + color: #FFF; + text-decoration: none; +} +#guides a:hover { + text-decoration: underline; +} +.guides-section-container { + display: flex; + flex-direction: column; + flex-wrap: wrap; + width: 100%; + max-height: 35em; +} + +.guides-section { + min-width: 5em; + margin: 0 2em 0.5em 0; + flex: auto; + max-width: 12em; +} + +.guides-section dd { + line-height: 1.3; + margin-bottom: 0.5em; +} + #guides hr { display: block; border: none; @@ -515,13 +538,26 @@ h6 { #header h1 { float: left; - background: url(../images/rails_guides_logo.gif) no-repeat; + background: url(../images/rails_guides_logo_1x.png) no-repeat; width: 297px; text-indent: -9999em; margin: 0; padding: 0; } +@media +only screen and (-webkit-min-device-pixel-ratio: 2), +only screen and ( min--moz-device-pixel-ratio: 2), +only screen and ( -o-min-device-pixel-ratio: 2/1), +only screen and ( min-device-pixel-ratio: 2), +only screen and ( min-resolution: 192dpi), +only screen and ( min-resolution: 2dppx) { + #header h1 { + background: url(../images/rails_guides_logo_2x.png) no-repeat; + background-size: 160%; + } +} + @media screen and (max-width: 480px) { #header h1 { float: none; diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index 82bb4d6de1..f186ac526f 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -31,7 +31,7 @@ HTML header_with_id = text.scan(/(.*){#(.*)}/) unless header_with_id.empty? - %(<h#{header_level} id=#{header_with_id[0][1].strip}>#{header_with_id[0][0].strip}</h#{header_level}>) + %(<h#{header_level} id="#{header_with_id[0][1].strip}">#{header_with_id[0][0].strip}</h#{header_level}>) else %(<h#{header_level}>#{text}</h#{header_level}>) end diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md index d26d3d3b95..a5a7eb4b2e 100644 --- a/guides/source/5_1_release_notes.md +++ b/guides/source/5_1_release_notes.md @@ -399,7 +399,7 @@ Please refer to the [Changelog][action-view] for detailed changes. * Change `datetime_field` and `datetime_field_tag` to generate `datetime-local` fields. - ([Pull Request](https://github.com/rails/rails/pull/28061)) + ([Pull Request](https://github.com/rails/rails/pull/25469)) * New Builder-style syntax for HTML tags (`tag.div`, `tag.br`, etc.) ([Pull Request](https://github.com/rails/rails/pull/25543)) diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 14c859994c..e6c0ae31a8 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -665,7 +665,7 @@ The above will start a cable server on port 28080. The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle -authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication). +authentication. You can see one way of doing that with Devise in this [article](https://greg.molnar.io/blog/actioncable-devise-authentication/). ## Dependencies diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 43bc9306ce..aa746e4731 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -166,7 +166,7 @@ NOTE: Support for parsing XML parameters has been extracted into a gem named `ac The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id`, will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL: ```ruby -get '/clients/:status' => 'clients#index', foo: 'bar' +get '/clients/:status', to: 'clients#index', foo: 'bar' ``` In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar", as if it were passed in the query string. Your controller will also receive `params[:action]` as "index" and `params[:controller]` as "clients". diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index fad4c19827..b9e24099b1 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -82,9 +82,9 @@ of two or more words, the model class name should follow the Ruby conventions, using the CamelCase form, while the table name must contain the words separated by underscores. Examples: -* Database Table - Plural with underscores separating words (e.g., `book_clubs`). * Model Class - Singular with the first letter of each word capitalized (e.g., `BookClub`). +* Database Table - Plural with underscores separating words (e.g., `book_clubs`). | Model / Class | Table / Schema | | ---------------- | -------------- | diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md index 71ba6184e0..d5387219f5 100644 --- a/guides/source/active_storage_overview.md +++ b/guides/source/active_storage_overview.md @@ -82,6 +82,14 @@ To use the Amazon S3 service in production, you add the following to config.active_storage.service = :amazon ``` +To use the test service when testing, you add the following to +`config/environments/test.rb`: + +```ruby +# Store uploaded files on the local file system in a temporary directory. +config.active_storage.service = :test +``` + Continue reading for more information on the built-in service adapters (e.g. `Disk` and `S3`) and the configuration they require. diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 9963125fa2..64db141381 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -623,7 +623,7 @@ The block receives the following arguments: * The name of the event * Time when it started * Time when it finished -* A unique ID for this event +* A unique ID for the instrumenter that fired the event * The payload (described in previous sections) ```ruby @@ -672,7 +672,8 @@ Creating custom events Adding your own events is easy as well. `ActiveSupport::Notifications` will take care of all the heavy lifting for you. Simply call `instrument` with a `name`, `payload` and a block. The notification will be sent after the block returns. `ActiveSupport` will generate the start and end times -as well as the unique ID. All data passed into the `instrument` call will make it into the payload. +and add the instrumenter's unique ID. All data passed into the `instrument` call will make +it into the payload. Here's an example: diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 66cf9da33b..500e230ff9 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -233,11 +233,6 @@ code for JavaScript plugins and CSS frameworks. Keep in mind that third party code with references to other files also processed by the asset Pipeline (images, stylesheets, etc.), will need to be rewritten to use helpers like `asset_path`. -WARNING: If you are upgrading from Rails 3, please take into account that assets -under `lib/assets` or `vendor/assets` are available for inclusion via the -application manifests but no longer part of the precompile array. See -[Precompiling Assets](#precompiling-assets) for guidance. - #### Search Paths When a file is referenced from a manifest or a helper, Sprockets searches the @@ -1234,60 +1229,3 @@ it as a preprocessor for your mime type. Sprockets.register_preprocessor 'text/css', AddComment ``` -Upgrading from Old Versions of Rails ------------------------------------- - -There are a few issues when upgrading from Rails 3.0 or Rails 2.x. The first is -moving the files from `public/` to the new locations. See [Asset -Organization](#asset-organization) above for guidance on the correct locations -for different file types. - -Next is updating the various environment files with the correct default -options. - -In `application.rb`: - -```ruby -# Version of your assets, change this if you want to expire all your assets -config.assets.version = '1.0' - -# Change the path that assets are served from config.assets.prefix = "/assets" -``` - -In `development.rb`: - -```ruby -# Expands the lines which load the assets -config.assets.debug = true -``` - -And in `production.rb`: - -```ruby -# Choose the compressors to use (if any) -config.assets.js_compressor = :uglifier -# config.assets.css_compressor = :yui - -# Don't fallback to assets pipeline if a precompiled asset is missed -config.assets.compile = false - -# Generate digests for assets URLs. -config.assets.digest = true - -# Precompile additional assets (application.js, application.css, and all -# non-JS/CSS are already added) -# config.assets.precompile += %w( admin.js admin.css ) -``` - -Rails 4 and above no longer set default config values for Sprockets in `test.rb`, so -`test.rb` now requires Sprockets configuration. The old defaults in the test -environment are: `config.assets.compile = true`, `config.assets.compress = false`, -`config.assets.debug = false` and `config.assets.digest = false`. - -The following should also be added to your `Gemfile`: - -```ruby -gem 'sass-rails', "~> 3.2.3" -gem 'coffee-rails', "~> 3.2.1" -gem 'uglifier' -``` diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index a2231c55d7..78a1f47407 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -109,7 +109,7 @@ class CreateBooks < ActiveRecord::Migration[5.0] end create_table :books do |t| - t.belongs_to :author, index: true + t.belongs_to :author t.datetime :published_at t.timestamps end @@ -140,7 +140,7 @@ class CreateSuppliers < ActiveRecord::Migration[5.0] end create_table :accounts do |t| - t.belongs_to :supplier, index: true + t.belongs_to :supplier t.string :account_number t.timestamps end @@ -184,7 +184,7 @@ class CreateAuthors < ActiveRecord::Migration[5.0] end create_table :books do |t| - t.belongs_to :author, index: true + t.belongs_to :author t.datetime :published_at t.timestamps end @@ -231,8 +231,8 @@ class CreateAppointments < ActiveRecord::Migration[5.0] end create_table :appointments do |t| - t.belongs_to :physician, index: true - t.belongs_to :patient, index: true + t.belongs_to :physician + t.belongs_to :patient t.datetime :appointment_date t.timestamps end @@ -312,13 +312,13 @@ class CreateAccountHistories < ActiveRecord::Migration[5.0] end create_table :accounts do |t| - t.belongs_to :supplier, index: true + t.belongs_to :supplier t.string :account_number t.timestamps end create_table :account_histories do |t| - t.belongs_to :account, index: true + t.belongs_to :account t.integer :credit_rating t.timestamps end @@ -358,8 +358,8 @@ class CreateAssembliesAndParts < ActiveRecord::Migration[5.0] end create_table :assemblies_parts, id: false do |t| - t.belongs_to :assembly, index: true - t.belongs_to :part, index: true + t.belongs_to :assembly + t.belongs_to :part end end end @@ -487,7 +487,7 @@ class CreatePictures < ActiveRecord::Migration[5.0] def change create_table :pictures do |t| t.string :name - t.references :imageable, polymorphic: true, index: true + t.references :imageable, polymorphic: true t.timestamps end end @@ -517,7 +517,7 @@ In your migrations/schema, you will add a references column to the model itself. class CreateEmployees < ActiveRecord::Migration[5.0] def change create_table :employees do |t| - t.references :manager, index: true + t.references :manager t.timestamps end end @@ -868,7 +868,7 @@ While Rails uses intelligent defaults that will work well in most situations, th ```ruby class Book < ApplicationRecord - belongs_to :author, dependent: :destroy, + belongs_to :author, touch: :books_updated_at, counter_cache: true end ``` @@ -1048,8 +1048,7 @@ There may be times when you wish to customize the query used by `belongs_to`. Su ```ruby class Book < ApplicationRecord - belongs_to :author, -> { where active: true }, - dependent: :destroy + belongs_to :author, -> { where active: true } end ``` diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 812565b207..61bb35cf93 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -200,8 +200,6 @@ The full set of methods that can be used in this block are as follows: * `helper` defines whether or not to generate helpers. Defaults to `true`. * `integration_tool` defines which integration tool to use to generate integration tests. Defaults to `:test_unit`. * `system_tests` defines which integration tool to use to generate system tests. Defaults to `:test_unit`. -* `javascripts` turns on the hook for JavaScript files in generators. Used in Rails for when the `scaffold` generator is run. Defaults to `true`. -* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `:js`. * `orm` defines which orm to use. Defaults to `false` and will use Active Record by default. * `resource_controller` defines which generator to use for generating a controller when using `rails generate resource`. Defaults to `:controller`. * `resource_route` defines whether a resource route definition should be generated diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 01848bdc11..ed47a0de0f 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -336,6 +336,26 @@ $ bundle exec ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_lay The `-n` option allows you to run a single method instead of the whole file. +#### Running tests with a specific seed + +Test execution is randomized with a randomization seed. If you are experiencing random +test failures you can more accurately reproduce a failing test scenario by specifically +setting the randomization seed. + +Running all tests for a component: + +```bash +$ cd actionmailer +$ SEED=15002 bundle exec rake test +``` + +Running a single test file: + +```bash +$ cd actionmailer +$ SEED=15002 bundle exec ruby -w -Itest test/mail_layout_test.rb +``` + #### Testing Active Record First, create the databases you'll need. You can find a list of the required @@ -576,13 +596,13 @@ $ git rebase -i rails/master < Choose 'squash' for all of your commits except the first one. > < Edit the commit message to make sense, and describe all your changes. > -$ git push fork my_new_branch -f +$ git push fork my_new_branch --force-with-lease ``` You should be able to refresh the pull request on GitHub and see that it has been updated. -#### Updating pull request +#### Updating a pull request Sometimes you will be asked to make some changes to the code you have already committed. This can include amending existing commits. In this @@ -592,12 +612,13 @@ you can force push to your branch on GitHub as described earlier in squashing commits section: ```bash -$ git push fork my_new_branch -f +$ git push fork my_new_branch --force-with-lease ``` -This will update the branch and pull request on GitHub with your new code. Do -note that using force push may result in commits being lost on the remote branch; use it with care. - +This will update the branch and pull request on GitHub with your new code. +By force pushing with `--force-with-lease`, git will more safely update +the remote than with a typical `-f`, which can delete work from the remote +that you don't already have. ### Older Versions of Ruby on Rails diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index 88d205e1ab..7f7766e7d7 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -186,14 +186,17 @@ end Here's an example of the log generated when this controller action is executed: ``` -Started POST "/articles" for 127.0.0.1 at 2017-08-20 20:53:10 +0900 +Started POST "/articles" for 127.0.0.1 at 2018-10-18 20:09:23 -0400 Processing by ArticlesController#create as HTML - Parameters: {"utf8"=>"✓", "authenticity_token"=>"xhuIbSBFytHCE1agHgvrlKnSVIOGD6jltW2tO+P6a/ACjQ3igjpV4OdbsZjIhC98QizWH9YdKokrqxBCJrtoqQ==", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", "published"=>"0"}, "commit"=>"Create Article"} -New article: {"id"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", "published"=>false, "created_at"=>nil, "updated_at"=>nil} + Parameters: {"utf8"=>"✓", "authenticity_token"=>"XLveDrKzF1SwaiNRPTaMtkrsTzedtebPPkmxEFIU0ordLjICSnXsSNfrdMa4ccyBjuGwnnEiQhEoMN6H1Gtz3A==", "article"=>{"title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>"0"}, "commit"=>"Create Article"} +New article: {"id"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs.", "published"=>false, "created_at"=>nil, "updated_at"=>nil} Article should be valid: true - (0.1ms) BEGIN - SQL (0.4ms) INSERT INTO "articles" ("title", "body", "published", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["title", "Debugging Rails"], ["body", "I'm learning how to print in logs!!!"], ["published", "f"], ["created_at", "2017-08-20 11:53:10.010435"], ["updated_at", "2017-08-20 11:53:10.010435"]] - (0.3ms) COMMIT + (0.0ms) begin transaction + ↳ app/controllers/articles_controller.rb:31 + Article Create (0.5ms) INSERT INTO "articles" ("title", "body", "published", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["title", "Debugging Rails"], ["body", "I'm learning how to print in logs."], ["published", 0], ["created_at", "2018-10-19 00:09:23.216549"], ["updated_at", "2018-10-19 00:09:23.216549"]] + ↳ app/controllers/articles_controller.rb:31 + (2.3ms) commit transaction + ↳ app/controllers/articles_controller.rb:31 The article was saved and now the user is going to be redirected... Redirected to http://localhost:3000/articles/1 Completed 302 Found in 4ms (ActiveRecord: 0.8ms) @@ -201,6 +204,40 @@ Completed 302 Found in 4ms (ActiveRecord: 0.8ms) Adding extra logging like this makes it easy to search for unexpected or unusual behavior in your logs. If you add extra logging, be sure to make sensible use of log levels to avoid filling your production logs with useless trivia. +### Verbose Query Logs + +When looking at database query output in logs, it may not be immediately clear why multiple database queries are triggered when a single method is called: + +``` +irb(main):001:0> Article.pamplemousse + Article Load (0.4ms) SELECT "articles".* FROM "articles" + Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 1]] + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 2]] + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 3]] +=> #<Comment id: 2, author: "1", body: "Well, actually...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10"> +``` + +After running `ActiveRecord::Base.verbose_query_logs = true` in the `rails console` session to enable verbose query logs and running the method again, it becomes obvious what single line of code is generating all these discrete database calls: + +``` +irb(main):003:0> Article.pamplemousse + Article Load (0.2ms) SELECT "articles".* FROM "articles" + ↳ app/models/article.rb:5 + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 1]] + ↳ app/models/article.rb:6 + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 2]] + ↳ app/models/article.rb:6 + Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."article_id" = ? [["article_id", 3]] + ↳ app/models/article.rb:6 +=> #<Comment id: 2, author: "1", body: "Well, actually...", article_id: 1, created_at: "2018-10-19 00:56:10", updated_at: "2018-10-19 00:56:10"> +``` + +Below each database statement you can see arrows pointing to the specific source filename (and line number) of the method that resulted in a database call. This can help you identity and address performance problems caused by N+1 queries: single database queries that generates multiple additional queries. + +Verbose query logs are enabled by default in the development environment logs after Rails 5.2. + +WARNING: We recommend against using this setting in production environments. It relies on Ruby's `Kernel#caller` method which tends to allocate a lot of memory in order to generate stacktraces of method calls. + ### Tagged Logging When running multi-user, multi-account applications, it's often useful diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 8f2312458d..551179c523 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -173,7 +173,7 @@ description: This guide describes the considerations needed and tools available when working directly with concurrency in a Rails application. work_in_progress: true - - name: Contributing to Ruby on Rails + name: Contributions documents: - name: Contributing to Ruby on Rails @@ -184,14 +184,14 @@ url: api_documentation_guidelines.html description: This guide documents the Ruby on Rails API documentation guidelines. - - name: Ruby on Rails Guides Guidelines + name: Guides Guidelines url: ruby_on_rails_guides_guidelines.html description: This guide documents the Ruby on Rails guides guidelines. - - name: Maintenance Policy + name: Policies documents: - - name: Maintenance Policy for Ruby on Rails + name: Maintenance Policy url: maintenance_policy.html description: What versions of Ruby on Rails are currently supported, and when to expect new versions. - @@ -202,51 +202,51 @@ url: upgrading_ruby_on_rails.html description: This guide helps in upgrading applications to latest Ruby on Rails versions. - - name: Ruby on Rails 6.0 Release Notes + name: 6.0 Release Notes work_in_progress: true url: 6_0_release_notes.html description: Release notes for Rails 6.0. - - name: Ruby on Rails 5.2 Release Notes + name: 5.2 Release Notes url: 5_2_release_notes.html description: Release notes for Rails 5.2. - - name: Ruby on Rails 5.1 Release Notes + name: 5.1 Release Notes url: 5_1_release_notes.html description: Release notes for Rails 5.1. - - name: Ruby on Rails 5.0 Release Notes + name: 5.0 Release Notes url: 5_0_release_notes.html description: Release notes for Rails 5.0. - - name: Ruby on Rails 4.2 Release Notes + name: 4.2 Release Notes url: 4_2_release_notes.html description: Release notes for Rails 4.2. - - name: Ruby on Rails 4.1 Release Notes + name: 4.1 Release Notes url: 4_1_release_notes.html description: Release notes for Rails 4.1. - - name: Ruby on Rails 4.0 Release Notes + name: 4.0 Release Notes url: 4_0_release_notes.html description: Release notes for Rails 4.0. - - name: Ruby on Rails 3.2 Release Notes + name: 3.2 Release Notes url: 3_2_release_notes.html description: Release notes for Rails 3.2. - - name: Ruby on Rails 3.1 Release Notes + name: 3.1 Release Notes url: 3_1_release_notes.html description: Release notes for Rails 3.1. - - name: Ruby on Rails 3.0 Release Notes + name: 3.0 Release Notes url: 3_0_release_notes.html description: Release notes for Rails 3.0. - - name: Ruby on Rails 2.3 Release Notes + name: 2.3 Release Notes url: 2_3_release_notes.html description: Release notes for Rails 2.3. - - name: Ruby on Rails 2.2 Release Notes + name: 2.2 Release Notes url: 2_2_release_notes.html description: Release notes for Rails 2.2. diff --git a/guides/source/generators.md b/guides/source/generators.md index f028d14998..88ce4be8da 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -219,7 +219,7 @@ If we want to avoid generating the default `app/assets/stylesheets/scaffolds.scs end ``` -The next customization on the workflow will be to stop generating stylesheet, JavaScript, and test fixture files for scaffolds altogether. We can achieve that by changing our configuration to the following: +The next customization on the workflow will be to stop generating stylesheet and test fixture files for scaffolds altogether. We can achieve that by changing our configuration to the following: ```ruby config.generators do |g| @@ -227,7 +227,6 @@ config.generators do |g| g.template_engine :erb g.test_framework :test_unit, fixture: false g.stylesheets false - g.javascripts false end ``` @@ -285,7 +284,6 @@ config.generators do |g| g.template_engine :erb g.test_framework :test_unit, fixture: false g.stylesheets false - g.javascripts false g.helper :my_helper end ``` @@ -350,7 +348,6 @@ config.generators do |g| g.template_engine :erb g.test_framework :test_unit, fixture: false g.stylesheets false - g.javascripts false end ``` @@ -385,7 +382,6 @@ config.generators do |g| g.template_engine :erb g.test_framework :shoulda, fixture: false g.stylesheets false - g.javascripts false # Add a fallback! g.fallbacks[:shoulda] = :test_unit diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 9eb77d2a50..e2f558d74c 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -126,7 +126,7 @@ run the following: $ rails --version ``` -If it says something like "Rails 5.1.1", you are ready to continue. +If it says something like "Rails 5.2.1", you are ready to continue. ### Creating the Blog Application diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 78e5f27448..7465726dca 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -116,7 +116,7 @@ NOTE: The backend lazy-loads these translations when a translation is looked up You can change the default locale as well as configure the translations load paths in `config/application.rb` as follows: ```ruby - config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}')] config.i18n.default_locale = :de ``` @@ -135,6 +135,8 @@ I18n.available_locales = [:en, :pt] I18n.default_locale = :pt ``` +Note that appending directly to `I18n.load_paths` instead of to the application's configured i18n will _not_ override translations from external gems. + ### Managing the Locale across Requests The default locale is used for all translations unless `I18n.locale` is explicitly set. @@ -1103,7 +1105,7 @@ For several reasons the Simple backend shipped with Active Support only does the That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs, by passing a backend instance to the `I18n.backend=` setter. -For example, you can replace the Simple backend with the the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends. +For example, you can replace the Simple backend with the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends. With the Chain backend, you could use the Active Record backend and fall back to the (default) Simple backend: diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index dd9175e312..1f42d72756 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -45,16 +45,16 @@ <a href="index.html" id="guidesMenu" class="guides-index-item nav-item">Guides Index</a> <div id="guides" class="clearfix" style="display: none;"> <hr /> - <% ['L', 'R'].each do |position| %> - <dl class="<%= position %>"> - <% docs_for_menu(position).each do |section| %> - <dt><%= section['name'] %></dt> - <% finished_documents(section['documents']).each do |document| %> - <dd><a href="<%= document['url'] %>"><%= document['name'] %></a></dd> - <% end %> + <div class="guides-section-container"> + <% documents_by_section.each do |section| %> + <div class="guides-section"> + <dt><%= section['name'] %></dt> + <% finished_documents(section['documents']).each do |document| %> + <dd><a href="<%= document['url'] %>"><%= document['name'] %></a></dd> + <% end %> + </div> <% end %> - </dl> - <% end %> + </div> </div> </li> <li><a class="nav-item" href="contributing_to_ruby_on_rails.html">Contribute</a></li> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index 00da65b784..ad08e5a5a9 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -1266,7 +1266,7 @@ You can also pass in arbitrary local variables to any partial you are rendering In this case, the partial will have access to a local variable `title` with the value "Products Page". -TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view. +TIP: Rails also makes a counter variable available within a partial called by the collection, named after the title of the partial followed by `_counter`. For example, when rendering a collection `@products` the partial `_product.html.erb` can access the variable `product_counter` which indexes the number of times it has been rendered within the enclosing view. Note that it also applies for when the partial name was changed by using the `as:` option. For example, the counter variable for the code above would be `item_counter`. You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option: diff --git a/guides/source/testing.md b/guides/source/testing.md index 9de2229672..8c21ccfba6 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -1562,7 +1562,7 @@ class UserMailerTest < ActionMailer::TestCase end ``` -In the test we send the email and store the returned object in the `email` +In the test we create the email and store the returned object in the `email` variable. We then ensure that it was sent (the first assert), then, in the second batch of assertions, we ensure that the email does indeed contain what we expect. The helper `read_fixture` is used to read in the content from this file. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 925dac7b3c..f94b67a0ac 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,14 @@ +* Use Ids instead of memory addresses when displaying references in scaffold views. + + Fixes #29200. + + *Rasesh Patel* + +* Adds support for multiple databases to `rails db:migrate:status`. + Subtasks are also added to get the status of individual databases (eg. `rails db:migrate:status:animals`). + + *Gannon McGibbon* + * Use Webpacker by default to manage app-level JavaScript through the new app/javascript directory. Sprockets is now solely in charge, by default, of compiling CSS and other static assets. Action Cable channel generators will create ES6 stubs rather than use CoffeeScript. @@ -6,34 +17,18 @@ *DHH*, *Lachlan Sylvester* -* Refactors `migrations_paths` command option in generators - to `database` (aliased as `db`). Now, the migrations paths - will be read from the specified database configuration in the - current environment. - - ``` - bin/rails g model Chair brand:string --database=kingston - invoke active_record - create db/kingston_migrate/20180830151055_create_chairs.rb - ``` - - `--database` can be used with the migration, model, and scaffold generators. - - *Gannon McGibbon* - -* Adds an option to the model generator to allow setting the - migrations paths for that migration. This is useful for - applications that use multiple databases and put migrations - per database in their own directories. +* Add `database` (aliased as `db`) option to model generator to allow + setting the database. This is useful for applications that use + multiple databases and put migrations per database in their own directories. ``` - bin/rails g model Room capacity:integer --migrations-paths=db/kingston_migrate + bin/rails g model Room capacity:integer --database=kingston invoke active_record create db/kingston_migrate/20180830151055_create_rooms.rb ``` Because rails scaffolding uses the model generator, you can - also specify migrations paths with the scaffold generator. + also specify a database with the scaffold generator. *Gannon McGibbon* @@ -61,15 +56,15 @@ *Yoshiyuki Kinjo* -* Add `--migrations_paths` option to migration generator. +* Add `database` (aliased as `db`) option to migration generator. If you're using multiple databases and have a folder for each database for migrations (ex db/migrate and db/new_db_migrate) you can now pass the - `--migrations_paths` option to the generator to make sure the the migration + `--database` option to the generator to make sure the the migration is inserted into the correct folder. ``` - rails g migration CreateHouses --migrations_paths=db/kingston_migrate + rails g migration CreateHouses --database=kingston invoke active_record create db/kingston_migrate/20180830151055_create_houses.rb ``` diff --git a/railties/lib/rails/app_updater.rb b/railties/lib/rails/app_updater.rb index a243968a39..19d136e041 100644 --- a/railties/lib/rails/app_updater.rb +++ b/railties/lib/rails/app_updater.rb @@ -21,7 +21,7 @@ module Rails private def generator_options options = { api: !!Rails.application.config.api_only, update: true } - options[:skip_yarn] = !File.exist?(Rails.root.join("bin", "yarn")) + options[:skip_javascript] = !File.exist?(Rails.root.join("bin", "yarn")) options[:skip_active_record] = !defined?(ActiveRecord::Railtie) options[:skip_active_storage] = !defined?(ActiveStorage::Engine) || !defined?(ActiveRecord::Railtie) options[:skip_action_mailer] = !defined?(ActionMailer::Railtie) diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index eae902a938..d6f8c4f47c 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -202,7 +202,7 @@ module Rails "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ "Error: #{e.message}" rescue => e - raise e, "Cannot load `Rails.application.database_configuration`:\n#{e.message}", e.backtrace + raise e, "Cannot load database configuration:\n#{e.message}", e.backtrace end def colorize_logging diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index cd8f16e247..5e8cebc50a 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -54,8 +54,6 @@ module Rails force_plural: false, helper: true, integration_tool: nil, - javascripts: true, - javascript_engine: :js, orm: false, resource_controller: :controller, resource_route: true, diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 410e53b4c0..4dc4d27a46 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -31,9 +31,6 @@ module Rails class_option :database, type: :string, aliases: "-d", default: "sqlite3", desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})" - class_option :skip_yarn, type: :boolean, default: false, - desc: "Don't use Yarn for managing JavaScript dependencies" - class_option :skip_gemfile, type: :boolean, default: false, desc: "Don't create a Gemfile" @@ -399,6 +396,10 @@ module Rails !options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin") end + def webpack_install? + !(options[:skip_javascript] || options[:skip_webpack_install]) + end + def depends_on_system_test? !(options[:skip_system_test] || options[:skip_test] || options[:api]) end @@ -420,7 +421,7 @@ module Rails end def run_webpack - unless options[:skip_javascript] + if webpack_install? rails_command "webpacker:install" rails_command "webpacker:install:#{options[:webpack]}" if options[:webpack] && options[:webpack] != "webpack" end diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt index e1ede7c713..2cf4e5c9d0 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb.tt @@ -16,7 +16,7 @@ <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %> <tr> <% attributes.reject(&:password_digest?).each do |attribute| -%> - <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td> + <td><%%= <%= singular_table_name %>.<%= attribute.column_name %> %></td> <% end -%> <td><%%= link_to 'Show', <%= model_resource_name %> %></td> <td><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %></td> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt index 5e634153be..7deba07926 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt +++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt @@ -3,7 +3,7 @@ <% attributes.reject(&:password_digest?).each do |attribute| -%> <p> <strong><%= attribute.human_name %>:</strong> - <%%= @<%= singular_table_name %>.<%= attribute.name %> %> + <%%= @<%= singular_table_name %>.<%= attribute.column_name %> %> </p> <% end -%> diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index cefaffcb20..f56f79b8d4 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -95,7 +95,7 @@ module Rails def bin_when_updating bin - if options[:skip_yarn] + if options[:skip_javascript] remove_file "bin/yarn" end end @@ -261,6 +261,9 @@ module Rails class_option :webpack, type: :string, default: nil, desc: "Preconfigure Webpack with a particular framework (options: #{WEBPACKS.join('/')})" + class_option :skip_webpack_install, type: :boolean, default: false, + desc: "Don't run Webpack install" + def initialize(*args) super @@ -271,7 +274,7 @@ module Rails # Force sprockets and yarn to be skipped when generating API only apps. # Can't modify options hash as it's frozen by default. if options[:api] - self.options = options.merge(skip_sprockets: true, skip_javascript: true, skip_yarn: true).freeze + self.options = options.merge(skip_sprockets: true, skip_javascript: true).freeze end end @@ -286,7 +289,7 @@ module Rails build(:gitignore) unless options[:skip_git] build(:gemfile) unless options[:skip_gemfile] build(:version_control) - build(:package_json) unless options[:skip_yarn] + build(:package_json) unless options[:skip_javascript] end def create_app_files @@ -435,7 +438,6 @@ module Rails def delete_action_cable_files_skipping_action_cable if options[:skip_action_cable] - remove_file "app/javascript/channels/consumer.js" remove_dir "app/javascript/channels" remove_dir "app/channels" end @@ -460,8 +462,8 @@ module Rails end end - def delete_bin_yarn_if_skip_yarn_option - remove_file "bin/yarn" if options[:skip_yarn] + def delete_bin_yarn + remove_file "bin/yarn" if options[:skip_javascript] end def finish_template diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index 1567333023..fb264935bd 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -45,6 +45,7 @@ group :development, :test do gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end +<% end -%> group :development do <%- unless options.api? -%> # Access an interactive console on exception pages or by calling 'console' anywhere in the code. @@ -75,7 +76,6 @@ group :test do gem 'chromedriver-helper' end <%- end -%> -<% end -%> # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js index 5da1ce2dce..0cfcf74919 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/index.js @@ -1,5 +1,5 @@ -// Load all the channels within this directory and all subdirectories. +// Load all the channels within this directory and all subdirectories. // Channel files must be named *_channel.js. -const channels = require.context('.', true, /\_channel\.js$/) +const channels = require.context('.', true, /_channel\.js$/) channels.keys().forEach(channels) diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt index a009ace51c..d394c3d106 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt @@ -1,2 +1,7 @@ class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt index 233b5a1d95..a18e03e7db 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup.tt @@ -1,5 +1,4 @@ require 'fileutils' -include FileUtils # path to your application root. APP_ROOT = File.expand_path('..', __dir__) @@ -8,16 +7,16 @@ def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end -chdir APP_ROOT do +FileUtils.chdir APP_ROOT do # This script is a starting point to setup your application. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') -<% unless options.skip_yarn? -%> +<% unless options.skip_javascript? -%> - # Install JavaScript dependencies if using Yarn + # Install JavaScript dependencies # system('bin/yarn') <% end -%> <% unless options.skip_active_record? -%> diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update.tt b/railties/lib/rails/generators/rails/app/templates/bin/update.tt index 99c2430bc6..03b77d0d46 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/update.tt +++ b/railties/lib/rails/generators/rails/app/templates/bin/update.tt @@ -1,5 +1,4 @@ require 'fileutils' -include FileUtils # path to your application root. APP_ROOT = File.expand_path('..', __dir__) @@ -8,16 +7,16 @@ def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end -chdir APP_ROOT do +FileUtils.chdir APP_ROOT do # This script is a way to update your development environment automatically. # Add necessary update steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') -<% unless options.skip_yarn? -%> +<% unless options.skip_javascript? -%> - # Install JavaScript dependencies if using Yarn + # Install JavaScript dependencies # system('bin/yarn') <% end -%> <% unless options.skip_active_record? -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt index 51196ae743..fe48fc34ee 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt @@ -5,10 +5,6 @@ Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path -<%- unless options[:skip_yarn] -%> -# Add Yarn node_modules folder to the asset load path. -Rails.application.config.assets.paths << Rails.root.join('node_modules') -<%- end -%> # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore.tt b/railties/lib/rails/generators/rails/app/templates/gitignore.tt index 4e114fb1d9..860baa1595 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore.tt +++ b/railties/lib/rails/generators/rails/app/templates/gitignore.tt @@ -22,19 +22,14 @@ <% end -%> <% unless skip_active_storage? -%> -# Ignore uploaded files in development +# Ignore uploaded files in development. /storage/* <% if keeps? -%> !/storage/.keep <% end -%> <% end -%> - -<% unless options.skip_yarn? -%> -/node_modules -/yarn-error.log - -<% end -%> <% unless options.api? -%> + /public/assets <% end -%> .byebug_history diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 9ec0ccbe7a..239b3a5739 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -88,7 +88,7 @@ task default: :test PASSTHROUGH_OPTIONS = [ :skip_active_record, :skip_active_storage, :skip_action_mailer, :skip_javascript, :skip_action_cable, :skip_sprockets, :database, - :javascript, :skip_yarn, :api, :quiet, :pretend, :skip + :api, :quiet, :pretend, :skip ] def generate_test_dummy(force = false) @@ -98,6 +98,7 @@ task default: :test opts[:skip_listen] = true opts[:skip_git] = true opts[:skip_turbolinks] = true + opts[:skip_webpack_install] = true opts[:dummy_app] = true invoke Rails::Generators::AppGenerator, diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt index 7a68da5c4b..0aabf09252 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore.tt @@ -7,7 +7,7 @@ pkg/ <%= dummy_path %>/db/*.sqlite3-journal <% end -%> <%= dummy_path %>/log/*.log -<% unless options[:skip_yarn] -%> +<% unless options[:skip_javascript] -%> <%= dummy_path %>/node_modules/ <%= dummy_path %>/yarn-error.log <% end -%> diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index 238ffdd677..ec29ad12ba 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -68,7 +68,7 @@ module Rails capture(:stdout) do args += ["--skip-bundle"] unless args.include? "--dev" args |= ["--skip-bootsnap"] unless args.include? "--no-skip-bootsnap" - args |= ["--skip-javascript"] unless args.include? "--no-skip-javascript" + args |= ["--skip-webpack-install"] unless args.include? "--no-skip-webpack-install" generator_class.start(args, config.reverse_merge(destination_root: destination_root)) end diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb index 54934dbe24..d02100d94c 100644 --- a/railties/test/application/bin_setup_test.rb +++ b/railties/test/application/bin_setup_test.rb @@ -43,18 +43,20 @@ module ApplicationTests # Ignore line that's only output by Bundler < 1.14 output.sub!(/^Resolving dependencies\.\.\.\n/, "") + # Suppress Bundler platform warnings from output + output.gsub!(/^The dependency .* will be unused .*\.\n/, "") - assert_equal(<<-OUTPUT, output) -== Installing dependencies == -The Gemfile's dependencies are satisfied + assert_equal(<<~OUTPUT, output) + == Installing dependencies == + The Gemfile's dependencies are satisfied -== Preparing database == -Created database 'db/development.sqlite3' -Created database 'db/test.sqlite3' + == Preparing database == + Created database 'db/development.sqlite3' + Created database 'db/test.sqlite3' -== Removing old logs and tempfiles == + == Removing old logs and tempfiles == -== Restarting application server == + == Restarting application server == OUTPUT end end diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb index bc6708c89e..6478e06250 100644 --- a/railties/test/application/rake/multi_dbs_test.rb +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -55,11 +55,19 @@ module ApplicationTests end end - def db_migrate_and_schema_dump_and_load(namespace, expected_database, format) + def db_migrate_and_migrate_status Dir.chdir(app_path) do - rails "generate", "model", "book", "title:string" - rails "generate", "model", "dog", "name:string" - write_models_for_animals + generate_models_for_animals + rails "db:migrate" + output = rails "db:migrate:status" + assert_match(/up \d+ Create books/, output) + assert_match(/up \d+ Create dogs/, output) + end + end + + def db_migrate_and_schema_dump_and_load(format) + Dir.chdir(app_path) do + generate_models_for_animals rails "db:migrate", "db:#{format}:dump" if format == "schema" @@ -86,9 +94,7 @@ module ApplicationTests def db_migrate_namespaced(namespace, expected_database) Dir.chdir(app_path) do - rails "generate", "model", "book", "title:string" - rails "generate", "model", "dog", "name:string" - write_models_for_animals + generate_models_for_animals output = rails("db:migrate:#{namespace}") if namespace == "primary" assert_match(/CreateBooks: migrated/, output) @@ -98,6 +104,18 @@ module ApplicationTests end end + def db_migrate_status_namespaced(namespace, expected_database) + Dir.chdir(app_path) do + generate_models_for_animals + output = rails("db:migrate:status:#{namespace}") + if namespace == "primary" + assert_match(/up \d+ Create books/, output) + else + assert_match(/up \d+ Create dogs/, output) + end + end + end + def write_models_for_animals # make a directory for the animals migration FileUtils.mkdir_p("#{app_path}/db/animals_migrate") @@ -117,17 +135,22 @@ module ApplicationTests # create the base model for dog to inherit from File.open("#{app_path}/app/models/animals_base.rb", "w") do |file| - file.write(<<-EOS -class AnimalsBase < ActiveRecord::Base - self.abstract_class = true + file.write(<<~EOS) + class AnimalsBase < ActiveRecord::Base + self.abstract_class = true - establish_connection :animals -end -EOS -) + establish_connection :animals + end + EOS end end + def generate_models_for_animals + rails "generate", "model", "book", "title:string" + rails "generate", "model", "dog", "name:string" + write_models_for_animals + end + test "db:create and db:drop works on all databases for env" do require "#{app_path}/config/environment" ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| @@ -144,22 +167,31 @@ EOS test "db:migrate and db:schema:dump and db:schema:load works on all databases" do require "#{app_path}/config/environment" - ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| - db_migrate_and_schema_dump_and_load db_config.spec_name, db_config.config["database"], "schema" - end + db_migrate_and_schema_dump_and_load "schema" end test "db:migrate and db:structure:dump and db:structure:load works on all databases" do require "#{app_path}/config/environment" + db_migrate_and_schema_dump_and_load "structure" + end + + test "db:migrate:namespace works" do + require "#{app_path}/config/environment" ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| - db_migrate_and_schema_dump_and_load db_config.spec_name, db_config.config["database"], "structure" + db_migrate_namespaced db_config.spec_name, db_config.config["database"] end end - test "db:migrate:namespace works" do + test "db:migrate:status works on all databases" do + require "#{app_path}/config/environment" + db_migrate_and_migrate_status + end + + test "db:migrate:status:namespace works" do require "#{app_path}/config/environment" ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| db_migrate_namespaced db_config.spec_name, db_config.config["database"] + db_migrate_status_namespaced db_config.spec_name, db_config.config["database"] end end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 48e8b7123f..bb3aaa9d14 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -15,6 +15,8 @@ DEFAULT_APP_FILES = %w( app/assets/images app/javascript app/javascript/channels + app/javascript/channels/consumer.js + app/javascript/channels/index.js app/javascript/packs/application.js app/assets/stylesheets app/assets/stylesheets/application.css @@ -104,7 +106,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_skip_bundle - assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do + assert_not_called(generator([destination_root], skip_bundle: true, skip_webpack_install: true), :bundle_command) do quietly { generator.invoke_all } # skip_bundle is only about running bundle install, ensure the Gemfile is still # generated. @@ -113,7 +115,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_assets - run_generator [destination_root, "--no-skip-javascript"] + run_generator assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track': 'reload'/) assert_file("app/views/layouts/application.html.erb", /javascript_pack_tag\s+'application', 'data-turbolinks-track': 'reload'/) @@ -209,19 +211,20 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_new_application_doesnt_need_defaults + run_generator assert_no_file "config/initializers/new_framework_defaults_6_0.rb" end def test_new_application_load_defaults app_root = File.join(destination_root, "myfirstapp") - run_generator [app_root, "--no-skip-javascript"] + run_generator [app_root] output = nil assert_file "#{app_root}/config/application.rb", /\s+config\.load_defaults #{Rails::VERSION::STRING.to_f}/ Dir.chdir(app_root) do - output = `./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"` + output = `SKIP_REQUIRE_WEBPACKER=true ./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"` end assert_equal "false\n", output @@ -297,10 +300,10 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_app_update_does_not_generate_yarn_contents_when_bin_yarn_is_not_used app_root = File.join(destination_root, "myapp") - run_generator [app_root, "--skip-yarn"] + run_generator [app_root, "--skip-javascript"] stub_rails_application(app_root) do - generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_yarn: true }, { destination_root: app_root, shell: @shell } + generator = Rails::Generators::AppGenerator.new ["rails"], { update: true, skip_javascript: true }, { destination_root: app_root, shell: @shell } generator.send(:app_const) quietly { generator.send(:update_bin_files) } @@ -603,11 +606,11 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_javascript_is_skipped_if_required run_generator [destination_root, "--skip-javascript"] - assert_no_file "app/assets/javascripts" + assert_no_file "app/javascript" assert_file "app/views/layouts/application.html.erb" do |contents| assert_match(/stylesheet_link_tag\s+'application', media: 'all' %>/, contents) - assert_no_match(/javascript_include_tag\s+'application' \%>/, contents) + assert_no_match(/javascript_pack_tag\s+'application'/, contents) end end @@ -709,7 +712,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_web_console_with_dev_option - run_generator [destination_root, "--dev"] + run_generator [destination_root, "--dev", "--skip-bundle"] assert_file "Gemfile" do |content| assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content) @@ -727,13 +730,13 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_generation_runs_bundle_install - generator([destination_root], {}) + generator([destination_root], skip_webpack_install: true) assert_bundler_command_called("install") end def test_dev_option - generator([destination_root], dev: true) + generator([destination_root], dev: true, skip_webpack_install: true) assert_bundler_command_called("install") rails_path = File.expand_path("../../..", Rails.root) @@ -741,7 +744,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_edge_option - generator([destination_root], edge: true) + generator([destination_root], edge: true, skip_webpack_install: true) assert_bundler_command_called("install") assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$} @@ -753,12 +756,16 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_bundler_binstub + generator([destination_root], skip_webpack_install: true) + assert_bundler_command_called("binstubs bundler") end def test_spring_binstubs jruby_skip "spring doesn't run on JRuby" + generator([destination_root], skip_webpack_install: true) + assert_bundler_command_called("exec spring binstub --all") end @@ -779,7 +786,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_spring_with_dev_option - run_generator [destination_root, "--dev"] + run_generator [destination_root, "--dev", "--skip-bundle"] assert_no_gem "spring" end @@ -789,7 +796,7 @@ class AppGeneratorTest < Rails::Generators::TestCase @called ||= 0 if command == "webpacker:install" @called += 1 - assert_equal 0, @called, "webpacker:install expected not to be called once, but was called #{@called} times." + assert_equal 0, @called, "webpacker:install expected not to be called, but was called #{@called} times." end end @@ -825,8 +832,22 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_gem "webpacker" end + def test_skip_webpack_install + command_check = -> command do + if command == "webpacker:install" + assert false, "webpacker:install expected not to be called." + end + end + + generator([destination_root], skip_webpack_install: 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", "--no-skip-javascript"] + run_generator [destination_root, "--skip-turbolinks"] assert_no_gem "turbolinks" assert_file "app/views/layouts/application.html.erb" do |content| @@ -863,7 +884,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_bootsnap_with_dev_option - run_generator [destination_root, "--dev"] + run_generator [destination_root, "--dev", "--skip-bundle"] assert_no_gem "bootsnap" assert_file "config/boot.rb" do |content| diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb index 265d7c9618..1cb8465539 100644 --- a/railties/test/generators/channel_generator_test.rb +++ b/railties/test/generators/channel_generator_test.rb @@ -57,7 +57,7 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_no_file "app/javascript/channels/chat_channel.js" end - def test_cable_js_is_created_if_not_present_already + def test_consumer_js_is_created_if_not_present_already run_generator ["chat"] FileUtils.rm("#{destination_root}/app/javascript/channels/index.js") FileUtils.rm("#{destination_root}/app/javascript/channels/consumer.js") @@ -72,7 +72,7 @@ class ChannelGeneratorTest < Rails::Generators::TestCase run_generator ["chat"], behavior: :revoke assert_no_file "app/channels/chat_channel.rb" - assert_no_file "app/assets/javascripts/channels/chat.js" + assert_no_file "app/javascript/channels/chat_channel.js" assert_file "app/channels/application_cable/channel.rb" assert_file "app/channels/application_cable/connection.rb" @@ -86,7 +86,7 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_no_file "app/channels/chat_channel_channel.rb" assert_file "app/channels/chat_channel.rb" - assert_no_file "app/assets/javascripts/channels/chat_channel.js" + assert_no_file "app/javascript/channels/chat_channel_channel.js" assert_file "app/javascript/channels/chat_channel.js" end end diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index adef56255a..8786756c68 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -130,8 +130,6 @@ class ControllerGeneratorTest < Rails::Generators::TestCase assert_no_file "app/helpers/account_controller_helper.rb" assert_file "app/helpers/account_helper.rb" - assert_no_file "app/assets/javascripts/account_controller.js" - assert_no_file "app/assets/stylesheets/account_controller.css" assert_file "app/assets/stylesheets/account.css" end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 21b5013484..3cffdddd38 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -435,8 +435,8 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end - def test_scaffold_generator_belongs_to - run_generator ["account", "name", "currency:belongs_to"] + def test_scaffold_generator_belongs_to_and_references + run_generator ["account", "name", "currency:belongs_to", "user:references"] assert_file "app/models/account.rb", /belongs_to :currency/ @@ -449,7 +449,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_file "app/controllers/accounts_controller.rb" do |content| assert_instance_method :account_params, content do |m| - assert_match(/permit\(:name, :currency_id\)/, m) + assert_match(/permit\(:name, :currency_id, :user_id\)/, m) end end @@ -457,6 +457,16 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/^\W{4}<%= form\.text_field :name %>/, content) assert_match(/^\W{4}<%= form\.text_field :currency_id %>/, content) end + + assert_file "app/views/accounts/index.html.erb" do |content| + assert_match(/^\W{8}<td><%= account\.name %><\/td>/, content) + assert_match(/^\W{8}<td><%= account\.user_id %><\/td>/, content) + end + + assert_file "app/views/accounts/show.html.erb" do |content| + assert_match(/^\W{2}<%= @account\.name %>/, content) + assert_match(/^\W{2}<%= @account\.user_id %>/, content) + end end def test_scaffold_generator_database diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 521f775553..2dda856f25 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -27,7 +27,7 @@ module SharedGeneratorTests end def test_skeleton_is_created - run_generator [destination_root, "--no-skip-javascript"] + run_generator default_files.each { |path| assert_file path } end @@ -91,7 +91,7 @@ module SharedGeneratorTests template end - generator([destination_root], template: path).stub(:open, check_open, template) do + generator([destination_root], template: path, skip_webpack_install: true).stub(:open, check_open, template) do generator.stub :bundle_command, nil do quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) } end @@ -99,7 +99,7 @@ module SharedGeneratorTests end def test_skip_gemfile - assert_not_called(generator([destination_root], skip_gemfile: true), :bundle_command) do + assert_not_called(generator([destination_root], skip_gemfile: true, skip_webpack_install: true), :bundle_command) do quietly { generator.invoke_all } assert_no_file "Gemfile" end @@ -196,7 +196,7 @@ module SharedGeneratorTests end def test_generator_for_active_storage - run_generator [destination_root, "--no-skip-javascript"] + run_generator unless generator_class.name == "Rails::Generators::PluginGenerator" assert_file "#{application_path}/app/javascript/packs/application.js" do |content| @@ -226,12 +226,12 @@ module SharedGeneratorTests end def test_generator_if_skip_active_storage_is_given - run_generator [destination_root, "--skip-active-storage", "--no-skip-javascript"] + run_generator [destination_root, "--skip-active-storage"] assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/ assert_file "#{application_path}/app/javascript/packs/application.js" do |content| - assert_no_match(/^\/\/= require activestorage/, content) + assert_no_match(/activestorage/, content) end assert_file "#{application_path}/config/environments/development.rb" do |content| @@ -256,12 +256,12 @@ module SharedGeneratorTests end def test_generator_does_not_generate_active_storage_contents_if_skip_active_record_is_given - run_generator [destination_root, "--skip-active-record", "--no-skip-javascript"] + run_generator [destination_root, "--skip-active-record"] assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/ assert_file "#{application_path}/app/javascript/packs/application.js" do |content| - assert_no_match(/^import * as ActiveStorage from "activestorage"\nActiveStorage.start()/, content) + assert_no_match(/^import * as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content) end assert_file "#{application_path}/config/environments/development.rb" do |content| @@ -305,8 +305,8 @@ module SharedGeneratorTests run_generator [destination_root, "--skip-action-cable"] assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ assert_no_file "#{application_path}/config/cable.yml" - assert_no_file "#{application_path}/app/assets/javascripts/cable.js" - assert_no_directory "#{application_path}/app/assets/javascripts/channels" + assert_no_file "#{application_path}/app/javascript/consumer.js" + assert_no_directory "#{application_path}/app/javascript/channels" assert_no_directory "#{application_path}/app/channels" assert_file "Gemfile" do |content| assert_no_match(/redis/, content) @@ -336,28 +336,15 @@ module SharedGeneratorTests end def test_generator_for_yarn + skip "#34009 disabled JS by default for plugins" if generator_class.name == "Rails::Generators::PluginGenerator" run_generator assert_file "#{application_path}/package.json", /dependencies/ - assert_file "#{application_path}/config/initializers/assets.rb", /node_modules/ - - assert_file ".gitignore" do |content| - assert_match(/node_modules/, content) - assert_match(/yarn-error\.log/, content) - end + assert_file "#{application_path}/bin/yarn" end def test_generator_for_yarn_skipped - run_generator([destination_root, "--skip-yarn"]) + run_generator([destination_root, "--skip-javascript"]) assert_no_file "#{application_path}/package.json" assert_no_file "#{application_path}/bin/yarn" - - assert_file "#{application_path}/config/initializers/assets.rb" do |content| - assert_no_match(/node_modules/, content) - end - - assert_file ".gitignore" do |content| - assert_no_match(/node_modules/, content) - assert_no_match(/yarn-error\.log/, content) - end end end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index e44f21380e..d4eed69a87 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -16,6 +16,9 @@ require "active_support/testing/autorun" require "active_support/testing/stream" require "active_support/testing/method_call_assertions" require "active_support/test_case" +require "minitest/retry" + +Minitest::Retry.use!(verbose: false, retry_count: 1) RAILS_FRAMEWORK_ROOT = File.expand_path("../../..", __dir__) |