diff options
Diffstat (limited to 'activerecord')
140 files changed, 1282 insertions, 556 deletions
diff --git a/activerecord/.gitignore b/activerecord/.gitignore new file mode 100644 index 0000000000..884ee009eb --- /dev/null +++ b/activerecord/.gitignore @@ -0,0 +1,3 @@ +/sqlnet.log +/test/config.yml +/test/fixtures/*.sqlite* diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d73c0859ee..647c96d4b1 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,19 @@ +* Rails 6 requires Ruby 2.4.1 or newer. + + *Jeremy Daer* + +* Deprecate `update_attributes`/`!` in favor of `update`/`!`. + + *Eddie Lebow* + +* Add ActiveRecord::Base.create_or_find_by/! to deal with the SELECT/INSERT race condition in + ActiveRecord::Base.find_or_create_by/! by leaning on unique constraints in the database. + + *DHH* + +* Add `Relation#pick` as short-hand for single-value plucks. + + *DHH* Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 8e42a11df4..b43e7c50f5 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "Object-relational mapper framework (part of Rails)." s.description = "Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in." - s.required_ruby_version = ">= 2.2.2" + s.required_ruby_version = ">= 2.4.1" s.license = "MIT" diff --git a/activerecord/bin/test b/activerecord/bin/test index 83c192531e..9ecf27ce67 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -1,6 +1,12 @@ #!/usr/bin/env ruby # frozen_string_literal: true +adapter_index = ARGV.index("--adapter") || ARGV.index("-a") +if adapter_index + ARGV.delete_at(adapter_index) + ENV["ARCONN"] = ARGV.delete_at(adapter_index).strip +end + COMPONENT_ROOT = File.expand_path("..", __dir__) require_relative "../../tools/test" @@ -17,4 +23,5 @@ module Minitest end end +Minitest.load_plugins Minitest.extensions.unshift "active_record" diff --git a/activerecord/examples/.gitignore b/activerecord/examples/.gitignore deleted file mode 100644 index 0dfc1cb7fb..0000000000 --- a/activerecord/examples/.gitignore +++ /dev/null @@ -1 +0,0 @@ -performance.sql diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index b4377ad6be..d43378c64f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -163,6 +163,7 @@ module ActiveRecord "active_record/tasks/postgresql_database_tasks" end + autoload :TestDatabases, "active_record/test_databases" autoload :TestFixtures, "active_record/fixtures" def self.eager_load! diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 7667c6ed8b..ca8c7794e0 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -156,9 +156,9 @@ module ActiveRecord reset end - # We can't dump @reflection since it contains the scope proc + # We can't dump @reflection and @through_reflection since it contains the scope proc def marshal_dump - ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } + ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] } [@reflection.name, ivars] end @@ -201,8 +201,8 @@ module ActiveRecord if (reflection.has_one? || reflection.collection?) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] - if reflection.options[:as] - attributes[reflection.type] = owner.class.base_class.name + if reflection.type + attributes[reflection.type] = owner.class.polymorphic_name end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 11967e0571..0a90a6104a 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -35,24 +35,20 @@ module ActiveRecord binds << last_reflection.join_id_for(owner) if last_reflection.type - binds << owner.class.base_class.name + binds << owner.class.polymorphic_name end chain.each_cons(2).each do |reflection, next_reflection| if reflection.type - binds << next_reflection.klass.base_class.name + binds << next_reflection.klass.polymorphic_name end end binds end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :value_transformation - private def join(table, constraint) table.create_join(table, table.create_on(constraint)) end @@ -67,7 +63,7 @@ module ActiveRecord scope = apply_scope(scope, table, key, value) if reflection.type - polymorphic_type = transform_value(owner.class.base_class.name) + polymorphic_type = transform_value(owner.class.polymorphic_name) scope = apply_scope(scope, table, reflection.type, polymorphic_type) end @@ -88,7 +84,7 @@ module ActiveRecord constraint = table[key].eq(foreign_table[foreign_key]) if reflection.type - value = transform_value(next_reflection.klass.base_class.name) + value = transform_value(next_reflection.klass.polymorphic_name) scope = apply_scope(scope, table, reflection.type, value) end @@ -135,7 +131,7 @@ module ActiveRecord item = eval_scope(reflection, scope_chain_item, owner) if scope_chain_item == chain_head.scope - scope.merge! item.except(:where, :includes) + scope.merge! item.except(:where, :includes, :unscope, :order) end reflection.all_includes do @@ -144,7 +140,7 @@ module ActiveRecord scope.unscope!(*item.unscope_values) scope.where_clause += item.where_clause - scope.order_values |= item.order_values + scope.order_values = item.order_values | scope.order_values end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index bd2012df84..1109fee462 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -5,7 +5,15 @@ module ActiveRecord # = Active Record Belongs To Association class BelongsToAssociation < SingularAssociation #:nodoc: def handle_dependency - target.send(options[:dependent]) if load_target + return unless load_target + + case options[:dependent] + when :destroy + target.destroy + raise ActiveRecord::Rollback unless target.destroyed? + else + target.send(options[:dependent]) + end end def replace(record) diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 55d789c66a..75b4c4481a 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -13,7 +13,7 @@ module ActiveRecord def replace_keys(record) super - owner[reflection.foreign_type] = record ? record.class.base_class.name : nil + owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil end def different_target?(record) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 7953b89f61..090b082cb0 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -60,6 +60,7 @@ module ActiveRecord when :destroy target.destroyed_by_association = reflection target.destroy + throw(:abort) unless target.destroyed? when :nullify target.update_columns(reflection.foreign_key => nil) if target.persisted? end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index df4bf07999..f88e383fe0 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -67,30 +67,8 @@ module ActiveRecord end end - # base is the base class on which operation is taking place. - # associations is the list of associations which are joined using hash, symbol or array. - # joins is the list of all string join commands and arel nodes. - # - # Example : - # - # class Physician < ActiveRecord::Base - # has_many :appointments - # has_many :patients, through: :appointments - # end - # - # If I execute `@physician.patients.to_a` then - # base # => Physician - # associations # => [] - # joins # => [#<Arel::Nodes::InnerJoin: ...] - # - # However if I execute `Physician.joins(:appointments).to_a` then - # base # => Physician - # associations # => [:appointments] - # joins # => [] - # - def initialize(base, table, associations, alias_tracker, eager_loading: true) + def initialize(base, table, associations, alias_tracker) @alias_tracker = alias_tracker - @eager_loading = eager_loading tree = self.class.make_tree associations @join_root = JoinBase.new(base, table, build(tree, base)) @join_root.children.each { |child| construct_tables! @join_root, child } @@ -221,12 +199,11 @@ module ActiveRecord reflection.check_eager_loadable! if reflection.polymorphic? - next unless @eager_loading raise EagerLoadPolymorphicError.new(reflection) end JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker) - end.compact + end end def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 221c791bf8..c36386ec7e 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -58,7 +58,7 @@ module ActiveRecord tables.first end - protected + private attr_reader :alias_tracker end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 59320431ee..1ea0aeac3a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -169,7 +169,7 @@ module ActiveRecord owners.flat_map { |owner| owner.association(reflection.name).target } end - protected + private attr_reader :owners, :reflection end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index ec6f0df484..0f38d6bbda 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -27,10 +27,9 @@ module ActiveRecord end end - protected + private attr_reader :owners, :reflection, :preload_scope, :model, :klass - private # The name of the key on the associated records def association_key_name reflection.join_primary_key(klass) @@ -118,7 +117,7 @@ module ActiveRecord scope = klass.scope_for_association if reflection.type - scope.where!(reflection.type => model.base_class.sti_name) + scope.where!(reflection.type => model.polymorphic_name) end scope.merge!(reflection_scope) if reflection.scope diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 8b0d9aab01..b6f0e18764 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -4,14 +4,8 @@ require "active_model/forbidden_attributes_protection" module ActiveRecord module AttributeAssignment - extend ActiveSupport::Concern include ActiveModel::AttributeAssignment - # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment. - def attributes=(attributes) - assign_attributes(attributes) - end - private def _assign_attributes(attributes) diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index c77790dc9a..7db9bbe46b 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -175,9 +175,20 @@ module ActiveRecord # Regexp whitelist. Matches the following: # "#{table_name}.#{column_name}" # "#{table_name}.#{column_name} #{direction}" + # "#{table_name}.#{column_name} #{direction} NULLS FIRST" + # "#{table_name}.#{column_name} NULLS LAST" # "#{column_name}" # "#{column_name} #{direction}" - COLUMN_NAME_ORDER_WHITELIST = /\A(?:\w+\.)?\w+(?:\s+asc|\s+desc)?\z/i + # "#{column_name} #{direction} NULLS FIRST" + # "#{column_name} NULLS LAST" + COLUMN_NAME_ORDER_WHITELIST = / + \A + (?:\w+\.)? + \w+ + (?:\s+asc|\s+desc)? + (?:\s+nulls\s+(?:first|last))? + \z + /ix def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc: unexpected = args.reject do |arg| @@ -432,33 +443,24 @@ module ActiveRecord @attributes.accessed end - protected - - def attribute_method?(attr_name) # :nodoc: + private + def attribute_method?(attr_name) # We check defined? because Syck calls respond_to? before actually calling initialize. defined?(@attributes) && @attributes.key?(attr_name) end - private - - def arel_attributes_with_values_for_create(attribute_names) - arel_attributes_with_values(attributes_for_create(attribute_names)) + def attributes_with_values_for_create(attribute_names) + attributes_with_values(attributes_for_create(attribute_names)) end - def arel_attributes_with_values_for_update(attribute_names) - arel_attributes_with_values(attributes_for_update(attribute_names)) + def attributes_with_values_for_update(attribute_names) + attributes_with_values(attributes_for_update(attribute_names)) end - # Returns a Hash of the Arel::Attributes and attribute values that have been - # typecasted for use in an Arel insert/update method. - def arel_attributes_with_values(attribute_names) - attrs = {} - arel_table = self.class.arel_table - - attribute_names.each do |name| - attrs[arel_table[name]] = _read_attribute(name) + def attributes_with_values(attribute_names) + attribute_names.each_with_object({}) do |name, attrs| + attrs[name] = _read_attribute(name) end - attrs end # Filters the primary keys and readonly attributes from the attribute names. diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index df4c79b0f6..3de6fe566d 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -32,7 +32,9 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do + @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @mutations_before_last_save = nil + @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new @mutations_from_database = nil end end @@ -112,12 +114,12 @@ module ActiveRecord # Alias for +changed+ def changed_attribute_names_to_save - mutations_from_database.changed_attribute_names + changes_to_save.keys end # Alias for +changed_attributes+ def attributes_in_database - mutations_from_database.changed_values + changes_to_save.transform_values(&:first) end private diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index d8fc046e10..2907547634 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -131,7 +131,7 @@ module ActiveRecord def suppress_composite_primary_key(pk) return pk unless pk.is_a?(Array) - warn <<-WARNING.strip_heredoc + warn <<~WARNING WARNING: Active Record does not support composite primary key. #{table_name} has composite primary key. Composite primary key is ignored. diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 4077250583..14f700b6a9 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -27,7 +27,7 @@ module ActiveRecord # 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.unpack("h*".freeze).first + safe_name = name.unpack1("h*".freeze) temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index ebc2baed34..6e0e90f39c 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -7,7 +7,7 @@ module ActiveRecord class ColumnNotSerializableError < StandardError def initialize(name, type) - super <<-EOS.strip_heredoc + super <<~EOS Column `#{name}` of type #{type.class} does not support `serialize` feature. Usually it means that you are trying to use `serialize` on a column that already implements serialization natively. diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index bb0ec6a8c3..c7521422bb 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -13,7 +13,7 @@ module ActiveRecord private def define_method_attribute=(name) - safe_name = name.unpack("h*".freeze).first + safe_name = name.unpack1("h*".freeze) ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index b7ad944cec..cc99401390 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -9,7 +9,6 @@ require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/array/extract_options" require "active_support/core_ext/hash/deep_merge" require "active_support/core_ext/hash/slice" -require "active_support/core_ext/hash/transform_values" require "active_support/core_ext/string/behavior" require "active_support/core_ext/kernel/singleton_class" require "active_support/core_ext/module/introspection" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 92e46ccf9f..aec5fa6ba1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -130,7 +130,7 @@ module ActiveRecord end def quoted_time(value) # :nodoc: - quoted_date(value).sub(/\A2000-01-01 /, "") + quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "") end def quoted_binary(value) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 4a191d337c..529c9d8ca6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/string/strip" - module ActiveRecord module ConnectionAdapters class AbstractAdapter @@ -17,9 +15,8 @@ module ActiveRecord end delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, to: :@conn - private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, + to: :@conn, private: true private @@ -66,7 +63,7 @@ module ActiveRecord end def visit_ForeignKeyDefinition(o) - sql = <<-SQL.strip_heredoc + sql = +<<~SQL CONSTRAINT #{quote_column_name(o.name)} FOREIGN KEY (#{quote_column_name(o.column)}) REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) @@ -133,7 +130,7 @@ module ActiveRecord when :cascade then "ON #{action} CASCADE" when :restrict then "ON #{action} RESTRICT" else - raise ArgumentError, <<-MSG.strip_heredoc + raise ArgumentError, <<~MSG '#{dependency}' is not supported for :on_update or :on_delete. Supported values are: :nullify, :cascade, :restrict MSG 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 0594b4b485..584a86da21 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -151,13 +151,8 @@ module ActiveRecord end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options - private + attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options def as_options(value) value.is_a?(Hash) ? value : {} diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 1926603474..622e00fffb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/hash/compact" - module ActiveRecord module ConnectionAdapters # :nodoc: class SchemaDumper < SchemaDumper # :nodoc: 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 db033db913..e2147b7fcf 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -406,7 +406,7 @@ module ActiveRecord # # Defaults to false. # - # Only supported on the MySQL adapter, ignored elsewhere. + # Only supported on the MySQL and PostgreSQL adapter, ignored elsewhere. # # ====== Add a column # diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 56a5ea153b..559f068c39 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -119,11 +119,11 @@ module ActiveRecord end end - def migrations_paths + def migrations_paths # :nodoc: @config[:migrations_paths] || Migrator.migrations_paths end - def migration_context + def migration_context # :nodoc: MigrationContext.new(migrations_paths) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 1fb0bd8d56..fbedddd7f9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -11,8 +11,6 @@ require "active_record/connection_adapters/mysql/schema_dumper" require "active_record/connection_adapters/mysql/schema_statements" require "active_record/connection_adapters/mysql/type_metadata" -require "active_support/core_ext/string/strip" - module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter @@ -284,7 +282,7 @@ module ActiveRecord def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name) - query_value(<<-SQL.strip_heredoc, "SCHEMA").presence + query_value(<<~SQL, "SCHEMA").presence SELECT table_comment FROM information_schema.tables WHERE table_schema = #{scope[:schema]} @@ -392,7 +390,7 @@ module ActiveRecord scope = quoted_scope(table_name) - fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") + fk_info = exec_query(<<~SQL, "SCHEMA") SELECT fk.referenced_table_name AS 'to_table', fk.referenced_column_name AS 'primary_key', fk.column_name AS 'column', @@ -480,7 +478,7 @@ module ActiveRecord scope = quoted_scope(table_name) - query_values(<<-SQL.strip_heredoc, "SCHEMA") + query_values(<<~SQL, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage WHERE constraint_name = 'PRIMARY' @@ -515,7 +513,7 @@ module ActiveRecord s.gsub(/\s+(?:ASC|DESC)\b/i, "") }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super, *order_columns].join(", ") + (order_columns << super).join(", ") end def strict_mode? diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 508132accb..901717ae3d 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -156,7 +156,6 @@ module ActiveRecord env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) end - config.reject! { |k, v| v.is_a?(Hash) && !(v.key?("adapter") || v.key?("url")) } config.merge! env_config if env_config config.each do |key, value| diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index 5beaeec9c4..458c9bfd70 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -59,7 +59,7 @@ module ActiveRecord end def discard_remaining_results - @connection.next_result while @connection.more_results? + @connection.abandon_results! end def exec_stmt_and_free(sql, name, binds, cache_stmt: false) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index 75377693c6..c9ea653b77 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -4,8 +4,7 @@ module ActiveRecord module ConnectionAdapters module MySQL class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: - delegate :add_sql_comment!, :mariadb?, to: :@conn - private :add_sql_comment!, :mariadb? + delegate :add_sql_comment!, :mariadb?, to: :@conn, private: true private diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index bfdc7995f0..4c57bd48ab 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -3,7 +3,7 @@ require "active_record/connection_adapters/abstract_mysql_adapter" require "active_record/connection_adapters/mysql/database_statements" -gem "mysql2", "~> 0.4.4" +gem "mysql2", ">= 0.4.4", "< 0.6.0" require "mysql2" module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 469ef3f5a0..3ccc7271ab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -20,10 +20,9 @@ module ActiveRecord end end - protected + private attr_reader :max_identifier_length - private def sequence_name_from_parts(table_name, column_name, suffix) over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 542ca75d3e..247a25054e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -5,6 +5,7 @@ require "active_record/connection_adapters/postgresql/oid/bit" require "active_record/connection_adapters/postgresql/oid/bit_varying" require "active_record/connection_adapters/postgresql/oid/bytea" require "active_record/connection_adapters/postgresql/oid/cidr" +require "active_record/connection_adapters/postgresql/oid/date" require "active_record/connection_adapters/postgresql/oid/date_time" require "active_record/connection_adapters/postgresql/oid/decimal" require "active_record/connection_adapters/postgresql/oid/enum" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 587e95d192..e9a79526f9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -43,10 +43,7 @@ module ActiveRecord /\A[0-9A-F]*\Z/i.match?(value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :value end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb new file mode 100644 index 0000000000..24a1daa95a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Date < Type::Date # :nodoc: + def cast_value(value) + case value + when "infinity" then ::Float::INFINITY + when "-infinity" then -::Float::INFINITY + when / BC$/ + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) + else + super + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 7999434ad8..e20e5f2914 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -107,7 +107,7 @@ module ActiveRecord oid = row[4] comment = row[5] - using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten + using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten orders = {} opclasses = {} @@ -115,7 +115,7 @@ module ActiveRecord if indkey.include?(0) columns = expressions else - columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact + columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact SELECT a.attnum, a.attname FROM pg_attribute a WHERE a.attrelid = #{oid} @@ -158,7 +158,7 @@ module ActiveRecord def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name, type: "BASE TABLE") if scope[:name] - query_value(<<-SQL.strip_heredoc, "SCHEMA") + query_value(<<~SQL, "SCHEMA") SELECT pg_catalog.obj_description(c.oid, 'pg_class') FROM pg_catalog.pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace @@ -353,7 +353,7 @@ module ActiveRecord end def primary_keys(table_name) # :nodoc: - query_values(<<-SQL.strip_heredoc, "SCHEMA") + query_values(<<~SQL, "SCHEMA") SELECT a.attname FROM ( SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx @@ -502,7 +502,7 @@ module ActiveRecord def foreign_keys(table_name) scope = quoted_scope(table_name) - fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") + fk_info = exec_query(<<~SQL, "SCHEMA") SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid @@ -583,7 +583,7 @@ module ActiveRecord .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "") }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - [super, *order_columns].join(", ") + (order_columns << super).join(", ") end def update_table_definition(table_name, base) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index dc6287e32c..fdf6f75108 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -465,7 +465,7 @@ module ActiveRecord register_class_with_limit m, "bit", OID::Bit register_class_with_limit m, "varbit", OID::BitVarying m.alias_type "timestamptz", "timestamp" - m.register_type "date", Type::Date.new + m.register_type "date", OID::Date.new m.register_type "money", OID::Money.new m.register_type "bytea", OID::Bytea.new @@ -837,6 +837,7 @@ module ActiveRecord ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql) ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql) ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql) + ActiveRecord::Type.register(:date, OID::Date, adapter: :postgresql) ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql) ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql) ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index 8042dbfea2..70de96326c 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -17,7 +17,7 @@ module ActiveRecord end def quoted_time(value) - quoted_date(value) + quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ") end def quoted_binary(value) diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 88d28dc52a..ee0e651912 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -57,6 +57,10 @@ module ActiveRecord spec = resolver.resolve(config).symbolize_keys spec[:name] = spec_name + # use the primary config if a config is not passed in and + # it's a three tier config + spec = spec[spec_name.to_sym] if spec[spec_name.to_sym] + connection_handler.establish_connection(spec) end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 1a3e6e4d09..23ecb24542 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -141,10 +141,7 @@ module ActiveRecord end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :name, :mapping, :subtype end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index efcbd44776..c2a180c939 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -120,13 +120,13 @@ module ActiveRecord def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil) @adapter = adapter if table - msg = <<-EOM.strip_heredoc + msg = +<<~EOM Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`. This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`. To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`). EOM else - msg = <<-EOM + msg = +<<~EOM There is a mismatch between the foreign key and primary key column types. Verify that the foreign key column type and the primary key of the associated table match types. EOM diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index d9a75d9ad6..8f022ff7a7 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -874,6 +874,7 @@ module ActiveRecord class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances class_attribute :pre_loaded_fixtures, default: false class_attribute :config, default: ActiveRecord::Base + class_attribute :lock_threads, default: true end module ClassMethods @@ -973,7 +974,7 @@ module ActiveRecord @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| connection.begin_transaction joinable: false - connection.pool.lock_thread = true + connection.pool.lock_thread = true if lock_threads end # When connections are established in the future, begin a transaction too @@ -989,7 +990,7 @@ module ActiveRecord if connection && !@fixture_connections.include?(connection) connection.begin_transaction joinable: false - connection.pool.lock_thread = true + connection.pool.lock_thread = true if lock_threads @fixture_connections << connection end end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index f3fe610c09..208ba95c52 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -104,21 +104,47 @@ module ActiveRecord end end - # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>). - # If you are using inheritance with ActiveRecord and don't want child classes - # to utilize the implied STI table name of the parent class, this will need to be true. - # For example, given the following: + # Set this to +true+ if this is an abstract class (see + # <tt>abstract_class?</tt>). + # If you are using inheritance with Active Record and don't want a class + # to be considered as part of the STI hierarchy, you must set this to + # true. + # +ApplicationRecord+, for example, is generated as an abstract class. # - # class SuperClass < ActiveRecord::Base + # Consider the following default behaviour: + # + # Shape = Class.new(ActiveRecord::Base) + # Polygon = Class.new(Shape) + # Square = Class.new(Polygon) + # + # Shape.table_name # => "shapes" + # Polygon.table_name # => "shapes" + # Square.table_name # => "shapes" + # Shape.create! # => #<Shape id: 1, type: nil> + # Polygon.create! # => #<Polygon id: 2, type: "Polygon"> + # Square.create! # => #<Square id: 3, type: "Square"> + # + # However, when using <tt>abstract_class</tt>, +Shape+ is omitted from + # the hierarchy: + # + # class Shape < ActiveRecord::Base # self.abstract_class = true # end - # class Child < SuperClass - # self.table_name = 'the_table_i_really_want' - # end - # + # Polygon = Class.new(Shape) + # Square = Class.new(Polygon) # - # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt> + # Shape.table_name # => nil + # Polygon.table_name # => "polygons" + # Square.table_name # => "polygons" + # Shape.create! # => NotImplementedError: Shape is an abstract class and cannot be instantiated. + # Polygon.create! # => #<Polygon id: 1, type: nil> + # Square.create! # => #<Square id: 2, type: "Square"> # + # Note that in the above example, to disallow the creation of a plain + # +Polygon+, you should use <tt>validates :type, presence: true</tt>, + # instead of setting it as an abstract class. This way, +Polygon+ will + # stay in the hierarchy, and Active Record will continue to correctly + # derive the table name. attr_accessor :abstract_class # Returns whether this class is an abstract class or not. @@ -130,6 +156,10 @@ module ActiveRecord store_full_sti_class ? name : name.demodulize end + def polymorphic_name + base_class.name + end + def inherited(subclass) subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new) super diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 5a65edf27e..3626a13d7c 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -17,7 +17,7 @@ module ActiveRecord end def []=(key, value) - find_or_initialize_by(key: key).update_attributes!(value: value) + find_or_initialize_by(key: key).update!(value: value) end def [](key) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index e1e24e2814..7f096bb532 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -61,13 +61,6 @@ module ActiveRecord end private - - def increment_lock - lock_col = self.class.locking_column - previous_lock_value = send(lock_col) - send("#{lock_col}=", previous_lock_value + 1) - end - def _create_record(attribute_names = self.attribute_names, *) if locking_enabled? # We always want to persist the locking version, even if we don't detect @@ -77,63 +70,56 @@ module ActiveRecord super end - def _update_record(attribute_names = self.attribute_names) + def _touch_row(attribute_names, time) + super + ensure + clear_attribute_change(self.class.locking_column) if locking_enabled? + end + + def _update_row(attribute_names, attempted_action = "update") return super unless locking_enabled? - return 0 if attribute_names.empty? begin - lock_col = self.class.locking_column - - previous_lock_value = read_attribute_before_type_cast(lock_col) - - increment_lock - - attribute_names.push(lock_col) + locking_column = self.class.locking_column + previous_lock_value = read_attribute_before_type_cast(locking_column) + attribute_names << locking_column - relation = self.class.unscoped + self[locking_column] += 1 - affected_rows = relation.where( - self.class.primary_key => id, - lock_col => previous_lock_value - ).update_all( - attributes_for_update(attribute_names).map do |name| - [name, _read_attribute(name)] - end.to_h + affected_rows = self.class._update_record( + attributes_with_values(attribute_names), + self.class.primary_key => id_in_database, + locking_column => previous_lock_value ) - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "update") + if affected_rows != 1 + raise ActiveRecord::StaleObjectError.new(self, attempted_action) end affected_rows # If something went wrong, revert the locking_column value. rescue Exception - send("#{lock_col}=", previous_lock_value.to_i) - + self[locking_column] = previous_lock_value.to_i raise end end def destroy_row - affected_rows = super - - if locking_enabled? && affected_rows != 1 - raise ActiveRecord::StaleObjectError.new(self, "destroy") - end + return super unless locking_enabled? - affected_rows - end + locking_column = self.class.locking_column - def relation_for_destroy - relation = super + affected_rows = self.class._delete_record( + self.class.primary_key => id_in_database, + locking_column => read_attribute_before_type_cast(locking_column) + ) - if locking_enabled? - locking_column = self.class.locking_column - relation = relation.where(locking_column => read_attribute_before_type_cast(locking_column)) + if affected_rows != 1 + raise ActiveRecord::StaleObjectError.new(self, "destroy") end - relation + affected_rows end module ClassMethods diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 5322571820..663b3c590a 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -140,6 +140,7 @@ module ActiveRecord class ConcurrentMigrationError < MigrationError #:nodoc: DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze + RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze def initialize(message = DEFAULT_MESSAGE) super @@ -1355,12 +1356,17 @@ module ActiveRecord def with_advisory_lock lock_id = generate_migrator_advisory_lock_id - got_lock = Base.connection.get_advisory_lock(lock_id) + connection = Base.connection + got_lock = connection.get_advisory_lock(lock_id) raise ConcurrentMigrationError unless got_lock load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock yield ensure - Base.connection.release_advisory_lock(lock_id) if got_lock + if got_lock && !connection.release_advisory_lock(lock_id) + raise ConcurrentMigrationError.new( + ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE + ) + end end MIGRATOR_SALT = 2053462845 diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 81ef4828f8..087632b10f 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -85,7 +85,7 @@ module ActiveRecord # invert the +command+. def inverse_of(command, args, &block) method = :"invert_#{command}" - raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true) + raise IrreversibleMigration, <<~MSG unless respond_to?(method, true) This migration uses #{command}, which is not automatically reversible. To make the migration reversible you can either: 1. Define #up and #down methods in place of the #change method. diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 60823815fa..b04dc04899 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -379,6 +379,7 @@ module ActiveRecord end def _default_attributes # :nodoc: + load_schema @default_attributes ||= ActiveModel::AttributeSet.new({}) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a45d011d75..6ec477c7f3 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -169,12 +169,11 @@ module ActiveRecord primary_key_value = nil if primary_key && Hash === values - arel_primary_key = arel_attribute(primary_key) - primary_key_value = values[arel_primary_key] + primary_key_value = values[primary_key] if !primary_key_value && prefetch_primary_key? primary_key_value = next_sequence_value - values[arel_primary_key] = primary_key_value + values[primary_key] = primary_key_value end end @@ -188,15 +187,26 @@ module ActiveRecord connection.insert(im, "#{self} Create", primary_key || false, primary_key_value) end - def _update_record(values, id, id_was) # :nodoc: - bind = predicate_builder.build_bind_attribute(primary_key, id_was || id) + def _update_record(values, constraints) # :nodoc: + constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) } + um = arel_table.where( - arel_attribute(primary_key).eq(bind) + constraints.reduce(&:and) ).compile_update(_substitute_values(values), primary_key) connection.update(um, "#{self} Update") end + def _delete_record(constraints) # :nodoc: + constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) } + + dm = Arel::DeleteManager.new + dm.from(arel_table) + dm.wheres = constraints + + connection.delete(dm, "#{self} Destroy") + end + private # Called by +instantiate+ to decide which class to use for a new # record instance. @@ -208,8 +218,9 @@ module ActiveRecord end def _substitute_values(values) - values.map do |attr, value| - bind = predicate_builder.build_bind_attribute(attr.name, value) + values.map do |name, value| + attr = arel_attribute(name) + bind = predicate_builder.build_bind_attribute(name, value) [attr, bind] end end @@ -310,7 +321,7 @@ module ActiveRecord # callbacks or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. def delete - _relation_for_itself.delete_all if persisted? + _delete_row if persisted? @destroyed = true freeze end @@ -363,6 +374,7 @@ module ActiveRecord became.send(:initialize) became.instance_variable_set("@attributes", @attributes) became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database) + became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.errors.copy!(errors) @@ -418,6 +430,7 @@ module ActiveRecord end alias update_attributes update + deprecate :update_attributes # Updates its receiver just like #update but calls #save! instead # of +save+, so an exception is raised if the record is invalid and saving will fail. @@ -431,6 +444,7 @@ module ActiveRecord end alias update_attributes! update! + deprecate :update_attributes! # Equivalent to <code>update_columns(name => value)</code>. def update_column(name, value) @@ -461,13 +475,16 @@ module ActiveRecord verify_readonly_attribute(key.to_s) end - updated_count = _relation_for_itself.update_all(attributes) + affected_rows = self.class._update_record( + attributes, + self.class.primary_key => id_in_database + ) attributes.each do |k, v| write_attribute_without_type_cast(k, v) end - updated_count == 1 + affected_rows == 1 end # Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1). @@ -640,35 +657,12 @@ module ActiveRecord MSG end - time ||= current_time_from_proper_timezone - attributes = timestamp_attributes_for_update_in_model - attributes.concat(names) - - unless attributes.empty? - changes = {} - - attributes.each do |column| - column = column.to_s - changes[column] = write_attribute(column, time) - end - - scope = _relation_for_itself - - if locking_enabled? - locking_column = self.class.locking_column - scope = scope.where(locking_column => read_attribute_before_type_cast(locking_column)) - changes[locking_column] = increment_lock - end - - clear_attribute_changes(changes.keys) - result = scope.update_all(changes) == 1 - - if !result && locking_enabled? - raise ActiveRecord::StaleObjectError.new(self, "touch") - end + attribute_names = timestamp_attributes_for_update_in_model + attribute_names.concat(names) - @_trigger_update_callback = result - result + unless attribute_names.empty? + affected_rows = _touch_row(attribute_names, time) + @_trigger_update_callback = affected_rows == 1 else true end @@ -681,15 +675,29 @@ module ActiveRecord end def destroy_row - relation_for_destroy.delete_all + _delete_row end - def relation_for_destroy - _relation_for_itself + def _delete_row + self.class._delete_record(self.class.primary_key => id_in_database) end - def _relation_for_itself - self.class.unscoped.where(self.class.primary_key => id) + def _touch_row(attribute_names, time) + time ||= current_time_from_proper_timezone + + attribute_names.each do |attr_name| + write_attribute(attr_name, time) + clear_attribute_change(attr_name) + end + + _update_row(attribute_names, "touch") + end + + def _update_row(attribute_names, attempted_action = "update") + self.class._update_record( + attributes_with_values(attribute_names), + self.class.primary_key => id_in_database + ) end def create_or_update(*args, &block) @@ -702,24 +710,27 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def _update_record(attribute_names = self.attribute_names) - attributes_values = arel_attributes_with_values_for_update(attribute_names) - if attributes_values.empty? - rows_affected = 0 + attribute_names &= self.class.column_names + attribute_names = attributes_for_update(attribute_names) + + if attribute_names.empty? + affected_rows = 0 @_trigger_update_callback = true else - rows_affected = self.class._update_record(attributes_values, id, id_in_database) - @_trigger_update_callback = rows_affected > 0 + affected_rows = _update_row(attribute_names) + @_trigger_update_callback = affected_rows == 1 end yield(self) if block_given? - rows_affected + affected_rows end # Creates a record with values matching those of the instance attributes # and returns its id. def _create_record(attribute_names = self.attribute_names) - attributes_values = arel_attributes_with_values_for_create(attribute_names) + attribute_names &= self.class.column_names + attributes_values = attributes_with_values_for_create(attribute_names) new_id = self.class._insert_record(attributes_values) self.id ||= new_id if self.class.primary_key diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 8e23128333..c8e340712d 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -7,20 +7,20 @@ module ActiveRecord # Enable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def cache(&block) - if configurations.empty? - yield - else + if connected? || !configurations.empty? connection.cache(&block) + else + yield end end # Disable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def uncached(&block) - if configurations.empty? - yield - else + if connected? || !configurations.empty? connection.uncached(&block) + else + yield end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 3996d5661f..d33d36ac02 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -5,7 +5,7 @@ module ActiveRecord delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all - delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all + delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all delegate :destroy_all, :delete_all, :update_all, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all @@ -13,7 +13,7 @@ module ActiveRecord :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all - delegate :pluck, :ids, to: :all + delegate :pluck, :pick, :ids, to: :all # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index ade5946dd5..6ab80a654d 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -157,6 +157,13 @@ end_warning end end + initializer "active_record.collection_cache_association_loading" do + require "active_record/railties/collection_cache_association_loading" + ActiveSupport.on_load(:action_view) do + ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading) + end + end + initializer "active_record.set_reloader_hooks" do ActiveSupport.on_load(:active_record) do ActiveSupport::Reloader.before_class_unload do diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb new file mode 100644 index 0000000000..b5129e4239 --- /dev/null +++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ActiveRecord + module Railties # :nodoc: + module CollectionCacheAssociationLoading #:nodoc: + def setup(context, options, block) + @relation = relation_from_options(options) + + super + end + + def relation_from_options(cached: nil, partial: nil, collection: nil, **_) + return unless cached + + relation = partial if partial.is_a?(ActiveRecord::Relation) + relation ||= collection if collection.is_a?(ActiveRecord::Relation) + + if relation && !relation.loaded? + relation.skip_preloading! + end + end + + def collection_without_template + @relation.preload_associations(@collection) if @relation + super + end + + def collection_with_template + @relation.preload_associations(@collection) if @relation + super + end + end + end +end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index 2ae733f657..309441a057 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -8,49 +8,44 @@ module ActiveRecord module ControllerRuntime #:nodoc: extend ActiveSupport::Concern - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_internal :db_runtime - - private - - def process_action(action, *args) - # We also need to reset the runtime before each action - # because of queries in middleware or in cases we are streaming - # and it won't be cleaned up by the method below. - ActiveRecord::LogSubscriber.reset_runtime - super + module ClassMethods # :nodoc: + def log_process_action(payload) + messages, db_runtime = super, payload[:db_runtime] + messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime + messages + end end - def cleanup_view_runtime - if logger && logger.info? && ActiveRecord::Base.connected? - db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime - self.db_runtime = (db_runtime || 0) + db_rt_before_render - runtime = super - db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime - self.db_runtime += db_rt_after_render - runtime - db_rt_after_render - else + private + attr_internal :db_runtime + + def process_action(action, *args) + # We also need to reset the runtime before each action + # because of queries in middleware or in cases we are streaming + # and it won't be cleaned up by the method below. + ActiveRecord::LogSubscriber.reset_runtime super end - end - def append_info_to_payload(payload) - super - if ActiveRecord::Base.connected? - payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime + def cleanup_view_runtime + if logger && logger.info? && ActiveRecord::Base.connected? + db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime + self.db_runtime = (db_runtime || 0) + db_rt_before_render + runtime = super + db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime + self.db_runtime += db_rt_after_render + runtime - db_rt_after_render + else + super + end end - end - module ClassMethods # :nodoc: - def log_process_action(payload) - messages, db_runtime = super, payload[:db_runtime] - messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime - messages + def append_info_to_payload(payload) + super + if ActiveRecord::Base.connected? + payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime + end end - end end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 2e55713311..662a8bc720 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -229,7 +229,7 @@ db_namespace = namespace :db do base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path Dir["#{base_dir}/**/*.yml"].each do |file| - if data = YAML::load(ERB.new(IO.read(file)).result) + if data = YAML.load(ERB.new(IO.read(file)).result) data.each_key do |key| key_id = ActiveRecord::FixtureSet.identify(key) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index e640d75d2f..2f43d005f3 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -193,7 +193,7 @@ module ActiveRecord klass_scope = klass_join_scope(table, predicate_builder) if type - klass_scope.where!(type => foreign_klass.base_class.sti_name) + klass_scope.where!(type => foreign_klass.polymorphic_name) end scope_chain_items.inject(klass_scope, &:merge!) @@ -416,6 +416,9 @@ module ActiveRecord # Active Record class. class AssociationReflection < MacroReflection #:nodoc: def compute_class(name) + if polymorphic? + raise ArgumentError, "Polymorphic association does not support to compute class." + end active_record.send(:compute_type, name) end @@ -626,9 +629,6 @@ module ActiveRecord # +automatic_inverse_of+ method is a valid reflection. We must # make sure that the reflection's active_record name matches up # with the current reflection's klass name. - # - # Note: klass will always be valid because when there's a NameError - # from calling +klass+, +reflection+ will already be set to false. def valid_inverse_reflection?(reflection) reflection && klass <= reflection.active_record && @@ -732,6 +732,9 @@ module ActiveRecord end private + def can_find_inverse_of_automatically?(_) + !polymorphic? && super + end def calculate_constructable(macro, options) !polymorphic? @@ -962,16 +965,14 @@ module ActiveRecord collect_join_reflections(seed + [self]) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :delegate_reflection - def actual_source_reflection # FIXME: this is a horrible name source_reflection.actual_source_reflection end private + attr_reader :delegate_reflection + def collect_join_reflections(seed) a = source_reflection.add_as_source seed if options[:source_type] diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index d71c430045..34e643b2de 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -8,7 +8,8 @@ module ActiveRecord :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, - :reverse_order, :distinct, :create_with, :skip_query_cache] + :reverse_order, :distinct, :create_with, :skip_query_cache, + :skip_preloading] CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having] @@ -142,23 +143,12 @@ module ActiveRecord # failed due to validation errors it won't be persisted, you get what # #create returns in such situation. # - # Please note *this method is not atomic*, it runs first a SELECT, and if + # Please note <b>this method is not atomic</b>, it runs first a SELECT, and if # there are no results an INSERT is attempted. If there are other threads # or processes there is a race condition between both calls and it could # be the case that you end up with two similar records. # - # Whether that is a problem or not depends on the logic of the - # application, but in the particular case in which rows have a UNIQUE - # constraint an exception may be raised, just retry: - # - # begin - # CreditAccount.transaction(requires_new: true) do - # CreditAccount.find_or_create_by(user_id: user.id) - # end - # rescue ActiveRecord::RecordNotUnique - # retry - # end - # + # If this might be a problem for your application, please see #create_or_find_by. def find_or_create_by(attributes, &block) find_by(attributes) || create(attributes, &block) end @@ -170,6 +160,47 @@ module ActiveRecord find_by(attributes) || create!(attributes, &block) end + # Attempts to create a record with the given attributes in a table that has a unique constraint + # on one or several of its columns. If a row already exists with one or several of these + # unique constraints, the exception such an insertion would normally raise is caught, + # and the existing record with those attributes is found using #find_by. + # + # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT + # and the INSERT, as that method needs to first query the table, then attempt to insert a row + # if none is found. + # + # There are several drawbacks to #create_or_find_by, though: + # + # * The underlying table must have the relevant columns defined with unique constraints. + # * A unique constraint violation may be triggered by only one, or at least less than all, + # of the given attributes. This means that the subsequent #find_by may fail to find a + # matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception, + # rather than a record with the given attributes. + # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by, + # we actually have another race condition between INSERT -> SELECT, which can be triggered + # if a DELETE between those two statements is run by another client. But for most applications, + # that's a significantly less likely condition to hit. + # * It relies on exception handling to handle control flow, which may be marginally slower. + # + # This method will return a record if all given attributes are covered by unique constraints + # (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted + # and failed due to validation errors it won't be persisted, you get what #create returns in + # such situation. + def create_or_find_by(attributes, &block) + transaction(requires_new: true) { create(attributes, &block) } + rescue ActiveRecord::RecordNotUnique + find_by!(attributes) + end + + # Like #create_or_find_by, but calls + # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception + # is raised if the created record is invalid. + def create_or_find_by!(attributes, &block) + transaction(requires_new: true) { create!(attributes, &block) } + rescue ActiveRecord::RecordNotUnique + find_by!(attributes) + end + # Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new] # instead of {create}[rdoc-ref:Persistence::ClassMethods#create]. def find_or_initialize_by(attributes, &block) @@ -516,6 +547,16 @@ module ActiveRecord ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins) end + def preload_associations(records) + preload = preload_values + preload += includes_values unless eager_loading? + preloader = nil + preload.each do |associations| + preloader ||= build_preloader + preloader.preload records, associations + end + end + protected def load_records(records) @@ -545,13 +586,7 @@ module ActiveRecord klass.find_by_sql(arel, &block).freeze end - preload = preload_values - preload += includes_values unless eager_loading? - preloader = nil - preload.each do |associations| - preloader ||= build_preloader - preloader.preload @records, associations - end + preload_associations(@records) unless skip_preloading_value @records.each(&:readonly!) if readonly_value diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 561869017a..ec4bb06c57 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -251,25 +251,31 @@ module ActiveRecord end end - attr = Relation::QueryAttribute.new(primary_key, primary_key_offset, klass.type_for_attribute(primary_key)) - batch_relation = relation.where(arel_attribute(primary_key).gt(Arel::Nodes::BindParam.new(attr))) + bind = primary_key_bind(primary_key_offset) + batch_relation = relation.where(arel_attribute(primary_key).gt(bind)) end end private def apply_limits(relation, start, finish) - if start - attr = Relation::QueryAttribute.new(primary_key, start, klass.type_for_attribute(primary_key)) - relation = relation.where(arel_attribute(primary_key).gteq(Arel::Nodes::BindParam.new(attr))) - end - if finish - attr = Relation::QueryAttribute.new(primary_key, finish, klass.type_for_attribute(primary_key)) - relation = relation.where(arel_attribute(primary_key).lteq(Arel::Nodes::BindParam.new(attr))) - end + relation = apply_start_limit(relation, start) if start + relation = apply_finish_limit(relation, finish) if finish relation end + def apply_start_limit(relation, start) + relation.where(arel_attribute(primary_key).gteq(primary_key_bind(start))) + end + + def apply_finish_limit(relation, finish) + relation.where(arel_attribute(primary_key).lteq(primary_key_bind(finish))) + end + + def primary_key_bind(value) + predicate_builder.build_bind_attribute(primary_key, value) + end + def batch_order arel_attribute(primary_key).asc end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index dba2f33dd4..f215c95f51 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -132,7 +132,7 @@ module ActiveRecord if has_include?(column_name) relation = apply_join_dependency - if operation.to_s.downcase == "count" && !distinct_value + if operation.to_s.downcase == "count" relation.distinct! # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT if (column_name == :all || column_name.nil?) && select_values.empty? @@ -200,6 +200,24 @@ module ActiveRecord end end + # Pick the value(s) from the named column(s) in the current relation. + # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful + # when you have a relation that's already narrowed down to a single row. + # + # Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also + # more efficient. The value is, again like with pluck, typecast by the column type. + # + # Person.where(id: 1).pick(:name) + # # SELECT people.name FROM people WHERE id = 1 LIMIT 1 + # # => 'David' + # + # Person.where(id: 1).pick(:name, :email_address) + # # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1 + # # => [ 'David', 'david@loudthinking.com' ] + def pick(*column_names) + limit(1).pluck(*column_names).first + end + # Pluck all the ID's for the relation using the table's primary key # # Person.ids # SELECT people.id FROM people diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 4863befec8..4b16b49cdf 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -38,7 +38,7 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :encode_with, :length, :each, :uniq, :join, + delegate :to_xml, :encode_with, :length, :each, :join, :[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of, :to_sentence, :to_formatted_s, :as_json, :shuffle, :split, :slice, :index, :rindex, to: :records diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 5f959af5dc..f7613a187d 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -371,16 +371,16 @@ module ActiveRecord relation end - def construct_join_dependency(eager_loading: true) + def construct_join_dependency including = eager_load_values + includes_values joins = joins_values.select { |join| join.is_a?(Arel::Nodes::Join) } ActiveRecord::Associations::JoinDependency.new( - klass, table, including, alias_tracker(joins), eager_loading: eager_loading + klass, table, including, alias_tracker(joins) ) end def apply_join_dependency(eager_loading: true) - join_dependency = construct_join_dependency(eager_loading: eager_loading) + join_dependency = construct_join_dependency relation = except(:includes, :eager_load, :preload).joins!(join_dependency) if eager_loading && !using_limitable_reflections?(join_dependency.reflections) diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index f3286846d2..7a0edcbc33 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -57,9 +57,6 @@ module ActiveRecord end protected - - attr_reader :table - def expand_from_hash(attributes) return ["1=0"] if attributes.empty? @@ -109,6 +106,7 @@ module ActiveRecord end private + attr_reader :table def associated_predicate_builder(association_name) self.class.new(table.associated_table(association_name)) diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 2fd75c8958..64bf83e3c1 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -34,8 +34,7 @@ module ActiveRecord array_predicates.inject(&:or) end - protected - + private attr_reader :predicate_builder module NullPredicate # :nodoc: diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb index 28c7483c95..88cd71cf69 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -12,12 +12,9 @@ module ActiveRecord [associated_table.association_join_foreign_key.to_s => ids] end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected + private attr_reader :associated_table, :value - private def ids case value when Relation diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb index 112821135f..10c5c1a66a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -11,8 +11,7 @@ module ActiveRecord predicate_builder.build(attribute, value.id) end - protected - + private attr_reader :predicate_builder end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb index 34db266f05..e8c9f60860 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -12,8 +12,7 @@ module ActiveRecord attribute.eq(bind) end - protected - + private attr_reader :predicate_builder end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb index e8e2f2c626..aae04d9348 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -17,27 +17,26 @@ module ActiveRecord end end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected + private attr_reader :associated_table, :values - private def type_to_ids_mapping default_hash = Hash.new { |hsh, key| hsh[key] = [] } - values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + values.each_with_object(default_hash) do |value, hash| + hash[klass(value).polymorphic_name] << convert_to_id(value) + end end def primary_key(value) - associated_table.association_join_primary_key(base_class(value)) + associated_table.association_join_primary_key(klass(value)) end - def base_class(value) + def klass(value) case value when Base - value.class.base_class + value.class when Relation - value.klass.base_class + value.klass end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb index 6d16579708..44bb2c7ab6 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -16,15 +16,16 @@ module ActiveRecord def call(attribute, value) begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin) end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end) - if value.begin.respond_to?(:infinite?) && value.begin.infinite? - if value.end.respond_to?(:infinite?) && value.end.infinite? + + if begin_bind.value.infinity? + if end_bind.value.infinity? attribute.not_in([]) elsif value.exclude_end? attribute.lt(end_bind) else attribute.lteq(end_bind) end - elsif value.end.respond_to?(:infinite?) && value.end.infinite? + elsif end_bind.value.infinity? attribute.gteq(begin_bind) elsif value.exclude_end? attribute.gteq(begin_bind).and(attribute.lt(end_bind)) @@ -33,8 +34,7 @@ module ActiveRecord end end - protected - + private attr_reader :predicate_builder end end diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index 3532f28858..f64bd30d38 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -21,6 +21,23 @@ module ActiveRecord !value_before_type_cast.is_a?(StatementCache::Substitute) && (value_before_type_cast.nil? || value_for_database.nil?) end + + def boundable? + return @_boundable if defined?(@_boundable) + nil? + @_boundable = true + rescue ::RangeError + @_boundable = false + end + + def infinity? + _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database) + end + + private + def _infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 86882c7ce7..4e60863e52 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -231,6 +231,7 @@ module ActiveRecord end def _select!(*fields) # :nodoc: + fields.reject!(&:blank?) fields.flatten! fields.map! do |field| klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field @@ -898,6 +899,11 @@ module ActiveRecord self end + def skip_preloading! # :nodoc: + self.skip_preloading_value = true + self + end + # Returns the Arel object associated with the relation. def arel(aliases = nil) # :nodoc: @arel ||= build_arel(aliases) diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 92b720068c..c1b3eea9df 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -26,8 +26,7 @@ module ActiveRecord WhereClause.new(parts) end - protected - + private attr_reader :klass, :predicate_builder end end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 59acd63a0f..b41d3504fd 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -114,8 +114,7 @@ module ActiveRecord end end - protected - + private attr_reader :query_builder, :bind_map, :klass end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 6dbc977f9a..3290675338 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -17,8 +17,8 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # - # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for - # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. + # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+ + # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. # Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access # using a symbol. diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index d3b9171159..b67479fb6a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -73,10 +73,7 @@ module ActiveRecord klass.reflect_on_aggregation(aggregation_name) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :klass, :arel_table, :association end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index ed589f9b43..af1bbc7e93 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -117,9 +117,9 @@ module ActiveRecord def create(*arguments) configuration = arguments.first class_for_adapter(configuration["adapter"]).new(*arguments).create - $stdout.puts "Created database '#{configuration['database']}'" + $stdout.puts "Created database '#{configuration['database']}'" if verbose? rescue DatabaseAlreadyExists - $stderr.puts "Database '#{configuration['database']}' already exists" + $stderr.puts "Database '#{configuration['database']}' already exists" if verbose? rescue Exception => error $stderr.puts error $stderr.puts "Couldn't create database for #{configuration.inspect}" @@ -144,7 +144,7 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration["adapter"]).new(*arguments).drop - $stdout.puts "Dropped database '#{configuration['database']}'" + $stdout.puts "Dropped database '#{configuration['database']}'" if verbose? rescue ActiveRecord::NoDatabaseError $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error @@ -166,9 +166,8 @@ module ActiveRecord def migrate check_target_version - verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true scope = ENV["SCOPE"] - verbose_was, Migration.verbose = Migration.verbose, verbose + verbose_was, Migration.verbose = Migration.verbose, verbose? Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end @@ -297,6 +296,9 @@ module ActiveRecord end private + def verbose? + ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true + end def class_for_adapter(adapter) _key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] } diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb new file mode 100644 index 0000000000..606a3b0fb5 --- /dev/null +++ b/activerecord/lib/active_record/test_databases.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "active_support/testing/parallelization" + +module ActiveRecord + module TestDatabases # :nodoc: + ActiveSupport::Testing::Parallelization.after_fork_hook do |i| + create_and_migrate(i, spec_name: Rails.env) + end + + ActiveSupport::Testing::Parallelization.run_cleanup_hook do |i| + drop(i, spec_name: Rails.env) + end + + def self.create_and_migrate(i, spec_name:) + old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" + + connection_spec = ActiveRecord::Base.configurations[spec_name] + + connection_spec["database"] += "-#{i}" + ActiveRecord::Tasks::DatabaseTasks.create(connection_spec) + ActiveRecord::Base.establish_connection(connection_spec) + ActiveRecord::Tasks::DatabaseTasks.migrate + ensure + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) + ENV["VERBOSE"] = old + end + + def self.drop(i, spec_name:) + old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" + connection_spec = ActiveRecord::Base.configurations[spec_name] + + ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec) + ensure + ENV["VERBOSE"] = old + end + end +end diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index e7468aa542..b300fdfa05 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -52,8 +52,6 @@ module ActiveRecord priority <=> other.priority end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :block, :adapter, :override @@ -114,13 +112,8 @@ module ActiveRecord super | 4 end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :options, :klass - private + attr_reader :options, :klass def matches_options?(**kwargs) options.all? do |key, value| diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index af4e4e37e2..7cf8181d8e 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -14,15 +14,10 @@ module ActiveRecord connection.type_cast_from_column(column, value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :table_name delegate :connection, to: :@klass - private - def column_for(attribute_name) if connection.schema_cache.data_source_exists?(table_name) connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index c5cfdba8b1..663cdadb03 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -13,10 +13,7 @@ module ActiveRecord type.serialize(value) end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - + private attr_reader :types end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 4c2c5dd852..5a1dbc8e53 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -23,7 +23,7 @@ module ActiveRecord relation = build_relation(finder_class, attribute, value) if record.persisted? if finder_class.primary_key - relation = relation.where.not(finder_class.primary_key => record.id_in_database || record.id) + relation = relation.where.not(finder_class.primary_key => record.id_in_database) else raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 856fcc5897..a07b00ef79 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -15,12 +15,8 @@ module ActiveRecord migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb") end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - attr_reader :migration_action, :join_tables - private + attr_reader :migration_action, :join_tables # Sets the default migration template that is being used for the generation of the migration. # Depending on command line arguments, the migration template and the table name instance diff --git a/activerecord/test/.gitignore b/activerecord/test/.gitignore deleted file mode 100644 index a0ec5967dd..0000000000 --- a/activerecord/test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/config.yml diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 0092146e09..4c41a68407 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -20,7 +20,7 @@ module ActiveRecord b = Book.create(name: "my \x00 book") b.reload assert_equal "my \x00 book", b.name - b.update_attributes(name: "my other \x00 book") + b.update(name: "my other \x00 book") b.reload assert_equal "my other \x00 book", b.name end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index d18fb97e05..0719baaa23 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -25,25 +25,25 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase end def test_columns_for_distinct_one_order - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", ["posts.created_at desc"]) end def test_columns_for_distinct_few_orders - assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1", + assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id", @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) end def test_columns_for_distinct_with_case assert_equal( - "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0", + "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"]) ) end def test_columns_for_distinct_blank_not_nil_orders - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) end @@ -52,7 +52,7 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase def order.to_sql "posts.created_at desc" end - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", [order]) end diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index f921515c10..52e283f247 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -13,7 +13,7 @@ module ActiveRecord setup do @abort, Thread.abort_on_exception = Thread.abort_on_exception, false - Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception @connection = ActiveRecord::Base.connection @connection.clear_cache! @@ -32,7 +32,7 @@ module ActiveRecord @connection.drop_table "samples", if_exists: true Thread.abort_on_exception = @abort - Thread.report_on_exception = @original_report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception = @original_report_on_exception end test "raises Deadlocked when a deadlock is encountered" do @@ -46,7 +46,7 @@ module ActiveRecord Sample.transaction do s1.lock! barrier.wait - s2.update_attributes value: 1 + s2.update value: 1 end end @@ -54,7 +54,7 @@ module ActiveRecord Sample.transaction do s2.lock! barrier.wait - s1.update_attributes value: 2 + s1.update value: 2 end ensure thread.join diff --git a/activerecord/test/cases/adapters/postgresql/date_test.rb b/activerecord/test/cases/adapters/postgresql/date_test.rb new file mode 100644 index 0000000000..a86abac2be --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/date_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/topic" + +class PostgresqlDateTest < ActiveRecord::PostgreSQLTestCase + def test_load_infinity_and_beyond + topic = Topic.find_by_sql("SELECT 'infinity'::date AS last_read").first + assert topic.last_read.infinite?, "timestamp should be infinite" + assert_operator topic.last_read, :>, 0 + + topic = Topic.find_by_sql("SELECT '-infinity'::date AS last_read").first + assert topic.last_read.infinite?, "timestamp should be infinite" + assert_operator topic.last_read, :<, 0 + end + + def test_save_infinity_and_beyond + topic = Topic.create!(last_read: 1.0 / 0.0) + assert_equal(1.0 / 0.0, topic.last_read) + + topic = Topic.create!(last_read: -1.0 / 0.0) + assert_equal(-1.0 / 0.0, topic.last_read) + end + + def test_bc_date + date = Date.new(0) - 1.week + topic = Topic.create!(last_read: date) + assert_equal date, Topic.find(topic.id).last_read + end + + def test_bc_date_leap_year + date = Time.utc(-4, 2, 29).to_date + topic = Topic.create!(last_read: date) + assert_equal date, Topic.find(topic.id).last_read + end + + def test_bc_date_year_zero + date = Time.utc(0, 4, 7).to_date + topic = Topic.create!(last_read: date) + assert_equal date, Topic.find(topic.id).last_read + end +end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 0b18c0c9d7..5e56ce8427 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -13,6 +13,7 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase @connection.create_table(:postgresql_infinities) do |t| t.float :float t.datetime :datetime + t.date :date end end @@ -43,11 +44,25 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase end test "type casting infinity on a datetime column" do + record = PostgresqlInfinity.create!(datetime: "infinity") + record.reload + assert_equal Float::INFINITY, record.datetime + record = PostgresqlInfinity.create!(datetime: Float::INFINITY) record.reload assert_equal Float::INFINITY, record.datetime end + test "type casting infinity on a date column" do + record = PostgresqlInfinity.create!(date: "infinity") + record.reload + assert_equal Float::INFINITY, record.date + + record = PostgresqlInfinity.create!(date: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.date + end + test "update_all with infinity on a datetime column" do record = PostgresqlInfinity.create! PostgresqlInfinity.update_all(datetime: Float::INFINITY) @@ -68,4 +83,28 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase PostgresqlInfinity.reset_column_information end end + + test "where clause with infinite range on a datetime column" do + record = PostgresqlInfinity.create!(datetime: Time.current) + + string = PostgresqlInfinity.where(datetime: "-infinity".."infinity") + assert_equal record, string.take + + infinity = PostgresqlInfinity.where(datetime: -::Float::INFINITY..::Float::INFINITY) + assert_equal record, infinity.take + + assert_equal infinity.to_sql, string.to_sql + end + + test "where clause with infinite range on a date column" do + record = PostgresqlInfinity.create!(date: Date.current) + + string = PostgresqlInfinity.where(date: "-infinity".."infinity") + assert_equal record, string.take + + infinity = PostgresqlInfinity.where(date: -::Float::INFINITY..::Float::INFINITY) + assert_equal record, infinity.take + + assert_equal infinity.to_sql, string.to_sql + end end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 1951230c8a..cbb6cd42b5 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -263,25 +263,25 @@ module ActiveRecord end def test_columns_for_distinct_one_order - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @connection.columns_for_distinct("posts.id", ["posts.created_at desc"]) end def test_columns_for_distinct_few_orders - assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1", + assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id", @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) end def test_columns_for_distinct_with_case assert_equal( - "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0", + "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id", @connection.columns_for_distinct("posts.id", ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"]) ) end def test_columns_for_distinct_blank_not_nil_orders - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) end @@ -290,23 +290,23 @@ module ActiveRecord def order.to_sql "posts.created_at desc" end - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @connection.columns_for_distinct("posts.id", [order]) end def test_columns_for_distinct_with_nulls - assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"]) - assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"]) + assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"]) + assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"]) end def test_columns_for_distinct_without_order_specifiers - assert_equal "posts.title, posts.updater_id AS alias_0", + assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id"]) - assert_equal "posts.title, posts.updater_id AS alias_0", + assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"]) - assert_equal "posts.title, posts.updater_id AS alias_0", + assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"]) end diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index 9821b103df..984b2f5ea4 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -14,7 +14,7 @@ module ActiveRecord setup do @abort, Thread.abort_on_exception = Thread.abort_on_exception, false - Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception @connection = ActiveRecord::Base.connection @@ -32,7 +32,7 @@ module ActiveRecord @connection.drop_table "samples", if_exists: true Thread.abort_on_exception = @abort - Thread.report_on_exception = @original_report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception = @original_report_on_exception end test "raises SerializationFailure when a serialization failure occurs" do @@ -76,7 +76,7 @@ module ActiveRecord Sample.transaction do s1.lock! barrier.wait - s2.update_attributes value: 1 + s2.update value: 1 end end @@ -84,7 +84,7 @@ module ActiveRecord Sample.transaction do s2.lock! barrier.wait - s1.update_attributes value: 2 + s1.update value: 2 end ensure thread.join diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 6fdb353368..1c85ff5674 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -55,4 +55,11 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) end + + def test_quoted_time_normalizes_date_qualified_time + value = ::Time.utc(2018, 3, 11, 12, 30, 0, 999999) + type = ActiveRecord::Type::Time.new + + assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) + end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 9e6d94191b..6b4f826766 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -931,6 +931,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal error.message, "The :dependent option must be one of [:destroy, :delete], but is :nullify" end + class DestroyableBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "UndestroyableAuthor", dependent: :destroy + end + + class UndestroyableAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "DestroyableBook", foreign_key: "author_id" + before_destroy :dont + + def dont + throw(:abort) + end + end + + def test_dependency_should_halt_parent_destruction + author = UndestroyableAuthor.create!(name: "Test") + book = DestroyableBook.create!(author: author) + + assert_no_difference ["UndestroyableAuthor.count", "DestroyableBook.count"] do + assert_not book.destroy + end + end + def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause new_firm = accounts(:signals37).build_firm(name: "Apple") assert_equal new_firm.name, "Apple" diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index 8754889143..5fca972aee 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -8,37 +8,81 @@ module Namespaced class Post < ActiveRecord::Base self.table_name = "posts" has_one :tagging, as: :taggable, class_name: "Tagging" + + def self.polymorphic_name + sti_name + end end end -class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase +module PolymorphicFullStiClassNamesSharedTest def setup + @old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = store_full_sti_class + post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1) @tagging = Tagging.create(taggable: post) - @old = ActiveRecord::Base.store_full_sti_class end def teardown - ActiveRecord::Base.store_full_sti_class = @old + ActiveRecord::Base.store_full_sti_class = @old_store_full_sti_class + end + + def test_class_names + ActiveRecord::Base.store_full_sti_class = !store_full_sti_class + post = Namespaced::Post.find_by_title("Great stuff") + assert_nil post.tagging + + ActiveRecord::Base.store_full_sti_class = store_full_sti_class + post = Namespaced::Post.find_by_title("Great stuff") + assert_equal @tagging, post.tagging end def test_class_names_with_includes - ActiveRecord::Base.store_full_sti_class = false + ActiveRecord::Base.store_full_sti_class = !store_full_sti_class post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff") assert_nil post.tagging - ActiveRecord::Base.store_full_sti_class = true + ActiveRecord::Base.store_full_sti_class = store_full_sti_class post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff") assert_equal @tagging, post.tagging end def test_class_names_with_eager_load - ActiveRecord::Base.store_full_sti_class = false + ActiveRecord::Base.store_full_sti_class = !store_full_sti_class post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff") assert_nil post.tagging - ActiveRecord::Base.store_full_sti_class = true + ActiveRecord::Base.store_full_sti_class = store_full_sti_class post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff") assert_equal @tagging, post.tagging end + + def test_class_names_with_find_by + post = Namespaced::Post.find_by_title("Great stuff") + + ActiveRecord::Base.store_full_sti_class = !store_full_sti_class + assert_nil Tagging.find_by(taggable: post) + + ActiveRecord::Base.store_full_sti_class = store_full_sti_class + assert_equal @tagging, Tagging.find_by(taggable: post) + end +end + +class PolymorphicFullStiClassNamesTest < ActiveRecord::TestCase + include PolymorphicFullStiClassNamesSharedTest + + private + def store_full_sti_class + true + end +end + +class PolymorphicNonFullStiClassNamesTest < ActiveRecord::TestCase + include PolymorphicFullStiClassNamesSharedTest + + private + def store_full_sti_class + false + end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index cb07ca5cd3..f18c6177ac 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1501,8 +1501,10 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal posts(:welcome), post end - test "eager-loading with a polymorphic association and using the existential predicate" do - assert_equal true, authors(:david).essays.eager_load(:writer).exists? + test "eager-loading with a polymorphic association won't work consistently" do + assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).to_a } + assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).count } + assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).exists? } end # CollectionProxy#reader is expensive, so the preloader avoids calling it. 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 d9e05cf7e2..5f771fe85f 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 @@ -662,7 +662,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_includes developer.sym_special_projects, sp end - def test_update_attributes_after_push_without_duplicate_join_table_rows + def test_update_columns_after_push_without_duplicate_join_table_rows developer = Developer.new("name" => "Kano") project = SpecialProject.create("name" => "Special Project") assert developer.save diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 9ee42cef0f..00821f2319 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1211,20 +1211,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_calling_update_attributes_on_id_changes_the_counter_cache + def test_calling_update_on_id_changes_the_counter_cache topic = Topic.order("id ASC").first original_count = topic.replies.to_a.size assert_equal original_count, topic.replies_count first_reply = topic.replies.first - first_reply.update_attributes(parent_id: nil) + first_reply.update(parent_id: nil) assert_equal original_count - 1, topic.reload.replies_count - first_reply.update_attributes(parent_id: topic.id) + first_reply.update(parent_id: topic.id) assert_equal original_count, topic.reload.replies_count end - def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache + def test_calling_update_changing_ids_doesnt_change_counter_cache topic1 = Topic.find(1) topic2 = Topic.find(3) original_count1 = topic1.replies.to_a.size @@ -1233,11 +1233,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase reply1 = topic1.replies.first reply2 = topic2.replies.first - reply1.update_attributes(parent_id: topic2.id) + reply1.update(parent_id: topic2.id) assert_equal original_count1 - 1, topic1.reload.replies_count assert_equal original_count2 + 1, topic2.reload.replies_count - reply2.update_attributes(parent_id: topic1.id) + reply2.update(parent_id: topic1.id) assert_equal original_count1, topic1.reload.replies_count assert_equal original_count2, topic2.reload.replies_count end @@ -1842,6 +1842,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase ].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) } end + def test_associations_order_should_be_priority_over_throughs_order + david = authors(:david) + expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1] + assert_equal expected, david.comments_desc.map(&:id) + assert_equal expected, Author.includes(:comments_desc).find(david.id).comments_desc.map(&:id) + end + def test_dynamic_find_should_respect_association_order_for_through assert_equal Comment.find(10), authors(:david).comments_desc.where("comments.type = 'SpecialComment'").first assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type("SpecialComment") 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 25118b26f5..3b3d4037b9 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -46,6 +46,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create person_id: 0, post_id: 0 end + def test_marshal_dump + preloaded = Post.includes(:first_blue_tags).first + assert_equal preloaded, Marshal.load(Marshal.dump(preloaded)) + end + def test_preload_sti_rhs_class developers = Developer.includes(:firms).all.to_a assert_no_queries do diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 1a213ef7e4..602fe52701 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -725,4 +725,28 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_not DestroyByParentBook.exists?(book.id) end + + class UndestroyableBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "DestroyableAuthor" + before_destroy :dont + + def dont + throw(:abort) + end + end + + class DestroyableAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "UndestroyableBook", foreign_key: "author_id", dependent: :destroy + end + + def test_dependency_should_halt_parent_destruction + author = DestroyableAuthor.create!(name: "Test") + UndestroyableBook.create!(author: author) + + assert_no_difference ["DestroyableAuthor.count", "UndestroyableBook.count"] do + assert_not author.destroy + end + end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index bad1bcdb67..896bf574f4 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -190,6 +190,16 @@ class InverseAssociationTests < ActiveRecord::TestCase assert_nil belongs_to_ref.inverse_of end + def test_polymorphic_associations_dont_attempt_to_find_inverse_of + belongs_to_ref = Sponsor.reflect_on_association(:sponsor) + assert_raise(ArgumentError) { belongs_to_ref.klass } + assert_nil belongs_to_ref.inverse_of + + belongs_to_ref = Face.reflect_on_association(:human) + assert_raise(ArgumentError) { belongs_to_ref.klass } + assert_nil belongs_to_ref.inverse_of + end + def test_this_inverse_stuff firm = Firm.create!(name: "Adequate Holdings") Project.create!(name: "Project 1", firm: firm) diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb index 7b5c394177..0e54e8c1b0 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -5,6 +5,7 @@ require "models/post" require "models/comment" require "models/author" require "models/essay" +require "models/category" require "models/categorization" require "models/person" diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 09684520d1..b8243d148a 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -50,7 +50,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase } u = person.create!(first_name: "cool") - u.update_attributes!(first_name: "nah") # still valid because validation only applies on 'create' + u.update!(first_name: "nah") # still valid because validation only applies on 'create' assert_predicate reference.create!(person: u), :persisted? end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 983a3d366a..7dfb05a6a5 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1501,7 +1501,7 @@ class BasicsTest < ActiveRecord::TestCase query = Developer.from("developers").to_sql quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}" - assert_match(/SELECT #{quoted_id}.* FROM developers/, query) + assert_match(/SELECT #{Regexp.escape(quoted_id)}.* FROM developers/, query) end test "using table name qualified column names unless having SELECT list explicitly" do diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index ad701fb200..c8163901c6 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -508,7 +508,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches Post.update_all(author_id: 0) person = Post.last - person.update_attributes(author_id: 1) + person.update(author_id: 1) Post.in_batches(of: 2) do |batch| batch.where("author_id >= 1").update_all("author_id = author_id + 1") diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 82b15e565b..080d2a54bc 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -242,6 +242,12 @@ class CalculationsTest < ActiveRecord::TestCase assert_queries(1) { assert_equal 11, posts.count(:all) } end + def test_count_with_eager_loading_and_custom_order_and_distinct + posts = Post.includes(:comments).order("comments.id").distinct + assert_queries(1) { assert_equal 11, posts.count } + assert_queries(1) { assert_equal 11, posts.count(:all) } + end + def test_distinct_count_all_with_custom_select_and_order accounts = Account.distinct.select("credit_limit % 10").order(Arel.sql("credit_limit % 10")) assert_queries(1) { assert_equal 3, accounts.count(:all) } @@ -793,6 +799,23 @@ class CalculationsTest < ActiveRecord::TestCase end end + def test_pick_one + assert_equal "The First Topic", Topic.order(:id).pick(:heading) + assert_nil Topic.none.pick(:heading) + assert_nil Topic.where("1=0").pick(:heading) + end + + def test_pick_two + assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address) + assert_nil Topic.none.pick(:author_name, :author_email_address) + assert_nil Topic.where("1=0").pick(:author_name, :author_email_address) + end + + def test_pick_delegate_to_all + cool_first = minivans(:cool_first) + assert_equal cool_first.color, Minivan.pick(:color) + end + def test_grouped_calculation_with_polymorphic_relation part = ShipPart.create!(name: "has trinket") part.trinkets.create! diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index f4cc251fb9..c06a4e2c52 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -71,6 +71,54 @@ module ActiveRecord ENV["RAILS_ENV"] = previous_env end + unless in_memory_db? + def test_establish_connection_using_3_level_config_defaults_to_default_env_primary_db + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + }, + "another_env" => { + "primary" => { "adapter" => "sqlite3", "database" => "db/another-primary.sqlite3" }, + "readonly" => { "adapter" => "sqlite3", "database" => "db/another-readonly.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.establish_connection + + assert_equal "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ENV["RAILS_ENV"] = previous_env + ActiveRecord::Base.establish_connection(:arunit) + end + + def test_establish_connection_using_2_level_config_defaults_to_default_env_primary_db + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "adapter" => "sqlite3", "database" => "db/primary.sqlite3" + }, + "another_env" => { + "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3" + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.establish_connection + + assert_equal "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ENV["RAILS_ENV"] = previous_env + ActiveRecord::Base.establish_connection(:arunit) + end + end + def test_establish_connection_using_two_level_configurations config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } } @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index bfaaa3c54e..9ac03629c3 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -469,7 +469,7 @@ module ActiveRecord end def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns - Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout [:disconnect, :clear_reloadable_connections].each do |group_action_method| @pool.with_connection do |connection| @@ -479,7 +479,7 @@ module ActiveRecord end end ensure - Thread.report_on_exception = original_report_on_exception if Thread.respond_to?(:report_on_exception) + Thread.report_on_exception = original_report_on_exception end def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 356afdbd2b..6e7ae2efb4 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -4,7 +4,6 @@ require "cases/helper" require "models/person" require "models/topic" require "pp" -require "active_support/core_ext/string/strip" class NonExistentTable < ActiveRecord::Base; end @@ -39,26 +38,26 @@ class CoreTest < ActiveRecord::TestCase topic = Topic.new actual = "".dup PP.pp(topic, StringIO.new(actual)) - expected = <<-PRETTY.strip_heredoc - #<Topic:0xXXXXXX - id: nil, - title: nil, - author_name: nil, - author_email_address: "test@test.com", - written_on: nil, - bonus_time: nil, - last_read: nil, - content: nil, - important: nil, - approved: true, - replies_count: 0, - unique_replies_count: 0, - parent_id: nil, - parent_title: nil, - type: nil, - group: nil, - created_at: nil, - updated_at: nil> + expected = <<~PRETTY + #<Topic:0xXXXXXX + id: nil, + title: nil, + author_name: nil, + author_email_address: "test@test.com", + written_on: nil, + bonus_time: nil, + last_read: nil, + content: nil, + important: nil, + approved: true, + replies_count: 0, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: nil, + updated_at: nil> PRETTY assert actual.start_with?(expected.split("XXXXXX").first) assert actual.end_with?(expected.split("XXXXXX").last) @@ -68,26 +67,26 @@ class CoreTest < ActiveRecord::TestCase topic = topics(:first) actual = "".dup PP.pp(topic, StringIO.new(actual)) - expected = <<-PRETTY.strip_heredoc - #<Topic:0x\\w+ - id: 1, - title: "The First Topic", - author_name: "David", - author_email_address: "david@loudthinking.com", - written_on: 2003-07-16 14:28:11 UTC, - bonus_time: 2000-01-01 14:28:00 UTC, - last_read: Thu, 15 Apr 2004, - content: "Have a nice day", - important: nil, - approved: false, - replies_count: 1, - unique_replies_count: 0, - parent_id: nil, - parent_title: nil, - type: nil, - group: nil, - created_at: [^,]+, - updated_at: [^,>]+> + expected = <<~PRETTY + #<Topic:0x\\w+ + id: 1, + title: "The First Topic", + author_name: "David", + author_email_address: "david@loudthinking.com", + written_on: 2003-07-16 14:28:11 UTC, + bonus_time: 2000-01-01 14:28:00 UTC, + last_read: Thu, 15 Apr 2004, + content: "Have a nice day", + important: nil, + approved: false, + replies_count: 1, + unique_replies_count: 0, + parent_id: nil, + parent_title: nil, + type: nil, + group: nil, + created_at: [^,]+, + updated_at: [^,>]+> PRETTY assert_match(/\A#{expected}\z/, actual) end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 51f6164138..e64a8372d0 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -27,6 +27,24 @@ if subsecond_precision_supported? assert_equal 5, Foo.columns_hash["updated_at"].precision end + def test_datetime_precision_is_truncated_on_assignment + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :created_at, :datetime, precision: 0 + @connection.add_column :foos, :updated_at, :datetime, precision: 6 + + time = ::Time.now.change(nsec: 123456789) + foo = Foo.new(created_at: time, updated_at: time) + + assert_equal 0, foo.created_at.nsec + assert_equal 123456000, foo.updated_at.nsec + + foo.save! + foo.reload + + assert_equal 0, foo.created_at.nsec + assert_equal 123456000, foo.updated_at.nsec + end + def test_timestamps_helper_with_custom_precision @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 14c4e3cbd4..2f1434b2bc 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -736,6 +736,24 @@ class DirtyTest < ActiveRecord::TestCase assert record.save end + test "virtual attributes are not written with partial_writes off" do + with_partial_writes(ActiveRecord::Base, false) do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "people" + attribute :non_persisted_attribute, :string + end + + record = klass.new(first_name: "Sean") + record.non_persisted_attribute_will_change! + + assert record.save + + record.non_persisted_attribute_will_change! + + assert record.save + end + end + test "mutating and then assigning doesn't remove the change" do pirate = Pirate.create!(catchphrase: "arrrr") pirate.catchphrase << " matey!" diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 17654027a9..a0e75f4e89 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -2,7 +2,6 @@ require "cases/helper" require "models/car" -require "active_support/core_ext/string/strip" if ActiveRecord::Base.connection.supports_explain? class ExplainTest < ActiveRecord::TestCase @@ -53,7 +52,7 @@ if ActiveRecord::Base.connection.supports_explain? queries = sqls.zip(binds) stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do - expected = <<-SQL.strip_heredoc + expected = <<~SQL EXPLAIN for: #{sqls[0]} [["wadus", 1]] query plan foo diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index b413212e26..d85910d9c6 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -812,6 +812,15 @@ class FinderTest < ActiveRecord::TestCase assert_equal [1, 2, 6, 7, 8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort end + def test_find_on_hash_conditions_with_open_ended_range + assert_equal [1, 2, 3], Comment.where(id: Float::INFINITY..3).to_a.map(&:id).sort + end + + def test_find_on_hash_conditions_with_numeric_range_for_string + topic = Topic.create!(title: "12 Factor App") + assert_equal [topic], Topic.where(title: 10..2).to_a + end + def test_find_on_multiple_hash_conditions assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } @@ -1180,6 +1189,11 @@ class FinderTest < ActiveRecord::TestCase order("author_addresses_authors.id DESC").limit(3).to_a.size end + def test_find_with_eager_loading_collection_and_ordering_by_collection_primary_key + assert_equal Post.first, Post.eager_load(comments: :ratings). + order("posts.id, ratings.id, comments.id").first + end + def test_find_with_nil_inside_set_passed_for_one_attribute client_of = Company. where(client_of: [2, 1, nil], diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index c88b90c8eb..184b750161 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -86,7 +86,7 @@ class FixturesTest < ActiveRecord::TestCase create_fixtures("bulbs", "authors", "computers") - expected_sql = <<-EOS.strip_heredoc.chop + expected_sql = <<~EOS.chop INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("bulbs")} .* INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("authors")} .* INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("computers")} .* @@ -1186,7 +1186,7 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase end def test_table_name_is_defined_in_the_model - assert_equal "randomly_named_table2", ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name + assert_equal "randomly_named_table2", ActiveRecord::FixtureSet.all_loaded_fixtures["admin/randomly_named_a9"].table_name assert_equal "randomly_named_table2", Admin::ClassNameThatDoesNotFollowCONVENTIONS1.table_name end end diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb index c60a276850..9b79803503 100644 --- a/activerecord/test/cases/json_shared_test_cases.rb +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -101,7 +101,7 @@ module JSONSharedTestCases x = klass.where(payload: nil).first assert_nil(x) - json.update_attributes(payload: nil) + json.update(payload: nil) x = klass.where(payload: nil).first assert_equal(json.reload, x) end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index bb76137ef5..9d04750712 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -194,6 +194,45 @@ class OptimisticLockingTest < ActiveRecord::TestCase end end + def test_update_with_dirty_primary_key + assert_raises(ActiveRecord::RecordNotUnique) do + person = Person.find(1) + person.id = 2 + person.save! + end + + person = Person.find(1) + person.id = 42 + person.save! + + assert Person.find(42) + assert_raises(ActiveRecord::RecordNotFound) do + Person.find(1) + end + end + + def test_delete_with_dirty_primary_key + person = Person.find(1) + person.id = 2 + person.delete + + assert Person.find(2) + assert_raises(ActiveRecord::RecordNotFound) do + Person.find(1) + end + end + + def test_destroy_with_dirty_primary_key + person = Person.find(1) + person.id = 2 + person.destroy + + assert Person.find(2) + assert_raises(ActiveRecord::RecordNotFound) do + Person.find(1) + end + end + def test_explicit_update_lock_column_raise_error person = Person.find(1) @@ -415,7 +454,9 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 1, car.lock_version previously_car_updated_at = car.updated_at - car.wheels.first.update(size: 42) + travel(1.day) do + car.wheels.first.update(size: 42) + end assert_equal 1, car.reload.wheels_count assert_not_equal previously_car_updated_at, car.updated_at diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 8514ccd55b..a9deb92585 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -88,7 +88,7 @@ module ActiveRecord assert connection.table_exists? :felines assert_not connection.table_exists? :cats - primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0] + primary_key_name = connection.select_values(<<~SQL, "SCHEMA")[0] SELECT c.relname FROM pg_class c JOIN pg_index i diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index a4b8664a85..f77a275544 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -177,7 +177,7 @@ class MigrationTest < ActiveRecord::TestCase assert BigNumber.create( bank_balance: 1586.43, big_bank_balance: BigDecimal("1000234000567.95"), - world_population: 6000000000, + world_population: 2**62, my_house_population: 3, value_of_e: BigDecimal("2.7182818284590452353602875") ) @@ -191,10 +191,8 @@ class MigrationTest < ActiveRecord::TestCase assert_not_nil b.my_house_population assert_not_nil b.value_of_e - # TODO: set world_population >= 2**62 to cover 64-bit platforms and test - # is_a?(Bignum) assert_kind_of Integer, b.world_population - assert_equal 6000000000, b.world_population + assert_equal 2**62, b.world_population assert_kind_of Integer, b.my_house_population assert_equal 3, b.my_house_population assert_kind_of BigDecimal, b.bank_balance @@ -693,6 +691,25 @@ class MigrationTest < ActiveRecord::TestCase assert_no_column Person, :last_name, "without an advisory lock, the Migrator should not make any changes, but it did." end + + def test_with_advisory_lock_raises_the_right_error_when_it_fails_to_release_lock + migration = Class.new(ActiveRecord::Migration::Current).new + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + e = assert_raises(ActiveRecord::ConcurrentMigrationError) do + silence_stream($stderr) do + migrator.send(:with_advisory_lock) do + ActiveRecord::Base.connection.release_advisory_lock(lock_id) + end + end + end + + assert_match( + /#{ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE}/, + e.message + ) + end end private diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb index f917c8f727..14db63890e 100644 --- a/activerecord/test/cases/numeric_data_test.rb +++ b/activerecord/test/cases/numeric_data_test.rb @@ -19,7 +19,7 @@ class NumericDataTest < ActiveRecord::TestCase m = NumericData.new( bank_balance: 1586.43, big_bank_balance: BigDecimal("1000234000567.95"), - world_population: 6000000000, + world_population: 2**62, my_house_population: 3 ) assert m.save @@ -27,11 +27,8 @@ class NumericDataTest < ActiveRecord::TestCase m1 = NumericData.find(m.id) assert_not_nil m1 - # As with migration_test.rb, we should make world_population >= 2**62 - # to cover 64-bit platforms and test it is a Bignum, but the main thing - # is that it's an Integer. assert_kind_of Integer, m1.world_population - assert_equal 6000000000, m1.world_population + assert_equal 2**62, m1.world_population assert_kind_of Integer, m1.my_house_population assert_equal 3, m1.my_house_population @@ -47,7 +44,7 @@ class NumericDataTest < ActiveRecord::TestCase m = NumericData.new( bank_balance: 1586.43122334, big_bank_balance: BigDecimal("234000567.952344"), - world_population: 6000000000, + world_population: 2**62, my_house_population: 3 ) assert m.save @@ -55,11 +52,8 @@ class NumericDataTest < ActiveRecord::TestCase m1 = NumericData.find(m.id) assert_not_nil m1 - # As with migration_test.rb, we should make world_population >= 2**62 - # to cover 64-bit platforms and test it is a Bignum, but the main thing - # is that it's an Integer. assert_kind_of Integer, m1.world_population - assert_equal 6000000000, m1.world_population + assert_equal 2**62, m1.world_population assert_kind_of Integer, m1.my_house_population assert_equal 3, m1.my_house_population diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index e8052914d4..e4a65a48ca 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -251,6 +251,13 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal "The First Topic", topics(:first).becomes(Reply).title end + def test_becomes_after_reload_schema_from_cache + Reply.define_attribute_methods + Reply.serialize(:content) # invoke reload_schema_from_cache + assert_kind_of Reply, topics(:first).becomes(Reply) + assert_equal "The First Topic", topics(:first).becomes(Reply).title + end + def test_becomes_includes_errors company = Company.new(name: nil) assert_not_predicate company, :valid? @@ -518,7 +525,7 @@ class PersistenceTest < ActiveRecord::TestCase 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_attributes(title: "Another New Topic") } + assert_queries(0) { assert topic.update(title: "Another New Topic") } end def test_delete @@ -927,42 +934,33 @@ class PersistenceTest < ActiveRecord::TestCase topic.reload assert_not_predicate topic, :approved? assert_equal "The First Topic", topic.title - end - - def test_update_attributes - topic = Topic.find(1) - assert_not_predicate topic, :approved? - assert_equal "The First Topic", topic.title - - topic.update_attributes("approved" => true, "title" => "The First Topic Updated") - topic.reload - assert_predicate topic, :approved? - assert_equal "The First Topic Updated", topic.title - - topic.update_attributes(approved: false, title: "The First Topic") - topic.reload - assert_not_predicate topic, :approved? - assert_equal "The First Topic", topic.title error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do - topic.update_attributes(id: 3, title: "Hm is it possible?") + topic.update(id: 3, title: "Hm is it possible?") end assert_not_nil error.cause assert_not_equal "Hm is it possible?", Topic.find(3).title - topic.update_attributes(id: 1234) + topic.update(id: 1234) assert_nothing_raised { topic.reload } assert_equal topic.title, Topic.find(1234).title end - def test_update_attributes_parameters + def test_update_attributes + topic = Topic.find(1) + assert_deprecated do + topic.update_attributes("title" => "The First Topic Updated") + end + end + + def test_update_parameters topic = Topic.find(1) assert_nothing_raised do - topic.update_attributes({}) + topic.update({}) end assert_raises(ArgumentError) do - topic.update_attributes(nil) + topic.update(nil) end end @@ -988,24 +986,10 @@ class PersistenceTest < ActiveRecord::TestCase end def test_update_attributes! - Reply.validates_presence_of(:title) reply = Reply.find(2) - assert_equal "The Second Topic of the day", reply.title - assert_equal "Have a nice day", reply.content - - reply.update_attributes!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening") - reply.reload - assert_equal "The Second Topic of the day updated", reply.title - assert_equal "Have a nice evening", reply.content - - reply.update_attributes!(title: "The Second Topic of the day", content: "Have a nice day") - reply.reload - assert_equal "The Second Topic of the day", reply.title - assert_equal "Have a nice day", reply.content - - assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") } - ensure - Reply.clear_validators! + assert_deprecated do + reply.update_attributes!("title" => "The Second Topic of the day updated") + end end def test_destroyed_returns_boolean diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 8d37f5ea95..d635a47c0e 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -320,6 +320,17 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_cache_is_available_when_connection_is_connected + conf = ActiveRecord::Base.configurations + + ActiveRecord::Base.configurations = {} + Task.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + end + ensure + ActiveRecord::Base.configurations = conf + end + def test_cache_is_available_when_using_a_not_connected_connection skip "In-Memory DB can't test for using a not connected connection" if in_memory_db? with_temporary_connection_pool do @@ -366,7 +377,7 @@ class QueryCacheTest < ActiveRecord::TestCase post = Post.first Post.transaction do - post.update_attributes(title: "rollback") + post.update(title: "rollback") assert_equal 1, Post.where(title: "rollback").to_a.count raise ActiveRecord::Rollback end @@ -379,7 +390,7 @@ class QueryCacheTest < ActiveRecord::TestCase begin Post.transaction do - post.update_attributes(title: "rollback") + post.update(title: "rollback") assert_equal 1, Post.where(title: "rollback").to_a.count raise "broken" end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 6534770c57..92eb0c814f 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -46,27 +46,60 @@ module ActiveRecord assert_equal t.to_s(:db), @quoter.quoted_date(t) end - def test_quoted_time_utc + def test_quoted_timestamp_utc with_timezone_config default: :utc do t = Time.now.change(usec: 0) assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end end - def test_quoted_time_local + def test_quoted_timestamp_local with_timezone_config default: :local do t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end - def test_quoted_time_crazy + def test_quoted_timestamp_crazy with_timezone_config default: :asdfasdf do t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end + def test_quoted_time_utc + with_timezone_config default: :utc do + t = Time.now.change(usec: 0) + + expected = t.getutc.change(year: 2000, month: 1, day: 1) + expected = expected.to_s(:db).sub("2000-01-01 ", "") + + assert_equal expected, @quoter.quoted_time(t) + end + end + + def test_quoted_time_local + with_timezone_config default: :local do + t = Time.now.change(usec: 0) + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "") + + assert_equal expected, @quoter.quoted_time(t) + end + end + + def test_quoted_time_crazy + with_timezone_config default: :asdfasdf do + t = Time.now.change(usec: 0) + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "") + + assert_equal expected, @quoter.quoted_time(t) + end + end + def test_quoted_datetime_utc with_timezone_config default: :utc do t = Time.now.change(usec: 0).to_datetime diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 2696d1bb00..3f3d41980c 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -54,4 +54,32 @@ module ActiveRecord Comment.all end end + + class QueryingMethodsDelegationTest < ActiveRecord::TestCase + QUERYING_METHODS = [ + :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, + :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, + :first_or_create, :first_or_create!, :first_or_initialize, + :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, + :find_by, :find_by!, + :destroy_all, :delete_all, :update_all, + :find_each, :find_in_batches, :in_batches, + :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, + :having, :create_with, :distinct, :references, :none, :unscope, :merge, + :count, :average, :minimum, :maximum, :sum, :calculate, + :pluck, :pick, :ids, + ] + + def test_delegate_querying_methods + klass = Class.new(ActiveRecord::Base) do + self.table_name = "posts" + end + + QUERYING_METHODS.each do |method| + assert_respond_to klass.all, method + assert_respond_to klass, method + end + end + end end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 1428b3e132..d6351bfe88 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -59,7 +59,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :skip_query_cache]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :skip_query_cache, :skip_preloading]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") @@ -137,6 +137,11 @@ module ActiveRecord assert relation.skip_query_cache_value end + test "skip_preloading!" do + relation.skip_preloading! + assert relation.skip_preloading_value + end + private def relation @relation ||= Relation.new(FakeKlass) diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb new file mode 100644 index 0000000000..0577e6bfdb --- /dev/null +++ b/activerecord/test/cases/relation/select_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" + +module ActiveRecord + class SelectTest < ActiveRecord::TestCase + fixtures :posts + + def test_select_with_nil_agrument + expected = Post.select(:title).to_sql + assert_equal expected, Post.select(nil).select(:title).to_sql + end + end +end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index bc7c54dbe0..cf65789b97 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -22,6 +22,7 @@ require "models/reader" require "models/category" require "models/categorization" require "models/edge" +require "models/subscriber" class RelationTest < ActiveRecord::TestCase fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans @@ -975,6 +976,12 @@ class RelationTest < ActiveRecord::TestCase assert_queries(1) { assert_equal 11, posts.load.size } end + def test_size_with_eager_loading_and_custom_order_and_distinct + posts = Post.includes(:comments).order("comments.id").distinct + assert_queries(1) { assert_equal 11, posts.size } + assert_queries(1) { assert_equal 11, posts.load.size } + end + def test_update_all_with_scope tag = Tag.first Post.tagged_with(tag.id).update_all title: "rofl" @@ -1349,6 +1356,34 @@ class RelationTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") } end + def test_create_or_find_by + assert_nil Subscriber.find_by(nick: "bob") + + subscriber = Subscriber.create!(nick: "bob") + + assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob") + assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat") + end + + def test_create_or_find_by_with_non_unique_attributes + Subscriber.create!(nick: "bob", name: "the builder") + + assert_raises(ActiveRecord::RecordNotFound) do + Subscriber.create_or_find_by(nick: "bob", name: "the cat") + end + end + + def test_create_or_find_by_within_transaction + assert_nil Subscriber.find_by(nick: "bob") + + subscriber = Subscriber.create!(nick: "bob") + + Subscriber.transaction do + assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob") + assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat") + end + end + def test_find_or_initialize_by assert_nil Bird.find_by(name: "bob") diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index a612ce9bb2..50d766a99e 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -298,7 +298,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_expression_indices index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip - assert_equal 't.index "lower((name)::text)", name: "company_expression_index"', index_definition + assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition end def test_schema_dump_interval_type diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 41455637bb..086500de38 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -27,6 +27,24 @@ if subsecond_precision_supported? assert_equal 6, Foo.columns_hash["finish"].precision end + def test_time_precision_is_truncated_on_assignment + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :start, :time, precision: 0 + @connection.add_column :foos, :finish, :time, precision: 6 + + time = ::Time.now.change(nsec: 123456789) + foo = Foo.new(start: time, finish: time) + + assert_equal 0, foo.start.nsec + assert_equal 123456000, foo.finish.nsec + + foo.save! + foo.reload + + assert_equal 0, foo.start.nsec + assert_equal 123456000, foo.finish.nsec + end + def test_passing_precision_to_time_does_not_set_limit @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 3 diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 1c7cec4dad..e89ac53732 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -394,6 +394,28 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end end +class TransactionAfterCommitCallbacksWithOptimisticLockingTest < ActiveRecord::TestCase + class PersonWithCallbacks < ActiveRecord::Base + self.table_name = :people + + after_create_commit { |record| record.history << :commit_on_create } + after_update_commit { |record| record.history << :commit_on_update } + after_destroy_commit { |record| record.history << :commit_on_destroy } + + def history + @history ||= [] + end + end + + def test_after_commit_callbacks_with_optimistic_locking + person = PersonWithCallbacks.create!(first_name: "first name") + person.update!(first_name: "another name") + person.destroy + + assert_equal [:commit_on_create, :commit_on_update, :commit_on_destroy], person.history + end +end + class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase self.use_transactional_tests = false diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb index 72d4997d0b..d5d8f2a09a 100644 --- a/activerecord/test/cases/unsafe_raw_sql_test.rb +++ b/activerecord/test/cases/unsafe_raw_sql_test.rb @@ -107,6 +107,26 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase assert_equal ids_expected, ids_disabled end + test "order: allows NULLS FIRST and NULLS LAST too" do + raise "precondition failed" if Post.count < 2 + + # Ensure there are NULL and non-NULL post types. + Post.first.update_column(:type, nil) + Post.last.update_column(:type, "Programming") + + ["asc", "desc", ""].each do |direction| + %w(first last).each do |position| + ids_expected = Post.order(Arel.sql("type #{direction} nulls #{position}")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("type #{direction} nulls #{position}").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("type #{direction} nulls #{position}").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + end + end if current_adapter?(:PostgreSQLAdapter) + test "order: disallows invalid column name" do with_unsafe_raw_sql_disabled do assert_raises(ActiveRecord::UnknownAttributeReference) do diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 73422a31cd..62cd89041a 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -56,7 +56,7 @@ class LengthValidationTest < ActiveRecord::TestCase assert owner.save pet_count = Pet.count - assert_not owner.update_attributes pets_attributes: [ { _destroy: 1, id: pet.id } ] + assert_not owner.update pets_attributes: [ { _destroy: 1, id: pet.id } ] assert_not_predicate owner, :valid? assert_predicate owner.errors[:pets], :any? assert_equal pet_count, Pet.count diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 1fa94a5b75..941aed5402 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -62,7 +62,7 @@ class TopicWithAfterCreate < Topic after_create :set_author def set_author - update_attributes!(author_name: "#{title} #{id}") + update!(author_name: "#{title} #{id}") end end diff --git a/activerecord/test/fixtures/.gitignore b/activerecord/test/fixtures/.gitignore deleted file mode 100644 index 885029a512..0000000000 --- a/activerecord/test/fixtures/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.sqlite*
\ No newline at end of file diff --git a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb index b892f50e41..7d4233fe31 100644 --- a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb +++ b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb @@ -5,7 +5,7 @@ class GiveMeBigNumbers < ActiveRecord::Migration::Current create_table :big_numbers do |table| table.column :bank_balance, :decimal, precision: 10, scale: 2 table.column :big_bank_balance, :decimal, precision: 15, scale: 2 - table.column :world_population, :decimal, precision: 10 + table.column :world_population, :decimal, precision: 20 table.column :my_house_population, :decimal, precision: 2 table.column :value_of_e, :decimal end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 27da886e1c..bd12cdf7ef 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -8,6 +8,7 @@ class Author < ActiveRecord::Base has_many :posts_with_comments, -> { includes(:comments) }, class_name: "Post" has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, class_name: "Post" has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order("comments.id") }, class_name: "Post" + has_many :posts_sorted_by_id, -> { order(:id) }, class_name: "Post" has_many :posts_sorted_by_id_limited, -> { order("posts.id").limit(1) }, class_name: "Post" has_many :posts_with_categories, -> { includes(:categories) }, class_name: "Post" has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, class_name: "Post" @@ -40,7 +41,7 @@ class Author < ActiveRecord::Base -> { where(title: "Welcome to the weblog").where(Post.arel_table[:comments_count].gt(0)) }, class_name: "Post" - has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts, source: :comments + has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts_sorted_by_id, source: :comments has_many :unordered_comments, -> { unscope(:order).distinct }, through: :posts_sorted_by_id_limited, source: :comments has_many :funky_comments, through: :posts, source: :comments has_many :ordered_uniq_comments, -> { distinct.order("comments.id") }, through: :posts, source: :comments diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 5ab433f2d9..f0f0576709 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -76,7 +76,7 @@ class CommentThatAutomaticallyAltersPostBody < Comment belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id after_save do |comment| - comment.post.update_attributes(body: "Automatically altered") + comment.post.update(body: "Automatically altered") end end @@ -87,6 +87,6 @@ end class CommentWithAfterCreateUpdate < Comment after_create do - update_attributes(body: "bar") + update(body: "bar") end end diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index 948435136d..e900fd40fb 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -2,6 +2,7 @@ class Face < ActiveRecord::Base belongs_to :man, inverse_of: :face + belongs_to :human, polymorphic: true belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face # Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly` belongs_to :poly_man_without_inverse, polymorphic: true diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index 3acd89a48e..e26920e951 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -11,3 +11,6 @@ class Man < ActiveRecord::Base has_many :secret_interests, class_name: "Interest", inverse_of: :secret_man has_one :mixed_case_monkey end + +class Human < Man +end diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb index f190860fd1..18ff103ffe 100644 --- a/activerecord/test/models/sponsor.rb +++ b/activerecord/test/models/sponsor.rb @@ -3,6 +3,7 @@ class Sponsor < ActiveRecord::Base belongs_to :sponsor_club, class_name: "Club", foreign_key: "club_id" belongs_to :sponsorable, polymorphic: true + belongs_to :sponsor, polymorphic: true belongs_to :thing, polymorphic: true, foreign_type: :sponsorable_type, foreign_key: :sponsorable_id belongs_to :sponsorable_with_conditions, -> { where name: "Ernie" }, polymorphic: true, foreign_type: "sponsorable_type", foreign_key: "sponsorable_id" diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7d008eecd5..ca86100bc5 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -210,7 +210,7 @@ ActiveRecord::Schema.define do t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)" t.index :name, name: "company_name_index", using: :btree - t.index "lower(name)", name: "company_expression_index" if supports_expression_index? + t.index "(CASE WHEN rating > 0 THEN lower(name) END)", name: "company_expression_index" if supports_expression_index? end create_table :content, force: true do |t| @@ -547,7 +547,7 @@ ActiveRecord::Schema.define do create_table :numeric_data, force: true do |t| t.decimal :bank_balance, precision: 10, scale: 2 t.decimal :big_bank_balance, precision: 15, scale: 2 - t.decimal :world_population, precision: 10, scale: 0 + t.decimal :world_population, precision: 20, scale: 0 t.decimal :my_house_population, precision: 2, scale: 0 t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78 t.float :temperature @@ -814,6 +814,7 @@ ActiveRecord::Schema.define do create_table :sponsors, force: true do |t| t.integer :club_id t.references :sponsorable, polymorphic: true, index: false + t.references :sponsor, polymorphic: true, index: false end create_table :string_key_objects, id: false, force: true do |t| @@ -951,6 +952,7 @@ ActiveRecord::Schema.define do t.string :poly_man_without_inverse_type t.integer :horrible_polymorphic_man_id t.string :horrible_polymorphic_man_type + t.references :human, polymorphic: true, index: false end create_table :interests, force: true do |t| |