diff options
Diffstat (limited to 'activerecord')
56 files changed, 1332 insertions, 540 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 96fd6a62c6..97b7ad93d1 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,73 @@ +* Fix collection cache key with limit and custom select to avoid ambiguous timestamp column error. + + Fixes #33056. + + *Federico Martinez* + +* Add basic API for connection switching to support multiple databases. + + 1) Adds a `connects_to` method for models to connect to multiple databases. Example: + + ``` + class AnimalsModel < ApplicationRecord + self.abstract_class = true + + connects_to database: { writing: :animals_primary, reading: :animals_replica } + end + + class Dog < AnimalsModel + # connected to both the animals_primary db for writing and the animals_replica for reading + end + ``` + + 2) Adds a `connected_to` block method for switching connection roles or connecting to + a database that the model didn't connect to. Connecting to the database in this block is + useful when you have another defined connection, for example `slow_replica` that you don't + want to connect to by default but need in the console, or a specific code block. + + ``` + ActiveRecord::Base.connected_to(role: :reading) do + Dog.first # finds dog from replica connected to AnimalsBase + Book.first # doesn't have a reading connection, will raise an error + end + ``` + + ``` + ActiveRecord::Base.connected_to(database: :slow_replica) do + SlowReplicaModel.first # if the db config has a slow_replica configuration this will be used to do the lookup, otherwise this will throw an exception + end + ``` + + *Eileen M. Uchitelle* + +* Enum raises on invalid definition values + + When defining a Hash enum it can be easy to use [] instead of {}. This + commit checks that only valid definition values are provided, those can + be a Hash, an array of Symbols or an array of Strings. Otherwise it + raises an ArgumentError. + + Fixes #33961 + + *Alberto Almagro* + +* Reloading associations now clears the Query Cache like `Persistence#reload` does. + + ``` + class Post < ActiveRecord::Base + has_one :category + belongs_to :author + has_many :comments + end + + # Each of the following will now clear the query cache. + post.reload_category + post.reload_author + post.comments.reload + ``` + + *Christophe Maximin* + * Added `index` option for `change_table` migration helpers. With this change you can create indexes while adding new columns into the existing tables. diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 403667fb70..4c538ef2bd 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -31,9 +31,9 @@ module ActiveRecord private def exec_queries - super do |r| - @association.set_inverse_instance r - yield r if block_given? + super do |record| + @association.set_inverse_instance_from_queries(record) + yield record if block_given? end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 44596f4424..bf4942aac8 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -40,7 +40,9 @@ module ActiveRecord end # Reloads the \target and returns +self+ on success. - def reload + # The QueryCache is cleared if +force+ is true. + def reload(force = false) + klass.connection.clear_query_cache if force && klass reset reset_scope load_target @@ -79,18 +81,6 @@ module ActiveRecord target_scope.merge!(association_scope) end - # The scope for this association. - # - # Note that the association_scope is merged into the target_scope only when the - # scope method is called. This is because at that point the call may be surrounded - # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which - # actually gets built. - def association_scope - if klass - @association_scope ||= AssociationScope.scope(self) - end - end - def reset_scope @association_scope = nil end @@ -103,6 +93,13 @@ module ActiveRecord record end + def set_inverse_instance_from_queries(record) + if inverse = inverse_association_for(record) + inverse.inversed_from_queries(owner) + end + record + end + # Remove the inverse association, if possible def remove_inverse_instance(record) if inverse = inverse_association_for(record) @@ -114,6 +111,7 @@ module ActiveRecord self.target = record @inversed = !!record end + alias :inversed_from_queries :inversed_from # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. @@ -121,12 +119,6 @@ module ActiveRecord reflection.klass end - # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the - # through association's scope) - def target_scope - AssociationRelation.create(klass, self).merge!(klass.all) - end - def extensions extensions = klass.default_extensions | reflection.extensions @@ -187,6 +179,24 @@ module ActiveRecord end private + # The scope for this association. + # + # Note that the association_scope is merged into the target_scope only when the + # scope method is called. This is because at that point the call may be surrounded + # by scope.scoping { ... } or unscoped { ... } etc, which affects the scope which + # actually gets built. + def association_scope + if klass + @association_scope ||= AssociationScope.scope(self) + end + end + + # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the + # through association's scope) + def target_scope + AssociationRelation.create(klass, self).merge!(klass.all) + end + def scope_for_create scope.scope_for_create end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 9a30198b95..811fbfd927 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -1088,7 +1088,7 @@ module ActiveRecord # person.pets.reload # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload - proxy_association.reload + proxy_association.reload(true) reset_scope end @@ -1125,7 +1125,7 @@ module ActiveRecord SpawnMethods, ].flat_map { |klass| klass.public_instance_methods(false) - } - self.public_instance_methods(false) - [:select] + [:scoping] + } - self.public_instance_methods(false) - [:select] + [:scoping, :values] delegate(*delegate_methods, to: :scope) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 617956c768..f84ac65fa2 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -21,20 +21,6 @@ module ActiveRecord super end - def concat_records(records) - ensure_not_nested - - records = super(records, true) - - if owner.new_record? && records - records.flatten.each do |record| - build_through_record(record) - end - end - - records - end - def insert_record(record, validate = true, raise = false) ensure_not_nested @@ -48,6 +34,20 @@ module ActiveRecord end private + def concat_records(records) + ensure_not_nested + + records = super(records, true) + + if owner.new_record? && records + records.flatten.each do |record| + build_through_record(record) + end + end + + records + end + # The through record (built with build_record) is temporarily cached # so that it may be reused if insert_record is subsequently called. # diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index cfab16a745..8e50cce102 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -26,7 +26,7 @@ module ActiveRecord # Implements the reload reader method, e.g. foo.reload_bar for # Foo.has_one :bar def force_reload_reader - klass.uncached { reload } + reload(true) target end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 221ebea8ea..eeb88e4b7a 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -22,15 +22,6 @@ module ActiveRecord delegate :column_for_attribute, to: :class end - AttrNames = Module.new { - def self.set_name_cache(name, value) - const_name = "ATTR_#{name}" - unless const_defined? const_name - const_set const_name, -value - end - end - } - RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) class GeneratedAttributeMethods < Module #:nodoc: @@ -270,21 +261,14 @@ module ActiveRecord def respond_to?(name, include_private = false) return false unless super - case name - when :to_partial_path - name = "to_partial_path" - when :to_model - name = "to_model" - else - name = name.to_s - end - # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@attributes) && self.class.column_names.include?(name) - return has_attribute?(name) + if defined?(@attributes) + if name = self.class.symbol_column_to_string(name.to_sym) + return has_attribute?(name) + end end true diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 903fe86e04..6e1275e990 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -8,42 +8,19 @@ module ActiveRecord module ClassMethods # :nodoc: private - # We want to generate the methods via module_eval rather than - # define_method, because define_method is slower on dispatch. - # Evaluating many similar methods may use more memory as the instruction - # sequences are duplicated and cached (in MRI). define_method may - # be slower on dispatch, but if you're careful about the closure - # created, then define_method will consume much less memory. - # - # But sometimes the database might return columns with - # characters that are not allowed in normal method names (like - # 'my_column(omg)'. So to work around this we first define with - # the __temp__ identifier, and then use alias method to rename - # it to what we want. - # - # We are also defining a constant to hold the frozen string of - # the attribute name. Using a constant means that we do not have - # to allocate an object on each call to the attribute method. - # Making it frozen means that it doesn't get duped when used to - # key the @attributes in read_attribute. def define_method_attribute(name) - safe_name = name.unpack1("h*") - temp_method = "__temp__#{safe_name}" - - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{temp_method} - #{sync_with_transaction_state} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - STR - - generated_attribute_methods.module_eval do - alias_method name, temp_method - undef_method temp_method + ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( + generated_attribute_methods, name + ) do |temp_method_name, attr_name_expr| + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{temp_method_name} + #{sync_with_transaction_state} + name = #{attr_name_expr} + _read_attribute(name) { |n| missing_attribute(n, caller) } + end + RUBY end end end @@ -52,10 +29,9 @@ module ActiveRecord # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) - name = if self.class.attribute_alias?(attr_name) - self.class.attribute_alias(attr_name).to_s - else - attr_name.to_s + name = attr_name.to_s + if self.class.attribute_alias?(name) + name = self.class.attribute_alias(name) end primary_key = self.class.primary_key diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 62743bc9d8..455e67e19b 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -13,19 +13,19 @@ module ActiveRecord private def define_method_attribute=(name) - safe_name = name.unpack1("h*") - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key - generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def __temp__#{safe_name}=(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - #{sync_with_transaction_state} - _write_attribute(name, value) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR + ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method( + generated_attribute_methods, name, writer: true, + ) do |temp_method_name, attr_name_expr| + generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{temp_method_name}(value) + name = #{attr_name_expr} + #{sync_with_transaction_state} + _write_attribute(name, value) + end + RUBY + end end end @@ -33,10 +33,9 @@ module ActiveRecord # specified +value+. Empty strings for Integer and Float columns are # turned into +nil+. def write_attribute(attr_name, value) - name = if self.class.attribute_alias?(attr_name) - self.class.attribute_alias(attr_name).to_s - else - attr_name.to_s + name = attr_name.to_s + if self.class.attribute_alias?(name) + name = self.class.attribute_alias(name) end primary_key = self.class.primary_key diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 61581b0451..4b6db8a96c 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -20,9 +20,9 @@ module ActiveRecord select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" if collection.has_limit_or_offset? - query = collection.select(column) + query = collection.select("#{column} AS collection_cache_key_timestamp") subquery_alias = "subquery_for_cache_key" - subquery_column = "#{subquery_alias}.#{timestamp_column}" + subquery_column = "#{subquery_alias}.collection_cache_key_timestamp" subquery = query.arel.as(subquery_alias) arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column) else diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 015204c056..70607fda5a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -348,8 +348,8 @@ module ActiveRecord # # create_table :taggings do |t| # t.references :tag, index: { name: 'index_taggings_on_tag_id' } - # t.references :tagger, polymorphic: true, index: true - # t.references :taggable, polymorphic: { default: 'Photo' } + # t.references :tagger, polymorphic: true + # t.references :taggable, polymorphic: { default: 'Photo' }, index: false # end def column(name, type, options = {}) name = name.to_s diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 723d8c318d..8a7020a799 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -846,17 +846,17 @@ module ActiveRecord # [<tt>:null</tt>] # Whether the column allows nulls. Defaults to true. # - # ====== Create a user_id bigint column + # ====== Create a user_id bigint column without a index # - # add_reference(:products, :user) + # add_reference(:products, :user, index: false) # # ====== Create a user_id string column # # add_reference(:products, :user, type: :string) # - # ====== Create supplier_id, supplier_type columns and appropriate index + # ====== Create supplier_id, supplier_type columns # - # add_reference(:products, :supplier, polymorphic: true, index: true) + # add_reference(:products, :supplier, polymorphic: true) # # ====== Create a supplier_id column with a unique index # @@ -884,7 +884,7 @@ module ActiveRecord # # ====== Remove the reference # - # remove_reference(:products, :user, index: true) + # remove_reference(:products, :user, index: false) # # ====== Remove polymorphic reference # @@ -892,7 +892,7 @@ module ActiveRecord # # ====== Remove the reference with a foreign key # - # remove_reference(:products, :user, index: true, foreign_key: true) + # remove_reference(:products, :user, foreign_key: true) # def remove_reference(table_name, ref_name, foreign_key: false, polymorphic: false, **options) if foreign_key diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index fa10f18cb7..0fe868478c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -125,6 +125,8 @@ module ActiveRecord @advisory_locks_enabled = self.class.type_cast_config_to_boolean( config.fetch(:advisory_locks, true) ) + + check_version end def replica? @@ -502,6 +504,9 @@ module ActiveRecord end private + def check_version + end + def type_map @type_map ||= Type::TypeMap.new.tap do |mapping| initialize_type_map(mapping) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index d40f38fb77..0f73641369 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -54,10 +54,6 @@ module ActiveRecord super(connection, logger, config) @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - - if version < "5.5.8" - raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8." - end end def version #:nodoc: @@ -535,6 +531,12 @@ module ActiveRecord end private + def check_version + if version < "5.5.8" + raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8." + end + end + def combine_multi_statements(total_sql) total_sql.each_with_object([]) do |sql, total_sql_chunks| previous_packet = total_sql_chunks.last @@ -622,6 +624,8 @@ module ActiveRecord # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 ER_NOT_NULL_VIOLATION = 1048 + ER_NO_REFERENCED_ROW = 1216 + ER_ROW_IS_REFERENCED = 1217 ER_DO_NOT_HAVE_DEFAULT = 1364 ER_ROW_IS_REFERENCED_2 = 1451 ER_NO_REFERENCED_ROW_2 = 1452 @@ -638,7 +642,7 @@ module ActiveRecord case error_number(exception) when ER_DUP_ENTRY RecordNotUnique.new(message) - when ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 + when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 InvalidForeignKey.new(message) when ER_CANNOT_ADD_FOREIGN mismatched_foreign_key(message) diff --git a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb index 3dcb916d99..f158946c6d 100644 --- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb @@ -10,8 +10,17 @@ module ActiveRecord super end - def visit_Arel_Nodes_In(*) + def visit_Arel_Nodes_In(o, collector) @preparable = false + + if Array === o.right && !o.right.empty? + o.right.delete_if do |bind| + if Arel::Nodes::BindParam === bind && Relation::QueryAttribute === bind.value + !bind.value.boundable? + end + end + end + super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index bc6eb11572..a11a786ec7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -5,7 +5,7 @@ gem "pg", ">= 0.18", "< 2.0" require "pg" # Use async_exec instead of exec_params on pg versions before 1.1 -class ::PG::Connection +class ::PG::Connection # :nodoc: unless self.public_method_defined?(:async_exec_params) remove_method :exec_params alias exec_params async_exec @@ -43,9 +43,14 @@ module ActiveRecord valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl] conn_params.slice!(*valid_conn_param_keys) - # The postgres drivers don't allow the creation of an unconnected PG::Connection object, - # so just pass a nil connection object for the time being. - ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config) + conn = PG.connect(conn_params) + ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config) + rescue ::PG::Error => error + if error.message.include?("does not exist") + raise ActiveRecord::NoDatabaseError + else + raise + end end end @@ -220,15 +225,11 @@ module ActiveRecord @local_tz = nil @max_identifier_length = nil - connect + configure_connection add_pg_encoders @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config[:statement_limit]) - if postgresql_version < 90100 - raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." - end - add_pg_decoders @type_map = Type::HashLookupTypeMap.new @@ -410,6 +411,12 @@ module ActiveRecord end private + def check_version + if postgresql_version < 90100 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." + end + end + # See https://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" NUMERIC_VALUE_OUT_OF_RANGE = "22003" @@ -699,12 +706,6 @@ module ActiveRecord def connect @connection = PG.connect(@connection_parameters) configure_connection - rescue ::PG::Error => error - if error.message.include?("does not exist") - raise ActiveRecord::NoDatabaseError - else - raise - end end # Configures the encoding, verbosity, schema search path, and time zone of the connection. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 81882f6cc1..e0355a316b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -105,11 +105,6 @@ module ActiveRecord @active = true @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - - if sqlite_version < "3.8.0" - raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." - end - configure_connection end @@ -401,6 +396,12 @@ module ActiveRecord end private + def check_version + if sqlite_version < "3.8.0" + raise "Your version of SQLite (#{sqlite_version}) is too old. Active Record supports SQLite >= 3.8." + end + end + def initialize_type_map(m = type_map) super register_class_with_limit m, %r(int)i, SQLite3Integer diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 18114f9e1c..5141271db9 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -47,6 +47,92 @@ module ActiveRecord # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. def establish_connection(config_or_env = nil) + config_hash = resolve_config_for_connection(config_or_env) + connection_handler.establish_connection(config_hash) + end + + # Connects a model to the databases specified. The +database+ keyword + # takes a hash consisting of a +role+ and a +database_key+. + # + # This will create a connection handler for switching between connections, + # look up the config hash using the +database_key+ and finally + # establishes a connection to that config. + # + # class AnimalsModel < ApplicationRecord + # self.abstract_class = true + # + # connects_to database: { writing: :primary, reading: :primary_replica } + # end + # + # Returns an array of established connections. + def connects_to(database: {}) + connections = [] + + database.each do |role, database_key| + config_hash = resolve_config_for_connection(database_key) + handler = lookup_connection_handler(role.to_sym) + + connections << handler.establish_connection(config_hash) + end + + connections + end + + # Connects to a database or role (ex writing, reading, or another + # custom role) for the duration of the block. + # + # If a role is passed, Active Record will look up the connection + # based on the requested role: + # + # ActiveRecord::Base.connected_to(role: :writing) do + # Dog.create! # creates dog using dog connection + # end + # + # ActiveRecord::Base.connected_to(role: :reading) do + # Dog.create! # throws exception because we're on a replica + # end + # + # ActiveRecord::Base.connected_to(role: :unknown_ode) do + # # raises exception due to non-existent role + # end + # + # For cases where you may want to connect to a database outside of the model, + # you can use +connected_to+ with a +database+ argument. The +database+ argument + # expects a symbol that corresponds to the database key in your config. + # + # This will connect to a new database for the queries inside the block. + # + # ActiveRecord::Base.connected_to(database: :animals_slow_replica) do + # Dog.run_a_long_query # runs a long query while connected to the +animals_slow_replica+ + # end + def connected_to(database: nil, role: nil, &blk) + if database && role + raise ArgumentError, "connected_to can only accept a database or role argument, but not both arguments." + elsif database + config_hash = resolve_config_for_connection(database) + handler = lookup_connection_handler(database.to_sym) + + with_handler(database.to_sym) do + handler.establish_connection(config_hash) + return yield + end + elsif role + with_handler(role.to_sym, &blk) + else + raise ArgumentError, "must provide a `database` or a `role`." + end + end + + def lookup_connection_handler(handler_key) # :nodoc: + connection_handlers[handler_key] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new + end + + def with_handler(handler_key, &blk) # :nodoc: + handler = lookup_connection_handler(handler_key) + swap_connection_handler(handler, &blk) + end + + def resolve_config_for_connection(config_or_env) # :nodoc: raise "Anonymous class is not allowed." unless name config_or_env ||= DEFAULT_ENV.call.to_sym @@ -57,7 +143,7 @@ module ActiveRecord config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys config_hash[:name] = pool_name - connection_handler.establish_connection(config_hash) + config_hash end # Returns the connection currently associated with the class. This can @@ -118,5 +204,14 @@ module ActiveRecord delegate :clear_active_connections!, :clear_reloadable_connections!, :clear_all_connections!, :flush_idle_connections!, to: :connection_handler + + private + + def swap_connection_handler(handler, &blk) # :nodoc: + old_handler, ActiveRecord::Base.connection_handler = ActiveRecord::Base.connection_handler, handler + yield + ensure + ActiveRecord::Base.connection_handler = old_handler + end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 392602bc0f..0718688863 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -124,6 +124,8 @@ module ActiveRecord mattr_accessor :belongs_to_required_by_default, instance_accessor: false + mattr_accessor :connection_handlers, instance_accessor: false, default: {} + class_attribute :default_connection_handler, instance_writer: false self.filter_attributes = [] @@ -137,6 +139,7 @@ module ActiveRecord end self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new + self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } end module ClassMethods @@ -341,34 +344,22 @@ module ActiveRecord # post = Post.allocate # post.init_with(coder) # post.title # => 'hello world' - def init_with(coder) + def init_with(coder, &block) coder = LegacyYamlAdapter.convert(self.class, coder) - @attributes = self.class.yaml_encoder.decode(coder) - - init_internals - - @new_record = coder["new_record"] - - self.class.define_attribute_methods - - yield self if block_given? - - _run_find_callbacks - _run_initialize_callbacks - - self + attributes = self.class.yaml_encoder.decode(coder) + init_with_attributes(attributes, coder["new_record"], &block) end ## - # Initializer used for instantiating objects that have been read from the - # database. +attributes+ should be an attributes object, and unlike the + # Initialize an empty model object from +attributes+. + # +attributes+ should be an attributes object, and unlike the # `initialize` method, no assignment calls are made per attribute. # # :nodoc: - def init_from_db(attributes) + def init_with_attributes(attributes, new_record = false) init_internals - @new_record = false + @new_record = new_record @attributes = attributes self.class.define_attribute_methods diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index fa1589511e..30cb0a27e7 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -29,7 +29,7 @@ module ActiveRecord # configs for all environments. # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults # to +nil+. - # <tt>include_replicas:</tt> Determines whether to include replicas in the + # <tt>include_replicas:</tt> Determines whether to include replicas in # the returned list. Most of the time we're only iterating over the write # connection (i.e. migrations don't need to run for the write and read connection). # Defaults to +false+. diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 23ecb24542..3d97e4e513 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -150,6 +150,7 @@ module ActiveRecord enum_prefix = definitions.delete(:_prefix) enum_suffix = definitions.delete(:_suffix) definitions.each do |name, values| + assert_valid_enum_definition_values(values) # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new name = name.to_s @@ -210,10 +211,20 @@ module ActiveRecord end end + def assert_valid_enum_definition_values(values) + unless values.is_a?(Hash) || values.all? { |v| v.is_a?(Symbol) } || values.all? { |v| v.is_a?(String) } + error_message = <<~MSG + Enum values #{values} must be either a hash, an array of symbols, or an array of strings. + MSG + raise ArgumentError, error_message + end + end + ENUM_CONFLICT_MESSAGE = \ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \ "this will generate a %{type} method \"%{method}\", which is already defined " \ "by %{source}." + private_constant :ENUM_CONFLICT_MESSAGE def detect_enum_conflict!(enum_name, method_name, klass_method = false) if klass_method && dangerous_class_method?(method_name) diff --git a/activerecord/lib/active_record/fixture_set/model_metadata.rb b/activerecord/lib/active_record/fixture_set/model_metadata.rb new file mode 100644 index 0000000000..edc03939c8 --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/model_metadata.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module ActiveRecord + class FixtureSet + class ModelMetadata # :nodoc: + def initialize(model_class, table_name) + @model_class = model_class + @table_name = table_name + end + + def primary_key_name + @primary_key_name ||= @model_class && @model_class.primary_key + end + + def primary_key_type + @primary_key_type ||= @model_class && @model_class.type_for_attribute(@model_class.primary_key).type + end + + def has_primary_key_column? + @has_primary_key_column ||= primary_key_name && + @model_class.columns.any? { |col| col.name == primary_key_name } + end + + def timestamp_column_names + @timestamp_column_names ||= + %w(created_at created_on updated_at updated_on) & column_names + end + + def inheritance_column_name + @inheritance_column_name ||= @model_class && @model_class.inheritance_column + end + + private + + def column_names + @column_names ||= @model_class.connection.columns(@table_name).collect(&:name) + end + end + end +end diff --git a/activerecord/lib/active_record/fixture_set/table_row.rb b/activerecord/lib/active_record/fixture_set/table_row.rb new file mode 100644 index 0000000000..5f72c1df38 --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/table_row.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module ActiveRecord + class FixtureSet + class TableRow # :nodoc: + def initialize(fixture, table_rows:, label:, now:) + @table_rows = table_rows + @label = label + @now = now + @row = fixture.to_hash + fill_row_model_attributes + end + + def to_hash + @row + end + + private + + def model_metadata + @table_rows.model_metadata + end + + def model_class + @table_rows.model_class + end + + def fill_row_model_attributes + return unless model_class + fill_timestamps + interpolate_label + generate_primary_key + resolve_enums + @table_rows.resolve_sti_reflections(@row) + end + + def reflection_class + @reflection_class ||= if @row.include?(model_metadata.inheritance_column_name) + @row[model_metadata.inheritance_column_name].constantize rescue model_class + else + model_class + end + end + + def fill_timestamps + # fill in timestamp columns if they aren't specified and the model is set to record_timestamps + if model_class.record_timestamps + model_metadata.timestamp_column_names.each do |c_name| + @row[c_name] = @now unless @row.key?(c_name) + end + end + end + + def interpolate_label + # interpolate the fixture label + @row.each do |key, value| + @row[key] = value.gsub("$LABEL", @label.to_s) if value.is_a?(String) + end + end + + def generate_primary_key + # generate a primary key if necessary + if model_metadata.has_primary_key_column? && !@row.include?(model_metadata.primary_key_name) + @row[model_metadata.primary_key_name] = ActiveRecord::FixtureSet.identify( + @label, model_metadata.primary_key_type + ) + end + end + + def resolve_enums + model_class.defined_enums.each do |name, values| + if @row.include?(name) + @row[name] = values.fetch(@row[name], @row[name]) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/fixture_set/table_rows.rb b/activerecord/lib/active_record/fixture_set/table_rows.rb new file mode 100644 index 0000000000..e8335a2e10 --- /dev/null +++ b/activerecord/lib/active_record/fixture_set/table_rows.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require "active_record/fixture_set/table_row" +require "active_record/fixture_set/model_metadata" + +module ActiveRecord + class FixtureSet + class TableRows # :nodoc: + class ReflectionProxy # :nodoc: + def initialize(association) + @association = association + end + + def join_table + @association.join_table + end + + def name + @association.name + end + + def primary_key_type + @association.klass.type_for_attribute(@association.klass.primary_key).type + end + end + + class HasManyThroughProxy < ReflectionProxy # :nodoc: + def rhs_key + @association.foreign_key + end + + def lhs_key + @association.through_reflection.foreign_key + end + + def join_table + @association.through_reflection.table_name + end + end + + def initialize(table_name, model_class:, fixtures:, config:) + @table_name = table_name + @model_class = model_class + + # track any join tables we need to insert later + @tables = Hash.new { |h, table| h[table] = [] } + + build_table_rows_from(fixtures, config) + end + + attr_reader :table_name, :model_class + + def to_hash + @tables.transform_values { |rows| rows.map(&:to_hash) } + end + + def model_metadata + @model_metadata ||= ModelMetadata.new(model_class, table_name) + end + + def resolve_sti_reflections(row) + # If STI is used, find the correct subclass for association reflection + reflection_class = reflection_class_for(row) + + reflection_class._reflections.each_value do |association| + case association.macro + when :belongs_to + # Do not replace association name with association foreign key if they are named the same + fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s + + if association.name.to_s != fk_name && value = row.delete(association.name.to_s) + if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + row[association.foreign_type] = $1 + end + + fk_type = reflection_class.type_for_attribute(fk_name).type + row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) + end + when :has_many + if association.options[:through] + add_join_records(row, HasManyThroughProxy.new(association)) + end + end + end + end + + private + + def build_table_rows_from(fixtures, config) + now = config.default_timezone == :utc ? Time.now.utc : Time.now + + @tables[table_name] = fixtures.map do |label, fixture| + TableRow.new( + fixture, + table_rows: self, + label: label, + now: now, + ) + end + end + + def reflection_class_for(row) + if row.include?(model_metadata.inheritance_column_name) + row[model_metadata.inheritance_column_name].constantize rescue model_class + else + model_class + end + end + + def add_join_records(row, association) + # This is the case when the join table has no fixtures file + if (targets = row.delete(association.name.to_s)) + table_name = association.join_table + column_type = association.primary_key_type + lhs_key = association.lhs_key + rhs_key = association.rhs_key + + targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) + joins = targets.map do |target| + { lhs_key => row[model_metadata.primary_key_name], + rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } + end + @tables[table_name].concat(joins) + end + end + end + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index e697c30bb6..b090c76a38 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -8,6 +8,7 @@ require "active_support/dependencies" require "active_support/core_ext/digest/uuid" require "active_record/fixture_set/file" require "active_record/fixture_set/render_context" +require "active_record/fixture_set/table_rows" require "active_record/test_fixtures" require "active_record/errors" @@ -442,60 +443,6 @@ module ActiveRecord @@all_cached_fixtures = Hash.new { |h, k| h[k] = {} } - def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: - config.pluralize_table_names ? - fixture_set_name.singularize.camelize : - fixture_set_name.camelize - end - - def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: - "#{ config.table_name_prefix }"\ - "#{ fixture_set_name.tr('/', '_') }"\ - "#{ config.table_name_suffix }".to_sym - end - - def self.reset_cache - @@all_cached_fixtures.clear - end - - def self.cache_for_connection(connection) - @@all_cached_fixtures[connection] - end - - def self.fixture_is_cached?(connection, table_name) - cache_for_connection(connection)[table_name] - end - - def self.cached_fixtures(connection, keys_to_fetch = nil) - if keys_to_fetch - cache_for_connection(connection).values_at(*keys_to_fetch) - else - cache_for_connection(connection).values - end - end - - def self.cache_fixtures(connection, fixtures_map) - cache_for_connection(connection).update(fixtures_map) - end - - def self.instantiate_fixtures(object, fixture_set, load_instances = true) - if load_instances - fixture_set.each do |fixture_name, fixture| - begin - object.instance_variable_set "@#{fixture_name}", fixture.find - rescue FixtureClassNotFound - nil - end - end - end - end - - def self.instantiate_all_loaded_fixtures(object, load_instances = true) - all_loaded_fixtures.each_value do |fixture_set| - instantiate_fixtures(object, fixture_set, load_instances) - end - end - cattr_accessor :all_loaded_fixtures, default: {} class ClassCache @@ -504,14 +451,16 @@ module ActiveRecord @config = config # Remove string values that aren't constants or subclasses of AR - @class_names.delete_if { |klass_name, klass| !insert_class(@class_names, klass_name, klass) } + @class_names.delete_if do |klass_name, klass| + !insert_class(@class_names, klass_name, klass) + end end def [](fs_name) - @class_names.fetch(fs_name) { + @class_names.fetch(fs_name) do klass = default_fixture_model(fs_name, @config).safe_constantize insert_class(@class_names, fs_name, klass) - } + end end private @@ -530,38 +479,129 @@ module ActiveRecord end end - def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) - fixture_set_names = Array(fixture_set_names).map(&:to_s) - class_names = ClassCache.new class_names, config + class << self + def default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + config.pluralize_table_names ? + fixture_set_name.singularize.camelize : + fixture_set_name.camelize + end + + def default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: + "#{ config.table_name_prefix }"\ + "#{ fixture_set_name.tr('/', '_') }"\ + "#{ config.table_name_suffix }".to_sym + end + + def reset_cache + @@all_cached_fixtures.clear + end - # FIXME: Apparently JK uses this. - connection = block_given? ? yield : ActiveRecord::Base.connection + def cache_for_connection(connection) + @@all_cached_fixtures[connection] + end - files_to_read = fixture_set_names.reject { |fs_name| - fixture_is_cached?(connection, fs_name) - } + def fixture_is_cached?(connection, table_name) + cache_for_connection(connection)[table_name] + end - unless files_to_read.empty? - fixtures_map = {} + def cached_fixtures(connection, keys_to_fetch = nil) + if keys_to_fetch + cache_for_connection(connection).values_at(*keys_to_fetch) + else + cache_for_connection(connection).values + end + end + + def cache_fixtures(connection, fixtures_map) + cache_for_connection(connection).update(fixtures_map) + end + + def instantiate_fixtures(object, fixture_set, load_instances = true) + return unless load_instances + fixture_set.each do |fixture_name, fixture| + begin + object.instance_variable_set "@#{fixture_name}", fixture.find + rescue FixtureClassNotFound + nil + end + end + end + + def instantiate_all_loaded_fixtures(object, load_instances = true) + all_loaded_fixtures.each_value do |fixture_set| + instantiate_fixtures(object, fixture_set, load_instances) + end + end + + def create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base) + fixture_set_names = Array(fixture_set_names).map(&:to_s) + class_names = ClassCache.new class_names, config + + # FIXME: Apparently JK uses this. + connection = block_given? ? yield : ActiveRecord::Base.connection + + fixture_files_to_read = fixture_set_names.reject do |fs_name| + fixture_is_cached?(connection, fs_name) + end + + if fixture_files_to_read.any? + fixtures_map = read_and_insert( + fixtures_directory, + fixture_files_to_read, + class_names, + connection, + ) + cache_fixtures(connection, fixtures_map) + end + cached_fixtures(connection, fixture_set_names) + end + + # Returns a consistent, platform-independent identifier for +label+. + # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. + def identify(label, column_type = :integer) + if column_type == :uuid + Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) + else + Zlib.crc32(label.to_s) % MAX_ID + end + end - fixture_sets = files_to_read.map do |fs_name| - klass = class_names[fs_name] - conn = klass ? klass.connection : connection - fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new + # Superclass for the evaluation contexts used by ERB fixtures. + def context_class + @context_class ||= Class.new + end + + private + + def read_and_insert(fixtures_directory, fixture_files, class_names, connection) # :nodoc: + fixtures_map = {} + fixture_sets = fixture_files.map do |fixture_set_name| + klass = class_names[fixture_set_name] + conn = klass&.connection || connection + fixtures_map[fixture_set_name] = new( # ActiveRecord::FixtureSet.new conn, - fs_name, + fixture_set_name, klass, - ::File.join(fixtures_directory, fs_name)) + ::File.join(fixtures_directory, fixture_set_name) + ) end + update_all_loaded_fixtures(fixtures_map) - update_all_loaded_fixtures fixtures_map - fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection } + insert(fixture_sets, connection) + + fixtures_map + end + + def insert(fixture_sets, connection) # :nodoc: + fixture_sets_by_connection = fixture_sets.group_by do |fixture_set| + fixture_set.model_class&.connection || connection + end fixture_sets_by_connection.each do |conn, set| table_rows_for_connection = Hash.new { |h, k| h[k] = [] } - set.each do |fs| - fs.table_rows.each do |table, rows| + set.each do |fixture_set| + fixture_set.table_rows.each do |table, rows| table_rows_for_connection[table].unshift(*rows) end end @@ -572,31 +612,13 @@ module ActiveRecord set.each { |fs| conn.reset_pk_sequence!(fs.table_name) } end end - - cache_fixtures(connection, fixtures_map) end - cached_fixtures(connection, fixture_set_names) - end - # Returns a consistent, platform-independent identifier for +label+. - # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. - def self.identify(label, column_type = :integer) - if column_type == :uuid - Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) - else - Zlib.crc32(label.to_s) % MAX_ID + def update_all_loaded_fixtures(fixtures_map) # :nodoc: + all_loaded_fixtures.update(fixtures_map) end end - # Superclass for the evaluation contexts used by ERB fixtures. - def self.context_class - @context_class ||= Class.new - end - - def self.update_all_loaded_fixtures(fixtures_map) # :nodoc: - all_loaded_fixtures.update(fixtures_map) - end - attr_reader :table_name, :name, :fixtures, :model_class, :config def initialize(connection, name, class_name, path, config = ActiveRecord::Base) @@ -634,152 +656,18 @@ module ActiveRecord # Returns a hash of rows to be inserted. The key is the table, the value is # a list of rows to insert to that table. def table_rows - now = config.default_timezone == :utc ? Time.now.utc : Time.now - # allow a standard key to be used for doing defaults in YAML fixtures.delete("DEFAULTS") - # track any join tables we need to insert later - rows = Hash.new { |h, table| h[table] = [] } - - rows[table_name] = fixtures.map do |label, fixture| - row = fixture.to_hash - - if model_class - # fill in timestamp columns if they aren't specified and the model is set to record_timestamps - if model_class.record_timestamps - timestamp_column_names.each do |c_name| - row[c_name] = now unless row.key?(c_name) - end - end - - # interpolate the fixture label - row.each do |key, value| - row[key] = value.gsub("$LABEL", label.to_s) if value.is_a?(String) - end - - # generate a primary key if necessary - if has_primary_key_column? && !row.include?(primary_key_name) - row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type) - end - - # Resolve enums - model_class.defined_enums.each do |name, values| - if row.include?(name) - row[name] = values.fetch(row[name], row[name]) - end - end - - # If STI is used, find the correct subclass for association reflection - reflection_class = - if row.include?(inheritance_column_name) - row[inheritance_column_name].constantize rescue model_class - else - model_class - end - - reflection_class._reflections.each_value do |association| - case association.macro - when :belongs_to - # Do not replace association name with association foreign key if they are named the same - fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s - - if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - # support polymorphic belongs_to as "label (Type)" - row[association.foreign_type] = $1 - end - - fk_type = reflection_class.type_for_attribute(fk_name).type - row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) - end - when :has_many - if association.options[:through] - add_join_records(rows, row, HasManyThroughProxy.new(association)) - end - end - end - end - - row - end - rows - end - - class ReflectionProxy # :nodoc: - def initialize(association) - @association = association - end - - def join_table - @association.join_table - end - - def name - @association.name - end - - def primary_key_type - @association.klass.type_for_attribute(@association.klass.primary_key).type - end - end - - class HasManyThroughProxy < ReflectionProxy # :nodoc: - def rhs_key - @association.foreign_key - end - - def lhs_key - @association.through_reflection.foreign_key - end - - def join_table - @association.through_reflection.table_name - end + TableRows.new( + table_name, + model_class: model_class, + fixtures: fixtures, + config: config, + ).to_hash end private - def primary_key_name - @primary_key_name ||= model_class && model_class.primary_key - end - - def primary_key_type - @primary_key_type ||= model_class && model_class.type_for_attribute(model_class.primary_key).type - end - - def add_join_records(rows, row, association) - # This is the case when the join table has no fixtures file - if (targets = row.delete(association.name.to_s)) - table_name = association.join_table - column_type = association.primary_key_type - lhs_key = association.lhs_key - rhs_key = association.rhs_key - - targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - rows[table_name].concat targets.map { |target| - { lhs_key => row[primary_key_name], - rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } - } - end - end - - def has_primary_key_column? - @has_primary_key_column ||= primary_key_name && - model_class.columns.any? { |c| c.name == primary_key_name } - end - - def timestamp_column_names - @timestamp_column_names ||= - %w(created_at created_on updated_at updated_on) & column_names - end - - def inheritance_column_name - @inheritance_column_name ||= model_class && model_class.inheritance_column - end - - def column_names - @column_names ||= @connection.columns(@table_name).collect(&:name) - end def model_class=(class_name) if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? @@ -843,12 +731,9 @@ module ActiveRecord alias :to_hash :fixture def find - if model_class - model_class.unscoped do - model_class.find(fixture[model_class.primary_key]) - end - else - raise FixtureClassNotFound, "No class attached to find." + raise FixtureClassNotFound, "No class attached to find." unless model_class + model_class.unscoped do + model_class.find(fixture[model_class.primary_key]) end end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 6cf26a9792..456689ec6d 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -60,7 +60,7 @@ module ActiveRecord # the cache key will also include a version. # # Product.cache_versioning = false - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + # Product.find(5).cache_key # => "products/5-20071224150000" (updated_at available) def cache_key(*timestamp_names) if new_record? "#{model_name.cache_key}/new" diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 5a4a1fb969..c50a420432 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -388,6 +388,11 @@ module ActiveRecord @column_names ||= columns.map(&:name) end + def symbol_column_to_string(name_symbol) # :nodoc: + @symbol_column_to_string_name_hash ||= column_names.index_by(&:to_sym) + @symbol_column_to_string_name_hash[name_symbol] + end + # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", # and columns used for single table inheritance have been removed. def content_columns @@ -477,6 +482,7 @@ module ActiveRecord def reload_schema_from_cache @arel_table = nil @column_names = nil + @symbol_column_to_string_name_hash = nil @attribute_types = nil @content_columns = nil @default_attributes = nil diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 50767ee93f..8b9098df6c 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -426,7 +426,7 @@ module ActiveRecord existing_record.assign_attributes(assignable_attributes) association(association_name).initialize_attributes(existing_record) else - method = "build_#{association_name}" + method = :"build_#{association_name}" if respond_to?(method) send(method, assignable_attributes) else diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 6eb2bfb452..45ceb1e3ad 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -209,7 +209,7 @@ module ActiveRecord # new instance of the class. Accepts only keys as strings. def instantiate_instance_of(klass, attributes, column_types = {}, &block) attributes = klass.attributes_builder.build_from_database(attributes, column_types) - klass.allocate.init_from_db(attributes, &block) + klass.allocate.init_with_attributes(attributes, &block) end # Called by +instantiate+ to decide which class to use for a new @@ -758,6 +758,8 @@ module ActiveRecord @_association_destroy_exception = nil end + # The name of the method used to touch a +belongs_to+ association when the + # +:touch+ option is used. def belongs_to_touch_method :touch end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index be5ef350a9..1c7ceb4981 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -149,18 +149,21 @@ db_namespace = namespace :db do desc "Display status of migrations" task status: :load_config do - unless ActiveRecord::SchemaMigration.table_exists? - abort "Schema migrations table does not exist yet." + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate_status end + end - # output - puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" - puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" - puts "-" * 50 - ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| - puts "#{status.center(8)} #{version.ljust(14)} #{name}" + namespace :status do + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Display status of migrations for #{spec_name} database" + task spec_name => :load_config do + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate_status + end end - puts end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 8f657840f5..383dc1bf4b 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -17,6 +17,7 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } + include_relation_methods(delegate) mangled_name = klass.name.gsub("::", "_") const_set mangled_name, delegate private_constant mangled_name @@ -29,6 +30,35 @@ module ActiveRecord child_class.initialize_relation_delegate_cache super end + + protected + def include_relation_methods(delegate) + superclass.include_relation_methods(delegate) unless base_class? + delegate.include generated_relation_methods + end + + private + def generated_relation_methods + @generated_relation_methods ||= Module.new.tap do |mod| + mod_name = "GeneratedRelationMethods" + const_set mod_name, mod + private_constant mod_name + end + end + + def generate_relation_method(method) + if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) + generated_relation_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args, &block) + scoping { klass.#{method}(*args, &block) } + end + RUBY + else + generated_relation_methods.send(:define_method, method) do |*args, &block| + scoping { klass.public_send(method, *args, &block) } + end + end + end end extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index fadb3c420d..ee2ece1560 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -22,9 +22,8 @@ module ActiveRecord when 1 then predicate_builder.build(attribute, values.first) else values.map! do |v| - bind = predicate_builder.build_bind_attribute(attribute.name, v) - bind if bind.value.boundable? - end.compact! + predicate_builder.build_bind_attribute(attribute.name, v) + end values.empty? ? NullPredicate : attribute.in(values) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 5170d38fa8..eb80aab701 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -909,21 +909,18 @@ module ActiveRecord @arel ||= build_arel(aliases) end - # Returns a relation value with a given name - def get_value(name) # :nodoc: - @values.fetch(name, DEFAULT_VALUES[name]) - end - - protected + private + # Returns a relation value with a given name + def get_value(name) + @values.fetch(name, DEFAULT_VALUES[name]) + end # Sets the relation value with the given name - def set_value(name, value) # :nodoc: + def set_value(name, value) assert_mutability! @values[name] = value end - private - def assert_mutability! raise ImmutableRelation if @loaded raise ImmutableRelation if defined?(@arel) && @arel @@ -1188,8 +1185,9 @@ module ActiveRecord STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references] def structurally_incompatible_values_for_or(other) + values = other.values STRUCTURAL_OR_METHODS.reject do |method| - get_value(method) == other.get_value(method) + get_value(method) == values.fetch(method, DEFAULT_VALUES[method]) end end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 573d97b819..d5cc5db97e 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -191,6 +191,8 @@ module ActiveRecord scope end end + + generate_relation_method(name) end private diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 5e29085aff..974d7a1c0a 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -197,6 +197,21 @@ module ActiveRecord Migration.verbose = verbose_was end + def migrate_status + unless ActiveRecord::SchemaMigration.table_exists? + Kernel.abort "Schema migrations table does not exist yet." + end + + # output + puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" + puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" + puts "-" * 50 + ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name| + puts "#{status.center(8)} #{version.ljust(14)} #{name}" + end + puts + end + def check_target_version if target_version && !(Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"])) raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`" diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb index 9d9294fecc..32f6705d04 100644 --- a/activerecord/lib/arel/visitors/mysql.rb +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -9,20 +9,20 @@ module Arel # :nodoc: all collector << "( " end - collector = case o.left - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.left, collector, true - else - visit o.left, collector + case o.left + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.left, collector, true + else + visit o.left, collector end collector << " UNION " - collector = case o.right - when Arel::Nodes::Union - visit_Arel_Nodes_Union o.right, collector, true - else - visit o.right, collector + case o.right + when Arel::Nodes::Union + visit_Arel_Nodes_Union o.right, collector, true + else + visit o.right, collector end if suppress_parens @@ -75,14 +75,7 @@ module Arel # :nodoc: all o end end - - def prepare_delete_statement(o) - if o.offset || has_join_sources?(o) - super - else - o - end - end + alias :prepare_delete_statement :prepare_update_statement # MySQL is too stupid to create a temporary table for use subquery, so we have # to give it some prompting in the form of a subsubquery. diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 7c0f6c2e97..7ce26884a5 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -76,10 +76,18 @@ module Arel # :nodoc: all def visit_Arel_Nodes_DeleteStatement(o, collector) o = prepare_delete_statement(o) - collector << "DELETE FROM " + if has_join_sources?(o) + collector << "DELETE " + visit o.relation.left, collector + collector << " FROM " + else + collector << "DELETE FROM " + end collector = visit o.relation, collector - collect_where_for(o, collector) + collect_nodes_for o.wheres, collector, " WHERE ", " AND " + collect_nodes_for o.orders, collector, " ORDER BY " + maybe_visit o.limit, collector end def visit_Arel_Nodes_UpdateStatement(o, collector) @@ -87,12 +95,11 @@ module Arel # :nodoc: all collector << "UPDATE " collector = visit o.relation, collector - unless o.values.empty? - collector << " SET " - collector = inject_join o.values, collector, ", " - end + collect_nodes_for o.values, collector, " SET " - collect_where_for(o, collector) + collect_nodes_for o.wheres, collector, " WHERE ", " AND " + collect_nodes_for o.orders, collector, " ORDER BY " + maybe_visit o.limit, collector end def visit_Arel_Nodes_InsertStatement(o, collector) @@ -225,10 +232,7 @@ module Arel # :nodoc: all collect_nodes_for o.wheres, collector, WHERE, AND collect_nodes_for o.groups, collector, GROUP_BY - unless o.havings.empty? - collector << " HAVING " - inject_join o.havings, collector, AND - end + collect_nodes_for o.havings, collector, " HAVING ", AND collect_nodes_for o.windows, collector, WINDOW collector @@ -237,11 +241,7 @@ module Arel # :nodoc: all def collect_nodes_for(nodes, collector, spacer, connector = COMMA) unless nodes.empty? collector << spacer - len = nodes.length - 1 - nodes.each_with_index do |x, i| - collector = visit(x, collector) - collector << connector unless len == i - end + inject_join nodes, collector, connector end end @@ -296,10 +296,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_Window(o, collector) collector << "(" - if o.partitions.any? - collector << "PARTITION BY " - collector = inject_join o.partitions, collector, ", " - end + collect_nodes_for o.partitions, collector, "PARTITION BY " if o.orders.any? collector << SPACE if o.partitions.any? @@ -830,20 +827,6 @@ module Arel # :nodoc: all stmt end - def collect_where_for(o, collector) - unless o.wheres.empty? - collector << " WHERE " - collector = inject_join o.wheres, collector, " AND " - end - - unless o.orders.empty? - collector << " ORDER BY " - collector = inject_join o.orders, collector, ", " - end - - maybe_visit o.limit, collector - end - def infix_value(o, collector, value) collector = visit o.left, collector collector << value diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index d520919332..93dd427951 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -367,6 +367,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal "ODEGY", odegy_account.reload_firm.name end + def test_reload_the_belonging_object_with_query_cache + odegy_account_id = accounts(:odegy_account).id + + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + odegy_account = Account.find(odegy_account_id) + + # Populate the cache with a second query + odegy_account.firm + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the firm again, populating the cache with a query + assert_queries(1) { odegy_account.reload_firm } + + # This query is not cached anymore, so it should make a real SQL query + assert_queries(1) { Account.find(odegy_account_id) } + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + def test_natural_assignment_to_nil client = Client.find(3) client.firm = nil diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 39034746c9..ed7715c1e2 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -34,7 +34,7 @@ class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase fixtures :citations def test_preloading_too_many_ids - assert_equal Citation.count, Citation.preload(:citations).to_a.size + assert_equal Citation.count, Citation.preload(:reference_of).to_a.size end def test_eager_loading_too_may_ids diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index a90dcc0576..515eb65d37 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -25,6 +25,8 @@ require "models/user" require "models/member" require "models/membership" require "models/sponsor" +require "models/lesson" +require "models/student" require "models/country" require "models/treaty" require "models/vertex" @@ -275,7 +277,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_habtm_saving_multiple_relationships new_project = Project.new("name" => "Grimetime") amount_of_developers = 4 - developers = (0...amount_of_developers).collect { |i| Developer.create(name: "JME #{i}") }.reverse + developers = (0...amount_of_developers).reverse_each.map { |i| Developer.create(name: "JME #{i}") } new_project.developer_ids = [developers[0].id, developers[1].id] new_project.developers_with_callback_ids = [developers[2].id, developers[3].id] @@ -310,6 +312,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_build devel = Developer.find(1) + + # Load schema information so we don't query below if running just this test. + Project.define_attribute_methods + proj = assert_no_queries { devel.projects.build("name" => "Projekt") } assert_not_predicate devel.projects, :loaded? @@ -325,6 +331,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build devel = Developer.find(1) + + # Load schema information so we don't query below if running just this test. + Project.define_attribute_methods + proj = assert_no_queries { devel.projects.new("name" => "Projekt") } assert_not_predicate devel.projects, :loaded? @@ -772,6 +782,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort end + def test_singular_ids_are_reloaded_after_collection_concat + student = Student.create(name: "Alberto Almagro") + student.lesson_ids + + lesson = Lesson.create(name: "DSI") + student.lessons << lesson + + assert_includes student.lesson_ids, lesson.id + end + def test_scoped_find_on_through_association_doesnt_return_read_only_records tag = Post.find(1).tags.find_by_name("General") diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 036df1c31e..d13e1a86e9 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -458,6 +458,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_finder_method_with_dirty_target company = companies(:first_firm) new_clients = [] + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + assert_no_queries do new_clients << company.clients_of_firm.build(name: "Another Client") new_clients << company.clients_of_firm.build(name: "Another Client II") @@ -478,6 +482,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_finder_bang_method_with_dirty_target company = companies(:first_firm) new_clients = [] + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + assert_no_queries do new_clients << company.clients_of_firm.build(name: "Another Client") new_clients << company.clients_of_firm.build(name: "Another Client II") @@ -821,6 +829,48 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_not_same original_object, collection.first, "Expected #first after #reload to return a new object" end + def test_reload_with_query_cache + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + firm = Firm.first + # Populate the cache with a second query + firm.clients.load + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the clients again, populating the cache with a query + assert_queries(1) { firm.clients.reload } + # This query is cached, so it shouldn't make a real SQL query + assert_queries(0) { firm.clients.load } + + assert_equal 1, connection.query_cache.size + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + + def test_reloading_unloaded_associations_with_query_cache + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + firm = Firm.create!(name: "firm name") + client = firm.clients.create!(name: "client name") + firm.clients.to_a # add request to cache + + connection.uncached do + client.update!(name: "new client name") + end + + firm = Firm.find(firm.id) + + assert_equal [client.name], firm.clients.reload.map(&:name) + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + def test_find_all_with_include_and_conditions assert_nothing_raised do Developer.all.merge!(joins: :audit_logs, where: { "audit_logs.message" => nil, :name => "Smith" }).to_a @@ -955,8 +1005,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_adding_to_new_record + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + firm = Firm.new assert_no_queries do - firm = Firm.new firm.clients_of_firm.concat(Client.new("name" => "Natural Company")) end end @@ -970,6 +1023,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build company = companies(:first_firm) + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? @@ -980,6 +1037,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build company = companies(:first_firm) + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? @@ -1037,6 +1098,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many company = companies(:first_firm) + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + new_clients = assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } assert_equal 2, new_clients.size end @@ -1049,10 +1114,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_without_loading_association first_topic = topics(:first) - Reply.column_names assert_equal 1, first_topic.replies.length + # Load schema information so we don't query below if running just this test. + Reply.define_attribute_methods + assert_no_queries do first_topic.replies.build(title: "Not saved", content: "Superstars") assert_equal 2, first_topic.replies.size @@ -1063,6 +1130,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_via_block company = companies(:first_firm) + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert_not_predicate company.clients_of_firm, :loaded? @@ -1073,6 +1144,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many_via_block company = companies(:first_firm) + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + new_clients = assert_no_queries do company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" @@ -1086,8 +1161,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_without_loading_association first_firm = companies(:first_firm) - Firm.column_names - Client.column_names assert_equal 2, first_firm.clients_of_firm.size first_firm.clients_of_firm.reset @@ -1364,8 +1437,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transaction_when_deleting_new_record + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + firm = Firm.new assert_no_queries do - firm = Firm.new client = Client.new("name" => "New Client") firm.clients_of_firm << client firm.clients_of_firm.destroy(client) @@ -1822,8 +1898,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_replacing_on_new_record + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + + firm = Firm.new assert_no_queries do - firm = Firm.new firm.clients_of_firm = [Client.new("name" => "New Client")] end end @@ -2354,6 +2433,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_collection_association_with_private_kernel_method firm = companies(:first_firm) assert_equal [accounts(:signals37)], firm.accounts.open + assert_equal [accounts(:signals37)], firm.accounts.available + end + + def test_association_with_or_doesnt_set_inverse_instance_key + firm = companies(:first_firm) + accounts = firm.accounts.or(Account.where(firm_id: nil)).order(:id) + assert_equal [firm.id, nil], accounts.map(&:firm_id) + end + + def test_association_with_rewhere_doesnt_set_inverse_instance_key + firm = companies(:first_firm) + accounts = firm.accounts.rewhere(firm_id: [firm.id, nil]).order(:id) + assert_equal [firm.id, nil], accounts.map(&:firm_id) end test "first_or_initialize adds the record to the association" do diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 94ad6e2d4d..7b405c74c4 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -274,6 +274,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_queries(1) { posts(:thinking) } new_person = nil # so block binding catches it + # Load schema information so we don't query below if running just this test. + Person.define_attribute_methods + assert_no_queries do new_person = Person.new first_name: "bob" end @@ -294,6 +297,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_associate_new_by_building assert_queries(1) { posts(:thinking) } + # Load schema information so we don't query below if running just this test. + Person.define_attribute_methods + assert_no_queries do posts(:thinking).people.build(first_name: "Bob") posts(:thinking).people.new(first_name: "Ted") diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 885d9d7c2c..adfb3ce072 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -231,9 +231,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end def test_build_association_dont_create_transaction - assert_no_queries { - Firm.new.build_account - } + # Load schema information so we don't query below if running just this test. + Account.define_attribute_methods + + firm = Firm.new + assert_no_queries do + firm.build_account + end end def test_building_the_associated_object_with_implicit_sti_base_class @@ -329,6 +333,29 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal 80, odegy.reload_account.credit_limit end + def test_reload_association_with_query_cache + odegy_id = companies(:odegy).id + + connection = ActiveRecord::Base.connection + connection.enable_query_cache! + connection.clear_query_cache + + # Populate the cache with a query + odegy = Company.find(odegy_id) + # Populate the cache with a second query + odegy.account + + assert_equal 2, connection.query_cache.size + + # Clear the cache and fetch the account again, populating the cache with a query + assert_queries(1) { odegy.reload_account } + + # This query is not cached anymore, so it should make a real SQL query + assert_queries(1) { Company.find(odegy_id) } + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 42d47527b2..88df0eed55 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -642,6 +642,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_before_save company = companies(:first_firm) + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } assert_not_predicate company.clients_of_firm, :loaded? @@ -653,6 +657,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_before_save company = companies(:first_firm) + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } company.name += "-changed" @@ -662,6 +670,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_via_block_before_save company = companies(:first_firm) + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert_not_predicate company.clients_of_firm, :loaded? @@ -673,6 +685,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_via_block_before_save company = companies(:first_firm) + + # Load schema information so we don't query below if running just this test. + Client.define_attribute_methods + assert_no_queries do company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 9c1f7aaef2..fddc2781b8 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -36,7 +36,7 @@ if ActiveRecord::Base.connection.prepared_statements def test_too_many_binds bind_params_length = @connection.send(:bind_params_length) - topics = Topic.where(id: (1 .. bind_params_length + 1).to_a) + topics = Topic.where(id: (1 .. bind_params_length).to_a << 2**63) assert_equal Topic.count, topics.count end diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index 844b2b2162..483383257b 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -42,6 +42,20 @@ module ActiveRecord assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end + test "cache_key for relation with custom select and limit" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5) + developers_with_select = developers.select("developers.*") + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers_with_select.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers_with_select.cache_key + + assert_equal ActiveSupport::Digest.hexdigest(developers_with_select.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + test "cache_key for loaded relation" do developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load last_developer_timestamp = developers.first.updated_at diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb new file mode 100644 index 0000000000..d4e8cbee81 --- /dev/null +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/person" + +module ActiveRecord + module ConnectionAdapters + class ConnectionHandlersMultiDbTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + fixtures :people + + def setup + @handlers = { writing: ConnectionHandler.new, reading: ConnectionHandler.new } + @rw_handler = @handlers[:writing] + @ro_handler = @handlers[:reading] + @spec_name = "primary" + @rw_pool = @handlers[:writing].establish_connection(ActiveRecord::Base.configurations["arunit"]) + @ro_pool = @handlers[:reading].establish_connection(ActiveRecord::Base.configurations["arunit"]) + end + + def teardown + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + + class MultiConnectionTestModel < ActiveRecord::Base + end + + def test_multiple_connection_handlers_works_in_a_threaded_environment + tf_writing = Tempfile.open "test_writing" + tf_reading = Tempfile.open "test_reading" + + MultiConnectionTestModel.connects_to database: { writing: { database: tf_writing.path, adapter: "sqlite3" }, reading: { database: tf_reading.path, adapter: "sqlite3" } } + + MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))") + MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('writing')") + + ActiveRecord::Base.connected_to(role: :reading) do + MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))") + MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('reading')") + end + + read_latch = Concurrent::CountDownLatch.new + write_latch = Concurrent::CountDownLatch.new + + MultiConnectionTestModel.connection + + thread = Thread.new do + MultiConnectionTestModel.connection + + write_latch.wait + assert_equal "writing", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1") + read_latch.count_down + end + + ActiveRecord::Base.connected_to(role: :reading) do + write_latch.count_down + assert_equal "reading", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1") + read_latch.wait + end + + thread.join + ensure + tf_reading.close + tf_reading.unlink + tf_writing.close + tf_writing.unlink + end + + unless in_memory_db? + def test_establish_connection_using_3_levels_config + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to(database: { writing: :primary, reading: :readonly }) + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_switching_connections_via_handler + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to(database: { writing: :primary, reading: :readonly }) + + ActiveRecord::Base.connected_to(role: :reading) do + @ro_handler = ActiveRecord::Base.connection_handler + assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:reading] + end + + ActiveRecord::Base.connected_to(role: :writing) do + assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] + assert_not_equal @ro_handler, ActiveRecord::Base.connection_handler + end + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_connects_to_with_single_configuration + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to database: { writing: :development } + + assert_equal 1, ActiveRecord::Base.connection_handlers.size + assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + end + + def test_connects_to_using_top_level_key_in_two_level_config + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly } + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + end + end + + def test_connection_pools + assert_equal([@rw_pool], @handlers[:writing].connection_pools) + assert_equal([@ro_pool], @handlers[:reading].connection_pools) + end + + def test_retrieve_connection + assert @rw_handler.retrieve_connection(@spec_name) + assert @ro_handler.retrieve_connection(@spec_name) + end + + def test_active_connections? + assert_not_predicate @rw_handler, :active_connections? + assert_not_predicate @ro_handler, :active_connections? + + assert @rw_handler.retrieve_connection(@spec_name) + assert @ro_handler.retrieve_connection(@spec_name) + + assert_predicate @rw_handler, :active_connections? + assert_predicate @ro_handler, :active_connections? + + @rw_handler.clear_active_connections! + assert_not_predicate @rw_handler, :active_connections? + + @ro_handler.clear_active_connections! + assert_not_predicate @ro_handler, :active_connections? + end + + def test_retrieve_connection_pool + assert_not_nil @rw_handler.retrieve_connection_pool(@spec_name) + assert_not_nil @ro_handler.retrieve_connection_pool(@spec_name) + end + + def test_retrieve_connection_pool_with_invalid_id + assert_nil @rw_handler.retrieve_connection_pool("foo") + assert_nil @ro_handler.retrieve_connection_pool("foo") + end + end + end +end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index d5a1d11e12..b4593ccdf2 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -265,6 +265,17 @@ class EnumTest < ActiveRecord::TestCase assert_equal "published", @book.status end + test "invalid definition values raise an ArgumentError" do + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [proposed: 1, written: 2, published: 3] + end + end + + assert_match(/must be either a hash, an array of symbols, or an array of strings./, e.message) + end + test "reserved enum names" do klass = Class.new(ActiveRecord::Base) do self.table_name = "books" diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 36cd63c4d4..5687afbc71 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -157,18 +157,40 @@ class IntegrationTest < ActiveRecord::TestCase skip("Subsecond precision is not supported") unless subsecond_precision_supported? dev = Developer.first key = dev.cache_key - dev.touch + travel_to dev.updated_at + 0.000001 do + dev.touch + end assert_not_equal key, dev.cache_key end def test_cache_key_format_is_not_too_precise - skip("Subsecond precision is not supported") unless subsecond_precision_supported? dev = Developer.first dev.touch key = dev.cache_key assert_equal key, dev.reload.cache_key end + def test_cache_version_format_is_precise_enough + skip("Subsecond precision is not supported") unless subsecond_precision_supported? + with_cache_versioning do + dev = Developer.first + version = dev.cache_version.to_param + travel_to Developer.first.updated_at + 0.000001 do + dev.touch + end + assert_not_equal version, dev.cache_version.to_param + end + end + + def test_cache_version_format_is_not_too_precise + with_cache_versioning do + dev = Developer.first + dev.touch + key = dev.cache_version.to_param + assert_equal key, dev.reload.cache_version.to_param + end + end + def test_named_timestamps_for_cache_key assert_deprecated do owner = owners(:blackbeard) @@ -185,50 +207,52 @@ class IntegrationTest < ActiveRecord::TestCase end def test_cache_key_is_stable_with_versioning_on - Developer.cache_versioning = true - - developer = Developer.first - first_key = developer.cache_key + with_cache_versioning do + developer = Developer.first + first_key = developer.cache_key - developer.touch - second_key = developer.cache_key + developer.touch + second_key = developer.cache_key - assert_equal first_key, second_key - ensure - Developer.cache_versioning = false + assert_equal first_key, second_key + end end def test_cache_version_changes_with_versioning_on - Developer.cache_versioning = true - - developer = Developer.first - first_version = developer.cache_version + with_cache_versioning do + developer = Developer.first + first_version = developer.cache_version - travel 10.seconds do - developer.touch - end + travel 10.seconds do + developer.touch + end - second_version = developer.cache_version + second_version = developer.cache_version - assert_not_equal first_version, second_version - ensure - Developer.cache_versioning = false + assert_not_equal first_version, second_version + end end def test_cache_key_retains_version_when_custom_timestamp_is_used - Developer.cache_versioning = true + with_cache_versioning do + developer = Developer.first + first_key = developer.cache_key_with_version - developer = Developer.first - first_key = developer.cache_key_with_version + travel 10.seconds do + developer.touch + end - travel 10.seconds do - developer.touch - end + second_key = developer.cache_key_with_version - second_key = developer.cache_key_with_version + assert_not_equal first_key, second_key + end + end - assert_not_equal first_key, second_key + def with_cache_versioning(value = true) + @old_cache_versioning = ActiveRecord::Base.cache_versioning + ActiveRecord::Base.cache_versioning = value + yield ensure - Developer.cache_versioning = false + ActiveRecord::Base.cache_versioning = @old_cache_versioning end end diff --git a/activerecord/test/cases/null_relation_test.rb b/activerecord/test/cases/null_relation_test.rb index c5d5ded5ab..ee96ea1af6 100644 --- a/activerecord/test/cases/null_relation_test.rb +++ b/activerecord/test/cases/null_relation_test.rb @@ -17,6 +17,7 @@ class NullRelationTest < ActiveRecord::TestCase end def test_none_chainable + Developer.send(:load_schema) assert_no_queries do assert_equal [], Developer.none.where(name: "David") end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 9914a61033..e471ee8039 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1812,6 +1812,16 @@ class RelationTest < ActiveRecord::TestCase assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",") end + def test_relation_with_private_kernel_method + accounts = Account.all + assert_equal [accounts(:signals37)], accounts.open + assert_equal [accounts(:signals37)], accounts.available + + sub_accounts = SubAccount.all + assert_equal [accounts(:signals37)], sub_accounts.open + assert_equal [accounts(:signals37)], sub_accounts.available + end + test "#skip_query_cache!" do Post.cache do assert_queries(1) do diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index d674bd562f..3fd1813d64 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -731,7 +731,7 @@ module ActiveRecord end if current_adapter?(:SQLite3Adapter) && !in_memory_db? - class DatabaseTasksMigrateTest < ActiveRecord::TestCase + class DatabaseTasksMigrationTestCase < ActiveRecord::TestCase self.use_transactional_tests = false # Use a memory db here to avoid having to rollback at the end @@ -751,7 +751,9 @@ module ActiveRecord @conn.release_connection if @conn ActiveRecord::Base.establish_connection :arunit end + end + class DatabaseTasksMigrateTest < DatabaseTasksMigrationTestCase def test_migrate_set_and_unset_verbose_and_version_env_vars verbose, version = ENV["VERBOSE"], ENV["VERSION"] ENV["VERSION"] = "2" @@ -812,6 +814,26 @@ module ActiveRecord end end end + + class DatabaseTasksMigrateStatusTest < DatabaseTasksMigrationTestCase + def test_migrate_status_table + ActiveRecord::SchemaMigration.create_table + output = capture_migration_status + assert_match(/database: :memory:/, output) + assert_match(/down 001 Valid people have last names/, output) + assert_match(/down 002 We need reminders/, output) + assert_match(/down 003 Innocent jointable/, output) + ActiveRecord::SchemaMigration.drop_table + end + + private + + def capture_migration_status + capture(:stdout) do + ActiveRecord::Tasks::DatabaseTasks.migrate_status + end + end + end end class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase diff --git a/activerecord/test/fixtures/citations.yml b/activerecord/test/fixtures/citations.yml index d31cb8efa1..396099621c 100644 --- a/activerecord/test/fixtures/citations.yml +++ b/activerecord/test/fixtures/citations.yml @@ -1,4 +1,5 @@ <% 65536.times do |i| %> fixture_no_<%= i %>: id: <%= i %> + book2_id: <%= i*i %> <% end %> diff --git a/activerecord/test/models/account.rb b/activerecord/test/models/account.rb index 0c3cd45a81..639e395743 100644 --- a/activerecord/test/models/account.rb +++ b/activerecord/test/models/account.rb @@ -11,9 +11,8 @@ class Account < ActiveRecord::Base end # Test private kernel method through collection proxy using has_many. - def self.open - where("firm_name = ?", "37signals") - end + scope :open, -> { where("firm_name = ?", "37signals") } + scope :available, -> { open } before_destroy do |account| if account.firm @@ -32,3 +31,11 @@ class Account < ActiveRecord::Base "Sir, yes sir!" end end + +class SubAccount < Account + def self.instantiate_instance_of(klass, attributes, column_types = {}, &block) + klass = superclass + super + end + private_class_method :instantiate_instance_of +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 528585fb75..710a75ad30 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -297,8 +297,6 @@ end class FakeKlass extend ActiveRecord::Delegation::DelegateCache - inherited self - class << self def connection Post.connection @@ -335,5 +333,11 @@ class FakeKlass def predicate_builder Post.predicate_builder end + + def base_class? + true + end end + + inherited self end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 2aaf393009..4ef463fdad 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -93,7 +93,7 @@ ActiveRecord::Schema.define do t.integer :pirate_id end - create_table :books, force: true do |t| + create_table :books, id: :integer, force: true do |t| t.references :author t.string :format t.column :name, :string @@ -158,8 +158,8 @@ ActiveRecord::Schema.define do end create_table :citations, force: true do |t| - t.column :book1_id, :integer - t.column :book2_id, :integer + t.references :book1 + t.references :book2 t.references :citation end |