diff options
23 files changed, 252 insertions, 68 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 954ab3b1cb..7327f1e631 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -162,6 +162,9 @@ Style/UnneededPercentQ: Lint/RequireParentheses: Enabled: true +Lint/StringConversionInInterpolation: + Enabled: true + Style/RedundantReturn: Enabled: true AllowMultipleReturnValues: true diff --git a/activejob/test/jobs/retry_job.rb b/activejob/test/jobs/retry_job.rb index 56c9a78a2e..a12c65015b 100644 --- a/activejob/test/jobs/retry_job.rb +++ b/activejob/test/jobs/retry_job.rb @@ -18,9 +18,9 @@ class RetryJob < ActiveJob::Base retry_on ShortWaitTenAttemptsError, wait: 1.second, attempts: 10 retry_on ExponentialWaitTenAttemptsError, wait: :exponentially_longer, attempts: 10 retry_on CustomWaitTenAttemptsError, wait: ->(executions) { executions * 2 }, attempts: 10 - retry_on(CustomCatchError) { |job, exception| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts. Message: #{exception.message}") } + retry_on(CustomCatchError) { |job, error| JobBuffer.add("Dealt with a job that failed to retry in a custom way after #{job.arguments.second} attempts. Message: #{error.message}") } discard_on DiscardableError - discard_on(CustomDiscardableError) { |job, exception| JobBuffer.add("Dealt with a job that was discarded in a custom way. Message: #{exception.message}") } + discard_on(CustomDiscardableError) { |job, error| JobBuffer.add("Dealt with a job that was discarded in a custom way. Message: #{error.message}") } def perform(raising, attempts) if executions < attempts 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 54dc917e99..199674f531 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1062,13 +1062,7 @@ module ActiveRecord if (duplicate = inserting.detect { |v| inserting.count(v) > 1 }) raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." end - if supports_multi_insert? - execute insert_versions_sql(inserting) - else - inserting.each do |v| - execute insert_versions_sql(v) - end - end + execute insert_versions_sql(inserting) end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 0ce3796829..b59df2fff7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -17,11 +17,19 @@ module ActiveRecord end def committed? - @state == :committed + @state == :committed || @state == :fully_committed + end + + def fully_committed? + @state == :fully_committed end def rolledback? - @state == :rolledback + @state == :rolledback || @state == :fully_rolledback + end + + def fully_rolledback? + @state == :fully_rolledback end def fully_completed? @@ -55,10 +63,19 @@ module ActiveRecord @state = :rolledback end + def full_rollback! + @children.each { |c| c.rollback! } + @state = :fully_rolledback + end + def commit! @state = :committed end + def full_commit! + @state = :fully_committed + end + def nullify! @state = nil end @@ -88,10 +105,6 @@ module ActiveRecord records << record end - def rollback - @state.rollback! - end - def rollback_records ite = records.uniq while record = ite.shift @@ -103,10 +116,6 @@ module ActiveRecord end end - def commit - @state.commit! - end - def before_commit_records records.uniq.each(&:before_committed!) if @run_commit_callbacks end @@ -145,12 +154,12 @@ module ActiveRecord def rollback connection.rollback_to_savepoint(savepoint_name) - super + @state.rollback! end def commit connection.release_savepoint(savepoint_name) - super + @state.commit! end def full_rollback?; false; end @@ -168,12 +177,12 @@ module ActiveRecord def rollback connection.rollback_db_transaction - super + @state.full_rollback! end def commit connection.commit_db_transaction - super + @state.full_commit! end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 559f068c39..8bdf1712b1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -137,6 +137,10 @@ module ActiveRecord def <=>(version_string) @version <=> version_string.split(".").map(&:to_i) end + + def to_s + @version.join(".") + end end def valid_type?(type) # :nodoc: @@ -320,6 +324,7 @@ module ActiveRecord def supports_multi_insert? true end + deprecate :supports_multi_insert? # Does this adapter support virtual columns? def supports_virtual_columns? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index b252a76caa..ffd3be26b0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module ActiveRecord + # :stopdoc: module ConnectionAdapters class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) undef to_yaml if method_defined?(:to_yaml) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 800e731f06..32e2ed6fc6 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -104,6 +104,10 @@ 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 @@ -116,7 +120,7 @@ module ActiveRecord end def supports_partial_index? - sqlite_version >= "3.8.0" + true end def requires_reloading? @@ -124,7 +128,7 @@ module ActiveRecord end def supports_foreign_keys_in_create? - sqlite_version >= "3.6.19" + true end def supports_views? @@ -139,10 +143,6 @@ module ActiveRecord true end - def supports_multi_insert? - sqlite_version >= "3.7.11" - end - def active? @active end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 013c3765b2..cf884bc6da 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -104,7 +104,7 @@ module ActiveRecord if source_line if defined?(::Rails.root) - app_root = "#{::Rails.root.to_s}/".freeze + app_root = "#{::Rails.root}/" source_line = source_line.sub(app_root, "") end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 2f43d005f3..22d195c9a4 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -417,7 +417,7 @@ module ActiveRecord class AssociationReflection < MacroReflection #:nodoc: def compute_class(name) if polymorphic? - raise ArgumentError, "Polymorphic association does not support to compute class." + raise ArgumentError, "Polymorphic associations do not support computing the class." end active_record.send(:compute_type, name) end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 20603aaf2d..82adb19f5b 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -383,8 +383,8 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state - @_start_transaction_state[:id] = id @_start_transaction_state.reverse_merge!( + id: id, new_record: @new_record, destroyed: @destroyed, frozen?: frozen?, @@ -472,7 +472,8 @@ module ActiveRecord def update_attributes_from_transaction_state(transaction_state) if transaction_state && transaction_state.finalized? - restore_transaction_record_state if transaction_state.rolledback? + restore_transaction_record_state(transaction_state.fully_rolledback?) if transaction_state.rolledback? + force_clear_transaction_record_state if transaction_state.fully_committed? clear_transaction_record_state if transaction_state.fully_completed? end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 02c7e47583..79642f5871 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -295,6 +295,10 @@ module ActiveRecord assert_equal "ы", error.message end end + + def test_supports_multi_insert_is_deprecated + assert_deprecated { @connection.supports_multi_insert? } + end end class AdapterForeignKeyTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/arel/support/fake_record.rb b/activerecord/test/cases/arel/support/fake_record.rb index 8620d6fd34..559ff5d4e6 100644 --- a/activerecord/test/cases/arel/support/fake_record.rb +++ b/activerecord/test/cases/arel/support/fake_record.rb @@ -51,11 +51,11 @@ module FakeRecord end def quote_table_name(name) - "\"#{name.to_s}\"" + "\"#{name}\"" end def quote_column_name(name) - "\"#{name.to_s}\"" + "\"#{name}\"" end def schema_cache diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 31bdf3f357..15f4cea1a6 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -37,24 +37,6 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.delete_all end - if current_adapter?(:SQLite3Adapter) - %w{3.7.8 3.7.11 3.7.12}.each do |version_string| - test "dumps schema version for sqlite version #{version_string}" do - version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new(version_string) - ActiveRecord::Base.connection.stubs(:sqlite_version).returns(version) - - versions = %w{ 20100101010101 20100201010101 20100301010101 } - versions.reverse_each do |v| - ActiveRecord::SchemaMigration.create!(version: v) - end - - schema_info = ActiveRecord::Base.connection.dump_schema_information - assert_match(/20100201010101.*20100301010101/m, schema_info) - ActiveRecord::SchemaMigration.delete_all - end - end - end - def test_schema_dump output = standard_dump assert_match %r{create_table "accounts"}, output @@ -192,7 +174,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_partial_indices index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_partial_index/).first.strip - if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? + if ActiveRecord::Base.connection.supports_partial_index? assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)"', index_definition else assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 17aeabe34e..5b685ca564 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -679,6 +679,36 @@ class TransactionTest < ActiveRecord::TestCase assert_not_predicate topic, :frozen? end + def test_restore_new_record_after_double_save + topic = Topic.new + + Topic.transaction do + topic.save! + topic.save! + raise ActiveRecord::Rollback + end + + assert_nil topic.id + assert_predicate topic, :new_record? + end + + def test_dont_restore_new_record_in_subsequent_transaction + topic = Topic.new + + Topic.transaction do + topic.save! + topic.save! + end + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + assert_predicate topic, :persisted? + assert_not_predicate topic, :new_record? + end + def test_restore_id_after_rollback topic = Topic.new diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 7b724b7b81..c8911fe611 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,13 @@ +* Variant arguments of `false` or `nil` will no longer be passed to the + processor. For example, the following will not have the monochrome + variation applied: + + ```ruby + avatar.variant(monochrome: false) + ``` + + *Jacob Smith* + * Generated attachment getter and setter methods are created within the model's `GeneratedAssociationMethods` module to allow overriding and composition using `super`. diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index 42f00beb82..806af6366d 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -66,11 +66,13 @@ class ActiveStorage::Variation # Applies image transformations using the ImageProcessing gem. def image_processing_transform(file, format) operations = transformations.inject([]) do |list, (name, argument)| - if name.to_s == "combine_options" - ActiveSupport::Deprecation.warn("The ImageProcessing ActiveStorage variant backend doesn't need :combine_options, as it already generates a single MiniMagick command. In Rails 6.1 :combine_options will not be supported anymore.") - list.concat argument.to_a - else - list << [name, argument] + list.tap do |list| + if name.to_s == "combine_options" + ActiveSupport::Deprecation.warn("The ImageProcessing ActiveStorage variant backend doesn't need :combine_options, as it already generates a single MiniMagick command. In Rails 6.1 :combine_options will not be supported anymore.") + list.concat argument.keep_if { |key, value| value.present? }.to_a + elsif argument.present? + list << [name, argument] + end end end @@ -116,14 +118,10 @@ class ActiveStorage::Variation end def pass_transform_argument(command, method, argument) - if eligible_argument?(argument) - command.public_send(method, argument) - else + if argument == true command.public_send(method) + elsif argument.present? + command.public_send(method, argument) end end - - def eligible_argument?(argument) - argument.present? && argument != true - end end diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb index e74bbc9ab4..6577f1cd9f 100644 --- a/activestorage/test/models/variant_test.rb +++ b/activestorage/test/models/variant_test.rb @@ -25,6 +25,67 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase assert_match(/Gray/, image.colorspace) end + test "monochrome with default variant_processor" do + begin + ActiveStorage.variant_processor = nil + + blob = create_file_blob(filename: "racecar.jpg") + variant = blob.variant(monochrome: true).processed + image = read_image(variant) + assert_match(/Gray/, image.colorspace) + ensure + ActiveStorage.variant_processor = :mini_magick + end + end + + test "disabled variation of JPEG blob" do + blob = create_file_blob(filename: "racecar.jpg") + variant = blob.variant(resize: "100x100", monochrome: false).processed + assert_match(/racecar\.jpg/, variant.service_url) + + image = read_image(variant) + assert_equal 100, image.width + assert_equal 67, image.height + assert_match(/RGB/, image.colorspace) + end + + test "disabled variation of JPEG blob with :combine_options" do + blob = create_file_blob(filename: "racecar.jpg") + variant = ActiveSupport::Deprecation.silence do + blob.variant(combine_options: { + resize: "100x100", + monochrome: false + }).processed + end + assert_match(/racecar\.jpg/, variant.service_url) + + image = read_image(variant) + assert_equal 100, image.width + assert_equal 67, image.height + assert_match(/RGB/, image.colorspace) + end + + test "disabled variation using :combine_options" do + begin + ActiveStorage.variant_processor = nil + blob = create_file_blob(filename: "racecar.jpg") + variant = ActiveSupport::Deprecation.silence do + blob.variant(combine_options: { + crop: "100x100+0+0", + monochrome: false + }).processed + end + assert_match(/racecar\.jpg/, variant.service_url) + + image = read_image(variant) + assert_equal 100, image.width + assert_equal 100, image.height + assert_match(/RGB/, image.colorspace) + ensure + ActiveStorage.variant_processor = :mini_magick + end + end + test "center-weighted crop of JPEG blob using :combine_options" do begin ActiveStorage.variant_processor = nil diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 62c0f612a8..a384fc3bd2 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,18 @@ +* Add `index_with` to Enumerable. + + Allows creating a hash from an enumerable with the value from a passed block + or a default argument. + + %i( title body ).index_with { |attr| post.public_send(attr) } + # => { title: "hey", body: "what's up?" } + + %i( title body ).index_with(nil) + # => { title: nil, body: nil } + + Closely linked with its brethen `index_by`. + + *Kasper Timm Hansen* + * Fix bug where `ActiveSupport::Timezone.all` would fail when tzinfo data for any timezone defined in `ActiveSupport::TimeZone::MAPPING` is missing. diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index edde4f46b9..7713c52cb1 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true module Enumerable + INDEX_WITH_DEFAULT = Object.new + private_constant :INDEX_WITH_DEFAULT + # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements # when we omit an identity. @@ -37,10 +40,11 @@ module Enumerable end end - # Convert an enumerable to a hash. + # Convert an enumerable to a hash keying it by the block return value. # # people.index_by(&:login) # # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...} + # # people.index_by { |person| "#{person.first_name} #{person.last_name}" } # # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...} def index_by @@ -53,6 +57,26 @@ module Enumerable end end + # Convert an enumerable to a hash keying it with the enumerable items and with the values returned in the block. + # + # post = Post.new(title: "hey there", body: "what's up?") + # + # %i( title body ).index_with { |attr_name| post.public_send(attr_name) } + # # => { title: "hey there", body: "what's up?" } + def index_with(default = INDEX_WITH_DEFAULT) + if block_given? + result = {} + each { |elem| result[elem] = yield(elem) } + result + elsif default != INDEX_WITH_DEFAULT + result = {} + each { |elem| result[elem] = default } + result + else + to_enum(:index_with) { size if respond_to?(:size) } + end + end + # Returns +true+ if the enumerable has more than 1 element. Functionally # equivalent to <tt>enum.to_a.size > 1</tt>. Can be called with a block too, # much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns +true+ diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 8d71320931..b63464a36a 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -179,6 +179,21 @@ class EnumerableTests < ActiveSupport::TestCase payments.index_by.each(&:price)) end + def test_index_with + payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ]) + + assert_equal({ Payment.new(5) => 5, Payment.new(15) => 15, Payment.new(10) => 10 }, payments.index_with(&:price)) + + assert_equal({ title: nil, body: nil }, %i( title body ).index_with(nil)) + assert_equal({ title: [], body: [] }, %i( title body ).index_with([])) + assert_equal({ title: {}, body: {} }, %i( title body ).index_with({})) + + assert_equal Enumerator, payments.index_with.class + assert_nil payments.index_with.size + assert_equal 42, (1..42).index_with.size + assert_equal({ Payment.new(5) => 5, Payment.new(15) => 15, Payment.new(10) => 10 }, payments.index_with.each(&:price)) + end + def test_many assert_equal false, GenericEnumerable.new([]).many? assert_equal false, GenericEnumerable.new([ 1 ]).many? diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index ae2e1faf14..cde217e1e4 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -2039,6 +2039,21 @@ WARNING. Keys should normally be unique. If the block returns the same value for NOTE: Defined in `active_support/core_ext/enumerable.rb`. +### `index_with` + +The method `index_with` generates a hash with the elements of an enumerable as keys. The value +is either a passed default or returned in a block. + +```ruby +%i( title body created_at ).index_with { |attr_name| public_send(attr_name) } +# => { title: "hey", body: "what's up?", … } + +WEEKDAYS.index_with([ Interval.all_day ]) +# => { monday: [ 0, 1440 ], … } +``` + +NOTE: Defined in `active_support/core_ext/enumerable.rb`. + ### `many?` The method `many?` is shorthand for `collection.size > 1`: diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 84606d3b90..c2699006f6 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -1979,6 +1979,23 @@ module ApplicationTests assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8 end + test "ActionView::Template.finalize_compiled_template_methods is true by default" do + app "test" + assert_equal true, ActionView::Template.finalize_compiled_template_methods + end + + test "ActionView::Template.finalize_compiled_template_methods can be configured via config.action_view.finalize_compiled_template_methods" do + app_file "config/environments/test.rb", <<-RUBY + Rails.application.configure do + config.action_view.finalize_compiled_template_methods = false + end + RUBY + + app "test" + + assert_equal false, ActionView::Template.finalize_compiled_template_methods + end + private def force_lazy_load_hooks yield # Tasty clarifying sugar, homie! We only need to reference a constant to load it. diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb index f3a7e00a4d..92b991dd05 100644 --- a/railties/test/application/server_test.rb +++ b/railties/test/application/server_test.rb @@ -36,7 +36,7 @@ module ApplicationTests skip "PTY unavailable" unless available_pty? File.open("#{app_path}/config/boot.rb", "w") do |f| - f.puts "ENV['BUNDLE_GEMFILE'] = '#{Bundler.default_gemfile.to_s}'" + f.puts "ENV['BUNDLE_GEMFILE'] = '#{Bundler.default_gemfile}'" f.puts "require 'bundler/setup'" end |