diff options
Diffstat (limited to 'activerecord')
16 files changed, 115 insertions, 47 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index bca719cb2f..4d0c1a4178 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,36 @@ +* Serialize JSON attribute value `nil` as SQL `NULL`, not JSON `null` + + *Trung Duc Tran* + +* Return `true` from `update_attribute` when the value of the attribute + to be updated is unchanged. + + Fixes #26593. + + *Prathamesh Sonpatki* + +* Always store errors details information with symbols. + + When the association is autosaved we were storing the details with + string keys. This was creating inconsistency with other details that are + added using the `Errors#add` method. It was also inconsistent with the + `Errors#messages` storage. + + To fix this inconsistency we are always storing with symbols. This will + cause a small breaking change because in those cases the details could + be accessed as strings keys but now it can not. + + Fix #26499. + + *Rafael Mendonça França*, *Marcus Vieira* + +* Calling `touch` on a model using optimistic locking will now leave the model + in a non-dirty state with no attribute changes. + + Fixes #26496. + + *Jakob Skjerning* + * Using a mysql2 connection after it fails to reconnect will now have an error message saying the connection is closed rather than an undefined method error message. diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 08bd532fb0..e5b3af8252 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -259,7 +259,6 @@ module ActiveRecord seen[record.id] = true unless seen.key?(record.id) end end - alias uniq distinct # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index db84876b0a..d3e0dee731 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -329,26 +329,20 @@ module ActiveRecord return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) + unless valid = record.valid?(validation_context) if reflection.options[:autosave] indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) record.errors.each do |attribute, message| - if indexed_attribute - attribute = "#{reflection.name}[#{index}].#{attribute}" - else - attribute = "#{reflection.name}.#{attribute}" - end + attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) errors[attribute] << message errors[attribute].uniq! end record.errors.details.each_key do |attribute| - if indexed_attribute - reflection_attribute = "#{reflection.name}[#{index}].#{attribute}" - else - reflection_attribute = "#{reflection.name}.#{attribute}" - end + reflection_attribute = + normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym record.errors.details[attribute].each do |error| errors.details[reflection_attribute] << error @@ -362,6 +356,14 @@ module ActiveRecord valid end + def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) + if indexed_attribute + "#{reflection.name}[#{index}].#{attribute}" + else + "#{reflection.name}.#{attribute}" + end + end + # Is used as a before_save callback to check while saving a collection # association whether or not the parent was a new record before saving. def before_save_collection_association 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 7414eba6c5..092543259f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -89,10 +89,10 @@ module ActiveRecord end end - # Executes an SQL statement, returning a PGresult object on success - # or raising a PGError exception otherwise. - # Note: the PGresult object is manually memory managed; if you don't - # need it specifically, you many want consider the exec_query wrapper. + # Executes an SQL statement, returning a PG::Result object on success + # or raising a PG::Error exception otherwise. + # Note: the PG::Result object is manually memory managed; if you don't + # need it specifically, you may want consider the <tt>exec_query</tt> wrapper. def execute(sql, name = nil) log(sql, name) do @connection.async_exec(sql) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 3465b68ac6..622df0cfc1 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -452,7 +452,7 @@ module ActiveRecord # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] def hash if id - [self.class, id].hash + self.class.hash ^ self.id.hash else super end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index a04ef2e263..6933f3f9b8 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -107,7 +107,7 @@ module ActiveRecord # # By default, save always runs validations. If any of them fail the action # is cancelled and #save returns +false+, and the record won't be saved. However, if you supply - # validate: false, validations are bypassed altogether. See + # <tt>validate: false</tt>, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # # By default, #save also sets the +updated_at+/+updated_on+ attributes to @@ -134,7 +134,7 @@ module ActiveRecord # # By default, #save! always runs validations. If any of them fail # ActiveRecord::RecordInvalid gets raised, and the record won't be saved. However, if you supply - # validate: false, validations are bypassed altogether. See + # <tt>validate: false</tt>, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # # By default, #save! also sets the +updated_at+/+updated_on+ attributes to @@ -252,7 +252,8 @@ module ActiveRecord name = name.to_s verify_readonly_attribute(name) public_send("#{name}=", value) - save(validate: false) if changed? + + changed? ? save(validate: false) : true end # Updates the attributes of the model from the passed-in hash and saves the @@ -498,7 +499,6 @@ module ActiveRecord changes[column] = write_attribute(column, time) end - clear_attribute_changes(changes.keys) primary_key = self.class.primary_key scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key)) @@ -508,6 +508,7 @@ module ActiveRecord changes[locking_column] = increment_lock end + clear_attribute_changes(changes.keys) result = scope.update_all(changes) == 1 if !result && locking_enabled? diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 09ca30e434..a887be8a20 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -252,11 +252,11 @@ module ActiveRecord result = @klass.connection.select_all(query_builder, nil, bound_attributes) row = result.first value = row && row.values.first - column = result.column_types.fetch(column_alias) do + type = result.column_types.fetch(column_alias) do type_for(column_name) end - type_cast_calculated_value(value, column, operation) + type_cast_calculated_value(value, type, operation) end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: @@ -310,18 +310,16 @@ module ActiveRecord Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, col_name| - column = type_for(col_name) do - calculated_data.column_types.fetch(aliaz) do - Type.default_value - end + type = type_for(col_name) do + calculated_data.column_types.fetch(aliaz, Type.default_value) end - type_cast_calculated_value(row[aliaz], column) + type_cast_calculated_value(row[aliaz], type) } key = key.first if key.size == 1 key = key_records[key] if associated - column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } - [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] + type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } + [key, type_cast_calculated_value(row[aggregate_alias], type, operation)] end] end @@ -356,7 +354,7 @@ module ActiveRecord when "count" then value.to_i when "sum" then type.deserialize(value || 0) when "average" then value.respond_to?(:to_d) ? value.to_d : value - else type.deserialize(value) + else type.deserialize(value) end end diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb index dbd08811fa..31544c730e 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -2,15 +2,15 @@ module ActiveRecord class Relation module RecordFetchWarning # When this module is prepended to ActiveRecord::Relation and - # `config.active_record.warn_on_records_fetched_greater_than` is + # +config.active_record.warn_on_records_fetched_greater_than+ is # set to an integer, if the number of records a query returns is - # greater than the value of `warn_on_records_fetched_greater_than`, + # greater than the value of +warn_on_records_fetched_greater_than+, # a warning is logged. This allows for the detection of queries that # return a large number of records, which could cause memory bloat. # # In most cases, fetching large number of records can be performed # efficiently using the ActiveRecord::Batches methods. - # See active_record/lib/relation/batches.rb for more information. + # See ActiveRecord::Batches for more information. def exec_queries QueryRegistry.reset diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index dc00149130..1e7deeffad 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -7,8 +7,6 @@ module ActiveRecord end def build(opts, other) - binds = [] - case opts when String, Array parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] @@ -26,7 +24,7 @@ module ActiveRecord raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end - WhereClause.new(parts, binds) + WhereClause.new(parts, binds || []) end protected diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index d19bb96ede..691940ab70 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -7,12 +7,12 @@ module ActiveRecord # end # # The cached statement is executed by using the - # [connection.execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} method: + # {connection.execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] method: # # cache.execute([], Book, Book.connection) # # The relation returned by the block is cached, and for each - # [execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} + # {execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] # call the cached relation gets duped. Database is queried when +to_a+ is called on the relation. # # If you want to cache the statement without the values you can use the +bind+ method of the diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb index 513c938088..e19c5a14da 100644 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -17,7 +17,11 @@ module ActiveRecord end def serialize(value) - ::ActiveSupport::JSON.encode(value) + if value.nil? + nil + else + ::ActiveSupport::JSON.encode(value) + end end def accessor diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index 6b7d259023..630cdb36a4 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -102,6 +102,22 @@ if ActiveRecord::Base.connection.supports_json? assert_equal(["v0", { "k1" => "v1" }], x.payload) end + def test_select_nil_json_after_create + json = JsonDataType.create(payload: nil) + x = JsonDataType.where(payload:nil).first + assert_equal(json, x) + end + + def test_select_nil_json_after_update + json = JsonDataType.create(payload: "foo") + x = JsonDataType.where(payload:nil).first + assert_equal(nil, x) + + json.update_attributes payload: nil + x = JsonDataType.where(payload:nil).first + assert_equal(json.reload, x) + end + def test_rewrite_array_json_value @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| x = JsonDataType.first diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index c74f70f251..273b2c1c7b 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -113,6 +113,22 @@ module PostgresqlJSONSharedTestCases assert_equal(nil, x.payload) end + def test_select_nil_json_after_create + json = JsonDataType.create(payload: nil) + x = JsonDataType.where(payload:nil).first + assert_equal(json, x) + end + + def test_select_nil_json_after_update + json = JsonDataType.create(payload: "foo") + x = JsonDataType.where(payload:nil).first + assert_equal(nil, x) + + json.update_attributes payload: nil + x = JsonDataType.where(payload:nil).first + assert_equal(json.reload, x) + end + def test_select_array_json_value @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| x = JsonDataType.first diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index c6e983a106..c24d7b8835 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -443,7 +443,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib assert_not invalid_electron.valid? assert valid_electron.valid? assert_not molecule.valid? - assert_equal [{ error: :blank }], molecule.errors.details["electrons.name"] + assert_equal [{ error: :blank }], molecule.errors.details[:"electrons.name"] end def test_errors_details_should_be_indexed_when_passed_as_array @@ -457,8 +457,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib assert_not tuning_peg_invalid.valid? assert tuning_peg_valid.valid? assert_not guitar.valid? - assert_equal [{ error: :not_a_number, value: nil }] , guitar.errors.details["tuning_pegs[1].pitch"] - assert_equal [], guitar.errors.details["tuning_pegs.pitch"] + assert_equal [{ error: :not_a_number, value: nil }], guitar.errors.details[:"tuning_pegs[1].pitch"] + assert_equal [], guitar.errors.details[:"tuning_pegs.pitch"] end def test_errors_details_should_be_indexed_when_global_flag_is_set @@ -474,8 +474,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib assert_not invalid_electron.valid? assert valid_electron.valid? assert_not molecule.valid? - assert_equal [{ error: :blank }], molecule.errors.details["electrons[1].name"] - assert_equal [], molecule.errors.details["electrons.name"] + assert_equal [{ error: :blank }], molecule.errors.details[:"electrons[1].name"] + assert_equal [], molecule.errors.details[:"electrons.name"] ensure ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 5c55584ff7..13b6f6daaf 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -181,6 +181,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1.touch assert_equal 1, p1.lock_version + assert_not p1.changed?, "Changes should have been cleared" end def test_touch_stale_object diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index d83360e327..688c3ed2b1 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -391,14 +391,14 @@ class PersistenceTest < ActiveRecord::TestCase end topic = klass.create(title: "Another New Topic") assert_queries(0) do - topic.update_attribute(:title, "Another New Topic") + assert topic.update_attribute(:title, "Another New Topic") end end def test_update_does_not_run_sql_if_record_has_not_changed topic = Topic.create(title: "Another New Topic") - assert_queries(0) { topic.update(title: "Another New Topic") } - assert_queries(0) { topic.update_attributes(title: "Another New Topic") } + assert_queries(0) { assert topic.update(title: "Another New Topic") } + assert_queries(0) { assert topic.update_attributes(title: "Another New Topic") } end def test_delete |