diff options
Diffstat (limited to 'activerecord')
26 files changed, 117 insertions, 107 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6a40d32ef9..ad65efc7bc 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,18 @@ +* Concurrent `AR::Base#increment!` and `#decrement!` on the same record + are all reflected in the database rather than overwriting each other. + + *Bogdan Gusiev* + +* Avoid leaking the first relation we call `first` on, per model. + + Fixes #21921. + + *Matthew Draper*, *Jean Boussier* + +* Remove unused `pk_and_sequence_for` in AbstractMysqlAdapter. + + *Ryuta Kamizono* + * Allow fixtures files to set the model class in the YAML file itself. To load the fixtures file `accounts.yml` as the `User` model, use: diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 260a0c6a2d..41698c5360 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -10,7 +10,7 @@ module ActiveRecord def replace(record) if record raise_on_type_mismatch!(record) - update_counters(record) + update_counters_on_replace(record) replace_keys(record) set_inverse_instance(record) @updated = true @@ -32,45 +32,37 @@ module ActiveRecord end def decrement_counters # :nodoc: - with_cache_name { |name| decrement_counter name } + update_counters(-1) end def increment_counters # :nodoc: - with_cache_name { |name| increment_counter name } + update_counters(1) end private - def find_target? - !loaded? && foreign_key_present? && klass - end - - def with_cache_name - counter_cache_name = reflection.counter_cache_column - return unless counter_cache_name && owner.persisted? - yield counter_cache_name + def update_counters(by) + if require_counter_update? && foreign_key_present? + if target && !stale_target? + target.increment!(reflection.counter_cache_column, by) + else + klass.update_counters(target_id, reflection.counter_cache_column => by) + end + end end - def update_counters(record) - with_cache_name do |name| - return unless different_target? record - record.class.increment_counter(name, record.id) - decrement_counter name - end + def find_target? + !loaded? && foreign_key_present? && klass end - def decrement_counter(counter_cache_name) - if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) - end + def require_counter_update? + reflection.counter_cache_column && owner.persisted? end - def increment_counter(counter_cache_name) - if foreign_key_present? - klass.increment_counter(counter_cache_name, target_id) - if target && !stale_target? - target.increment(counter_cache_name) - end + def update_counters_on_replace(record) + if require_counter_update? && different_target?(record) + record.increment!(reflection.counter_cache_column) + decrement_counters end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 7da20d8eea..a9f6aaafef 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -88,21 +88,15 @@ module ActiveRecord end def update_counter(difference, reflection = reflection()) - update_counter_in_database(difference, reflection) - update_counter_in_memory(difference, reflection) - end - - def update_counter_in_database(difference, reflection = reflection()) if reflection.has_cached_counter? - owner.class.update_counters(owner.id, reflection.counter_cache_column => difference) + owner.increment!(reflection.counter_cache_column, difference) end end def update_counter_in_memory(difference, reflection = reflection()) if reflection.counter_must_be_updated_by_has_many? counter = reflection.counter_cache_column - owner[counter] ||= 0 - owner[counter] += difference + owner.increment(counter, difference) owner.send(:clear_attribute_change, counter) # eww end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 92792a7a15..7a5a8f8ae6 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -137,7 +137,9 @@ module ActiveRecord scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - scope._select! preload_values[:select] || values[:select] || table[Arel.star] + if preload_values[:select] || values[:select] + scope._select!(preload_values[:select] || values[:select]) + end scope.includes! preload_values[:includes] || values[:includes] if preload_scope.joins_values.any? scope.joins!(preload_scope.joins_values) diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index e8a782ed13..0bcfa5f00d 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -133,7 +133,7 @@ module ActiveRecord end def previous_mutation_tracker - @previous_mutation_tracker ||= NullMutationTracker.new + @previous_mutation_tracker ||= NullMutationTracker.instance end def cache_changed_attributes diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb index ba7348918b..0133b4d0be 100644 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -46,6 +46,8 @@ module ActiveRecord end class NullMutationTracker # :nodoc: + include Singleton + def changed_values {} end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 026b3cf014..be581ac2a9 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -91,6 +91,10 @@ module ActiveRecord AttributeSet.new(new_attributes) end + def ==(other) + attributes == other.attributes + end + protected attr_reader :attributes diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index f974b7a876..3bd7c7997b 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -68,10 +68,29 @@ module ActiveRecord end end + def ==(other) + if other.is_a?(LazyAttributeHash) + materialize == other.materialize + else + materialize == other + end + end + protected attr_reader :types, :values, :additional_types, :delegate_hash + def materialize + unless @materialized + values.each_key { |key| self[key] } + types.each_key { |key| self[key] } + unless frozen? + @materialized = true + end + end + delegate_hash + end + private def assign_default_value(name) @@ -85,16 +104,5 @@ module ActiveRecord delegate_hash[name] = Attribute.uninitialized(name, type) end end - - def materialize - unless @materialized - values.each_key { |key| self[key] } - types.each_key { |key| self[key] } - unless frozen? - @materialized = true - end - end - delegate_hash - end end end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 8b2c4c7170..a573f76a8e 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -5,9 +5,6 @@ module ActiveRecord module Attributes extend ActiveSupport::Concern - # :nodoc: - Type = ActiveRecord::Type - included do class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: self.attributes_to_define_after_schema_loads = {} 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 7e53f8958a..ccff853987 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -702,9 +702,11 @@ module ActiveRecord # [<tt>:index</tt>] # Add an appropriate index. Defaults to false. # [<tt>:foreign_key</tt>] - # Add an appropriate foreign key. Defaults to false. + # Add an appropriate foreign key constraint. Defaults to false. # [<tt>:polymorphic</tt>] # Whether an additional +_type+ column should be added. Defaults to false. + # [<tt>:null</tt>] + # Whether the column allows nulls. Defaults to true. # # ====== Create a user_id integer column # 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 79a24d1996..24408d6bb6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -458,7 +458,7 @@ module ActiveRecord end class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the # MySQL shell: # # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ @@ -850,13 +850,6 @@ module ActiveRecord nil end - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) - if pk = primary_key(table) - [ pk, nil ] - end - end - def primary_keys(table_name) # :nodoc: raise ArgumentError unless table_name.present? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 11d3f5301a..43e18ebb2b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -8,7 +8,7 @@ module ActiveRecord end class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of a EXPLAIN in a way that resembles the output of the + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the # PostgreSQL shell: # # QUERY PLAN diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index f6a6e3eb4d..505a71720f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -218,7 +218,7 @@ module ActiveRecord end class ExplainPrettyPrinter - # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles + # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles # the output of the SQLite shell: # # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 370c1562c9..19d2589db3 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -15,7 +15,7 @@ module ActiveRecord self.cache_timestamp_format = :nsec end - # Returns a String, which Action Pack uses for constructing an URL to this + # Returns a String, which Action Pack uses for constructing a URL to this # object. The default implementation returns this record's id as a String, # or nil if this record's unsaved. # diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 0c50826c63..90b8a29869 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -947,7 +947,7 @@ module ActiveRecord def migrations_paths @migrations_paths ||= ['db/migrate'] - # just to not break things if someone uses: migration_path = some_string + # just to not break things if someone uses: migrations_path = some_string Array(@migrations_paths) end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 4c4afb4dbd..0fa665c7e0 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -9,6 +9,7 @@ module ActiveRecord # * add_index # * add_reference # * add_timestamps + # * change_column # * change_column_default (must supply a :from and :to option) # * change_column_null # * create_join_table @@ -18,6 +19,7 @@ module ActiveRecord # * drop_table (must supply a block) # * enable_extension # * remove_column (must supply a type) + # * remove_columns (must specify at least one column name or more) # * remove_foreign_key (must supply a second table) # * remove_index # * remove_reference diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 3f02f73a5a..f659d0b10f 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -332,16 +332,18 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def increment!(attribute, by = 1) - increment(attribute, by).update_attribute(attribute, self[attribute]) + increment(attribute, by) + change = public_send(attribute) - (attribute_was(attribute.to_s) || 0) + self.class.update_counters(id, attribute => change) + clear_attribute_change(attribute) # eww + self end # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). # The decrement is performed directly on the underlying attribute, no setter is invoked. # Only makes sense for number-based attributes. Returns +self+. def decrement(attribute, by = 1) - self[attribute] ||= 0 - self[attribute] -= by - self + increment(attribute, -by) end # Wrapper around +decrement+ that saves the record. This method differs from @@ -349,7 +351,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def decrement!(attribute, by = 1) - decrement(attribute, by).update_attribute(attribute, self[attribute]) + increment!(attribute, -by) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 6dd54f9262..2744673c12 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -121,7 +121,7 @@ module ActiveRecord # This sets the database configuration from Configuration#database_configuration # and then establishes the connection. - initializer "active_record.initialize_database" do |app| + initializer "active_record.initialize_database" do ActiveSupport.on_load(:active_record) do self.configurations = Rails.application.config.database_configuration diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 65ec356735..5b9d45d871 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -62,7 +62,7 @@ module ActiveRecord aggregate_reflections[aggregation.to_s] end - # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. + # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value. # # Account.reflections # => {"balance" => AggregateReflection} # diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index e596df8742..36cdeed489 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -400,11 +400,11 @@ module ActiveRecord # people = Person.where(group: 'expert') # people.update(group: 'masters') # - # Note: Updating a large number of records will run a - # UPDATE query for each record, which may cause a performance - # issue. So if it is not needed to run callbacks for each update, it is - # preferred to use <tt>update_all</tt> for updating all records using - # a single query. + # Note: Updating a large number of records will run an + # UPDATE query for each record, which may cause a performance + # issue. So if it is not needed to run callbacks for each update, it is + # preferred to use <tt>update_all</tt> for updating all records using + # a single query. def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index ccb0ab18ae..7d95fe9a22 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -3,7 +3,6 @@ require "active_record/relation/query_attribute" require "active_record/relation/where_clause" require "active_record/relation/where_clause_factory" require 'active_model/forbidden_attributes_protection' -require 'active_support/core_ext/string/filters' module ActiveRecord module QueryMethods diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 7b62626896..b2f25752d3 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -39,8 +39,12 @@ module ActiveRecord end end - # Adds a class method for retrieving and querying objects. A \scope - # represents a narrowing of a database query, such as + # Adds a class method for retrieving and querying objects. + # The method is intended to return an <tt>ActiveRecord::Relation</tt> + # object, which is composable with other scopes. + # If it returns +nil+ or +false+, an <tt>all</tt> scope is returned instead. + # + # A \scope represents a narrowing of a database query, such as # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>. # # class Shirt < ActiveRecord::Base @@ -66,7 +70,8 @@ module ActiveRecord # end # # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by - # <tt>Shirt.red</tt> is not an Array; it resembles the association object + # <tt>Shirt.red</tt> is not an Array but an <tt>ActiveRecord::Relation</tt>, + # which is composable with other scopes; it resembles the association object # constructed by a +has_many+ declaration. For instance, you can invoke # <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, # <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 29573d8e0d..d2ce48fc00 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -65,30 +65,6 @@ module ActiveRecord end end - def test_pk_and_sequence_for - with_example_table do - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'id', pk - assert_equal @conn.default_sequence_name('ex', 'id'), seq - end - end - - def test_pk_and_sequence_for_with_non_standard_primary_key - with_example_table '`code` INT auto_increment, PRIMARY KEY (`code`)' do - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'code', pk - assert_equal @conn.default_sequence_name('ex', 'code'), seq - end - end - - def test_pk_and_sequence_for_with_custom_index_type_pk - with_example_table '`id` INT auto_increment, PRIMARY KEY USING BTREE (`id`)' do - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'id', pk - assert_equal @conn.default_sequence_name('ex', 'id'), seq - end - end - def test_composite_primary_key with_example_table '`id` INT, `number` INT, foo INT, PRIMARY KEY (`id`, `number`)' do assert_nil @conn.primary_key('ex') diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 5a0e463a48..7a24b85a36 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -239,5 +239,15 @@ module ActiveRecord assert_equal 2, new_attributes.fetch_value(:foo) assert_equal 3, new_attributes.fetch_value(:bar) end + + test "comparison for equality is correctly implemented" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: "1", bar: "2") + attributes2 = builder.build_from_database(foo: "1", bar: "2") + attributes3 = builder.build_from_database(foo: "2", bar: "2") + + assert_equal attributes, attributes2 + assert_not_equal attributes2, attributes3 + end end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 8773986882..89bd3f888e 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -47,9 +47,7 @@ def in_memory_db? end def subsecond_precision_supported? - !current_adapter?(:MysqlAdapter, :Mysql2Adapter) || - (ActiveRecord::Base.connection.send(:version) >= '5.6.0' && - ActiveRecord::Base.connection.send(:version) < '5.7.0') + !current_adapter?(:MysqlAdapter, :Mysql2Adapter) || ActiveRecord::Base.connection.send(:version) >= '5.6.4' end def mysql_enforcing_gtid_consistency? diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 7f14082a9a..31686bde3f 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -120,6 +120,15 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal 59, accounts(:signals37, :reload).credit_limit end + def test_increment_updates_counter_in_db_using_offset + a1 = accounts(:signals37) + initial_credit = a1.credit_limit + a2 = Account.find(accounts(:signals37).id) + a1.increment!(:credit_limit) + a2.increment!(:credit_limit) + assert_equal initial_credit + 2, a1.reload.credit_limit + end + def test_destroy_all conditions = "author_name = 'Mary'" topics_by_mary = Topic.all.merge!(:where => conditions, :order => 'id').to_a |