diff options
Diffstat (limited to 'activerecord')
15 files changed, 190 insertions, 21 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index c5b94763ff..341e85c59e 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,52 @@ +* Confirm a record has not already been destroyed before decrementing counter cache. + + *Ben Tucker* + +* Fixed a bug in `ActiveRecord#sanitize_sql_hash_for_conditions` in which + `self.class` is an argument to `PredicateBuilder#build_from_hash` + causing `PredicateBuilder` to call non-existant method + `Class#reflect_on_association`. + + *Zach Ohlgren* + +* While removing index if column option is missing then raise IrreversibleMigration exception. + + Following code should raise `IrreversibleMigration`. But the code was + failing since options is an array and not a hash. + + def change + change_table :users do |t| + t.remove_index [:name, :email] + end + end + + Fix was to check if the options is a Hash before operating on it. + + Fixes #10419. + + *Neeraj Singh* + +* Do not overwrite manually built records during one-to-one nested attribute assignment + + For one-to-one nested associations, if you build the new (in-memory) + child object yourself before assignment, then the NestedAttributes + module will not overwrite it, e.g.: + + class Member < ActiveRecord::Base + has_one :avatar + accepts_nested_attributes_for :avatar + + def avatar + super || build_avatar(width: 200) + end + end + + member = Member.new + member.avatar_attributes = {icon: 'sad'} + member.avatar.width # => 200 + + *Olek Janiszewski* + * fixes bug introduced by #3329. Now, when autosaving associations, deletions happen before inserts and saves. This prevents a 'duplicate unique value' database error that would occur if a record being created had @@ -42,9 +91,9 @@ * Abort a rake task when missing db/structure.sql like `db:schema:load` task. *kennyj* - + * rake:db:test:prepare falls back to original environment after execution. - + *Slava Markevich* Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index db0553ea76..710babe024 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -164,6 +164,13 @@ module ActiveRecord @reflection = @owner.class.reflect_on_association(reflection_name) end + def initialize_attributes(record) #:nodoc: + skip_assign = [reflection.foreign_key, reflection.type].compact + attributes = create_scope.except(*(record.changed - skip_assign)) + record.assign_attributes(attributes) + set_inverse_instance(record) + end + private def find_target? @@ -233,10 +240,7 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| - skip_assign = [reflection.foreign_key, reflection.type].compact - attributes = create_scope.except(*(record.changed - skip_assign)) - record.assign_attributes(attributes) - set_inverse_instance(record) + initialize_attributes(record) end end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 543a0247d1..63e9526436 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -34,7 +34,9 @@ module ActiveRecord::Associations::Builder def belongs_to_counter_cache_before_destroy_for_#{name} unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == #{foreign_key.to_sym.inspect} record = #{name} - record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? + if record && !self.destroyed? + record.class.decrement_counter(:#{cache_column}, record.id) + end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index d9b807bba4..98916b06a5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -321,6 +321,7 @@ module ActiveRecord result = query(<<-end_sql, 'SCHEMA')[0] SELECT attr.attname, CASE + WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1) @@ -332,7 +333,7 @@ module ActiveRecord JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) WHERE t.oid = '#{quote_table_name(table)}'::regclass AND cons.contype = 'p' - AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval' + AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate' end_sql end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index df729259e9..88b09e7999 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -768,7 +768,7 @@ module ActiveRecord end def exec_cache(sql, binds) - stmt_key = prepare_statement sql + stmt_key = prepare_statement(sql) # Clear the queue @connection.get_last_result diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 79c55045ba..9782a48055 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -144,7 +144,10 @@ module ActiveRecord def invert_remove_index(args) table, options = *args - raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." unless options && options[:column] + + unless options && options.is_a?(Hash) && options[:column] + raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + end options = options.dup [:add_index, [table, options.delete(:column), options]] diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index d607f49e2b..021832de46 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -229,6 +229,23 @@ module ActiveRecord # belongs_to :member, inverse_of: :posts # validates_presence_of :member # end + # + # For one-to-one nested associations, if you build the new (in-memory) + # child object yourself before assignment, then this module will not + # overwrite it, e.g.: + # + # class Member < ActiveRecord::Base + # has_one :avatar + # accepts_nested_attributes_for :avatar + # + # def avatar + # super || build_avatar(width: 200) + # end + # end + # + # member = Member.new + # member.avatar_attributes = {icon: 'sad'} + # member.avatar.width # => 200 module ClassMethods REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } } @@ -356,20 +373,28 @@ module ActiveRecord def assign_nested_attributes_for_one_to_one_association(association_name, attributes) options = self.nested_attributes_options[association_name] attributes = attributes.with_indifferent_access + existing_record = send(association_name) - if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) && - (options[:update_only] || record.id.to_s == attributes['id'].to_s) - assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) + if (options[:update_only] || !attributes['id'].blank?) && existing_record && + (options[:update_only] || existing_record.id.to_s == attributes['id'].to_s) + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) elsif attributes['id'].present? raise_nested_attributes_record_not_found!(association_name, attributes['id']) elsif !reject_new_record?(association_name, attributes) - method = "build_#{association_name}" - if respond_to?(method) - send(method, attributes.except(*UNASSIGNABLE_KEYS)) + assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS) + + if existing_record && existing_record.new_record? + existing_record.assign_attributes(assignable_attributes) + association(association_name).initialize_attributes(existing_record) else - raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" + method = "build_#{association_name}" + if respond_to?(method) + send(method, assignable_attributes) + else + raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" + end end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 3c5b871e99..0ed97b66d6 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -89,7 +89,7 @@ module ActiveRecord attrs = expand_hash_conditions_for_aggregates(attrs) table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) - PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b| + PredicateBuilder.build_from_hash(self, attrs, table).map { |b| connection.visitor.accept b }.join(' AND ') end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 10c6d272cd..1181cc739e 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -106,9 +106,12 @@ HEADER end tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" - if columns.detect { |c| c.name == pk } + pkcol = columns.detect { |c| c.name == pk } + if pkcol if pk != 'id' tbl.print %Q(, primary_key: "#{pk}") + elsif pkcol.sql_type == 'uuid' + tbl.print ", id: :uuid" end else tbl.print ", id: false" diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 8c17372286..b5d7ea603e 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -281,7 +281,6 @@ _SQL tz = ::ActiveRecord::Base.default_timezone assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Float::INFINITY, @third_range.ts_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) assert_equal nil, @empty_range.ts_range end @@ -290,7 +289,6 @@ _SQL skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges? assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range - assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Float::INFINITY, @third_range.tstz_range assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) assert_equal nil, @empty_range.tstz_range end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 04c9065393..b573d48807 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -50,6 +50,18 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase u.reload assert_not_nil u.other_uuid end + + def test_pk_and_sequence_for_uuid_primary_key + pk, seq = @connection.pk_and_sequence_for('pg_uuids') + assert_equal 'id', pk + assert_equal nil, seq + end + + def test_schema_dumper_for_uuid_primary_key + schema = StringIO.new + ActiveRecord::SchemaDumper.dump(@connection, schema) + assert_match(/\bcreate_table "pg_uuids", id: :uuid\b/, schema.string) + end end class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index f5316952b8..87af24cbe6 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -414,6 +414,26 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 15, topic.replies.size end + def test_counter_cache_double_destroy + topic = Topic.create :title => "Zoom-zoom-zoom" + + 5.times do + topic.replies.create(:title => "re: zoom", :content => "speedy quick!") + end + + assert_equal 5, topic.reload[:replies_count] + assert_equal 5, topic.replies.size + + reply = topic.replies.first + + reply.destroy + assert_equal 4, topic.reload[:replies_count] + + reply.destroy + assert_equal 4, topic.reload[:replies_count] + assert_equal 4, topic.replies.size + end + def test_custom_counter_cache reply = Reply.create(:title => "re: zoom", :content => "speedy quick!") assert_equal 0, reply[:replies_count] diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index be59ffc4ab..428145d00b 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -58,6 +58,24 @@ module ActiveRecord end end + class RemoveIndexMigration1 < SilentMigration + def self.up + create_table("horses") do |t| + t.column :name, :string + t.column :color, :string + t.index [:name, :color] + end + end + end + + class RemoveIndexMigration2 < SilentMigration + def change + change_table("horses") do |t| + t.remove_index [:name, :color] + end + end + end + class LegacyMigration < ActiveRecord::Migration def self.up create_table("horses") do |t| @@ -104,6 +122,16 @@ module ActiveRecord end end + def test_exception_on_removing_index_without_column_option + RemoveIndexMigration1.new.migrate(:up) + migration = RemoveIndexMigration2.new + migration.migrate(:up) + + assert_raises(IrreversibleMigration) do + migration.migrate(:down) + end + end + def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index b6e140b912..165b7454b7 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -131,6 +131,20 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert_equal 's1', ship.reload.name end + def test_reuse_already_built_new_record + pirate = Pirate.new + ship_built_first = pirate.build_ship + pirate.ship_attributes = { name: 'Ship 1' } + assert_equal ship_built_first.object_id, pirate.ship.object_id + end + + def test_do_not_allow_assigning_foreign_key_when_reusing_existing_new_record + pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") + pirate.build_ship + pirate.ship_attributes = { name: 'Ship 1', pirate_id: pirate.id + 1 } + assert_equal pirate.id, pirate.ship.pirate_id + end + def test_reject_if_with_a_proc_which_returns_true_always_for_has_many Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true } man = Man.create(name: "John") diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 817897ceac..f061c28e88 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -5,6 +5,16 @@ class SanitizeTest < ActiveRecord::TestCase def setup end + def test_sanitize_sql_hash_handles_associations + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + expected_value = "`adorable_animals`.`name` = 'Bambi'" + else + expected_value = "\"adorable_animals\".\"name\" = 'Bambi'" + end + + assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}}) + end + def test_sanitize_sql_array_handles_string_interpolation quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"]) |