diff options
Diffstat (limited to 'activerecord/lib')
78 files changed, 679 insertions, 455 deletions
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/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index e5e89734d2..27a641f05b 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -35,7 +35,7 @@ module ActiveRecord # the database). # # class Customer < ActiveRecord::Base - # composed_of :balance, class_name: "Money", mapping: %w(amount currency) + # composed_of :balance, class_name: "Money", mapping: %w(balance amount) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # end # @@ -177,9 +177,9 @@ module ActiveRecord # # Once a #composed_of relationship is specified for a model, records can be loaded from the database # by specifying an instance of the value object in the conditions hash. The following example - # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": + # finds all customers with +address_street+ equal to "May Street" and +address_city+ equal to "Chicago": # - # Customer.where(balance: Money.new(20, "USD")) + # Customer.where(address: Address.new("May Street", "Chicago")) # module ClassMethods # Adds reader and writer methods for manipulating a value object: @@ -212,8 +212,7 @@ module ActiveRecord # # Option examples: # composed_of :temperature, mapping: %w(reading celsius) - # composed_of :balance, class_name: "Money", mapping: %w(balance amount), - # converter: Proc.new { |balance| balance.to_money } + # composed_of :balance, class_name: "Money", mapping: %w(balance amount) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # composed_of :gps_location # composed_of :gps_location, allow_nil: true diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 2b0b2864bc..403667fb70 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -2,8 +2,8 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, predicate_builder, association) - super(klass, table, predicate_builder) + def initialize(klass, association) + super(klass) @association = association end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index ca1f9f1650..364f1fe74f 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -124,7 +124,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) + AssociationRelation.create(klass, self).merge!(klass.all) end def extensions @@ -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 diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 11967e0571..ad41ec8d2f 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -46,13 +46,9 @@ module ActiveRecord 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 @@ -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/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index de8afc9ccb..443ccaaa72 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -53,7 +53,7 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) primary_key = reflection.association_primary_key - pk_type = klass.type_for_attribute(primary_key.to_s) + pk_type = klass.type_for_attribute(primary_key) ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 8b4a48a38c..9a30198b95 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -32,7 +32,7 @@ module ActiveRecord class CollectionProxy < Relation def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table, klass.predicate_builder + super klass extensions = association.extensions extend(*extensions) if extensions.any? diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 7a3bef969b..59929b8c4e 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -8,9 +8,7 @@ module ActiveRecord def initialize(owner, reflection) super - - @through_records = {} - @through_association = nil + @through_records = {} end def concat(*records) @@ -50,11 +48,6 @@ module ActiveRecord end private - - def through_association - @through_association ||= owner.association(through_reflection.name) - end - # The through record (built with build_record) is temporarily cached # so that it may be reused if insert_record is subsequently called. # @@ -145,7 +138,7 @@ module ActiveRecord case method when :destroy if scope.klass.primary_key - count = scope.destroy_all.length + count = scope.destroy_all.count(&:destroyed?) else scope.each(&:_run_destroy_callbacks) count = scope.delete_all diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 36746f9115..491282adf7 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -6,17 +6,16 @@ module ActiveRecord class HasOneThroughAssociation < HasOneAssociation #:nodoc: include ThroughAssociation - def replace(record) - create_through_record(record) + def replace(record, save = true) + create_through_record(record, save) self.target = record end private - - def create_through_record(record) + def create_through_record(record, save) ensure_not_nested - through_proxy = owner.association(through_reflection.name) + through_proxy = through_association through_record = through_proxy.load_target if through_record && !record @@ -30,7 +29,7 @@ module ActiveRecord if through_record through_record.update(attributes) - elsif owner.new_record? + elsif owner.new_record? || !save through_proxy.build(attributes) else through_proxy.create(attributes) 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 735da152b7..ddcad4504c 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) @@ -82,11 +81,11 @@ module ActiveRecord end def association_key_type - @klass.type_for_attribute(association_key_name.to_s).type + @klass.type_for_attribute(association_key_name).type end def owner_key_type - @model.type_for_attribute(owner_key_name.to_s).type + @model.type_for_attribute(owner_key_name).type end def load_records(&block) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 54673b74f7..5afb0bc068 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -19,6 +19,10 @@ module ActiveRecord end end + def through_association + @through_association ||= owner.association(through_reflection.name) + end + # We merge in these scopes for two reasons: # # 1. To get the default_scope conditions for any of the other reflections in the chain diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 64f81ca582..842f407517 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -432,15 +432,12 @@ 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)) end @@ -456,7 +453,7 @@ module ActiveRecord arel_table = self.class.arel_table attribute_names.each do |name| - attrs[arel_table[name]] = typecasted_attribute_value(name) + attrs[arel_table[name]] = _read_attribute(name) end attrs end @@ -483,9 +480,5 @@ module ActiveRecord def pk_attribute?(name) name == self.class.primary_key end - - def typecasted_attribute_value(name) - _read_attribute(name) - end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 3de6fe566d..df4c79b0f6 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -32,9 +32,7 @@ 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 @@ -114,12 +112,12 @@ module ActiveRecord # Alias for +changed+ def changed_attribute_names_to_save - changes_to_save.keys + mutations_from_database.changed_attribute_names end # Alias for +changed_attributes+ def attributes_in_database - changes_to_save.transform_values(&:first) + mutations_from_database.changed_values 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/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/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/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 0520591f4f..dfba78614e 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -15,7 +15,7 @@ module ActiveRecord if collection.eager_loading? collection = collection.send(:apply_join_dependency) end - column_type = type_for_attribute(timestamp_column.to_s) + column_type = type_for_attribute(timestamp_column) column = connection.column_name_from_arel_node(collection.arel_attribute(timestamp_column)) select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 36048bee03..08f3e15a4b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -356,35 +356,33 @@ module ActiveRecord # Inserts a set of fixtures into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixtures(fixtures, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG return if fixtures.empty? - columns = schema_cache.columns_hash(table_name) + execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert") + end - values = fixtures.map do |fixture| - fixture = fixture.stringify_keys + def insert_fixtures_set(fixture_set, tables_to_delete = []) + fixture_inserts = fixture_set.map do |table_name, fixtures| + next if fixtures.empty? - unknown_columns = fixture.keys - columns.keys - if unknown_columns.any? - raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) - end + build_fixture_sql(fixtures, table_name) + end.compact - columns.map do |name, column| - if fixture.key?(name) - type = lookup_cast_type_from_column(column) - bind = Relation::QueryAttribute.new(name, fixture[name], type) - with_yaml_fallback(bind.value_for_database) - else - Arel.sql("DEFAULT") + table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup } + total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts)) + + disable_referential_integrity do + transaction(requires_new: true) do + total_sql.each do |sql| + execute sql, "Fixtures Load" + yield if block_given? end end end - - table = Arel::Table.new(table_name) - manager = Arel::InsertManager.new - manager.into(table) - columns.each_key { |column| manager.columns << table[column] } - manager.values = manager.create_values_list(values) - execute manager.to_sql, "Fixtures Insert" end def empty_insert_statement_value @@ -416,6 +414,44 @@ module ActiveRecord alias join_to_delete join_to_update private + def default_insert_value(column) + Arel.sql("DEFAULT") + end + + def build_fixture_sql(fixtures, table_name) + columns = schema_cache.columns_hash(table_name) + + values = fixtures.map do |fixture| + fixture = fixture.stringify_keys + + unknown_columns = fixture.keys - columns.keys + if unknown_columns.any? + raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) + end + + columns.map do |name, column| + if fixture.key?(name) + type = lookup_cast_type_from_column(column) + bind = Relation::QueryAttribute.new(name, fixture[name], type) + with_yaml_fallback(bind.value_for_database) + else + default_insert_value(column) + end + end + end + + table = Arel::Table.new(table_name) + manager = Arel::InsertManager.new + manager.into(table) + columns.each_key { |column| manager.columns << table[column] } + manager.values = manager.create_values_list(values) + + manager.to_sql + end + + def combine_multi_statements(total_sql) + total_sql.join(";\n") + end # Returns a subquery for the given key using the join information. def subquery_for(key, select) 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..22abdee4b8 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 @@ -66,7 +64,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 +131,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_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 4f58b0242c..db033db913 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -715,7 +715,7 @@ module ActiveRecord # # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname) # - # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it). + # Note: MySQL only supports index order from 8.0.1 onwards (earlier versions accepted the syntax but ignored it). # # ====== Creating a partial index # @@ -1049,8 +1049,8 @@ module ActiveRecord sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i) - versions = ActiveRecord::Migrator.migration_files(migrations_paths).map do |file| - ActiveRecord::Migrator.parse_migration_filename(file).first.to_i + versions = migration_context.migration_files.map do |file| + migration_context.parse_migration_filename(file).first.to_i end unless migrated.include?(version) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 2655407d75..559f068c39 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -119,6 +119,14 @@ module ActiveRecord end end + def migrations_paths # :nodoc: + @config[:migrations_paths] || Migrator.migrations_paths + end + + def migration_context # :nodoc: + MigrationContext.new(migrations_paths) + end + class Version include Comparable @@ -318,6 +326,11 @@ module ActiveRecord false end + # Does this adapter support foreign/external tables? + def supports_foreign_tables? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) 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 b394a8969f..608258d05c 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' @@ -526,23 +524,49 @@ module ActiveRecord index.using == :btree || super end - def insert_fixtures(*) - without_sql_mode("NO_AUTO_VALUE_ON_ZERO") { super } + def insert_fixtures_set(fixture_set, tables_to_delete = []) + with_multi_statements do + super { discard_remaining_results } + end end private + def combine_multi_statements(total_sql) + total_sql.each_with_object([]) do |sql, total_sql_chunks| + previous_packet = total_sql_chunks.last + sql << ";\n" + if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty? + total_sql_chunks << sql + else + previous_packet << sql + end + end + end + + def max_allowed_packet_reached?(current_packet, previous_packet) + if current_packet.bytesize > max_allowed_packet + raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable." + elsif previous_packet.nil? + false + else + (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet + end + end + + def max_allowed_packet + bytes_margin = 2 + @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin) + end - def without_sql_mode(mode) - result = execute("SELECT @@SESSION.sql_mode") - current_mode = result.first[0] - return yield unless current_mode.include?(mode) + def with_multi_statements + previous_flags = @config[:flags] + @config[:flags] = Mysql2::Client::MULTI_STATEMENTS + reconnect! - sql_mode = "REPLACE(@@sql_mode, '#{mode}', '')" - execute("SET @@SESSION.sql_mode = #{sql_mode}") yield ensure - sql_mode = "CONCAT(@@sql_mode, ',#{mode}')" - execute("SET @@SESSION.sql_mode = #{sql_mode}") + @config[:flags] = previous_flags + reconnect! end def initialize_type_map(m = type_map) 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 a058a72872..458c9bfd70 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -11,7 +11,7 @@ module ActiveRecord else super end - @connection.next_result while @connection.more_results? + discard_remaining_results result end @@ -50,11 +50,18 @@ module ActiveRecord alias :exec_update :exec_delete private + def default_insert_value(column) + Arel.sql("DEFAULT") unless column.auto_increment? + end def last_inserted_id(result) @connection.last_id end + def discard_remaining_results + @connection.abandon_results! + end + def exec_stmt_and_free(sql, name, binds, cache_stmt: false) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection 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 a8895f8606..6f3db772cd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -38,7 +38,7 @@ module ActiveRecord " TABLESPACE = \"#{value}\"" when :connection_limit " CONNECTION LIMIT = #{value}" - else + else "" end end @@ -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} @@ -124,9 +124,13 @@ module ActiveRecord # add info on sort order (only desc order is explicitly specified, asc is the default) # and non-default opclasses - expressions.scan(/(\w+)(?: (?!DESC)(\w+))?(?: (DESC))?/).each do |column, opclass, desc| + expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls| opclasses[column] = opclass.to_sym if opclass - orders[column] = :desc if desc + if nulls + orders[column] = [desc, nulls].compact.join(" ") + else + orders[column] = :desc if desc + end end end @@ -154,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 @@ -349,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 @@ -375,7 +379,7 @@ module ActiveRecord if respond_to?(method, true) sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) } sql_fragments << sqls - non_combinable_operations << procs if procs.present? + non_combinable_operations.concat(procs) else execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty? non_combinable_operations.each(&:call) @@ -498,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 @@ -527,6 +531,14 @@ module ActiveRecord end end + def foreign_tables + query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA") + end + + def foreign_table_exists?(table_name) + query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present? + end + # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ @@ -739,7 +751,7 @@ module ActiveRecord def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) - scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view + scope[:type] ||= "'r','v','m','f'" # (r)elation/table, (v)iew, (m)aterialized view, (f)oreign table sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup sql << " WHERE n.nspname = #{scope[:schema]}" @@ -756,6 +768,8 @@ module ActiveRecord "'r'" when "VIEW" "'v','m'" + when "FOREIGN TABLE" + "'f'" end scope = {} scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 9ac5a8760e..fdf6f75108 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -281,7 +281,7 @@ module ActiveRecord end def discard! # :nodoc: - @connection.socket_io.reopen(IO::NULL) + @connection.socket_io.reopen(IO::NULL) rescue nil @connection = nil end @@ -318,6 +318,10 @@ module ActiveRecord postgresql_version >= 90300 end + def supports_foreign_tables? + postgresql_version >= 90300 + end + def supports_pgcrypto_uuid? postgresql_version >= 90400 end @@ -461,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 @@ -833,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/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index f34b6733da..c29cf1f9a1 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -28,7 +28,7 @@ module ActiveRecord coder["columns_hash"] = @columns_hash coder["primary_keys"] = @primary_keys coder["data_sources"] = @data_sources - coder["version"] = ActiveRecord::Migrator.current_version + coder["version"] = connection.migration_context.current_version end def init_with(coder) @@ -100,7 +100,7 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. - @version = ActiveRecord::Migrator.current_version + @version = connection.migration_context.current_version [@version, @columns, @columns_hash, @primary_keys, @data_sources] end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index d4f5bd16ac..a958600446 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -101,7 +101,7 @@ module ActiveRecord def initialize(connection, logger, connection_options, config) super(connection, logger, config) - @active = nil + @active = true @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) configure_connection @@ -144,7 +144,7 @@ module ActiveRecord end def active? - @active != false + @active end # Disconnects from the database if already connected. Otherwise, this @@ -367,8 +367,22 @@ module ActiveRecord end def insert_fixtures(rows, table_name) - rows.each do |row| - insert_fixture(row, table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `insert_fixtures` is deprecated and will be removed in the next version of Rails. + Consider using `insert_fixtures_set` for performance improvement. + MSG + insert_fixtures_set(table_name => rows) + end + + def insert_fixtures_set(fixture_set, tables_to_delete = []) + disable_referential_integrity do + transaction(requires_new: true) do + tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" } + + fixture_set.each do |table_name, rows| + rows.each { |row| insert_fixture(row, table_name) } + end + end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 88810cb328..e1a0b2ecf8 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -281,7 +281,7 @@ module ActiveRecord end def relation - relation = Relation.create(self, arel_table, predicate_builder) + relation = Relation.create(self) if finder_needs_type_condition? && !ignore_default_scope? relation.where!(type_condition) @@ -382,8 +382,10 @@ module ActiveRecord _run_initialize_callbacks - @new_record = true - @destroyed = false + @new_record = true + @destroyed = false + @_start_transaction_state = {} + @transaction_state = nil super 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 86f13d75d5..8f022ff7a7 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -169,13 +169,13 @@ module ActiveRecord # self.use_transactional_tests = true # # test "godzilla" do - # assert !Foo.all.empty? + # assert_not_empty Foo.all # Foo.destroy_all - # assert Foo.all.empty? + # assert_empty Foo.all # end # # test "godzilla aftermath" do - # assert !Foo.all.empty? + # assert_not_empty Foo.all # end # end # @@ -540,47 +540,38 @@ module ActiveRecord } unless files_to_read.empty? - connection.disable_referential_integrity do - fixtures_map = {} - - fixture_sets = files_to_read.map do |fs_name| - klass = class_names[fs_name] - conn = klass ? klass.connection : connection - fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new - conn, - fs_name, - klass, - ::File.join(fixtures_directory, fs_name)) - end - - update_all_loaded_fixtures fixtures_map - - connection.transaction(requires_new: true) do - deleted_tables = Hash.new { |h, k| h[k] = Set.new } - fixture_sets.each do |fs| - conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection - table_rows = fs.table_rows + fixtures_map = {} + + fixture_sets = files_to_read.map do |fs_name| + klass = class_names[fs_name] + conn = klass ? klass.connection : connection + fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new + conn, + fs_name, + klass, + ::File.join(fixtures_directory, fs_name)) + end - table_rows.each_key do |table| - unless deleted_tables[conn].include? table - conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete" - end - deleted_tables[conn] << table - end + update_all_loaded_fixtures fixtures_map + fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection } - table_rows.each do |fixture_set_name, rows| - conn.insert_fixtures(rows, fixture_set_name) - end + fixture_sets_by_connection.each do |conn, set| + table_rows_for_connection = Hash.new { |h, k| h[k] = [] } - # Cap primary key sequences to max(pk). - if conn.respond_to?(:reset_pk_sequence!) - conn.reset_pk_sequence!(fs.table_name) - end + set.each do |fs| + fs.table_rows.each do |table, rows| + table_rows_for_connection[table].unshift(*rows) end end + conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys) - cache_fixtures(connection, fixtures_map) + # Cap primary key sequences to max(pk). + if conn.respond_to?(:reset_pk_sequence!) + set.each { |fs| conn.reset_pk_sequence!(fs.table_name) } + end end + + cache_fixtures(connection, fixtures_map) end cached_fixtures(connection, fixture_set_names) end @@ -883,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 @@ -982,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 @@ -998,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/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 7e47dac016..72035a986b 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -7,10 +7,10 @@ module ActiveRecord end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 - PRE = "beta2" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end 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/migration.rb b/activerecord/lib/active_record/migration.rb index f6648a4e3d..5322571820 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -550,7 +550,7 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + mtime = ActiveRecord::Base.connection.migration_context.last_migration.mtime.to_i if @last_check < mtime ActiveRecord::Migration.check_pending!(connection) @last_check = mtime @@ -575,11 +575,11 @@ module ActiveRecord # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) - raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) + raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration? end def load_schema_if_pending! - if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? + if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations? # Roundtrip to Rake to allow plugins to hook into database initialization. root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root FileUtils.cd(root) do @@ -876,10 +876,10 @@ module ActiveRecord FileUtils.mkdir_p(destination) unless File.exist?(destination) - destination_migrations = ActiveRecord::Migrator.migrations(destination) + destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations last = destination_migrations.last sources.each do |scope, path| - source_migrations = ActiveRecord::Migrator.migrations(path) + source_migrations = ActiveRecord::MigrationContext.new(path).migrations source_migrations.each do |migration| source = File.binread(migration.filename) @@ -997,132 +997,147 @@ module ActiveRecord end end - class Migrator#:nodoc: - class << self - attr_writer :migrations_paths - alias :migrations_path= :migrations_paths= - - def migrate(migrations_paths, target_version = nil, &block) - case - when target_version.nil? - up(migrations_paths, target_version, &block) - when current_version == 0 && target_version == 0 - [] - when current_version > target_version - down(migrations_paths, target_version, &block) - else - up(migrations_paths, target_version, &block) - end - end + class MigrationContext # :nodoc: + attr_reader :migrations_paths - def rollback(migrations_paths, steps = 1) - move(:down, migrations_paths, steps) - end + def initialize(migrations_paths) + @migrations_paths = migrations_paths + end - def forward(migrations_paths, steps = 1) - move(:up, migrations_paths, steps) + def migrate(target_version = nil, &block) + case + when target_version.nil? + up(target_version, &block) + when current_version == 0 && target_version == 0 + [] + when current_version > target_version + down(target_version, &block) + else + up(target_version, &block) end + end - def up(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + def rollback(steps = 1) + move(:down, steps) + end - new(:up, migrations, target_version).migrate + def forward(steps = 1) + move(:up, steps) + end + + def up(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def down(migrations_paths, target_version = nil) - migrations = migrations(migrations_paths) - migrations.select! { |m| yield m } if block_given? + Migrator.new(:up, selected_migrations, target_version).migrate + end - new(:down, migrations, target_version).migrate + def down(target_version = nil) + selected_migrations = if block_given? + migrations.select { |m| yield m } + else + migrations end - def run(direction, migrations_paths, target_version) - new(direction, migrations(migrations_paths), target_version).run - end + Migrator.new(:down, selected_migrations, target_version).migrate + end - def open(migrations_paths) - new(:up, migrations(migrations_paths), nil) - end + def run(direction, target_version) + Migrator.new(direction, migrations, target_version).run + end - def get_all_versions - if SchemaMigration.table_exists? - SchemaMigration.all_versions.map(&:to_i) - else - [] - end - end + def open + Migrator.new(:up, migrations, nil) + end - def current_version(connection = nil) - get_all_versions.max || 0 - rescue ActiveRecord::NoDatabaseError + def get_all_versions + if SchemaMigration.table_exists? + SchemaMigration.all_versions.map(&:to_i) + else + [] end + end - def needs_migration?(connection = nil) - (migrations(migrations_paths).collect(&:version) - get_all_versions).size > 0 - end + def current_version + get_all_versions.max || 0 + rescue ActiveRecord::NoDatabaseError + end - def any_migrations? - migrations(migrations_paths).any? - end + def needs_migration? + (migrations.collect(&:version) - get_all_versions).size > 0 + end - def last_migration #:nodoc: - migrations(migrations_paths).last || NullMigration.new - end + def any_migrations? + migrations.any? + end - def migrations_paths - @migrations_paths ||= ["db/migrate"] - # just to not break things if someone uses: migrations_path = some_string - Array(@migrations_paths) - end + def last_migration #:nodoc: + migrations.last || NullMigration.new + end - def parse_migration_filename(filename) # :nodoc: - File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + def parse_migration_filename(filename) # :nodoc: + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + + def migrations + migrations = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = version.to_i + name = name.camelize + + MigrationProxy.new(name, version, file, scope) end - def migrations(paths) - paths = Array(paths) + migrations.sort_by(&:version) + end - migrations = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = version.to_i - name = name.camelize + def migrations_status + db_list = ActiveRecord::SchemaMigration.normalized_versions - MigrationProxy.new(name, version, file, scope) - end + file_list = migration_files.map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) + status = db_list.delete(version) ? "up" : "down" + [status, version, (name + scope).humanize] + end.compact - migrations.sort_by(&:version) + db_list.map! do |version| + ["up", version, "********** NO FILE **********"] end - def migrations_status(paths) - paths = Array(paths) - - db_list = ActiveRecord::SchemaMigration.normalized_versions + (db_list + file_list).sort_by { |_, version, _| version } + end - file_list = migration_files(paths).map do |file| - version, name, scope = parse_migration_filename(file) - raise IllegalMigrationNameError.new(file) unless version - version = ActiveRecord::SchemaMigration.normalize_migration_number(version) - status = db_list.delete(version) ? "up" : "down" - [status, version, (name + scope).humanize] - end.compact + def migration_files + paths = Array(migrations_paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end - db_list.map! do |version| - ["up", version, "********** NO FILE **********"] - end + def current_environment + ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end - (db_list + file_list).sort_by { |_, version, _| version } - end + def protected_environment? + ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment + end - def migration_files(paths) - Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] - end + def last_stored_environment + return nil if current_version == 0 + raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - private + environment = ActiveRecord::InternalMetadata[:environment] + raise NoEnvironmentInSchemaError unless environment + environment + end - def move(direction, migrations_paths, steps) - migrator = new(direction, migrations(migrations_paths)) + private + def move(direction, steps) + migrator = Migrator.new(direction, migrations) if current_version != 0 && !migrator.current_migration raise UnknownMigrationVersionError.new(current_version) @@ -1137,10 +1152,29 @@ module ActiveRecord finish = migrator.migrations[start_index + steps] version = finish ? finish.version : 0 - send(direction, migrations_paths, version) + send(direction, version) + end + end + + class Migrator # :nodoc: + class << self + attr_accessor :migrations_paths + + def migrations_path=(path) + ActiveSupport::Deprecation.warn \ + "ActiveRecord::Migrator.migrations_paths= is now deprecated and will be removed in Rails 6.0." \ + "You can set the `migrations_paths` on the `connection` instead through the `database.yml`." + self.migrations_paths = [path] + end + + # For cases where a table doesn't exist like loading from schema cache + def current_version + MigrationContext.new(migrations_paths).current_version end end + self.migrations_paths = ["db/migrate"] + def initialize(direction, migrations, target_version = nil) @direction = direction @target_version = target_version @@ -1203,7 +1237,7 @@ module ActiveRecord end def load_migrated - @migrated_versions = Set.new(self.class.get_all_versions) + @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions) end private @@ -1235,7 +1269,7 @@ module ActiveRecord # Stores the current environment in the database. def record_environment return if down? - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end def ran?(migration) @@ -1294,23 +1328,6 @@ module ActiveRecord end end - def self.last_stored_environment - return nil if current_version == 0 - raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? - - environment = ActiveRecord::InternalMetadata[:environment] - raise NoEnvironmentInSchemaError unless environment - environment - end - - def self.current_environment - ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - end - - def self.protected_environment? - ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment - end - def up? @direction == :up end 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/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 7ae8073478..0edaaa0cf9 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -13,7 +13,10 @@ module ActiveRecord const_get(name) end - V5_2 = Current + V6_0 = Current + + class V5_2 < V6_0 + end class V5_1 < V5_2 def change_column(table_name, column_name, type, options = {}) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index fa7537c1a3..b04dc04899 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -361,8 +361,9 @@ module ActiveRecord # it). # # +attr_name+ The name of the attribute to retrieve the type for. Must be - # a string + # a string or a symbol. def type_for_attribute(attr_name, &block) + attr_name = attr_name.to_s if block attribute_types.fetch(attr_name, &block) else @@ -378,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 c1b1a5334a..88b971327b 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -359,10 +359,10 @@ module ActiveRecord # Any change to the attributes on either instance will affect both instances. # If you want to change the sti column as well, use #becomes! instead. def becomes(klass) - became = klass.new + became = klass.allocate + 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 +418,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 +432,7 @@ module ActiveRecord end alias update_attributes! update! + deprecate :update_attributes! # Equivalent to <code>update_columns(name => value)</code>. def update_column(name, value) 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..7c8f794910 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 diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 4538ed6a5f..ade5946dd5 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -59,6 +59,7 @@ module ActiveRecord console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console end + ActiveRecord::Base.verbose_query_logs = false end runner do @@ -91,6 +92,7 @@ module ActiveRecord if File.file?(filename) current_version = ActiveRecord::Migrator.current_version + next if current_version.nil? cache = YAML.load(File.read(filename)) 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 fce3e1c5cf..662a8bc720 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -6,7 +6,7 @@ db_namespace = namespace :db do desc "Set the environment value for the database" task "environment:set" => :load_config do ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment end task check_protected_environments: :load_config do @@ -99,9 +99,8 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :up, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke @@ -113,9 +112,8 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.check_target_version - ActiveRecord::Migrator.run( + ActiveRecord::Base.connection.migration_context.run( :down, - ActiveRecord::Tasks::DatabaseTasks.migrations_paths, ActiveRecord::Tasks::DatabaseTasks.target_version ) db_namespace["_dump"].invoke @@ -131,8 +129,7 @@ db_namespace = namespace :db do puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths - ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name| + ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts @@ -142,14 +139,14 @@ db_namespace = namespace :db do desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)." task rollback: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.rollback(step) db_namespace["_dump"].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' task forward: :load_config do step = ENV["STEP"] ? ENV["STEP"].to_i : 1 - ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) + ActiveRecord::Base.connection.migration_context.forward(step) db_namespace["_dump"].invoke end @@ -172,12 +169,12 @@ db_namespace = namespace :db do desc "Retrieves the current schema version number" task version: :load_config do - puts "Current version: #{ActiveRecord::Migrator.current_version}" + puts "Current version: #{ActiveRecord::Base.connection.migration_context.current_version}" end # desc "Raises an error if there are pending migrations" task abort_if_pending_migrations: :load_config do - pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations + pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" @@ -232,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 c28e31a3da..c66c7778f4 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -291,7 +291,11 @@ module ActiveRecord end def build_scope(table, predicate_builder = predicate_builder(table)) - Relation.create(klass, table, predicate_builder) + Relation.create( + klass, + table: table, + predicate_builder: predicate_builder + ) end def join_primary_key(*) @@ -958,16 +962,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 10be583ef4..736173ae1b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -22,7 +22,7 @@ module ActiveRecord alias :loaded? :loaded alias :locked? :lock_value - def initialize(klass, table, predicate_builder, values = {}) + def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {}) @klass = klass @table = table @values = values @@ -142,23 +142,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 +159,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) diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index cb0b06cfdc..6a63eb70b1 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -131,7 +131,14 @@ module ActiveRecord def calculate(operation, column_name) if has_include?(column_name) relation = apply_join_dependency - relation.distinct! if operation.to_s.downcase == "count" + + if operation.to_s.downcase == "count" && !distinct_value + relation.distinct! + # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT + if (column_name == :all || column_name.nil?) && select_values.empty? + relation.order_values = [] + end + end relation.calculate(operation, column_name) else @@ -193,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 @@ -235,7 +260,7 @@ module ActiveRecord def aggregate_column(column_name) return column_name if Arel::Expressions === column_name - if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s) + if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name) @klass.arel_attribute(column_name) else Arel.sql(column_name == :all ? "*" : column_name.to_s) 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/merger.rb b/activerecord/lib/active_record/relation/merger.rb index ebdd4144bb..25510d4a57 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -23,7 +23,11 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table, relation.predicate_builder) + other = Relation.create( + relation.klass, + table: relation.table, + predicate_builder: relation.predicate_builder + ) hash.each { |k, v| if k == :joins if Hash === v diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 885c26d7aa..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? @@ -86,6 +83,18 @@ module ActiveRecord expand_from_hash(query).reduce(&:and) end queries.reduce(&:or) + elsif table.aggregated_with?(key) + mapping = table.reflect_on_aggregation(key).mapping + queries = Array.wrap(value).map do |object| + mapping.map do |field_attr, aggregate_attr| + if mapping.size == 1 && !object.respond_to?(aggregate_attr) + build(table.arel_attribute(field_attr), object) + else + build(table.arel_attribute(field_attr), object.send(aggregate_attr)) + end + end.reduce(&:and) + end + queries.reduce(&:or) # FIXME: Deprecate this and provide a public API to force equality elsif (value.is_a?(Range) || value.is_a?(Array)) && table.type(key.to_s).respond_to?(:subtype) @@ -97,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..a5e9a0473e 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,12 +17,9 @@ 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) } 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..3afa368575 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 diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 617d8de8b2..562e04194c 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -69,7 +69,7 @@ module ActiveRecord private def relation_with(values) - result = Relation.create(klass, table, predicate_builder, values) + result = Relation.create(klass, values: values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 4ae94f4bfe..c1b3eea9df 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -14,7 +14,6 @@ module ActiveRecord parts = [klass.sanitize_sql(other.empty? ? opts : ([opts] + other))] when Hash attributes = predicate_builder.resolve_column_aliases(opts) - attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) attributes.stringify_keys! parts = predicate_builder.build_from_hash(attributes) @@ -27,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/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 58da106092..c6c268855e 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -84,7 +84,7 @@ module ActiveRecord def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - type = type_for_attribute(attr.to_s) + type = type_for_attribute(attr) value = type.serialize(type.cast(value)) "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(", ") @@ -155,10 +155,12 @@ module ActiveRecord if aggregation = reflect_on_aggregation(attr.to_sym) mapping = aggregation.mapping mapping.each do |field_attr, aggregate_attr| - if mapping.size == 1 && !value.respond_to?(aggregate_attr) - expanded_attrs[field_attr] = value + expanded_attrs[field_attr] = if value.is_a?(Array) + value.map { |it| it.send(aggregate_attr) } + elsif mapping.size == 1 && !value.respond_to?(aggregate_attr) + value else - expanded_attrs[field_attr] = value.send(aggregate_attr) + value.send(aggregate_attr) end end else @@ -167,6 +169,7 @@ module ActiveRecord end expanded_attrs end + deprecate :expand_hash_conditions_for_aggregates def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 1e121f2a09..216359867c 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -55,7 +55,7 @@ module ActiveRecord end ActiveRecord::InternalMetadata.create_table - ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end private diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 16ccba6b6c..b8d848b999 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -44,7 +44,7 @@ module ActiveRecord def initialize(connection, options = {}) @connection = connection - @version = Migrator::current_version rescue nil + @version = connection.migration_context.current_version rescue nil @options = options 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 202b82fa61..6dbc977f9a 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -135,7 +135,7 @@ module ActiveRecord end def store_accessor_for(store_attribute) - type_for_attribute(store_attribute.to_s).accessor + type_for_attribute(store_attribute).accessor end class HashAccessor # :nodoc: diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 0459cbdc59..b67479fb6a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -30,7 +30,7 @@ module ActiveRecord def type(column_name) if klass - klass.type_for_attribute(column_name.to_s) + klass.type_for_attribute(column_name) else Type.default_value end @@ -65,10 +65,15 @@ module ActiveRecord association && association.polymorphic? end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected + def aggregated_with?(aggregation_name) + klass && reflect_on_aggregation(aggregation_name) + end + + def reflect_on_aggregation(aggregation_name) + klass.reflect_on_aggregation(aggregation_name) + end + 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 4657e51e6d..d8e0cd1e30 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -54,10 +54,10 @@ module ActiveRecord def check_protected_environments! unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] - current = ActiveRecord::Migrator.current_environment - stored = ActiveRecord::Migrator.last_stored_environment + current = ActiveRecord::Base.connection.migration_context.current_environment + stored = ActiveRecord::Base.connection.migration_context.last_stored_environment - if ActiveRecord::Migrator.protected_environment? + if ActiveRecord::Base.connection.migration_context.protected_environment? raise ActiveRecord::ProtectedEnvironmentError.new(stored) end @@ -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 @@ -163,13 +163,17 @@ module ActiveRecord } end + def verbose? + ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true + end + def migrate check_target_version - verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true + verbose = verbose? scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose - Migrator.migrate(migrations_paths, target_version) do |migration| + Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end ActiveRecord::Base.clear_cache! 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 d51350ba83..663cdadb03 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -9,14 +9,11 @@ module ActiveRecord def type_cast_for_database(attr_name, value) return value if value.is_a?(Arel::Nodes::BindParam) - type = types.type_for_attribute(attr_name.to_s) + type = types.type_for_attribute(attr_name) 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/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 |