From 3145c8afd19dc495930ccc3cdfb0d4b3ac7c370a Mon Sep 17 00:00:00 2001 From: Vijay Dev Date: Sat, 18 Dec 2010 01:11:58 +0530 Subject: changes in examples - reflect new mailer api and mysql2 adapter --- activerecord/README.rdoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 101a595ecd..91a23da8ad 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -84,7 +84,7 @@ A short rundown of some of the major features: class CommentObserver < ActiveRecord::Observer def after_create(comment) # is called just after Comment#save - Notifications.deliver_new_comment("david@loudthinking.com", comment) + CommentMailer.new_comment_email("david@loudthinking.com", comment) end end @@ -128,7 +128,7 @@ A short rundown of some of the major features: # connect to MySQL with authentication ActiveRecord::Base.establish_connection( - :adapter => "mysql", + :adapter => "mysql2", :host => "localhost", :username => "me", :password => "secret", -- cgit v1.2.3 From 379c02267b3cbbf6d1ac48bc79ec6ea01af7b53a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 16 Dec 2010 22:06:19 +0000 Subject: Specify insert_record with NotImplementedError in AssociationCollection, to indicate that subclasses should implement it. Also add save_record to reduce duplication. --- .../lib/active_record/associations/association_collection.rb | 11 +++++++++++ .../associations/has_and_belongs_to_many_association.rb | 6 +----- .../lib/active_record/associations/has_many_association.rb | 2 +- .../associations/has_many_through_association.rb | 6 +----- 4 files changed, 14 insertions(+), 11 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 11a7a725e5..51d8952eca 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -476,6 +476,17 @@ module ActiveRecord end private + # Do the relevant stuff to insert the given record into the association collection. The + # force param specifies whether or not an exception should be raised on failure. The + # validate param specifies whether validation should be performed (if force is false). + def insert_record(record, force = true, validate = true) + raise NotImplementedError + end + + def save_record(record, force, validate) + force ? record.save! : record.save(:validate => validate) + end + def create_record(attrs) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) ensure_owner_is_persisted! diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index e2ce9aefcf..259bf1f591 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -35,11 +35,7 @@ module ActiveRecord def insert_record(record, force = true, validate = true) if record.new_record? - if force - record.save! - else - return false unless record.save(:validate => validate) - end + return false unless save_record(record, force, validate) end if @reflection.options[:insert_sql] diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 4ff61fff45..cfbb49b6a3 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -55,7 +55,7 @@ module ActiveRecord def insert_record(record, force = false, validate = true) set_belongs_to_association_for(record) - force ? record.save! : record.save(:validate => validate) + save_record(record, force, validate) end # Deletes the records according to the :dependent option. 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 781aa7ef62..179a27725b 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -60,11 +60,7 @@ module ActiveRecord def insert_record(record, force = true, validate = true) if record.new_record? - if force - record.save! - else - return false unless record.save(:validate => validate) - end + return false unless save_record(record, force, validate) end through_association = @owner.send(@reflection.through_reflection.name) -- cgit v1.2.3 From ffa57671bb664a715eb2eebaa7476c747abf0fb1 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 16 Dec 2010 22:39:46 +0000 Subject: Delete create, create! and create_record from HasManyThroughAssociation in exchange for more generic versions in AssociationCollection --- .../associations/association_collection.rb | 26 ++++++++++++---------- .../associations/has_many_through_association.rb | 18 --------------- 2 files changed, 14 insertions(+), 30 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 51d8952eca..3f80133299 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -263,7 +263,7 @@ module ActiveRecord else create_record(attrs) do |record| yield(record) if block_given? - record.save + insert_record(record, false) end end end @@ -271,7 +271,7 @@ module ActiveRecord def create!(attrs = {}) create_record(attrs) do |record| yield(record) if block_given? - record.save! + insert_record(record, true) end end @@ -488,18 +488,20 @@ module ActiveRecord end def create_record(attrs) - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) ensure_owner_is_persisted! - scoped_where = scoped.where_values_hash - create_scope = scoped_where ? @scope[:create].merge(scoped_where) : @scope[:create] - record = @reflection.klass.send(:with_scope, :create => create_scope) do - @reflection.build_association(attrs) - end - if block_given? - add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } - else - add_record_to_target_with_callbacks(record) + attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) + create_scope = @scope[:create].merge(scoped.where_values_hash || {}) + + transaction do + record = with_scope(:create => create_scope) do + @reflection.build_association(attrs) + end + if block_given? + add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } + else + add_record_to_target_with_callbacks(record) + end end end 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 179a27725b..f0bc6aedf2 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -9,14 +9,6 @@ module ActiveRecord alias_method :new, :build - def create!(attrs = nil) - create_record(attrs, true) - end - - def create(attrs = nil) - create_record(attrs, false) - end - def destroy(*records) transaction do delete_records(flatten_deeper(records)) @@ -35,16 +27,6 @@ module ActiveRecord end protected - def create_record(attrs, force = true) - ensure_owner_is_persisted! - - transaction do - object = @reflection.klass.new(attrs) - add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) } - object - end - end - def target_reflection_has_associated_record? if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank? false -- cgit v1.2.3 From 7f5fcc0785e28175b1a54971e5adc34ecd50787d Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 17 Dec 2010 09:56:47 +0000 Subject: Refactor create_record and build_record in AssociationCollection --- .../associations/association_collection.rb | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 3f80133299..18d05aa133 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -487,32 +487,20 @@ module ActiveRecord force ? record.save! : record.save(:validate => validate) end - def create_record(attrs) + def create_record(attrs, &block) ensure_owner_is_persisted! - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - create_scope = @scope[:create].merge(scoped.where_values_hash || {}) - transaction do - record = with_scope(:create => create_scope) do - @reflection.build_association(attrs) - end - if block_given? - add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } - else - add_record_to_target_with_callbacks(record) + with_scope(:create => @scope[:create].merge(scoped.where_values_hash || {})) do + build_record(attrs, &block) end end end - def build_record(attrs) + def build_record(attrs, &block) attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) record = @reflection.build_association(attrs) - if block_given? - add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) } - else - add_record_to_target_with_callbacks(record) - end + add_record_to_target_with_callbacks(record, &block) end def remove_records(*records) -- cgit v1.2.3 From 9863d8a5f6576ab10df51230c0531cec8d4468f9 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 17 Dec 2010 09:57:27 +0000 Subject: Remove unnecessary overloaded methods create, create! and create_record from HasAndBelongsToManyAssociation --- .../has_and_belongs_to_many_association.rb | 18 ------------------ 1 file changed, 18 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 259bf1f591..e17ac6f2cc 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -2,14 +2,6 @@ module ActiveRecord # = Active Record Has And Belongs To Many Association module Associations class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: - def create(attributes = {}) - create_record(attributes) { |record| insert_record(record) } - end - - def create!(attributes = {}) - create_record(attributes) { |record| insert_record(record, true) } - end - def columns @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns") end @@ -109,16 +101,6 @@ module ActiveRecord end private - def create_record(attributes, &block) - # Can't use Base.create because the foreign key may be a protected attribute. - ensure_owner_is_persisted! - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr) } - else - build_record(attributes, &block) - end - end - def record_timestamp_columns(record) if record.record_timestamps record.send(:all_timestamp_attributes).map { |x| x.to_s } -- cgit v1.2.3 From b8153fd5a18441567f787a33ca882acb3bb5088a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 16 Dec 2010 10:29:13 +0000 Subject: Fix problem where wrong keys are used in JoinAssociation when an association goes :through a belongs_to [#2801 state:resolved] --- .../join_dependency/join_association.rb | 21 ++++++++++++--------- activerecord/lib/active_record/reflection.rb | 4 ++++ .../has_many_through_associations_test.rb | 7 +++++++ activerecord/test/cases/reflection_test.rb | 5 +++++ activerecord/test/models/post.rb | 1 + 5 files changed, 29 insertions(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb index 694778008b..02707dfae1 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -190,17 +190,20 @@ module ActiveRecord ).alias @aliased_join_table_name jt_conditions = [] - jt_foreign_key = first_key = second_key = nil + first_key = second_key = nil - if through_reflection.options[:as] # has_many :through against a polymorphic join - as_key = through_reflection.options[:as].to_s - jt_foreign_key = as_key + '_id' - - jt_conditions << - join_table[as_key + '_type']. - eq(parent.active_record.base_class.name) + if through_reflection.macro == :belongs_to + jt_primary_key = through_reflection.primary_key_name + jt_foreign_key = through_reflection.association_primary_key else + jt_primary_key = through_reflection.active_record_primary_key jt_foreign_key = through_reflection.primary_key_name + + if through_reflection.options[:as] # has_many :through against a polymorphic join + jt_conditions << + join_table["#{through_reflection.options[:as]}_type"]. + eq(parent.active_record.base_class.name) + end end case source_reflection.macro @@ -233,7 +236,7 @@ module ActiveRecord end jt_conditions << - parent_table[parent.primary_key]. + parent_table[jt_primary_key]. eq(join_table[jt_foreign_key]) if through_reflection.options[:conditions] diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index b9caa64a0e..70165c600e 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -208,6 +208,10 @@ module ActiveRecord @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end + def association_primary_key + @association_primary_key ||= options[:primary_key] || klass.primary_key + end + def active_record_primary_key @active_record_primary_key ||= options[:primary_key] || active_record.primary_key end 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 77bc369ecc..cf0eedbd13 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -479,4 +479,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 1, mary.unique_categorized_posts.length assert_equal 1, mary.unique_categorized_post_ids.length end + + def test_joining_has_many_through_belongs_to + posts = Post.joins(:author_categorizations). + where('categorizations.id' => categorizations(:mary_thinking_sti).id) + + assert_equal [posts(:eager_other)], posts + end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 912e3c47bb..1e205714a7 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -191,6 +191,11 @@ class ReflectionTest < ActiveRecord::TestCase assert_kind_of ThroughReflection, Subscriber.reflect_on_association(:books) end + def test_association_primary_key + assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s + assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s + end + def test_active_record_primary_key assert_equal "nick", Subscriber.reflect_on_association(:subscriptions).active_record_primary_key.to_s assert_equal "name", Author.reflect_on_association(:essay).active_record_primary_key.to_s diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 974e87d2bf..34ea49f731 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -38,6 +38,7 @@ class Post < ActiveRecord::Base end has_many :author_favorites, :through => :author + has_many :author_categorizations, :through => :author, :source => :categorizations has_many :comments_with_interpolated_conditions, :class_name => 'Comment', :conditions => ['#{"#{aliased_table_name}." rescue ""}body = ?', 'Thank you for the welcome'] -- cgit v1.2.3 From 57b90098930a52fa160f9b1aed2dcea0f557d670 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 17 Dec 2010 10:20:18 +0000 Subject: Refactor delete_records in HasManyAssociation --- .../active_record/associations/has_many_association.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index cfbb49b6a3..bb9dfd4f70 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -62,15 +62,16 @@ module ActiveRecord def delete_records(records) case @reflection.options[:dependent] when :destroy - records.each { |r| r.destroy } + records.each(&:destroy) when :delete_all - @reflection.klass.delete(records.map { |record| record.id }) + @reflection.klass.delete(records.map(&:id)) else - relation = Arel::Table.new(@reflection.table_name) - stmt = relation.where(relation[@reflection.primary_key_name].eq(@owner.id). - and(relation[@reflection.klass.primary_key].in(records.map { |r| r.id })) - ).compile_update(relation[@reflection.primary_key_name] => nil) - @owner.connection.update stmt.to_sql + updates = { @reflection.primary_key_name => nil } + conditions = { @reflection.association_primary_key => records.map(&:id) } + + with_scope(@scope) do + @reflection.klass.update_all(updates, conditions) + end @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter? end -- cgit v1.2.3 From 834e5336a5d8a8250251e756385e39ebfb4917c3 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 17 Dec 2010 20:54:50 +0000 Subject: has_many associations with :dependent => :delete_all should update the counter cache when deleting records --- .../lib/active_record/associations/has_many_association.rb | 4 +++- .../test/cases/associations/has_many_associations_test.rb | 12 ++++++++++++ activerecord/test/models/post.rb | 2 ++ activerecord/test/schema/schema.rb | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index bb9dfd4f70..2e99177af4 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -72,8 +72,10 @@ module ActiveRecord with_scope(@scope) do @reflection.klass.update_all(updates, conditions) end + end - @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) if has_cached_counter? + if has_cached_counter? && @reflection.options[:dependent] != :destroy + @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index fb772bb8e6..2b7ad3642a 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -616,6 +616,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_deleting_updates_counter_cache_with_dependent_delete_all + post = posts(:welcome) + + # Manually update the count as the tagging will have been added to the taggings association, + # rather than to the taggings_with_delete_all one (which is just a 'shadow' of the former) + post.update_attribute(:taggings_with_delete_all_count, post.taggings_with_delete_all.to_a.count) + + assert_difference "post.reload.taggings_with_delete_all_count", -1 do + post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first) + end + end + def test_deleting_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 34ea49f731..0083560ebe 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -59,6 +59,8 @@ class Post < ActiveRecord::Base end end + has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all + has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => "tags.name = 'Misc'" has_many :funky_tags, :through => :taggings, :source => :tag has_many :super_tags, :through => :taggings diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 177045ac51..99761a4160 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -441,6 +441,7 @@ ActiveRecord::Schema.define do t.string :type t.integer :comments_count, :default => 0 t.integer :taggings_count, :default => 0 + t.integer :taggings_with_delete_all_count, :default => 0 end create_table :price_estimates, :force => true do |t| -- cgit v1.2.3 From 37b67df7e47966c88eaf5c6ed8c5286df69d20b9 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 20 Dec 2010 21:53:11 +0000 Subject: Avoid Symbol#to_proc for performance reasons in Ruby 1.8 --- activerecord/lib/active_record/associations/has_many_association.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 2e99177af4..4b05cf6e81 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -62,12 +62,12 @@ module ActiveRecord def delete_records(records) case @reflection.options[:dependent] when :destroy - records.each(&:destroy) + records.each { |r| r.destroy } when :delete_all - @reflection.klass.delete(records.map(&:id)) + @reflection.klass.delete(records.map { |r| r.id }) else updates = { @reflection.primary_key_name => nil } - conditions = { @reflection.association_primary_key => records.map(&:id) } + conditions = { @reflection.association_primary_key => records.map { |r| r.id } } with_scope(@scope) do @reflection.klass.update_all(updates, conditions) -- cgit v1.2.3 From 6e14feb978434802e7a46b26d99d64e31f545fe2 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 20 Dec 2010 14:24:04 -0800 Subject: use array arithmetic rather than create sets --- .../lib/active_record/associations/association_collection.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 18d05aa133..1c8541be67 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -344,12 +344,10 @@ module ActiveRecord other_array.each { |val| raise_on_type_mismatch(val) } load_target - other = other_array.size < 100 ? other_array : other_array.to_set - current = @target.size < 100 ? @target : @target.to_set transaction do - delete(@target.select { |v| !other.include?(v) }) - concat(other_array.select { |v| !current.include?(v) }) + delete(@target - other_array) + concat(other_array - @target) end end -- cgit v1.2.3 From 3ce3c21997c92cee4123e76d75e39fcfe3d1e209 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 20 Dec 2010 14:40:07 -0800 Subject: no use for set, no need to to_ary, reduce extra objects --- .../lib/active_record/associations/association_collection.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 1c8541be67..c513e8ab08 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -1,4 +1,3 @@ -require 'set' require 'active_support/core_ext/array/wrap' module ActiveRecord @@ -75,7 +74,7 @@ module ActiveRecord find(:first, *args) else load_target unless loaded? - args = args[1..-1] if args.first.kind_of?(Hash) && args.first.empty? + args.shift if args.first.kind_of?(Hash) && args.first.empty? @target.first(*args) end end @@ -93,7 +92,7 @@ module ActiveRecord def to_ary load_target if @target.is_a?(Array) - @target.to_ary + @target else Array.wrap(@target) end -- cgit v1.2.3 From 05168067609b867eb77a99ff3eb39250d22ae046 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 20 Dec 2010 16:12:08 -0800 Subject: remove some lasigns --- .../lib/active_record/attribute_methods/primary_key.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 75ae06f5e9..a10e94d5f7 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -24,14 +24,14 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - key = 'id' case primary_key_prefix_type - when :table_name - key = base_name.to_s.foreign_key(false) - when :table_name_with_underscore - key = base_name.to_s.foreign_key + when :table_name + base_name.to_s.foreign_key(false) + when :table_name_with_underscore + base_name.to_s.foreign_key + else + 'id' end - key end # Sets the name of the primary key column to use to the given value, -- cgit v1.2.3 From 099a210c837daae692154add3b9da0a207825d35 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 20 Dec 2010 16:33:07 -0800 Subject: if there is no base name, we cannot determine a primary key --- activerecord/lib/active_record/attribute_methods/primary_key.rb | 6 ++++-- activerecord/test/cases/base_test.rb | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index a10e94d5f7..5ae1ebc9cf 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -24,11 +24,13 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: + return unless base_name + case primary_key_prefix_type when :table_name - base_name.to_s.foreign_key(false) + base_name.foreign_key(false) when :table_name_with_underscore - base_name.to_s.foreign_key + base_name.foreign_key else 'id' end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 86d4a90fc4..dc5e912412 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1069,6 +1069,7 @@ class BasicsTest < ActiveRecord::TestCase def test_define_attr_method_with_block k = Class.new( ActiveRecord::Base ) + k.primary_key = "id" k.send(:define_attr_method, :primary_key) { "sys_" + original_primary_key } assert_equal "sys_id", k.primary_key end @@ -1109,6 +1110,7 @@ class BasicsTest < ActiveRecord::TestCase def test_set_primary_key_with_block k = Class.new( ActiveRecord::Base ) + k.primary_key = 'id' k.set_primary_key { "sys_" + original_primary_key } assert_equal "sys_id", k.primary_key end -- cgit v1.2.3 From 207f266ccaaa9cd04cd2a7513ae5598c4358b510 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 20 Dec 2010 17:33:26 -0800 Subject: define_attr_method must serialize nil correctly --- activerecord/lib/active_record/attribute_methods/primary_key.rb | 8 ++++++-- activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb | 5 +++++ activerecord/test/cases/associations/join_model_test.rb | 4 ++++ activerecord/test/cases/base_test.rb | 5 +++++ 4 files changed, 20 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 5ae1ebc9cf..c016e936cb 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -24,7 +24,7 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return unless base_name + return unless base_name && !base_name.blank? case primary_key_prefix_type when :table_name @@ -32,7 +32,11 @@ module ActiveRecord when :table_name_with_underscore base_name.foreign_key else - 'id' + if ActiveRecord::Base != self && connection.table_exists?(table_name) + connection.primary_key(table_name) + else + 'id' + end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 234696adeb..b8abdface4 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -9,6 +9,11 @@ module ActiveRecord :timeout => 100 end + def test_primary_key_returns_nil_for_no_pk + @conn.exec_query('create table ex(id int, data string)') + assert_nil @conn.primary_key('ex') + end + def test_connection_no_db assert_raises(ArgumentError) do Base.sqlite3_connection {} diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 0a57c7883f..58542bc939 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -512,6 +512,10 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) } end + def test_add_to_join_table_with_no_id + assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) } + end + def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded author = authors(:david) assert_equal 10, author.comments.size diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index dc5e912412..e186b2aea7 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -19,6 +19,7 @@ require 'models/minimalistic' require 'models/warehouse_thing' require 'models/parrot' require 'models/loose_person' +require 'models/edge' require 'rexml/document' require 'active_support/core_ext/exception' @@ -48,6 +49,10 @@ class Boolean < ActiveRecord::Base; end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts + def test_primary_key_with_no_id + assert_nil Edge.primary_key + end + def test_select_symbol topic_ids = Topic.select(:id).map(&:id).sort assert_equal Topic.find(:all).map(&:id).sort, topic_ids -- cgit v1.2.3 From 87ae85d5360ac8965f2467680e556c1f94806ba8 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 20 Dec 2010 18:33:55 -0800 Subject: returning id (for some yet to be discovered reason) --- activerecord/lib/active_record/attribute_methods/primary_key.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index c016e936cb..b891b2c50e 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -24,7 +24,7 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return unless base_name && !base_name.blank? + return 'id' unless base_name && !base_name.blank? case primary_key_prefix_type when :table_name -- cgit v1.2.3 From 186a1b11449a2b94030791b2c778a76b07bb099f Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 22 Dec 2010 17:30:57 -0800 Subject: build an AST rather than build SQL strings --- activerecord/lib/active_record/association_preload.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index f2094283a2..9504b00515 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -185,6 +185,9 @@ module ActiveRecord end def preload_has_and_belongs_to_many_association(records, reflection, preload_options={}) + + left = reflection.klass.arel_table + table_name = reflection.klass.quoted_table_name id_to_record_map, ids = construct_id_map(records) records.each {|record| record.send(reflection.name).loaded} @@ -193,9 +196,15 @@ module ActiveRecord conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" conditions << append_conditions(reflection, preload_options) + right = Arel::Table.new(options[:join_table]).alias('t0') + condition = left[reflection.klass.primary_key].eq( + right[reflection.association_foreign_key]) + + join = left.create_join(right, left.create_on(condition)) + associated_records_proxy = reflection.klass.unscoped. includes(options[:include]). - joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}"). + joins(join). select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id"). order(options[:order]) -- cgit v1.2.3 From 2de9b858a09e735fa4e7705e929f945947e68dae Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 22 Dec 2010 18:02:49 -0800 Subject: to_sym stuff before passing it to arel --- activerecord/lib/active_record/base.rb | 2 +- activerecord/lib/active_record/relation/predicate_builder.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 858ccebbfa..6aac25ba9b 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -913,7 +913,7 @@ module ActiveRecord #:nodoc: end def type_condition - sti_column = arel_table[inheritance_column] + sti_column = arel_table[inheritance_column.to_sym] condition = sti_column.eq(sti_name) descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) } diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 70d84619a1..d748eebc41 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -15,7 +15,7 @@ module ActiveRecord table = Arel::Table.new(table_name, :engine => engine) end - attribute = table[column] || Arel::Attribute.new(table, column) + attribute = table[column.to_sym] || Arel::Attribute.new(table, column) case value when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation -- cgit v1.2.3 From 6ca921a98cefa2bce67486f1ba2a8f52f4170d78 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 22 Dec 2010 18:05:30 -0800 Subject: use arel to compile SQL rather than build strings --- activerecord/lib/active_record/association_preload.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 9504b00515..9ac792fe3f 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -201,11 +201,18 @@ module ActiveRecord right[reflection.association_foreign_key]) join = left.create_join(right, left.create_on(condition)) + select = [ + # FIXME: options[:select] is always nil in the tests. Do we really + # need it? + options[:select] || left[Arel.star], + right[reflection.primary_key_name].as( + Arel.sql('the_parent_record_id')) + ] associated_records_proxy = reflection.klass.unscoped. includes(options[:include]). joins(join). - select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id"). + select(select). order(options[:order]) all_associated_records = associated_records(ids) do |some_ids| -- cgit v1.2.3 From 3e64336647fefe598ff3b0775401e2c2c5d378d3 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 22 Dec 2010 18:22:54 -0800 Subject: removing SQL interpolation, please use scoping and attribute conditionals as a replacement --- activerecord/CHANGELOG | 3 +++ activerecord/lib/active_record/association_preload.rb | 7 +------ activerecord/test/cases/associations/eager_test.rb | 4 ---- activerecord/test/models/post.rb | 3 --- 4 files changed, 4 insertions(+), 13 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index fd571c4ca4..8ebc87145c 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,8 @@ *Rails 3.1.0 (unreleased)* +* Removed support for interpolated SQL conditions. Please use scoping +along with attribute conditionals as a replacement. + * Added ActiveRecord::Base#has_secure_password (via ActiveModel::SecurePassword) to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH]. Example: # Schema: User(name:string, password_digest:string, password_salt:string) diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 9ac792fe3f..fc7c544a55 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -389,14 +389,9 @@ module ActiveRecord end end - - def interpolate_sql_for_preload(sql) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) - end - def append_conditions(reflection, preload_options) sql = "" - sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions + sql << " AND (#{reflection.sanitized_conditions})" if reflection.sanitized_conditions sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions] sql end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index d5262b1ee4..6efb0d57e4 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -658,10 +658,6 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal people(:david, :susan), Person.find(:all, :include => [:readers, :primary_contact, :number1_fan], :conditions => "number1_fans_people.first_name like 'M%'", :order => 'people.id', :limit => 2, :offset => 0) end - def test_preload_with_interpolation - assert_equal [comments(:greetings)], Post.find(posts(:welcome).id, :include => :comments_with_interpolated_conditions).comments_with_interpolated_conditions - end - def test_polymorphic_type_condition post = Post.find(posts(:thinking).id, :include => :taggings) assert post.taggings.include?(taggings(:thinking_general)) diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 0083560ebe..b51f7ff48f 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -40,9 +40,6 @@ class Post < ActiveRecord::Base has_many :author_favorites, :through => :author has_many :author_categorizations, :through => :author, :source => :categorizations - has_many :comments_with_interpolated_conditions, :class_name => 'Comment', - :conditions => ['#{"#{aliased_table_name}." rescue ""}body = ?', 'Thank you for the welcome'] - has_one :very_special_comment has_one :very_special_comment_with_post, :class_name => "VerySpecialComment", :include => :post has_many :special_comments -- cgit v1.2.3 From 35f5938c9128718a2c5515524c815def75a53f00 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 22 Dec 2010 18:30:18 -0800 Subject: probably should use the some_ids variable here. o_O --- activerecord/lib/active_record/association_preload.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index fc7c544a55..7f5c5dd2ad 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -216,7 +216,7 @@ module ActiveRecord order(options[:order]) all_associated_records = associated_records(ids) do |some_ids| - associated_records_proxy.where([conditions, ids]).to_a + associated_records_proxy.where([conditions, some_ids]).to_a end set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id') -- cgit v1.2.3 From 83ffb82fb9f53e1c90d3b598067494a12258b605 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 22 Dec 2010 19:17:44 -0800 Subject: Arel::Table#[] always returns an attribute, so no need for || --- activerecord/lib/active_record/relation/predicate_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index d748eebc41..6760d65186 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -15,7 +15,7 @@ module ActiveRecord table = Arel::Table.new(table_name, :engine => engine) end - attribute = table[column.to_sym] || Arel::Attribute.new(table, column) + attribute = table[column.to_sym] case value when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation -- cgit v1.2.3 From c7f81f14dfe02a2b3839b86d9fd47ce784aeeca6 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 22 Dec 2010 19:20:08 -0800 Subject: arel can escape the id, so avoid using the database connection --- activerecord/lib/active_record/relation/predicate_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 6760d65186..b3f1b9e36a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -26,7 +26,7 @@ module ActiveRecord when Range, Arel::Relation attribute.in(value) when ActiveRecord::Base - attribute.eq(Arel.sql(value.quoted_id)) + attribute.eq(value.id) when Class # FIXME: I think we need to deprecate this behavior attribute.eq(value.name) -- cgit v1.2.3 From d9c8c47e3db89ca75de6ae9a8497659378ef0c1d Mon Sep 17 00:00:00 2001 From: Raimonds Simanovskis Date: Thu, 23 Dec 2010 23:11:32 +0800 Subject: Fix for default_scope tests to ensure comparing of equally sorted lists This is additional fix for commit ebc47465a5865ab91dc7d058d2d8a0cc961510d7 Respect the default_scope on a join model when reading a through association which otherwise was failing on Oracle (as it returned fixture comments in different order). --- .../test/cases/associations/has_many_through_associations_test.rb | 2 +- .../test/cases/associations/has_one_through_associations_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') 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 cf0eedbd13..816b43ab9e 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -466,7 +466,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_with_default_scope_on_join_model - assert_equal posts(:welcome).comments, authors(:david).comments_on_first_posts + assert_equal posts(:welcome).comments.order('id').all, authors(:david).comments_on_first_posts end def test_create_has_many_through_with_default_scope_on_join_model diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 93a4f498d0..856214997c 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -235,6 +235,6 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_has_one_through_with_default_scope_on_join_model - assert_equal posts(:welcome).comments.first, authors(:david).comment_on_first_posts + assert_equal posts(:welcome).comments.order('id').first, authors(:david).comment_on_first_posts end end -- cgit v1.2.3 From 2b795050de72c6d68aba7513510f74ddc8959ee7 Mon Sep 17 00:00:00 2001 From: Raimonds Simanovskis Date: Thu, 23 Dec 2010 21:55:42 +0800 Subject: fixed retrieval of primary key value in Ralation#insert method previously primary key value was always assigned nil which caused Oracle enhanced adapter failing tests --- activerecord/lib/active_record/relation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 7ecba1c43a..d4c5c85594 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -31,7 +31,7 @@ module ActiveRecord im = arel.compile_insert values im.into @table primary_key_name = @klass.primary_key - primary_key_value = Hash === values ? values[primary_key_name] : nil + primary_key_value = primary_key_name && Hash === values ? values[primary_key] : nil @klass.connection.insert( im.to_sql, -- cgit v1.2.3 From df3cfa6aaea326bb60b639524abbcc1f73854d1f Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 23 Dec 2010 07:33:22 -0800 Subject: avoid duping and new objects --- activerecord/lib/active_record/association_preload.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 7f5c5dd2ad..6d905fe6fe 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -211,10 +211,11 @@ module ActiveRecord associated_records_proxy = reflection.klass.unscoped. includes(options[:include]). - joins(join). - select(select). order(options[:order]) + associated_records_proxy.joins_values = [join] + associated_records_proxy.select_values = select + all_associated_records = associated_records(ids) do |some_ids| associated_records_proxy.where([conditions, some_ids]).to_a end -- cgit v1.2.3 From c6db37e69b1ff07f7ad535d4752d0e6eb2d15bff Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 19 Dec 2010 14:17:29 +0000 Subject: Don't allow a has_one association to go :through a collection association [#2976 state:resolved] --- activerecord/lib/active_record/associations.rb | 6 ++++++ activerecord/lib/active_record/reflection.rb | 4 ++++ .../cases/associations/has_one_through_associations_test.rb | 12 +++++++----- activerecord/test/models/author.rb | 4 +++- activerecord/test/models/member.rb | 9 ++++++--- 5 files changed, 26 insertions(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index cdc8f25119..c0cd222244 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -32,6 +32,12 @@ module ActiveRecord end end + class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection, through_reflection) + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.") + end + end + class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc: def initialize(reflection) through_reflection = reflection.through_reflection diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 70165c600e..7a7f7812df 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -388,6 +388,10 @@ module ActiveRecord raise HasManyThroughSourceAssociationMacroError.new(self) end + if macro == :has_one && through_reflection.collection? + raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) + end + check_validity_of_inverse! end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 856214997c..f6307e6cf0 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -25,10 +25,6 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:boring_club), @member.club end - def test_has_one_through_with_has_many - assert_equal clubs(:moustache_club), @member.favourite_club - end - def test_creating_association_creates_through_record new_member = Member.create(:name => "Chris") new_member.club = Club.create(:name => "LRUG") @@ -235,6 +231,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_has_one_through_with_default_scope_on_join_model - assert_equal posts(:welcome).comments.order('id').first, authors(:david).comment_on_first_posts + assert_equal posts(:welcome).comments.order('id').first, authors(:david).comment_on_first_post + end + + def test_has_one_through_many_raises_exception + assert_raise(ActiveRecord::HasOneThroughCantAssociateThroughCollection) do + members(:groucho).club_through_many + end end end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index fd6d2b384a..244c5ac4f5 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -28,7 +28,9 @@ class Author < ActiveRecord::Base has_many :first_posts has_many :comments_on_first_posts, :through => :first_posts, :source => :comments, :order => 'posts.id desc, comments.id asc' - has_one :comment_on_first_posts, :through => :first_posts, :source => :comments, :order => 'posts.id desc, comments.id asc' + + has_one :first_post + has_one :comment_on_first_post, :through => :first_post, :source => :comments, :order => 'posts.id desc, comments.id asc' has_many :thinking_posts, :class_name => 'Post', :conditions => { :title => 'So I was thinking' }, :dependent => :delete_all has_many :welcome_posts, :class_name => 'Post', :conditions => { :title => 'Welcome to the weblog' } diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 255fb569d7..5b0a5ebf7f 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -1,12 +1,15 @@ class Member < ActiveRecord::Base has_one :current_membership - has_many :memberships + has_one :membership has_many :fellow_members, :through => :club, :source => :members has_one :club, :through => :current_membership - has_one :favourite_club, :through => :memberships, :conditions => ["memberships.favourite = ?", true], :source => :club + has_one :favourite_club, :through => :membership, :conditions => ["memberships.favourite = ?", true], :source => :club has_one :sponsor, :as => :sponsorable has_one :sponsor_club, :through => :sponsor has_one :member_detail has_one :organization, :through => :member_detail belongs_to :member_type -end \ No newline at end of file + + has_many :current_memberships + has_one :club_through_many, :through => :current_memberships, :source => :club +end -- cgit v1.2.3 From b79823832e6cd30a9f14f97ffdf1642d4d63d4ea Mon Sep 17 00:00:00 2001 From: Will Bryant Date: Thu, 13 Aug 2009 12:38:20 +1200 Subject: Verify that has_one :through preload respects the :conditions [#2976 state:resolved] --- .../cases/associations/has_one_through_associations_test.rb | 12 ++++++++++++ activerecord/test/models/member.rb | 1 + 2 files changed, 13 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index f6307e6cf0..f6365e0216 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -84,6 +84,18 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_not_nil assert_no_queries {members[0].sponsor_club} end + def test_has_one_through_with_conditions_eager_loading + # conditions on the through table + assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :favourite_club).favourite_club + memberships(:membership_of_favourite_club).update_attribute(:favourite, false) + assert_equal nil, Member.find(@member.id, :include => :favourite_club).favourite_club + + # conditions on the source table + assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :hairy_club).hairy_club + clubs(:moustache_club).update_attribute(:name, "Association of Clean-Shaven Persons") + assert_equal nil, Member.find(@member.id, :include => :hairy_club).hairy_club + end + def test_has_one_through_polymorphic_with_source_type assert_equal members(:groucho), clubs(:moustache_club).sponsored_member end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 5b0a5ebf7f..15ad6aedd3 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -4,6 +4,7 @@ class Member < ActiveRecord::Base has_many :fellow_members, :through => :club, :source => :members has_one :club, :through => :current_membership has_one :favourite_club, :through => :membership, :conditions => ["memberships.favourite = ?", true], :source => :club + has_one :hairy_club, :through => :membership, :conditions => {:clubs => {:name => "Moustache and Eyebrow Fancier Club"}}, :source => :club has_one :sponsor, :as => :sponsorable has_one :sponsor_club, :through => :sponsor has_one :member_detail -- cgit v1.2.3 From 85683f2a79dbf81130361cb6426786cf6b0d1925 Mon Sep 17 00:00:00 2001 From: Szymon Nowak Date: Thu, 13 Aug 2009 21:18:43 +0200 Subject: Fix creation of has_many through records with custom primary_key option on belongs_to [#2990 state:resolved] --- .../associations/through_association_scope.rb | 7 ++++- .../has_many_through_associations_test.rb | 30 +++++++++++++++++++++- activerecord/test/models/author.rb | 1 + activerecord/test/models/categorization.rb | 1 + activerecord/test/schema/schema.rb | 1 + 5 files changed, 38 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 5dc5b0c048..99920d4c63 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -106,7 +106,12 @@ module ActiveRecord # TODO: revisit this to allow it for deletion, supposing dependent option is supported raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) - join_attributes = construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) + join_attributes = construct_owner_attributes(@reflection.through_reflection) + + join_attributes.merge!( + @reflection.source_reflection.primary_key_name => + associate.send(@reflection.source_reflection.association_primary_key) + ) if @reflection.options[:source_type] join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name) 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 816b43ab9e..d237273464 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -21,7 +21,7 @@ require 'models/categorization' require 'models/category' class HasManyThroughAssociationsTest < ActiveRecord::TestCase - fixtures :posts, :readers, :people, :comments, :authors, + fixtures :posts, :readers, :people, :comments, :authors, :categories, :owners, :pets, :toys, :jobs, :references, :companies, :subscribers, :books, :subscriptions, :developers, :categorizations @@ -397,6 +397,34 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal people(:susan).agents.map(&:agents).flatten, people(:susan).agents_of_agents end + def test_associate_existing_with_nonstandard_primary_key_on_belongs_to + Categorization.create(:author => authors(:mary), :named_category_name => categories(:general).name) + assert_equal categories(:general), authors(:mary).named_categories.first + end + + def test_collection_build_with_nonstandard_primary_key_on_belongs_to + author = authors(:mary) + category = author.named_categories.build(:name => "Primary") + author.save + assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name) + assert author.named_categories(true).include?(category) + end + + def test_collection_create_with_nonstandard_primary_key_on_belongs_to + author = authors(:mary) + category = author.named_categories.create(:name => "Primary") + assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name) + assert author.named_categories(true).include?(category) + end + + def test_collection_delete_with_nonstandard_primary_key_on_belongs_to + author = authors(:mary) + category = author.named_categories.create(:name => "Primary") + author.named_categories.delete(category) + assert !Categorization.exists?(:author_id => author.id, :named_category_name => category.name) + assert author.named_categories(true).empty? + end + def test_collection_singular_ids_getter_with_string_primary_keys book = books(:awdr) assert_equal 2, book.subscriber_ids.size diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 244c5ac4f5..83a6f5d926 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -78,6 +78,7 @@ class Author < ActiveRecord::Base has_many :categorizations has_many :categories, :through => :categorizations + has_many :named_categories, :through => :categorizations has_many :special_categorizations has_many :special_categories, :through => :special_categorizations, :source => :category diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index fdb0a11540..45f50e4af3 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -1,6 +1,7 @@ class Categorization < ActiveRecord::Base belongs_to :post belongs_to :category + belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name belongs_to :author belongs_to :author_using_custom_pk, :class_name => 'Author', :foreign_key => :author_id, :primary_key => :author_address_extra_id diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 99761a4160..3dea7e1492 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -110,6 +110,7 @@ ActiveRecord::Schema.define do create_table :categorizations, :force => true do |t| t.column :category_id, :integer + t.string :named_category_name t.column :post_id, :integer t.column :author_id, :integer t.column :special, :boolean -- cgit v1.2.3 From 030480ac1f4fbf8bf74a0d9298544426caf26894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81omnicki?= Date: Sun, 12 Dec 2010 01:37:56 +0100 Subject: Fix behaviour of foo.has_many_through_association.select('custom select') [#6089 state:resolved] --- .../lib/active_record/associations/through_association_scope.rb | 4 ++-- .../test/cases/associations/has_many_through_associations_test.rb | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 99920d4c63..c11fce5db0 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -63,8 +63,8 @@ module ActiveRecord end def construct_select(custom_select = nil) - distinct = "DISTINCT " if @reflection.options[:uniq] - custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*" + distinct = "DISTINCT #{@reflection.quoted_table_name}.*" if @reflection.options[:uniq] + custom_select || @reflection.options[:select] || distinct end def construct_joins 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 d237273464..6b71e73718 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -514,4 +514,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [posts(:eager_other)], posts end + + def test_select_chosen_fields_only + author = authors(:david) + assert_equal ['body'], author.comments.select('comments.body').first.attributes.keys + end end -- cgit v1.2.3 From ff7bde62c857ec94f45a5be3bc76468deb8b0b3a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 22 Dec 2010 00:19:59 +0000 Subject: When a has_many association is not :uniq, appending the same record multiple times should append it to the @target multiple times [#5964 state:resolved] --- .../active_record/associations/association_collection.rb | 4 ++-- activerecord/lib/active_record/nested_attributes.rb | 13 ++++++++++++- .../associations/has_many_through_associations_test.rb | 10 ++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index c513e8ab08..7964f4fa2b 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -462,10 +462,10 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? @target ||= [] unless loaded? - if index = @target.index(record) + if @reflection.options[:uniq] && index = @target.index(record) @target[index] = record else - @target << record + @target << record end callback(:after_add, record) set_inverse_instance(record, @owner) diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 050b521b6a..16023defe3 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -405,7 +405,18 @@ module ActiveRecord end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } - association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? && !call_reject_if(association_name, attributes) + unless association.loaded? || call_reject_if(association_name, attributes) + # Make sure we are operating on the actual object which is in the association's + # proxy_target array (either by finding it, or adding it if not found) + target_record = association.proxy_target.detect { |record| record == existing_record } + + if target_record + existing_record = target_record + else + association.send(:add_record_to_target_with_callbacks, existing_record) + end + end + assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) else 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 6b71e73718..dfb3292502 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -45,6 +45,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).reload.people(true).include?(people(:david)) end + def test_associate_existing_record_twice_should_add_to_target_twice + post = posts(:thinking) + person = people(:david) + + assert_difference 'post.people.to_a.count', 2 do + post.people << person + post.people << person + end + end + def test_associating_new assert_queries(1) { posts(:thinking) } new_person = nil # so block binding catches it -- cgit v1.2.3 From 4e13625818579551c01640cb405a8c22a3bd0e68 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 22 Dec 2010 10:11:27 +0000 Subject: Test demonstrating problem with foo.association_ids where it's a has_many :through with :conditions, with a belongs_to as the source reflection --- .../test/cases/associations/has_many_through_associations_test.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'activerecord') 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 dfb3292502..ad67886202 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -529,4 +529,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase author = authors(:david) assert_equal ['body'], author.comments.select('comments.body').first.attributes.keys end + + def test_get_has_many_through_belongs_to_ids_with_conditions + assert_equal [categories(:general).id], authors(:mary).categories_like_general_ids + end end -- cgit v1.2.3 From 1619c2435b2b9c821b2b0dcab9624dbb6b23eaaa Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 22 Dec 2010 10:13:21 +0000 Subject: Revert "Optimize _ids for hm:t with belongs_to source". The optimisation has too many edge cases, such as when the reflection, source reflection, or through reflection has conditions, orders, etc. [#6153 state:resolved] This reverts commit 373b053dc8b99dac1abc3879a17a2bf8c30302b5. Conflicts: activerecord/lib/active_record/associations.rb --- activerecord/lib/active_record/associations.rb | 9 +-------- .../cases/associations/has_many_through_associations_test.rb | 8 ++------ 2 files changed, 3 insertions(+), 14 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index c0cd222244..e4dff69efe 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1512,16 +1512,9 @@ module ActiveRecord if send(reflection.name).loaded? || reflection.options[:finder_sql] send(reflection.name).map { |r| r.id } else - if reflection.through_reflection && reflection.source_reflection.belongs_to? - through = reflection.through_reflection - primary_key = reflection.source_reflection.primary_key_name - send(through.name).select("DISTINCT #{through.quoted_table_name}.#{primary_key}").map! { |r| r.send(primary_key) } - else - send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map! { |r| r.id } - end + send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map! { |r| r.id } end end - end def collection_accessor_methods(reflection, association_proxy_class, writer = true) 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 ad67886202..a244d310c8 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -334,12 +334,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 2, people(:michael).jobs.size end - def test_get_ids_for_belongs_to_source - assert_sql(/DISTINCT/) { assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort } - end - - def test_get_ids_for_has_many_source - assert_equal [comments(:eager_other_comment1).id], authors(:mary).comment_ids + def test_get_ids + assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort end def test_get_ids_for_loaded_associations -- cgit v1.2.3 From 3f17ed407c5d61bc01fd59776205486c2350f36e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 22 Dec 2010 11:45:37 +0000 Subject: Test to verify that #2189 (count with has_many :through and a named_scope) is fixed --- .../test/cases/associations/has_many_through_associations_test.rb | 5 +++++ activerecord/test/models/category.rb | 2 ++ 2 files changed, 7 insertions(+) (limited to 'activerecord') 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 a244d310c8..a417345780 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -529,4 +529,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_get_has_many_through_belongs_to_ids_with_conditions assert_equal [categories(:general).id], authors(:mary).categories_like_general_ids end + + def test_count_has_many_through_with_named_scope + assert_equal 2, authors(:mary).categories.count + assert_equal 1, authors(:mary).categories.general.count + end end diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index 48415846dd..06908ea85e 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -23,6 +23,8 @@ class Category < ActiveRecord::Base has_many :categorizations has_many :authors, :through => :categorizations, :select => 'authors.*, categorizations.post_id' + + scope :general, :conditions => { :name => 'General' } end class SpecialCategory < Category -- cgit v1.2.3 From 2d9626fc74c2d57f90c856c37e5967bbe6651bd8 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 22 Dec 2010 20:57:41 +0000 Subject: Improved strategy for updating a belongs_to association when the foreign key changes. Rather than resetting each affected association when the foreign key changes, we should lazily check for 'staleness' (where fk does not match target id) when the association is accessed. --- activerecord/lib/active_record/associations.rb | 43 +--------------------- .../associations/association_proxy.rb | 10 +++++ .../associations/belongs_to_association.rb | 8 ++++ .../belongs_to_polymorphic_association.rb | 9 +++++ activerecord/lib/active_record/reflection.rb | 5 ++- activerecord/test/cases/nested_attributes_test.rb | 5 ++- activerecord/test/cases/reflection_test.rb | 2 + 7 files changed, 37 insertions(+), 45 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e4dff69efe..c6c41a7a12 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1229,14 +1229,12 @@ module ActiveRecord if reflection.options[:polymorphic] association_accessor_methods(reflection, BelongsToPolymorphicAssociation) - association_foreign_type_setter_method(reflection) else association_accessor_methods(reflection, BelongsToAssociation) association_constructor_method(:build, reflection, BelongsToAssociation) association_constructor_method(:create, reflection, BelongsToAssociation) end - association_foreign_key_setter_method(reflection) add_counter_cache_callbacks(reflection) if options[:counter_cache] add_touch_callbacks(reflection, options[:touch]) if options[:touch] @@ -1456,7 +1454,7 @@ module ActiveRecord force_reload = params.first unless params.empty? association = association_instance_get(reflection.name) - if association.nil? || force_reload + if association.nil? || force_reload || association.stale_target? association = association_proxy_class.new(self, reflection) retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload if retval.nil? and association_proxy_class == BelongsToAssociation @@ -1556,45 +1554,6 @@ module ActiveRecord end end - def association_foreign_key_setter_method(reflection) - setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection| - if belongs_to_reflection.primary_key_name == reflection.primary_key_name - "association_instance_set(:#{belongs_to_reflection.name}, nil);" - end - end.compact.join - - if method_defined?(:"#{reflection.primary_key_name}=") - undef_method :"#{reflection.primary_key_name}=" - end - - class_eval <<-FILE, __FILE__, __LINE__ + 1 - def #{reflection.primary_key_name}=(new_id) - write_attribute :#{reflection.primary_key_name}, new_id - if #{reflection.primary_key_name}_changed? - #{ setters } - end - end - FILE - end - - def association_foreign_type_setter_method(reflection) - setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection| - if belongs_to_reflection.options[:foreign_type] == reflection.options[:foreign_type] - "association_instance_set(:#{belongs_to_reflection.name}, nil);" - end - end.compact.join - - field = reflection.options[:foreign_type] - class_eval <<-FILE, __FILE__, __LINE__ + 1 - def #{field}=(new_id) - write_attribute :#{field}, new_id - if #{field}_changed? - #{ setters } - end - end - FILE - end - def add_counter_cache_callbacks(reflection) cache_column = reflection.counter_cache_column diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 252ff7e7ea..f4eceeed8c 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -130,6 +130,16 @@ module ActiveRecord @loaded = true end + # The target is stale if the target no longer points to the record(s) that the + # relevant foreign_key(s) refers to. If stale, the association accessor method + # on the owner will reload the target. It's up to subclasses to implement this + # method if relevant. + # + # Note that if the target has not been loaded, it is not considered stale. + def stale_target? + false + end + # Returns the target of this proxy, same as +proxy_target+. def target @target diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index b438620c8f..c9abfe36e8 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -42,6 +42,14 @@ module ActiveRecord @updated end + def stale_target? + if @target && @target.persisted? + @target.send(@reflection.association_primary_key).to_i != @owner.send(@reflection.primary_key_name).to_i + else + false + end + end + private def find_target find_method = if @reflection.options[:primary_key] diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index a0df860623..844ae94c3d 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -23,6 +23,15 @@ module ActiveRecord @updated end + def stale_target? + if @target && @target.persisted? + @target.send(@reflection.association_primary_key).to_i != @owner.send(@reflection.primary_key_name).to_i || + @target.class.base_class.name.to_s != @owner.send(@reflection.options[:foreign_type]).to_s + else + false + end + end + private # NOTE - for now, we're only supporting inverse setting from belongs_to back onto diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 7a7f7812df..87a842bc6b 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -209,7 +209,10 @@ module ActiveRecord end def association_primary_key - @association_primary_key ||= options[:primary_key] || klass.primary_key + @association_primary_key ||= + options[:primary_key] || + !options[:polymorphic] && klass.primary_key || + 'id' end def active_record_primary_key diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index ffcc7a081a..7d9b1104cd 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -298,7 +298,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true @ship.delete - + @pirate.reload.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' }) assert_not_nil @pirate.ship @@ -411,6 +411,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id @pirate.stubs(:id).returns('ABC1X') + @ship.stubs(:pirate_id).returns(@pirate.id) # prevents pirate from being reloaded due to non-matching foreign key @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' } assert_equal 'Arr', @ship.pirate.catchphrase @@ -451,7 +452,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_not_destroy_the_associated_model_until_the_parent_is_saved pirate = @ship.pirate - + @ship.attributes = { :pirate_attributes => { :id => pirate.id, '_destroy' => true } } assert_nothing_raised(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) } @ship.save diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 1e205714a7..901c12b26c 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -7,6 +7,7 @@ require 'models/subscriber' require 'models/ship' require 'models/pirate' require 'models/price_estimate' +require 'models/tagging' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -194,6 +195,7 @@ class ReflectionTest < ActiveRecord::TestCase def test_association_primary_key assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s + assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s end def test_active_record_primary_key -- cgit v1.2.3 From 1c07b84df95e932d50376c1d0a13585b2e2ef868 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 13:36:25 +0000 Subject: If a has_many goes :through a belongs_to, and the foreign key of the belongs_to changes, then the has_many should be considered stale. --- activerecord/lib/active_record/associations.rb | 6 ++- .../associations/belongs_to_association.rb | 5 +- .../belongs_to_polymorphic_association.rb | 8 ++- .../associations/has_many_through_association.rb | 1 + .../associations/has_one_through_association.rb | 1 + .../associations/through_association_scope.rb | 19 +++++++ .../associations/belongs_to_associations_test.rb | 60 +++++++++++++--------- .../has_many_through_associations_test.rb | 16 ++++++ .../has_one_through_associations_test.rb | 15 ++++++ activerecord/test/fixtures/dashboards.yml | 5 +- activerecord/test/fixtures/members.yml | 2 + activerecord/test/fixtures/memberships.yml | 6 +-- activerecord/test/fixtures/speedometers.yml | 6 ++- activerecord/test/fixtures/sponsors.yml | 9 ++-- 14 files changed, 124 insertions(+), 35 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index c6c41a7a12..0aea05b348 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1501,7 +1501,11 @@ module ActiveRecord association_instance_set(reflection.name, association) end - reflection.klass.uncached { association.reload } if force_reload + if force_reload + reflection.klass.uncached { association.reload } + elsif association.stale_target? + association.reload + end association end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index c9abfe36e8..bbfe18f9fb 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -44,7 +44,10 @@ module ActiveRecord def stale_target? if @target && @target.persisted? - @target.send(@reflection.association_primary_key).to_i != @owner.send(@reflection.primary_key_name).to_i + target_id = @target.send(@reflection.association_primary_key).to_s + foreign_key = @owner.send(@reflection.primary_key_name).to_s + + target_id != foreign_key else false end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 844ae94c3d..c580de7fbe 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -25,8 +25,12 @@ module ActiveRecord def stale_target? if @target && @target.persisted? - @target.send(@reflection.association_primary_key).to_i != @owner.send(@reflection.primary_key_name).to_i || - @target.class.base_class.name.to_s != @owner.send(@reflection.options[:foreign_type]).to_s + target_id = @target.send(@reflection.association_primary_key).to_s + foreign_key = @owner.send(@reflection.primary_key_name).to_s + target_type = @target.class.base_class.name + foreign_type = @owner.send(@reflection.options[:foreign_type]).to_s + + target_id != foreign_key || target_type != foreign_type else false end 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 f0bc6aedf2..5f4667b4d8 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -59,6 +59,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? + update_stale_state scoped.all end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index e8cf73976b..eb17935d6a 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -33,6 +33,7 @@ module ActiveRecord private def find_target + update_stale_state scoped.first end end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index c11fce5db0..e57de84f66 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -10,6 +10,17 @@ module ActiveRecord end end + def stale_target? + if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key) + previous_key = @through_foreign_key.to_s + current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s + + previous_key != current_key + else + false + end + end + protected def construct_find_scope @@ -165,6 +176,14 @@ module ActiveRecord end alias_method :sql_conditions, :conditions + + def update_stale_state + construct_scope if stale_target? + + if @reflection.through_reflection.macro == :belongs_to + @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name) + end + end end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 1820f95261..cdde9a80d3 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -17,7 +17,7 @@ require 'models/essay' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, :developers_projects, :computers, :authors, :author_addresses, - :posts, :tags, :taggings, :comments + :posts, :tags, :taggings, :comments, :sponsors, :members def test_belongs_to Client.find(3).firm.name @@ -488,39 +488,53 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_reassigning_the_parent_id_updates_the_object - original_parent = Firm.create! :name => "original" - updated_parent = Firm.create! :name => "updated" + client = companies(:second_client) - client = Client.new("client_of" => original_parent.id) - assert_equal original_parent, client.firm - assert_equal original_parent, client.firm_with_condition - assert_equal original_parent, client.firm_with_other_name + client.firm + client.firm_with_condition + firm_proxy = client.send(:association_instance_get, :firm) + firm_with_condition_proxy = client.send(:association_instance_get, :firm_with_condition) - client.client_of = updated_parent.id - assert_equal updated_parent, client.firm - assert_equal updated_parent, client.firm_with_condition - assert_equal updated_parent, client.firm_with_other_name + assert !firm_proxy.stale_target? + assert !firm_with_condition_proxy.stale_target? + assert_equal companies(:first_firm), client.firm + assert_equal companies(:first_firm), client.firm_with_condition + + client.client_of = companies(:another_firm).id + + assert firm_proxy.stale_target? + assert firm_with_condition_proxy.stale_target? + assert_equal companies(:another_firm), client.firm + assert_equal companies(:another_firm), client.firm_with_condition end def test_polymorphic_reassignment_of_associated_id_updates_the_object - member1 = Member.create! - member2 = Member.create! + sponsor = sponsors(:moustache_club_sponsor_for_groucho) + + sponsor.sponsorable + proxy = sponsor.send(:association_instance_get, :sponsorable) + + assert !proxy.stale_target? + assert_equal members(:groucho), sponsor.sponsorable - sponsor = Sponsor.new("sponsorable_type" => "Member", "sponsorable_id" => member1.id) - assert_equal member1, sponsor.sponsorable + sponsor.sponsorable_id = members(:some_other_guy).id - sponsor.sponsorable_id = member2.id - assert_equal member2, sponsor.sponsorable + assert proxy.stale_target? + assert_equal members(:some_other_guy), sponsor.sponsorable end def test_polymorphic_reassignment_of_associated_type_updates_the_object - member1 = Member.create! + sponsor = sponsors(:moustache_club_sponsor_for_groucho) - sponsor = Sponsor.new("sponsorable_type" => "Member", "sponsorable_id" => member1.id) - assert_equal member1, sponsor.sponsorable + sponsor.sponsorable + proxy = sponsor.send(:association_instance_get, :sponsorable) - sponsor.sponsorable_type = "Firm" - assert_not_equal member1, sponsor.sponsorable - end + assert !proxy.stale_target? + assert_equal members(:groucho), sponsor.sponsorable + + sponsor.sponsorable_type = 'Firm' + assert proxy.stale_target? + assert_equal companies(:first_firm), sponsor.sponsorable + end end 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 a417345780..e98d178ff5 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -19,6 +19,7 @@ require 'models/book' require 'models/subscription' require 'models/categorization' require 'models/category' +require 'models/essay' class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, @@ -534,4 +535,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 2, authors(:mary).categories.count assert_equal 1, authors(:mary).categories.general.count end + + def test_has_many_through_belongs_to_should_update_when_the_through_foreign_key_changes + post = posts(:eager_other) + + post.author_categorizations + proxy = post.send(:association_instance_get, :author_categorizations) + + assert !proxy.stale_target? + assert_equal authors(:mary).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id) + + post.author_id = authors(:david).id + + assert proxy.stale_target? + assert_equal authors(:david).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id) + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index f6365e0216..7d55d909a7 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -251,4 +251,19 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase members(:groucho).club_through_many end end + + def test_has_one_through_belongs_to_should_update_when_the_through_foreign_key_changes + minivan = minivans(:cool_first) + + minivan.dashboard + proxy = minivan.send(:association_instance_get, :dashboard) + + assert !proxy.stale_target? + assert_equal dashboards(:cool_first), minivan.dashboard + + minivan.speedometer_id = speedometers(:second).id + + assert proxy.stale_target? + assert_equal dashboards(:second), minivan.dashboard + end end diff --git a/activerecord/test/fixtures/dashboards.yml b/activerecord/test/fixtures/dashboards.yml index e75bf46e6c..a4c7e0d309 100644 --- a/activerecord/test/fixtures/dashboards.yml +++ b/activerecord/test/fixtures/dashboards.yml @@ -1,3 +1,6 @@ cool_first: dashboard_id: d1 - name: my_dashboard \ No newline at end of file + name: my_dashboard +second: + dashboard_id: d2 + name: second diff --git a/activerecord/test/fixtures/members.yml b/activerecord/test/fixtures/members.yml index 6db945e61d..824840b7e5 100644 --- a/activerecord/test/fixtures/members.yml +++ b/activerecord/test/fixtures/members.yml @@ -1,6 +1,8 @@ groucho: + id: 1 name: Groucho Marx member_type_id: 1 some_other_guy: + id: 2 name: Englebert Humperdink member_type_id: 2 diff --git a/activerecord/test/fixtures/memberships.yml b/activerecord/test/fixtures/memberships.yml index b9722dbc8a..eed8b22af8 100644 --- a/activerecord/test/fixtures/memberships.yml +++ b/activerecord/test/fixtures/memberships.yml @@ -1,20 +1,20 @@ membership_of_boring_club: joined_on: <%= 3.weeks.ago.to_s(:db) %> club: boring_club - member: groucho + member_id: 1 favourite: false type: CurrentMembership membership_of_favourite_club: joined_on: <%= 3.weeks.ago.to_s(:db) %> club: moustache_club - member: groucho + member_id: 1 favourite: true type: Membership other_guys_membership: joined_on: <%= 4.weeks.ago.to_s(:db) %> club: boring_club - member: some_other_guy + member_id: 2 favourite: false type: CurrentMembership diff --git a/activerecord/test/fixtures/speedometers.yml b/activerecord/test/fixtures/speedometers.yml index 6a471aba0a..e12398f0c4 100644 --- a/activerecord/test/fixtures/speedometers.yml +++ b/activerecord/test/fixtures/speedometers.yml @@ -1,4 +1,8 @@ cool_first: speedometer_id: s1 name: my_speedometer - dashboard_id: d1 \ No newline at end of file + dashboard_id: d1 +second: + speedometer_id: s2 + name: second + dashboard_id: d2 diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml index 42df8957d1..bfc6b238b1 100644 --- a/activerecord/test/fixtures/sponsors.yml +++ b/activerecord/test/fixtures/sponsors.yml @@ -1,9 +1,12 @@ moustache_club_sponsor_for_groucho: sponsor_club: moustache_club - sponsorable: groucho (Member) + sponsorable_id: 1 + sponsorable_type: Member boring_club_sponsor_for_groucho: sponsor_club: boring_club - sponsorable: some_other_guy (Member) + sponsorable_id: 2 + sponsorable_type: Member crazy_club_sponsor_for_groucho: sponsor_club: crazy_club - sponsorable: some_other_guy (Member) \ No newline at end of file + sponsorable_id: 2 + sponsorable_type: Member -- cgit v1.2.3 From fb3a8c51b4028e8d122fdbb783d73d0ed37ca168 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 14:19:08 +0000 Subject: Raise an error for associations which try to go :through a polymorphic association [#6212 state:resolved] --- activerecord/lib/active_record/associations.rb | 8 +++++++- activerecord/lib/active_record/reflection.rb | 6 +++++- activerecord/test/cases/associations/join_model_test.rb | 11 ++++++++--- activerecord/test/models/tagging.rb | 3 ++- 4 files changed, 22 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 0aea05b348..1056c51a3d 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -20,12 +20,18 @@ module ActiveRecord end end - class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc: + class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc: def initialize(owner_class_name, reflection, source_reflection) super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.") end end + class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection) + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + end + end + class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc: def initialize(owner_class_name, reflection, source_reflection) super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.") diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 87a842bc6b..0310e7050d 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -375,6 +375,10 @@ module ActiveRecord raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) end + if through_reflection.options[:polymorphic] + raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) + end + if source_reflection.nil? raise HasManyThroughSourceAssociationNotFoundError.new(self) end @@ -384,7 +388,7 @@ module ActiveRecord end if source_reflection.options[:polymorphic] && options[:source_type].nil? - raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection) + raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection) end unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil? diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 58542bc939..263c90097f 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -340,11 +340,16 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_polymorphic - assert_raise ActiveRecord::HasManyThroughAssociationPolymorphicError do - assert_equal posts(:welcome, :thinking), tags(:general).taggables + assert_raise ActiveRecord::HasManyThroughAssociationPolymorphicSourceError do + tags(:general).taggables end + + assert_raise ActiveRecord::HasManyThroughAssociationPolymorphicThroughError do + taggings(:welcome_general).things + end + assert_raise ActiveRecord::EagerLoadPolymorphicError do - assert_equal posts(:welcome, :thinking), tags(:general).taggings.find(:all, :include => :taggable, :conditions => 'bogus_table.column = 1') + tags(:general).taggings.find(:all, :include => :taggable, :conditions => 'bogus_table.column = 1') end end diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index a1fa1a9750..33ffc623d7 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -7,4 +7,5 @@ class Tagging < ActiveRecord::Base belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id' belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' belongs_to :taggable, :polymorphic => true, :counter_cache => true -end \ No newline at end of file + has_many :things, :through => :taggable +end -- cgit v1.2.3 From 6974c595fd480dc0ae3311ef60920fa87c5ff9d0 Mon Sep 17 00:00:00 2001 From: oleg dashevskii Date: Thu, 26 Aug 2010 11:22:27 +0700 Subject: Verify that there is no unwanted implicit readonly set on Model.has_many_through.find(id) [#5442 state:resolved] --- activerecord/test/cases/readonly_test.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 98011f40a4..a1eb96ef09 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -12,7 +12,7 @@ def Project.foo() find :first end class ReadOnlyTest < ActiveRecord::TestCase - fixtures :posts, :comments, :developers, :projects, :developers_projects + fixtures :posts, :comments, :developers, :projects, :developers_projects, :people, :readers def test_cant_save_readonly_record dev = Developer.find(1) @@ -71,6 +71,18 @@ class ReadOnlyTest < ActiveRecord::TestCase assert !people.any?(&:readonly?) end + def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_by_id + assert !posts(:welcome).people.find(1).readonly? + end + + def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_first + assert !posts(:welcome).people.first.readonly? + end + + def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_last + assert !posts(:welcome).people.last.readonly? + end + def test_readonly_scoping Post.send(:with_scope, :find => { :conditions => '1=1' }) do assert !Post.find(1).readonly? -- cgit v1.2.3 From 34d79fad85b0388270300a91430211ddc82cb183 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 24 Dec 2010 15:48:46 -0700 Subject: setting the primary key on the update manager --- activerecord/lib/active_record/relation.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index d4c5c85594..9a882ba01a 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -169,6 +169,7 @@ module ActiveRecord # Apply limit and order only if they're both present if @limit_value.present? == @order_values.present? stmt = arel.compile_update(Arel::SqlLiteral.new(@klass.send(:sanitize_sql_for_assignment, updates))) + stmt.key = @klass.arel_table[@klass.primary_key] @klass.connection.update stmt.to_sql else except(:limit, :order).update_all(updates) -- cgit v1.2.3 From 0fbf829b1e147c6c0f6c9d5e447bad0e9216b7b1 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 24 Dec 2010 15:58:47 -0700 Subject: stop the recursive insanity --- activerecord/lib/active_record/relation.rb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 9a882ba01a..dd73972416 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -166,14 +166,19 @@ module ActiveRecord if conditions || options.present? where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) else + limit = nil + order = [] # Apply limit and order only if they're both present if @limit_value.present? == @order_values.present? - stmt = arel.compile_update(Arel::SqlLiteral.new(@klass.send(:sanitize_sql_for_assignment, updates))) - stmt.key = @klass.arel_table[@klass.primary_key] - @klass.connection.update stmt.to_sql - else - except(:limit, :order).update_all(updates) + limit = arel.limit + order = arel.orders end + + stmt = arel.compile_update(Arel::SqlLiteral.new(@klass.send(:sanitize_sql_for_assignment, updates))) + stmt.take limit + stmt.order(*order) + stmt.key = @klass.arel_table[@klass.primary_key] + @klass.connection.update stmt.to_sql end end -- cgit v1.2.3 From 23b03baba611b0ef664eec9e9384c14099eb73e9 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 24 Dec 2010 16:01:07 -0700 Subject: use the sql literal factory method --- activerecord/lib/active_record/relation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index dd73972416..20e983b5f7 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -174,7 +174,7 @@ module ActiveRecord order = arel.orders end - stmt = arel.compile_update(Arel::SqlLiteral.new(@klass.send(:sanitize_sql_for_assignment, updates))) + stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))) stmt.take limit stmt.order(*order) stmt.key = @klass.arel_table[@klass.primary_key] -- cgit v1.2.3 From ec13305b21d7146417b17a2cdf976bbc5cac2189 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 24 Dec 2010 22:15:41 -0700 Subject: stop redifining methods on every call to set_primary_key --- .../lib/active_record/attribute_methods/primary_key.rb | 15 +++++++++++---- activerecord/test/cases/base_test.rb | 12 ++++++++---- activerecord/test/cases/inheritance_test.rb | 4 ++++ 3 files changed, 23 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index b891b2c50e..978cd7fbe3 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -14,11 +14,13 @@ module ActiveRecord # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the # primary_key_prefix_type setting, though. def primary_key - reset_primary_key + @primary_key ||= reset_primary_key end def reset_primary_key #:nodoc: - key = get_primary_key(base_class.name) + key = self == base_class ? get_primary_key(base_class.name) : + base_class.primary_key + set_primary_key(key) key end @@ -40,6 +42,9 @@ module ActiveRecord end end + attr_accessor :original_primary_key + attr_writer :primary_key + # Sets the name of the primary key column to use to the given value, # or (if the value is nil or false) to the value returned by the given # block. @@ -48,9 +53,11 @@ module ActiveRecord # set_primary_key "sysid" # end def set_primary_key(value = nil, &block) - define_attr_method :primary_key, value, &block + @primary_key ||= '' + self.original_primary_key = @primary_key + value &&= value.to_s + self.primary_key = block_given? ? instance_eval(&block) : value end - alias :primary_key= :set_primary_key end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index e186b2aea7..09ef04a656 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1073,10 +1073,14 @@ class BasicsTest < ActiveRecord::TestCase end def test_define_attr_method_with_block - k = Class.new( ActiveRecord::Base ) - k.primary_key = "id" - k.send(:define_attr_method, :primary_key) { "sys_" + original_primary_key } - assert_equal "sys_id", k.primary_key + k = Class.new( ActiveRecord::Base ) do + class << self + attr_accessor :foo_key + end + end + k.foo_key = "id" + k.send(:define_attr_method, :foo_key) { "sys_" + original_foo_key } + assert_equal "sys_id", k.foo_key end def test_set_table_name_with_value diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index c3da9cdf53..6abbe45492 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -219,6 +219,10 @@ class InheritanceTest < ActiveRecord::TestCase switch_to_default_inheritance_column end + def test_inherits_custom_primary_key + assert_equal Subscriber.primary_key, SpecialSubscriber.primary_key + end + def test_inheritance_without_mapping assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132") assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save } -- cgit v1.2.3 From f855090a78b99fc9e1414a1fab21473ef10bc00c Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 25 Dec 2010 14:31:22 -0700 Subject: use arel to compile SQL statements --- .../lib/active_record/association_preload.rb | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 6d905fe6fe..7dfb142bb8 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -193,14 +193,15 @@ module ActiveRecord records.each {|record| record.send(reflection.name).loaded} options = reflection.options - conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}" - conditions << append_conditions(reflection, preload_options) - right = Arel::Table.new(options[:join_table]).alias('t0') - condition = left[reflection.klass.primary_key].eq( + + + custom_conditions = append_conditions(reflection, preload_options) + + join_condition = left[reflection.klass.primary_key].eq( right[reflection.association_foreign_key]) - join = left.create_join(right, left.create_on(condition)) + join = left.create_join(right, left.create_on(join_condition)) select = [ # FIXME: options[:select] is always nil in the tests. Do we really # need it? @@ -216,8 +217,16 @@ module ActiveRecord associated_records_proxy.joins_values = [join] associated_records_proxy.select_values = select + method = ids.length > 1 ? 'in' : 'eq' + all_associated_records = associated_records(ids) do |some_ids| - associated_records_proxy.where([conditions, some_ids]).to_a + + conditions = right[reflection.primary_key_name].send( + method, some_ids.length == 1 ? some_ids.first : some_ids + ) + conditions = conditions.and(custom_conditions) unless custom_conditions.empty? + + associated_records_proxy.where(conditions).to_a end set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id') -- cgit v1.2.3 From 3fe9951fcce97edbc7d1443f165a0b2cb82de1ef Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 25 Dec 2010 14:34:13 -0700 Subject: refactoring AST building --- activerecord/lib/active_record/association_preload.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 7dfb142bb8..9c79e6225a 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -217,13 +217,11 @@ module ActiveRecord associated_records_proxy.joins_values = [join] associated_records_proxy.select_values = select - method = ids.length > 1 ? 'in' : 'eq' - all_associated_records = associated_records(ids) do |some_ids| + method = some_ids.length == 1 ? ['eq', some_ids.first] : + ['in', some_ids] - conditions = right[reflection.primary_key_name].send( - method, some_ids.length == 1 ? some_ids.first : some_ids - ) + conditions = right[reflection.primary_key_name].send(*method) conditions = conditions.and(custom_conditions) unless custom_conditions.empty? associated_records_proxy.where(conditions).to_a -- cgit v1.2.3 From a6fe244e9b05c60aac88842a38fb2b6285d5571b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 25 Dec 2010 15:23:45 -0700 Subject: take more advantage of arel sql compiler --- .../lib/active_record/association_preload.rb | 32 ++++++++++++++-------- activerecord/test/cases/inheritance_test.rb | 2 +- 2 files changed, 21 insertions(+), 13 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 9c79e6225a..11c2084b5b 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -196,8 +196,6 @@ module ActiveRecord right = Arel::Table.new(options[:join_table]).alias('t0') - custom_conditions = append_conditions(reflection, preload_options) - join_condition = left[reflection.klass.primary_key].eq( right[reflection.association_foreign_key]) @@ -217,12 +215,16 @@ module ActiveRecord associated_records_proxy.joins_values = [join] associated_records_proxy.select_values = select + custom_conditions = append_conditions(reflection, preload_options) + all_associated_records = associated_records(ids) do |some_ids| method = some_ids.length == 1 ? ['eq', some_ids.first] : ['in', some_ids] conditions = right[reflection.primary_key_name].send(*method) - conditions = conditions.and(custom_conditions) unless custom_conditions.empty? + conditions = custom_conditions.inject(conditions) do |ast, cond| + ast.and cond + end associated_records_proxy.where(conditions).to_a end @@ -348,7 +350,7 @@ module ActiveRecord klasses_and_ids.each do |klass_name, _id_map| klass = klass_name.constantize - table_name = klass.quoted_table_name + table = klass.arel_table primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s column_type = klass.columns.detect{|c| c.name == primary_key}.type @@ -362,10 +364,15 @@ module ActiveRecord end end - conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}" - conditions << append_conditions(reflection, preload_options) + method = ids.length == 1 ? ['eq', ids.first] : ['in', ids] + conditions = table[primary_key].send(*method) + + custom_conditions = append_conditions(reflection, preload_options) + conditions = custom_conditions.inject(conditions) do |ast, cond| + ast.and cond + end - associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a + associated_records = klass.unscoped.where(conditions).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a set_association_single_records(_id_map, reflection.name, associated_records, primary_key) end @@ -382,7 +389,8 @@ module ActiveRecord conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}" end - conditions << append_conditions(reflection, preload_options) + conditions = ([conditions] + + append_conditions(reflection, preload_options)).join(' AND ') find_options = { :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"), @@ -398,10 +406,10 @@ module ActiveRecord end def append_conditions(reflection, preload_options) - sql = "" - sql << " AND (#{reflection.sanitized_conditions})" if reflection.sanitized_conditions - sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions] - sql + [ + ("(#{reflection.sanitized_conditions})" if reflection.sanitized_conditions), + ("(#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]), + ].compact.map { |x| Arel.sql x } end def in_or_equals_for_ids(ids) diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 6abbe45492..9fac9373eb 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -208,7 +208,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/\(#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1\)/) do + assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do Account.find(1, :include => :firm) end end -- cgit v1.2.3 From 0a609eea504f72baead7548d47f0fe707314a033 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 25 Dec 2010 15:25:11 -0700 Subject: use sql literal factory method --- activerecord/lib/active_record/association_preload.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 11c2084b5b..83383e9b19 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -393,7 +393,7 @@ module ActiveRecord append_conditions(reflection, preload_options)).join(' AND ') find_options = { - :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"), + :select => preload_options[:select] || options[:select] || Arel.sql("#{table_name}.*"), :include => preload_options[:include] || options[:include], :joins => options[:joins], :group => preload_options[:group] || options[:group], -- cgit v1.2.3 From 5b918bb97cb1801945ef778508a3738da98012c5 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 25 Dec 2010 16:19:59 -0700 Subject: using arel to compile sql statements --- .../lib/active_record/association_preload.rb | 25 +++++++++++++++------- .../associations/has_one_association.rb | 3 ++- activerecord/test/cases/finder_test.rb | 2 +- activerecord/test/fixtures/companies.yml | 1 + 4 files changed, 21 insertions(+), 10 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 83383e9b19..7b4ff69b87 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -379,18 +379,20 @@ module ActiveRecord end def find_associated_records(ids, reflection, preload_options) - options = reflection.options + options = reflection.options + table = reflection.klass.arel_table table_name = reflection.klass.quoted_table_name + conditions = [] + + key = reflection.primary_key_name + if interface = reflection.options[:as] - conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" - else - foreign_key = reflection.primary_key_name - conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}" + key = "#{interface}_id" + conditions << table["#{interface}_type"].eq(base_class.sti_name) end - conditions = ([conditions] + - append_conditions(reflection, preload_options)).join(' AND ') + conditions += append_conditions(reflection, preload_options) find_options = { :select => preload_options[:select] || options[:select] || Arel.sql("#{table_name}.*"), @@ -401,7 +403,14 @@ module ActiveRecord } associated_records(ids) do |some_ids| - reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => [conditions, some_ids])).to_a + method = some_ids.length == 1 ? ['eq', some_ids.first] : + ['in', some_ids] + + where = conditions.inject(table[key].send(*method)) do |ast, cond| + ast.and cond + end + + reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => where)).to_a end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index c49fd6e66a..d08cbea199 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -89,7 +89,8 @@ module ActiveRecord "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" else - sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" + test = owner_quoted_id == "NULL" ? "IS" : "=" + sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} #{test} #{owner_quoted_id}" end sql << " AND (#{conditions})" if conditions { :conditions => sql } diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 31e4981a1d..c35590b84b 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -982,7 +982,7 @@ class FinderTest < ActiveRecord::TestCase def test_select_rows assert_equal( - [["1", nil, nil, "37signals"], + [["1", "1", nil, "37signals"], ["2", "1", "2", "Summit"], ["3", "1", "1", "Microsoft"]], Company.connection.select_rows("SELECT id, firm_id, client_of, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! {|i| i.map! {|j| j.to_s unless j.nil?}}) diff --git a/activerecord/test/fixtures/companies.yml b/activerecord/test/fixtures/companies.yml index ffaa097686..30b73d8e84 100644 --- a/activerecord/test/fixtures/companies.yml +++ b/activerecord/test/fixtures/companies.yml @@ -9,6 +9,7 @@ first_client: first_firm: id: 1 + firm_id: 1 type: Firm name: 37signals ruby_type: Firm -- cgit v1.2.3 From 33b5a2637fbed8b2e5a10b84b79b32245edfb411 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 25 Dec 2010 16:36:07 -0700 Subject: refactoring method selection --- activerecord/lib/active_record/association_preload.rb | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 7b4ff69b87..5feda5db0c 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -188,7 +188,6 @@ module ActiveRecord left = reflection.klass.arel_table - table_name = reflection.klass.quoted_table_name id_to_record_map, ids = construct_id_map(records) records.each {|record| record.send(reflection.name).loaded} options = reflection.options @@ -218,9 +217,7 @@ module ActiveRecord custom_conditions = append_conditions(reflection, preload_options) all_associated_records = associated_records(ids) do |some_ids| - method = some_ids.length == 1 ? ['eq', some_ids.first] : - ['in', some_ids] - + method = in_or_equal(some_ids) conditions = right[reflection.primary_key_name].send(*method) conditions = custom_conditions.inject(conditions) do |ast, cond| ast.and cond @@ -364,7 +361,7 @@ module ActiveRecord end end - method = ids.length == 1 ? ['eq', ids.first] : ['in', ids] + method = in_or_equal(ids) conditions = table[primary_key].send(*method) custom_conditions = append_conditions(reflection, preload_options) @@ -403,9 +400,7 @@ module ActiveRecord } associated_records(ids) do |some_ids| - method = some_ids.length == 1 ? ['eq', some_ids.first] : - ['in', some_ids] - + method = in_or_equal(some_ids) where = conditions.inject(table[key].send(*method)) do |ast, cond| ast.and cond end @@ -421,8 +416,8 @@ module ActiveRecord ].compact.map { |x| Arel.sql x } end - def in_or_equals_for_ids(ids) - ids.size > 1 ? "IN (?)" : "= ?" + def in_or_equal(ids) + ids.length == 1 ? ['eq', ids.first] : ['in', ids] end # Some databases impose a limit on the number of ids in a list (in Oracle its 1000) -- cgit v1.2.3 From 75ac9c4271df65b94b2a6862d87b1ec42f676efe Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 25 Dec 2010 16:38:59 -0700 Subject: use arel to determine selection column --- activerecord/lib/active_record/association_preload.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 5feda5db0c..3409ca9099 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -378,7 +378,6 @@ module ActiveRecord def find_associated_records(ids, reflection, preload_options) options = reflection.options table = reflection.klass.arel_table - table_name = reflection.klass.quoted_table_name conditions = [] @@ -392,7 +391,7 @@ module ActiveRecord conditions += append_conditions(reflection, preload_options) find_options = { - :select => preload_options[:select] || options[:select] || Arel.sql("#{table_name}.*"), + :select => preload_options[:select] || options[:select] || table[Arel.star], :include => preload_options[:include] || options[:include], :joins => options[:joins], :group => preload_options[:group] || options[:group], -- cgit v1.2.3 From bde643fbec7d1b80309d3387635f4a2e9dadccc2 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 25 Dec 2010 16:42:49 -0700 Subject: arel will deal with casting the ids, so we can delete this --- activerecord/lib/active_record/association_preload.rb | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 3409ca9099..a305551093 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -349,19 +349,7 @@ module ActiveRecord table = klass.arel_table primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s - column_type = klass.columns.detect{|c| c.name == primary_key}.type - - ids = _id_map.keys.map do |id| - if column_type == :integer - id.to_i - elsif column_type == :float - id.to_f - else - id - end - end - - method = in_or_equal(ids) + method = in_or_equal(_id_map.keys) conditions = table[primary_key].send(*method) custom_conditions = append_conditions(reflection, preload_options) -- cgit v1.2.3 From d767252d72063af053fdf1391461704a1a5e1f4a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 25 Dec 2010 16:47:59 -0700 Subject: refactor to use group_by --- activerecord/lib/active_record/association_preload.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index a305551093..ba225b700c 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -336,11 +336,11 @@ module ActiveRecord end end else - id_map = {} - records.each do |record| + id_map = records.group_by do |record| key = record.send(primary_key_name) - (id_map[key.to_s] ||= []) << record if key + key && key.to_s end + id_map.delete nil klasses_and_ids[reflection.klass.name] = id_map unless id_map.empty? end -- cgit v1.2.3 From c6e0433ca3520e9bc999f70840d82a76b9470873 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 17:33:42 +0000 Subject: scoped.where_values_hash is never nil --- activerecord/lib/active_record/associations/association_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 7964f4fa2b..8bb32400cf 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -488,7 +488,7 @@ module ActiveRecord ensure_owner_is_persisted! transaction do - with_scope(:create => @scope[:create].merge(scoped.where_values_hash || {})) do + with_scope(:create => @scope[:create].merge(scoped.where_values_hash)) do build_record(attrs, &block) end end -- cgit v1.2.3 From b5c30f9dacd73715830bbe53a45e98a7d2007d19 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 17:38:13 +0000 Subject: Remove target_obsolete? which is not called from anywhere --- activerecord/lib/active_record/associations/has_many_association.rb | 4 ---- 1 file changed, 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 4b05cf6e81..2d2f377e1f 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -79,10 +79,6 @@ module ActiveRecord end end - def target_obsolete? - false - end - def construct_conditions if @reflection.options[:as] sql = -- cgit v1.2.3 From 3eef0977e15d74518673e0bb3a9305cb41682dac Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 19:53:52 +0000 Subject: Use the through association proxy for operations on the through record, so that those operations are automatically scoped and therefore construct_join_attributes does not need to use construct_owner_attributes. --- .../associations/has_many_through_association.rb | 4 ++-- .../associations/has_one_through_association.rb | 24 ++++++++++++---------- .../associations/through_association_scope.rb | 6 ++---- 3 files changed, 17 insertions(+), 17 deletions(-) (limited to 'activerecord') 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 5f4667b4d8..358ba2754b 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -51,9 +51,9 @@ module ActiveRecord # TODO - add dependent option support def delete_records(records) - klass = @reflection.through_reflection.klass + through_association = @owner.send(@reflection.through_reflection.name) records.each do |associate| - klass.delete_all(construct_join_attributes(associate)) + through_association.where(construct_join_attributes(associate)).delete_all end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index eb17935d6a..2558941476 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -13,20 +13,22 @@ module ActiveRecord private - def create_through_record(new_value) #nodoc: - klass = @reflection.through_reflection.klass + def create_through_record(new_value) + proxy = @owner.send(@reflection.through_reflection.name) || + @owner.send(:association_instance_get, @reflection.through_reflection.name) + record = proxy.target - current_object = @owner.send(@reflection.through_reflection.name) - - if current_object - new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy + if record && !new_value + record.destroy elsif new_value - if @owner.new_record? - self.target = new_value - through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name) - through_association.build(construct_join_attributes(new_value)) + attributes = construct_join_attributes(new_value) + + if record + record.update_attributes(attributes) + elsif @owner.new_record? + proxy.build(attributes) else - @owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value))) + proxy.create(attributes) end end end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index e57de84f66..7cb039859c 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -117,12 +117,10 @@ module ActiveRecord # TODO: revisit this to allow it for deletion, supposing dependent option is supported raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) - join_attributes = construct_owner_attributes(@reflection.through_reflection) - - join_attributes.merge!( + join_attributes = { @reflection.source_reflection.primary_key_name => associate.send(@reflection.source_reflection.association_primary_key) - ) + } if @reflection.options[:source_type] join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name) -- cgit v1.2.3 From 99db97a32237876f65468aa116441a621b80cc01 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 19:54:43 +0000 Subject: Remove pointless use of 'private' --- .../lib/active_record/associations/has_one_through_association.rb | 1 - 1 file changed, 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 2558941476..8c8c9fbe5d 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -33,7 +33,6 @@ module ActiveRecord end end - private def find_target update_stale_state scoped.first -- cgit v1.2.3 From 739ea1fbfe704cdb25e9c2a7f0911ade7431e7f4 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 20:00:54 +0000 Subject: Remove has_cached_counter? and cached_counter_attribute_name from HasManyThroughAssociation, as the exact same methods are inherited from HasManyAssociation --- .../active_record/associations/has_many_through_association.rb | 8 -------- 1 file changed, 8 deletions(-) (limited to 'activerecord') 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 358ba2754b..4a3f53d70f 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -63,14 +63,6 @@ module ActiveRecord scoped.all end - def has_cached_counter? - @owner.attribute_present?(cached_counter_attribute_name) - end - - def cached_counter_attribute_name - "#{@reflection.name}_count" - end - # NOTE - not sure that we can actually cope with inverses here def we_can_set_the_inverse_on_this?(record) false -- cgit v1.2.3 From ac67eee4e65082f4312976bd89bbec6f71025aa1 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 20:05:34 +0000 Subject: Use conditionals and implicit returns rather than explicit returns and postfix ifs (it's easier to read) --- .../active_record/associations/has_many_through_association.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'activerecord') 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 4a3f53d70f..4dabfe2ea3 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -21,9 +21,13 @@ module ActiveRecord # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer # SELECT query if you use #length. def size - return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter? - return @target.size if loaded? - return count + if has_cached_counter? + @owner.send(:read_attribute, cached_counter_attribute_name) + elsif loaded? + @target.size + else + count + end end protected -- cgit v1.2.3 From 93861d19e3571206baf51af710cb3f2bd2c65bde Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 20:41:20 +0000 Subject: Set the create scope to an empty hash in ThroughAssociationScope. For reasoning please see the inline code comments. --- .../lib/active_record/associations/through_association_scope.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 7cb039859c..7a0fa08f66 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -35,8 +35,12 @@ module ActiveRecord } end + # This scope affects the creation of the associated records (not the join records). At the + # moment we only support creating on a :through association when the source reflection is a + # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so + # this scope has can legitimately be empty. def construct_create_scope - construct_owner_attributes(@reflection) + { } end def aliased_through_table -- cgit v1.2.3 From d7a659334cf5b342156682758f971308021deb4a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 20:50:51 +0000 Subject: Remove construct_from from ThroughAssociationScope - it's not called from anywhere --- .../lib/active_record/associations/through_association_scope.rb | 4 ---- 1 file changed, 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 7a0fa08f66..fa84de533f 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -73,10 +73,6 @@ module ActiveRecord end end - def construct_from - @reflection.table_name - end - def construct_select(custom_select = nil) distinct = "DISTINCT #{@reflection.quoted_table_name}.*" if @reflection.options[:uniq] custom_select || @reflection.options[:select] || distinct -- cgit v1.2.3 From 7021b6b851c1567a73eae5e74eba437a52b112bf Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 23 Dec 2010 20:52:56 +0000 Subject: Remove custom_select param from construct_select, as it isn't used --- .../lib/active_record/associations/through_association_scope.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index fa84de533f..415fc53e1c 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -73,9 +73,9 @@ module ActiveRecord end end - def construct_select(custom_select = nil) - distinct = "DISTINCT #{@reflection.quoted_table_name}.*" if @reflection.options[:uniq] - custom_select || @reflection.options[:select] || distinct + def construct_select + @reflection.options[:select] || + @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" end def construct_joins -- cgit v1.2.3 From f2230c06edf9c1ca72892bbe00e816be1dafa840 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 24 Dec 2010 00:14:46 +0000 Subject: Fix dodgy tests which were effectively asserting nil == nil --- activerecord/test/cases/associations/eager_test.rb | 6 +++--- activerecord/test/fixtures/companies.yml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 6efb0d57e4..ad7eb6e4e6 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -886,15 +886,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preload_has_one_using_primary_key - expected = Firm.find(:first).account_using_primary_key - firm = Firm.find :first, :include => :account_using_primary_key + expected = accounts(:signals37) + firm = Firm.find :first, :include => :account_using_primary_key, :order => 'companies.id' assert_no_queries do assert_equal expected, firm.account_using_primary_key end end def test_include_has_one_using_primary_key - expected = Firm.find(1).account_using_primary_key + expected = accounts(:signals37) firm = Firm.find(:all, :include => :account_using_primary_key, :order => 'accounts.id').detect {|f| f.id == 1} assert_no_queries do assert_equal expected, firm.account_using_primary_key diff --git a/activerecord/test/fixtures/companies.yml b/activerecord/test/fixtures/companies.yml index 30b73d8e84..c75bc5dad3 100644 --- a/activerecord/test/fixtures/companies.yml +++ b/activerecord/test/fixtures/companies.yml @@ -13,6 +13,7 @@ first_firm: type: Firm name: 37signals ruby_type: Firm + firm_id: 1 second_client: id: 3 -- cgit v1.2.3 From e8ada11aac28f0850f0e485acacf34e7eb81aa19 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 24 Dec 2010 00:29:04 +0000 Subject: Associations: DRY up the code which is generating conditions, and make it all use arel rather than SQL strings --- .../associations/association_collection.rb | 2 +- .../associations/association_proxy.rb | 45 +++++++++++++++++----- .../has_and_belongs_to_many_association.rb | 10 +++-- .../associations/has_many_association.rb | 18 +-------- .../associations/has_one_association.rb | 17 ++------ .../associations/through_association_scope.rb | 22 +---------- activerecord/test/fixtures/companies.yml | 1 - 7 files changed, 50 insertions(+), 65 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 8bb32400cf..c9f9c925b0 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -111,7 +111,7 @@ module ActiveRecord else build_record(attributes) do |record| block.call(record) if block_given? - set_belongs_to_association_for(record) + set_owner_attributes(record) end end end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index f4eceeed8c..f51275d86d 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -181,18 +181,41 @@ module ActiveRecord @reflection.klass.send(:sanitize_sql, sql, table_name) end - # Assigns the ID of the owner to the corresponding foreign key in +record+. - # If the association is polymorphic the type of the owner is also set. - def set_belongs_to_association_for(record) - if @reflection.options[:as] - record["#{@reflection.options[:as]}_id"] = @owner.id if @owner.persisted? - record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s + # Sets the owner attributes on the given record + # Note: does not really make sense for belongs_to associations, but this method is not + # used by belongs_to + def set_owner_attributes(record) + if @owner.persisted? + construct_owner_attributes.each { |key, value| record[key] = value } + end + end + + # Returns a has linking the owner to the association represented by the reflection + def construct_owner_attributes(reflection = @reflection) + attributes = {} + if reflection.macro == :belongs_to + attributes[reflection.association_primary_key] = @owner.send(reflection.primary_key_name) else - if @owner.persisted? - primary_key = @reflection.options[:primary_key] || :id - record[@reflection.primary_key_name] = @owner.send(primary_key) + attributes[reflection.primary_key_name] = @owner.send(reflection.active_record_primary_key) + + if reflection.options[:as] + attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name end end + attributes + end + + # Builds an array of arel nodes from the owner attributes hash + def construct_owner_conditions(table = aliased_table, reflection = @reflection) + construct_owner_attributes(reflection).map do |attr, value| + table[attr].eq(value) + end + end + + def construct_conditions + conditions = construct_owner_conditions + conditions << Arel.sql(sql_conditions) if sql_conditions + aliased_table.create_and(conditions) end # Merges into +options+ the ones coming from the reflection. @@ -232,6 +255,10 @@ module ActiveRecord {} end + def aliased_table + @reflection.klass.arel_table + end + private # Forwards any missing method call to the \target. def method_missing(method, *args) diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index e17ac6f2cc..24871303eb 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -75,10 +75,12 @@ module ActiveRecord "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" end - def construct_conditions - sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " - sql << " AND (#{conditions})" if conditions - sql + def join_table + Arel::Table.new(@reflection.options[:join_table]) + end + + def construct_owner_conditions + super(join_table) end def construct_find_scope diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 2d2f377e1f..d35ecfd603 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -54,7 +54,7 @@ module ActiveRecord end def insert_record(record, force = false, validate = true) - set_belongs_to_association_for(record) + set_owner_attributes(record) save_record(record, force, validate) end @@ -79,18 +79,6 @@ module ActiveRecord end end - def construct_conditions - if @reflection.options[:as] - sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - else - sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}" - end - sql << " AND (#{conditions})" if conditions - sql - end - def construct_find_scope { :conditions => construct_conditions, @@ -102,9 +90,7 @@ module ActiveRecord end def construct_create_scope - create_scoping = {} - set_belongs_to_association_for(create_scoping) - create_scoping + construct_owner_attributes end def we_can_set_the_inverse_on_this?(record) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index d08cbea199..fd3827390f 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -49,7 +49,7 @@ module ActiveRecord @target = nil else raise_on_type_mismatch(obj) - set_belongs_to_association_for(obj) + set_owner_attributes(obj) @target = (AssociationProxy === obj ? obj.target : obj) end @@ -84,22 +84,11 @@ module ActiveRecord end def construct_find_scope - if @reflection.options[:as] - sql = - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " + - "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}" - else - test = owner_quoted_id == "NULL" ? "IS" : "=" - sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} #{test} #{owner_quoted_id}" - end - sql << " AND (#{conditions})" if conditions - { :conditions => sql } + { :conditions => construct_conditions } end def construct_create_scope - create_scoping = {} - set_belongs_to_association_for(create_scoping) - create_scoping + construct_owner_attributes end def new_record(replace_existing) diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb index 415fc53e1c..1abeb178ff 100644 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ b/activerecord/lib/active_record/associations/through_association_scope.rb @@ -51,26 +51,8 @@ module ActiveRecord @reflection.through_reflection.klass.arel_table end - # Build SQL conditions from attributes, qualified by table name. - def construct_conditions - table = aliased_through_table - conditions = construct_owner_attributes(@reflection.through_reflection).map do |attr, value| - table[attr].eq(value) - end - conditions << Arel.sql(sql_conditions) if sql_conditions - table.create_and(conditions) - end - - # Associate attributes pointing to owner - def construct_owner_attributes(reflection) - if as = reflection.options[:as] - { "#{as}_id" => @owner[reflection.active_record_primary_key], - "#{as}_type" => @owner.class.base_class.name } - elsif reflection.macro == :belongs_to - { reflection.klass.primary_key => @owner[reflection.primary_key_name] } - else - { reflection.primary_key_name => @owner[reflection.active_record_primary_key] } - end + def construct_owner_conditions + super(aliased_through_table, @reflection.through_reflection) end def construct_select diff --git a/activerecord/test/fixtures/companies.yml b/activerecord/test/fixtures/companies.yml index c75bc5dad3..a982d3921d 100644 --- a/activerecord/test/fixtures/companies.yml +++ b/activerecord/test/fixtures/companies.yml @@ -9,7 +9,6 @@ first_client: first_firm: id: 1 - firm_id: 1 type: Firm name: 37signals ruby_type: Firm -- cgit v1.2.3 From 0c272471fe815c121a83d959468b2f1b6f8aaba8 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 26 Dec 2010 19:04:26 +0000 Subject: Remove AssociationProxy#dependent? - it's badly named and only used in one place --- activerecord/lib/active_record/associations/association_proxy.rb | 5 ----- activerecord/lib/active_record/associations/has_one_association.rb | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index f51275d86d..c7b171b41d 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -167,11 +167,6 @@ module ActiveRecord end protected - # Does the association have a :dependent option? - def dependent? - @reflection.options[:dependent] - end - def interpolate_sql(sql, record = nil) @owner.send(:interpolate_sql, sql, record) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index fd3827390f..654157d998 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -27,7 +27,7 @@ module ActiveRecord load_target unless @target.nil? || @target == obj - if dependent? && !dont_save + if @reflection.options[:dependent] && !dont_save case @reflection.options[:dependent] when :delete @target.delete if @target.persisted? -- cgit v1.2.3 From b0498372a10bda006350af42708a5588ab28ffcb Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 26 Dec 2010 19:37:35 +0000 Subject: Add a HasAssociation module for common code for has_* associations --- activerecord/lib/active_record/associations.rb | 2 + .../associations/association_collection.rb | 2 + .../associations/association_proxy.rb | 59 +------ .../active_record/associations/has_association.rb | 54 +++++++ .../associations/has_many_through_association.rb | 3 +- .../associations/has_one_association.rb | 2 + .../associations/has_one_through_association.rb | 4 +- .../associations/through_association.rb | 169 +++++++++++++++++++++ .../associations/through_association_scope.rb | 169 --------------------- 9 files changed, 236 insertions(+), 228 deletions(-) create mode 100644 activerecord/lib/active_record/associations/has_association.rb create mode 100644 activerecord/lib/active_record/associations/through_association.rb delete mode 100644 activerecord/lib/active_record/associations/through_association_scope.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1056c51a3d..b49cf8de95 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -120,6 +120,8 @@ module ActiveRecord # So there is no need to eager load them. autoload :AssociationCollection, 'active_record/associations/association_collection' autoload :AssociationProxy, 'active_record/associations/association_proxy' + autoload :HasAssociation, 'active_record/associations/has_association' + autoload :ThroughAssociation, 'active_record/associations/through_association' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association' diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index c9f9c925b0..86cc17394c 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -18,6 +18,8 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class AssociationCollection < AssociationProxy #:nodoc: + include HasAssociation + delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped def select(select = nil) diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index c7b171b41d..b60aa33c98 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -4,17 +4,17 @@ module ActiveRecord module Associations # = Active Record Associations # - # This is the root class of all association proxies: + # This is the root class of all association proxies ('+ Foo' signifies an included module Foo): # # AssociationProxy # BelongsToAssociation - # HasOneAssociation # BelongsToPolymorphicAssociation - # AssociationCollection + # AssociationCollection + HasAssociation # HasAndBelongsToManyAssociation # HasManyAssociation - # HasManyThroughAssociation - # HasOneThroughAssociation + # HasManyThroughAssociation + ThroughAssociation + # HasOneAssociation + HasAssociation + # HasOneThroughAssociation + ThroughAssociation # # Association proxies in Active Record are middlemen between the object that # holds the association, known as the @owner, and the actual associated @@ -176,43 +176,6 @@ module ActiveRecord @reflection.klass.send(:sanitize_sql, sql, table_name) end - # Sets the owner attributes on the given record - # Note: does not really make sense for belongs_to associations, but this method is not - # used by belongs_to - def set_owner_attributes(record) - if @owner.persisted? - construct_owner_attributes.each { |key, value| record[key] = value } - end - end - - # Returns a has linking the owner to the association represented by the reflection - def construct_owner_attributes(reflection = @reflection) - attributes = {} - if reflection.macro == :belongs_to - attributes[reflection.association_primary_key] = @owner.send(reflection.primary_key_name) - else - attributes[reflection.primary_key_name] = @owner.send(reflection.active_record_primary_key) - - if reflection.options[:as] - attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name - end - end - attributes - end - - # Builds an array of arel nodes from the owner attributes hash - def construct_owner_conditions(table = aliased_table, reflection = @reflection) - construct_owner_attributes(reflection).map do |attr, value| - table[attr].eq(value) - end - end - - def construct_conditions - conditions = construct_owner_conditions - conditions << Arel.sql(sql_conditions) if sql_conditions - aliased_table.create_and(conditions) - end - # Merges into +options+ the ones coming from the reflection. def merge_options_from_reflection!(options) options.reverse_merge!( @@ -312,18 +275,6 @@ module ActiveRecord end end - if RUBY_VERSION < '1.9.2' - # Array#flatten has problems with recursive arrays before Ruby 1.9.2. - # Going one level deeper solves the majority of the problems. - def flatten_deeper(array) - array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten - end - else - def flatten_deeper(array) - array.flatten - end - end - # Returns the ID of the owner, quoted if needed. def owner_quoted_id @owner.quoted_id diff --git a/activerecord/lib/active_record/associations/has_association.rb b/activerecord/lib/active_record/associations/has_association.rb new file mode 100644 index 0000000000..4407e2ea9a --- /dev/null +++ b/activerecord/lib/active_record/associations/has_association.rb @@ -0,0 +1,54 @@ +module ActiveRecord + module Associations + # Included in all has_* associations (i.e. everything except belongs_to) + module HasAssociation #:nodoc: + protected + # Sets the owner attributes on the given record + def set_owner_attributes(record) + if @owner.persisted? + construct_owner_attributes.each { |key, value| record[key] = value } + end + end + + # Returns a hash linking the owner to the association represented by the reflection + def construct_owner_attributes(reflection = @reflection) + attributes = {} + if reflection.macro == :belongs_to + attributes[reflection.association_primary_key] = @owner.send(reflection.primary_key_name) + else + attributes[reflection.primary_key_name] = @owner.send(reflection.active_record_primary_key) + + if reflection.options[:as] + attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name + end + end + attributes + end + + # Builds an array of arel nodes from the owner attributes hash + def construct_owner_conditions(table = aliased_table, reflection = @reflection) + construct_owner_attributes(reflection).map do |attr, value| + table[attr].eq(value) + end + end + + def construct_conditions + conditions = construct_owner_conditions + conditions << Arel.sql(sql_conditions) if sql_conditions + aliased_table.create_and(conditions) + end + + if RUBY_VERSION < '1.9.2' + # Array#flatten has problems with recursive arrays before Ruby 1.9.2. + # Going one level deeper solves the majority of the problems. + def flatten_deeper(array) + array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten + end + else + def flatten_deeper(array) + array.flatten + end + end + end + end +end 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 4dabfe2ea3..8569a2f004 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,11 +1,10 @@ -require "active_record/associations/through_association_scope" require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Has Many Through Association module Associations class HasManyThroughAssociation < HasManyAssociation #:nodoc: - include ThroughAssociationScope + include ThroughAssociation alias_method :new, :build diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 654157d998..ca0828ea7b 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,6 +2,8 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < AssociationProxy #:nodoc: + include HasAssociation + def create(attrs = {}, replace_existing = true) new_record(replace_existing) do |reflection| attrs = merge_with_conditions(attrs) diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 8c8c9fbe5d..c9ae930e93 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -1,10 +1,8 @@ -require "active_record/associations/through_association_scope" - module ActiveRecord # = Active Record Has One Through Association module Associations class HasOneThroughAssociation < HasOneAssociation - include ThroughAssociationScope + include ThroughAssociation def replace(new_value) create_through_record(new_value) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb new file mode 100644 index 0000000000..57718285f8 --- /dev/null +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -0,0 +1,169 @@ +module ActiveRecord + # = Active Record Through Association + module Associations + module ThroughAssociation + + def scoped + with_scope(@scope) do + @reflection.klass.scoped & + @reflection.through_reflection.klass.scoped + end + end + + def stale_target? + if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key) + previous_key = @through_foreign_key.to_s + current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s + + previous_key != current_key + else + false + end + end + + protected + + def construct_find_scope + { + :conditions => construct_conditions, + :joins => construct_joins, + :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], + :select => construct_select, + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :readonly => @reflection.options[:readonly] + } + end + + # This scope affects the creation of the associated records (not the join records). At the + # moment we only support creating on a :through association when the source reflection is a + # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so + # this scope has can legitimately be empty. + def construct_create_scope + { } + end + + def aliased_through_table + name = @reflection.through_reflection.table_name + + @reflection.table_name == name ? + @reflection.through_reflection.klass.arel_table.alias(name + "_join") : + @reflection.through_reflection.klass.arel_table + end + + def construct_owner_conditions + super(aliased_through_table, @reflection.through_reflection) + end + + def construct_select + @reflection.options[:select] || + @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" + end + + def construct_joins + right = aliased_through_table + left = @reflection.klass.arel_table + + conditions = [] + + if @reflection.source_reflection.macro == :belongs_to + reflection_primary_key = @reflection.source_reflection.options[:primary_key] || + @reflection.klass.primary_key + source_primary_key = @reflection.source_reflection.primary_key_name + if @reflection.options[:source_type] + column = @reflection.source_reflection.options[:foreign_type] + conditions << + right[column].eq(@reflection.options[:source_type]) + end + else + reflection_primary_key = @reflection.source_reflection.primary_key_name + source_primary_key = @reflection.source_reflection.options[:primary_key] || + @reflection.through_reflection.klass.primary_key + if @reflection.source_reflection.options[:as] + column = "#{@reflection.source_reflection.options[:as]}_type" + conditions << + left[column].eq(@reflection.through_reflection.klass.name) + end + end + + conditions << + left[reflection_primary_key].eq(right[source_primary_key]) + + right.create_join( + right, + right.create_on(right.create_and(conditions))) + end + + # Construct attributes for :through pointing to owner and associate. + def construct_join_attributes(associate) + # TODO: revisit this to allow it for deletion, supposing dependent option is supported + raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) + + join_attributes = { + @reflection.source_reflection.primary_key_name => + associate.send(@reflection.source_reflection.association_primary_key) + } + + if @reflection.options[:source_type] + join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name) + end + + if @reflection.through_reflection.options[:conditions].is_a?(Hash) + join_attributes.merge!(@reflection.through_reflection.options[:conditions]) + end + + join_attributes + end + + def conditions + @conditions = build_conditions unless defined?(@conditions) + @conditions + end + + def build_conditions + association_conditions = @reflection.options[:conditions] + through_conditions = build_through_conditions + source_conditions = @reflection.source_reflection.options[:conditions] + uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? + + if association_conditions || through_conditions || source_conditions || uses_sti + all = [] + + [association_conditions, source_conditions].each do |conditions| + all << interpolate_sql(sanitize_sql(conditions)) if conditions + end + + all << through_conditions if through_conditions + all << build_sti_condition if uses_sti + + all.map { |sql| "(#{sql})" } * ' AND ' + end + end + + def build_through_conditions + conditions = @reflection.through_reflection.options[:conditions] + if conditions.is_a?(Hash) + interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( + @reflection.quoted_table_name, + @reflection.through_reflection.quoted_table_name) + elsif conditions + interpolate_sql(sanitize_sql(conditions)) + end + end + + def build_sti_condition + @reflection.through_reflection.klass.send(:type_condition).to_sql + end + + alias_method :sql_conditions, :conditions + + def update_stale_state + construct_scope if stale_target? + + if @reflection.through_reflection.macro == :belongs_to + @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name) + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb deleted file mode 100644 index 1abeb178ff..0000000000 --- a/activerecord/lib/active_record/associations/through_association_scope.rb +++ /dev/null @@ -1,169 +0,0 @@ -module ActiveRecord - # = Active Record Through Association Scope - module Associations - module ThroughAssociationScope - - def scoped - with_scope(@scope) do - @reflection.klass.scoped & - @reflection.through_reflection.klass.scoped - end - end - - def stale_target? - if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key) - previous_key = @through_foreign_key.to_s - current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s - - previous_key != current_key - else - false - end - end - - protected - - def construct_find_scope - { - :conditions => construct_conditions, - :joins => construct_joins, - :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], - :select => construct_select, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :readonly => @reflection.options[:readonly] - } - end - - # This scope affects the creation of the associated records (not the join records). At the - # moment we only support creating on a :through association when the source reflection is a - # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so - # this scope has can legitimately be empty. - def construct_create_scope - { } - end - - def aliased_through_table - name = @reflection.through_reflection.table_name - - @reflection.table_name == name ? - @reflection.through_reflection.klass.arel_table.alias(name + "_join") : - @reflection.through_reflection.klass.arel_table - end - - def construct_owner_conditions - super(aliased_through_table, @reflection.through_reflection) - end - - def construct_select - @reflection.options[:select] || - @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" - end - - def construct_joins - right = aliased_through_table - left = @reflection.klass.arel_table - - conditions = [] - - if @reflection.source_reflection.macro == :belongs_to - reflection_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.klass.primary_key - source_primary_key = @reflection.source_reflection.primary_key_name - if @reflection.options[:source_type] - column = @reflection.source_reflection.options[:foreign_type] - conditions << - right[column].eq(@reflection.options[:source_type]) - end - else - reflection_primary_key = @reflection.source_reflection.primary_key_name - source_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.through_reflection.klass.primary_key - if @reflection.source_reflection.options[:as] - column = "#{@reflection.source_reflection.options[:as]}_type" - conditions << - left[column].eq(@reflection.through_reflection.klass.name) - end - end - - conditions << - left[reflection_primary_key].eq(right[source_primary_key]) - - right.create_join( - right, - right.create_on(right.create_and(conditions))) - end - - # Construct attributes for :through pointing to owner and associate. - def construct_join_attributes(associate) - # TODO: revisit this to allow it for deletion, supposing dependent option is supported - raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) - - join_attributes = { - @reflection.source_reflection.primary_key_name => - associate.send(@reflection.source_reflection.association_primary_key) - } - - if @reflection.options[:source_type] - join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name) - end - - if @reflection.through_reflection.options[:conditions].is_a?(Hash) - join_attributes.merge!(@reflection.through_reflection.options[:conditions]) - end - - join_attributes - end - - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end - - def build_conditions - association_conditions = @reflection.options[:conditions] - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - - if association_conditions || through_conditions || source_conditions || uses_sti - all = [] - - [association_conditions, source_conditions].each do |conditions| - all << interpolate_sql(sanitize_sql(conditions)) if conditions - end - - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti - - all.map { |sql| "(#{sql})" } * ' AND ' - end - end - - def build_through_conditions - conditions = @reflection.through_reflection.options[:conditions] - if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) - end - end - - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql - end - - alias_method :sql_conditions, :conditions - - def update_stale_state - construct_scope if stale_target? - - if @reflection.through_reflection.macro == :belongs_to - @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name) - end - end - end - end -end -- cgit v1.2.3 From 9f5c18ce075179cfc73a00cba9a19d69aaf5274c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 26 Dec 2010 21:37:25 +0000 Subject: Refactor we_can_set_the_inverse_on_this? to use a less bizarre name amongst other things --- .../lib/active_record/association_preload.rb | 5 ++--- .../associations/association_collection.rb | 6 ++---- .../associations/association_proxy.rb | 24 ++++++++++++++-------- .../associations/belongs_to_association.rb | 9 ++++---- .../belongs_to_polymorphic_association.rb | 24 +++++++--------------- .../associations/class_methods/join_dependency.rb | 4 ++-- .../has_and_belongs_to_many_association.rb | 4 ++++ .../associations/has_many_association.rb | 4 ---- .../associations/has_many_through_association.rb | 2 +- .../associations/has_one_association.rb | 11 +++------- 10 files changed, 41 insertions(+), 52 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index ba225b700c..94bcc869c2 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -127,8 +127,7 @@ module ActiveRecord association_proxy = parent_record.send(reflection_name) association_proxy.loaded association_proxy.target.push(*Array.wrap(associated_record)) - - association_proxy.__send__(:set_inverse_instance, associated_record, parent_record) + association_proxy.send(:set_inverse_instance, associated_record) end end @@ -157,7 +156,7 @@ module ActiveRecord mapped_records = id_to_record_map[associated_record[key].to_s] mapped_records.each do |mapped_record| association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record) - association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record) + association_proxy.send(:set_inverse_instance, associated_record) end end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 86cc17394c..97d9288874 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -454,9 +454,7 @@ module ActiveRecord end records = @reflection.options[:uniq] ? uniq(records) : records - records.each do |record| - set_inverse_instance(record, @owner) - end + records.each { |record| set_inverse_instance(record) } records end @@ -470,7 +468,7 @@ module ActiveRecord @target << record end callback(:after_add, record) - set_inverse_instance(record, @owner) + set_inverse_instance(record) record end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index b60aa33c98..6720d83199 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -217,6 +217,13 @@ module ActiveRecord @reflection.klass.arel_table end + # Set the inverse association, if possible + def set_inverse_instance(record) + if record && invertible_for?(record) + record.send("set_#{inverse_reflection_for(record).name}_target", @owner) + end + end + private # Forwards any missing method call to the \target. def method_missing(method, *args) @@ -280,17 +287,16 @@ module ActiveRecord @owner.quoted_id end - def set_inverse_instance(record, instance) - return if record.nil? || !we_can_set_the_inverse_on_this?(record) - inverse_relationship = @reflection.inverse_of - unless inverse_relationship.nil? - record.send(:"set_#{inverse_relationship.name}_target", instance) - end + # Can be redefined by subclasses, notably polymorphic belongs_to + # The record parameter is necessary to support polymorphic inverses as we must check for + # the association in the specific class of the record. + def inverse_reflection_for(record) + @reflection.inverse_of end - # Override in subclasses - def we_can_set_the_inverse_on_this?(record) - false + # Is this association invertible? Can be redefined by subclasses. + def invertible_for?(record) + inverse_reflection_for(record) end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index bbfe18f9fb..98c1c13524 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -32,7 +32,7 @@ module ActiveRecord @updated = true end - set_inverse_instance(record, @owner) + set_inverse_instance(record) loaded record @@ -69,7 +69,7 @@ module ActiveRecord options ) if @owner[@reflection.primary_key_name] end - set_inverse_instance(the_target, @owner) + set_inverse_instance(the_target) the_target end @@ -83,8 +83,9 @@ module ActiveRecord # NOTE - for now, we're only supporting inverse setting from belongs_to back onto # has_one associations. - def we_can_set_the_inverse_on_this?(record) - @reflection.has_inverse? && @reflection.inverse_of.macro == :has_one + def invertible_for?(record) + inverse = inverse_reflection_for(record) + inverse && inverse.macro == :has_one end def record_id(record) diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index c580de7fbe..90eff7399c 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -14,7 +14,7 @@ module ActiveRecord @updated = true end - set_inverse_instance(record, @owner) + set_inverse_instance(record) loaded record end @@ -38,23 +38,13 @@ module ActiveRecord private - # NOTE - for now, we're only supporting inverse setting from belongs_to back onto - # has_one associations. - def we_can_set_the_inverse_on_this?(record) - if @reflection.has_inverse? - inverse_association = @reflection.polymorphic_inverse_of(record.class) - inverse_association && inverse_association.macro == :has_one - else - false - end + def inverse_reflection_for(record) + @reflection.polymorphic_inverse_of(record.class) end - def set_inverse_instance(record, instance) - return if record.nil? || !we_can_set_the_inverse_on_this?(record) - inverse_relationship = @reflection.polymorphic_inverse_of(record.class) - if inverse_relationship - record.send(:"set_#{inverse_relationship.name}_target", instance) - end + def invertible_for?(record) + inverse = inverse_reflection_for(record) + inverse && inverse.macro == :has_one end def construct_find_scope @@ -71,7 +61,7 @@ module ActiveRecord :include => @reflection.options[:include] ) end - set_inverse_instance(target, @owner) + set_inverse_instance(target) target end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb index a74d0a4ca8..bb6145656e 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb @@ -212,7 +212,7 @@ module ActiveRecord collection = record.send(join_part.reflection.name) collection.loaded collection.target.push(association) - collection.__send__(:set_inverse_instance, association, record) + collection.send(:set_inverse_instance, association) when :belongs_to set_target_and_inverse(join_part, association, record) else @@ -224,7 +224,7 @@ module ActiveRecord def set_target_and_inverse(join_part, association, record) association_proxy = record.send("set_#{join_part.reflection.name}_target", association) - association_proxy.__send__(:set_inverse_instance, association, record) + association_proxy.send(:set_inverse_instance, association) end end end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 24871303eb..b1d454545f 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -110,6 +110,10 @@ module ActiveRecord [] end end + + def invertible_for?(record) + false + end end 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 d35ecfd603..c3360463cd 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -92,10 +92,6 @@ module ActiveRecord def construct_create_scope construct_owner_attributes end - - def we_can_set_the_inverse_on_this?(record) - @reflection.inverse_of - end end end end 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 8569a2f004..ba464c8d79 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -67,7 +67,7 @@ module ActiveRecord end # NOTE - not sure that we can actually cope with inverses here - def we_can_set_the_inverse_on_this?(record) + def invertible_for?(record) false end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index ca0828ea7b..c32aaf986e 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -55,7 +55,7 @@ module ActiveRecord @target = (AssociationProxy === obj ? obj.target : obj) end - set_inverse_instance(obj, @owner) + set_inverse_instance(obj) @loaded = true unless !@owner.persisted? || obj.nil? || dont_save @@ -81,7 +81,7 @@ module ActiveRecord the_target = with_scope(:find => @scope[:find]) do @reflection.klass.find(:first, options) end - set_inverse_instance(the_target, @owner) + set_inverse_instance(the_target) the_target end @@ -107,17 +107,12 @@ module ActiveRecord else record[@reflection.primary_key_name] = @owner.id if @owner.persisted? self.target = record - set_inverse_instance(record, @owner) + set_inverse_instance(record) end record end - def we_can_set_the_inverse_on_this?(record) - inverse = @reflection.inverse_of - return !inverse.nil? - end - def merge_with_conditions(attrs={}) attrs ||= {} attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) -- cgit v1.2.3 From 7e91ad3f89ba134d863072db8db06ece6ec3ef19 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sun, 26 Dec 2010 19:56:18 -0700 Subject: stop calling deprecated apis --- activerecord/test/cases/method_scoping_test.rb | 8 ++++---- activerecord/test/cases/relation_scoping_test.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index 0ffd0e2ab3..e3ba65b4fa 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -14,7 +14,7 @@ class MethodScopingTest < ActiveRecord::TestCase def test_set_conditions Developer.send(:with_scope, :find => { :conditions => 'just a test...' }) do - assert_equal '(just a test...)', Developer.scoped.arel.send(:where_clauses).join(' AND ') + assert_match '(just a test...)', Developer.scoped.arel.to_sql end end @@ -275,7 +275,7 @@ class NestedScopingTest < ActiveRecord::TestCase Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do Developer.send(:with_scope, :find => { :limit => 10 }) do devs = Developer.scoped - assert_equal '(salary = 80000)', devs.arel.send(:where_clauses).join(' AND ') + assert_match '(salary = 80000)', devs.arel.to_sql assert_equal 10, devs.taken end end @@ -309,7 +309,7 @@ class NestedScopingTest < ActiveRecord::TestCase Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do devs = Developer.scoped - assert_equal "(name = 'David') AND (salary = 80000)", devs.arel.send(:where_clauses).join(' AND ') + assert_match "(name = 'David') AND (salary = 80000)", devs.arel.to_sql assert_equal(1, Developer.count) end Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do @@ -322,7 +322,7 @@ class NestedScopingTest < ActiveRecord::TestCase Developer.send(:with_scope, :find => { :conditions => 'salary = 80000', :limit => 10 }) do Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do devs = Developer.scoped - assert_equal "(salary = 80000) AND (name = 'David')", devs.arel.send(:where_clauses).join(' AND ') + assert_match "(salary = 80000) AND (name = 'David')", devs.arel.to_sql assert_equal 10, devs.taken end end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index f113a9c516..e8672515fc 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -152,7 +152,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase Developer.where('salary = 80000').scoping do Developer.limit(10).scoping do devs = Developer.scoped - assert_equal '(salary = 80000)', devs.arel.send(:where_clauses).join(' AND ') + assert_match '(salary = 80000)', devs.arel.to_sql assert_equal 10, devs.taken end end -- cgit v1.2.3 From 67da59097909295c59574e3fd3b502022f860aea Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sun, 26 Dec 2010 20:15:09 -0700 Subject: make our hash of klasses and ids actually have classes for keys --- activerecord/lib/active_record/association_preload.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 94bcc869c2..b39b703a92 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -329,7 +329,7 @@ module ActiveRecord if klass = record.send(polymorph_type) klass_id = record.send(primary_key_name) if klass_id - id_map = klasses_and_ids[klass] ||= {} + id_map = klasses_and_ids[klass.constantize] ||= {} (id_map[klass_id.to_s] ||= []) << record end end @@ -340,16 +340,14 @@ module ActiveRecord key && key.to_s end id_map.delete nil - klasses_and_ids[reflection.klass.name] = id_map unless id_map.empty? + klasses_and_ids[reflection.klass] = id_map unless id_map.empty? end - klasses_and_ids.each do |klass_name, _id_map| - klass = klass_name.constantize - - table = klass.arel_table + klasses_and_ids.each do |klass, _id_map| + table = klass.arel_table primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s - method = in_or_equal(_id_map.keys) - conditions = table[primary_key].send(*method) + method = in_or_equal(_id_map.keys) + conditions = table[primary_key].send(*method) custom_conditions = append_conditions(reflection, preload_options) conditions = custom_conditions.inject(conditions) do |ast, cond| -- cgit v1.2.3 From 9bac649fa4ca6f05795e7cab8d30049aa2410cb8 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sun, 26 Dec 2010 20:22:13 -0700 Subject: try not to make so many funcalls --- activerecord/lib/active_record/association_preload.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index b39b703a92..ecf7b6c210 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -147,13 +147,16 @@ module ActiveRecord def set_association_single_records(id_to_record_map, reflection_name, associated_records, key) seen_keys = {} associated_records.each do |associated_record| + seen_key = associated_record[key].to_s + #this is a has_one or belongs_to: there should only be one record. #Unfortunately we can't (in portable way) ask the database for #'all records where foo_id in (x,y,z), but please # only one row per distinct foo_id' so this where we enforce that - next if seen_keys[associated_record[key].to_s] - seen_keys[associated_record[key].to_s] = true - mapped_records = id_to_record_map[associated_record[key].to_s] + next if seen_keys.key? seen_key + + seen_keys[seen_key] = true + mapped_records = id_to_record_map[seen_key] mapped_records.each do |mapped_record| association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record) association_proxy.send(:set_inverse_instance, associated_record) -- cgit v1.2.3 From 6b099975fa7e18356b19eca659595713512209e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 27 Dec 2010 09:30:36 +0100 Subject: No need to symbolize these. --- activerecord/lib/active_record/base.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 6aac25ba9b..f96aa8b3fb 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1490,8 +1490,10 @@ MSG attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] + elsif respond_to?("#{k}=") + send("#{k}=", v) else - respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}") + raise(UnknownAttributeError, "unknown attribute: #{k}") end end @@ -1816,7 +1818,7 @@ MSG if scope = self.class.send(:current_scoped_methods) create_with = scope.scope_for_create create_with.each { |att,value| - respond_to?(:"#{att}=") && send("#{att}=", value) + respond_to?("#{att}=") && send("#{att}=", value) } end end -- cgit v1.2.3 From 897b56bb2f8c2a904f546db1a32bad074463ec9b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 27 Dec 2010 12:47:30 -0700 Subject: I N C E P T I O N: flatten_deeper works around a bug in Ruby 1.8.2. --- .../lib/active_record/associations/association_collection.rb | 4 ++-- .../lib/active_record/associations/has_association.rb | 12 ------------ .../associations/has_many_through_association.rb | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 97d9288874..108e316672 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -125,7 +125,7 @@ module ActiveRecord load_target if @owner.new_record? transaction do - flatten_deeper(records).each do |record| + records.flatten.each do |record| raise_on_type_mismatch(record) add_record_to_target_with_callbacks(record) do |r| result &&= insert_record(record) unless @owner.new_record? @@ -501,7 +501,7 @@ module ActiveRecord end def remove_records(*records) - records = flatten_deeper(records) + records = records.flatten records.each { |record| raise_on_type_mismatch(record) } transaction do diff --git a/activerecord/lib/active_record/associations/has_association.rb b/activerecord/lib/active_record/associations/has_association.rb index 4407e2ea9a..0ecdb696ea 100644 --- a/activerecord/lib/active_record/associations/has_association.rb +++ b/activerecord/lib/active_record/associations/has_association.rb @@ -37,18 +37,6 @@ module ActiveRecord conditions << Arel.sql(sql_conditions) if sql_conditions aliased_table.create_and(conditions) end - - if RUBY_VERSION < '1.9.2' - # Array#flatten has problems with recursive arrays before Ruby 1.9.2. - # Going one level deeper solves the majority of the problems. - def flatten_deeper(array) - array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten - end - else - def flatten_deeper(array) - array.flatten - end - end end end end 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 ba464c8d79..e2b008034e 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -10,7 +10,7 @@ module ActiveRecord def destroy(*records) transaction do - delete_records(flatten_deeper(records)) + delete_records(records.flatten) super end end -- cgit v1.2.3 From 304d38c0536dc32a8a1595ba34370ebf69a0d50d Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Tue, 28 Dec 2010 00:45:35 -0200 Subject: Allow primary_key to be an attribute when the model is a new record --- activerecord/lib/active_record/persistence.rb | 2 +- activerecord/lib/active_record/transactions.rb | 4 ++-- activerecord/test/cases/attribute_methods_test.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 9ac8fcb176..1fc9472231 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -282,7 +282,7 @@ module ActiveRecord # that instances loaded from the database would. def attributes_from_column_definition Hash[self.class.columns.map do |column| - [column.name, column.default] unless column.name == self.class.primary_key + [column.name, column.default] end] end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 443f318067..868f761a33 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -301,8 +301,8 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc @_start_transaction_state ||= {} + @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) @_start_transaction_state[:new_record] = @new_record end unless @_start_transaction_state.include?(:destroyed) @@ -329,7 +329,7 @@ module ActiveRecord @attributes = @attributes.dup if @attributes.frozen? @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] - if restore_state[:id] + if restore_state.has_key?(:id) self.id = restore_state[:id] else @attributes.delete(self.class.primary_key) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 8214815bde..e3cbae4a84 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -105,7 +105,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_read_attributes_before_type_cast category = Category.new({:name=>"Test categoty", :type => nil}) - category_attrs = {"name"=>"Test categoty", "type" => nil, "categorizations_count" => nil} + category_attrs = {"name"=>"Test categoty", "id" => nil, "type" => nil, "categorizations_count" => nil} assert_equal category_attrs , category.attributes_before_type_cast end -- cgit v1.2.3 From eb2ebe76807fadace0799019bb830cba0e12f64b Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Tue, 28 Dec 2010 00:49:34 -0200 Subject: Simplify inspect implementation After 304d38c0536dc32a8a1595ba34370ebf69a0d50d we don't need the new_record? check anymore. --- activerecord/lib/active_record/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f96aa8b3fb..396ab226a9 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1630,7 +1630,7 @@ MSG # Returns the contents of the record as a nicely formatted string. def inspect attributes_as_nice_string = self.class.column_names.collect { |name| - if has_attribute?(name) || new_record? + if has_attribute?(name) "#{name}: #{attribute_for_inspect(name)}" end }.compact.join(", ") -- cgit v1.2.3 From bf22b287a4720cd4e5fc4206262a09ebf490609a Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Wed, 29 Dec 2010 01:22:12 -0200 Subject: Do not use primary key on insertion when it's nil --- activerecord/lib/active_record/persistence.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 1fc9472231..b05957981d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -258,11 +258,11 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def create - if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name) + if id.nil? && connection.prefetch_primary_key?(self.class.table_name) self.id = connection.next_sequence_value(self.class.sequence_name) end - attributes_values = arel_attributes_values + attributes_values = arel_attributes_values(!id.nil?) new_id = if attributes_values.empty? self.class.unscoped.insert connection.empty_insert_statement_value -- cgit v1.2.3 From fd1cf13f743ac7ba71f19dff6d8e22f5ac7bc603 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Wed, 29 Dec 2010 16:15:45 +0000 Subject: Make serialized fixtures work again --- .../lib/active_record/connection_adapters/abstract/quoting.rb | 3 ++- activerecord/test/cases/base_test.rb | 2 +- activerecord/test/cases/fixtures_test.rb | 7 ++++++- activerecord/test/cases/quoting_test.rb | 4 ++-- activerecord/test/fixtures/traffic_lights.yml | 6 ++++++ activerecord/test/models/traffic_light.rb | 3 +++ 6 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 activerecord/test/fixtures/traffic_lights.yml create mode 100644 activerecord/test/models/traffic_light.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index a7a12faac2..7489e88eef 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -33,8 +33,9 @@ module ActiveRecord when BigDecimal then value.to_s('F') when Numeric then value.to_s when Date, Time then "'#{quoted_date(value)}'" + when Symbol then "'#{quote_string(value.to_s)}'" else - "'#{quote_string(value.to_s)}'" + "'#{quote_string(value.to_yaml)}'" end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 09ef04a656..594f3b80c6 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -703,7 +703,7 @@ class BasicsTest < ActiveRecord::TestCase duped_topic.reload # FIXME: I think this is poor behavior, and will fix it with #5686 - assert_equal({'a' => 'c'}.to_s, duped_topic.title) + assert_equal({'a' => 'c'}.to_yaml, duped_topic.title) end def test_dup_with_aggregate_of_same_name_as_attribute diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 9ce163a00f..864a7a2acc 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -13,6 +13,7 @@ require 'models/category' require 'models/parrot' require 'models/pirate' require 'models/treasure' +require 'models/traffic_light' require 'models/matey' require 'models/ship' require 'models/book' @@ -24,7 +25,7 @@ class FixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true self.use_transactional_fixtures = false - fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries + fixtures :topics, :developers, :accounts, :tasks, :categories, :funny_jokes, :binaries, :traffic_lights FIXTURES = %w( accounts binaries companies customers developers developers_projects entrants @@ -204,6 +205,10 @@ class FixturesTest < ActiveRecord::TestCase data.freeze assert_equal data, @flowers.data end + + def test_serialized_fixtures + assert_equal ["Green", "Red", "Orange"], traffic_lights(:uk).state + end end if Account.connection.respond_to?(:reset_pk_sequence!) diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 2ef5b5a800..b87fb51d97 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -154,13 +154,13 @@ module ActiveRecord end def test_crazy_object - crazy = Class.new { def to_s; 'lol' end }.new + crazy = Class.new { def to_yaml; 'lol' end }.new assert_equal "'lol'", @quoter.quote(crazy, nil) assert_equal "'lol'", @quoter.quote(crazy, Object.new) end def test_crazy_object_calls_quote_string - crazy = Class.new { def to_s; 'lo\l' end }.new + crazy = Class.new { def to_yaml; 'lo\l' end }.new assert_equal "'lo\\\\l'", @quoter.quote(crazy, nil) assert_equal "'lo\\\\l'", @quoter.quote(crazy, Object.new) end diff --git a/activerecord/test/fixtures/traffic_lights.yml b/activerecord/test/fixtures/traffic_lights.yml new file mode 100644 index 0000000000..6dabd53474 --- /dev/null +++ b/activerecord/test/fixtures/traffic_lights.yml @@ -0,0 +1,6 @@ +uk: + location: UK + state: + - Green + - Red + - Orange diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb new file mode 100644 index 0000000000..228f3f7bd4 --- /dev/null +++ b/activerecord/test/models/traffic_light.rb @@ -0,0 +1,3 @@ +class TrafficLight < ActiveRecord::Base + serialize :state, Array +end -- cgit v1.2.3 From 573fd39e22b3d278457fa764c107095808b361fe Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Thu, 30 Dec 2010 18:41:53 +0000 Subject: Make sure Model#touch doesn't try to update non existing columns --- activerecord/lib/active_record/timestamp.rb | 2 +- activerecord/test/models/task.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 2ecbd906bd..5617adea1f 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -67,7 +67,7 @@ module ActiveRecord end def timestamp_attributes_for_update_in_model - timestamp_attributes_for_update.select { |c| respond_to?(c) } + timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) } end def timestamp_attributes_for_update #:nodoc: diff --git a/activerecord/test/models/task.rb b/activerecord/test/models/task.rb index ee0282c79b..e36989dd56 100644 --- a/activerecord/test/models/task.rb +++ b/activerecord/test/models/task.rb @@ -1,3 +1,5 @@ class Task < ActiveRecord::Base - + def updated_at + ending + end end -- cgit v1.2.3 From 38fbfa6390c449dfc9a3db2975e65bd0fd665b18 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 30 Dec 2010 20:41:02 +0000 Subject: Refactor configure_dependency_for_has_many to use AssociationCollection#delete_all. It was necessary to change test_before_destroy in lifecycle_test.rb so that it checks topic.replies.size *before* doing the destroy, as afterwards it will now (correctly) be 0. --- activerecord/lib/active_record/associations.rb | 58 +++++++++----------------- activerecord/test/cases/lifecycle_test.rb | 7 ++-- 2 files changed, 24 insertions(+), 41 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b49cf8de95..17889bebfd 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1617,11 +1617,13 @@ module ActiveRecord # Active Record codebase, is meant to allow plugins to define extra # finder conditions. def configure_dependency_for_has_many(reflection, extra_conditions = nil) - if reflection.options.include?(:dependent) + if reflection.options[:dependent] + method_name = "has_many_dependent_for_#{reflection.name}" + case reflection.options[:dependent] - when :destroy - method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym - define_method(method_name) do + when :destroy, :delete_all, :nullify + define_method(method_name) do + if reflection.options[:dependent] == :destroy send(reflection.name).each do |o| # No point in executing the counter update since we're going to destroy the parent anyway counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym @@ -1633,35 +1635,23 @@ module ActiveRecord o.destroy end end - before_destroy method_name - when :delete_all - before_destroy do |record| - self.class.send(:delete_all_has_many_dependencies, - record, - reflection.name, - reflection.klass, - reflection.dependent_conditions(record, self.class, extra_conditions)) - end - when :nullify - before_destroy do |record| - self.class.send(:nullify_has_many_dependencies, - record, - reflection.name, - reflection.klass, - reflection.primary_key_name, - reflection.dependent_conditions(record, self.class, extra_conditions)) + + reflection.klass.send(:with_scope, :find => { :conditions => extra_conditions }) do + # AssociationProxy#delete_all looks at the :dependent option and acts accordingly + send(reflection.name).delete_all end - when :restrict - method_name = "has_many_dependent_restrict_for_#{reflection.name}".to_sym - define_method(method_name) do - unless send(reflection.name).empty? - raise DeleteRestrictionError.new(reflection) - end + end + when :restrict + define_method(method_name) do + unless send(reflection.name).empty? + raise DeleteRestrictionError.new(reflection) end - before_destroy method_name - else - raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})" + end + else + raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, :nullify or :restrict (#{reflection.options[:dependent].inspect})" end + + before_destroy method_name end end @@ -1724,14 +1714,6 @@ module ActiveRecord end end - def delete_all_has_many_dependencies(record, reflection_name, association_class, dependent_conditions) - association_class.delete_all(dependent_conditions) - end - - def nullify_has_many_dependencies(record, reflection_name, association_class, primary_key_name, dependent_conditions) - association_class.update_all("#{primary_key_name} = NULL", dependent_conditions) - end - mattr_accessor :valid_keys_for_has_many_association @@valid_keys_for_has_many_association = [ :class_name, :table_name, :foreign_key, :primary_key, diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index b8c3ffb9cb..0558deb71b 100644 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -101,9 +101,10 @@ class LifecycleTest < ActiveRecord::TestCase fixtures :topics, :developers, :minimalistics def test_before_destroy - original_count = Topic.count - (topic_to_be_destroyed = Topic.find(1)).destroy - assert_equal original_count - (1 + topic_to_be_destroyed.replies.size), Topic.count + topic = Topic.find(1) + assert_difference 'Topic.count', -(1 + topic.replies.size) do + topic.destroy + end end def test_auto_observer -- cgit v1.2.3 From b0bb911fa1923d809b6e4fc120cc1adc0b8dc321 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 30 Dec 2010 20:42:30 +0000 Subject: Now we can drop-kick AssociationReflection#dependent_conditions into oblivion. --- activerecord/lib/active_record/reflection.rb | 11 ----------- 1 file changed, 11 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0310e7050d..8c68a1bc00 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -298,17 +298,6 @@ module ActiveRecord !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many) end - def dependent_conditions(record, base_class, extra_conditions) - dependent_conditions = [] - dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}" - dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as] - dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions] - dependent_conditions << extra_conditions if extra_conditions - dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ") - dependent_conditions = dependent_conditions.gsub('@', '\@') - dependent_conditions - end - # Returns +true+ if +self+ is a +belongs_to+ reflection. def belongs_to? macro == :belongs_to -- cgit v1.2.3 From 6db908515919a22c475bec2dd0ddc157e7933f3d Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 30 Dec 2010 20:43:55 +0000 Subject: And owner_quoted_id can go too --- activerecord/lib/active_record/associations/association_proxy.rb | 5 ----- .../lib/active_record/associations/has_many_association.rb | 8 -------- .../lib/active_record/associations/has_one_association.rb | 9 --------- 3 files changed, 22 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 6720d83199..97eab8a793 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -282,11 +282,6 @@ module ActiveRecord end end - # Returns the ID of the owner, quoted if needed. - def owner_quoted_id - @owner.quoted_id - end - # Can be redefined by subclasses, notably polymorphic belongs_to # The record parameter is necessary to support polymorphic inverses as we must check for # the association in the specific class of the record. diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index c3360463cd..a0e2e8b479 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,14 +7,6 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) - else - @owner.quoted_id - end - end - # Returns the number of records in this collection. # # If the association has a counter cache it gets that value. Otherwise diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index c32aaf986e..04cb16b909 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -65,15 +65,6 @@ module ActiveRecord end end - protected - def owner_quoted_id - if @reflection.options[:primary_key] - @owner.class.quote_value(@owner.send(@reflection.options[:primary_key])) - else - @owner.quoted_id - end - end - private def find_target options = @reflection.options.dup.slice(:select, :order, :include, :readonly) -- cgit v1.2.3 From f7a15d0e03dc173ab5f88773a97a3bf26d7b957e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 30 Dec 2010 20:47:31 +0000 Subject: Get rid of extra_conditions param from configure_dependency_for_has_many. I can't see a particularly plausible argument for this being used by plugins, and if they really want they can just redefine the callback or whatever. Note also that before my recent commit the extra_conditions param was completely ignored for :dependent => :destroy. --- activerecord/lib/active_record/associations.rb | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 17889bebfd..b82651ebce 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1612,11 +1612,7 @@ module ActiveRecord # - set the foreign key to NULL if the option is set to :nullify # - do not delete the parent record if there is any child record if the # option is set to :restrict - # - # The +extra_conditions+ parameter, which is not used within the main - # Active Record codebase, is meant to allow plugins to define extra - # finder conditions. - def configure_dependency_for_has_many(reflection, extra_conditions = nil) + def configure_dependency_for_has_many(reflection) if reflection.options[:dependent] method_name = "has_many_dependent_for_#{reflection.name}" @@ -1636,10 +1632,8 @@ module ActiveRecord end end - reflection.klass.send(:with_scope, :find => { :conditions => extra_conditions }) do - # AssociationProxy#delete_all looks at the :dependent option and acts accordingly - send(reflection.name).delete_all - end + # AssociationProxy#delete_all looks at the :dependent option and acts accordingly + send(reflection.name).delete_all end when :restrict define_method(method_name) do -- cgit v1.2.3 From 2bf31868033c50d71d6d68c1ddad67147908adc4 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 30 Dec 2010 21:44:29 +0000 Subject: Verify that when has_many associated objects are destroyed via :dependent => :destroy, when the parent is destroyed, the callbacks are run --- activerecord/test/cases/associations/callbacks_test.rb | 9 +++++++++ activerecord/test/models/company.rb | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 6a30e2905b..2d0d4541b4 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/author' require 'models/project' require 'models/developer' +require 'models/company' class AssociationCallbacksTest < ActiveRecord::TestCase fixtures :posts, :authors, :projects, :developers @@ -81,6 +82,14 @@ class AssociationCallbacksTest < ActiveRecord::TestCase assert_equal callback_log, jack.post_log end + def test_has_many_callbacks_for_destroy_on_parent + firm = Firm.create! :name => "Firm" + client = firm.clients.create! :name => "Client" + firm.destroy + + assert_equal ["before_remove#{client.id}", "after_remove#{client.id}"], firm.log + end + def test_has_and_belongs_to_many_add_callback david = developers(:david) ar = projects(:active_record) diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index ee5f77b613..7af4dfe2c9 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -38,7 +38,9 @@ end class Firm < Company has_many :clients, :order => "id", :dependent => :destroy, :counter_sql => "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " + - "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )" + "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )", + :before_remove => :log_before_remove, + :after_remove => :log_after_remove has_many :unsorted_clients, :class_name => "Client" has_many :clients_sorted_desc, :class_name => "Client", :order => "id DESC" has_many :clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id" @@ -88,6 +90,19 @@ class Firm < Company has_one :unautosaved_account, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false has_many :accounts has_many :unautosaved_accounts, :foreign_key => "firm_id", :class_name => 'Account', :autosave => false + + def log + @log ||= [] + end + + private + def log_before_remove(record) + log << "before_remove#{record.id}" + end + + def log_after_remove(record) + log << "after_remove#{record.id}" + end end class DependentFirm < Company -- cgit v1.2.3 From 62b084f80759300f10a4e5c4235bf1d13693a7d3 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 31 Dec 2010 10:43:42 +0000 Subject: Specify the STI type condition using SQL IN rather than a whole load of ORs. Required a fix to ActiveRecord::Relation#merge for properly merging create_with_value. This also fixes a situation where the type condition was appearing twice in the resultant SQL query. --- .../lib/active_record/associations/association_collection.rb | 2 +- activerecord/lib/active_record/base.rb | 12 ++++++++---- activerecord/lib/active_record/relation/spawn_methods.rb | 6 +++++- activerecord/test/cases/relation_scoping_test.rb | 7 +++++++ 4 files changed, 21 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 108e316672..64e10f56e2 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -488,7 +488,7 @@ module ActiveRecord ensure_owner_is_persisted! transaction do - with_scope(:create => @scope[:create].merge(scoped.where_values_hash)) do + with_scope(:create => @scope[:create].merge(scoped.scope_for_create)) do build_record(attrs, &block) end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 396ab226a9..0a2e436ecc 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -874,7 +874,12 @@ module ActiveRecord #:nodoc: def relation #:nodoc: @relation ||= Relation.new(self, arel_table) - finder_needs_type_condition? ? @relation.where(type_condition) : @relation + + if finder_needs_type_condition? + @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + else + @relation + end end # Finder methods must instantiate through this method to work with the @@ -914,10 +919,9 @@ module ActiveRecord #:nodoc: def type_condition sti_column = arel_table[inheritance_column.to_sym] - condition = sti_column.eq(sti_name) - descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) } + sti_names = ([self] + descendants).map { |model| model.sti_name } - condition + sti_column.in(sti_names) end # Guesses the table name, but does not decorate it with prefix and suffix information. diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 5acf3ec83a..e1f7fa2949 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -46,13 +46,17 @@ module ActiveRecord merged_relation.where_values = merged_wheres - (Relation::SINGLE_VALUE_METHODS - [:lock]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :create_with]).each do |method| value = r.send(:"#{method}_value") merged_relation.send(:"#{method}_value=", value) unless value.nil? end merged_relation.lock_value = r.lock_value unless merged_relation.lock_value + if r.create_with_value + merged_relation.create_with_value = (merged_relation.create_with_value || {}).merge(r.create_with_value) + end + # Apply scope extension modules merged_relation.send :apply_modules, r.extensions diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index e8672515fc..7c6899d438 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -485,4 +485,11 @@ class DefaultScopingTest < ActiveRecord::TestCase posts_offset_limit = Post.offset(2).limit(3) assert_equal posts_limit_offset, posts_offset_limit end + + def test_create_with_merge + aaron = (PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20) & + PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new + assert_equal 20, aaron.salary + assert_equal 'Aaron', aaron.name + end end -- cgit v1.2.3 From bea4065d3c8c8f845ddda45b3ec98e3fb308d913 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 31 Dec 2010 18:08:44 +0000 Subject: Refactor BelongsToAssociation to allow BelongsToPolymorphicAssociation to inherit from it --- .../associations/association_collection.rb | 4 - .../associations/association_proxy.rb | 31 ++++-- .../associations/belongs_to_association.rb | 104 +++++++++++---------- .../belongs_to_polymorphic_association.rb | 64 +++---------- .../associations/through_association.rb | 2 - .../associations/belongs_to_associations_test.rb | 42 +++++++-- 6 files changed, 127 insertions(+), 120 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 64e10f56e2..0959ea2c5a 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -31,10 +31,6 @@ module ActiveRecord end end - def scoped - with_scope(@scope) { @reflection.klass.scoped } - end - def find(*args) options = args.extract_options! diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 97eab8a793..7e68241a2c 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -8,7 +8,7 @@ module ActiveRecord # # AssociationProxy # BelongsToAssociation - # BelongsToPolymorphicAssociation + # BelongsToPolymorphicAssociation # AssociationCollection + HasAssociation # HasAndBelongsToManyAssociation # HasManyAssociation @@ -116,6 +116,7 @@ module ActiveRecord # Reloads the \target and returns +self+ on success. def reload reset + construct_scope load_target self unless @target.nil? end @@ -166,6 +167,10 @@ module ActiveRecord end end + def scoped + with_scope(@scope) { target_klass.scoped } + end + protected def interpolate_sql(sql, record = nil) @owner.send(:interpolate_sql, sql, record) @@ -192,15 +197,19 @@ module ActiveRecord # Forwards +with_scope+ to the reflection. def with_scope(*args, &block) - @reflection.klass.send :with_scope, *args, &block + target_klass.send :with_scope, *args, &block end # Construct the scope used for find/create queries on the target def construct_scope - @scope = { - :find => construct_find_scope, - :create => construct_create_scope - } + if target_klass + @scope = { + :find => construct_find_scope, + :create => construct_create_scope + } + else + @scope = nil + end end # Implemented by subclasses @@ -214,7 +223,7 @@ module ActiveRecord end def aliased_table - @reflection.klass.arel_table + target_klass.arel_table end # Set the inverse association, if possible @@ -224,6 +233,12 @@ module ActiveRecord end end + # This class of the target. belongs_to polymorphic overrides this to look at the + # polymorphic_type field on the owner. + def target_klass + @reflection.klass + end + private # Forwards any missing method call to the \target. def method_missing(method, *args) @@ -254,7 +269,7 @@ module ActiveRecord def load_target return nil unless defined?(@loaded) - if !loaded? && (!@owner.new_record? || foreign_key_present) + if !loaded? && (!@owner.new_record? || foreign_key_present) && @scope @target = find_target end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 98c1c13524..6b29e3ef92 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -11,29 +11,16 @@ module ActiveRecord end def replace(record) - counter_cache_name = @reflection.counter_cache_column - - if record.nil? - if counter_cache_name && @owner.persisted? - @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name] - end - - @target = @owner[@reflection.primary_key_name] = nil - else - raise_on_type_mismatch(record) - - if counter_cache_name && @owner.persisted? && record.id != @owner[@reflection.primary_key_name] - @reflection.klass.increment_counter(counter_cache_name, record.id) - @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name] - end - - @target = (AssociationProxy === record ? record.target : record) - @owner[@reflection.primary_key_name] = record_id(record) if record.persisted? - @updated = true - end + record = record.target if AssociationProxy === record + raise_on_type_mismatch(record) unless record.nil? + update_counters(record) + replace_keys(record) set_inverse_instance(record) + @target = record + @updated = true if record + loaded record end @@ -44,8 +31,8 @@ module ActiveRecord def stale_target? if @target && @target.persisted? - target_id = @target.send(@reflection.association_primary_key).to_s - foreign_key = @owner.send(@reflection.primary_key_name).to_s + target_id = @target[@reflection.association_primary_key].to_s + foreign_key = @owner[@reflection.primary_key_name].to_s target_id != foreign_key else @@ -54,27 +41,51 @@ module ActiveRecord end private + def update_counters(record) + counter_cache_name = @reflection.counter_cache_column + + if counter_cache_name && @owner.persisted? && different_target?(record) + if record + target_klass.increment_counter(counter_cache_name, record.id) + end + + if foreign_key_present + target_klass.decrement_counter(counter_cache_name, target_id) + end + end + end + + # Checks whether record is different to the current target, without loading it + def different_target?(record) + record.nil? && @owner[@reflection.primary_key_name] || + record.id != @owner[@reflection.primary_key_name] + end + + def replace_keys(record) + @owner[@reflection.primary_key_name] = record && record[@reflection.association_primary_key] + end + def find_target - find_method = if @reflection.options[:primary_key] - "find_by_#{@reflection.options[:primary_key]}" - else - "find" - end - - options = @reflection.options.dup.slice(:select, :include, :readonly) - - the_target = with_scope(:find => @scope[:find]) do - @reflection.klass.send(find_method, - @owner[@reflection.primary_key_name], - options - ) if @owner[@reflection.primary_key_name] + if foreign_key_present + scoped.first.tap { |record| set_inverse_instance(record) } end - set_inverse_instance(the_target) - the_target end def construct_find_scope - { :conditions => conditions } + { + :conditions => construct_conditions, + :select => @reflection.options[:select], + :include => @reflection.options[:include], + :readonly => @reflection.options[:readonly] + } + end + + def construct_conditions + conditions = aliased_table[@reflection.association_primary_key]. + eq(@owner[@reflection.primary_key_name]) + + conditions = conditions.and(Arel.sql(sql_conditions)) if sql_conditions + conditions end def foreign_key_present @@ -88,17 +99,12 @@ module ActiveRecord inverse && inverse.macro == :has_one end - def record_id(record) - record.send(@reflection.options[:primary_key] || :id) - end - - def previous_record_id - @previous_record_id ||= if @reflection.options[:primary_key] - previous_record = @owner.send(@reflection.name) - previous_record.nil? ? nil : previous_record.id - else - @owner[@reflection.primary_key_name] - end + def target_id + if @reflection.options[:primary_key] + @owner.send(@reflection.name).try(:id) + else + @owner[@reflection.primary_key_name] + end end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 90eff7399c..6293c4ca42 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -1,28 +1,7 @@ module ActiveRecord # = Active Record Belongs To Polymorphic Association module Associations - class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc: - def replace(record) - if record.nil? - @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil - else - @target = (AssociationProxy === record ? record.target : record) - - @owner[@reflection.primary_key_name] = record_id(record) - @owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s - - @updated = true - end - - set_inverse_instance(record) - loaded - record - end - - def updated? - @updated - end - + class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: def stale_target? if @target && @target.persisted? target_id = @target.send(@reflection.association_primary_key).to_s @@ -38,43 +17,26 @@ module ActiveRecord private - def inverse_reflection_for(record) - @reflection.polymorphic_inverse_of(record.class) + def replace_keys(record) + super + @owner[@reflection.options[:foreign_type]] = record && record.class.base_class.name end - def invertible_for?(record) - inverse = inverse_reflection_for(record) - inverse && inverse.macro == :has_one + def different_target?(record) + super || record.class != target_klass end - def construct_find_scope - { :conditions => conditions } - end - - def find_target - return nil if association_class.nil? - - target = association_class.send(:with_scope, :find => @scope[:find]) do - association_class.find( - @owner[@reflection.primary_key_name], - :select => @reflection.options[:select], - :include => @reflection.options[:include] - ) - end - set_inverse_instance(target) - target - end - - def foreign_key_present - !@owner[@reflection.primary_key_name].nil? + def inverse_reflection_for(record) + @reflection.polymorphic_inverse_of(record.class) end - def record_id(record) - record.send(@reflection.options[:primary_key] || :id) + def target_klass + type = @owner[@reflection.options[:foreign_type]] + type && type.constantize end - def association_class - @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil + def raise_on_type_mismatch(record) + # A polymorphic association cannot have a type mismatch, by definition end end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 57718285f8..65ca4b1282 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -158,8 +158,6 @@ module ActiveRecord alias_method :sql_conditions, :conditions def update_stale_state - construct_scope if stale_target? - if @reflection.through_reflection.macro == :belongs_to @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name) end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index cdde9a80d3..6dcbbc7e34 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -198,16 +198,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Post.find(p.id).comments.size end - def test_belongs_to_with_primary_key_counter_with_assigning_nil - debate = Topic.create("title" => "debate") - reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate") + def test_belongs_to_with_primary_key_counter + debate = Topic.create("title" => "debate") + debate2 = Topic.create("title" => "debate2") + reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate") + + assert_equal 1, debate.reload.replies_count + assert_equal 0, debate2.reload.replies_count + + reply.topic_with_primary_key = debate2 - assert_equal debate.title, reply.parent_title - assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count") + assert_equal 0, debate.reload.replies_count + assert_equal 1, debate2.reload.replies_count reply.topic_with_primary_key = nil - assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count") + assert_equal 0, debate.reload.replies_count + assert_equal 0, debate2.reload.replies_count end def test_belongs_to_counter_with_reassigning @@ -419,6 +426,18 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_nil sponsor.sponsorable_id end + def test_assignment_updates_foreign_id_field_for_new_and_saved_records + client = Client.new + saved_firm = Firm.create :name => "Saved" + new_firm = Firm.new + + client.firm = saved_firm + assert_equal saved_firm.id, client.client_of + + client.firm = new_firm + assert_nil client.client_of + end + def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records essay = Essay.new saved_writer = Author.create(:name => "David") @@ -537,4 +556,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert proxy.stale_target? assert_equal companies(:first_firm), sponsor.sponsorable end + + def test_reloading_association_with_key_change + client = companies(:second_client) + firm = client.firm # note this is a proxy object + + client.firm = companies(:another_firm) + assert_equal companies(:another_firm), firm.reload + + client.client_of = companies(:first_firm).id + assert_equal companies(:first_firm), firm.reload + end end -- cgit v1.2.3 From 3c400627eb9cfac380d716ccf1182d61db4a45a6 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 31 Dec 2010 18:36:02 +0000 Subject: Support for :counter_cache on polymorphic belongs_to --- .../lib/active_record/associations/belongs_to_association.rb | 2 +- .../test/cases/associations/belongs_to_associations_test.rb | 12 ++++++++++++ activerecord/test/schema/schema.rb | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 6b29e3ef92..e4758f4369 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -46,7 +46,7 @@ module ActiveRecord if counter_cache_name && @owner.persisted? && different_target?(record) if record - target_klass.increment_counter(counter_cache_name, record.id) + record.class.increment_counter(counter_cache_name, record.id) end if foreign_key_present diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 6dcbbc7e34..f97f89b6fe 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -567,4 +567,16 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client.client_of = companies(:first_firm).id assert_equal companies(:first_firm), firm.reload end + + def test_polymorphic_counter_cache + tagging = taggings(:welcome_general) + post = posts(:welcome) + comment = comments(:greetings) + + assert_difference 'post.reload.taggings_count', -1 do + assert_difference 'comment.reload.taggings_count', +1 do + tagging.taggable = comment + end + end + end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 3dea7e1492..7f366b2c91 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -143,6 +143,7 @@ ActiveRecord::Schema.define do t.text :body, :null => false end t.string :type + t.integer :taggings_count, :default => 0 end create_table :companies, :force => true do |t| -- cgit v1.2.3 From 12675988813e82ac30f7c0e0008c12c4cf5d8cdc Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 31 Dec 2010 20:00:24 +0000 Subject: Rename AssociationReflection#primary_key_name to foreign_key, since the options key which it relates to is :foreign_key --- .../lib/active_record/association_preload.rb | 22 +++++++++++----------- activerecord/lib/active_record/associations.rb | 4 ++-- .../associations/belongs_to_association.rb | 14 +++++++------- .../belongs_to_polymorphic_association.rb | 2 +- .../join_dependency/join_association.rb | 8 ++++---- .../has_and_belongs_to_many_association.rb | 4 ++-- .../active_record/associations/has_association.rb | 4 ++-- .../associations/has_many_association.rb | 2 +- .../associations/has_many_through_association.rb | 2 +- .../associations/has_one_association.rb | 6 +++--- .../associations/through_association.rb | 10 +++++----- .../lib/active_record/autosave_association.rb | 6 +++--- activerecord/lib/active_record/fixtures.rb | 2 +- activerecord/lib/active_record/reflection.rb | 14 +++++++------- .../lib/active_record/relation/calculations.rb | 2 +- activerecord/test/cases/reflection_test.rb | 8 +++++--- 16 files changed, 56 insertions(+), 54 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index ecf7b6c210..638897a86b 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -205,7 +205,7 @@ module ActiveRecord # FIXME: options[:select] is always nil in the tests. Do we really # need it? options[:select] || left[Arel.star], - right[reflection.primary_key_name].as( + right[reflection.foreign_key].as( Arel.sql('the_parent_record_id')) ] @@ -220,7 +220,7 @@ module ActiveRecord all_associated_records = associated_records(ids) do |some_ids| method = in_or_equal(some_ids) - conditions = right[reflection.primary_key_name].send(*method) + conditions = right[reflection.foreign_key].send(*method) conditions = custom_conditions.inject(conditions) do |ast, cond| ast.and cond end @@ -241,7 +241,7 @@ module ActiveRecord unless through_records.empty? through_reflection = reflections[options[:through]] - through_primary_key = through_reflection.primary_key_name + through_primary_key = through_reflection.foreign_key source = reflection.source_reflection.name through_records.first.class.preload_associations(through_records, source) if through_reflection.macro == :belongs_to @@ -255,7 +255,7 @@ module ActiveRecord end end else - set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) + set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.foreign_key) end end @@ -263,8 +263,8 @@ module ActiveRecord return if records.first.send(reflection.name).loaded? options = reflection.options - primary_key_name = reflection.through_reflection_primary_key_name - id_to_record_map, ids = construct_id_map(records, primary_key_name || reflection.options[:primary_key]) + foreign_key = reflection.through_reflection_foreign_key + id_to_record_map, ids = construct_id_map(records, foreign_key || reflection.options[:primary_key]) records.each {|record| record.send(reflection.name).loaded} if options[:through] @@ -281,7 +281,7 @@ module ActiveRecord else set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), - reflection.primary_key_name) + reflection.foreign_key) end end @@ -319,7 +319,7 @@ module ActiveRecord def preload_belongs_to_association(records, reflection, preload_options={}) return if records.first.send("loaded_#{reflection.name}?") options = reflection.options - primary_key_name = reflection.primary_key_name + foreign_key = reflection.foreign_key klasses_and_ids = {} @@ -330,7 +330,7 @@ module ActiveRecord # to their parent_records records.each do |record| if klass = record.send(polymorph_type) - klass_id = record.send(primary_key_name) + klass_id = record.send(foreign_key) if klass_id id_map = klasses_and_ids[klass.constantize] ||= {} (id_map[klass_id.to_s] ||= []) << record @@ -339,7 +339,7 @@ module ActiveRecord end else id_map = records.group_by do |record| - key = record.send(primary_key_name) + key = record.send(foreign_key) key && key.to_s end id_map.delete nil @@ -369,7 +369,7 @@ module ActiveRecord conditions = [] - key = reflection.primary_key_name + key = reflection.foreign_key if interface = reflection.options[:as] key = "#{interface}_id" diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b82651ebce..9a4f6d4dfd 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1670,7 +1670,7 @@ module ActiveRecord class_eval <<-eoruby, __FILE__, __LINE__ + 1 def #{method_name} association = #{reflection.name} - association.update_attribute(#{reflection.primary_key_name.inspect}, nil) if association + association.update_attribute(#{reflection.foreign_key.inspect}, nil) if association end eoruby when :restrict @@ -1782,7 +1782,7 @@ module ActiveRecord reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) - if reflection.association_foreign_key == reflection.primary_key_name + if reflection.association_foreign_key == reflection.foreign_key raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index e4758f4369..63eb38ab34 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -32,7 +32,7 @@ module ActiveRecord def stale_target? if @target && @target.persisted? target_id = @target[@reflection.association_primary_key].to_s - foreign_key = @owner[@reflection.primary_key_name].to_s + foreign_key = @owner[@reflection.foreign_key].to_s target_id != foreign_key else @@ -57,12 +57,12 @@ module ActiveRecord # Checks whether record is different to the current target, without loading it def different_target?(record) - record.nil? && @owner[@reflection.primary_key_name] || - record.id != @owner[@reflection.primary_key_name] + record.nil? && @owner[@reflection.foreign_key] || + record.id != @owner[@reflection.foreign_key] end def replace_keys(record) - @owner[@reflection.primary_key_name] = record && record[@reflection.association_primary_key] + @owner[@reflection.foreign_key] = record && record[@reflection.association_primary_key] end def find_target @@ -82,14 +82,14 @@ module ActiveRecord def construct_conditions conditions = aliased_table[@reflection.association_primary_key]. - eq(@owner[@reflection.primary_key_name]) + eq(@owner[@reflection.foreign_key]) conditions = conditions.and(Arel.sql(sql_conditions)) if sql_conditions conditions end def foreign_key_present - !@owner[@reflection.primary_key_name].nil? + !@owner[@reflection.foreign_key].nil? end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto @@ -103,7 +103,7 @@ module ActiveRecord if @reflection.options[:primary_key] @owner.send(@reflection.name).try(:id) else - @owner[@reflection.primary_key_name] + @owner[@reflection.foreign_key] end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 6293c4ca42..46adc048b8 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -5,7 +5,7 @@ module ActiveRecord def stale_target? if @target && @target.persisted? target_id = @target.send(@reflection.association_primary_key).to_s - foreign_key = @owner.send(@reflection.primary_key_name).to_s + foreign_key = @owner.send(@reflection.foreign_key).to_s target_type = @target.class.base_class.name foreign_type = @owner.send(@reflection.options[:foreign_type]).to_s diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb index 02707dfae1..1e5149d80f 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -193,11 +193,11 @@ module ActiveRecord first_key = second_key = nil if through_reflection.macro == :belongs_to - jt_primary_key = through_reflection.primary_key_name + jt_primary_key = through_reflection.foreign_key jt_foreign_key = through_reflection.association_primary_key else jt_primary_key = through_reflection.active_record_primary_key - jt_foreign_key = through_reflection.primary_key_name + jt_foreign_key = through_reflection.foreign_key if through_reflection.options[:as] # has_many :through against a polymorphic join jt_conditions << @@ -231,7 +231,7 @@ module ActiveRecord join_table[reflection.source_reflection.options[:foreign_type]]. eq(reflection.options[:source_type]) else - second_key = source_reflection.primary_key_name + second_key = source_reflection.foreign_key end end @@ -262,7 +262,7 @@ module ActiveRecord end def join_belongs_to_to(relation) - foreign_key = options[:foreign_key] || reflection.primary_key_name + foreign_key = options[:foreign_key] || reflection.foreign_key primary_key = options[:primary_key] || reflection.klass.primary_key join_target_table( diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index b1d454545f..5336b6cc28 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -40,7 +40,7 @@ module ActiveRecord attributes = columns.map do |column| name = column.name value = case name.to_s - when @reflection.primary_key_name.to_s + when @reflection.foreign_key.to_s @owner.id when @reflection.association_foreign_key.to_s record.id @@ -64,7 +64,7 @@ module ActiveRecord records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } else relation = Arel::Table.new(@reflection.options[:join_table]) - stmt = relation.where(relation[@reflection.primary_key_name].eq(@owner.id). + stmt = relation.where(relation[@reflection.foreign_key].eq(@owner.id). and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) ).compile_delete @owner.connection.delete stmt.to_sql diff --git a/activerecord/lib/active_record/associations/has_association.rb b/activerecord/lib/active_record/associations/has_association.rb index 0ecdb696ea..190fed77c2 100644 --- a/activerecord/lib/active_record/associations/has_association.rb +++ b/activerecord/lib/active_record/associations/has_association.rb @@ -14,9 +14,9 @@ module ActiveRecord def construct_owner_attributes(reflection = @reflection) attributes = {} if reflection.macro == :belongs_to - attributes[reflection.association_primary_key] = @owner.send(reflection.primary_key_name) + attributes[reflection.association_primary_key] = @owner.send(reflection.foreign_key) else - attributes[reflection.primary_key_name] = @owner.send(reflection.active_record_primary_key) + attributes[reflection.foreign_key] = @owner.send(reflection.active_record_primary_key) if reflection.options[:as] attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index a0e2e8b479..0d044b28e4 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -58,7 +58,7 @@ module ActiveRecord when :delete_all @reflection.klass.delete(records.map { |r| r.id }) else - updates = { @reflection.primary_key_name => nil } + updates = { @reflection.foreign_key => nil } conditions = { @reflection.association_primary_key => records.map { |r| r.id } } with_scope(@scope) do 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 e2b008034e..2348ee099c 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -31,7 +31,7 @@ module ActiveRecord protected def target_reflection_has_associated_record? - if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank? + if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank? false else true diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 04cb16b909..12ccb3af8a 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -38,11 +38,11 @@ module ActiveRecord @target.destroy if @target.persisted? @owner.clear_association_cache when :nullify - @target[@reflection.primary_key_name] = nil + @target[@reflection.foreign_key] = nil @target.save if @owner.persisted? && @target.persisted? end else - @target[@reflection.primary_key_name] = nil + @target[@reflection.foreign_key] = nil @target.save if @owner.persisted? && @target.persisted? end end @@ -96,7 +96,7 @@ module ActiveRecord if replace_existing replace(record, true) else - record[@reflection.primary_key_name] = @owner.id if @owner.persisted? + record[@reflection.foreign_key] = @owner.id if @owner.persisted? self.target = record set_inverse_instance(record) end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 65ca4b1282..c78f5d969f 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -13,7 +13,7 @@ module ActiveRecord def stale_target? if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key) previous_key = @through_foreign_key.to_s - current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s + current_key = @owner.send(@reflection.through_reflection.foreign_key).to_s previous_key != current_key else @@ -69,14 +69,14 @@ module ActiveRecord if @reflection.source_reflection.macro == :belongs_to reflection_primary_key = @reflection.source_reflection.options[:primary_key] || @reflection.klass.primary_key - source_primary_key = @reflection.source_reflection.primary_key_name + source_primary_key = @reflection.source_reflection.foreign_key if @reflection.options[:source_type] column = @reflection.source_reflection.options[:foreign_type] conditions << right[column].eq(@reflection.options[:source_type]) end else - reflection_primary_key = @reflection.source_reflection.primary_key_name + reflection_primary_key = @reflection.source_reflection.foreign_key source_primary_key = @reflection.source_reflection.options[:primary_key] || @reflection.through_reflection.klass.primary_key if @reflection.source_reflection.options[:as] @@ -100,7 +100,7 @@ module ActiveRecord raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) join_attributes = { - @reflection.source_reflection.primary_key_name => + @reflection.source_reflection.foreign_key => associate.send(@reflection.source_reflection.association_primary_key) } @@ -159,7 +159,7 @@ module ActiveRecord def update_stale_state if @reflection.through_reflection.macro == :belongs_to - @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name) + @through_foreign_key = @owner.send(@reflection.through_reflection.foreign_key) end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 4a18719551..c6c1dd8b87 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -343,8 +343,8 @@ module ActiveRecord association.destroy else key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave) - association[reflection.primary_key_name] = key + if autosave != false && (new_record? || association.new_record? || association[reflection.foreign_key] != key || autosave) + association[reflection.foreign_key] = key saved = association.save(:validate => !autosave) raise ActiveRecord::Rollback if !saved && autosave saved @@ -367,7 +367,7 @@ module ActiveRecord if association.updated? association_id = association.send(reflection.options[:primary_key] || :id) - self[reflection.primary_key_name] = association_id + self[reflection.foreign_key] = association_id end saved if autosave diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 6fb723f2f5..3ddd6687b9 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -634,7 +634,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) targets.each do |target| join_fixtures["#{label}_#{target}"] = Fixture.new( - { association.primary_key_name => row[primary_key_name], + { association.foreign_key => row[primary_key_name], association.association_foreign_key => Fixtures.identify(target) }, nil, @connection) end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8c68a1bc00..81a95d4971 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -196,8 +196,8 @@ module ActiveRecord @quoted_table_name ||= klass.quoted_table_name end - def primary_key_name - @primary_key_name ||= options[:foreign_key] || derive_primary_key_name + def foreign_key + @foreign_key ||= options[:foreign_key] || derive_foreign_key end def primary_key_column @@ -251,7 +251,7 @@ module ActiveRecord false end - def through_reflection_primary_key_name + def through_reflection_foreign_key end def source_reflection @@ -310,7 +310,7 @@ module ActiveRecord class_name end - def derive_primary_key_name + def derive_foreign_key if belongs_to? "#{name}_id" elsif options[:as] @@ -392,11 +392,11 @@ module ActiveRecord end def through_reflection_primary_key - through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name + through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.foreign_key end - def through_reflection_primary_key_name - through_reflection.primary_key_name if through_reflection.belongs_to? + def through_reflection_foreign_key + through_reflection.foreign_key if through_reflection.belongs_to? end private diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index fd45bb24dd..139752b190 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -211,7 +211,7 @@ module ActiveRecord group_attr = @group_values association = @klass.reflect_on_association(group_attr.first.to_sym) associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations - group_fields = Array(associated ? association.primary_key_name : group_attr) + group_fields = Array(associated ? association.foreign_key : group_attr) group_aliases = group_fields.map { |field| column_alias_for(field) } group_columns = group_aliases.zip(group_fields).map { |aliaz,field| [aliaz, column_for(field)] diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 901c12b26c..e3db34520e 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -8,6 +8,8 @@ require 'models/ship' require 'models/pirate' require 'models/price_estimate' require 'models/tagging' +require 'models/author' +require 'models/post' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -127,11 +129,11 @@ class ReflectionTest < ActiveRecord::TestCase def test_belongs_to_inferred_foreign_key_from_assoc_name Company.belongs_to :foo - assert_equal "foo_id", Company.reflect_on_association(:foo).primary_key_name + assert_equal "foo_id", Company.reflect_on_association(:foo).foreign_key Company.belongs_to :bar, :class_name => "Xyzzy" - assert_equal "bar_id", Company.reflect_on_association(:bar).primary_key_name + assert_equal "bar_id", Company.reflect_on_association(:bar).foreign_key Company.belongs_to :baz, :class_name => "Xyzzy", :foreign_key => "xyzzy_id" - assert_equal "xyzzy_id", Company.reflect_on_association(:baz).primary_key_name + assert_equal "xyzzy_id", Company.reflect_on_association(:baz).foreign_key end def test_association_reflection_in_modules -- cgit v1.2.3 From 1f8ecb85d7c1b3efdf45c3cf3461502b608c1a7c Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Sun, 2 Jan 2011 03:35:38 +0700 Subject: Update CHANGELOGs to include 3.0.3 changes --- activerecord/CHANGELOG | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 8ebc87145c..da4bd4f1cb 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -103,7 +103,13 @@ IrreversibleMigration exception will be raised when going down. [Aaron Patterson] -*Rails 3.0.2 (unreleased)* + +*Rails 3.0.3 (November 16, 2010)* + +* Support find by class like this: Post.where(:name => Post) + + +*Rails 3.0.2 (November 15, 2010)* * reorder is deprecated in favor of except(:order).order(...) [Santiago Pastorino] @@ -132,10 +138,12 @@ IrreversibleMigration exception will be raised when going down. [Aaron Patterson] + *Rails 3.0.1 (October 15, 2010)* * Introduce a fix for CVE-2010-3993 + *Rails 3.0.0 (August 29, 2010)* * Changed update_attribute to not run callbacks and update the record directly in the database [Neeraj Singh] -- cgit v1.2.3 From 99424eb0996d41eaccb1b140cc30820fb5779fef Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Sun, 2 Jan 2011 00:20:59 +0100 Subject: Revert "Update CHANGELOGs to include 3.0.3 changes" Reason: Sorry, CHANGELOGs can only be edited in master. If you provide a patch I'll apply it myself. Thanks! This reverts commit 1f8ecb85d7c1b3efdf45c3cf3461502b608c1a7c. --- activerecord/CHANGELOG | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index da4bd4f1cb..8ebc87145c 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -103,13 +103,7 @@ IrreversibleMigration exception will be raised when going down. [Aaron Patterson] - -*Rails 3.0.3 (November 16, 2010)* - -* Support find by class like this: Post.where(:name => Post) - - -*Rails 3.0.2 (November 15, 2010)* +*Rails 3.0.2 (unreleased)* * reorder is deprecated in favor of except(:order).order(...) [Santiago Pastorino] @@ -138,12 +132,10 @@ IrreversibleMigration exception will be raised when going down. [Aaron Patterson] - *Rails 3.0.1 (October 15, 2010)* * Introduce a fix for CVE-2010-3993 - *Rails 3.0.0 (August 29, 2010)* * Changed update_attribute to not run callbacks and update the record directly in the database [Neeraj Singh] -- cgit v1.2.3 From febdf5a5a91ac04fad2c7996f7f55fa19f0ff7d7 Mon Sep 17 00:00:00 2001 From: "Robert Pankowecki (Gavdi)" Date: Fri, 31 Dec 2010 20:48:48 +0800 Subject: Added one more failing test for bug #6036 --- activerecord/test/cases/relations_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 20bfafbc5e..ed6ea4db0a 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -27,6 +27,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal van.id, Minivan.where(:minivan_id => van).to_a.first.minivan_id end + def test_do_not_double_quote_string_id_with_array + van = Minivan.last + assert van + assert_equal van, Minivan.where(:minivan_id => [van]).to_a.first + end + def test_bind_values relation = Post.scoped assert_equal [], relation.bind_values -- cgit v1.2.3 From b9ce3419e5a9bdc4ade20f21a6c93e76a3b69caf Mon Sep 17 00:00:00 2001 From: "Robert Pankowecki (Gavdi)" Date: Fri, 31 Dec 2010 21:17:07 +0800 Subject: User id instead of quoted_id to prevent double quoting. Fixes failing test for bug #6036. --- activerecord/lib/active_record/relation/predicate_builder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index b3f1b9e36a..61d9974570 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -20,7 +20,7 @@ module ActiveRecord case value when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation values = value.to_a.map { |x| - x.respond_to?(:quoted_id) ? x.quoted_id : x + x.is_a?(ActiveRecord::Base) ? x.id : x } attribute.in(values) when Range, Arel::Relation -- cgit v1.2.3 From 60cf65def805995bcca184c40b44bb01d86a48aa Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 3 Jan 2011 15:12:51 -0800 Subject: herp derpricating add_limit_offset! --- .../connection_adapters/abstract/database_statements.rb | 3 +++ .../lib/active_record/connection_adapters/mysql_adapter.rb | 1 + activerecord/test/cases/adapter_test.rb | 12 ------------ 3 files changed, 4 insertions(+), 12 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index ee9a0af35c..3f15af561b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -229,6 +229,8 @@ module ActiveRecord # # This method *modifies* the +sql+ parameter. # + # This method is deprecated!! Stop using it! + # # ===== Examples # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50}) # generates @@ -243,6 +245,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! def default_sequence_name(table, column) nil diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index ce2352486b..43de9c2090 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -439,6 +439,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! # SCHEMA STATEMENTS ======================================== diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 646aa88d80..49b2e945c3 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -141,16 +141,4 @@ class AdapterTest < ActiveRecord::TestCase end end end - - def test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas - sql_inject = "1 select * from schema" - assert_no_match(/schema/, @connection.add_limit_offset!("", :limit=>sql_inject)) - assert_no_match(/schema/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)) - end - - def test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas - sql_inject = "1, 7 procedure help()" - assert_no_match(/procedure/, @connection.add_limit_offset!("", :limit=>sql_inject)) - assert_no_match(/procedure/, @connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7)) - end end -- cgit v1.2.3 From 16065b4f19b77111b7fec343969bcf98635e7e27 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 1 Jan 2011 18:18:54 +0000 Subject: Some basic tests for the :foreign_type option on belongs_to, which was previously completely untested. --- .../associations/belongs_to_associations_test.rb | 19 +++++++++++++++++++ activerecord/test/cases/associations/eager_test.rb | 13 ++++++++++++- activerecord/test/models/sponsor.rb | 3 ++- 3 files changed, 33 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index f97f89b6fe..f697fdf628 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -579,4 +579,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end end end + + def test_polymorphic_with_custom_foreign_type + sponsor = sponsors(:moustache_club_sponsor_for_groucho) + groucho = members(:groucho) + other = members(:some_other_guy) + + assert_equal groucho, sponsor.sponsorable + assert_equal groucho, sponsor.thing + + sponsor.thing = other + + assert_equal other, sponsor.sponsorable + assert_equal other, sponsor.thing + + sponsor.sponsorable = groucho + + assert_equal groucho, sponsor.sponsorable + assert_equal groucho, sponsor.thing + end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index ad7eb6e4e6..19c69ed7bc 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -21,12 +21,13 @@ require 'models/member' require 'models/membership' require 'models/club' require 'models/categorization' +require 'models/sponsor' class EagerAssociationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts, :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations, :owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books, - :developers, :projects, :developers_projects, :members, :memberships, :clubs + :developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors def setup # preheat table existence caches @@ -913,4 +914,14 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 1, mary.unique_categorized_posts.length assert_equal 1, mary.unique_categorized_post_ids.length end + + def test_preloading_polymorphic_with_custom_foreign_type + sponsor = sponsors(:moustache_club_sponsor_for_groucho) + groucho = members(:groucho) + + sponsor = assert_queries(2) { + Sponsor.includes(:thing).where(:id => sponsor.id).first + } + assert_no_queries { assert_equal groucho, sponsor.thing } + end end diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb index 50c2c2d76c..7e5a1dc38b 100644 --- a/activerecord/test/models/sponsor.rb +++ b/activerecord/test/models/sponsor.rb @@ -1,4 +1,5 @@ class Sponsor < ActiveRecord::Base belongs_to :sponsor_club, :class_name => "Club", :foreign_key => "club_id" belongs_to :sponsorable, :polymorphic => true -end \ No newline at end of file + belongs_to :thing, :polymorphic => true, :foreign_type => :sponsorable_type, :foreign_key => :sponsorable_id +end -- cgit v1.2.3 From d18a27031f8794b0134645eb0d62ec16653ac537 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 1 Jan 2011 18:23:06 +0000 Subject: Add documentation for the :foreign_type option on belongs_to --- activerecord/lib/active_record/associations.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 9a4f6d4dfd..0c71e76899 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1177,6 +1177,11 @@ module ActiveRecord # association will use "person_id" as the default :foreign_key. Similarly, # belongs_to :favorite_person, :class_name => "Person" will use a foreign key # of "favorite_person_id". + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the association with a "_type" + # suffix. So a class that defines a belongs_to :taggable, :polymorphic => true + # association will use "taggable_type" as the default :foreign_type. # [:primary_key] # Specify the method that returns the primary key of associated object used for the association. # By default this is id. -- cgit v1.2.3 From c47f802d0e7b0156512f197887d6e9bda6d0f269 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 1 Jan 2011 18:52:48 +0000 Subject: Have a proper AssociationReflection#foreign_type method rather than using options[:foreign_type] --- activerecord/lib/active_record/association_preload.rb | 11 ++++------- activerecord/lib/active_record/associations.rb | 8 +------- .../associations/belongs_to_polymorphic_association.rb | 6 +++--- .../class_methods/join_dependency/join_association.rb | 2 +- .../lib/active_record/associations/through_association.rb | 4 ++-- activerecord/lib/active_record/fixtures.rb | 11 +++-------- activerecord/lib/active_record/reflection.rb | 4 ++++ activerecord/test/cases/reflection_test.rb | 6 ++++++ 8 files changed, 24 insertions(+), 28 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 638897a86b..e3aee701a8 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -287,7 +287,7 @@ module ActiveRecord def preload_through_records(records, reflection, through_association) if reflection.options[:source_type] - interface = reflection.source_reflection.options[:foreign_type] + interface = reflection.source_reflection.foreign_type preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]} records.compact! @@ -319,18 +319,15 @@ module ActiveRecord def preload_belongs_to_association(records, reflection, preload_options={}) return if records.first.send("loaded_#{reflection.name}?") options = reflection.options - foreign_key = reflection.foreign_key klasses_and_ids = {} if options[:polymorphic] - polymorph_type = options[:foreign_type] - # Construct a mapping from klass to a list of ids to load and a mapping of those ids back # to their parent_records records.each do |record| - if klass = record.send(polymorph_type) - klass_id = record.send(foreign_key) + if klass = record.send(reflection.foreign_type) + klass_id = record.send(reflection.foreign_key) if klass_id id_map = klasses_and_ids[klass.constantize] ||= {} (id_map[klass_id.to_s] ||= []) << record @@ -339,7 +336,7 @@ module ActiveRecord end else id_map = records.group_by do |record| - key = record.send(foreign_key) + key = record.send(reflection.foreign_key) key && key.to_s end id_map.delete nil diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 0c71e76899..b3d5a29b16 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1761,13 +1761,7 @@ module ActiveRecord def create_belongs_to_reflection(association_id, options) options.assert_valid_keys(valid_keys_for_belongs_to_association) - reflection = create_reflection(:belongs_to, association_id, options, self) - - if options[:polymorphic] - reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type" - end - - reflection + create_reflection(:belongs_to, association_id, options, self) end mattr_accessor :valid_keys_for_has_and_belongs_to_many_association diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 46adc048b8..4608ffad67 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -7,7 +7,7 @@ module ActiveRecord target_id = @target.send(@reflection.association_primary_key).to_s foreign_key = @owner.send(@reflection.foreign_key).to_s target_type = @target.class.base_class.name - foreign_type = @owner.send(@reflection.options[:foreign_type]).to_s + foreign_type = @owner.send(@reflection.foreign_type).to_s target_id != foreign_key || target_type != foreign_type else @@ -19,7 +19,7 @@ module ActiveRecord def replace_keys(record) super - @owner[@reflection.options[:foreign_type]] = record && record.class.base_class.name + @owner[@reflection.foreign_type] = record && record.class.base_class.name end def different_target?(record) @@ -31,7 +31,7 @@ module ActiveRecord end def target_klass - type = @owner[@reflection.options[:foreign_type]] + type = @owner[@reflection.foreign_type] type && type.constantize end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb index 1e5149d80f..3fea24ebf8 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -228,7 +228,7 @@ module ActiveRecord second_key = source_reflection.association_foreign_key jt_conditions << - join_table[reflection.source_reflection.options[:foreign_type]]. + join_table[reflection.source_reflection.foreign_type]. eq(reflection.options[:source_type]) else second_key = source_reflection.foreign_key diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index c78f5d969f..6536fb44b2 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -71,7 +71,7 @@ module ActiveRecord @reflection.klass.primary_key source_primary_key = @reflection.source_reflection.foreign_key if @reflection.options[:source_type] - column = @reflection.source_reflection.options[:foreign_type] + column = @reflection.source_reflection.foreign_type conditions << right[column].eq(@reflection.options[:source_type]) end @@ -105,7 +105,7 @@ module ActiveRecord } if @reflection.options[:source_type] - join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name) + join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name) end if @reflection.through_reflection.options[:conditions].is_a?(Hash) diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 3ddd6687b9..b6f0511b9a 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -615,14 +615,9 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) 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.options[:polymorphic] - if value.sub!(/\s*\(([^\)]*)\)\s*$/, "") - target_type = $1 - target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s - - # support polymorphic belongs_to as "label (Type)" - row[target_type_name] = target_type - end + if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + # support polymorphic belongs_to as "label (Type)" + row[association.foreign_type] = $1 end row[fk_name] = Fixtures.identify(value) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 81a95d4971..bc5824104e 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -200,6 +200,10 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end + def foreign_type + @foreign_type ||= options[:foreign_type] || "#{name}_type" + end + def primary_key_column @primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key } end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index e3db34520e..081e3cc861 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -10,6 +10,7 @@ require 'models/price_estimate' require 'models/tagging' require 'models/author' require 'models/post' +require 'models/sponsor' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -205,6 +206,11 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "name", Author.reflect_on_association(:essay).active_record_primary_key.to_s end + def test_foreign_type + assert_equal "sponsorable_type", Sponsor.reflect_on_association(:sponsorable).foreign_type.to_s + assert_equal "sponsorable_type", Sponsor.reflect_on_association(:thing).foreign_type.to_s + end + def test_collection_association assert Pirate.reflect_on_association(:birds).collection? assert Pirate.reflect_on_association(:parrots).collection? -- cgit v1.2.3 From a0be389d39b790e0625339251d2674b8250b16b1 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 2 Jan 2011 14:28:53 +0000 Subject: Allow assignment on has_one :through where the owner is a new record [#5137 state:resolved] This required changing the code to keep the association proxy for a belongs_to around, despite its target being nil. Which in turn required various changes to the way that stale target checking is handled, in order to support various edge cases (loaded target is nil then foreign key added, foreign key is changed and then changed back, etc). A side effect is that the code is nicer and more succinct. Note that I am removing test_no_unexpected_aliasing since that is basically checking that the proxy for a belongs_to *does* change, which is the exact opposite of the intention of this commit. Also adding various tests for various edge cases and related things. Phew, long commit message! --- activerecord/lib/active_record/associations.rb | 23 +++++++-------- .../associations/association_proxy.rb | 31 ++++++++++++++------ .../associations/belongs_to_association.rb | 15 +++------- .../belongs_to_polymorphic_association.rb | 17 +++-------- .../associations/has_many_through_association.rb | 1 - .../associations/has_one_association.rb | 4 +-- .../associations/has_one_through_association.rb | 2 +- .../associations/through_association.rb | 20 +++++-------- .../lib/active_record/autosave_association.rb | 1 + .../associations/belongs_to_associations_test.rb | 33 +++++++++------------- activerecord/test/cases/associations/eager_test.rb | 20 +++++++++++-- .../has_one_through_associations_test.rb | 22 +++++++++++++++ .../test/cases/autosave_association_test.rb | 7 +++++ activerecord/test/cases/nested_attributes_test.rb | 1 - activerecord/test/models/company.rb | 1 + 15 files changed, 113 insertions(+), 85 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b3d5a29b16..35fd1395f7 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1467,16 +1467,17 @@ module ActiveRecord force_reload = params.first unless params.empty? association = association_instance_get(reflection.name) - if association.nil? || force_reload || association.stale_target? + if association.nil? association = association_proxy_class.new(self, reflection) - retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload - if retval.nil? and association_proxy_class == BelongsToAssociation - association_instance_set(reflection.name, nil) - return nil - end association_instance_set(reflection.name, association) end + if force_reload + reflection.klass.uncached { association.reload } + elsif !association.loaded? || association.stale_target? + association.reload + end + association.target.nil? ? nil : association end @@ -1485,19 +1486,19 @@ module ActiveRecord association && association.loaded? end - redefine_method("#{reflection.name}=") do |new_value| + redefine_method("#{reflection.name}=") do |record| association = association_instance_get(reflection.name) - if association.nil? || association.target != new_value + if association.nil? association = association_proxy_class.new(self, reflection) + association_instance_set(reflection.name, association) end - association.replace(new_value) - association_instance_set(reflection.name, new_value.nil? ? nil : association) + association.replace(record) + association.target.nil? ? nil : association end redefine_method("set_#{reflection.name}_target") do |target| - return if target.nil? and association_proxy_class == BelongsToAssociation association = association_proxy_class.new(self, reflection) association.target = target association_instance_set(reflection.name, association) diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 7e68241a2c..65fef81d64 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -128,17 +128,18 @@ module ActiveRecord # Asserts the \target has been loaded setting the \loaded flag to +true+. def loaded - @loaded = true + @loaded = true + @stale_state = stale_state end # The target is stale if the target no longer points to the record(s) that the # relevant foreign_key(s) refers to. If stale, the association accessor method - # on the owner will reload the target. It's up to subclasses to implement this - # method if relevant. + # on the owner will reload the target. It's up to subclasses to implement the + # state_state method if relevant. # # Note that if the target has not been loaded, it is not considered stale. def stale_target? - false + loaded? && @stale_state != stale_state end # Returns the target of this proxy, same as +proxy_target+. @@ -273,16 +274,19 @@ module ActiveRecord @target = find_target end - @loaded = true + loaded @target rescue ActiveRecord::RecordNotFound reset end - # Can be overwritten by associations that might have the foreign key - # available for an association without having the object itself (and - # still being a new record). Currently, only +belongs_to+ presents - # this scenario (both vanilla and polymorphic). + # Should be true if there is a foreign key present on the @owner which + # references the target. This is used to determine whether we can load + # the target if the @owner is currently a new record (and therefore + # without a key). + # + # Currently implemented by belongs_to (vanilla and polymorphic) and + # has_one/has_many :through associations which go through a belongs_to def foreign_key_present false end @@ -308,6 +312,15 @@ module ActiveRecord def invertible_for?(record) inverse_reflection_for(record) end + + # This should be implemented to return the values of the relevant key(s) on the owner, + # so that when state_state is different from the value stored on the last find_target, + # the target is stale. + # + # This is only relevant to certain associations, which is why it returns nil by default. + def stale_state + nil + end end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 63eb38ab34..c54e397ae4 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -29,17 +29,6 @@ module ActiveRecord @updated end - def stale_target? - if @target && @target.persisted? - target_id = @target[@reflection.association_primary_key].to_s - foreign_key = @owner[@reflection.foreign_key].to_s - - target_id != foreign_key - else - false - end - end - private def update_counters(record) counter_cache_name = @reflection.counter_cache_column @@ -106,6 +95,10 @@ module ActiveRecord @owner[@reflection.foreign_key] end end + + def stale_state + @owner[@reflection.foreign_key].to_s + end end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 4608ffad67..4f67b02d00 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -2,19 +2,6 @@ module ActiveRecord # = Active Record Belongs To Polymorphic Association module Associations class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: - def stale_target? - if @target && @target.persisted? - target_id = @target.send(@reflection.association_primary_key).to_s - foreign_key = @owner.send(@reflection.foreign_key).to_s - target_type = @target.class.base_class.name - foreign_type = @owner.send(@reflection.foreign_type).to_s - - target_id != foreign_key || target_type != foreign_type - else - false - end - end - private def replace_keys(record) @@ -38,6 +25,10 @@ module ActiveRecord def raise_on_type_mismatch(record) # A polymorphic association cannot have a type mismatch, by definition end + + def stale_state + [super, @owner[@reflection.foreign_type].to_s] + end end end end 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 2348ee099c..d432e8486d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -62,7 +62,6 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - update_stale_state scoped.all end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 12ccb3af8a..7bbeb8829a 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -33,10 +33,8 @@ module ActiveRecord case @reflection.options[:dependent] when :delete @target.delete if @target.persisted? - @owner.clear_association_cache when :destroy @target.destroy if @target.persisted? - @owner.clear_association_cache when :nullify @target[@reflection.foreign_key] = nil @target.save if @owner.persisted? && @target.persisted? @@ -56,7 +54,7 @@ module ActiveRecord end set_inverse_instance(obj) - @loaded = true + loaded unless !@owner.persisted? || obj.nil? || dont_save return (obj.save ? self : false) diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index c9ae930e93..11fa40a5c4 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -7,6 +7,7 @@ module ActiveRecord def replace(new_value) create_through_record(new_value) @target = new_value + loaded end private @@ -32,7 +33,6 @@ module ActiveRecord end def find_target - update_stale_state scoped.first end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 6536fb44b2..0d83b46130 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -10,17 +10,6 @@ module ActiveRecord end end - def stale_target? - if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key) - previous_key = @through_foreign_key.to_s - current_key = @owner.send(@reflection.through_reflection.foreign_key).to_s - - previous_key != current_key - else - false - end - end - protected def construct_find_scope @@ -157,11 +146,16 @@ module ActiveRecord alias_method :sql_conditions, :conditions - def update_stale_state + def stale_state if @reflection.through_reflection.macro == :belongs_to - @through_foreign_key = @owner.send(@reflection.through_reflection.foreign_key) + @owner[@reflection.through_reflection.foreign_key].to_s end end + + def foreign_key_present + @reflection.through_reflection.macro == :belongs_to && + !@owner[@reflection.through_reflection.foreign_key].nil? + end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index c6c1dd8b87..70ed16eeaf 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -368,6 +368,7 @@ module ActiveRecord if association.updated? association_id = association.send(reflection.options[:primary_key] || :id) self[reflection.foreign_key] = association_id + association.loaded end saved if autosave diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index f697fdf628..38ee4ad4e0 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -88,19 +88,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key_symbols") end - def test_no_unexpected_aliasing - first_firm = companies(:first_firm) - another_firm = companies(:another_firm) - - citibank = Account.create("credit_limit" => 10) - citibank.firm = first_firm - original_proxy = citibank.firm - citibank.firm = another_firm - - assert_equal first_firm.object_id, original_proxy.target.object_id - assert_equal another_firm.object_id, citibank.firm.target.object_id - end - def test_creating_the_belonging_object citibank = Account.create("credit_limit" => 10) apple = citibank.create_firm("name" => "Apple") @@ -318,13 +305,21 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal Firm.find(:first, :order => "id"), c.firm_with_basic_id end - def test_forgetting_the_load_when_foreign_key_enters_late - c = Client.new - assert_nil c.firm_with_basic_id + def test_setting_foreign_key_after_nil_target_loaded + client = Client.new + client.firm_with_basic_id + client.firm_id = 1 - c.firm_id = 1 - # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first - assert_equal Firm.find(:first, :order => "id"), c.firm_with_basic_id + assert_equal companies(:first_firm), client.firm_with_basic_id + end + + def test_polymorphic_setting_foreign_key_after_nil_target_loaded + sponsor = Sponsor.new + sponsor.sponsorable + sponsor.sponsorable_id = 1 + sponsor.sponsorable_type = "Member" + + assert_equal members(:groucho), sponsor.sponsorable end def test_field_name_same_as_foreign_key diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 19c69ed7bc..2a9351507a 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -902,11 +902,25 @@ class EagerAssociationTest < ActiveRecord::TestCase end end - def test_preloading_empty_polymorphic_parent + def test_preloading_empty_belongs_to + c = Client.create!(:name => 'Foo', :client_of => Company.maximum(:id) + 1) + + client = assert_queries(2) { Client.preload(:firm).find(c.id) } + assert_no_queries { assert_nil client.firm } + end + + def test_preloading_empty_belongs_to_polymorphic t = Tagging.create!(:taggable_type => 'Post', :taggable_id => Post.maximum(:id) + 1, :tag => tags(:general)) - assert_queries(2) { @tagging = Tagging.preload(:taggable).find(t.id) } - assert_no_queries { assert ! @tagging.taggable } + tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) } + assert_no_queries { assert_nil tagging.taggable } + end + + def test_preloading_through_empty_belongs_to + c = Client.create!(:name => 'Foo', :client_of => Company.maximum(:id) + 1) + + client = assert_queries(2) { Client.preload(:accounts).find(c.id) } + assert_no_queries { assert client.accounts.empty? } end def test_preloading_has_many_through_with_uniq diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 7d55d909a7..0afbef5c87 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -266,4 +266,26 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert proxy.stale_target? assert_equal dashboards(:second), minivan.dashboard end + + def test_has_one_through_belongs_to_setting_belongs_to_foreign_key_after_nil_target_loaded + minivan = Minivan.new + + minivan.dashboard + proxy = minivan.send(:association_instance_get, :dashboard) + + minivan.speedometer_id = speedometers(:second).id + + assert proxy.stale_target? + assert_equal dashboards(:second), minivan.dashboard + end + + def test_assigning_has_one_through_belongs_to_with_new_record_owner + minivan = Minivan.new + dashboard = dashboards(:cool_first) + + minivan.dashboard = dashboard + + assert_equal dashboard, minivan.dashboard + assert_equal dashboard, minivan.speedometer.dashboard + end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 27aee400f9..71fd3fd836 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -340,6 +340,13 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test tags(:misc).create_tagging(:taggable => posts(:thinking)) assert_equal num_tagging + 1, Tagging.count end + + def test_build_and_then_save_parent_should_not_reload_target + client = Client.find(:first) + apple = client.build_firm(:name => "Apple") + client.save! + assert_no_queries { assert_equal apple, client.firm } + end end class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 7d9b1104cd..d290afc1dd 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -411,7 +411,6 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id @pirate.stubs(:id).returns('ABC1X') - @ship.stubs(:pirate_id).returns(@pirate.id) # prevents pirate from being reloaded due to non-matching foreign key @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' } assert_equal 'Arr', @ship.pirate.catchphrase diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 7af4dfe2c9..d08e593db1 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -124,6 +124,7 @@ class Client < Company belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name" belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true + has_many :accounts, :through => :firm # Record destruction so we can test whether firm.clients.clear has # is calling client.destroy, deleting from the database, or setting -- cgit v1.2.3 From 4e194ed1e68c13901f486334a5a1e9f509b10722 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 2 Jan 2011 14:42:17 +0000 Subject: Rename AssociationProxy#foreign_key_present to foreign_key_present? --- .../lib/active_record/associations/association_collection.rb | 2 +- activerecord/lib/active_record/associations/association_proxy.rb | 4 ++-- .../lib/active_record/associations/belongs_to_association.rb | 6 +++--- activerecord/lib/active_record/associations/through_association.rb | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 0959ea2c5a..be5d264a8f 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -364,7 +364,7 @@ module ActiveRecord end def load_target - if !@owner.new_record? || foreign_key_present + if !@owner.new_record? || foreign_key_present? begin unless loaded? if @target.is_a?(Array) && @target.any? diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 65fef81d64..e86f4c0283 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -270,7 +270,7 @@ module ActiveRecord def load_target return nil unless defined?(@loaded) - if !loaded? && (!@owner.new_record? || foreign_key_present) && @scope + if !loaded? && (!@owner.new_record? || foreign_key_present?) && @scope @target = find_target end @@ -287,7 +287,7 @@ module ActiveRecord # # Currently implemented by belongs_to (vanilla and polymorphic) and # has_one/has_many :through associations which go through a belongs_to - def foreign_key_present + def foreign_key_present? false end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index c54e397ae4..98b846f97c 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -38,7 +38,7 @@ module ActiveRecord record.class.increment_counter(counter_cache_name, record.id) end - if foreign_key_present + if foreign_key_present? target_klass.decrement_counter(counter_cache_name, target_id) end end @@ -55,7 +55,7 @@ module ActiveRecord end def find_target - if foreign_key_present + if foreign_key_present? scoped.first.tap { |record| set_inverse_instance(record) } end end @@ -77,7 +77,7 @@ module ActiveRecord conditions end - def foreign_key_present + def foreign_key_present? !@owner[@reflection.foreign_key].nil? end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 0d83b46130..2b28dda363 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -152,7 +152,7 @@ module ActiveRecord end end - def foreign_key_present + def foreign_key_present? @reflection.through_reflection.macro == :belongs_to && !@owner[@reflection.through_reflection.foreign_key].nil? end -- cgit v1.2.3 From d6289aadce1b8fa93e799500e52f92ce8d159d6f Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 2 Jan 2011 17:56:26 +0000 Subject: Fix test_any in relations_test.rb, which was failing when relations_test.rb is run on its own (it passes when the entire suite is run). This is a hacky fix for a problem I didn't quite get to the bottom of, so I'd welcome a better solution... --- activerecord/test/cases/relations_test.rb | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index ed6ea4db0a..cd774ce343 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -640,6 +640,14 @@ class RelationTest < ActiveRecord::TestCase def test_any posts = Post.scoped + # This test was failing when run on its own (as opposed to running the entire suite). + # The second line in the assert_queries block was causing visit_Arel_Attributes_Attribute + # in Arel::Visitors::ToSql to trigger a SHOW TABLES query. Running that line here causes + # the SHOW TABLES result to be cached so we don't have to do it again in the block. + # + # This is obviously a rubbish fix but it's the best I can come up with for now... + posts.where(:id => nil).any? + assert_queries(3) do assert posts.any? # Uses COUNT() assert ! posts.where(:id => nil).any? -- cgit v1.2.3 From 3103296a61709e808aa89c3d37cf22bcdbc5a675 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 2 Jan 2011 20:33:18 +0000 Subject: Let AssociationCollection#find use #scoped to do its finding. Note that I am removing test_polymorphic_has_many_going_through_join_model_with_disabled_include, since this specifies different behaviour for an association than for a regular scope. It seems reasonable to expect scopes and association proxies to behave in roughly the same way rather than having subtle differences. --- .../associations/association_collection.rb | 81 +++++++++++----------- .../associations/association_proxy.rb | 14 ---- .../has_and_belongs_to_many_association.rb | 34 +++++---- .../associations/has_many_association.rb | 10 --- .../associations/has_many_through_association.rb | 5 -- .../associations/through_association.rb | 19 ++--- .../lib/active_record/nested_attributes.rb | 2 +- .../lib/active_record/relation/spawn_methods.rb | 2 +- .../test/cases/associations/join_model_test.rb | 7 -- activerecord/test/cases/readonly_test.rb | 15 ++-- activerecord/test/cases/relations_test.rb | 4 ++ activerecord/test/models/comment.rb | 5 ++ activerecord/test/models/post.rb | 2 +- activerecord/test/models/project.rb | 4 ++ 14 files changed, 90 insertions(+), 114 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index be5d264a8f..8b0017e7bf 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -32,37 +32,10 @@ module ActiveRecord end def find(*args) - options = args.extract_options! - - # If using a custom finder_sql, scan the entire collection. if @reflection.options[:finder_sql] - expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.uniq.map { |arg| arg.to_i } - - if ids.size == 1 - id = ids.first - record = load_target.detect { |r| id == r.id } - expects_array ? [ record ] : record - else - load_target.select { |r| ids.include?(r.id) } - end + find_by_scan(*args) else - merge_options_from_reflection!(options) - construct_find_options!(options) - - with_scope(:find => @scope[:find].slice(:conditions, :order)) do - relation = @reflection.klass.send(:construct_finder_arel, options, @reflection.klass.send(:current_scoped_methods)) - - case args.first - when :first, :last - relation.send(args.first) - when :all - records = relation.all - @reflection.options[:uniq] ? uniq(records) : records - else - relation.find(*args) - end - end + find_by_sql(*args) end end @@ -360,7 +333,25 @@ module ActiveRecord end protected - def construct_find_options!(options) + + def construct_find_scope + { + :conditions => construct_conditions, + :select => construct_select, + :readonly => @reflection.options[:readonly], + :order => @reflection.options[:order], + :limit => @reflection.options[:limit], + :include => @reflection.options[:include], + :joins => @reflection.options[:joins], + :group => @reflection.options[:group], + :having => @reflection.options[:having], + :offset => @reflection.options[:offset] + } + end + + def construct_select + @reflection.options[:select] || + @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" end def load_target @@ -394,7 +385,7 @@ module ActiveRecord target end - def method_missing(method, *args) + def method_missing(method, *args, &block) match = DynamicFinderMatch.match(method) if match && match.creator? attributes = match.attribute_names @@ -406,15 +397,9 @@ module ActiveRecord elsif @reflection.klass.scopes[method] @_scopes_cache ||= {} @_scopes_cache[method] ||= {} - @_scopes_cache[method][args] ||= with_scope(@scope) { @reflection.klass.send(method, *args) } + @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) else - with_scope(@scope) do - if block_given? - @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) } - else - @reflection.klass.send(method, *args) - end - end + scoped.readonly(nil).send(method, *args, &block) end end @@ -547,6 +532,24 @@ module ActiveRecord @target.include?(record) end end + + # If using a custom finder_sql, #find scans the entire collection. + def find_by_scan(*args) + expects_array = args.first.kind_of?(Array) + ids = args.flatten.compact.uniq.map { |arg| arg.to_i } + + if ids.size == 1 + id = ids.first + record = load_target.detect { |r| id == r.id } + expects_array ? [ record ] : record + else + load_target.select { |r| ids.include?(r.id) } + end + end + + def find_by_sql(*args) + scoped.find(*args) + end end end end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index e86f4c0283..ab42d0f215 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -182,20 +182,6 @@ module ActiveRecord @reflection.klass.send(:sanitize_sql, sql, table_name) end - # Merges into +options+ the ones coming from the reflection. - def merge_options_from_reflection!(options) - options.reverse_merge!( - :group => @reflection.options[:group], - :having => @reflection.options[:having], - :limit => @reflection.options[:limit], - :offset => @reflection.options[:offset], - :joins => @reflection.options[:joins], - :include => @reflection.options[:include], - :select => @reflection.options[:select], - :readonly => @reflection.options[:readonly] - ) - end - # Forwards +with_scope+ to the reflection. def with_scope(*args, &block) target_klass.send :with_scope, *args, &block diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 5336b6cc28..3abe3c2dae 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -2,6 +2,7 @@ module ActiveRecord # = Active Record Has And Belongs To Many Association module Associations class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: + def columns @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns") end @@ -15,11 +16,6 @@ module ActiveRecord end protected - def construct_find_options!(options) - options[:joins] = Arel::SqlLiteral.new(@scope[:find][:joins]) - options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select]) - options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*')) - end def count_records load_target.size @@ -84,22 +80,23 @@ module ActiveRecord end def construct_find_scope - { - :conditions => construct_conditions, - :joins => construct_joins, - :readonly => false, - :order => @reflection.options[:order], - :include => @reflection.options[:include], - :limit => @reflection.options[:limit] - } + super.merge( + :joins => construct_joins, + :readonly => ambiguous_select?(@reflection.options[:select]), + :select => @reflection.options[:select] || Arel.star + ) end # Join tables with additional columns on top of the two foreign keys must be considered # ambiguous unless a select clause has been explicitly defined. Otherwise you can get # broken records back, if, for example, the join column also has an id column. This will # then overwrite the id column of the records coming back. - def finding_with_ambiguous_select?(select_clause) - !select_clause && columns.size != 2 + def ambiguous_select?(select) + extra_join_columns? && select.nil? + end + + def extra_join_columns? + columns.size > 2 end private @@ -114,6 +111,13 @@ module ActiveRecord def invertible_for?(record) false end + + def find_by_sql(*args) + options = args.extract_options! + ambiguous = ambiguous_select?(@reflection.options[:select] || options[:select]) + + scoped.readonly(ambiguous).find(*(args << options)) + end end 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 0d044b28e4..ca02aa8612 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -71,16 +71,6 @@ module ActiveRecord end end - def construct_find_scope - { - :conditions => construct_conditions, - :readonly => false, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :include => @reflection.options[:include] - } - end - def construct_create_scope construct_owner_attributes end 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 d432e8486d..400db6baf1 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -38,11 +38,6 @@ module ActiveRecord end end - def construct_find_options!(options) - options[:joins] = [construct_joins] + Array.wrap(options[:joins]) - options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? && @reflection.source_reflection.options[:include] - end - def insert_record(record, force = true, validate = true) if record.new_record? return false unless save_record(record, force, validate) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 2b28dda363..b2f1f941c0 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -13,15 +13,11 @@ module ActiveRecord protected def construct_find_scope - { - :conditions => construct_conditions, - :joins => construct_joins, - :include => @reflection.options[:include] || @reflection.source_reflection.options[:include], - :select => construct_select, - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :readonly => @reflection.options[:readonly] - } + super.merge( + :joins => construct_joins, + :include => @reflection.options[:include] || + @reflection.source_reflection.options[:include] + ) end # This scope affects the creation of the associated records (not the join records). At the @@ -44,11 +40,6 @@ module ActiveRecord super(aliased_through_table, @reflection.through_reflection) end - def construct_select - @reflection.options[:select] || - @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" - end - def construct_joins right = aliased_through_table left = @reflection.klass.arel_table diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 16023defe3..f3a32e85e6 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -393,7 +393,7 @@ module ActiveRecord association.to_a else attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact - attribute_ids.empty? ? [] : association.all(:conditions => {association.primary_key => attribute_ids}) + attribute_ids.empty? ? [] : association.all(:conditions => {association.klass.primary_key => attribute_ids}) end attributes_collection.each do |attributes| diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index e1f7fa2949..a8fb1bccf4 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -102,7 +102,7 @@ module ActiveRecord options.assert_valid_keys(VALID_FIND_OPTIONS) finders = options.dup - finders.delete_if { |key, value| value.nil? } + finders.delete_if { |key, value| value.nil? && key != :limit } ([:joins, :select, :group, :order, :having, :limit, :offset, :from, :lock, :readonly] & finders.keys).each do |finder| relation = relation.send(finder, finders[finder]) diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 263c90097f..7e9c190f42 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -85,13 +85,6 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end end - def test_polymorphic_has_many_going_through_join_model_with_disabled_include - assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first - assert_queries 1 do - tag.tagging - end - end - def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first tag.author_id diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index a1eb96ef09..8d694900ff 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -6,11 +6,6 @@ require 'models/project' require 'models/reader' require 'models/person' -# Dummy class methods to test implicit association scoping. -def Comment.foo() find :first end -def Project.foo() find :first end - - class ReadOnlyTest < ActiveRecord::TestCase fixtures :posts, :comments, :developers, :projects, :developers_projects, :people, :readers @@ -114,7 +109,13 @@ class ReadOnlyTest < ActiveRecord::TestCase end def test_association_collection_method_missing_scoping_not_readonly - assert !Developer.find(1).projects.foo.readonly? - assert !Post.find(1).comments.foo.readonly? + developer = Developer.find(1) + project = Post.find(1) + + assert !developer.projects.all_as_method.first.readonly? + assert !developer.projects.all_as_scope.first.readonly? + + assert !project.comments.all_as_method.first.readonly? + assert !project.comments.all_as_scope.first.readonly? end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index cd774ce343..4de180880c 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -806,4 +806,8 @@ class RelationTest < ActiveRecord::TestCase assert_equal [rails_author], [rails_author] & relation assert_equal [rails_author], relation & [rails_author] end + + def test_removing_limit_with_options + assert_not_equal 1, Post.limit(1).all(:limit => nil).count + end end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 88061b2145..a9aa0afced 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -15,6 +15,11 @@ class Comment < ActiveRecord::Base def self.search_by_type(q) self.find(:all, :conditions => ["#{QUOTED_TYPE} = ?", q]) end + + def self.all_as_method + all + end + scope :all_as_scope, {} end class SpecialComment < Comment diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index b51f7ff48f..1c95d30d6b 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -51,7 +51,7 @@ class Post < ActiveRecord::Base has_many :taggings, :as => :taggable has_many :tags, :through => :taggings do def add_joins_and_select - find :all, :select => 'tags.*, authors.id as author_id', :include => false, + find :all, :select => 'tags.*, authors.id as author_id', :joins => 'left outer join posts on taggings.taggable_id = posts.id left outer join authors on posts.author_id = authors.id' end end diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 416032cb75..8a53a8f803 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -28,6 +28,10 @@ class Project < ActiveRecord::Base @developers_log = [] end + def self.all_as_method + all + end + scope :all_as_scope, {} end class SpecialProject < Project -- cgit v1.2.3 From 31d101879f1acae604d24d831a4b82a4482acf31 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 2 Jan 2011 21:15:03 +0000 Subject: Use the association directly in other places too --- .../associations/association_collection.rb | 7 ++----- .../associations/has_many_association.rb | 6 ++---- .../associations/has_one_association.rb | 20 +++++++++----------- 3 files changed, 13 insertions(+), 20 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 8b0017e7bf..6defc465d8 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -155,14 +155,13 @@ module ActiveRecord @reflection.klass.count_by_sql(custom_counter_sql) else - if @reflection.options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name options.merge!(:distinct => true) end - value = @reflection.klass.send(:with_scope, @scope) { @reflection.klass.count(column_name, options) } + value = scoped.count(column_name, options) limit = @reflection.options[:limit] offset = @reflection.options[:offset] @@ -469,9 +468,7 @@ module ActiveRecord ensure_owner_is_persisted! transaction do - with_scope(:create => @scope[:create].merge(scoped.scope_for_create)) do - build_record(attrs, &block) - end + scoped.scoping { build_record(attrs, &block) } 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 ca02aa8612..2060f1ff62 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -26,7 +26,7 @@ module ActiveRecord elsif @reflection.options[:counter_sql] || @reflection.options[:finder_sql] @reflection.klass.count_by_sql(custom_counter_sql) else - @reflection.klass.count(@scope[:find].slice(:conditions, :joins, :include)) + scoped.count end # If there's nothing in the database and @target has no new records @@ -61,9 +61,7 @@ module ActiveRecord updates = { @reflection.foreign_key => nil } conditions = { @reflection.association_primary_key => records.map { |r| r.id } } - with_scope(@scope) do - @reflection.klass.update_all(updates, conditions) - end + scoped.where(conditions).update_all(updates) end if has_cached_counter? && @reflection.options[:dependent] != :destroy diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 7bbeb8829a..0070606c24 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -65,17 +65,17 @@ module ActiveRecord private def find_target - options = @reflection.options.dup.slice(:select, :order, :include, :readonly) - - the_target = with_scope(:find => @scope[:find]) do - @reflection.klass.find(:first, options) - end - set_inverse_instance(the_target) - the_target + scoped.first.tap { |record| set_inverse_instance(record) } end def construct_find_scope - { :conditions => construct_conditions } + { + :conditions => construct_conditions, + :select => @reflection.options[:select], + :include => @reflection.options[:include], + :readonly => @reflection.options[:readonly], + :order => @reflection.options[:order] + } end def construct_create_scope @@ -87,9 +87,7 @@ module ActiveRecord # instance. Otherwise, if the target has not previously been loaded # elsewhere, the instance we create will get orphaned. load_target if replace_existing - record = @reflection.klass.send(:with_scope, :create => @scope[:create]) do - yield @reflection - end + record = scoped.scoping { yield @reflection } if replace_existing replace(record, true) -- cgit v1.2.3 From 1313d386dacb580858e5951418a637f4e17cf5c1 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 3 Jan 2011 10:04:40 +0000 Subject: Make Relation#create_with always merge rather than overwrite, not just when merging two relations. If you wish to overwrite, you can do relation.create_with(nil), or for a specific attribute, relation.create_with(:attr => nil). --- activerecord/lib/active_record/relation/query_methods.rb | 2 +- activerecord/lib/active_record/relation/spawn_methods.rb | 4 +--- activerecord/test/cases/relation_scoping_test.rb | 10 ++++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 0ab55ae864..df09226977 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -128,7 +128,7 @@ module ActiveRecord def create_with(value) relation = clone - relation.create_with_value = value + relation.create_with_value = value && (@create_with_value || {}).merge(value) relation end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index a8fb1bccf4..db5f1af5ca 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -53,9 +53,7 @@ module ActiveRecord merged_relation.lock_value = r.lock_value unless merged_relation.lock_value - if r.create_with_value - merged_relation.create_with_value = (merged_relation.create_with_value || {}).merge(r.create_with_value) - end + merged_relation = merged_relation.create_with(r.create_with_value) if r.create_with_value # Apply scope extension modules merged_relation.send :apply_modules, r.extensions diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 7c6899d438..bff0151f1a 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -491,5 +491,15 @@ class DefaultScopingTest < ActiveRecord::TestCase PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new assert_equal 20, aaron.salary assert_equal 'Aaron', aaron.name + + aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20). + create_with(:name => 'Aaron').new + assert_equal 20, aaron.salary + assert_equal 'Aaron', aaron.name + end + + def test_create_with_reset + jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new + assert_equal 'Jamis', jamis.name end end -- cgit v1.2.3 From 99a8d8430f9b819cd3e8cb3aab44cb04ea402532 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 3 Jan 2011 12:04:33 +0000 Subject: Create the association scope directly rather than going through with_scope --- .../associations/association_collection.rb | 2 +- .../associations/association_proxy.rb | 36 ++++++++++++---------- .../associations/belongs_to_association.rb | 2 +- .../has_and_belongs_to_many_association.rb | 2 +- .../associations/has_many_association.rb | 2 +- .../associations/has_one_association.rb | 4 +-- .../associations/through_association.rb | 15 ++++----- 7 files changed, 32 insertions(+), 31 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 6defc465d8..4bd8a8e2d2 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -333,7 +333,7 @@ module ActiveRecord protected - def construct_find_scope + def finder_options { :conditions => construct_conditions, :select => construct_select, diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index ab42d0f215..3a0cda49f8 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -169,7 +169,9 @@ module ActiveRecord end def scoped - with_scope(@scope) { target_klass.scoped } + target_scope. + apply_finder_options(@finder_options). + create_with(@creation_attributes) end protected @@ -182,30 +184,26 @@ module ActiveRecord @reflection.klass.send(:sanitize_sql, sql, table_name) end - # Forwards +with_scope+ to the reflection. - def with_scope(*args, &block) - target_klass.send :with_scope, *args, &block - end - - # Construct the scope used for find/create queries on the target + # Construct the data used for the scope for this association + # + # Note that we don't actually build the scope here, we just construct the options and + # attributes. We must only build the scope when it's actually needed, 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 construct_scope if target_klass - @scope = { - :find => construct_find_scope, - :create => construct_create_scope - } - else - @scope = nil + @finder_options = finder_options + @creation_attributes = creation_attributes end end # Implemented by subclasses - def construct_find_scope + def finder_options raise NotImplementedError end # Implemented by (some) subclasses - def construct_create_scope + def creation_attributes {} end @@ -226,6 +224,12 @@ 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 + target_klass.scoped + end + private # Forwards any missing method call to the \target. def method_missing(method, *args) @@ -256,7 +260,7 @@ module ActiveRecord def load_target return nil unless defined?(@loaded) - if !loaded? && (!@owner.new_record? || foreign_key_present?) && @scope + if !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass @target = find_target end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 98b846f97c..4cde1c0960 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -60,7 +60,7 @@ module ActiveRecord end end - def construct_find_scope + def finder_options { :conditions => construct_conditions, :select => @reflection.options[:select], diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 3abe3c2dae..1c237eda33 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -79,7 +79,7 @@ module ActiveRecord super(join_table) end - def construct_find_scope + def finder_options super.merge( :joins => construct_joins, :readonly => ambiguous_select?(@reflection.options[:select]), diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 2060f1ff62..dba26a78da 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -69,7 +69,7 @@ module ActiveRecord end end - def construct_create_scope + def creation_attributes construct_owner_attributes end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 0070606c24..5716bef524 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -68,7 +68,7 @@ module ActiveRecord scoped.first.tap { |record| set_inverse_instance(record) } end - def construct_find_scope + def finder_options { :conditions => construct_conditions, :select => @reflection.options[:select], @@ -78,7 +78,7 @@ module ActiveRecord } end - def construct_create_scope + def creation_attributes construct_owner_attributes end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index b2f1f941c0..65c36419da 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,16 +3,13 @@ module ActiveRecord module Associations module ThroughAssociation - def scoped - with_scope(@scope) do - @reflection.klass.scoped & - @reflection.through_reflection.klass.scoped - end - end - protected - def construct_find_scope + def target_scope + super & @reflection.through_reflection.klass.scoped + end + + def finder_options super.merge( :joins => construct_joins, :include => @reflection.options[:include] || @@ -24,7 +21,7 @@ module ActiveRecord # moment we only support creating on a :through association when the source reflection is a # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so # this scope has can legitimately be empty. - def construct_create_scope + def creation_attributes { } end -- cgit v1.2.3 From a9bed985cfd7d1ae93f475542bb878aa939e1c1e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 3 Jan 2011 13:38:40 +0000 Subject: When preloading a belongs_to, the target should still be set (to nil) if there is no foreign key present. And the loaded flag should be set on the association proxy. This then allows us to remove the foreign_key_present? check from BelongsToAssociation#find_target. Also added a test for the same thing on polymorphic associations. --- .../lib/active_record/association_preload.rb | 23 +++++++++++++--------- activerecord/lib/active_record/associations.rb | 1 + .../associations/belongs_to_association.rb | 4 +--- activerecord/test/cases/associations/eager_test.rb | 9 +++++++++ 4 files changed, 25 insertions(+), 12 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index e3aee701a8..5aad2b4558 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -339,22 +339,27 @@ module ActiveRecord key = record.send(reflection.foreign_key) key && key.to_s end - id_map.delete nil klasses_and_ids[reflection.klass] = id_map unless id_map.empty? end klasses_and_ids.each do |klass, _id_map| - table = klass.arel_table primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s - method = in_or_equal(_id_map.keys) - conditions = table[primary_key].send(*method) + keys = _id_map.keys.compact - custom_conditions = append_conditions(reflection, preload_options) - conditions = custom_conditions.inject(conditions) do |ast, cond| - ast.and cond - end + unless keys.empty? + table = klass.arel_table + method = in_or_equal(keys) + conditions = table[primary_key].send(*method) - associated_records = klass.unscoped.where(conditions).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a + custom_conditions = append_conditions(reflection, preload_options) + conditions = custom_conditions.inject(conditions) do |ast, cond| + ast.and cond + end + + associated_records = klass.unscoped.where(conditions).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a + else + associated_records = [] + end set_association_single_records(_id_map, reflection.name, associated_records, primary_key) end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 35fd1395f7..e7d3e45da2 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1501,6 +1501,7 @@ module ActiveRecord redefine_method("set_#{reflection.name}_target") do |target| association = association_proxy_class.new(self, reflection) association.target = target + association.loaded association_instance_set(reflection.name, association) end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 4cde1c0960..b50ee689cd 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -55,9 +55,7 @@ module ActiveRecord end def find_target - if foreign_key_present? - scoped.first.tap { |record| set_inverse_instance(record) } - end + scoped.first.tap { |record| set_inverse_instance(record) } end def finder_options diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 2a9351507a..e11f1009dc 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -212,6 +212,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_finding_with_includes_on_null_belongs_to_polymorphic_association + sponsor = sponsors(:moustache_club_sponsor_for_groucho) + sponsor.update_attributes!(:sponsorable => nil) + sponsor = assert_queries(1) { Sponsor.find(sponsor.id, :include => :sponsorable) } + assert_no_queries do + assert_equal nil, sponsor.sponsorable + end + end + def test_loading_from_an_association posts = authors(:david).posts.find(:all, :include => :comments, :order => "posts.id") assert_equal 2, posts.first.comments.size -- cgit v1.2.3 From 0619dc2319cf839977ea9670a52d9280a1af3595 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 3 Jan 2011 19:54:38 +0000 Subject: Implement deprecated version of AssociationReflection#primary_key_name, which has been renamed to #foreign_key. Also bumping the deprecation_horizon in Active Support to 3.1. --- activerecord/lib/active_record/reflection.rb | 6 ++++++ activerecord/test/cases/reflection_test.rb | 12 ++++++++++++ 2 files changed, 18 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index bc5824104e..937efe395f 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/module/deprecation' module ActiveRecord # = Active Record Reflection @@ -200,6 +201,11 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end + def primary_key_name + foreign_key + end + deprecate :primary_key_name => :foreign_key + def foreign_type @foreign_type ||= options[:foreign_type] || "#{name}_type" end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 081e3cc861..d75bc3982e 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -248,6 +248,18 @@ class ReflectionTest < ActiveRecord::TestCase assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, { :autosave => true, :validate => false }, Firm).validate? end + def test_foreign_key + assert_equal "author_id", Author.reflect_on_association(:posts).foreign_key.to_s + assert_equal "category_id", Post.reflect_on_association(:categorizations).foreign_key.to_s + end + + def test_primary_key_name + assert_deprecated do + assert_equal "author_id", Author.reflect_on_association(:posts).primary_key_name.to_s + assert_equal "category_id", Post.reflect_on_association(:categorizations).primary_key_name.to_s + end + end + private def assert_reflection(klass, association, options) assert reflection = klass.reflect_on_association(association) -- cgit v1.2.3 From 2120da7f733ba33183a42e71256db9652c5f5fcc Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 3 Jan 2011 22:47:07 +0000 Subject: ActiveRecord::Relation#primary_key should return a string, just like ActiveRecord::Base.primary_key does. --- activerecord/lib/active_record/nested_attributes.rb | 2 +- activerecord/lib/active_record/relation.rb | 19 ++++++------------- activerecord/lib/active_record/relation/batches.rb | 6 +++--- .../lib/active_record/relation/calculations.rb | 2 +- .../lib/active_record/relation/finder_methods.rb | 18 +++++++++--------- .../lib/active_record/relation/query_methods.rb | 2 +- activerecord/test/cases/relations_test.rb | 4 ++++ 7 files changed, 25 insertions(+), 28 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index f3a32e85e6..16023defe3 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -393,7 +393,7 @@ module ActiveRecord association.to_a else attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact - attribute_ids.empty? ? [] : association.all(:conditions => {association.klass.primary_key => attribute_ids}) + attribute_ids.empty? ? [] : association.all(:conditions => {association.primary_key => attribute_ids}) end attributes_collection.each do |attributes| diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 20e983b5f7..1441e9750e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -10,7 +10,9 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches + # These are explicitly delegated to improve performance (avoids method_missing) delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to => :to_a + delegate :table_name, :primary_key, :to => :klass attr_reader :table, :klass, :loaded attr_accessor :extensions @@ -30,13 +32,12 @@ module ActiveRecord def insert(values) im = arel.compile_insert values im.into @table - primary_key_name = @klass.primary_key - primary_key_value = primary_key_name && Hash === values ? values[primary_key] : nil + primary_key_value = primary_key && Hash === values ? values[table[primary_key]] : nil @klass.connection.insert( im.to_sql, 'SQL', - primary_key_name, + primary_key, primary_key_value) end @@ -177,7 +178,7 @@ module ActiveRecord stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))) stmt.take limit stmt.order(*order) - stmt.key = @klass.arel_table[@klass.primary_key] + stmt.key = table[primary_key] @klass.connection.update stmt.to_sql end end @@ -320,7 +321,7 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) - where(@klass.primary_key => id_or_array).delete_all + where(primary_key => id_or_array).delete_all end def reload @@ -336,10 +337,6 @@ module ActiveRecord self end - def primary_key - @primary_key ||= table[@klass.primary_key] - end - def to_sql @to_sql ||= arel.to_sql end @@ -373,10 +370,6 @@ module ActiveRecord to_a.inspect end - def table_name - @klass.table_name - end - protected def method_missing(method, *args, &block) diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index b41e935ed5..359af9820f 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -65,7 +65,7 @@ module ActiveRecord batch_size = options.delete(:batch_size) || 1000 relation = relation.except(:order).order(batch_order).limit(batch_size) - records = relation.where(primary_key.gteq(start)).all + records = relation.where(table[primary_key].gteq(start)).all while records.any? yield records @@ -73,7 +73,7 @@ module ActiveRecord break if records.size < batch_size if primary_key_offset = records.last.id - records = relation.where(primary_key.gt(primary_key_offset)).to_a + records = relation.where(table[primary_key].gt(primary_key_offset)).to_a else raise "Primary key not included in the custom select clause" end @@ -83,7 +83,7 @@ module ActiveRecord private def batch_order - "#{@klass.table_name}.#{@klass.primary_key} ASC" + "#{table_name}.#{primary_key} ASC" end end end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 139752b190..e9e451ec5c 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -168,7 +168,7 @@ module ActiveRecord unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? distinct = true - column_name = @klass.primary_key if column_name == :all + column_name = primary_key if column_name == :all end distinct = nil if column_name =~ /\s*DISTINCT\s+/i diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8bc28c06cb..13f55319a7 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -171,13 +171,13 @@ module ActiveRecord def exists?(id = nil) id = id.id if ActiveRecord::Base === id - relation = select(primary_key).limit(1) + relation = select(table[primary_key]).limit(1) case id when Array, Hash relation = relation.where(id) else - relation = relation.where(primary_key.eq(id)) if id + relation = relation.where(table[primary_key].eq(id)) if id end relation.first ? true : false @@ -225,10 +225,10 @@ module ActiveRecord def construct_limited_ids_condition(relation) orders = relation.order_values - values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders) + values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders) - ids_array = relation.select(values).collect {|row| row[@klass.primary_key]} - ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array) + ids_array = relation.select(values).collect {|row| row[primary_key]} + ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array) end def find_by_attributes(match, attributes, *args) @@ -290,24 +290,24 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - column = columns_hash[primary_key.name.to_s] + column = columns_hash[primary_key] substitute = connection.substitute_for(column, @bind_values) - relation = where(primary_key.eq(substitute)) + relation = where(table[primary_key].eq(substitute)) relation.bind_values = [[column, id]] record = relation.first unless record conditions = arel.where_sql conditions = " [#{conditions}]" if conditions - raise RecordNotFound, "Couldn't find #{@klass.name} with #{@klass.primary_key}=#{id}#{conditions}" + raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}" end record end def find_some(ids) - result = where(primary_key.in(ids)).all + result = where(table[primary_key].in(ids)).all expected_size = if @limit_value && ids.size > @limit_value diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index df09226977..2cbb103eb9 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -152,7 +152,7 @@ module ActiveRecord order_clause = arel.order_clauses order = order_clause.empty? ? - "#{@klass.table_name}.#{@klass.primary_key} DESC" : + "#{table_name}.#{primary_key} DESC" : reverse_sql_order(order_clause).join(', ') except(:order).order(Arel.sql(order)) diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 4de180880c..5018b16b67 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -810,4 +810,8 @@ class RelationTest < ActiveRecord::TestCase def test_removing_limit_with_options assert_not_equal 1, Post.limit(1).all(:limit => nil).count end + + def test_primary_key + assert_equal "id", Post.scoped.primary_key + end end -- cgit v1.2.3 From 40afcade0dc1450e765a91fc15a6ac6d442c9826 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 3 Jan 2011 23:48:53 +0000 Subject: Remove undocumented feature from has_one where you could pass false as the second parameter to build_assoc or create_assoc, and the existing associated object would be untouched (the foreign key would not be nullified, and it would not be deleted). If you want behaviour similar to this you can do the following things: * Use :dependent => :nullify (or don't specify :dependent) if you want to prevent the existing associated object from being deleted * Use has_many if you actually want multiple associated objects * Explicitly set the foreign key if, for some reason, you really need to have multiple objects associated with the same has_one. E.g. previous = obj.assoc obj.create_assoc previous.update_attributes(:obj_id => obj.id) --- activerecord/lib/active_record/associations.rb | 11 +++----- .../associations/has_one_association.rb | 28 ++++++--------------- .../associations/has_one_associations_test.rb | 29 ---------------------- .../associations/inverse_associations_test.rb | 14 +++++------ 4 files changed, 18 insertions(+), 64 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e7d3e45da2..bbf96f52ed 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1556,20 +1556,15 @@ module ActiveRecord def association_constructor_method(constructor, reflection, association_proxy_class) redefine_method("#{constructor}_#{reflection.name}") do |*params| - attributees = params.first unless params.empty? - replace_existing = params[1].nil? ? true : params[1] - association = association_instance_get(reflection.name) + attributes = params.first unless params.empty? + association = association_instance_get(reflection.name) unless association association = association_proxy_class.new(self, reflection) association_instance_set(reflection.name, association) end - if association_proxy_class == HasOneAssociation - association.send(constructor, attributees, replace_existing) - else - association.send(constructor, attributees) - end + association.send(constructor, attributes) end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 5716bef524..f1e01197b5 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -4,22 +4,22 @@ module ActiveRecord class HasOneAssociation < AssociationProxy #:nodoc: include HasAssociation - def create(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| + def create(attrs = {}) + new_record do |reflection| attrs = merge_with_conditions(attrs) reflection.create_association(attrs) end end - def create!(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| + def create!(attrs = {}) + new_record do |reflection| attrs = merge_with_conditions(attrs) reflection.create_association!(attrs) end end - def build(attrs = {}, replace_existing = true) - new_record(replace_existing) do |reflection| + def build(attrs = {}) + new_record do |reflection| attrs = merge_with_conditions(attrs) reflection.build_association(attrs) end @@ -82,21 +82,9 @@ module ActiveRecord construct_owner_attributes end - def new_record(replace_existing) - # Make sure we load the target first, if we plan on replacing the existing - # instance. Otherwise, if the target has not previously been loaded - # elsewhere, the instance we create will get orphaned. - load_target if replace_existing + def new_record record = scoped.scoping { yield @reflection } - - if replace_existing - replace(record, true) - else - record[@reflection.foreign_key] = @owner.id if @owner.persisted? - self.target = record - set_inverse_instance(record) - end - + replace(record, true) record end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 64449df8f5..fa36b527a2 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -116,35 +116,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal company.account, account end - def test_assignment_without_replacement - apple = Firm.create("name" => "Apple") - citibank = Account.create("credit_limit" => 10) - apple.account = citibank - assert_equal apple.id, citibank.firm_id - - hsbc = apple.build_account({ :credit_limit => 20}, false) - assert_equal apple.id, hsbc.firm_id - hsbc.save - assert_equal apple.id, citibank.firm_id - - nykredit = apple.create_account({ :credit_limit => 30}, false) - assert_equal apple.id, nykredit.firm_id - assert_equal apple.id, citibank.firm_id - assert_equal apple.id, hsbc.firm_id - end - - def test_assignment_without_replacement_on_create - apple = Firm.create("name" => "Apple") - citibank = Account.create("credit_limit" => 10) - apple.account = citibank - assert_equal apple.id, citibank.firm_id - - hsbc = apple.create_account({:credit_limit => 10}, false) - assert_equal apple.id, hsbc.firm_id - hsbc.save - assert_equal apple.id, citibank.firm_id - end - def test_dependence num_accounts = Account.count diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 0491b2d1fa..a0f94a22e3 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -146,9 +146,9 @@ class InverseHasOneTests < ActiveRecord::TestCase assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" end - def test_parent_instance_should_be_shared_with_newly_built_child_when_we_dont_replace_existing + def test_parent_instance_should_be_shared_with_newly_built_child m = Man.find(:first) - f = m.build_face({:description => 'haunted'}, false) + f = m.build_face(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' @@ -157,9 +157,9 @@ class InverseHasOneTests < ActiveRecord::TestCase assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance" end - def test_parent_instance_should_be_shared_with_newly_created_child_when_we_dont_replace_existing + def test_parent_instance_should_be_shared_with_newly_created_child m = Man.find(:first) - f = m.create_face({:description => 'haunted'}, false) + f = m.create_face(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' @@ -168,9 +168,9 @@ class InverseHasOneTests < ActiveRecord::TestCase assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" end - def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method_when_we_dont_replace_existing + def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method m = Man.find(:first) - f = m.face.create!({:description => 'haunted'}, false) + f = m.face.create!(:description => 'haunted') assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' @@ -203,7 +203,7 @@ class InverseHasOneTests < ActiveRecord::TestCase assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance" end - def test_parent_instance_should_be_shared_with_replaced_via_method_child_when_we_dont_replace_existing + def test_parent_instance_should_be_shared_with_replaced_via_method_child m = Man.find(:first) f = Face.new(:description => 'haunted') m.face.replace(f, false) -- cgit v1.2.3 From 1d758d9086724faa4f46554135f304fa570c507d Mon Sep 17 00:00:00 2001 From: Raimonds Simanovskis Date: Tue, 4 Jan 2011 11:21:03 +0200 Subject: require ActiveSupport deprecatation file before using deprecate method in database_statements otherwise when using external ActiveRecord adapters (e.g. Oracle) database_statements might be loaded before active_support/core_ext/module/deprecation which results in NoMethodError (commit 60cf65def805995bcca184c40b44bb01d86a48aa added "deprecate" call to database_statements.rb) --- .../active_record/connection_adapters/abstract/database_statements.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 3f15af561b..01e53b46c8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/deprecation' + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements -- cgit v1.2.3 From 9c1c551f25577c01624b23bc53139c60a4fc451b Mon Sep 17 00:00:00 2001 From: Raimonds Simanovskis Date: Tue, 4 Jan 2011 17:06:33 +0200 Subject: Explicitly select * from has_and_belongs_to_many association tables, simplify exists? query Previous version (after commit 3103296a61709e808aa89c3d37cf22bcdbc5a675) was generating wrong SQL for Oracle when calling exists? method on HABTM association. --- .../active_record/associations/has_and_belongs_to_many_association.rb | 3 ++- activerecord/lib/active_record/relation/finder_methods.rb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 1c237eda33..a4194defc2 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -83,7 +83,8 @@ module ActiveRecord super.merge( :joins => construct_joins, :readonly => ambiguous_select?(@reflection.options[:select]), - :select => @reflection.options[:select] || Arel.star + :select => @reflection.options[:select] || + Arel.sql("#{@reflection.quoted_table_name}.*, #{@owner.connection.quote_table_name @reflection.options[:join_table]}.*") ) end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 13f55319a7..8bbc47ab75 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -171,7 +171,7 @@ module ActiveRecord def exists?(id = nil) id = id.id if ActiveRecord::Base === id - relation = select(table[primary_key]).limit(1) + relation = select("1").limit(1) case id when Array, Hash -- cgit v1.2.3 From 9e64dfad0df4ed8a10d2ad2a17cd0848017d652c Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Sat, 1 Jan 2011 01:15:42 +0700 Subject: Use Rails 3.1 `change` method in model generator --- .../lib/rails/generators/active_record/model/templates/migration.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb index 7d4e1a7404..cd2552d9b8 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb @@ -1,5 +1,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration - def up + def change create_table :<%= table_name %> do |t| <% for attribute in attributes -%> t.<%= attribute.type %> :<%= attribute.name %> @@ -13,8 +13,4 @@ class <%= migration_class_name %> < ActiveRecord::Migration add_index :<%= table_name %>, :<%= attribute.name %>_id <% end -%> end - - def down - drop_table :<%= table_name %> - end end -- cgit v1.2.3 From ad343d7263d0922bbe32f550e7057d55f2c4d311 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Sat, 1 Jan 2011 01:19:56 +0700 Subject: Use Rails 3.1 `change` method in 'add_' migration generator --- .../generators/active_record/migration/templates/migration.rb | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index 126b6f434b..ce8d7eed42 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -1,4 +1,11 @@ class <%= migration_class_name %> < ActiveRecord::Migration +<%- if migration_action == 'add' -%> + def change +<% attributes.each do |attribute| -%> + add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %> +<%- end -%> + end +<%- else -%> def up <% attributes.each do |attribute| -%> <%- if migration_action -%> @@ -14,4 +21,5 @@ class <%= migration_class_name %> < ActiveRecord::Migration <%- end -%> <%- end -%> end +<%- end -%> end -- cgit v1.2.3 From 3f4143eedb3ec1c8e55bfc1ea0083fbb35749659 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 4 Jan 2011 15:16:56 -0800 Subject: fixing merge errors --- .../associations/belongs_to_associations_test.rb | 11 ---- .../associations/inverse_associations_test.rb | 67 ---------------------- 2 files changed, 78 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 38ee4ad4e0..aaa5421eea 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -162,17 +162,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted" end - def test_belongs_to_with_primary_key_counter - debate = Topic.create("title" => "debate") - assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet" - - trash = debate.replies_with_primary_key.create("title" => "blah!", "content" => "world around!") - assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created" - - trash.destroy - assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted" - end - def test_belongs_to_counter_with_assigning_nil p = Post.find(1) c = Comment.find(1) diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index a0f94a22e3..4cca78da9d 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -113,39 +113,6 @@ class InverseHasOneTests < ActiveRecord::TestCase assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance" end - def test_parent_instance_should_be_shared_with_newly_built_child - m = men(:gordon) - f = m.build_face(:description => 'haunted') - assert_not_nil f.man - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = 'Mungo' - assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_created_child - m = men(:gordon) - f = m.create_face(:description => 'haunted') - assert_not_nil f.man - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = 'Mungo' - assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - - def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method - m = Man.find(:first) - f = m.face.create!(:description => 'haunted') - assert_not_nil f.man - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = 'Mungo' - assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - def test_parent_instance_should_be_shared_with_newly_built_child m = Man.find(:first) f = m.build_face(:description => 'haunted') @@ -191,18 +158,6 @@ class InverseHasOneTests < ActiveRecord::TestCase assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance" end - def test_parent_instance_should_be_shared_with_replaced_via_method_child - m = Man.find(:first) - f = Face.new(:description => 'haunted') - m.face.replace(f) - assert_not_nil f.man - assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance" - f.man.name = 'Mungo' - assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance" - end - def test_parent_instance_should_be_shared_with_replaced_via_method_child m = Man.find(:first) f = Face.new(:description => 'haunted') @@ -257,17 +212,6 @@ class InverseHasManyTests < ActiveRecord::TestCase end end - def test_parent_instance_should_be_shared_with_newly_built_child - m = men(:gordon) - i = m.interests.build(:topic => 'Industrial Revolution Re-enactment') - assert_not_nil i.man - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = 'Mungo' - assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance" - end - def test_parent_instance_should_be_shared_with_newly_block_style_built_child m = Man.find(:first) i = m.interests.build {|ii| ii.topic = 'Industrial Revolution Re-enactment'} @@ -280,17 +224,6 @@ class InverseHasManyTests < ActiveRecord::TestCase assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance" end - def test_parent_instance_should_be_shared_with_newly_created_child - m = men(:gordon) - i = m.interests.create(:topic => 'Industrial Revolution Re-enactment') - assert_not_nil i.man - assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" - m.name = 'Bongo' - assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance" - i.man.name = 'Mungo' - assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance" - end - def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child m = Man.find(:first) i = m.interests.create!(:topic => 'Industrial Revolution Re-enactment') -- cgit v1.2.3 From 35a225535f38df551a1399b596622f3e3a0bbfbe Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 09:23:25 -0800 Subject: use arel to construct AST rather than generate strings --- .../associations/has_and_belongs_to_many_association.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index a4194defc2..f3b1c9de3f 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -83,8 +83,9 @@ module ActiveRecord super.merge( :joins => construct_joins, :readonly => ambiguous_select?(@reflection.options[:select]), - :select => @reflection.options[:select] || - Arel.sql("#{@reflection.quoted_table_name}.*, #{@owner.connection.quote_table_name @reflection.options[:join_table]}.*") + :select => @reflection.options[:select] || [ + @reflection.klass.arel_table[Arel.star], + join_table[Arel.star]] ) end -- cgit v1.2.3 From e468a62dc89d4d3c2a16a6d3e8fd78342a2732df Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 09:47:13 -0800 Subject: use arel ast construction rather than generating strings --- .../associations/has_and_belongs_to_many_association.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index f3b1c9de3f..a8e1ea21c4 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -68,7 +68,13 @@ module ActiveRecord end def construct_joins - "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}" + right = join_table + left = @reflection.klass.arel_table + + condition = left[@reflection.klass.primary_key].eq( + right[@reflection.association_foreign_key]) + + right.create_join(right, right.create_on(condition)) end def join_table -- cgit v1.2.3 From 8bdc191994bb091310cbbe4690900e7a97da0b5e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 09:51:09 -0800 Subject: we have a method for this, so let's use it --- .../active_record/associations/has_and_belongs_to_many_association.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index a8e1ea21c4..3c939d7e85 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -29,7 +29,7 @@ module ActiveRecord if @reflection.options[:insert_sql] @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) else - relation = Arel::Table.new(@reflection.options[:join_table]) + relation = join_table timestamps = record_timestamp_columns(record) timezone = record.send(:current_time_from_proper_timezone) if timestamps.any? @@ -59,7 +59,7 @@ module ActiveRecord if sql = @reflection.options[:delete_sql] records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } else - relation = Arel::Table.new(@reflection.options[:join_table]) + relation = join_table stmt = relation.where(relation[@reflection.foreign_key].eq(@owner.id). and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) ).compile_delete -- cgit v1.2.3 From 9f1b0b32e27af668014c6fb21edbfc869f36dd2d Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 09:58:25 -0800 Subject: use attr_reader and alias methods to access instance variables --- .../lib/active_record/associations/association_proxy.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 3a0cda49f8..8e64056a06 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -75,11 +75,6 @@ module ActiveRecord @reflection end - # Returns the \target of the proxy, same as +target+. - def proxy_target - @target - end - # Does the proxy or its \target respond to +symbol+? def respond_to?(*args) proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args)) @@ -143,9 +138,10 @@ module ActiveRecord end # Returns the target of this proxy, same as +proxy_target+. - def target - @target - end + attr_reader :target + + # Returns the \target of the proxy, same as +target+. + alias :proxy_target :target # Sets the target of this proxy to \target, and the \loaded flag to +true+. def target=(target) -- cgit v1.2.3 From 90171ad833fa15a3030e15b7eb2043e1204d9db0 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 10:29:06 -0800 Subject: avoid creating so many Arel::Table objects --- .../associations/has_and_belongs_to_many_association.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 3c939d7e85..b18ec23037 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -2,9 +2,16 @@ module ActiveRecord # = Active Record Has And Belongs To Many Association module Associations class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: + attr_reader :join_table + + def initialize(owner, reflection) + @join_table_name = reflection.options[:join_table] + @join_table = Arel::Table.new(@join_table_name) + super + end def columns - @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns") + @reflection.columns(@join_table_name, "#{@join_table_name} Columns") end def reset_column_information @@ -12,7 +19,7 @@ module ActiveRecord end def has_primary_key? - @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table]) + @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@join_table_name) end protected @@ -77,10 +84,6 @@ module ActiveRecord right.create_join(right, right.create_on(condition)) end - def join_table - Arel::Table.new(@reflection.options[:join_table]) - end - def construct_owner_conditions super(join_table) end -- cgit v1.2.3 From 102255330bb6ffc5d0ca2888f206813445a29e44 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 10:50:46 -0800 Subject: no need to send a symbol to send() --- activerecord/lib/active_record/associations/association_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 4bd8a8e2d2..bd27365b08 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -521,7 +521,7 @@ module ActiveRecord def include_in_memory?(record) if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) - @owner.send(proxy_reflection.through_reflection.name.to_sym).any? do |source| + @owner.send(proxy_reflection.through_reflection.name).any? do |source| target = source.send(proxy_reflection.source_reflection.name) target.respond_to?(:include?) ? target.include?(record) : target == record end -- cgit v1.2.3 From c7dce2c7b2a9e27753f5e1c71fae677c31207087 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 11:08:21 -0800 Subject: no need to specify self --- .../lib/active_record/connection_adapters/abstract/query_cache.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index d555308485..1db397f584 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -60,7 +60,7 @@ module ActiveRecord result = if @query_cache[sql].key?(binds) ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :name => "CACHE", :connection_id => self.object_id) + :sql => sql, :name => "CACHE", :connection_id => object_id) @query_cache[sql][binds] else @query_cache[sql][binds] = yield -- cgit v1.2.3 From 9731c862ecb827b16213c5bdb0f2bebbfaf9d608 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 13:50:08 -0800 Subject: AR internals expect a normal hash, otherwise there are serialization incompatibilities --- activerecord/lib/active_record/result.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 8deff1478f..47a5ac2700 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -23,7 +23,7 @@ module ActiveRecord private def hash_rows @hash_rows ||= @rows.map { |row| - ActiveSupport::OrderedHash[@columns.zip(row)] + Hash[@columns.zip(row)] } end end -- cgit v1.2.3 From 6d747108286790bb7f5fa9ffbfcbdf4a557a2785 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 14:01:47 -0800 Subject: make sure that Psych can roundtrip an AR object --- activerecord/test/cases/yaml_serialization_test.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 0fc9918744..7503b06126 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -17,4 +17,17 @@ class YamlSerializationTest < ActiveRecord::TestCase t = YAML.load YAML.dump topic assert_equal topic, t end + + begin + require 'psych' + + def test_psych_roundtrip + topic = Topic.first + assert topic + t = Psych.load Psych.dump topic + assert_equal topic, t + end + + rescue LoadError + end end -- cgit v1.2.3 From 97bc74c74611d3d71d58776ed907ebd0cdb98a15 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 14:03:13 -0800 Subject: make sure new objects can round trip --- activerecord/test/cases/yaml_serialization_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 7503b06126..abcf61ffc0 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -28,6 +28,12 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal topic, t end + def test_psych_roundtrip_new_object + topic = Topic.new + assert topic + t = Psych.load Psych.dump topic + assert_equal topic.attributes, t.attributes + end rescue LoadError end end -- cgit v1.2.3 From eba8411652cb39529839083cf903f6ce76a69f4a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 5 Jan 2011 14:59:19 -0800 Subject: adding an `encode_with` method for Psych dump/load methods --- activerecord/lib/active_record/base.rb | 16 ++++++++++++++++ activerecord/test/cases/yaml_serialization_test.rb | 7 +++++++ 2 files changed, 23 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 0a2e436ecc..d3a739b98b 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1386,6 +1386,22 @@ MSG result end + # Populate +coder+ with attributes about this record that should be + # serialized. The structure of +coder+ defined in this method is + # guaranteed to match the structure of +coder+ passed to the +init_with+ + # method. + # + # Example: + # + # class Post < ActiveRecord::Base + # end + # coder = {} + # Post.new.encode_with(coder) + # coder # => { 'id' => nil, ... } + def encode_with(coder) + coder['attributes'] = attributes + end + # Initialize an empty model object from +coder+. +coder+ must contain # the attributes necessary for initializing an empty model object. For # example: diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index abcf61ffc0..0b54c309d1 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -18,6 +18,13 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal topic, t end + def test_encode_with_coder + topic = Topic.first + coder = {} + topic.encode_with coder + assert_equal({'attributes' => topic.attributes}, coder) + end + begin require 'psych' -- cgit v1.2.3 From f612069ae157ba4312672d59dd844a01197cac9e Mon Sep 17 00:00:00 2001 From: Ryan Bigg Date: Thu, 6 Jan 2011 20:06:52 +1000 Subject: Fix documentation for validates_uniqueness_of to NOT have a :scope argument as the prime example. Show scope examples after prime example. --- activerecord/lib/active_record/validations/uniqueness.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 853808eebf..e6a2b40403 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -85,11 +85,16 @@ module ActiveRecord # can be named "davidhh". # # class Person < ActiveRecord::Base - # validates_uniqueness_of :user_name, :scope => :account_id + # validates_uniqueness_of :user_name # end # - # It can also validate whether the value of the specified attributes are unique based on multiple - # scope parameters. For example, making sure that a teacher can only be on the schedule once + # It can also validate whether the value of the specified attributes are unique based on a scope parameter: + # + # class Person < ActiveRecord::Base + # validates_uniqueness_of :user_name, :scope => :account_id + # end + # + # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once # per semester for a particular class. # # class TeacherSchedule < ActiveRecord::Base -- cgit v1.2.3 From 8a1c5337808c53c5e9d2d842a09a90599c497e89 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 6 Jan 2011 15:15:36 -0800 Subject: no need for self --- activerecord/lib/active_record/associations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index bbf96f52ed..f6a7fd3ca0 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -134,7 +134,7 @@ module ActiveRecord def clear_association_cache #:nodoc: self.class.reflect_on_all_associations.to_a.each do |assoc| instance_variable_set "@#{assoc.name}", nil - end if self.persisted? + end if persisted? end private -- cgit v1.2.3 From 839f3bf6822ed3698df1e606c4215d650312f33e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 11:28:11 -0800 Subject: just use a hash for doing association caching --- activerecord/lib/active_record/associations.rb | 14 +++++++------- .../associations/class_methods/join_dependency.rb | 4 ++-- activerecord/lib/active_record/base.rb | 7 +++++-- .../cases/associations/belongs_to_associations_test.rb | 4 ++-- activerecord/test/cases/inheritance_test.rb | 2 +- activerecord/test/cases/modules_test.rb | 1 - 6 files changed, 17 insertions(+), 15 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index f6a7fd3ca0..78652ba0c5 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -132,24 +132,24 @@ module ActiveRecord # Clears out the association cache. def clear_association_cache #:nodoc: - self.class.reflect_on_all_associations.to_a.each do |assoc| - instance_variable_set "@#{assoc.name}", nil - end if persisted? + @association_cache.clear if persisted? end + # :nodoc: + attr_reader :association_cache + private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) - ivar = "@#{name}" - if instance_variable_defined?(ivar) - association = instance_variable_get(ivar) + if @association_cache.key? name + association = @association_cache[name] association if association.respond_to?(:loaded?) end end # Set the specified association instance. def association_instance_set(name, association) - instance_variable_set("@#{name}", association) + @association_cache[name] = association end # Associations are a set of macro-like class methods for tying objects together through diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb index bb6145656e..6263c4e3b0 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb @@ -176,7 +176,7 @@ module ActiveRecord join_part = join_parts.detect { |j| j.reflection.name.to_s == name && - j.parent_table_name == parent.class.table_name } + j.parent_table_name == parent.class.table_name } raise(ConfigurationError, "No such association") unless join_part @@ -201,7 +201,7 @@ module ActiveRecord macro = join_part.reflection.macro if macro == :has_one - return if record.instance_variable_defined?("@#{join_part.reflection.name}") + return if record.association_cache.key?(join_part.reflection.name) association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? set_target_and_inverse(join_part, association, record) else diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index d3a739b98b..f32132b18a 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1368,6 +1368,7 @@ MSG # hence you can't have attributes that aren't part of the table columns. def initialize(attributes = nil) @attributes = attributes_from_column_definition + @association_cache = {} @attributes_cache = {} @new_record = true @readonly = false @@ -1415,6 +1416,7 @@ MSG def init_with(coder) @attributes = coder['attributes'] @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} + @association_cache = {} @readonly = @destroyed = @marked_for_destruction = false @new_record = false _run_find_callbacks @@ -1627,8 +1629,9 @@ MSG end clear_aggregation_cache - clear_association_cache - @attributes_cache = {} + + @association_cache = {} + @attributes_cache = {} @new_record = true ensure_proper_type diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index aaa5421eea..1cb29a0fa1 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -78,14 +78,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key) - assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key") + assert citibank_result.association_cache.key?(:firm_with_primary_key) end def test_eager_loading_with_primary_key_as_symbol Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") citibank_result = Client.find(:first, :conditions => {:name => "Citibank"}, :include => :firm_with_primary_key_symbols) - assert_not_nil citibank_result.instance_variable_get("@firm_with_primary_key_symbols") + assert citibank_result.association_cache.key?(:firm_with_primary_key_symbols) end def test_creating_the_belonging_object diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 9fac9373eb..680d4ca5dd 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -203,7 +203,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_something_inherited account = Account.find(1, :include => :firm) - assert_not_nil account.instance_variable_get("@firm"), "nil proves eager load failed" + assert account.association_cache.key?(:firm), "nil proves eager load failed" end def test_eager_load_belongs_to_primary_key_quoting diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 14870cb0e2..a2041af16a 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -49,7 +49,6 @@ class ModulesTest < ActiveRecord::TestCase def test_find_account_and_include_company account = MyApplication::Billing::Account.find(1, :include => :firm) - assert_kind_of MyApplication::Business::Firm, account.instance_variable_get('@firm') assert_kind_of MyApplication::Business::Firm, account.firm end -- cgit v1.2.3 From 2ee4c8d90b6818867ad371907cb7d3c763318c3b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 11:48:29 -0800 Subject: only rescue from Mysql::Error exceptions [#6236 state:resolved] --- .../lib/active_record/connection_adapters/mysql_adapter.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 43de9c2090..47acf0b254 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -364,9 +364,14 @@ module ActiveRecord # statement API. For those queries, we need to use this method. :'( log(sql, name) do result = @connection.query(sql) - cols = result.fetch_fields.map { |field| field.name } - rows = result.to_a - result.free + cols = [] + rows = [] + + if result + cols = result.fetch_fields.map { |field| field.name } + rows = result.to_a + result.free + end ActiveRecord::Result.new(cols, rows) end end @@ -400,7 +405,7 @@ module ActiveRecord def begin_db_transaction #:nodoc: exec_without_stmt "BEGIN" - rescue Exception + rescue Mysql::Error # Transactions aren't supported end -- cgit v1.2.3 From f3d92f07565d1ec4b03fec7f3ba7c1c7d81e6073 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 13:41:38 -0800 Subject: method is never called with arguments --- activerecord/lib/active_record/aggregations.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 8cd7389005..0224187fed 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -223,14 +223,12 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) module_eval do - define_method(name) do |*args| - force_reload = args.first || false - + define_method(name) do unless instance_variable_defined?("@#{name}") instance_variable_set("@#{name}", nil) end - if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) + if (instance_variable_get("@#{name}").nil?) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) attrs = mapping.collect {|pair| read_attribute(pair.first)} object = case constructor when Symbol -- cgit v1.2.3 From 344a2d5adca154a4d13539421bdf5ea7865e0235 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 13:53:34 -0800 Subject: use a hash for caching aggregations rather than ivars --- activerecord/lib/active_record/aggregations.rb | 18 ++++++------------ activerecord/lib/active_record/base.rb | 5 +++-- 2 files changed, 9 insertions(+), 14 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 0224187fed..4b1fa5c59c 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -4,9 +4,7 @@ module ActiveRecord extend ActiveSupport::Concern def clear_aggregation_cache #:nodoc: - self.class.reflect_on_all_aggregations.to_a.each do |assoc| - instance_variable_set "@#{assoc.name}", nil - end if self.persisted? + @aggregation_cache.clear if persisted? end # Active Record implements aggregation through a macro-like class method called +composed_of+ @@ -224,11 +222,7 @@ module ActiveRecord def reader_method(name, class_name, mapping, allow_nil, constructor) module_eval do define_method(name) do - unless instance_variable_defined?("@#{name}") - instance_variable_set("@#{name}", nil) - end - - if (instance_variable_get("@#{name}").nil?) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) + if (@aggregation_cache[name].nil?) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) attrs = mapping.collect {|pair| read_attribute(pair.first)} object = case constructor when Symbol @@ -238,9 +232,9 @@ module ActiveRecord else raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.' end - instance_variable_set("@#{name}", object) + @aggregation_cache[name] = object end - instance_variable_get("@#{name}") + @aggregation_cache[name] end end @@ -251,7 +245,7 @@ module ActiveRecord define_method("#{name}=") do |part| if part.nil? && allow_nil mapping.each { |pair| self[pair.first] = nil } - instance_variable_set("@#{name}", nil) + @aggregation_cache[name] = nil else unless part.is_a?(class_name.constantize) || converter.nil? part = case converter @@ -265,7 +259,7 @@ module ActiveRecord end mapping.each { |pair| self[pair.first] = part.send(pair.last) } - instance_variable_set("@#{name}", part.freeze) + @aggregation_cache[name] = part.freeze end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f32132b18a..632c7aff4a 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1369,6 +1369,7 @@ MSG def initialize(attributes = nil) @attributes = attributes_from_column_definition @association_cache = {} + @aggregation_cache = {} @attributes_cache = {} @new_record = true @readonly = false @@ -1417,6 +1418,7 @@ MSG @attributes = coder['attributes'] @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} @association_cache = {} + @aggregation_cache = {} @readonly = @destroyed = @marked_for_destruction = false @new_record = false _run_find_callbacks @@ -1628,8 +1630,7 @@ MSG @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr]) end - clear_aggregation_cache - + @aggregation_cache = {} @association_cache = {} @attributes_cache = {} @new_record = true -- cgit v1.2.3 From 6e63e7a8745083e2a2556df268589f8bd9e7cd31 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 14:10:46 -0800 Subject: no need for parens --- activerecord/lib/active_record/aggregations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 4b1fa5c59c..44e595383e 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -222,7 +222,7 @@ module ActiveRecord def reader_method(name, class_name, mapping, allow_nil, constructor) module_eval do define_method(name) do - if (@aggregation_cache[name].nil?) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) attrs = mapping.collect {|pair| read_attribute(pair.first)} object = case constructor when Symbol -- cgit v1.2.3 From 2efd780dcb91b9df5eb05ae8b4837602a33c16fc Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 14:30:20 -0800 Subject: send() will raise an ArgumentError, so we should leverage ruby --- activerecord/lib/active_record/aggregations.rb | 22 ++++++---------------- activerecord/test/models/customer.rb | 2 +- 2 files changed, 7 insertions(+), 17 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 44e595383e..0bc26cc672 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -224,14 +224,9 @@ module ActiveRecord define_method(name) do if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) attrs = mapping.collect {|pair| read_attribute(pair.first)} - object = case constructor - when Symbol - class_name.constantize.send(constructor, *attrs) - when Proc, Method - constructor.call(*attrs) - else - raise ArgumentError, 'Constructor must be a symbol denoting the constructor method to call or a Proc to be invoked.' - end + object = constructor.respond_to?(:call) ? + constructor.call(*attrs) : + class_name.constantize.send(constructor, *attrs) @aggregation_cache[name] = object end @aggregation_cache[name] @@ -248,14 +243,9 @@ module ActiveRecord @aggregation_cache[name] = nil else unless part.is_a?(class_name.constantize) || converter.nil? - part = case converter - when Symbol - class_name.constantize.send(converter, part) - when Proc, Method - converter.call(part) - else - raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.' - end + part = converter.respond_to?(:call) ? + converter.call(part) : + class_name.constantize.send(converter, part) end mapping.each { |pair| self[pair.first] = part.send(pair.last) } diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index e258ccdb6c..777f6b5ba0 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -70,4 +70,4 @@ class Fullname def to_s "#{first} #{last.upcase}" end -end \ No newline at end of file +end -- cgit v1.2.3 From 441118458d57011ee1b1f1dcfea558de462c6da9 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 6 Jan 2011 17:01:24 +0000 Subject: Use encode_with for marshalling --- activerecord/lib/active_record/base.rb | 20 ++++++++++++++++++++ activerecord/test/cases/base_test.rb | 7 +++++++ 2 files changed, 27 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 632c7aff4a..1079094bbf 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -870,6 +870,16 @@ module ActiveRecord #:nodoc: reset_scoped_methods end + # Specifies how the record is loaded by +Marshal+. + # + # +_load+ sets an instance variable for each key in the hash it takes as input. + # Override this method if you require more complex marshalling. + def _load(data) + record = allocate + record.init_with(Marshal.load(data)) + record + end + private def relation #:nodoc: @@ -1425,6 +1435,16 @@ MSG _run_initialize_callbacks end + # Specifies how the record is dumped by +Marshal+. + # + # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this + # method if you require more complex marshalling. + def _dump(level) + dump = {} + encode_with(dump) + Marshal.dump(dump) + end + # Returns a String, which Action Pack uses for constructing an 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/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 594f3b80c6..a58d5dec81 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1503,4 +1503,11 @@ class BasicsTest < ActiveRecord::TestCase ensure Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) end + + def test_marshal_round_trip + expected = posts(:welcome) + actual = Marshal.load(Marshal.dump(expected)) + + assert_equal expected.attributes, actual.attributes + end end -- cgit v1.2.3 From 770e6893b9f2aaaebe3de10576931dc7194451bc Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 6 Jan 2011 18:04:32 +0000 Subject: Construct an actual ActiveRecord::Relation object for the association scope, rather than a hash which is passed to apply_finder_options. This allows more flexibility in how the scope is created, for example because scope.where(a, b) and scope.where(a).where(b) mean different things. --- .../associations/association_collection.rb | 27 ++++------ .../associations/association_proxy.rb | 58 ++++++++++++++++------ .../associations/belongs_to_association.rb | 17 ------- .../has_and_belongs_to_many_association.rb | 16 +++--- .../active_record/associations/has_association.rb | 28 ----------- .../associations/has_many_association.rb | 4 +- .../associations/has_one_association.rb | 14 ++---- .../associations/through_association.rb | 21 +++----- .../associations/has_one_associations_test.rb | 8 --- activerecord/test/cases/reflection_test.rb | 4 +- activerecord/test/cases/relation_scoping_test.rb | 3 +- activerecord/test/models/company.rb | 1 - 12 files changed, 76 insertions(+), 125 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index bd27365b08..f33a9ce732 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -333,23 +333,16 @@ module ActiveRecord protected - def finder_options - { - :conditions => construct_conditions, - :select => construct_select, - :readonly => @reflection.options[:readonly], - :order => @reflection.options[:order], - :limit => @reflection.options[:limit], - :include => @reflection.options[:include], - :joins => @reflection.options[:joins], - :group => @reflection.options[:group], - :having => @reflection.options[:having], - :offset => @reflection.options[:offset] - } - end - - def construct_select - @reflection.options[:select] || + def association_scope + options = @reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) + super.apply_finder_options(options) + end + + def select_value + super || uniq_select_value + end + + def uniq_select_value @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 8e64056a06..405a0307c1 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -165,9 +165,7 @@ module ActiveRecord end def scoped - target_scope. - apply_finder_options(@finder_options). - create_with(@creation_attributes) + target_scope & @association_scope end protected @@ -180,27 +178,32 @@ module ActiveRecord @reflection.klass.send(:sanitize_sql, sql, table_name) end - # Construct the data used for the scope for this association + # Construct the scope for this association. # - # Note that we don't actually build the scope here, we just construct the options and - # attributes. We must only build the scope when it's actually needed, because at that - # point the call may be surrounded by scope.scoping { ... } or with_scope { ... } etc, - # which affects the scope which actually gets built. + # Note that the association_scope is merged into the targed_scope only when the + # scoped 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 construct_scope - if target_klass - @finder_options = finder_options - @creation_attributes = creation_attributes - end + @association_scope = association_scope if target_klass + end + + def association_scope + scope = target_klass.unscoped + scope = scope.create_with(creation_attributes) + scope = scope.apply_finder_options(@reflection.options.slice(:conditions, :readonly, :include)) + scope = scope.where(construct_owner_conditions) + scope = scope.select(select_value) if select_value = self.select_value + scope end - # Implemented by subclasses - def finder_options - raise NotImplementedError + def select_value + @reflection.options[:select] end # Implemented by (some) subclasses def creation_attributes - {} + { } end def aliased_table @@ -226,6 +229,29 @@ module ActiveRecord target_klass.scoped end + # Returns a hash linking the owner to the association represented by the reflection + def construct_owner_attributes(reflection = @reflection) + attributes = {} + if reflection.macro == :belongs_to + attributes[reflection.association_primary_key] = @owner[reflection.foreign_key] + else + attributes[reflection.foreign_key] = @owner[reflection.active_record_primary_key] + + if reflection.options[:as] + attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name + end + end + attributes + end + + # Builds an array of arel nodes from the owner attributes hash + def construct_owner_conditions(table = aliased_table, reflection = @reflection) + conditions = construct_owner_attributes(reflection).map do |attr, value| + table[attr].eq(value) + end + table.create_and(conditions) + end + private # Forwards any missing method call to the \target. def method_missing(method, *args) diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index b50ee689cd..391471849c 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -58,23 +58,6 @@ module ActiveRecord scoped.first.tap { |record| set_inverse_instance(record) } end - def finder_options - { - :conditions => construct_conditions, - :select => @reflection.options[:select], - :include => @reflection.options[:include], - :readonly => @reflection.options[:readonly] - } - end - - def construct_conditions - conditions = aliased_table[@reflection.association_primary_key]. - eq(@owner[@reflection.foreign_key]) - - conditions = conditions.and(Arel.sql(sql_conditions)) if sql_conditions - conditions - end - def foreign_key_present? !@owner[@reflection.foreign_key].nil? end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index b18ec23037..bc7894173d 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -88,14 +88,14 @@ module ActiveRecord super(join_table) end - def finder_options - super.merge( - :joins => construct_joins, - :readonly => ambiguous_select?(@reflection.options[:select]), - :select => @reflection.options[:select] || [ - @reflection.klass.arel_table[Arel.star], - join_table[Arel.star]] - ) + def association_scope + scope = super.joins(construct_joins) + scope = scope.readonly if ambiguous_select?(@reflection.options[:select]) + scope + end + + def select_value + super || [@reflection.klass.arel_table[Arel.star], join_table[Arel.star]] end # Join tables with additional columns on top of the two foreign keys must be considered diff --git a/activerecord/lib/active_record/associations/has_association.rb b/activerecord/lib/active_record/associations/has_association.rb index 190fed77c2..8b180c7301 100644 --- a/activerecord/lib/active_record/associations/has_association.rb +++ b/activerecord/lib/active_record/associations/has_association.rb @@ -9,34 +9,6 @@ module ActiveRecord construct_owner_attributes.each { |key, value| record[key] = value } end end - - # Returns a hash linking the owner to the association represented by the reflection - def construct_owner_attributes(reflection = @reflection) - attributes = {} - if reflection.macro == :belongs_to - attributes[reflection.association_primary_key] = @owner.send(reflection.foreign_key) - else - attributes[reflection.foreign_key] = @owner.send(reflection.active_record_primary_key) - - if reflection.options[:as] - attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name - end - end - attributes - end - - # Builds an array of arel nodes from the owner attributes hash - def construct_owner_conditions(table = aliased_table, reflection = @reflection) - construct_owner_attributes(reflection).map do |attr, value| - table[attr].eq(value) - end - end - - def construct_conditions - conditions = construct_owner_conditions - conditions << Arel.sql(sql_conditions) if sql_conditions - aliased_table.create_and(conditions) - end end 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 dba26a78da..b07441f3c6 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -69,9 +69,7 @@ module ActiveRecord end end - def creation_attributes - construct_owner_attributes - end + alias creation_attributes construct_owner_attributes end end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index f1e01197b5..5107be1eaf 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -68,19 +68,11 @@ module ActiveRecord scoped.first.tap { |record| set_inverse_instance(record) } end - def finder_options - { - :conditions => construct_conditions, - :select => @reflection.options[:select], - :include => @reflection.options[:include], - :readonly => @reflection.options[:readonly], - :order => @reflection.options[:order] - } + def association_scope + super.order(@reflection.options[:order]) end - def creation_attributes - construct_owner_attributes - end + alias creation_attributes construct_owner_attributes def new_record record = scoped.scoping { yield @reflection } diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 65c36419da..d2112fb2b6 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -9,12 +9,12 @@ module ActiveRecord super & @reflection.through_reflection.klass.scoped end - def finder_options - super.merge( - :joins => construct_joins, - :include => @reflection.options[:include] || - @reflection.source_reflection.options[:include] - ) + def association_scope + scope = super.joins(construct_joins).where(conditions) + unless @reflection.options[:include] + scope = scope.includes(@reflection.source_reflection.options[:include]) + end + scope end # This scope affects the creation of the associated records (not the join records). At the @@ -98,18 +98,13 @@ module ActiveRecord end def build_conditions - association_conditions = @reflection.options[:conditions] through_conditions = build_through_conditions source_conditions = @reflection.source_reflection.options[:conditions] uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - if association_conditions || through_conditions || source_conditions || uses_sti + if through_conditions || source_conditions || uses_sti all = [] - - [association_conditions, source_conditions].each do |conditions| - all << interpolate_sql(sanitize_sql(conditions)) if conditions - end - + all << interpolate_sql(sanitize_sql(source_conditions)) if source_conditions all << through_conditions if through_conditions all << build_sti_condition if uses_sti diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index fa36b527a2..8203534a37 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -227,14 +227,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase firm.destroy end - def test_finding_with_interpolated_condition - firm = Firm.find(:first) - superior = firm.clients.create(:name => 'SuperiorCo') - superior.rating = 10 - superior.save - assert_equal 10, firm.clients_with_interpolated_conditions.first.rating - end - def test_assignment_before_child_saved firm = Firm.find(1) firm.account = a = Account.new("credit_limit" => 1000) diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index d75bc3982e..eb580928ba 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -181,8 +181,8 @@ class ReflectionTest < ActiveRecord::TestCase def test_reflection_of_all_associations # FIXME these assertions bust a lot - assert_equal 37, Firm.reflect_on_all_associations.size - assert_equal 27, Firm.reflect_on_all_associations(:has_many).size + assert_equal 36, Firm.reflect_on_all_associations.size + assert_equal 26, Firm.reflect_on_all_associations(:has_many).size assert_equal 10, Firm.reflect_on_all_associations(:has_one).size assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index bff0151f1a..1bdf3136d4 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -259,7 +259,8 @@ class HasManyScopingTest< ActiveRecord::TestCase end def test_should_default_scope_on_associations_is_overriden_by_association_conditions - assert_equal [], people(:michael).fixed_bad_references + reference = references(:michael_unicyclist).becomes(BadReference) + assert_equal [reference], people(:michael).fixed_bad_references end def test_should_maintain_default_scope_on_eager_loaded_associations diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index d08e593db1..f6e7a5ccf7 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -49,7 +49,6 @@ class Firm < Company has_many :exclusively_dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all has_many :limited_clients, :class_name => "Client", :limit => 1 has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id" - has_many :clients_with_interpolated_conditions, :class_name => "Client", :conditions => 'rating > #{rating}' has_many :clients_like_ms_with_hash_conditions, :conditions => { :name => 'Microsoft' }, :class_name => "Client", :order => "id" has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' has_many :clients_using_multiline_sql, :class_name => "Client", :finder_sql => ' -- cgit v1.2.3 From 45d0d18baef2de739dae89bb7bc79826392bbde5 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 6 Jan 2011 19:32:05 +0000 Subject: Not really worth having the HasAssociation module for just a single method --- activerecord/lib/active_record/associations.rb | 1 - .../active_record/associations/association_collection.rb | 2 -- .../lib/active_record/associations/association_proxy.rb | 11 +++++++++-- .../lib/active_record/associations/has_association.rb | 14 -------------- .../lib/active_record/associations/has_one_association.rb | 2 -- 5 files changed, 9 insertions(+), 21 deletions(-) delete mode 100644 activerecord/lib/active_record/associations/has_association.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 78652ba0c5..3e7c9a370d 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -120,7 +120,6 @@ module ActiveRecord # So there is no need to eager load them. autoload :AssociationCollection, 'active_record/associations/association_collection' autoload :AssociationProxy, 'active_record/associations/association_proxy' - autoload :HasAssociation, 'active_record/associations/has_association' autoload :ThroughAssociation, 'active_record/associations/through_association' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index f33a9ce732..daec8493ac 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -18,8 +18,6 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class AssociationCollection < AssociationProxy #:nodoc: - include HasAssociation - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped def select(select = nil) diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 405a0307c1..294e1cab50 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -9,11 +9,11 @@ module ActiveRecord # AssociationProxy # BelongsToAssociation # BelongsToPolymorphicAssociation - # AssociationCollection + HasAssociation + # AssociationCollection # HasAndBelongsToManyAssociation # HasManyAssociation # HasManyThroughAssociation + ThroughAssociation - # HasOneAssociation + HasAssociation + # HasOneAssociation # HasOneThroughAssociation + ThroughAssociation # # Association proxies in Active Record are middlemen between the object that @@ -252,6 +252,13 @@ module ActiveRecord table.create_and(conditions) end + # Sets the owner attributes on the given record + def set_owner_attributes(record) + if @owner.persisted? + construct_owner_attributes.each { |key, value| record[key] = value } + end + end + private # Forwards any missing method call to the \target. def method_missing(method, *args) diff --git a/activerecord/lib/active_record/associations/has_association.rb b/activerecord/lib/active_record/associations/has_association.rb deleted file mode 100644 index 8b180c7301..0000000000 --- a/activerecord/lib/active_record/associations/has_association.rb +++ /dev/null @@ -1,14 +0,0 @@ -module ActiveRecord - module Associations - # Included in all has_* associations (i.e. everything except belongs_to) - module HasAssociation #:nodoc: - protected - # Sets the owner attributes on the given record - def set_owner_attributes(record) - if @owner.persisted? - construct_owner_attributes.each { |key, value| record[key] = value } - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 5107be1eaf..bee5700bfc 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,8 +2,6 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < AssociationProxy #:nodoc: - include HasAssociation - def create(attrs = {}) new_record do |reflection| attrs = merge_with_conditions(attrs) -- cgit v1.2.3 From 5ecf6922487b5476509af2a89137c3cd3791f7ab Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 6 Jan 2011 19:35:19 +0000 Subject: merge_with_conditions is not necessary because the conditions will already be in the scope_for_create hash in the scope --- .../lib/active_record/associations/has_one_association.rb | 9 --------- 1 file changed, 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index bee5700bfc..285c2631ad 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -4,21 +4,18 @@ module ActiveRecord class HasOneAssociation < AssociationProxy #:nodoc: def create(attrs = {}) new_record do |reflection| - attrs = merge_with_conditions(attrs) reflection.create_association(attrs) end end def create!(attrs = {}) new_record do |reflection| - attrs = merge_with_conditions(attrs) reflection.create_association!(attrs) end end def build(attrs = {}) new_record do |reflection| - attrs = merge_with_conditions(attrs) reflection.build_association(attrs) end end @@ -77,12 +74,6 @@ module ActiveRecord replace(record, true) record end - - def merge_with_conditions(attrs={}) - attrs ||= {} - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - attrs - end end end end -- cgit v1.2.3 From d23c332e02932a6a06853193c79a21d70dbe139e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 6 Jan 2011 19:42:31 +0000 Subject: Clean up create, create! and build in HasOneAssociation --- .../associations/has_one_association.rb | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 285c2631ad..158ae376e1 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -2,22 +2,16 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < AssociationProxy #:nodoc: - def create(attrs = {}) - new_record do |reflection| - reflection.create_association(attrs) - end + def create(attributes = {}) + new_record(:create_association, attributes) end - def create!(attrs = {}) - new_record do |reflection| - reflection.create_association!(attrs) - end + def create!(attributes = {}) + new_record(:create_association!, attributes) end - def build(attrs = {}) - new_record do |reflection| - reflection.build_association(attrs) - end + def build(attributes = {}) + new_record(:build_association, attributes) end def replace(obj, dont_save = false) @@ -69,8 +63,8 @@ module ActiveRecord alias creation_attributes construct_owner_attributes - def new_record - record = scoped.scoping { yield @reflection } + def new_record(method, attributes) + record = scoped.scoping { @reflection.send(method, attributes) } replace(record, true) record end -- cgit v1.2.3 From 5b28e5254267c581ce6934408aaf458616e44e3f Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Thu, 6 Jan 2011 19:54:23 +0000 Subject: Don't not remove double negatives --- .../lib/active_record/associations/has_one_association.rb | 8 ++++---- activerecord/test/cases/associations/inverse_associations_test.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 158ae376e1..7503130e8c 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -14,11 +14,11 @@ module ActiveRecord new_record(:build_association, attributes) end - def replace(obj, dont_save = false) + def replace(obj, save = true) load_target unless @target.nil? || @target == obj - if @reflection.options[:dependent] && !dont_save + if @reflection.options[:dependent] && save case @reflection.options[:dependent] when :delete @target.delete if @target.persisted? @@ -45,7 +45,7 @@ module ActiveRecord set_inverse_instance(obj) loaded - unless !@owner.persisted? || obj.nil? || dont_save + unless !@owner.persisted? || obj.nil? || !save return (obj.save ? self : false) else return (obj.nil? ? nil : self) @@ -65,7 +65,7 @@ module ActiveRecord def new_record(method, attributes) record = scoped.scoping { @reflection.send(method, attributes) } - replace(record, true) + replace(record, false) record end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 4cca78da9d..e9a57a00a0 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -161,7 +161,7 @@ class InverseHasOneTests < ActiveRecord::TestCase def test_parent_instance_should_be_shared_with_replaced_via_method_child m = Man.find(:first) f = Face.new(:description => 'haunted') - m.face.replace(f, false) + m.face.replace(f) assert_not_nil f.man assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = 'Bongo' -- cgit v1.2.3 From 82b0ce9c978a1b73d5974604b90c5b7299eed65a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 7 Jan 2011 00:28:27 +0000 Subject: Refactor HasOneAssociation#replace --- .../associations/has_one_association.rb | 48 ++++++++++------------ 1 file changed, 22 insertions(+), 26 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 7503130e8c..739bb919c5 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -14,41 +14,27 @@ module ActiveRecord new_record(:build_association, attributes) end - def replace(obj, save = true) + def replace(record, save = true) + record = record.target if AssociationProxy === record + raise_on_type_mismatch(record) unless record.nil? load_target - unless @target.nil? || @target == obj - if @reflection.options[:dependent] && save - case @reflection.options[:dependent] - when :delete - @target.delete if @target.persisted? - when :destroy - @target.destroy if @target.persisted? - when :nullify - @target[@reflection.foreign_key] = nil - @target.save if @owner.persisted? && @target.persisted? - end - else - @target[@reflection.foreign_key] = nil - @target.save if @owner.persisted? && @target.persisted? - end + if @target && @target != record + remove_target(save && @reflection.options[:dependent]) end - if obj.nil? - @target = nil - else - raise_on_type_mismatch(obj) - set_owner_attributes(obj) - @target = (AssociationProxy === obj ? obj.target : obj) + if record + set_owner_attributes(record) + set_inverse_instance(record) end - set_inverse_instance(obj) + @target = record loaded - unless !@owner.persisted? || obj.nil? || !save - return (obj.save ? self : false) + if @owner.persisted? && record && save + record.save && self else - return (obj.nil? ? nil : self) + record && self end end @@ -68,6 +54,16 @@ module ActiveRecord replace(record, false) record end + + def remove_target(method) + case method + when :delete, :destroy + @target.send(method) + else + @target[@reflection.foreign_key] = nil + @target.save if @target.persisted? && @owner.persisted? + end + end end end end -- cgit v1.2.3 From 63ed6ca9989fbfcd2b323b98e4110c7504b8d3db Mon Sep 17 00:00:00 2001 From: Jeremy Kemper Date: Fri, 7 Jan 2011 16:24:06 -0800 Subject: Add test for e0e3adf --- activerecord/test/cases/attribute_methods_test.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index e3cbae4a84..a29e4349d6 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -247,6 +247,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase # puts "" end + def test_read_overridden_attribute + topic = Topic.new(:title => 'a') + def topic.title() 'b' end + assert_equal 'a', topic[:title] + end + def test_query_attribute_string [nil, "", " "].each do |value| assert_equal false, Topic.new(:author_name => value).author_name? -- cgit v1.2.3 From 1e2ab564f9680fe8ac4fbd55c36eb420f46498e6 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 16:00:05 -0800 Subject: fewer funcalls to the cached attributes variable --- activerecord/lib/active_record/attribute_methods/read.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 506f6e878f..2001b6522d 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -20,7 +20,7 @@ module ActiveRecord # be cached. Usually caching only pays off for attributes with expensive conversion # methods, like time related columns (e.g. +created_at+, +updated_at+). def cache_attributes(*attribute_names) - attribute_names.each {|attr| cached_attributes << attr.to_s} + cached_attributes.merge attribute_names.map { |attr| attr.to_s } end # Returns the attributes which are cached. By default time related columns -- cgit v1.2.3 From 33ebf9bd56427b424aa8fe12f7b4361cb9fd8414 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 16:00:20 -0800 Subject: String#insert() mutates the string, so no need for lasgn --- activerecord/lib/active_record/attribute_methods/read.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 2001b6522d..8b56f24acf 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -64,7 +64,7 @@ module ActiveRecord access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" unless attr_name.to_s == self.primary_key.to_s - access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") + access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") end if cache_attribute?(attr_name) -- cgit v1.2.3 From 84f81f57793689a1c5b5ef1fec800a29c96c30c5 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 16:50:44 -0800 Subject: no need for to_sym --- activerecord/lib/active_record/attribute_methods/read.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 8b56f24acf..b77076983e 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -39,7 +39,7 @@ module ActiveRecord if serialized_attributes.include?(attr_name) define_read_method_for_serialized_attribute(attr_name) else - define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name]) + define_read_method(attr_name, attr_name, columns_hash[attr_name]) end if attr_name == primary_key && attr_name != "id" -- cgit v1.2.3 From 36d7bd189818eb1bb0df4f9472113cf70cd652de Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 18:45:17 -0800 Subject: stop creating intermediate AR objects, just construct AR objects from a list of hashes --- activerecord/lib/active_record/relation/finder_methods.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8bbc47ab75..aa9d468bba 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -188,7 +188,8 @@ module ActiveRecord def find_with_associations including = (@eager_load_values + @includes_values).uniq join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, []) - rows = construct_relation_for_association_find(join_dependency).to_a + relation = construct_relation_for_association_find(join_dependency) + rows = connection.exec_query(relation.to_sql, 'SQL', relation.bind_values) join_dependency.instantiate(rows) rescue ThrowResult [] -- cgit v1.2.3 From ec960c3730ed34b3163a267b665daf6d7d024028 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 7 Jan 2011 18:45:30 -0800 Subject: join the cult of cargo. reduce the number of NoMethodErrors in the system --- activerecord/lib/active_record/attribute_methods/read.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index b77076983e..660fa9a564 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -55,7 +55,7 @@ module ActiveRecord # Define read method for serialized attribute. def define_read_method_for_serialized_attribute(attr_name) access_code = "@attributes_cache['#{attr_name}'] ||= unserialize_attribute('#{attr_name}')" - generated_attribute_methods.module_eval("def #{attr_name}; #{access_code}; end", __FILE__, __LINE__) + generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__) end # Define an attribute reader method. Cope with nil column. -- cgit v1.2.3 From 3b677aa0060ca70f7589c708b92d61648e9157eb Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 8 Jan 2011 19:59:31 -0800 Subject: use select_all because not all database adapters support bind values --- activerecord/lib/active_record/relation/finder_methods.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index aa9d468bba..e447de92a4 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -189,7 +189,7 @@ module ActiveRecord including = (@eager_load_values + @includes_values).uniq join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, []) relation = construct_relation_for_association_find(join_dependency) - rows = connection.exec_query(relation.to_sql, 'SQL', relation.bind_values) + rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values) join_dependency.instantiate(rows) rescue ThrowResult [] -- cgit v1.2.3 From 12f5158f098cdc714e826bfb0d3f722a1e9753c8 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Sat, 8 Jan 2011 20:15:15 -0800 Subject: remove unused string substitution --- .../lib/active_record/connection_adapters/sqlite_adapter.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index d76fc4103e..bf599a95f7 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -314,12 +314,7 @@ module ActiveRecord protected def select(sql, name = nil, binds = []) #:nodoc: - result = exec_query(sql, name, binds) - columns = result.columns.map { |column| - column.sub(/^"?\w+"?\./, '') - } - - result.rows.map { |row| Hash[columns.zip(row)] } + exec_query(sql, name, binds).to_a end def table_structure(table_name) -- cgit v1.2.3 From 4690bee301588b92fe8c0ae1026c14336e1be57c Mon Sep 17 00:00:00 2001 From: Katrina Owen Date: Thu, 6 Jan 2011 12:08:22 +0100 Subject: Adding postgresql template option when executing db:test:clone_structure Specify the template to use in config/database.yml, e.g. test: adapter: postgresql template: template_postgis If no template is specified, postgresql defaults to template1 --- activerecord/lib/active_record/railties/databases.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index a4fc18148e..9924755ddf 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -409,7 +409,7 @@ db_namespace = namespace :db do ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] - `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs["test"]["database"]}` + `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs["test"]["database"]} #{abcs["test"]["template"]}` when "sqlite", "sqlite3" dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"] `#{abcs["test"]["adapter"]} #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` -- cgit v1.2.3 From 3e2465521bdcbae978b445987af9b5fc68c43a3d Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Fri, 19 Nov 2010 13:46:57 -0800 Subject: Aligning master changelog w/ 3-0-stable --- activerecord/CHANGELOG | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 8ebc87145c..aa447ce992 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -103,7 +103,15 @@ IrreversibleMigration exception will be raised when going down. [Aaron Patterson] -*Rails 3.0.2 (unreleased)* + +*Rails 3.0.3 (November 16, 2010)* + +* Support find by class like this: Post.where(:name => Post) + + +*Rails 3.0.2 (November 15, 2010)* + +* Dramatic speed increase (see: http://engineering.attinteractive.com/2010/10/arel-two-point-ohhhhh-yaaaaaa/) [Aaron Patterson] * reorder is deprecated in favor of except(:order).order(...) [Santiago Pastorino] -- cgit v1.2.3 From 06165856196ac17b87163d146abea46019b17032 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 10 Jan 2011 11:36:23 -0800 Subject: use SQLite3::VERSION rather than the deprecated class --- activerecord/test/cases/query_cache_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 33916c4e46..53aefc7b58 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -63,7 +63,7 @@ class QueryCacheTest < ActiveRecord::TestCase # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter) && SQLite3::Version::VERSION > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) + elsif current_adapter?(:SQLite3Adapter) && SQLite3::VERSION > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") -- cgit v1.2.3 From f4f4964ce0a05cac38bbff3b308ac558228bad29 Mon Sep 17 00:00:00 2001 From: Raimonds Simanovskis Date: Mon, 10 Jan 2011 18:55:32 +0200 Subject: Always return decimal average of integer fields In previous version if database adapter (e.g. SQLite and Oracle) returned non-String calculated values then type_cast_using_column converted decimal average value of intefer field to integer value. Now operation parameter is always checked to decide which conversion of calculated value should be done. --- activerecord/lib/active_record/relation/calculations.rb | 14 +++++--------- activerecord/test/cases/calculations_test.rb | 5 +++++ 2 files changed, 10 insertions(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index e9e451ec5c..b75a65e3ca 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -282,15 +282,11 @@ module ActiveRecord end def type_cast_calculated_value(value, column, operation = nil) - if value.is_a?(String) || value.nil? - case operation - when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || '0', column) - when 'average' then value.try(:to_d) - else type_cast_using_column(value, column) - end - else - type_cast_using_column(value, column) + case operation + when 'count' then value.to_i + when 'sum' then type_cast_using_column(value || '0', column) + when 'average' then value.try(:to_d) + else type_cast_using_column(value, column) end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 5cb8485b4b..644c9cb528 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -23,6 +23,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 53.0, value end + def test_should_return_decimal_average_of_integer_field + value = Account.average(:id) + assert_equal 3.5, value + end + def test_should_return_nil_as_average assert_nil NumericData.average(:bank_balance) end -- cgit v1.2.3 From a60ea742226f09dc566ad5d9a0b465c5d5db9687 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 10 Jan 2011 17:30:40 -0800 Subject: only use one array when collecting split up queries --- activerecord/lib/active_record/association_preload.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 5aad2b4558..dc15b2f4c3 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -415,7 +415,7 @@ module ActiveRecord in_clause_length = connection.in_clause_length || ids.size records = [] ids.each_slice(in_clause_length) do |some_ids| - records += yield(some_ids) + records.concat yield(some_ids) end records end -- cgit v1.2.3 From 7d4d7457301faa85aa32b5ae29e13976e828954f Mon Sep 17 00:00:00 2001 From: Ernie Miller Date: Thu, 6 Jan 2011 20:06:29 -0500 Subject: Fix polymorphic belongs_to associationproxy raising errors when loading target. --- .../associations/belongs_to_polymorphic_association.rb | 5 +++++ .../test/cases/associations/belongs_to_associations_test.rb | 9 +++++++++ activerecord/test/models/sponsor.rb | 2 ++ 3 files changed, 16 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 4f67b02d00..cae35fe0d0 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -4,6 +4,11 @@ module ActiveRecord class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: private + def conditions + @conditions ||= interpolate_sql(target_klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions] + end + alias :sql_conditions :conditions + def replace_keys(record) super @owner[@reflection.foreign_type] = record && record.class.base_class.name diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 1cb29a0fa1..4c4891dcaf 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -146,6 +146,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_not_nil Company.find(3).firm_with_condition, "Microsoft should have a firm" end + def test_with_polymorphic_and_condition + sponsor = Sponsor.create + member = Member.create :name => "Bert" + sponsor.sponsorable = member + + assert_equal member, sponsor.sponsorable + assert_nil sponsor.sponsorable_with_conditions + end + def test_with_select assert_equal Company.find(2).firm_with_select.attributes.size, 1 assert_equal Company.find(2, :include => :firm_with_select ).firm_with_select.attributes.size, 1 diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb index 7e5a1dc38b..aa4a3638fd 100644 --- a/activerecord/test/models/sponsor.rb +++ b/activerecord/test/models/sponsor.rb @@ -2,4 +2,6 @@ class Sponsor < ActiveRecord::Base belongs_to :sponsor_club, :class_name => "Club", :foreign_key => "club_id" belongs_to :sponsorable, :polymorphic => true belongs_to :thing, :polymorphic => true, :foreign_type => :sponsorable_type, :foreign_key => :sponsorable_id + belongs_to :sponsorable_with_conditions, :polymorphic => true, + :foreign_type => 'sponsorable_type', :foreign_key => 'sponsorable_id', :conditions => {:name => 'Ernie'} end -- cgit v1.2.3 From f6b71dea15435ea91e4db27ddf657ff840fd3a72 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 11 Jan 2011 13:38:17 -0800 Subject: avoid splatting arrays by using concat --- activerecord/lib/active_record/association_preload.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index dc15b2f4c3..bb7ddfcae9 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -126,7 +126,7 @@ module ActiveRecord parent_records.each do |parent_record| association_proxy = parent_record.send(reflection_name) association_proxy.loaded - association_proxy.target.push(*Array.wrap(associated_record)) + association_proxy.target.concat(Array.wrap(associated_record)) association_proxy.send(:set_inverse_instance, associated_record) end end @@ -139,8 +139,8 @@ module ActiveRecord def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) associated_records.each do |associated_record| - mapped_records = id_to_record_map[associated_record[key].to_s] - add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record) + parent_records = id_to_record_map[associated_record[key].to_s] + add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) end end -- cgit v1.2.3 From 1390a443289dbe4bfed63bfd3192c8c6a29fd833 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 7 Jan 2011 15:28:47 +0000 Subject: Correctly indent the bullet points under 'One-to-one associations', so that the lines are not broken in the generated rdoc html --- activerecord/lib/active_record/associations.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 3e7c9a370d..7659bd2d37 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -332,26 +332,26 @@ module ActiveRecord # === One-to-one associations # # * Assigning an object to a +has_one+ association automatically saves that object and - # the object being replaced (if there is one), in order to update their primary - # keys - except if the parent object is unsaved (new_record? == true). + # the object being replaced (if there is one), in order to update their primary + # keys - except if the parent object is unsaved (new_record? == true). # * If either of these saves fail (due to one of the objects being invalid) the assignment - # statement returns +false+ and the assignment is cancelled. + # statement returns +false+ and the assignment is cancelled. # * If you wish to assign an object to a +has_one+ association without saving it, - # use the association.build method (documented below). + # use the association.build method (documented below). # * Assigning an object to a +belongs_to+ association does not save the object, since - # the foreign key field belongs on the parent. It does not save the parent either. + # the foreign key field belongs on the parent. It does not save the parent either. # # === Collections # # * Adding an object to a collection (+has_many+ or +has_and_belongs_to_many+) automatically - # saves that object, except if the parent object (the owner of the collection) is not yet - # stored in the database. + # saves that object, except if the parent object (the owner of the collection) is not yet + # stored in the database. # * If saving any of the objects being added to a collection (via push or similar) - # fails, then push returns +false+. + # fails, then push returns +false+. # * You can add an object to a collection without automatically saving it by using the - # collection.build method (documented below). + # collection.build method (documented below). # * All unsaved (new_record? == true) members of the collection are automatically - # saved when the parent is saved. + # saved when the parent is saved. # # === Association callbacks # -- cgit v1.2.3 From 00dc8f77a2f9962d1bcd6267c63632bb39e6c547 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 7 Jan 2011 15:31:01 +0000 Subject: For a singular association, it should be build_association, rather than association.build (as association may be nil) --- activerecord/lib/active_record/associations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 7659bd2d37..5f29f7c6f2 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -337,7 +337,7 @@ module ActiveRecord # * If either of these saves fail (due to one of the objects being invalid) the assignment # statement returns +false+ and the assignment is cancelled. # * If you wish to assign an object to a +has_one+ association without saving it, - # use the association.build method (documented below). + # use the build_association method (documented below). # * Assigning an object to a +belongs_to+ association does not save the object, since # the foreign key field belongs on the parent. It does not save the parent either. # -- cgit v1.2.3 From 15adcc3927b96328f3375aedd7d829f5e582854c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 7 Jan 2011 15:34:57 +0000 Subject: Remove incorrect documentation about build_assoc on has_one. This is proven, for example, by test_successful_build_association in has_one_associations_test.rb --- activerecord/lib/active_record/associations.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 5f29f7c6f2..d26519e7d5 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1021,8 +1021,7 @@ module ActiveRecord # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not - # yet been saved. Note: This ONLY works if an association already exists. - # It will NOT work if the association is +nil+. + # yet been saved. # [create_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+, linked to this object through a foreign key, and that -- cgit v1.2.3 From 665880c0809b563d3afaeadd073f5d0b6289a42e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 7 Jan 2011 18:27:33 +0000 Subject: Return value is irrelevant here as the RHS of the assignment is always returned by methods ending in '=' --- activerecord/lib/active_record/associations.rb | 1 - 1 file changed, 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index d26519e7d5..8907c9c487 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1493,7 +1493,6 @@ module ActiveRecord end association.replace(record) - association.target.nil? ? nil : association end redefine_method("set_#{reflection.name}_target") do |target| -- cgit v1.2.3 From c6e10b0f600a56e962ff7d1614c50fca20630379 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 7 Jan 2011 19:12:37 +0000 Subject: has_one should always remove the old record (properly), even if not saving the new record, so we don't get the database into a pickle --- .../associations/has_one_association.rb | 2 +- .../associations/has_one_associations_test.rb | 35 +++++++++++++++------- activerecord/test/fixtures/ships.yml | 1 + activerecord/test/models/pirate.rb | 4 +++ 4 files changed, 31 insertions(+), 11 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 739bb919c5..9135c60009 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -20,7 +20,7 @@ module ActiveRecord load_target if @target && @target != record - remove_target(save && @reflection.options[:dependent]) + remove_target(@reflection.options[:dependent]) end if record diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 8203534a37..f004d46c98 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -2,9 +2,11 @@ require "cases/helper" require 'models/developer' require 'models/project' require 'models/company' +require 'models/ship' +require 'models/pirate' class HasOneAssociationsTest < ActiveRecord::TestCase - fixtures :accounts, :companies, :developers, :projects, :developers_projects + fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates def setup Account.destroyed_account_ids.clear @@ -164,15 +166,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.account end - def test_build_association_twice_without_saving_affects_nothing - count_of_account = Account.count - firm = Firm.find(:first) - firm.build_account("credit_limit" => 1000) - firm.build_account("credit_limit" => 2000) - - assert_equal count_of_account, Account.count - end - def test_create_association firm = Firm.create(:name => "GlobalMegaCorp") account = firm.create_account(:credit_limit => 1000) @@ -293,4 +286,26 @@ class HasOneAssociationsTest < ActiveRecord::TestCase new_account = companies(:first_firm).build_account(:firm_name => 'Account') assert_equal new_account.firm_name, "Account" end + + def test_creation_failure_without_dependent_option + pirate = pirates(:blackbeard) + orig_ship = pirate.ship.target + + assert_equal ships(:black_pearl), orig_ship + new_ship = pirate.create_ship + assert_not_equal ships(:black_pearl), new_ship + assert_equal new_ship, pirate.ship + assert new_ship.new_record? + assert_nil orig_ship.pirate_id + assert !orig_ship.changed? # check it was saved + end + + def test_creation_failure_with_dependent_option + pirate = pirates(:blackbeard).becomes(DestructivePirate) + orig_ship = pirate.dependent_ship.target + + new_ship = pirate.create_dependent_ship + assert new_ship.new_record? + assert orig_ship.destroyed? + end end diff --git a/activerecord/test/fixtures/ships.yml b/activerecord/test/fixtures/ships.yml index 137055aad1..df914262b3 100644 --- a/activerecord/test/fixtures/ships.yml +++ b/activerecord/test/fixtures/ships.yml @@ -1,5 +1,6 @@ black_pearl: name: "Black Pearl" + pirate: blackbeard interceptor: id: 2 name: "Interceptor" diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index f2c45053e7..b0490f754e 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -78,3 +78,7 @@ class Pirate < ActiveRecord::Base ship_log << "#{callback}_#{record.class.name.downcase}_#{record.id || ''}" end end + +class DestructivePirate < Pirate + has_one :dependent_ship, :class_name => 'Ship', :foreign_key => :pirate_id, :dependent => :destroy +end -- cgit v1.2.3 From 80df74bf515124c9db85d4a670989ae5cc28c3ec Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 8 Jan 2011 18:33:33 +0000 Subject: Enable the sqlite3 in-memory test connection to work --- activerecord/test/cases/helper.rb | 16 +++++++++++----- activerecord/test/cases/locking_test.rb | 4 ++-- activerecord/test/cases/pooled_connections_test.rb | 2 +- activerecord/test/cases/unconnected_test.rb | 1 + .../native_sqlite3/in_memory_connection.rb | 19 ++++++++++--------- 5 files changed, 25 insertions(+), 17 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index f9bbc5299b..51104d9cf5 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -26,6 +26,11 @@ def current_adapter?(*types) end end +def in_memory_db? + current_adapter?(:SQLiteAdapter) && + ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" +end + def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz yield @@ -100,11 +105,11 @@ class ActiveSupport::TestCase end end -# silence verbose schema loading -original_stdout = $stdout -$stdout = StringIO.new +def load_schema + # silence verbose schema loading + original_stdout = $stdout + $stdout = StringIO.new -begin adapter_name = ActiveRecord::Base.connection.adapter_name.downcase adapter_specific_schema_file = SCHEMA_ROOT + "/#{adapter_name}_specific_schema.rb" @@ -117,6 +122,8 @@ ensure $stdout = original_stdout end +load_schema + class << Time unless method_defined? :now_before_time_travel alias_method :now_before_time_travel, :now @@ -133,4 +140,3 @@ class << Time @now = nil end end - diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index f9678cb0c5..71667defa7 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -252,13 +252,13 @@ end # TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking -unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) +unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase self.use_transactional_fixtures = false fixtures :people, :readers def setup - Person.connection_pool.clear_reloadable_connections! + Person.connection_pool.clear_reloadable_connections # Avoid introspection queries during tests. Person.columns; Reader.columns end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index de5fa140ba..6269437b14 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -137,4 +137,4 @@ class PooledConnectionsTest < ActiveRecord::TestCase def add_record(name) ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name } end -end unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name +end unless current_adapter?(:FrontBase) || in_memory_db? diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index 23ad10f3f9..e82ca3f93d 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -14,6 +14,7 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase def teardown @underlying = nil ActiveRecord::Base.establish_connection(@specification) + load_schema if in_memory_db? end def test_connection_no_longer_established diff --git a/activerecord/test/connections/native_sqlite3/in_memory_connection.rb b/activerecord/test/connections/native_sqlite3/in_memory_connection.rb index 6aba9719bb..14e10900d1 100644 --- a/activerecord/test/connections/native_sqlite3/in_memory_connection.rb +++ b/activerecord/test/connections/native_sqlite3/in_memory_connection.rb @@ -1,4 +1,8 @@ -print "Using native SQLite3\n" +# This file connects to an in-memory SQLite3 database, which is a very fast way to run the tests. +# The downside is that disconnect from the database results in the database effectively being +# wiped. For this reason, pooled_connections_test.rb is disabled when using an in-memory database. + +print "Using native SQLite3 (in memory)\n" require_dependency 'models/course' require 'logger' ActiveRecord::Base.logger = Logger.new("debug.log") @@ -6,13 +10,10 @@ ActiveRecord::Base.logger = Logger.new("debug.log") class SqliteError < StandardError end -def make_connection(clazz, db_definitions_file) - clazz.establish_connection(:adapter => 'sqlite3', :database => ':memory:') - File.read(SCHEMA_ROOT + "/#{db_definitions_file}").split(';').each do |command| - clazz.connection.execute(command) unless command.strip.empty? - end +def make_connection(clazz) + ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite3', :database => ':memory:' } } + clazz.establish_connection(clazz.name) end -make_connection(ActiveRecord::Base, 'sqlite.sql') -make_connection(Course, 'sqlite2.sql') -load(SCHEMA_ROOT + "/schema.rb") +make_connection(ActiveRecord::Base) +make_connection(Course) -- cgit v1.2.3 From c47c54140246f4e5b49376ae9c408c85968ed6c3 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 8 Jan 2011 18:36:30 +0000 Subject: Have a separate test connection directory for sqlite3 in-memory so that the tests can be run without having to specifically rename the connection file (which then causes git to pick up the changes) --- activerecord/Rakefile | 2 +- .../native_sqlite3/in_memory_connection.rb | 19 ------------------- .../test/connections/native_sqlite3_mem/connection.rb | 19 +++++++++++++++++++ 3 files changed, 20 insertions(+), 20 deletions(-) delete mode 100644 activerecord/test/connections/native_sqlite3/in_memory_connection.rb create mode 100644 activerecord/test/connections/native_sqlite3_mem/connection.rb (limited to 'activerecord') diff --git a/activerecord/Rakefile b/activerecord/Rakefile index f9b77c1799..064734f8e2 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -41,7 +41,7 @@ namespace :test do end end -%w( mysql mysql2 postgresql sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| +%w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| Rake::TestTask.new("test_#{adapter}") { |t| connection_path = "test/connections/#{adapter =~ /jdbc/ ? 'jdbc' : 'native'}_#{adapter}" adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] diff --git a/activerecord/test/connections/native_sqlite3/in_memory_connection.rb b/activerecord/test/connections/native_sqlite3/in_memory_connection.rb deleted file mode 100644 index 14e10900d1..0000000000 --- a/activerecord/test/connections/native_sqlite3/in_memory_connection.rb +++ /dev/null @@ -1,19 +0,0 @@ -# This file connects to an in-memory SQLite3 database, which is a very fast way to run the tests. -# The downside is that disconnect from the database results in the database effectively being -# wiped. For this reason, pooled_connections_test.rb is disabled when using an in-memory database. - -print "Using native SQLite3 (in memory)\n" -require_dependency 'models/course' -require 'logger' -ActiveRecord::Base.logger = Logger.new("debug.log") - -class SqliteError < StandardError -end - -def make_connection(clazz) - ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite3', :database => ':memory:' } } - clazz.establish_connection(clazz.name) -end - -make_connection(ActiveRecord::Base) -make_connection(Course) diff --git a/activerecord/test/connections/native_sqlite3_mem/connection.rb b/activerecord/test/connections/native_sqlite3_mem/connection.rb new file mode 100644 index 0000000000..14e10900d1 --- /dev/null +++ b/activerecord/test/connections/native_sqlite3_mem/connection.rb @@ -0,0 +1,19 @@ +# This file connects to an in-memory SQLite3 database, which is a very fast way to run the tests. +# The downside is that disconnect from the database results in the database effectively being +# wiped. For this reason, pooled_connections_test.rb is disabled when using an in-memory database. + +print "Using native SQLite3 (in memory)\n" +require_dependency 'models/course' +require 'logger' +ActiveRecord::Base.logger = Logger.new("debug.log") + +class SqliteError < StandardError +end + +def make_connection(clazz) + ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite3', :database => ':memory:' } } + clazz.establish_connection(clazz.name) +end + +make_connection(ActiveRecord::Base) +make_connection(Course) -- cgit v1.2.3 From 1bc71ed9607ba88c21c14b670a4308c8549f3941 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 8 Jan 2011 19:39:40 +0000 Subject: When assigning a has_one, if the existing record fails to be removed from the association, raise an error --- .../active_record/associations/has_one_association.rb | 16 +++++++++++----- .../test/cases/associations/has_one_associations_test.rb | 12 ++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 9135c60009..a4332a0511 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -20,7 +20,7 @@ module ActiveRecord load_target if @target && @target != record - remove_target(@reflection.options[:dependent]) + remove_target!(@reflection.options[:dependent]) end if record @@ -55,13 +55,19 @@ module ActiveRecord record end - def remove_target(method) - case method - when :delete, :destroy + def remove_target!(method) + if [:delete, :destroy].include?(method) @target.send(method) else @target[@reflection.foreign_key] = nil - @target.save if @target.persisted? && @owner.persisted? + + if @target.persisted? && @owner.persisted? + unless @target.save + @target[@reflection.foreign_key] = @target.send("#{@reflection.foreign_key}_was") + raise RecordNotSaved, "Failed to remove the existing associated #{@reflection.name}. " + + "The record failed to save when after its foreign key was set to nil." + end + end end end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index f004d46c98..46459df7c5 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -308,4 +308,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert new_ship.new_record? assert orig_ship.destroyed? end + + def test_replacement_failure_due_to_existing_record_should_raise_error + pirate = pirates(:blackbeard) + pirate.ship.name = nil + + assert !pirate.ship.valid? + assert_raise(ActiveRecord::RecordNotSaved) do + pirate.ship = ships(:interceptor) + end + assert_equal ships(:black_pearl), pirate.ship + assert_equal pirate.id, pirate.ship.pirate_id + end end -- cgit v1.2.3 From 7f7b480098fa780dd76b4c1243230d74be67b3ca Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 8 Jan 2011 19:59:30 +0000 Subject: When assigning a has_one, if the new record fails to save, raise an error --- .../associations/has_one_association.rb | 6 ++--- .../associations/has_one_associations_test.rb | 28 +++++++++++----------- .../test/cases/autosave_association_test.rb | 4 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index a4332a0511..693c80163a 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -32,9 +32,9 @@ module ActiveRecord loaded if @owner.persisted? && record && save - record.save && self - else - record && self + unless record.save + raise RecordNotSaved, "Failed to save the new associated #{@reflection.name}." + end end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 46459df7c5..b9719fa983 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -93,18 +93,18 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_nullification_on_association_change firm = companies(:rails_core) old_account_id = firm.account.id - firm.account = Account.new + firm.account = Account.new(:credit_limit => 5) # account is dependent with nullify, therefore its firm_id should be nil assert_nil Account.find(old_account_id).firm_id end def test_association_change_calls_delete - companies(:first_firm).deletable_account = Account.new + companies(:first_firm).deletable_account = Account.new(:credit_limit => 5) assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id] end def test_association_change_calls_destroy - companies(:first_firm).account = Account.new + companies(:first_firm).account = Account.new(:credit_limit => 5) assert_equal [companies(:first_firm).id], Account.destroyed_account_ids[companies(:first_firm).id] end @@ -182,17 +182,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.account end - def test_failing_build_association - firm = Firm.new("name" => "GlobalMegaCorp") - firm.save - - firm.account = account = Account.new - assert_equal account, firm.account - assert !account.save - assert_equal account, firm.account - assert_equal ["can't be empty"], account.errors["credit_limit"] - end - def test_create firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -320,4 +309,15 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal ships(:black_pearl), pirate.ship assert_equal pirate.id, pirate.ship.pirate_id end + + def test_replacement_failure_due_to_new_record_should_raise_error + pirate = pirates(:blackbeard) + new_ship = Ship.new + + assert_raise(ActiveRecord::RecordNotSaved) do + pirate.ship = new_ship + end + assert_equal new_ship, pirate.ship + assert_equal pirate.id, new_ship.pirate_id + end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 71fd3fd836..2e43a20a73 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -90,7 +90,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas firm = Firm.find(:first) assert firm.valid? - firm.account = Account.new + firm.build_account assert !firm.account.valid? assert !firm.valid? @@ -102,7 +102,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas firm = Firm.find(:first) assert firm.valid? - firm.unvalidated_account = Account.new + firm.build_unvalidated_account assert !firm.unvalidated_account.valid? assert firm.valid? -- cgit v1.2.3 From 29452abb846c74299a23d510b394768fe6dec47c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 15:01:04 +0000 Subject: SQLite3 has supported savepoints since version 3.6.8, we should use this! --- .../connection_adapters/abstract_adapter.rb | 4 ++-- .../active_record/connection_adapters/sqlite_adapter.rb | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 0282493219..5ff5813699 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -77,8 +77,8 @@ module ActiveRecord false end - # Does this adapter support savepoints? PostgreSQL and MySQL do, SQLite - # does not. + # Does this adapter support savepoints? PostgreSQL and MySQL do, + # SQLite < 3.6.8 does not. def supports_savepoints? false end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index bf599a95f7..b04383d5bf 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -62,6 +62,10 @@ module ActiveRecord sqlite_version >= '2.0.0' end + def supports_savepoints? + sqlite_version >= '3.6.8' + end + # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -189,6 +193,18 @@ module ActiveRecord exec_query(sql, name).rows end + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + def begin_db_transaction #:nodoc: @connection.transaction end -- cgit v1.2.3 From 4e19ec566c9132b85fdf0ff13a328238a6aca591 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 15:52:41 +0000 Subject: In a number of places in the tests, we only need to turn off transactional fixtures when the DB does not support savepoints. This speeds the test run up by about 8-9% on my computer, when running rake test_sqlite3_mem :) --- .../test/cases/associations/join_model_test.rb | 3 +- .../test/cases/autosave_association_test.rb | 20 ++++---- activerecord/test/cases/helper.rb | 4 ++ activerecord/test/cases/locking_test.rb | 54 ++++++++++++---------- activerecord/test/cases/migration_test.rb | 3 +- activerecord/test/cases/multiple_db_test.rb | 2 +- activerecord/test/cases/nested_attributes_test.rb | 4 +- .../test/cases/session_store/session_test.rb | 2 +- activerecord/test/cases/unconnected_test.rb | 2 +- 9 files changed, 51 insertions(+), 43 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 7e9c190f42..512a6d3ef8 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -13,7 +13,8 @@ require 'models/book' require 'models/citation' class AssociationsJoinModelTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? + fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books, # Reload edges table from fixtures as otherwise repeated test was failing :edges diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 2e43a20a73..11c0c5b0ef 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -572,7 +572,7 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase end class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @@ -797,7 +797,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @@ -917,7 +917,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase end class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @ship = Ship.create(:name => 'Nights Dirty Lightning') @@ -1164,7 +1164,7 @@ module AutosaveAssociationOnACollectionAssociationTests end class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @association_name = :birds @@ -1178,7 +1178,7 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase end class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @association_name = :parrots @@ -1193,7 +1193,7 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T end class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @@ -1209,7 +1209,7 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te end class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @@ -1230,7 +1230,7 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes end class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @@ -1250,7 +1250,7 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord:: end class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @@ -1272,7 +1272,7 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test end class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @pirate = Pirate.new diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 51104d9cf5..2cc993b6ed 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -31,6 +31,10 @@ def in_memory_db? ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" end +def supports_savepoints? + ActiveRecord::Base.connection.supports_savepoints? +end + def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz yield diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 71667defa7..c5a204b335 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -19,11 +19,6 @@ end class OptimisticLockingTest < ActiveRecord::TestCase fixtures :people, :legacy_things, :references - # need to disable transactional fixtures, because otherwise the sqlite3 - # adapter (at least) chokes when we try and change the schema in the middle - # of a test (see test_increment_counter_*). - self.use_transactional_fixtures = false - def test_lock_existing p1 = Person.find(1) p2 = Person.find(1) @@ -152,6 +147,33 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal "unchangeable name", p.first_name end + def test_quote_table_name + ref = references(:michael_magician) + ref.favourite = !ref.favourite + assert ref.save + end + + # Useful for partial updates, don't only update the lock_version if there + # is nothing else being updated. + def test_update_without_attributes_does_not_only_update_lock_version + assert_nothing_raised do + p1 = Person.create!(:first_name => 'anika') + lock_version = p1.lock_version + p1.save + p1.reload + assert_equal lock_version, p1.lock_version + end + end +end + +class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase + fixtures :people, :legacy_things, :references + + # need to disable transactional fixtures, because otherwise the sqlite3 + # adapter (at least) chokes when we try and change the schema in the middle + # of a test (see test_increment_counter_*). + self.use_transactional_fixtures = false + { :lock_version => Person, :custom_lock_version => LegacyThing }.each do |name, model| define_method("test_increment_counter_updates_#{name}") do counter_test model, 1 do |id| @@ -198,24 +220,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) } end - def test_quote_table_name - ref = references(:michael_magician) - ref.favourite = !ref.favourite - assert ref.save - end - - # Useful for partial updates, don't only update the lock_version if there - # is nothing else being updated. - def test_update_without_attributes_does_not_only_update_lock_version - assert_nothing_raised do - p1 = Person.create!(:first_name => 'anika') - lock_version = p1.lock_version - p1.save - p1.reload - assert_equal lock_version, p1.lock_version - end - end - private def add_counter_column_to(model, col='test_count') @@ -254,11 +258,11 @@ end unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? fixtures :people, :readers def setup - Person.connection_pool.clear_reloadable_connections + Person.connection_pool.clear_reloadable_connections! # Avoid introspection queries during tests. Person.columns; Reader.columns end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 1a65045ded..a5a9965c3a 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -14,7 +14,7 @@ if ActiveRecord::Base.connection.supports_migrations? class Reminder < ActiveRecord::Base; end class ActiveRecord::Migration - class < "My baby takes tha mornin' train!") @@ -899,7 +899,7 @@ class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRe end class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @ship = Ship.create!(:name => "The good ship Dollypop") diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb index 6f1c170a0c..f906bda8c3 100644 --- a/activerecord/test/cases/session_store/session_test.rb +++ b/activerecord/test/cases/session_store/session_test.rb @@ -5,7 +5,7 @@ require 'active_record/session_store' module ActiveRecord class SessionStore class SessionTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? && ActiveRecord::Base.connection.supports_ddl_transactions? def setup super diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index e82ca3f93d..f85fb4e5da 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base end class TestUnconnectedAdapter < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_fixtures = false unless supports_savepoints? def setup @underlying = ActiveRecord::Base.connection -- cgit v1.2.3 From 1d6e2184283d15d20ed3102ca462d905e5efa73d Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 16:06:14 +0000 Subject: When assigning a has_one, if anything fails, the assignment should be rolled back entirely --- .../associations/has_one_association.rb | 42 ++++++++++++---------- .../associations/has_one_associations_test.rb | 7 ++-- 2 files changed, 28 insertions(+), 21 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 693c80163a..f60547c22c 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -19,23 +19,25 @@ module ActiveRecord raise_on_type_mismatch(record) unless record.nil? load_target - if @target && @target != record - remove_target!(@reflection.options[:dependent]) - end + @reflection.klass.transaction do + if @target && @target != record + remove_target!(@reflection.options[:dependent]) + end + + if record + set_inverse_instance(record) + set_owner_attributes(record) - if record - set_owner_attributes(record) - set_inverse_instance(record) + if @owner.persisted? && save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(@target) + raise RecordNotSaved, "Failed to save the new associated #{@reflection.name}." + end + end end @target = record loaded - - if @owner.persisted? && record && save - unless record.save - raise RecordNotSaved, "Failed to save the new associated #{@reflection.name}." - end - end end private @@ -59,17 +61,19 @@ module ActiveRecord if [:delete, :destroy].include?(method) @target.send(method) else - @target[@reflection.foreign_key] = nil + nullify_owner_attributes(@target) - if @target.persisted? && @owner.persisted? - unless @target.save - @target[@reflection.foreign_key] = @target.send("#{@reflection.foreign_key}_was") - raise RecordNotSaved, "Failed to remove the existing associated #{@reflection.name}. " + - "The record failed to save when after its foreign key was set to nil." - end + if @target.persisted? && @owner.persisted? && !@target.save + set_owner_attributes(@target) + raise RecordNotSaved, "Failed to remove the existing associated #{@reflection.name}. " + + "The record failed to save when after its foreign key was set to nil." end end end + + def nullify_owner_attributes(record) + record[@reflection.foreign_key] = nil + end end end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index b9719fa983..925b76b901 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -6,6 +6,7 @@ require 'models/ship' require 'models/pirate' class HasOneAssociationsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false unless supports_savepoints? fixtures :accounts, :companies, :developers, :projects, :developers_projects, :ships, :pirates def setup @@ -317,7 +318,9 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotSaved) do pirate.ship = new_ship end - assert_equal new_ship, pirate.ship - assert_equal pirate.id, new_ship.pirate_id + assert_equal ships(:black_pearl), pirate.ship + assert_equal pirate.id, pirate.ship.pirate_id + assert_equal pirate.id, ships(:black_pearl).reload.pirate_id + assert_nil new_ship.pirate_id end end -- cgit v1.2.3 From 6055bbedaa4b7b4bb2377ac87147196eebb2edc1 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 16:34:23 +0000 Subject: Raise ActiveRecord::RecordNotSaved if an AssociationCollection fails to be replaced --- .../active_record/associations/association_collection.rb | 6 +++++- .../test/cases/associations/has_many_associations_test.rb | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index daec8493ac..3d8a23fdca 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -314,7 +314,11 @@ module ActiveRecord transaction do delete(@target - other_array) - concat(other_array - @target) + + unless concat(other_array - @target) + raise RecordNotSaved, "Failed to replace #{@reflection.name} because one or more of the " + "new records could not be saved." + end end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 2b7ad3642a..1ce91d7211 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -975,6 +975,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !firm.clients.include?(:first_client) end + def test_replace_failure + firm = companies(:first_firm) + account = Account.new + orig_accounts = firm.accounts.to_a + + assert !account.valid? + assert !orig_accounts.empty? + assert_raise ActiveRecord::RecordNotSaved do + firm.accounts = [account] + end + assert_equal orig_accounts, firm.accounts + end + def test_get_ids assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids end -- cgit v1.2.3 From 9086b02ba5b0bd3e956097c7edbd0f337c614801 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 16:42:30 +0000 Subject: Document the recent changes to association assignment --- activerecord/lib/active_record/associations.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 8907c9c487..3f9762383b 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -332,12 +332,14 @@ module ActiveRecord # === One-to-one associations # # * Assigning an object to a +has_one+ association automatically saves that object and - # the object being replaced (if there is one), in order to update their primary + # the object being replaced (if there is one), in order to update their foreign # keys - except if the parent object is unsaved (new_record? == true). - # * If either of these saves fail (due to one of the objects being invalid) the assignment - # statement returns +false+ and the assignment is cancelled. + # * If either of these saves fail (due to one of the objects being invalid), an + # ActiveRecord::RecordNotSaved exception is raised and the assignment is + # cancelled. # * If you wish to assign an object to a +has_one+ association without saving it, - # use the build_association method (documented below). + # use the build_association method (documented below). The object being + # replaced will still be saved to update its foreign key. # * Assigning an object to a +belongs_to+ association does not save the object, since # the foreign key field belongs on the parent. It does not save the parent either. # @@ -348,6 +350,9 @@ module ActiveRecord # stored in the database. # * If saving any of the objects being added to a collection (via push or similar) # fails, then push returns +false+. + # * If saving fails while replacing the collection (via association=), an + # ActiveRecord::RecordNotSaved exception is raised and the assignment is + # cancelled. # * You can add an object to a collection without automatically saving it by using the # collection.build method (documented below). # * All unsaved (new_record? == true) members of the collection are automatically -- cgit v1.2.3 From 4754018272d576590693e5418936db23c7d13172 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 16:45:22 +0000 Subject: find_target can be inherited --- .../lib/active_record/associations/has_one_through_association.rb | 4 ---- 1 file changed, 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 11fa40a5c4..ab8543e4af 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -31,10 +31,6 @@ module ActiveRecord end end end - - def find_target - scoped.first - end end end end -- cgit v1.2.3 From 3b797c8c8681c8f4619bce39d3f042244fe002d9 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 17:13:05 +0000 Subject: DRY up the code which instantiates the association proxy --- activerecord/lib/active_record/associations.rb | 59 +++++++++------------- activerecord/lib/active_record/reflection.rb | 25 +++++++++ .../validations/uniqueness_validation_test.rb | 13 ----- activerecord/test/models/reply.rb | 7 +++ activerecord/test/models/topic.rb | 4 ++ 5 files changed, 60 insertions(+), 48 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 3f9762383b..20f22f8d1c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -137,6 +137,22 @@ module ActiveRecord # :nodoc: attr_reader :association_cache + protected + + # Returns the proxy for the given association name, instantiating it if it doesn't + # already exist + def association_proxy(name) + association = association_instance_get(name) + + if association.nil? + reflection = self.class.reflect_on_association(name) + association = reflection.proxy_class.new(self, reflection) + association_instance_set(name, association) + end + + association + end + private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) @@ -1468,12 +1484,7 @@ module ActiveRecord def association_accessor_methods(reflection, association_proxy_class) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? - association = association_instance_get(reflection.name) - - if association.nil? - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) - end + association = association_proxy(reflection.name) if force_reload reflection.klass.uncached { association.reload } @@ -1485,38 +1496,26 @@ module ActiveRecord end redefine_method("loaded_#{reflection.name}?") do - association = association_instance_get(reflection.name) + association = association_proxy(reflection.name) association && association.loaded? end redefine_method("#{reflection.name}=") do |record| - association = association_instance_get(reflection.name) - - if association.nil? - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) - end - - association.replace(record) + association_proxy(reflection.name).replace(record) end redefine_method("set_#{reflection.name}_target") do |target| - association = association_proxy_class.new(self, reflection) + association = association_proxy(reflection.name) association.target = target association.loaded - association_instance_set(reflection.name, association) + association end end def collection_reader_method(reflection, association_proxy_class) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? - association = association_instance_get(reflection.name) - - unless association - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) - end + association = association_proxy(reflection.name) if force_reload reflection.klass.uncached { association.reload } @@ -1541,10 +1540,7 @@ module ActiveRecord if writer redefine_method("#{reflection.name}=") do |new_value| - # Loads proxy class instance (defined in collection_reader_method) if not already loaded - association = send(reflection.name) - association.replace(new_value) - association + association_proxy(reflection.name).replace(new_value) end redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| @@ -1559,14 +1555,7 @@ module ActiveRecord def association_constructor_method(constructor, reflection, association_proxy_class) redefine_method("#{constructor}_#{reflection.name}") do |*params| attributes = params.first unless params.empty? - association = association_instance_get(reflection.name) - - unless association - association = association_proxy_class.new(self, reflection) - association_instance_set(reflection.name, association) - end - - association.send(constructor, attributes) + association_proxy(reflection.name).send(constructor, attributes) end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 937efe395f..ceeb0ec39d 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -313,6 +313,31 @@ module ActiveRecord macro == :belongs_to end + def proxy_class + case macro + when :belongs_to + if options[:polymorphic] + Associations::BelongsToPolymorphicAssociation + else + Associations::BelongsToAssociation + end + when :has_and_belongs_to_many + Associations::HasAndBelongsToManyAssociation + when :has_many + if options[:through] + Associations::HasManyThroughAssociation + else + Associations::HasManyAssociation + end + when :has_one + if options[:through] + Associations::HasOneThroughAssociation + else + Associations::HasOneAssociation + end + end + end + private def derive_class_name class_name = name.to_s.camelize diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 679d67553b..b4f3dd034c 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -6,19 +6,6 @@ require 'models/warehouse_thing' require 'models/guid' require 'models/event' -# The following methods in Topic are used in test_conditional_validation_* -class Topic - has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" - has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id" -end - -class UniqueReply < Reply - validates_uniqueness_of :content, :scope => 'parent_id' -end - -class SillyUniqueReply < UniqueReply -end - class Wizard < ActiveRecord::Base self.abstract_class = true diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 110d540120..6adfe0ae3c 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -10,6 +10,13 @@ class Reply < Topic attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read, :parent_title end +class UniqueReply < Reply + validates_uniqueness_of :content, :scope => 'parent_id' +end + +class SillyUniqueReply < UniqueReply +end + class WrongReply < Reply validate :errors_on_empty_content validate :title_is_wrong_create, :on => :create diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 6496f36f7e..6440dbe8ab 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -45,6 +45,10 @@ class Topic < ActiveRecord::Base has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title" + + has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id" + has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id" + serialize :content before_create :default_written_on -- cgit v1.2.3 From 42b2e4f85bef64b7aa1382e96c79db1d4f318a56 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 18:33:51 +0000 Subject: We can use the association_proxy method directly in HasOneThroughAssociation now --- .../associations/association_proxy.rb | 35 +++++++++++----------- .../associations/has_one_through_association.rb | 5 ++-- 2 files changed, 20 insertions(+), 20 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 294e1cab50..5fbda0bd3d 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -259,23 +259,6 @@ module ActiveRecord end end - private - # Forwards any missing method call to the \target. - def method_missing(method, *args) - if load_target - unless @target.respond_to?(method) - message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}" - raise NoMethodError, message - end - - if block_given? - @target.send(method, *args) { |*block_args| yield(*block_args) } - else - @target.send(method, *args) - end - end - end - # Loads the \target if needed and returns it. # # This method is abstract in the sense that it relies on +find_target+, @@ -299,6 +282,24 @@ module ActiveRecord reset end + private + + # Forwards any missing method call to the \target. + def method_missing(method, *args) + if load_target + unless @target.respond_to?(method) + message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}" + raise NoMethodError, message + end + + if block_given? + @target.send(method, *args) { |*block_args| yield(*block_args) } + else + @target.send(method, *args) + end + end + end + # Should be true if there is a foreign key present on the @owner which # references the target. This is used to determine whether we can load # the target if the @owner is currently a new record (and therefore diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index ab8543e4af..59a704b7bf 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -13,9 +13,8 @@ module ActiveRecord private def create_through_record(new_value) - proxy = @owner.send(@reflection.through_reflection.name) || - @owner.send(:association_instance_get, @reflection.through_reflection.name) - record = proxy.target + proxy = @owner.send(:association_proxy, @reflection.through_reflection.name) + record = proxy.send(:load_target) if record && !new_value record.destroy -- cgit v1.2.3 From 681ab53ba15f9fc95c8a91e50bb0138aa66967b2 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 18:37:01 +0000 Subject: Get rid of set_association_target and association_loaded? as the parts of the code that need that can now just use association_proxy(:name).loaded?/target= --- activerecord/lib/active_record/association_preload.rb | 19 +++++++++++++------ activerecord/lib/active_record/associations.rb | 12 ------------ .../active_record/associations/association_proxy.rb | 3 ++- .../associations/class_methods/join_dependency.rb | 3 ++- .../associations/has_one_through_associations_test.rb | 2 +- 5 files changed, 18 insertions(+), 21 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index bb7ddfcae9..4c517e3339 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -133,7 +133,7 @@ module ActiveRecord def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| - parent_record.send("set_#{reflection_name}_target", associated_record) + parent_record.send(:association_proxy, reflection_name).target = associated_record end end @@ -158,14 +158,17 @@ module ActiveRecord seen_keys[seen_key] = true mapped_records = id_to_record_map[seen_key] mapped_records.each do |mapped_record| - association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record) + association_proxy = mapped_record.send(:association_proxy, reflection_name) + association_proxy.target = associated_record association_proxy.send(:set_inverse_instance, associated_record) end end id_to_record_map.each do |id, records| next if seen_keys.include?(id.to_s) - records.each {|record| record.send("set_#{reflection_name}_target", nil) } + records.each do |record| + record.send(:association_proxy, reflection_name).target = nil + end end end @@ -232,10 +235,14 @@ module ActiveRecord end def preload_has_one_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") + return if records.first.send(:association_proxy, reflection.name).loaded? id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) options = reflection.options - records.each {|record| record.send("set_#{reflection.name}_target", nil)} + + records.each do |record| + record.send(:association_proxy, reflection.name).target = nil + end + if options[:through] through_records = preload_through_records(records, reflection, options[:through]) @@ -317,7 +324,7 @@ module ActiveRecord end def preload_belongs_to_association(records, reflection, preload_options={}) - return if records.first.send("loaded_#{reflection.name}?") + return if records.first.send(:association_proxy, reflection.name).loaded? options = reflection.options klasses_and_ids = {} diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 20f22f8d1c..ba5d048f4f 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1495,21 +1495,9 @@ module ActiveRecord association.target.nil? ? nil : association end - redefine_method("loaded_#{reflection.name}?") do - association = association_proxy(reflection.name) - association && association.loaded? - end - redefine_method("#{reflection.name}=") do |record| association_proxy(reflection.name).replace(record) end - - redefine_method("set_#{reflection.name}_target") do |target| - association = association_proxy(reflection.name) - association.target = target - association.loaded - association - end end def collection_reader_method(reflection, association_proxy_class) diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 5fbda0bd3d..844d30c3f5 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -213,7 +213,8 @@ module ActiveRecord # Set the inverse association, if possible def set_inverse_instance(record) if record && invertible_for?(record) - record.send("set_#{inverse_reflection_for(record).name}_target", @owner) + inverse = record.send(:association_proxy, inverse_reflection_for(record).name) + inverse.target = @owner end end diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb index 6263c4e3b0..cb3edafab1 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb @@ -223,7 +223,8 @@ module ActiveRecord end def set_target_and_inverse(join_part, association, record) - association_proxy = record.send("set_#{join_part.reflection.name}_target", association) + association_proxy = record.send(:association_proxy, join_part.reflection.name) + association_proxy.target = association association_proxy.send(:set_inverse_instance, association) end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 0afbef5c87..91d3025468 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -197,7 +197,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase MemberDetail.find(:all, :include => :member_type) end @new_detail = @member_details[0] - assert @new_detail.loaded_member_type? + assert @new_detail.send(:association_proxy, :member_type).loaded? assert_not_nil assert_no_queries { @new_detail.member_type } end -- cgit v1.2.3 From f4a88e810f66bfde5e6148cf7c276c2d4087f7ca Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 18:41:59 +0000 Subject: It's not necessary to pass the association proxy class around now --- activerecord/lib/active_record/associations.rb | 37 +++++++++++--------------- 1 file changed, 15 insertions(+), 22 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index ba5d048f4f..45b68d5956 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1019,12 +1019,7 @@ module ActiveRecord reflection = create_has_many_reflection(association_id, options, &extension) configure_dependency_for_has_many(reflection) add_association_callbacks(reflection.name, reflection.options) - - if options[:through] - collection_accessor_methods(reflection, HasManyThroughAssociation) - else - collection_accessor_methods(reflection, HasManyAssociation) - end + collection_accessor_methods(reflection) end # Specifies a one-to-one association with another class. This method should only be used @@ -1135,14 +1130,13 @@ module ActiveRecord def has_one(association_id, options = {}) if options[:through] reflection = create_has_one_through_reflection(association_id, options) - association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation) else reflection = create_has_one_reflection(association_id, options) - association_accessor_methods(reflection, HasOneAssociation) - association_constructor_method(:build, reflection, HasOneAssociation) - association_constructor_method(:create, reflection, HasOneAssociation) + association_constructor_method(:build, reflection) + association_constructor_method(:create, reflection) configure_dependency_for_has_one(reflection) end + association_accessor_methods(reflection) end # Specifies a one-to-one association with another class. This method should only be used @@ -1259,12 +1253,11 @@ module ActiveRecord def belongs_to(association_id, options = {}) reflection = create_belongs_to_reflection(association_id, options) - if reflection.options[:polymorphic] - association_accessor_methods(reflection, BelongsToPolymorphicAssociation) - else - association_accessor_methods(reflection, BelongsToAssociation) - association_constructor_method(:build, reflection, BelongsToAssociation) - association_constructor_method(:create, reflection, BelongsToAssociation) + association_accessor_methods(reflection) + + unless reflection.options[:polymorphic] + association_constructor_method(:build, reflection) + association_constructor_method(:create, reflection) end add_counter_cache_callbacks(reflection) if options[:counter_cache] @@ -1449,7 +1442,7 @@ module ActiveRecord # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' def has_and_belongs_to_many(association_id, options = {}, &extension) reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) + collection_accessor_methods(reflection) # Don't use a before_destroy callback since users' before_destroy # callbacks will be executed after the association is wiped out. @@ -1481,7 +1474,7 @@ module ActiveRecord table_name_prefix + join_table + table_name_suffix end - def association_accessor_methods(reflection, association_proxy_class) + def association_accessor_methods(reflection) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? association = association_proxy(reflection.name) @@ -1500,7 +1493,7 @@ module ActiveRecord end end - def collection_reader_method(reflection, association_proxy_class) + def collection_reader_method(reflection) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? association = association_proxy(reflection.name) @@ -1523,8 +1516,8 @@ module ActiveRecord end end - def collection_accessor_methods(reflection, association_proxy_class, writer = true) - collection_reader_method(reflection, association_proxy_class) + def collection_accessor_methods(reflection, writer = true) + collection_reader_method(reflection) if writer redefine_method("#{reflection.name}=") do |new_value| @@ -1540,7 +1533,7 @@ module ActiveRecord end end - def association_constructor_method(constructor, reflection, association_proxy_class) + def association_constructor_method(constructor, reflection) redefine_method("#{constructor}_#{reflection.name}") do |*params| attributes = params.first unless params.empty? association_proxy(reflection.name).send(constructor, attributes) -- cgit v1.2.3 From d88caa6e4a8f0f64601fc8bf07b61b682263d712 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 18:49:32 +0000 Subject: Refactor the code for singular association constructors. This will allow me to define a create_association! method in a minute. --- activerecord/lib/active_record/associations.rb | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 45b68d5956..2e0f54e505 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1132,8 +1132,7 @@ module ActiveRecord reflection = create_has_one_through_reflection(association_id, options) else reflection = create_has_one_reflection(association_id, options) - association_constructor_method(:build, reflection) - association_constructor_method(:create, reflection) + association_constructor_methods(reflection) configure_dependency_for_has_one(reflection) end association_accessor_methods(reflection) @@ -1256,8 +1255,7 @@ module ActiveRecord association_accessor_methods(reflection) unless reflection.options[:polymorphic] - association_constructor_method(:build, reflection) - association_constructor_method(:create, reflection) + association_constructor_methods(reflection) end add_counter_cache_callbacks(reflection) if options[:counter_cache] @@ -1533,10 +1531,17 @@ module ActiveRecord end end - def association_constructor_method(constructor, reflection) - redefine_method("#{constructor}_#{reflection.name}") do |*params| - attributes = params.first unless params.empty? - association_proxy(reflection.name).send(constructor, attributes) + def association_constructor_methods(reflection) + constructors = { + "build_#{reflection.name}" => "build", + "create_#{reflection.name}" => "create" + } + + constructors.each do |name, proxy_name| + redefine_method(name) do |*params| + attributes = params.first unless params.empty? + association_proxy(reflection.name).send(proxy_name, attributes) + end end end -- cgit v1.2.3 From 552df9b933e05a3c1d2508c316f1f2bd240accc5 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 9 Jan 2011 19:09:51 +0000 Subject: Support for create_association! for has_one associations --- activerecord/lib/active_record/associations.rb | 1 + .../active_record/associations/has_one_association.rb | 5 ++++- .../cases/associations/has_one_associations_test.rb | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 2e0f54e505..a03d1bbb06 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1536,6 +1536,7 @@ module ActiveRecord "build_#{reflection.name}" => "build", "create_#{reflection.name}" => "create" } + constructors["create_#{reflection.name}!"] = "create!" if reflection.macro == :has_one constructors.each do |name, proxy_name| redefine_method(name) do |*params| diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index f60547c22c..c29ab8dcec 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -7,7 +7,7 @@ module ActiveRecord end def create!(attributes = {}) - new_record(:create_association!, attributes) + build(attributes).tap { |record| record.save! } end def build(attributes = {}) @@ -51,6 +51,9 @@ module ActiveRecord alias creation_attributes construct_owner_attributes + # The reason that the save param for replace is false, if for create (not just build), + # is because the setting of the foreign keys is actually handled by the scoping, and + # so they are set straight away and do not need to be updated within replace. def new_record(method, attributes) record = scoped.scoping { @reflection.send(method, attributes) } replace(record, false) diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 925b76b901..d9b6694dd8 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -173,6 +173,24 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.reload.account end + def test_create_association_with_bang + firm = Firm.create(:name => "GlobalMegaCorp") + account = firm.create_account!(:credit_limit => 1000) + assert_equal account, firm.reload.account + end + + def test_create_association_with_bang_failing + firm = Firm.create(:name => "GlobalMegaCorp") + assert_raise ActiveRecord::RecordInvalid do + firm.create_account! + end + account = firm.account + assert_not_nil account + account.credit_limit = 5 + account.save + assert_equal account, firm.reload.account + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save -- cgit v1.2.3 From af96018c9171a9021f915ec63bd0baf4cf5a8e39 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 11 Jan 2011 20:11:28 +0000 Subject: test_with_polymorphic_and_condition works without the conditions methods in BelongsToPolymorphicAssociation because the conditions are added straight to the association_scope as of a few days ago --- .../active_record/associations/belongs_to_polymorphic_association.rb | 5 ----- 1 file changed, 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index cae35fe0d0..4f67b02d00 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -4,11 +4,6 @@ module ActiveRecord class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: private - def conditions - @conditions ||= interpolate_sql(target_klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions] - end - alias :sql_conditions :conditions - def replace_keys(record) super @owner[@reflection.foreign_type] = record && record.class.base_class.name -- cgit v1.2.3 From 8c71e8b18f0e589b5413a8e6b6d7336a5506c977 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 11 Jan 2011 15:16:09 -0800 Subject: lazily instantiate AR objects in order to avoid NoMethodErrors --- .../lib/active_record/association_preload.rb | 22 ++++++++++++++-------- activerecord/lib/active_record/base.rb | 19 ++++++++++--------- 2 files changed, 24 insertions(+), 17 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 4c517e3339..68aaff175a 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -137,9 +137,9 @@ module ActiveRecord end end - def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key) + def set_association_collection_records(id_to_parent_map, reflection_name, associated_records, key) associated_records.each do |associated_record| - parent_records = id_to_record_map[associated_record[key].to_s] + parent_records = id_to_parent_map[associated_record[key].to_s] add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) end end @@ -199,7 +199,6 @@ module ActiveRecord right = Arel::Table.new(options[:join_table]).alias('t0') - join_condition = left[reflection.klass.primary_key].eq( right[reflection.association_foreign_key]) @@ -221,17 +220,24 @@ module ActiveRecord custom_conditions = append_conditions(reflection, preload_options) - all_associated_records = associated_records(ids) do |some_ids| + klass = associated_records_proxy.klass + + associated_records(ids) { |some_ids| method = in_or_equal(some_ids) conditions = right[reflection.foreign_key].send(*method) conditions = custom_conditions.inject(conditions) do |ast, cond| ast.and cond end - associated_records_proxy.where(conditions).to_a - end - - set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id') + relation = associated_records_proxy.where(conditions) + klass.connection.select_all(relation.arel.to_sql, 'SQL', relation.bind_values) + }.map! { |row| + parent_records = id_to_record_map[row['the_parent_record_id'].to_s] + associated_record = klass.instantiate row + add_preloaded_records_to_collection( + parent_records, reflection.name, associated_record) + associated_record + } end def preload_has_one_association(records, reflection, preload_options={}) diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1079094bbf..a5b71ba280 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -880,6 +880,16 @@ module ActiveRecord #:nodoc: record end + + # Finder methods must instantiate through this method to work with the + # single-table inheritance model that makes it possible to create + # objects of different types from the same table. + def instantiate(record) # :nodoc: + model = find_sti_class(record[inheritance_column]).allocate + model.init_with('attributes' => record) + model + end + private def relation #:nodoc: @@ -892,15 +902,6 @@ module ActiveRecord #:nodoc: end end - # Finder methods must instantiate through this method to work with the - # single-table inheritance model that makes it possible to create - # objects of different types from the same table. - def instantiate(record) - model = find_sti_class(record[inheritance_column]).allocate - model.init_with('attributes' => record) - model - end - def find_sti_class(type_name) if type_name.blank? || !columns_hash.include?(inheritance_column) self -- cgit v1.2.3 From 5696d948ed0eb9cf755e843564f80801ea864ee2 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 11 Jan 2011 15:29:35 -0800 Subject: kill unused variable warnings --- activerecord/lib/active_record/schema_dumper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e30b481fe1..a893c0ad85 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -83,7 +83,7 @@ HEADER # first dump primary key column if @connection.respond_to?(:pk_and_sequence_for) - pk, pk_seq = @connection.pk_and_sequence_for(table) + pk, _ = @connection.pk_and_sequence_for(table) elsif @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end -- cgit v1.2.3 From fcd8925f236b391d562dc504bcd901f501140c11 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 11 Jan 2011 15:39:26 -0800 Subject: use underlying _read_attribute method rather than causing NoMethodErrors --- activerecord/lib/active_record/base.rb | 2 +- activerecord/test/cases/associations/join_model_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index a5b71ba280..dde52269d4 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1578,7 +1578,7 @@ MSG # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings). def attribute_present?(attribute) - !read_attribute(attribute).blank? + !_read_attribute(attribute).blank? end # Returns the column object for the named attribute. diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 512a6d3ef8..c50fcd3f33 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -523,7 +523,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_collection_size_uses_counter_cache_if_it_exists author = authors(:david) - author.stubs(:read_attribute).with('comments_count').returns(100) + author.stubs(:_read_attribute).with('comments_count').returns(100) assert_equal 100, author.comments.size assert !author.comments.loaded? end -- cgit v1.2.3 From f8700038afdaea80cad34a4fca005e1ef068b53e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 11 Jan 2011 17:57:02 -0800 Subject: adding a test for no method error --- .../associations/association_proxy.rb | 16 +++---- .../cases/associations/association_proxy_test.rb | 52 ++++++++++++++++++++++ 2 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 activerecord/test/cases/associations/association_proxy_test.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 844d30c3f5..e4a449d4f4 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -286,19 +286,13 @@ module ActiveRecord private # Forwards any missing method call to the \target. - def method_missing(method, *args) + def method_missing(method, *args, &block) if load_target - unless @target.respond_to?(method) - message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}" - raise NoMethodError, message - end - - if block_given? - @target.send(method, *args) { |*block_args| yield(*block_args) } - else - @target.send(method, *args) - end + return super unless @target.respond_to?(method) + @target.send(method, *args, &block) end + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@target}") end # Should be true if there is a foreign key present on the @owner which diff --git a/activerecord/test/cases/associations/association_proxy_test.rb b/activerecord/test/cases/associations/association_proxy_test.rb new file mode 100644 index 0000000000..55d8da4c4e --- /dev/null +++ b/activerecord/test/cases/associations/association_proxy_test.rb @@ -0,0 +1,52 @@ +require "cases/helper" + +module ActiveRecord + module Associations + class AsssociationProxyTest < ActiveRecord::TestCase + class FakeOwner + attr_accessor :new_record + alias :new_record? :new_record + + def initialize + @new_record = false + end + end + + class FakeReflection < Struct.new(:options, :klass) + def initialize options = {}, klass = nil + super + end + + def check_validity! + true + end + end + + class FakeTarget + end + + class FakeTargetProxy < AssociationProxy + def association_scope + true + end + + def find_target + FakeTarget.new + end + end + + def test_method_missing_error + reflection = FakeReflection.new({}, Object.new) + owner = FakeOwner.new + proxy = FakeTargetProxy.new(owner, reflection) + + exception = assert_raises(NoMethodError) do + proxy.omg + end + + assert_match('omg', exception.message) + assert_match(FakeTarget.name, exception.message) + end + end + end +end -- cgit v1.2.3 From 8bee98fe3a3917f86d53f68b7cc11c4aafe5f011 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 12 Jan 2011 12:09:40 -0800 Subject: just use respond_to? and super rather than aliasing around methods --- .../lib/active_record/associations/association_collection.rb | 2 +- activerecord/lib/active_record/associations/association_proxy.rb | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 3d8a23fdca..6433cc3034 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -329,7 +329,7 @@ module ActiveRecord loaded? ? @target.include?(record) : exists?(record) end - def proxy_respond_to?(method, include_private = false) + def respond_to?(method, include_private = false) super || @reflection.klass.respond_to?(method, include_private) end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index e4a449d4f4..ba784fdb9d 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -50,10 +50,9 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class AssociationProxy #:nodoc: - alias_method :proxy_respond_to?, :respond_to? alias_method :proxy_extend, :extend delegate :to_param, :to => :proxy_target - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to_missing|proxy_/ } + instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } def initialize(owner, reflection) @owner, @reflection = owner, reflection @@ -77,7 +76,7 @@ module ActiveRecord # Does the proxy or its \target respond to +symbol+? def respond_to?(*args) - proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args)) + super || (load_target && @target.respond_to?(*args)) end # Forwards === explicitly to the \target because the instance method @@ -156,7 +155,7 @@ module ActiveRecord end def send(method, *args) - if proxy_respond_to?(method) + if respond_to?(method) super else load_target -- cgit v1.2.3 From 49696e0a62f39648d82660c689d15e56bb5f5207 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 12 Jan 2011 12:12:42 -0800 Subject: @loaded is defined in initialize, so we should not need this --- activerecord/lib/active_record/associations/association_proxy.rb | 2 -- 1 file changed, 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index ba784fdb9d..c19ded242e 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -270,8 +270,6 @@ module ActiveRecord # ActiveRecord::RecordNotFound is rescued within the method, and it is # not reraised. The proxy is \reset and +nil+ is the return value. def load_target - return nil unless defined?(@loaded) - if !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass @target = find_target end -- cgit v1.2.3 From bc993c690b656256e0f1dc5336a2f69c9d8dbe5d Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 12 Jan 2011 13:22:42 -0800 Subject: default return value is nil --- activerecord/lib/active_record/associations/association_proxy.rb | 1 - 1 file changed, 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index c19ded242e..ee0631edb6 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -331,7 +331,6 @@ module ActiveRecord # # This is only relevant to certain associations, which is why it returns nil by default. def stale_state - nil end end end -- cgit v1.2.3 From e9980f17fdca21655a9804d69632d83451067f58 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 12 Jan 2011 14:26:57 -0800 Subject: just call methods on return value of `load_target` --- .../lib/active_record/associations/association_proxy.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index ee0631edb6..a51adce958 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -150,17 +150,12 @@ module ActiveRecord # Forwards the call to the target. Loads the \target if needed. def inspect - load_target - @target.inspect + load_target.inspect end def send(method, *args) - if respond_to?(method) - super - else - load_target - @target.send(method, *args) - end + return super if respond_to?(method) + load_target.send(method, *args) end def scoped -- cgit v1.2.3 From 8f9944d5bcd684fedbd23e4fd77a2dabfaff6fad Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 12 Jan 2011 14:29:17 -0800 Subject: just use return value of load_target --- activerecord/lib/active_record/associations/association_proxy.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index a51adce958..f2de788522 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -82,8 +82,7 @@ module ActiveRecord # Forwards === explicitly to the \target because the instance method # removal above doesn't catch it. Loads the \target if needed. def ===(other) - load_target - other === @target + other === load_target end # Returns the name of the table of the related class: -- cgit v1.2.3 From e1beb7d2878cb55a045731b4a4c0c7a6046b3c09 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 12 Jan 2011 15:15:45 -0800 Subject: use array maths rather than *args --- .../lib/active_record/associations/association_collection.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 6433cc3034..da3800bf4a 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -358,8 +358,10 @@ module ActiveRecord if i @target.delete_at(i).tap do |t| keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) - f.attributes.except(*keys).each do |k,v| - t.send("#{k}=", v) + # FIXME: this call to attributes causes many NoMethodErrors + attributes = f.attributes + (attributes.keys - keys).each do |k| + t.send("#{k}=", attributes[k]) end end else -- cgit v1.2.3 From 3165dca28c1db741994c3176e7b158a9f684e816 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 12 Jan 2011 18:01:02 -0800 Subject: include_in_memory? should check against @target list in case of new records. [#6257 state:resolved] --- .../lib/active_record/associations/association_collection.rb | 4 ++-- .../test/cases/associations/has_many_through_associations_test.rb | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index da3800bf4a..e65ef2b768 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -518,10 +518,10 @@ module ActiveRecord def include_in_memory?(record) if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) - @owner.send(proxy_reflection.through_reflection.name).any? do |source| + @owner.send(proxy_reflection.through_reflection.name).any? { |source| target = source.send(proxy_reflection.source_reflection.name) target.respond_to?(:include?) ? target.include?(record) : target == record - end + } || @target.include?(record) else @target.include?(record) end 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 e98d178ff5..7235631b5a 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -32,6 +32,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create :person_id => 0, :post_id => 0 end + def test_include? + person = Person.new + post = Post.new + person.posts << post + assert person.posts.include?(post) + end + def test_associate_existing assert_queries(2) { posts(:thinking); people(:david) } -- cgit v1.2.3 From 9a57a2279eda066627b749a0a19a4ac4c49f48eb Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Thu, 13 Jan 2011 10:55:41 -0200 Subject: sorry, the CI cannot lie to us anymore (Part II) --- activerecord/Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 064734f8e2..ef99e0b26f 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -62,7 +62,7 @@ end (Dir["test/cases/**/*_test.rb"].reject { |x| x =~ /\/adapters\// } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| - system(ruby, "-Ilib:test:#{connection_path}", file) + sh(ruby, "-Ilib:test:#{connection_path}", file) end or raise "Failures" end -- cgit v1.2.3 From 3755ae04a1933762fce99d148734dc3de2a184c1 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Fri, 14 Jan 2011 17:11:15 -0200 Subject: Add missing require --- activerecord/test/cases/associations/eager_load_nested_include_test.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index c7671a8c22..8957586189 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -4,6 +4,7 @@ require 'models/author' require 'models/comment' require 'models/category' require 'models/categorization' +require 'models/tagging' require 'active_support/core_ext/array/random_access' module Remembered -- cgit v1.2.3 From 1e9685f15921d1acec65d07a27640aa1c674a29b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 11:16:31 -0800 Subject: preheat the table cache in arel --- activerecord/test/cases/batches_test.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index dcc49e12ca..6f65fb96d1 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -7,6 +7,7 @@ class EachTest < ActiveRecord::TestCase def setup @posts = Post.order("id asc") @total = Post.count + Post.count('id') # preheat arel's table cache end def test_each_should_excecute_one_query_per_batch -- cgit v1.2.3 From 92499b3c7e0343dcc89567f96178d1000b593333 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 13:11:24 -0800 Subject: Arel::Table.engine will be deprecated, so stop using it --- activerecord/lib/active_record/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index dde52269d4..47dc2d4a3a 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -829,7 +829,7 @@ module ActiveRecord #:nodoc: def arel_engine @arel_engine ||= begin if self == ActiveRecord::Base - Arel::Table.engine + ActiveRecord::Base else connection_handler.connection_pools[name] ? self : superclass.arel_engine end -- cgit v1.2.3 From f30a3106f3cba97ace9a5ec36b8c18e3f38ee043 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 13:45:30 -0800 Subject: transactional fixtures must be set to false for this test --- activerecord/test/cases/multiple_db_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 36e2c62fbb..bd51388e05 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -6,7 +6,7 @@ require 'models/bird' require_dependency 'models/course' class MultipleDbTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_fixtures = false def setup @courses = create_fixtures("courses") { Course.retrieve_connection } -- cgit v1.2.3 From 2947197421dd6b00a3c6694b768322d095cda69b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 14:07:16 -0800 Subject: use rake to create test databases for us --- activerecord/Rakefile | 9 +++++++++ activerecord/test/connections/native_sqlite3/connection.rb | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/Rakefile b/activerecord/Rakefile index ef99e0b26f..e414c4fb1c 100755 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -72,6 +72,15 @@ end end end +rule '.sqlite3' do |t| + sh %Q{sqlite3 "#{t.name}" "create table a (a integer); drop table a;"} +end + +task :test_sqlite3 => [ + 'test/fixtures/fixture_database.sqlite3', + 'test/fixtures/fixture_database_2.sqlite3' +] + namespace :mysql do desc 'Build the MySQL test databases' task :build_databases do diff --git a/activerecord/test/connections/native_sqlite3/connection.rb b/activerecord/test/connections/native_sqlite3/connection.rb index c517c2375e..c2aff5551f 100644 --- a/activerecord/test/connections/native_sqlite3/connection.rb +++ b/activerecord/test/connections/native_sqlite3/connection.rb @@ -3,21 +3,12 @@ require_dependency 'models/course' require 'logger' ActiveRecord::Base.logger = Logger.new("debug.log") -class SqliteError < StandardError -end - BASE_DIR = FIXTURES_ROOT sqlite_test_db = "#{BASE_DIR}/fixture_database.sqlite3" sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3" def make_connection(clazz, db_file) ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite3', :database => db_file, :timeout => 5000 } } - unless File.exist?(db_file) - puts "SQLite3 database not found at #{db_file}. Rebuilding it." - sqlite_command = %Q{sqlite3 "#{db_file}" "create table a (a integer); drop table a;"} - puts "Executing '#{sqlite_command}'" - raise SqliteError.new("Seems that there is no sqlite3 executable available") unless system(sqlite_command) - end clazz.establish_connection(clazz.name) end -- cgit v1.2.3 From ef4ffed660015772f67826e8aaa319febe84f271 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 14:30:47 -0800 Subject: reduce some lasigns --- activerecord/lib/active_record/associations/association_proxy.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index f2de788522..b752f1fdc0 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -185,9 +185,8 @@ module ActiveRecord scope = target_klass.unscoped scope = scope.create_with(creation_attributes) scope = scope.apply_finder_options(@reflection.options.slice(:conditions, :readonly, :include)) - scope = scope.where(construct_owner_conditions) scope = scope.select(select_value) if select_value = self.select_value - scope + scope.where(construct_owner_conditions) end def select_value -- cgit v1.2.3 From dc11a77ab7730ec213b5042e1261a7be8c211396 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 14:40:11 -0800 Subject: write the delegate method directly to avoid `delegate` callstack overhead --- activerecord/lib/active_record/associations/association_proxy.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index b752f1fdc0..7f6e335858 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -51,7 +51,7 @@ module ActiveRecord # instantiation of the actual post records. class AssociationProxy #:nodoc: alias_method :proxy_extend, :extend - delegate :to_param, :to => :proxy_target + instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } def initialize(owner, reflection) @@ -63,6 +63,10 @@ module ActiveRecord construct_scope end + def to_param + proxy_target.to_param + end + # Returns the owner of the proxy. def proxy_owner @owner -- cgit v1.2.3 From c326969745c38aaca552aebf240af644440afab3 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 14:45:46 -0800 Subject: reduce funcalls by using falsey objects --- activerecord/lib/active_record/associations/belongs_to_association.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 391471849c..b5545f4084 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -12,7 +12,7 @@ module ActiveRecord def replace(record) record = record.target if AssociationProxy === record - raise_on_type_mismatch(record) unless record.nil? + raise_on_type_mismatch(record) if record update_counters(record) replace_keys(record) @@ -59,7 +59,7 @@ module ActiveRecord end def foreign_key_present? - !@owner[@reflection.foreign_key].nil? + @owner[@reflection.foreign_key] end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto -- cgit v1.2.3 From a0a69b045adeced5754251706a0401b75e7e03ec Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 15:58:59 -0800 Subject: loaded? will not raise an AR::RecordNotFound exception, so move the rescue inside the conditional --- .../lib/active_record/associations/association_collection.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index e65ef2b768..bf40446871 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -350,8 +350,8 @@ module ActiveRecord def load_target if !@owner.new_record? || foreign_key_present? - begin - unless loaded? + unless loaded? + begin if @target.is_a?(Array) && @target.any? @target = find_target.map do |f| i = @target.index(f) @@ -371,9 +371,9 @@ module ActiveRecord else @target = find_target end + rescue ActiveRecord::RecordNotFound + reset end - rescue ActiveRecord::RecordNotFound - reset end end -- cgit v1.2.3 From b31ef7ee83f3fe808f7534172ce2bf22ef6c7cc0 Mon Sep 17 00:00:00 2001 From: Jordi Romero Date: Sat, 15 Jan 2011 01:15:05 +0100 Subject: document ActiveRecord's except and only Document methods that allow easily override arel queries --- activerecord/lib/active_record/relation/spawn_methods.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 5acf3ec83a..ae777208db 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -61,6 +61,13 @@ module ActiveRecord alias :& :merge + # Removes from the query the condition(s) specified in +skips+. + # + # Example: + # + # Post.order('id asc').except(:order) # discards the order condition + # Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order + # def except(*skips) result = self.class.new(@klass, table) @@ -75,6 +82,13 @@ module ActiveRecord result end + # Removes any condition from the query other than the one(s) specified in +onlies+. + # + # Example: + # + # Post.order('id asc').only(:where) # discards the order condition + # Post.order('id asc').only(:where, :order) # uses the specified order + # def only(*onlies) result = self.class.new(@klass, table) -- cgit v1.2.3 From 2afd6c75f18aee67fab85efef2b970572d459db3 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 16:46:02 -0800 Subject: move complex logic to it's own method --- .../associations/association_collection.rb | 34 ++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index bf40446871..60a8d71d26 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -353,21 +353,7 @@ module ActiveRecord unless loaded? begin if @target.is_a?(Array) && @target.any? - @target = find_target.map do |f| - i = @target.index(f) - if i - @target.delete_at(i).tap do |t| - keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) - # FIXME: this call to attributes causes many NoMethodErrors - attributes = f.attributes - (attributes.keys - keys).each do |k| - t.send("#{k}=", attributes[k]) - end - end - else - f - end - end + @target + @target = merge_target_lists(find_target, @target) else @target = find_target end @@ -450,6 +436,24 @@ module ActiveRecord end private + def merge_target_lists(loaded, existing) + loaded.map do |f| + i = existing.index(f) + if i + existing.delete_at(i).tap do |t| + keys = ["id"] + t.changes.keys + (f.attribute_names - t.attribute_names) + # FIXME: this call to attributes causes many NoMethodErrors + attributes = f.attributes + (attributes.keys - keys).each do |k| + t.send("#{k}=", attributes[k]) + end + end + else + f + end + end + existing + end + # Do the relevant stuff to insert the given record into the association collection. The # force param specifies whether or not an exception should be raised on failure. The # validate param specifies whether validation should be performed (if force is false). -- cgit v1.2.3 From 17687e4f969af363edf2854dd0c49914098dfda4 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 16:47:31 -0800 Subject: @target is always a list, so stop doing is_a? checks --- activerecord/lib/active_record/associations/association_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 60a8d71d26..9490f5401c 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -352,7 +352,7 @@ module ActiveRecord if !@owner.new_record? || foreign_key_present? unless loaded? begin - if @target.is_a?(Array) && @target.any? + if @target.any? @target = merge_target_lists(find_target, @target) else @target = find_target -- cgit v1.2.3 From f5d2cb9cac41c1655ad915418bb968011300688a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 16:50:26 -0800 Subject: we should use [] instead of Array.new --- activerecord/lib/active_record/associations/association_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 9490f5401c..c5c505342e 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -401,7 +401,7 @@ module ActiveRecord end def reset_target! - @target = Array.new + @target = [] end def reset_scopes_cache! -- cgit v1.2.3 From f548054700e59ac77b2a20df96b9ebd98abed195 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 16:54:02 -0800 Subject: we always have a target, so stop checking --- activerecord/lib/active_record/associations/association_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index c5c505342e..864d84d71a 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -363,7 +363,7 @@ module ActiveRecord end end - loaded if target + loaded target end -- cgit v1.2.3 From 75e29e871e925cb7485cdbc283594d3d97871703 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 16:57:14 -0800 Subject: only find_target can raise the exception, so isolate the rescue around that call --- .../active_record/associations/association_collection.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 864d84d71a..1254d68e9a 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -351,15 +351,19 @@ module ActiveRecord def load_target if !@owner.new_record? || foreign_key_present? unless loaded? + targets = [] + begin - if @target.any? - @target = merge_target_lists(find_target, @target) - else - @target = find_target - end + targets = find_target rescue ActiveRecord::RecordNotFound reset end + + if @target.any? + @target = merge_target_lists(targets, @target) + else + @target = targets + end end end -- cgit v1.2.3 From b8ed2d5ddff3db22430de983ba072f57b8aa3c00 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 17:03:15 -0800 Subject: return early in case the left or right side lists are empty --- .../lib/active_record/associations/association_collection.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 1254d68e9a..bd77aa30c1 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -359,11 +359,7 @@ module ActiveRecord reset end - if @target.any? - @target = merge_target_lists(targets, @target) - else - @target = targets - end + @target = merge_target_lists(targets, @target) end end @@ -441,6 +437,9 @@ module ActiveRecord private def merge_target_lists(loaded, existing) + return loaded if existing.empty? + return existing if loaded.empty? + loaded.map do |f| i = existing.index(f) if i -- cgit v1.2.3 From 0aef847927ed2f9c3e3dca92207d9a62baa01b1a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 14 Jan 2011 17:08:27 -0800 Subject: push !loaded? conditional up --- .../associations/association_collection.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index bd77aa30c1..b75e02c66b 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -349,18 +349,16 @@ module ActiveRecord end def load_target - if !@owner.new_record? || foreign_key_present? - unless loaded? - targets = [] - - begin - targets = find_target - rescue ActiveRecord::RecordNotFound - reset - end + if (!@owner.new_record? || foreign_key_present?) && !loaded? + targets = [] - @target = merge_target_lists(targets, @target) + begin + targets = find_target + rescue ActiveRecord::RecordNotFound + reset end + + @target = merge_target_lists(targets, @target) end loaded -- cgit v1.2.3 From 15e71347f6e018b10b9d93a40f7ab9da824bf09d Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Sun, 16 Jan 2011 18:55:35 +0100 Subject: updates AR's CHANGELOG with changes in 9e64dfa and ad343d7 --- activerecord/CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index aa447ce992..1d2f814d32 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,9 @@ *Rails 3.1.0 (unreleased)* +* Migration files generated from model and constructive migration generators + (for example, add_name_to_users) use the reversible migration's `change` + method instead of the ordinary `up` and `down` methods. [Prem Sichanugrist] + * Removed support for interpolated SQL conditions. Please use scoping along with attribute conditionals as a replacement. -- cgit v1.2.3 From 52c47556b7cf55549f97f3cfd5f69b2563198eac Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 18:25:50 +0000 Subject: Add create_association! for belongs_to --- activerecord/lib/active_record/associations.rb | 6 +++--- .../associations/belongs_to_association.rb | 4 ++++ .../cases/associations/belongs_to_associations_test.rb | 17 +++++++++++++++++ activerecord/test/models/company.rb | 1 + activerecord/test/schema/schema.rb | 1 + 5 files changed, 26 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index a03d1bbb06..70c8e75e20 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1533,10 +1533,10 @@ module ActiveRecord def association_constructor_methods(reflection) constructors = { - "build_#{reflection.name}" => "build", - "create_#{reflection.name}" => "create" + "build_#{reflection.name}" => "build", + "create_#{reflection.name}" => "create", + "create_#{reflection.name}!" => "create!" } - constructors["create_#{reflection.name}!"] = "create!" if reflection.macro == :has_one constructors.each do |name, proxy_name| redefine_method(name) do |*params| diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index b5545f4084..d311b0c572 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -6,6 +6,10 @@ module ActiveRecord replace(@reflection.create_association(attributes)) end + def create!(attributes = {}) + build(attributes).tap { |record| record.save! } + end + def build(attributes = {}) replace(@reflection.build_association(attributes)) end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 4c4891dcaf..ef6c482f67 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -120,6 +120,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal apple.name, client.firm_name end + def test_create! + client = Client.create!(:name => "Jimmy") + account = client.create_account!(:credit_limit => 10) + assert_equal account, client.account + assert account.persisted? + client.save + client.reload + assert_equal account, client.account + end + + def test_failing_create! + client = Client.create!(:name => "Jimmy") + assert_raise(ActiveRecord::RecordInvalid) { client.create_account! } + assert_not_nil client.account + assert client.account.new_record? + end + def test_natural_assignment_to_nil client = Client.find(3) client.firm = nil diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index f6e7a5ccf7..e8a126fb28 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -124,6 +124,7 @@ class Client < Company belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true has_many :accounts, :through => :firm + belongs_to :account # Record destruction so we can test whether firm.clients.clear has # is calling client.destroy, deleting from the database, or setting diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7f366b2c91..5f9bb7ee41 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -154,6 +154,7 @@ ActiveRecord::Schema.define do t.string :name t.integer :client_of t.integer :rating, :default => 1 + t.integer :account_id end add_index :companies, [:firm_id, :type, :rating, :ruby_type], :name => "company_index" -- cgit v1.2.3 From 3ef693724c4719564361b3f48f8977b795c935e0 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 18:37:09 +0000 Subject: Document the new create_association! method on one-to-one associations --- activerecord/lib/active_record/associations.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 70c8e75e20..4760a5fa57 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -208,7 +208,7 @@ module ActiveRecord # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X - # other.create!(attributes={}) | | | X + # create_other!(attributes={}) | X | | X # # ===Collection associations (one-to-many / many-to-many) # | | | has_many @@ -1042,6 +1042,9 @@ module ActiveRecord # Returns a new object of the associated type that has been instantiated # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as create_association, but raises ActiveRecord::RecordInvalid + # if the record is invalid. # # (+association+ is replaced with the symbol passed as the first argument, so # has_one :manager would add among others manager.nil?.) @@ -1053,6 +1056,7 @@ module ActiveRecord # * Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save) # * Account#build_beneficiary (similar to Beneficiary.new("account_id" => id)) # * Account#create_beneficiary (similar to b = Beneficiary.new("account_id" => id); b.save; b) + # * Account#create_beneficiary! (similar to b = Beneficiary.new("account_id" => id); b.save!; b) # # === Options # @@ -1157,6 +1161,9 @@ module ActiveRecord # Returns a new object of the associated type that has been instantiated # with +attributes+, linked to this object through a foreign key, and that # has already been saved (if it passed the validation). + # [create_association!(attributes = {})] + # Does the same as create_association, but raises ActiveRecord::RecordInvalid + # if the record is invalid. # # (+association+ is replaced with the symbol passed as the first argument, so # belongs_to :author would add among others author.nil?.) @@ -1168,6 +1175,7 @@ module ActiveRecord # * Post#author=(author) (similar to post.author_id = author.id) # * Post#build_author (similar to post.author = Author.new) # * Post#create_author (similar to post.author = Author.new; post.author.save; post.author) + # * Post#create_author! (similar to post.author = Author.new; post.author.save!; post.author) # The declaration can also include an options hash to specialize the behavior of the association. # # === Options -- cgit v1.2.3 From bf24fe810c0591619ac01cc88d8a40423895d9d7 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 19:20:37 +0000 Subject: belongs_to records should be initialized within the association scope --- .../associations/belongs_to_association.rb | 10 ++++++++-- .../associations/belongs_to_associations_test.rb | 21 +++++++++++++++++++++ activerecord/test/models/company.rb | 1 + 3 files changed, 30 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index d311b0c572..1abea8d831 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -3,7 +3,7 @@ module ActiveRecord module Associations class BelongsToAssociation < AssociationProxy #:nodoc: def create(attributes = {}) - replace(@reflection.create_association(attributes)) + new_record(:create_association, attributes) end def create!(attributes = {}) @@ -11,7 +11,7 @@ module ActiveRecord end def build(attributes = {}) - replace(@reflection.build_association(attributes)) + new_record(:build_association, attributes) end def replace(record) @@ -34,6 +34,12 @@ module ActiveRecord end private + def new_record(method, attributes) + record = scoped.scoping { @reflection.send(method, attributes) } + replace(record) + record + end + def update_counters(record) counter_cache_name = @reflection.counter_cache_column diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index ef6c482f67..01073bca3d 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -608,4 +608,25 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal groucho, sponsor.sponsorable assert_equal groucho, sponsor.thing end + + def test_build_with_conditions + client = companies(:second_client) + firm = client.build_bob_firm + + assert_equal "Bob", firm.name + end + + def test_create_with_conditions + client = companies(:second_client) + firm = client.create_bob_firm + + assert_equal "Bob", firm.name + end + + def test_create_bang_with_conditions + client = companies(:second_client) + firm = client.create_bob_firm! + + assert_equal "Bob", firm.name + end end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index e8a126fb28..3e219fbe4a 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -123,6 +123,7 @@ class Client < Company belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name" belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true + belongs_to :bob_firm, :class_name => "Firm", :foreign_key => "client_of", :conditions => { :name => "Bob" } has_many :accounts, :through => :firm belongs_to :account -- cgit v1.2.3 From ef79b917848f07b61e3243027f5e2ce4bc006d78 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 19:34:08 +0000 Subject: Abstract common code from BelongsToAssociation and HasOneAssociation into SingularAssociation --- activerecord/lib/active_record/associations.rb | 1 + .../associations/association_proxy.rb | 9 ++++--- .../associations/belongs_to_association.rb | 20 +------------- .../associations/has_one_association.rb | 23 ++++------------ .../associations/singular_association.rb | 31 ++++++++++++++++++++++ 5 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 activerecord/lib/active_record/associations/singular_association.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 4760a5fa57..e539e4968b 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -119,6 +119,7 @@ module ActiveRecord # These classes will be loaded when associations are created. # So there is no need to eager load them. autoload :AssociationCollection, 'active_record/associations/association_collection' + autoload :SingularAssociation, 'active_record/associations/singular_association' autoload :AssociationProxy, 'active_record/associations/association_proxy' autoload :ThroughAssociation, 'active_record/associations/through_association' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 7f6e335858..addc64cb42 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -7,14 +7,15 @@ module ActiveRecord # This is the root class of all association proxies ('+ Foo' signifies an included module Foo): # # AssociationProxy - # BelongsToAssociation - # BelongsToPolymorphicAssociation + # SingularAssociaton + # HasOneAssociation + # HasOneThroughAssociation + ThroughAssociation + # BelongsToAssociation + # BelongsToPolymorphicAssociation # AssociationCollection # HasAndBelongsToManyAssociation # HasManyAssociation # HasManyThroughAssociation + ThroughAssociation - # HasOneAssociation - # HasOneThroughAssociation + ThroughAssociation # # Association proxies in Active Record are middlemen between the object that # holds the association, known as the @owner, and the actual associated diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 1abea8d831..a818a780ed 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -1,19 +1,7 @@ module ActiveRecord # = Active Record Belongs To Associations module Associations - class BelongsToAssociation < AssociationProxy #:nodoc: - def create(attributes = {}) - new_record(:create_association, attributes) - end - - def create!(attributes = {}) - build(attributes).tap { |record| record.save! } - end - - def build(attributes = {}) - new_record(:build_association, attributes) - end - + class BelongsToAssociation < SingularAssociation #:nodoc: def replace(record) record = record.target if AssociationProxy === record raise_on_type_mismatch(record) if record @@ -34,12 +22,6 @@ module ActiveRecord end private - def new_record(method, attributes) - record = scoped.scoping { @reflection.send(method, attributes) } - replace(record) - record - end - def update_counters(record) counter_cache_name = @reflection.counter_cache_column diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index c29ab8dcec..c38843ea68 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,19 +1,7 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations - class HasOneAssociation < AssociationProxy #:nodoc: - def create(attributes = {}) - new_record(:create_association, attributes) - end - - def create!(attributes = {}) - build(attributes).tap { |record| record.save! } - end - - def build(attributes = {}) - new_record(:build_association, attributes) - end - + class HasOneAssociation < SingularAssociation #:nodoc: def replace(record, save = true) record = record.target if AssociationProxy === record raise_on_type_mismatch(record) unless record.nil? @@ -52,12 +40,11 @@ module ActiveRecord alias creation_attributes construct_owner_attributes # The reason that the save param for replace is false, if for create (not just build), - # is because the setting of the foreign keys is actually handled by the scoping, and - # so they are set straight away and do not need to be updated within replace. - def new_record(method, attributes) - record = scoped.scoping { @reflection.send(method, attributes) } + # is because the setting of the foreign keys is actually handled by the scoping when + # the record is instantiated, and so they are set straight away and do not need to be + # updated within replace. + def set_new_record(record) replace(record, false) - record end def remove_target!(method) diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb new file mode 100644 index 0000000000..023f7caf68 --- /dev/null +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -0,0 +1,31 @@ +module ActiveRecord + module Associations + class SingularAssociation < AssociationProxy #:nodoc: + def create(attributes = {}) + record = scoped.scoping { @reflection.create_association(attributes) } + set_new_record(record) + record + end + + def create!(attributes = {}) + build(attributes).tap { |record| record.save! } + end + + def build(attributes = {}) + record = scoped.scoping { @reflection.build_association(attributes) } + set_new_record(record) + record + end + + private + # Implemented by subclasses + def replace(record) + raise NotImplementedError + end + + def set_new_record(record) + replace(record) + end + end + end +end -- cgit v1.2.3 From 115eedbb410c52364e3353266316c218a29e5998 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 19:42:34 +0000 Subject: Use self.target= rather than @target= as the former automatically sets loaded --- activerecord/lib/active_record/associations/belongs_to_association.rb | 4 +--- activerecord/lib/active_record/associations/has_one_association.rb | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index a818a780ed..11e16de738 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -10,11 +10,9 @@ module ActiveRecord replace_keys(record) set_inverse_instance(record) - @target = record @updated = true if record - loaded - record + self.target = record end def updated? diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index c38843ea68..1b366b74b6 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -24,8 +24,7 @@ module ActiveRecord end end - @target = record - loaded + self.target = record end private -- cgit v1.2.3 From f1a15c2197d5da8e0c38bd59aa19973c9cfc0a01 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 19:47:58 +0000 Subject: Abstract a bit more into SingularAssociation --- .../lib/active_record/associations/belongs_to_association.rb | 3 +-- activerecord/lib/active_record/associations/has_one_association.rb | 3 +-- activerecord/lib/active_record/associations/singular_association.rb | 6 ++++++ 3 files changed, 8 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 11e16de738..271112bbe1 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -3,8 +3,7 @@ module ActiveRecord module Associations class BelongsToAssociation < SingularAssociation #:nodoc: def replace(record) - record = record.target if AssociationProxy === record - raise_on_type_mismatch(record) if record + record = check_record(record) update_counters(record) replace_keys(record) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 1b366b74b6..892a8b5bfb 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -3,8 +3,7 @@ module ActiveRecord module Associations class HasOneAssociation < SingularAssociation #:nodoc: def replace(record, save = true) - record = record.target if AssociationProxy === record - raise_on_type_mismatch(record) unless record.nil? + record = check_record(record) load_target @reflection.klass.transaction do diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 023f7caf68..4b457bd881 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -26,6 +26,12 @@ module ActiveRecord def set_new_record(record) replace(record) end + + def check_record(record) + record = record.target if AssociationProxy === record + raise_on_type_mismatch(record) if record + record + end end end end -- cgit v1.2.3 From b7594a075637f2d3039b066c282acfcb32126cdf Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 19:49:32 +0000 Subject: find_target can also go into SingularAssociation --- activerecord/lib/active_record/associations/belongs_to_association.rb | 4 ---- activerecord/lib/active_record/associations/has_one_association.rb | 4 ---- activerecord/lib/active_record/associations/singular_association.rb | 4 ++++ 3 files changed, 4 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 271112bbe1..e80b945dda 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -43,10 +43,6 @@ module ActiveRecord @owner[@reflection.foreign_key] = record && record[@reflection.association_primary_key] end - def find_target - scoped.first.tap { |record| set_inverse_instance(record) } - end - def foreign_key_present? @owner[@reflection.foreign_key] end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 892a8b5bfb..6614cbbf18 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -27,10 +27,6 @@ module ActiveRecord end private - def find_target - scoped.first.tap { |record| set_inverse_instance(record) } - end - def association_scope super.order(@reflection.options[:order]) end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 4b457bd881..b6f49c6f36 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -18,6 +18,10 @@ module ActiveRecord end private + def find_target + scoped.first.tap { |record| set_inverse_instance(record) } + end + # Implemented by subclasses def replace(record) raise NotImplementedError -- cgit v1.2.3 From 8aedd722e119770908eb3956ad91f4c88f425eb5 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 19:51:50 +0000 Subject: Use self.target= in HasOneThroughAssociation too --- .../lib/active_record/associations/has_one_through_association.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 59a704b7bf..6c455c8716 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -4,10 +4,9 @@ module ActiveRecord class HasOneThroughAssociation < HasOneAssociation include ThroughAssociation - def replace(new_value) - create_through_record(new_value) - @target = new_value - loaded + def replace(record) + create_through_record(record) + self.target = record end private -- cgit v1.2.3 From c4458b36024a20abacc39c069444bb0ac202778a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 19:53:40 +0000 Subject: Rename some variables --- .../associations/has_one_through_association.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 6c455c8716..dcd74e7346 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -11,21 +11,21 @@ module ActiveRecord private - def create_through_record(new_value) - proxy = @owner.send(:association_proxy, @reflection.through_reflection.name) - record = proxy.send(:load_target) + def create_through_record(record) + through_proxy = @owner.send(:association_proxy, @reflection.through_reflection.name) + through_record = through_proxy.send(:load_target) - if record && !new_value - record.destroy - elsif new_value - attributes = construct_join_attributes(new_value) + if through_record && !record + through_record.destroy + elsif record + attributes = construct_join_attributes(record) - if record - record.update_attributes(attributes) + if through_record + through_record.update_attributes(attributes) elsif @owner.new_record? - proxy.build(attributes) + through_proxy.build(attributes) else - proxy.create(attributes) + through_proxy.create(attributes) end end end -- cgit v1.2.3 From d1521719c5ac61a0c0e59827fe8cb197f5fe56f5 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 21:28:47 +0000 Subject: Removed support for accessing attributes on a has_and_belongs_to_many join table. This has been documented as deprecated behaviour since April 2006. Please use has_many :through instead. A deprecation warning will be added to the 3-0-stable branch for the 3.0.4 release. --- activerecord/CHANGELOG | 6 +- activerecord/lib/active_record/associations.rb | 6 -- .../has_and_belongs_to_many_association.rb | 68 ++---------------- .../has_and_belongs_to_many_associations_test.rb | 83 ---------------------- activerecord/test/cases/readonly_test.rb | 9 --- 5 files changed, 11 insertions(+), 161 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 1d2f814d32..43272160bd 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,9 @@ *Rails 3.1.0 (unreleased)* +* Removed support for accessing attributes on a has_and_belongs_to_many join table. This has been + documented as deprecated behaviour since April 2006. Please use has_many :through instead. + [Jon Leighton] + * Migration files generated from model and constructive migration generators (for example, add_name_to_users) use the reversible migration's `change` method instead of the ordinary `up` and `down` methods. [Prem Sichanugrist] @@ -26,7 +30,7 @@ along with attribute conditionals as a replacement. User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user -* When a model is generated add_index is added by default for belongs_to or references columns +* When a model is generated add_index is added by default for belongs_to or references columns rails g model post user:belongs_to will generate the following: diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e539e4968b..891ac52f8a 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1301,12 +1301,6 @@ module ActiveRecord # end # end # - # Deprecated: Any additional fields added to the join table will be placed as attributes when - # pulling records out through +has_and_belongs_to_many+ associations. Records returned from join - # tables with additional attributes will be marked as readonly (because we can't save changes - # to the additional attributes). It's strongly recommended that you upgrade any - # associations with attributes to a real join model (see introduction). - # # Adds the following methods for retrieval and query: # # [collection(force_reload = false)] diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index bc7894173d..b28554dce1 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -10,18 +10,6 @@ module ActiveRecord super end - def columns - @reflection.columns(@join_table_name, "#{@join_table_name} Columns") - end - - def reset_column_information - @reflection.reset_column_information - end - - def has_primary_key? - @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@join_table_name) - end - protected def count_records @@ -36,26 +24,11 @@ module ActiveRecord if @reflection.options[:insert_sql] @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) else - relation = join_table - timestamps = record_timestamp_columns(record) - timezone = record.send(:current_time_from_proper_timezone) if timestamps.any? - - attributes = columns.map do |column| - name = column.name - value = case name.to_s - when @reflection.foreign_key.to_s - @owner.id - when @reflection.association_foreign_key.to_s - record.id - when *timestamps - timezone - else - @owner.send(:quote_value, record[name], column) if record.has_attribute?(name) - end - [relation[name], value] unless value.nil? - end + stmt = join_table.compile_insert( + join_table[@reflection.foreign_key] => @owner.id, + join_table[@reflection.association_foreign_key] => record.id + ) - stmt = relation.compile_insert Hash[attributes] @owner.connection.insert stmt.to_sql end @@ -89,46 +62,17 @@ module ActiveRecord end def association_scope - scope = super.joins(construct_joins) - scope = scope.readonly if ambiguous_select?(@reflection.options[:select]) - scope + super.joins(construct_joins) end def select_value - super || [@reflection.klass.arel_table[Arel.star], join_table[Arel.star]] - end - - # Join tables with additional columns on top of the two foreign keys must be considered - # ambiguous unless a select clause has been explicitly defined. Otherwise you can get - # broken records back, if, for example, the join column also has an id column. This will - # then overwrite the id column of the records coming back. - def ambiguous_select?(select) - extra_join_columns? && select.nil? - end - - def extra_join_columns? - columns.size > 2 + super || @reflection.klass.arel_table[Arel.star] end private - def record_timestamp_columns(record) - if record.record_timestamps - record.send(:all_timestamp_attributes).map { |x| x.to_s } - else - [] - end - end - def invertible_for?(record) false end - - def find_by_sql(*args) - options = args.extract_options! - ambiguous = ambiguous_select?(@reflection.options[:select] || options[:select]) - - scoped.readonly(ambiguous).find(*(args << options)) - end end end end 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 705550216c..30730c7094 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 @@ -101,38 +101,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 't1', record[1] end - def test_should_record_timestamp_for_join_table - setup_data_for_habtm_case - - con = ActiveRecord::Base.connection - sql = 'select * from countries_treaties' - record = con.select_rows(sql).last - assert_not_nil record[2] - assert_not_nil record[3] - if current_adapter?(:Mysql2Adapter, :OracleAdapter) - assert_match %r{\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}}, record[2].to_s(:db) - assert_match %r{\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}}, record[3].to_s(:db) - else - assert_match %r{\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}}, record[2] - assert_match %r{\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}}, record[3] - end - end - - def test_should_record_timestamp_for_join_table_only_if_timestamp_should_be_recorded - begin - Treaty.record_timestamps = false - setup_data_for_habtm_case - - con = ActiveRecord::Base.connection - sql = 'select * from countries_treaties' - record = con.select_rows(sql).last - assert_nil record[2] - assert_nil record[3] - ensure - Treaty.record_timestamps = true - end - end - def test_has_and_belongs_to_many david = Developer.find(1) @@ -218,34 +186,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, aredridel.projects(true).size end - def test_adding_uses_default_values_on_join_table - ac = projects(:action_controller) - assert !developers(:jamis).projects.include?(ac) - developers(:jamis).projects << ac - - assert developers(:jamis, :reload).projects.include?(ac) - project = developers(:jamis).projects.detect { |p| p == ac } - assert_equal 1, project.access_level.to_i - end - - def test_habtm_attribute_access_and_respond_to - project = developers(:jamis).projects[0] - assert project.has_attribute?("name") - assert project.has_attribute?("joined_on") - assert project.has_attribute?("access_level") - assert project.respond_to?("name") - assert project.respond_to?("name=") - assert project.respond_to?("name?") - assert project.respond_to?("joined_on") - # given that the 'join attribute' won't be persisted, I don't - # think we should define the mutators - #assert project.respond_to?("joined_on=") - assert project.respond_to?("joined_on?") - assert project.respond_to?("access_level") - #assert project.respond_to?("access_level=") - assert project.respond_to?("access_level?") - end - def test_habtm_adding_before_save no_of_devels = Developer.count no_of_projects = Project.count @@ -430,10 +370,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty? end - def test_additional_columns_from_join_table - assert_date_from_db Date.new(2004, 10, 10), Developer.find(1).projects.first.joined_on.to_date - end - def test_destroying david = Developer.find(1) active_record = Project.find(1) @@ -675,25 +611,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_respond_to categories(:technology).select_testing_posts.find(:first), :correctness_marker end - def test_updating_attributes_on_rich_associations - david = projects(:action_controller).developers.first - david.name = "DHH" - assert_raise(ActiveRecord::ReadOnlyRecord) { david.save! } - end - - def test_updating_attributes_on_rich_associations_with_limited_find_from_reflection - david = projects(:action_controller).selected_developers.first - david.name = "DHH" - assert_nothing_raised { david.save! } - end - - - def test_updating_attributes_on_rich_associations_with_limited_find - david = projects(:action_controller).developers.find(:all, :select => "developers.*").first - david.name = "DHH" - assert david.save! - end - def test_join_table_alias assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL').size end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 8d694900ff..e21109baae 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -44,15 +44,6 @@ class ReadOnlyTest < ActiveRecord::TestCase Developer.joins(', projects').readonly(false).each { |d| assert !d.readonly? } end - - def test_habtm_find_readonly - dev = Developer.find(1) - assert !dev.projects.empty? - assert dev.projects.all?(&:readonly?) - assert dev.projects.find(:all).all?(&:readonly?) - assert dev.projects.readonly(true).all?(&:readonly?) - end - def test_has_many_find_readonly post = Post.find(1) assert !post.comments.empty? -- cgit v1.2.3 From 7a3f05f43d99d9b4442e3848a308fed3cb697e3b Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 16 Jan 2011 21:30:45 +0000 Subject: Add CHANGELOG entry for the addition of create_association! methods on has_one and belongs_to --- activerecord/CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 43272160bd..d84fe655a7 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -4,6 +4,8 @@ documented as deprecated behaviour since April 2006. Please use has_many :through instead. [Jon Leighton] +* Added a create_association! method for has_one and belongs_to associations. [Jon Leighton] + * Migration files generated from model and constructive migration generators (for example, add_name_to_users) use the reversible migration's `change` method instead of the ordinary `up` and `down` methods. [Prem Sichanugrist] -- cgit v1.2.3 From fdfabc99e80d5c48bdaf1c046c592ac81dc4c2c6 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 17 Jan 2011 14:22:17 -0800 Subject: fixing unused variable warnings --- activerecord/test/cases/adapters/mysql/reserved_word_test.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 90d8b0d923..b5c938b14a 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -105,9 +105,9 @@ class MysqlReservedWordTest < ActiveRecord::TestCase assert_nothing_raised { x.save } x.order = 'y' assert_nothing_raised { x.save } - assert_nothing_raised { y = Group.find_by_order('y') } - assert_nothing_raised { y = Group.find(1) } - x = Group.find(1) + assert_nothing_raised { Group.find_by_order('y') } + assert_nothing_raised { Group.find(1) } + Group.find(1) end # has_one association with reserved-word table name -- cgit v1.2.3 From 4b7dad2df3fc252acdb3fd8362370daac16540e4 Mon Sep 17 00:00:00 2001 From: Jaime Iniesta Date: Tue, 18 Jan 2011 00:35:07 +0100 Subject: ActiveRecord#save(false) is now deprecated, now it is save(:validate => false) --- activerecord/lib/active_record/validations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index f367315b22..26c1a9db93 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -37,7 +37,7 @@ module ActiveRecord end end - # The validation process on save can be skipped by passing false. The regular Base#save method is + # The validation process on save can be skipped by passing :validate => false. The regular Base#save method is # replaced with this when the validations module is mixed in, which it is by default. def save(options={}) perform_validations(options) ? super : false -- cgit v1.2.3 From 9d549986dd67df15ae45bb679ef1d9b40b4a1252 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 17 Jan 2011 16:42:34 -0800 Subject: remove useless conditional --- activerecord/lib/active_record/transactions.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 868f761a33..48d2f7c9a9 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -325,16 +325,14 @@ module ActiveRecord @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 if @_start_transaction_state[:level] < 1 restore_state = remove_instance_variable(:@_start_transaction_state) - if restore_state - @attributes = @attributes.dup if @attributes.frozen? - @new_record = restore_state[:new_record] - @destroyed = restore_state[:destroyed] - if restore_state.has_key?(:id) - self.id = restore_state[:id] - else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) - end + @attributes = @attributes.dup if @attributes.frozen? + @new_record = restore_state[:new_record] + @destroyed = restore_state[:destroyed] + if restore_state.has_key?(:id) + self.id = restore_state[:id] + else + @attributes.delete(self.class.primary_key) + @attributes_cache.delete(self.class.primary_key) end end end -- cgit v1.2.3 From 11fe2161ce8adc39aa69178211f7b661eafb201a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 17 Jan 2011 17:27:29 -0800 Subject: remove unnecessary module_eval --- activerecord/lib/active_record/aggregations.rb | 45 ++++++++++++-------------- 1 file changed, 20 insertions(+), 25 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 0bc26cc672..90d3b58c78 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -220,37 +220,32 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) - module_eval do - define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - attrs = mapping.collect {|pair| read_attribute(pair.first)} - object = constructor.respond_to?(:call) ? - constructor.call(*attrs) : - class_name.constantize.send(constructor, *attrs) - @aggregation_cache[name] = object - end - @aggregation_cache[name] + define_method(name) do + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) + attrs = mapping.collect {|pair| read_attribute(pair.first)} + object = constructor.respond_to?(:call) ? + constructor.call(*attrs) : + class_name.constantize.send(constructor, *attrs) + @aggregation_cache[name] = object end + @aggregation_cache[name] end - end def writer_method(name, class_name, mapping, allow_nil, converter) - module_eval do - define_method("#{name}=") do |part| - if part.nil? && allow_nil - mapping.each { |pair| self[pair.first] = nil } - @aggregation_cache[name] = nil - else - unless part.is_a?(class_name.constantize) || converter.nil? - part = converter.respond_to?(:call) ? - converter.call(part) : - class_name.constantize.send(converter, part) - end - - mapping.each { |pair| self[pair.first] = part.send(pair.last) } - @aggregation_cache[name] = part.freeze + define_method("#{name}=") do |part| + if part.nil? && allow_nil + mapping.each { |pair| self[pair.first] = nil } + @aggregation_cache[name] = nil + else + unless part.is_a?(class_name.constantize) || converter.nil? + part = converter.respond_to?(:call) ? + converter.call(part) : + class_name.constantize.send(converter, part) end + + mapping.each { |pair| self[pair.first] = part.send(pair.last) } + @aggregation_cache[name] = part.freeze end end end -- cgit v1.2.3 From e6881217ed8d7a20f70bb482a43512978fc4fc3b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 18 Jan 2011 10:49:50 -0800 Subject: fixing bug where 1.8 hangs while running pg tests --- activerecord/test/cases/locking_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index c5a204b335..2a72838d06 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -258,7 +258,7 @@ end unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? class PessimisticLockingTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_fixtures = false fixtures :people, :readers def setup -- cgit v1.2.3 From 4bc9bacd9458af9f9ad3a5075de6425e3c1c6a1e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 18 Jan 2011 15:05:18 -0800 Subject: refactor elaborate group_by in to a normal group_by --- activerecord/lib/active_record/association_preload.rb | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 68aaff175a..4b90845244 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -177,16 +177,11 @@ module ActiveRecord # (id_to_record_map, ids) where +id_to_record_map+ is the Hash, # and +ids+ is an Array of record IDs. def construct_id_map(records, primary_key=nil) - id_to_record_map = {} - ids = [] - records.each do |record| + id_to_record_map = records.group_by do |record| primary_key ||= record.class.primary_key - ids << record[primary_key] - mapped_records = (id_to_record_map[ids.last.to_s] ||= []) - mapped_records << record + record[primary_key].to_s end - ids.uniq! - return id_to_record_map, ids + return id_to_record_map, id_to_record_map.keys end def preload_has_and_belongs_to_many_association(records, reflection, preload_options={}) -- cgit v1.2.3 From ba62a87b8b3ab2cffb6e8af6c110ec17b9c0bab2 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 18 Jan 2011 15:15:20 -0800 Subject: ony bother with record map keys when we need them --- activerecord/lib/active_record/association_preload.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 4b90845244..d89f87dbc7 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -177,18 +177,18 @@ module ActiveRecord # (id_to_record_map, ids) where +id_to_record_map+ is the Hash, # and +ids+ is an Array of record IDs. def construct_id_map(records, primary_key=nil) - id_to_record_map = records.group_by do |record| + records.group_by do |record| primary_key ||= record.class.primary_key record[primary_key].to_s end - return id_to_record_map, id_to_record_map.keys end def preload_has_and_belongs_to_many_association(records, reflection, preload_options={}) left = reflection.klass.arel_table - id_to_record_map, ids = construct_id_map(records) + id_to_record_map = construct_id_map(records) + records.each {|record| record.send(reflection.name).loaded} options = reflection.options @@ -217,7 +217,7 @@ module ActiveRecord klass = associated_records_proxy.klass - associated_records(ids) { |some_ids| + associated_records(id_to_record_map.keys) { |some_ids| method = in_or_equal(some_ids) conditions = right[reflection.foreign_key].send(*method) conditions = custom_conditions.inject(conditions) do |ast, cond| @@ -237,7 +237,7 @@ module ActiveRecord def preload_has_one_association(records, reflection, preload_options={}) return if records.first.send(:association_proxy, reflection.name).loaded? - id_to_record_map, ids = construct_id_map(records, reflection.options[:primary_key]) + id_to_record_map = construct_id_map(records, reflection.options[:primary_key]) options = reflection.options records.each do |record| @@ -253,7 +253,7 @@ module ActiveRecord source = reflection.source_reflection.name through_records.first.class.preload_associations(through_records, source) if through_reflection.macro == :belongs_to - id_to_record_map = construct_id_map(records, through_primary_key).first + id_to_record_map = construct_id_map(records, through_primary_key) through_primary_key = through_reflection.klass.primary_key end @@ -263,7 +263,7 @@ module ActiveRecord end end else - set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.foreign_key) + set_association_single_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options), reflection.foreign_key) end end @@ -272,7 +272,7 @@ module ActiveRecord options = reflection.options foreign_key = reflection.through_reflection_foreign_key - id_to_record_map, ids = construct_id_map(records, foreign_key || reflection.options[:primary_key]) + id_to_record_map = construct_id_map(records, foreign_key || reflection.options[:primary_key]) records.each {|record| record.send(reflection.name).loaded} if options[:through] @@ -288,7 +288,7 @@ module ActiveRecord end else - set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), + set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options), reflection.foreign_key) end end -- cgit v1.2.3 From 9e42f1b416771ba91455de711ae680b55640377e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 18 Jan 2011 15:19:41 -0800 Subject: reduce method calls and loops when dealing with custom conditions --- activerecord/lib/active_record/association_preload.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index d89f87dbc7..74e957f828 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -219,10 +219,9 @@ module ActiveRecord associated_records(id_to_record_map.keys) { |some_ids| method = in_or_equal(some_ids) - conditions = right[reflection.foreign_key].send(*method) - conditions = custom_conditions.inject(conditions) do |ast, cond| - ast.and cond - end + conditions = right.create_and( + [right[reflection.foreign_key].send(*method)] + + custom_conditions) relation = associated_records_proxy.where(conditions) klass.connection.select_all(relation.arel.to_sql, 'SQL', relation.bind_values) -- cgit v1.2.3 From f3a5995bbf7a46f5b94e830f97908a3a9314d46e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 18 Jan 2011 15:21:14 -0800 Subject: keys will always be strings in the id => record map --- activerecord/lib/active_record/association_preload.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 74e957f828..3fb3642125 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -165,7 +165,7 @@ module ActiveRecord end id_to_record_map.each do |id, records| - next if seen_keys.include?(id.to_s) + next if seen_keys.include?(id) records.each do |record| record.send(:association_proxy, reflection_name).target = nil end -- cgit v1.2.3 From c107849a997f1014d2e28ce394c3d9ccb3a1f50c Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 18 Jan 2011 15:33:19 -0800 Subject: reduce objects, reduce loops and function calls while building the conditional --- activerecord/lib/active_record/association_preload.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 3fb3642125..8e71c6eec5 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -385,7 +385,7 @@ module ActiveRecord conditions << table["#{interface}_type"].eq(base_class.sti_name) end - conditions += append_conditions(reflection, preload_options) + conditions.concat append_conditions(reflection, preload_options) find_options = { :select => preload_options[:select] || options[:select] || table[Arel.star], @@ -397,9 +397,7 @@ module ActiveRecord associated_records(ids) do |some_ids| method = in_or_equal(some_ids) - where = conditions.inject(table[key].send(*method)) do |ast, cond| - ast.and cond - end + where = table.create_and(conditions + [table[key].send(*method)]) reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => where)).to_a end -- cgit v1.2.3 From a282301a77288252d135855d088f20de2397998e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 18 Jan 2011 16:33:18 -0800 Subject: we have a method for setting preloaded records, so use it --- activerecord/lib/active_record/association_preload.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 8e71c6eec5..cba4bab3ef 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -166,9 +166,7 @@ module ActiveRecord id_to_record_map.each do |id, records| next if seen_keys.include?(id) - records.each do |record| - record.send(:association_proxy, reflection_name).target = nil - end + add_preloaded_record_to_collection(records, reflection_name, nil) end end @@ -239,9 +237,7 @@ module ActiveRecord id_to_record_map = construct_id_map(records, reflection.options[:primary_key]) options = reflection.options - records.each do |record| - record.send(:association_proxy, reflection.name).target = nil - end + add_preloaded_record_to_collection(records, reflection.name, nil) if options[:through] through_records = preload_through_records(records, reflection, options[:through]) -- cgit v1.2.3 From 59f3218463228ca2301857cd7bf4f82f308924a6 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 21 Jan 2011 17:53:28 -0800 Subject: load and prefer psych as the YAML parser when it is available --- activerecord/lib/active_record/base.rb | 5 +++++ activerecord/lib/active_record/fixtures.rb | 6 ++++++ 2 files changed, 11 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 47dc2d4a3a..00324b14f2 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,3 +1,8 @@ +begin + require 'psych' +rescue LoadError +end + require 'yaml' require 'set' require 'active_support/benchmarkable' diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index b6f0511b9a..216c691833 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -1,4 +1,10 @@ require 'erb' + +begin + require 'psych' +rescue LoadError +end + require 'yaml' require 'csv' require 'zlib' -- cgit v1.2.3 From 018a88cf7ec5605d18e050504ce4090012f68c17 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Sat, 29 Jan 2011 00:56:07 +0900 Subject: missing parentheses --- activerecord/lib/active_record/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 47dc2d4a3a..e444e607d6 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -762,7 +762,7 @@ module ActiveRecord #:nodoc: :true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true) end - # Returns a string like 'Post id:integer, title:string, body:text' + # Returns a string like 'Post(id:integer, title:string, body:text)' def inspect if self == Base super -- cgit v1.2.3 From 2884482c3494b909e8e64751cfb0b588748d8854 Mon Sep 17 00:00:00 2001 From: Alexey Nayden Date: Thu, 13 Jan 2011 00:22:38 +0300 Subject: test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record added --- activerecord/test/cases/nested_attributes_test.rb | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index e1f938be84..ef25bde87e 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -147,6 +147,15 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.ship_attributes = { :id => "" } assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.save! } end + + def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record + Man.accepts_nested_attributes_for(:interests) + man = Man.create(:name => "John") + interest = Interest.create :topic => 'gardning', :man => man + man = Man.first + man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}] + assert_equal man.interests.first.topic, man.interests[0].topic + end end class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase -- cgit v1.2.3 From e92c2ffd8eebf4b0b1f260cfe77c0d9690ba8fef Mon Sep 17 00:00:00 2001 From: Alexey Nayden Date: Thu, 13 Jan 2011 00:24:12 +0300 Subject: Nested attributes and in-memory changed values #first and #[] behaviour consistency fix --- activerecord/lib/active_record/associations/association_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index b75e02c66b..24fb49a65d 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -518,7 +518,7 @@ module ActiveRecord def fetch_first_or_last_using_find?(args) (args.first.kind_of?(Hash) && !args.first.empty?) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || - @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer)) + @target.any? { |record| record.new_record? || record.changed? } || args.first.kind_of?(Integer)) end def include_in_memory?(record) -- cgit v1.2.3 From fa779c5357fd9396644ffc46e8ec575444f2f029 Mon Sep 17 00:00:00 2001 From: Alexey Nayden Date: Thu, 13 Jan 2011 03:50:37 +0300 Subject: Fixing incorrectly writtent testcase --- activerecord/test/cases/nested_attributes_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index ef25bde87e..d1afe7376a 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -151,8 +151,8 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record Man.accepts_nested_attributes_for(:interests) man = Man.create(:name => "John") - interest = Interest.create :topic => 'gardning', :man => man - man = Man.first + interest = man.interests.create :topic => 'gardning' + man = Man.find man.id man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}] assert_equal man.interests.first.topic, man.interests[0].topic end -- cgit v1.2.3 From 72c18189410513bbf474cb9d678b14d2e3591cc5 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 28 Jan 2011 17:39:38 -0800 Subject: use an identity conversion to avoid conditional codes --- activerecord/lib/active_record/attribute_methods/read.rb | 4 ++-- .../active_record/connection_adapters/abstract/schema_definitions.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 660fa9a564..ff088200a1 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -60,8 +60,8 @@ module ActiveRecord # Define an attribute reader method. Cope with nil column. def define_read_method(symbol, attr_name, column) - cast_code = column.type_cast_code('v') if column - access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']" + cast_code = column.type_cast_code('v') + access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}" unless attr_name.to_s == self.primary_key.to_s access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ") 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 60ccf9edf3..1a5d0c2f6d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -87,8 +87,8 @@ module ActiveRecord def type_cast_code(var_name) case type - when :string then nil - when :text then nil + when :string then var_name + when :text then var_name when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" when :float then "#{var_name}.to_f" when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" -- cgit v1.2.3 From 740bc35d328b262669e70065cd30e6b354456f26 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 28 Jan 2011 18:04:22 -0800 Subject: always return the identity function from type_cast_code --- .../active_record/connection_adapters/abstract/schema_definitions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') 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 1a5d0c2f6d..bc8a6e9cd6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -98,7 +98,7 @@ module ActiveRecord when :date then "#{self.class.name}.string_to_date(#{var_name})" when :binary then "#{self.class.name}.binary_to_string(#{var_name})" when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" - else nil + else var_name end end -- cgit v1.2.3 From 38d31b0ef0b9c9616918819b5ac5b9d21c539fd7 Mon Sep 17 00:00:00 2001 From: Jesse Storimer Date: Fri, 28 Jan 2011 23:23:39 -0500 Subject: Correct docs for after_find and after_initialize --- activerecord/lib/active_record/callbacks.rb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 47428cfd0f..64ffcb11fe 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -25,7 +25,11 @@ module ActiveRecord # Check out ActiveRecord::Transactions for more details about after_commit and # after_rollback. # - # That's a total of ten callbacks, which gives you immense power to react and prepare for each state in the + # Lastly an after_find and after_initialize callback is triggered for each object that + # is found and instantiated by a finder, with after_initialize being triggered after new objects + # are instantiated as well. + # + # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the # Active Record life cycle. The sequence for calling Base#save for an existing record is similar, # except that each _on_create callback is replaced by the corresponding _on_update callback. # @@ -185,14 +189,6 @@ module ActiveRecord # 'puts "Evaluated after parents are destroyed"' # end # - # == The +after_find+ and +after_initialize+ exceptions - # - # Because +after_find+ and +after_initialize+ are called for each object found and instantiated by a finder, - # such as Base.find(:all), we've had to implement a simple performance constraint (50% more speed - # on a simple test case). Unlike all the other callbacks, +after_find+ and +after_initialize+ will only be - # run if an explicit implementation is defined (def after_find). In that case, all of the - # callback types will be called. - # # == before_validation* returning statements # # If the returning value of a +before_validation+ callback can be evaluated to +false+, the process will be -- cgit v1.2.3 From 63c73dd0214188dc91442db538e141e30ec3b1b9 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 23 Jan 2011 21:29:36 +0000 Subject: We shouldn't be using scoped.scoping { ... } to build associated records, as this can affect validations/callbacks/etc inside the record itself [#6252 state:resolved] --- .../associations/association_collection.rb | 13 +++++-------- .../active_record/associations/singular_association.rb | 15 +++++++++------ .../cases/associations/has_many_associations_test.rb | 18 +++++++++++++++++- .../cases/associations/has_one_associations_test.rb | 15 +++++++++++++++ activerecord/test/models/bulb.rb | 11 +++++++++-- activerecord/test/models/pirate.rb | 2 ++ 6 files changed, 57 insertions(+), 17 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 24fb49a65d..a37c3dd432 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -466,17 +466,14 @@ module ActiveRecord force ? record.save! : record.save(:validate => validate) end - def create_record(attrs, &block) + def create_record(attributes, &block) ensure_owner_is_persisted! - - transaction do - scoped.scoping { build_record(attrs, &block) } - end + transaction { build_record(attributes, &block) } end - def build_record(attrs, &block) - attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash) - record = @reflection.build_association(attrs) + def build_record(attributes, &block) + attributes = scoped.scope_for_create.merge(attributes) + record = @reflection.build_association(attributes) add_record_to_target_with_callbacks(record, &block) end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index b6f49c6f36..b43b52fc52 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -2,9 +2,7 @@ module ActiveRecord module Associations class SingularAssociation < AssociationProxy #:nodoc: def create(attributes = {}) - record = scoped.scoping { @reflection.create_association(attributes) } - set_new_record(record) - record + new_record(:create, attributes) end def create!(attributes = {}) @@ -12,9 +10,7 @@ module ActiveRecord end def build(attributes = {}) - record = scoped.scoping { @reflection.build_association(attributes) } - set_new_record(record) - record + new_record(:build, attributes) end private @@ -36,6 +32,13 @@ module ActiveRecord raise_on_type_mismatch(record) if record record end + + def new_record(method, attributes) + attributes = scoped.scope_for_create.merge(attributes || {}) + record = @reflection.send("#{method}_association", attributes) + set_new_record(record) + record + end end end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 1ce91d7211..2ca412d424 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -43,7 +43,7 @@ end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, - :people, :posts, :readers, :taggings + :people, :posts, :readers, :taggings, :cars def setup Client.destroyed_client_ids.clear @@ -66,6 +66,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 'exotic', bulb.name end + # When creating objects on the association, we must not do it within a scope (even though it + # would be convenient), because this would cause that scope to be applied to any callbacks etc. + def test_build_and_create_should_not_happen_within_scope + car = cars(:honda) + original_scoped_methods = Bulb.scoped_methods + + bulb = car.bulbs.build + assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + + bulb = car.bulbs.create + assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + + bulb = car.bulbs.create! + assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + end + def test_no_sql_should_be_fired_if_association_already_loaded Car.create(:name => 'honda') bulbs = Car.first.bulbs diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index d9b6694dd8..dbf6dfe20d 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -4,6 +4,7 @@ require 'models/project' require 'models/company' require 'models/ship' require 'models/pirate' +require 'models/bulb' class HasOneAssociationsTest < ActiveRecord::TestCase self.use_transactional_fixtures = false unless supports_savepoints? @@ -167,6 +168,20 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.account end + def test_build_and_create_should_not_happen_within_scope + pirate = pirates(:blackbeard) + original_scoped_methods = Bulb.scoped_methods.dup + + bulb = pirate.build_bulb + assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + + bulb = pirate.create_bulb + assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + + bulb = pirate.create_bulb! + assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + end + def test_create_association firm = Firm.create(:name => "GlobalMegaCorp") account = firm.create_account(:credit_limit => 1000) diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 9eefc5803a..7178bb0d00 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,7 +1,14 @@ class Bulb < ActiveRecord::Base - + default_scope :conditions => {:name => 'defaulty' } - + belongs_to :car + attr_reader :scoped_methods_after_initialize + + after_initialize :record_scoped_methods_after_initialize + def record_scoped_methods_after_initialize + @scoped_methods_after_initialize = self.class.scoped_methods.dup + end + end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index b0490f754e..0d3f62bb33 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -34,6 +34,8 @@ class Pirate < ActiveRecord::Base :after_remove => proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.id}"} has_many :birds_with_reject_all_blank, :class_name => "Bird" + has_one :bulb, :foreign_key => :car_id + accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } accepts_nested_attributes_for :update_only_ship, :update_only => true -- cgit v1.2.3 From 15601c52e7c7094a6b7b54ef8acfc8299a4d6724 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 19:28:53 +0000 Subject: =?UTF-8?q?Let's=20be=20less=20blas=C3=A9=20about=20method=20visib?= =?UTF-8?q?ility=20on=20association=20proxies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../associations/association_collection.rb | 75 +++++++-------- .../associations/association_proxy.rb | 105 +++++++++++---------- .../associations/belongs_to_association.rb | 1 + .../has_and_belongs_to_many_association.rb | 19 ++-- .../associations/has_many_association.rb | 13 ++- .../associations/has_many_through_association.rb | 17 ++-- .../associations/has_one_association.rb | 5 +- .../associations/singular_association.rb | 1 + .../associations/through_association.rb | 16 ++-- 9 files changed, 135 insertions(+), 117 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index a37c3dd432..3c939b0ef0 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -333,6 +333,24 @@ module ActiveRecord super || @reflection.klass.respond_to?(method, include_private) end + def method_missing(method, *args, &block) + match = DynamicFinderMatch.match(method) + if match && match.creator? + attributes = match.attribute_names + return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) + end + + if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) + super + elsif @reflection.klass.scopes[method] + @_scopes_cache ||= {} + @_scopes_cache[method] ||= {} + @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) + else + scoped.readonly(nil).send(method, *args, &block) + end + end + protected def association_scope @@ -340,14 +358,6 @@ module ActiveRecord super.apply_finder_options(options) end - def select_value - super || uniq_select_value - end - - def uniq_select_value - @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" - end - def load_target if (!@owner.new_record? || foreign_key_present?) && !loaded? targets = [] @@ -365,22 +375,28 @@ module ActiveRecord target end - def method_missing(method, *args, &block) - match = DynamicFinderMatch.match(method) - if match && match.creator? - attributes = match.attribute_names - return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) - end - - if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) - super - elsif @reflection.klass.scopes[method] - @_scopes_cache ||= {} - @_scopes_cache[method] ||= {} - @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) + def add_record_to_target_with_callbacks(record) + callback(:before_add, record) + yield(record) if block_given? + @target ||= [] unless loaded? + if @reflection.options[:uniq] && index = @target.index(record) + @target[index] = record else - scoped.readonly(nil).send(method, *args, &block) + @target << record end + callback(:after_add, record) + set_inverse_instance(record) + record + end + + private + + def select_value + super || uniq_select_value + end + + def uniq_select_value + @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*" end def custom_counter_sql @@ -419,21 +435,6 @@ module ActiveRecord records end - def add_record_to_target_with_callbacks(record) - callback(:before_add, record) - yield(record) if block_given? - @target ||= [] unless loaded? - if @reflection.options[:uniq] && index = @target.index(record) - @target[index] = record - else - @target << record - end - callback(:after_add, record) - set_inverse_instance(record) - record - end - - private def merge_target_lists(loaded, existing) return loaded if existing.empty? return existing if loaded.empty? diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index addc64cb42..e7c9bbd192 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -84,6 +84,16 @@ module ActiveRecord super || (load_target && @target.respond_to?(*args)) end + # Forwards any missing method call to the \target. + def method_missing(method, *args, &block) + if load_target + return super unless @target.respond_to?(method) + @target.send(method, *args, &block) + end + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@target}") + end + # Forwards === explicitly to the \target because the instance method # removal above doesn't catch it. Loads the \target if needed. def ===(other) @@ -167,14 +177,6 @@ module ActiveRecord end protected - def interpolate_sql(sql, record = nil) - @owner.send(:interpolate_sql, sql, record) - end - - # Forwards the call to the reflection class. - def sanitize_sql(sql, table_name = @reflection.klass.table_name) - @reflection.klass.send(:sanitize_sql, sql, table_name) - end # Construct the scope for this association. # @@ -190,19 +192,12 @@ module ActiveRecord scope = target_klass.unscoped scope = scope.create_with(creation_attributes) scope = scope.apply_finder_options(@reflection.options.slice(:conditions, :readonly, :include)) - scope = scope.select(select_value) if select_value = self.select_value + if select = select_value + scope = scope.select(select) + end scope.where(construct_owner_conditions) end - def select_value - @reflection.options[:select] - end - - # Implemented by (some) subclasses - def creation_attributes - { } - end - def aliased_table target_klass.arel_table end @@ -227,6 +222,47 @@ module ActiveRecord target_klass.scoped end + # Loads the \target if needed and returns it. + # + # This method is abstract in the sense that it relies on +find_target+, + # which is expected to be provided by descendants. + # + # If the \target is already \loaded it is just returned. Thus, you can call + # +load_target+ unconditionally to get the \target. + # + # ActiveRecord::RecordNotFound is rescued within the method, and it is + # not reraised. The proxy is \reset and +nil+ is the return value. + def load_target + if !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass + @target = find_target + end + + loaded + @target + rescue ActiveRecord::RecordNotFound + reset + end + + private + + def interpolate_sql(sql, record = nil) + @owner.send(:interpolate_sql, sql, record) + end + + # Forwards the call to the reflection class. + def sanitize_sql(sql, table_name = @reflection.klass.table_name) + @reflection.klass.send(:sanitize_sql, sql, table_name) + end + + def select_value + @reflection.options[:select] + end + + # Implemented by (some) subclasses + def creation_attributes + { } + end + # Returns a hash linking the owner to the association represented by the reflection def construct_owner_attributes(reflection = @reflection) attributes = {} @@ -257,39 +293,6 @@ module ActiveRecord end end - # Loads the \target if needed and returns it. - # - # This method is abstract in the sense that it relies on +find_target+, - # which is expected to be provided by descendants. - # - # If the \target is already \loaded it is just returned. Thus, you can call - # +load_target+ unconditionally to get the \target. - # - # ActiveRecord::RecordNotFound is rescued within the method, and it is - # not reraised. The proxy is \reset and +nil+ is the return value. - def load_target - if !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass - @target = find_target - end - - loaded - @target - rescue ActiveRecord::RecordNotFound - reset - end - - private - - # Forwards any missing method call to the \target. - def method_missing(method, *args, &block) - if load_target - return super unless @target.respond_to?(method) - @target.send(method, *args, &block) - end - rescue NoMethodError => e - raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@target}") - end - # Should be true if there is a foreign key present on the @owner which # references the target. This is used to determine whether we can load # the target if the @owner is currently a new record (and therefore diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index e80b945dda..178c7204f8 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -19,6 +19,7 @@ module ActiveRecord end private + def update_counters(record) counter_cache_name = @reflection.counter_cache_column diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index b28554dce1..6ca287a4e3 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -12,10 +12,6 @@ module ActiveRecord protected - def count_records - load_target.size - end - def insert_record(record, force = true, validate = true) if record.new_record? return false unless save_record(record, force, validate) @@ -35,6 +31,16 @@ module ActiveRecord true end + def association_scope + super.joins(construct_joins) + end + + private + + def count_records + load_target.size + end + def delete_records(records) if sql = @reflection.options[:delete_sql] records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } @@ -61,15 +67,10 @@ module ActiveRecord super(join_table) end - def association_scope - super.joins(construct_joins) - end - def select_value super || @reflection.klass.arel_table[Arel.star] end - private def invertible_for?(record) false end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index b07441f3c6..002e1a9ae8 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,6 +7,14 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < AssociationCollection #:nodoc: protected + + def insert_record(record, force = false, validate = true) + set_owner_attributes(record) + save_record(record, force, validate) + end + + private + # Returns the number of records in this collection. # # If the association has a counter cache it gets that value. Otherwise @@ -45,11 +53,6 @@ module ActiveRecord "#{@reflection.name}_count" end - def insert_record(record, force = false, validate = true) - set_owner_attributes(record) - save_record(record, force, validate) - end - # Deletes the records according to the :dependent option. def delete_records(records) case @reflection.options[:dependent] 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 400db6baf1..d5b901beff 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -30,13 +30,6 @@ module ActiveRecord end protected - def target_reflection_has_associated_record? - if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank? - false - else - true - end - end def insert_record(record, force = true, validate = true) if record.new_record? @@ -47,6 +40,16 @@ module ActiveRecord through_association.create!(construct_join_attributes(record)) end + private + + def target_reflection_has_associated_record? + if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.foreign_key].blank? + false + else + true + end + end + # TODO - add dependent option support def delete_records(records) through_association = @owner.send(@reflection.through_reflection.name) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 6614cbbf18..a0828dcdea 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -26,11 +26,14 @@ module ActiveRecord self.target = record end - private + protected + def association_scope super.order(@reflection.options[:order]) end + private + alias creation_attributes construct_owner_attributes # The reason that the save param for replace is false, if for create (not just build), diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index b43b52fc52..7f92d9712a 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -14,6 +14,7 @@ module ActiveRecord end private + def find_target scoped.first.tap { |record| set_inverse_instance(record) } end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index d2112fb2b6..9c62ace4fb 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,6 +3,13 @@ module ActiveRecord module Associations module ThroughAssociation + def conditions + @conditions = build_conditions unless defined?(@conditions) + @conditions + end + + alias_method :sql_conditions, :conditions + protected def target_scope @@ -17,6 +24,8 @@ module ActiveRecord scope end + private + # This scope affects the creation of the associated records (not the join records). At the # moment we only support creating on a :through association when the source reflection is a # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so @@ -92,11 +101,6 @@ module ActiveRecord join_attributes end - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end - def build_conditions through_conditions = build_through_conditions source_conditions = @reflection.source_reflection.options[:conditions] @@ -127,8 +131,6 @@ module ActiveRecord @reflection.through_reflection.klass.send(:type_condition).to_sql end - alias_method :sql_conditions, :conditions - def stale_state if @reflection.through_reflection.macro == :belongs_to @owner[@reflection.through_reflection.foreign_key].to_s -- cgit v1.2.3 From d85ee50eda88fc1d486140b63d5c6826b7f3671b Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 19:32:34 +0000 Subject: Indent methods under private/protected sections --- .../associations/has_one_through_association.rb | 28 +-- .../associations/through_association.rb | 202 ++++++++++----------- 2 files changed, 115 insertions(+), 115 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index dcd74e7346..69771afe50 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -11,24 +11,24 @@ module ActiveRecord private - def create_through_record(record) - through_proxy = @owner.send(:association_proxy, @reflection.through_reflection.name) - through_record = through_proxy.send(:load_target) + def create_through_record(record) + through_proxy = @owner.send(:association_proxy, @reflection.through_reflection.name) + through_record = through_proxy.send(:load_target) - if through_record && !record - through_record.destroy - elsif record - attributes = construct_join_attributes(record) + if through_record && !record + through_record.destroy + elsif record + attributes = construct_join_attributes(record) - if through_record - through_record.update_attributes(attributes) - elsif @owner.new_record? - through_proxy.build(attributes) - else - through_proxy.create(attributes) + if through_record + through_record.update_attributes(attributes) + elsif @owner.new_record? + through_proxy.build(attributes) + else + through_proxy.create(attributes) + end end end - end end end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 9c62ace4fb..e777d108a5 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -12,135 +12,135 @@ module ActiveRecord protected - def target_scope - super & @reflection.through_reflection.klass.scoped - end + def target_scope + super & @reflection.through_reflection.klass.scoped + end - def association_scope - scope = super.joins(construct_joins).where(conditions) - unless @reflection.options[:include] - scope = scope.includes(@reflection.source_reflection.options[:include]) + def association_scope + scope = super.joins(construct_joins).where(conditions) + unless @reflection.options[:include] + scope = scope.includes(@reflection.source_reflection.options[:include]) + end + scope end - scope - end private - # This scope affects the creation of the associated records (not the join records). At the - # moment we only support creating on a :through association when the source reflection is a - # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so - # this scope has can legitimately be empty. - def creation_attributes - { } - end + # This scope affects the creation of the associated records (not the join records). At the + # moment we only support creating on a :through association when the source reflection is a + # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so + # this scope has can legitimately be empty. + def creation_attributes + { } + end - def aliased_through_table - name = @reflection.through_reflection.table_name + def aliased_through_table + name = @reflection.through_reflection.table_name - @reflection.table_name == name ? - @reflection.through_reflection.klass.arel_table.alias(name + "_join") : - @reflection.through_reflection.klass.arel_table - end + @reflection.table_name == name ? + @reflection.through_reflection.klass.arel_table.alias(name + "_join") : + @reflection.through_reflection.klass.arel_table + end - def construct_owner_conditions - super(aliased_through_table, @reflection.through_reflection) - end + def construct_owner_conditions + super(aliased_through_table, @reflection.through_reflection) + end - def construct_joins - right = aliased_through_table - left = @reflection.klass.arel_table + def construct_joins + right = aliased_through_table + left = @reflection.klass.arel_table + + conditions = [] + + if @reflection.source_reflection.macro == :belongs_to + reflection_primary_key = @reflection.source_reflection.options[:primary_key] || + @reflection.klass.primary_key + source_primary_key = @reflection.source_reflection.foreign_key + if @reflection.options[:source_type] + column = @reflection.source_reflection.foreign_type + conditions << + right[column].eq(@reflection.options[:source_type]) + end + else + reflection_primary_key = @reflection.source_reflection.foreign_key + source_primary_key = @reflection.source_reflection.options[:primary_key] || + @reflection.through_reflection.klass.primary_key + if @reflection.source_reflection.options[:as] + column = "#{@reflection.source_reflection.options[:as]}_type" + conditions << + left[column].eq(@reflection.through_reflection.klass.name) + end + end - conditions = [] + conditions << + left[reflection_primary_key].eq(right[source_primary_key]) - if @reflection.source_reflection.macro == :belongs_to - reflection_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.klass.primary_key - source_primary_key = @reflection.source_reflection.foreign_key - if @reflection.options[:source_type] - column = @reflection.source_reflection.foreign_type - conditions << - right[column].eq(@reflection.options[:source_type]) - end - else - reflection_primary_key = @reflection.source_reflection.foreign_key - source_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.through_reflection.klass.primary_key - if @reflection.source_reflection.options[:as] - column = "#{@reflection.source_reflection.options[:as]}_type" - conditions << - left[column].eq(@reflection.through_reflection.klass.name) - end + right.create_join( + right, + right.create_on(right.create_and(conditions))) end - conditions << - left[reflection_primary_key].eq(right[source_primary_key]) - - right.create_join( - right, - right.create_on(right.create_and(conditions))) - end + # Construct attributes for :through pointing to owner and associate. + def construct_join_attributes(associate) + # TODO: revisit this to allow it for deletion, supposing dependent option is supported + raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) - # Construct attributes for :through pointing to owner and associate. - def construct_join_attributes(associate) - # TODO: revisit this to allow it for deletion, supposing dependent option is supported - raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) + join_attributes = { + @reflection.source_reflection.foreign_key => + associate.send(@reflection.source_reflection.association_primary_key) + } - join_attributes = { - @reflection.source_reflection.foreign_key => - associate.send(@reflection.source_reflection.association_primary_key) - } + if @reflection.options[:source_type] + join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name) + end - if @reflection.options[:source_type] - join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name) - end + if @reflection.through_reflection.options[:conditions].is_a?(Hash) + join_attributes.merge!(@reflection.through_reflection.options[:conditions]) + end - if @reflection.through_reflection.options[:conditions].is_a?(Hash) - join_attributes.merge!(@reflection.through_reflection.options[:conditions]) + join_attributes end - join_attributes - end - - def build_conditions - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? + def build_conditions + through_conditions = build_through_conditions + source_conditions = @reflection.source_reflection.options[:conditions] + uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - if through_conditions || source_conditions || uses_sti - all = [] - all << interpolate_sql(sanitize_sql(source_conditions)) if source_conditions - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti + if through_conditions || source_conditions || uses_sti + all = [] + all << interpolate_sql(sanitize_sql(source_conditions)) if source_conditions + all << through_conditions if through_conditions + all << build_sti_condition if uses_sti - all.map { |sql| "(#{sql})" } * ' AND ' + all.map { |sql| "(#{sql})" } * ' AND ' + end end - end - def build_through_conditions - conditions = @reflection.through_reflection.options[:conditions] - if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) + def build_through_conditions + conditions = @reflection.through_reflection.options[:conditions] + if conditions.is_a?(Hash) + interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( + @reflection.quoted_table_name, + @reflection.through_reflection.quoted_table_name) + elsif conditions + interpolate_sql(sanitize_sql(conditions)) + end end - end - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql - end + def build_sti_condition + @reflection.through_reflection.klass.send(:type_condition).to_sql + end - def stale_state - if @reflection.through_reflection.macro == :belongs_to - @owner[@reflection.through_reflection.foreign_key].to_s + def stale_state + if @reflection.through_reflection.macro == :belongs_to + @owner[@reflection.through_reflection.foreign_key].to_s + end end - end - def foreign_key_present? - @reflection.through_reflection.macro == :belongs_to && - !@owner[@reflection.through_reflection.foreign_key].nil? - end + def foreign_key_present? + @reflection.through_reflection.macro == :belongs_to && + !@owner[@reflection.through_reflection.foreign_key].nil? + end end end end -- cgit v1.2.3 From dbb6fa723e5b1582d6508e9846d81cf42a797e19 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 19:59:06 +0000 Subject: Don't pass around conditions as strings in ThroughAssociation --- .../associations/through_association.rb | 55 ++++++++++------------ 1 file changed, 25 insertions(+), 30 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index e777d108a5..ac62f854e8 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,13 +3,6 @@ module ActiveRecord module Associations module ThroughAssociation - def conditions - @conditions = build_conditions unless defined?(@conditions) - @conditions - end - - alias_method :sql_conditions, :conditions - protected def target_scope @@ -17,7 +10,8 @@ module ActiveRecord end def association_scope - scope = super.joins(construct_joins).where(conditions) + scope = super.joins(construct_joins) + scope = add_conditions(scope) unless @reflection.options[:include] scope = scope.includes(@reflection.source_reflection.options[:include]) end @@ -101,34 +95,35 @@ module ActiveRecord join_attributes end - def build_conditions - through_conditions = build_through_conditions - source_conditions = @reflection.source_reflection.options[:conditions] - uses_sti = !@reflection.through_reflection.klass.descends_from_active_record? - - if through_conditions || source_conditions || uses_sti - all = [] - all << interpolate_sql(sanitize_sql(source_conditions)) if source_conditions - all << through_conditions if through_conditions - all << build_sti_condition if uses_sti - - all.map { |sql| "(#{sql})" } * ' AND ' + # The reason that we are operating directly on the scope here (rather than passing + # back some arel conditions to be added to the scope) is because scope.where([x, y]) + # has a different meaning to scope.where(x).where(y) - the first version might + # perform some substitution if x is a string. + def add_conditions(scope) + unless @reflection.through_reflection.klass.descends_from_active_record? + scope = scope.where(@reflection.through_reflection.klass.send(:type_condition)) end + + scope = scope.where(@reflection.source_reflection.options[:conditions]) + scope.where(through_conditions) end - def build_through_conditions + # If there is a hash of conditions then we make sure the keys are scoped to the + # through table name if left ambiguous. + def through_conditions conditions = @reflection.through_reflection.options[:conditions] + if conditions.is_a?(Hash) - interpolate_sql(@reflection.through_reflection.klass.send(:sanitize_sql, conditions)).gsub( - @reflection.quoted_table_name, - @reflection.through_reflection.quoted_table_name) - elsif conditions - interpolate_sql(sanitize_sql(conditions)) - end - end + Hash[conditions.map { |key, value| + unless value.is_a?(Hash) || key.to_s.include?('.') + key = aliased_through_table.name + '.' + key.to_s + end - def build_sti_condition - @reflection.through_reflection.klass.send(:type_condition).to_sql + [key, value] + }] + else + conditions + end end def stale_state -- cgit v1.2.3 From d392c67d2c614644d678627e6cd0124878982fc7 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 20:04:00 +0000 Subject: Remove unused methods conditions, sql_conditions and sanitize_sql --- .../lib/active_record/associations/association_proxy.rb | 12 ------------ 1 file changed, 12 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index e7c9bbd192..59b0c54f2f 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -108,13 +108,6 @@ module ActiveRecord @reflection.klass.table_name end - # Returns the SQL string that corresponds to the :conditions - # option of the macro, if given, or +nil+ otherwise. - def conditions - @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions - end - alias :sql_conditions :conditions - # Resets the \loaded flag to +false+ and sets the \target to +nil+. def reset @loaded = false @@ -249,11 +242,6 @@ module ActiveRecord @owner.send(:interpolate_sql, sql, record) end - # Forwards the call to the reflection class. - def sanitize_sql(sql, table_name = @reflection.klass.table_name) - @reflection.klass.send(:sanitize_sql, sql, table_name) - end - def select_value @reflection.options[:select] end -- cgit v1.2.3 From de05e2fb15ee4fd521aae202eb4517ae05114c28 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 20:47:06 +0000 Subject: Abstract load_target conditional logic --- .../lib/active_record/associations/association_collection.rb | 2 +- .../lib/active_record/associations/association_proxy.rb | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 3c939b0ef0..95526be82f 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -359,7 +359,7 @@ module ActiveRecord end def load_target - if (!@owner.new_record? || foreign_key_present?) && !loaded? + if find_target? targets = [] begin diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 59b0c54f2f..ead2c5ede2 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -226,18 +226,19 @@ module ActiveRecord # ActiveRecord::RecordNotFound is rescued within the method, and it is # not reraised. The proxy is \reset and +nil+ is the return value. def load_target - if !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass - @target = find_target - end - + @target = find_target if find_target? loaded - @target + target rescue ActiveRecord::RecordNotFound reset end private + def find_target? + !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass + end + def interpolate_sql(sql, record = nil) @owner.send(:interpolate_sql, sql, record) end -- cgit v1.2.3 From aa86420be24d7df9c07379bcf6f33904d0d41adc Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 20:57:11 +0000 Subject: Rename AssociationProxy#loaded to loaded! as it mutates the association --- activerecord/lib/active_record/association_preload.rb | 6 +++--- .../lib/active_record/associations/association_collection.rb | 2 +- activerecord/lib/active_record/associations/association_proxy.rb | 6 +++--- .../lib/active_record/associations/class_methods/join_dependency.rb | 2 +- activerecord/lib/active_record/associations/has_many_association.rb | 2 +- activerecord/lib/active_record/autosave_association.rb | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index cba4bab3ef..b83c00e9f8 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -125,7 +125,7 @@ module ActiveRecord def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| association_proxy = parent_record.send(reflection_name) - association_proxy.loaded + association_proxy.loaded! association_proxy.target.concat(Array.wrap(associated_record)) association_proxy.send(:set_inverse_instance, associated_record) end @@ -187,7 +187,7 @@ module ActiveRecord id_to_record_map = construct_id_map(records) - records.each {|record| record.send(reflection.name).loaded} + records.each { |record| record.send(reflection.name).loaded! } options = reflection.options right = Arel::Table.new(options[:join_table]).alias('t0') @@ -268,7 +268,7 @@ module ActiveRecord foreign_key = reflection.through_reflection_foreign_key id_to_record_map = construct_id_map(records, foreign_key || reflection.options[:primary_key]) - records.each {|record| record.send(reflection.name).loaded} + records.each { |record| record.send(reflection.name).loaded! } if options[:through] through_records = preload_through_records(records, reflection, options[:through]) diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 95526be82f..50c69abefb 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -371,7 +371,7 @@ module ActiveRecord @target = merge_target_lists(targets, @target) end - loaded + loaded! target end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index ead2c5ede2..8e16246e80 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -128,7 +128,7 @@ module ActiveRecord end # Asserts the \target has been loaded setting the \loaded flag to +true+. - def loaded + def loaded! @loaded = true @stale_state = stale_state end @@ -152,7 +152,7 @@ module ActiveRecord # Sets the target of this proxy to \target, and the \loaded flag to +true+. def target=(target) @target = target - loaded + loaded! end # Forwards the call to the target. Loads the \target if needed. @@ -227,7 +227,7 @@ module ActiveRecord # not reraised. The proxy is \reset and +nil+ is the return value. def load_target @target = find_target if find_target? - loaded + loaded! target rescue ActiveRecord::RecordNotFound reset diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb index cb3edafab1..fdd4fe8946 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb @@ -210,7 +210,7 @@ module ActiveRecord case macro when :has_many, :has_and_belongs_to_many collection = record.send(join_part.reflection.name) - collection.loaded + collection.loaded! collection.target.push(association) collection.send(:set_inverse_instance, association) when :belongs_to diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 002e1a9ae8..caefd14ee3 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -40,7 +40,7 @@ module ActiveRecord # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a # documented side-effect of the method that may avoid an extra SELECT. - @target ||= [] and loaded if count == 0 + @target ||= [] and loaded! if count == 0 [@reflection.options[:limit], count].compact.min end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 70ed16eeaf..9c7bb67479 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -368,7 +368,7 @@ module ActiveRecord if association.updated? association_id = association.send(reflection.options[:primary_key] || :id) self[reflection.foreign_key] = association_id - association.loaded + association.loaded! end saved if autosave -- cgit v1.2.3 From db503c4142d85cc1d2d511873f8eb7e7250bbedb Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 20:58:43 +0000 Subject: load_target returns the target --- activerecord/lib/active_record/associations/association_collection.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 50c69abefb..c112f2b5d0 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -22,8 +22,7 @@ module ActiveRecord def select(select = nil) if block_given? - load_target - @target.select.each { |e| yield e } + load_target.select.each { |e| yield e } else scoped.select(select) end -- cgit v1.2.3 From 1e7cf6c7e9aede02e417885847346c225d309302 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 22:11:20 +0000 Subject: Try to make fetch_first_or_last_using_find? more readable --- .../associations/association_collection.rb | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index c112f2b5d0..0ab2b4fb9d 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -513,9 +513,27 @@ module ActiveRecord end end + # Should we deal with assoc.first or assoc.last by issuing an independent query to + # the database, or by getting the target, and then taking the first/last item from that? + # + # If the args is just a non-empty options hash, go to the database. + # + # Otherwise, go to the database only if none of the following are true: + # * target already loaded + # * owner is new record + # * custom :finder_sql exists + # * target contains new or changed record(s) + # * the first arg is an integer (which indicates the number of records to be returned) def fetch_first_or_last_using_find?(args) - (args.first.kind_of?(Hash) && !args.first.empty?) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] || - @target.any? { |record| record.new_record? || record.changed? } || args.first.kind_of?(Integer)) + if args.first.kind_of?(Hash) && !args.first.empty? + true + else + !(loaded? || + @owner.new_record? || + @reflection.options[:finder_sql] || + @target.any? { |record| record.new_record? || record.changed? } || + args.first.kind_of?(Integer)) + end end def include_in_memory?(record) -- cgit v1.2.3 From 1a4bbaf106f09c8a67c7e4da109a46449c06374f Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 22:14:32 +0000 Subject: Use scoped.first and scoped.last instead of find(:first, ...) and find(:last, ...) --- activerecord/lib/active_record/associations/association_collection.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 0ab2b4fb9d..2b0ab32f34 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -39,7 +39,7 @@ module ActiveRecord # Fetches the first one using SQL if possible. def first(*args) if fetch_first_or_last_using_find?(args) - find(:first, *args) + scoped.first(*args) else load_target unless loaded? args.shift if args.first.kind_of?(Hash) && args.first.empty? @@ -50,7 +50,7 @@ module ActiveRecord # Fetches the last one using SQL if possible. def last(*args) if fetch_first_or_last_using_find?(args) - find(:last, *args) + scoped.last(*args) else load_target unless loaded? @target.last(*args) -- cgit v1.2.3 From e8d7152a89f16a487a3d67d16a63bf065183b405 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 22:15:38 +0000 Subject: Use scoped.find directly rather than having a find_by_sql method --- .../lib/active_record/associations/association_collection.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 2b0ab32f34..5b9f2f99db 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -32,7 +32,7 @@ module ActiveRecord if @reflection.options[:finder_sql] find_by_scan(*args) else - find_by_sql(*args) + scoped.find(*args) end end @@ -560,10 +560,6 @@ module ActiveRecord load_target.select { |r| ids.include?(r.id) } end end - - def find_by_sql(*args) - scoped.find(*args) - end end end end -- cgit v1.2.3 From b7bcc7e1905062f330e0a84b93a1ecacfea2a4c0 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 22:25:32 +0000 Subject: DRY up first/last and hence make last benefit from the bugfix in first --- .../associations/association_collection.rb | 28 ++++++++++------------ .../associations/has_many_associations_test.rb | 6 +++++ 2 files changed, 19 insertions(+), 15 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 5b9f2f99db..a59de18313 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -36,25 +36,12 @@ module ActiveRecord end end - # Fetches the first one using SQL if possible. def first(*args) - if fetch_first_or_last_using_find?(args) - scoped.first(*args) - else - load_target unless loaded? - args.shift if args.first.kind_of?(Hash) && args.first.empty? - @target.first(*args) - end + first_or_last(:first, *args) end - # Fetches the last one using SQL if possible. def last(*args) - if fetch_first_or_last_using_find?(args) - scoped.last(*args) - else - load_target unless loaded? - @target.last(*args) - end + first_or_last(:last, *args) end def to_ary @@ -560,6 +547,17 @@ module ActiveRecord load_target.select { |r| ids.include?(r.id) } end end + + # Fetches the first/last using SQL if possible, otherwise from the target array. + def first_or_last(type, *args) + if fetch_first_or_last_using_find?(args) + scoped.send(type, *args) + else + load_target unless loaded? + args.shift if args.first.kind_of?(Hash) && args.first.empty? + @target.send(type, *args) + end + end end end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 2ca412d424..3bec9c97f4 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -86,10 +86,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Car.create(:name => 'honda') bulbs = Car.first.bulbs bulbs.inspect # to load all instances of bulbs + assert_no_queries do bulbs.first() bulbs.first({}) end + + assert_no_queries do + bulbs.last() + bulbs.last({}) + end end def test_create_resets_cached_counters -- cgit v1.2.3 From 47309826e4d7d402c248ff507c4c4ef7a867449a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 22:27:48 +0000 Subject: load_target will return the target. it also will not load if loaded? is true. --- activerecord/lib/active_record/associations/association_collection.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index a59de18313..07b5b07195 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -553,9 +553,8 @@ module ActiveRecord if fetch_first_or_last_using_find?(args) scoped.send(type, *args) else - load_target unless loaded? args.shift if args.first.kind_of?(Hash) && args.first.empty? - @target.send(type, *args) + load_target.send(type, *args) end end end -- cgit v1.2.3 From 5dd3dadcdd17b1bb367a127c10ae7aee4916e15c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 22:34:16 +0000 Subject: target is always an array --- .../lib/active_record/associations/association_collection.rb | 5 ----- 1 file changed, 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 07b5b07195..76cdc6edd5 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -46,11 +46,6 @@ module ActiveRecord def to_ary load_target - if @target.is_a?(Array) - @target - else - Array.wrap(@target) - end end alias_method :to_a, :to_ary -- cgit v1.2.3 From fdee153ff8cde2d44d751f8f961e2df13a80cd5c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 22:41:34 +0000 Subject: Get rid of separate reset_target! and reset_scopes_cache! methods --- .../associations/association_collection.rb | 43 +++++++++------------- 1 file changed, 17 insertions(+), 26 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 76cdc6edd5..586965bb3b 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -50,9 +50,9 @@ module ActiveRecord alias_method :to_a, :to_ary def reset - reset_target! - reset_scopes_cache! - @loaded = false + @_scopes_cache = {} + @loaded = false + @target = [] end def build(attributes = {}, &block) @@ -106,10 +106,20 @@ module ActiveRecord # # See delete for more info. def delete_all - load_target - delete(@target) - reset_target! - reset_scopes_cache! + delete(load_target).tap do + reset + loaded! + end + end + + # Destroy all the records from this association. + # + # See destroy for more info. + def destroy_all + destroy(load_target).tap do + reset + loaded! + end end # Calculate sum using SQL, not Enumerable @@ -194,17 +204,6 @@ module ActiveRecord self end - # Destroy all the records from this association. - # - # See destroy for more info. - def destroy_all - load_target - destroy(@target).tap do - reset_target! - reset_scopes_cache! - end - end - def create(attrs = {}) if attrs.is_a?(Array) attrs.collect { |attr| create(attr) } @@ -395,14 +394,6 @@ module ActiveRecord interpolate_sql(@reflection.options[:finder_sql]) end - def reset_target! - @target = [] - end - - def reset_scopes_cache! - @_scopes_cache = {} - end - def find_target records = if @reflection.options[:finder_sql] -- cgit v1.2.3 From ca7785847eafaf687b10c7757dc034d08d1e6ba8 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 22:47:18 +0000 Subject: Condense first_or_last a bit more --- .../lib/active_record/associations/association_collection.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 586965bb3b..7151f53fdb 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -498,7 +498,7 @@ module ActiveRecord # * target contains new or changed record(s) # * the first arg is an integer (which indicates the number of records to be returned) def fetch_first_or_last_using_find?(args) - if args.first.kind_of?(Hash) && !args.first.empty? + if args.first.is_a?(Hash) true else !(loaded? || @@ -536,12 +536,10 @@ module ActiveRecord # Fetches the first/last using SQL if possible, otherwise from the target array. def first_or_last(type, *args) - if fetch_first_or_last_using_find?(args) - scoped.send(type, *args) - else - args.shift if args.first.kind_of?(Hash) && args.first.empty? - load_target.send(type, *args) - end + args.shift if args.first.is_a?(Hash) && args.first.empty? + + collection = fetch_first_or_last_using_find?(args) ? scoped : load_target + collection.send(type, *args) end end end -- cgit v1.2.3 From 140b269fb7aee85e6da32da5305f771099d35af5 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 23:01:30 +0000 Subject: Call sum on the scope directly, rather than relying on method_missing and calculate --- activerecord/lib/active_record/associations/association_collection.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 7151f53fdb..7fd15e4bd6 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -125,9 +125,9 @@ module ActiveRecord # Calculate sum using SQL, not Enumerable def sum(*args) if block_given? - calculate(:sum, *args) { |*block_args| yield(*block_args) } + scoped.sum(*args) { |*block_args| yield(*block_args) } else - calculate(:sum, *args) + scoped.sum(*args) end end -- cgit v1.2.3 From 1da1ac159f9391b9a053a0fb0d426499b9edd5b7 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 23:11:38 +0000 Subject: Just use primary_key here, AR::Relation will resolve the ambiguity before it is converted to SQL --- activerecord/lib/active_record/associations/association_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 7fd15e4bd6..7504773639 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -146,7 +146,7 @@ module ActiveRecord else if @reflection.options[:uniq] # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" unless column_name + column_name ||= @reflection.klass.primary_key options.merge!(:distinct => true) end -- cgit v1.2.3 From 88df88095c82cde53501abe2a44f6c1f66c272b4 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 23:30:11 +0000 Subject: AssociationCollection#to_ary should definitely dup the target! Also changed #replace which was previously incorrect, but the test passed due to the fact that to_a was not duping. --- .../lib/active_record/associations/association_collection.rb | 6 +++--- activerecord/test/cases/associations/has_many_associations_test.rb | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 7504773639..f2997b9db3 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -45,7 +45,7 @@ module ActiveRecord end def to_ary - load_target + load_target.dup end alias_method :to_a, :to_ary @@ -289,13 +289,13 @@ module ActiveRecord # This will perform a diff and delete/add only records that have changed. def replace(other_array) other_array.each { |val| raise_on_type_mismatch(val) } - - load_target + original_target = load_target.dup transaction do delete(@target - other_array) unless concat(other_array - @target) + @target = original_target raise RecordNotSaved, "Failed to replace #{@reflection.name} because one or more of the " "new records could not be saved." end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 3bec9c97f4..5904966ee5 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1349,4 +1349,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal reply.id, first.id assert_equal true, first.approved? end + + def test_to_a_should_dup_target + ary = topics(:first).replies.to_a + target = topics(:first).replies.target + + assert_not_equal target.object_id, ary.object_id + end end -- cgit v1.2.3 From 2e24cf7cc2392c8fa252ef3b4831a4516ae852d6 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 23:43:44 +0000 Subject: AssociationCollection#clear can basically just use #delete_all, except it should return self. --- .../associations/association_collection.rb | 20 +++++++------------- .../cases/associations/has_many_associations_test.rb | 9 ++++++--- 2 files changed, 13 insertions(+), 16 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index f2997b9db3..8e63159190 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -112,6 +112,13 @@ module ActiveRecord end end + # Identical to delete_all, except that the return value is the association (for chaining) + # rather than the records which have been removed. + def clear + delete_all + self + end + # Destroy all the records from this association. # # See destroy for more info. @@ -191,19 +198,6 @@ module ActiveRecord load_target end - # Removes all records from this association. Returns +self+ so method calls may be chained. - def clear - unless length.zero? # forces load_target if it hasn't happened already - if @reflection.options[:dependent] == :destroy - destroy_all - else - delete_all - end - end - - self - end - def create(attrs = {}) if attrs.is_a?(Array) attrs.collect { |attr| create(attr) } diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 5904966ee5..e36124a055 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -662,8 +662,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert_equal 2, companies(:first_firm).clients_of_firm.size - companies(:first_firm).clients_of_firm.delete_all + clients = companies(:first_firm).clients_of_firm.to_a + assert_equal 2, clients.count + deleted = companies(:first_firm).clients_of_firm.delete_all + assert_equal clients.sort_by(&:id), deleted.sort_by(&:id) assert_equal 0, companies(:first_firm).clients_of_firm.size assert_equal 0, companies(:first_firm).clients_of_firm(true).size end @@ -683,11 +685,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase client_id = firm.clients_of_firm.first.id assert_equal 1, firm.clients_of_firm.size - firm.clients_of_firm.clear + cleared = firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size assert_equal 0, firm.clients_of_firm(true).size assert_equal [], Client.destroyed_client_ids[firm.id] + assert_equal firm.clients_of_firm.object_id, cleared.object_id # Should not be destroyed since the association is not dependent. assert_nothing_raised do -- cgit v1.2.3 From 0645fd2c80c05109f148e28cdf78d636406cb6d7 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 23:50:04 +0000 Subject: Don't use method_missing when we don't have to --- activerecord/lib/active_record/associations/association_collection.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 8e63159190..b1a8d772f3 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -257,7 +257,7 @@ module ActiveRecord def any? if block_given? - method_missing(:any?) { |*block_args| yield(*block_args) } + load_target.any? { |*block_args| yield(*block_args) } else !empty? end @@ -266,7 +266,7 @@ module ActiveRecord # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1. def many? if block_given? - method_missing(:many?) { |*block_args| yield(*block_args) } + load_target.many? { |*block_args| yield(*block_args) } else size > 1 end -- cgit v1.2.3 From 59d54c3ebaa08c3298a36d1655a2492074ed8a87 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 24 Jan 2011 23:55:14 +0000 Subject: Make AssociationCollection#include? a bit more readable --- .../active_record/associations/association_collection.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index b1a8d772f3..2811f53424 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -297,10 +297,16 @@ module ActiveRecord end def include?(record) - return false unless record.is_a?(@reflection.klass) - return include_in_memory?(record) if record.new_record? - load_target if @reflection.options[:finder_sql] && !loaded? - loaded? ? @target.include?(record) : exists?(record) + if record.is_a?(@reflection.klass) + if record.new_record? + include_in_memory?(record) + else + load_target if @reflection.options[:finder_sql] + loaded? ? @target.include?(record) : scoped.exists?(record) + end + else + false + end end def respond_to?(method, include_private = false) -- cgit v1.2.3 From c5e912a8b38bc3555385e43a052a8d3eb0541ff3 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 26 Jan 2011 22:40:05 +0000 Subject: @join_table_name is no longer used --- .../active_record/associations/has_and_belongs_to_many_association.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 6ca287a4e3..3329a4af8e 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -5,8 +5,7 @@ module ActiveRecord attr_reader :join_table def initialize(owner, reflection) - @join_table_name = reflection.options[:join_table] - @join_table = Arel::Table.new(@join_table_name) + @join_table = Arel::Table.new(reflection.options[:join_table]) super end -- cgit v1.2.3 From 3fa61ccb9eed0f17cdef85470ae708b4b09a3c06 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 26 Jan 2011 23:00:13 +0000 Subject: Has many through - It is not necessary to manually merge in the conditions hash for the through record, because the creation is done directly on the through association, which will already handle setting the conditions. --- .../lib/active_record/associations/through_association.rb | 4 ---- .../cases/associations/has_many_through_associations_test.rb | 12 +++++++++++- activerecord/test/models/club.rb | 3 ++- activerecord/test/models/member.rb | 3 +++ 4 files changed, 16 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index ac62f854e8..be1fc79846 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -88,10 +88,6 @@ module ActiveRecord join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name) end - if @reflection.through_reflection.options[:conditions].is_a?(Hash) - join_attributes.merge!(@reflection.through_reflection.options[:conditions]) - end - join_attributes end 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 7235631b5a..96f4597726 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -20,10 +20,13 @@ require 'models/subscription' require 'models/categorization' require 'models/category' require 'models/essay' +require 'models/member' +require 'models/membership' +require 'models/club' class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, - :owners, :pets, :toys, :jobs, :references, :companies, + :owners, :pets, :toys, :jobs, :references, :companies, :members, :subscribers, :books, :subscriptions, :developers, :categorizations # Dummies to force column loads so query counts are clean. @@ -557,4 +560,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert proxy.stale_target? assert_equal authors(:david).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id) end + + def test_create_with_conditions_hash_on_through_association + member = members(:groucho) + club = member.clubs.create! + + assert_equal true, club.reload.membership.favourite + end end diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 6e7cdd643a..c432a6ace8 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -1,4 +1,5 @@ class Club < ActiveRecord::Base + has_one :membership has_many :memberships has_many :members, :through => :memberships has_many :current_memberships @@ -10,4 +11,4 @@ class Club < ActiveRecord::Base def private_method "I'm sorry sir, this is a *private* club, not a *pirate* club" end -end \ No newline at end of file +end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 15ad6aedd3..e6e78f9e45 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -13,4 +13,7 @@ class Member < ActiveRecord::Base has_many :current_memberships has_one :club_through_many, :through => :current_memberships, :source => :club + + has_many :current_memberships, :conditions => { :favourite => true } + has_many :clubs, :through => :current_memberships end -- cgit v1.2.3 From 9db4c07e0bdf60982d08cb26035573995404eb98 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Wed, 26 Jan 2011 23:17:40 +0000 Subject: Make use of helpers in AssociationReflection --- .../lib/active_record/associations/through_association.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index be1fc79846..c840a16160 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -47,9 +47,9 @@ module ActiveRecord conditions = [] if @reflection.source_reflection.macro == :belongs_to - reflection_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.klass.primary_key + reflection_primary_key = @reflection.source_reflection.association_primary_key source_primary_key = @reflection.source_reflection.foreign_key + if @reflection.options[:source_type] column = @reflection.source_reflection.foreign_type conditions << @@ -57,8 +57,8 @@ module ActiveRecord end else reflection_primary_key = @reflection.source_reflection.foreign_key - source_primary_key = @reflection.source_reflection.options[:primary_key] || - @reflection.through_reflection.klass.primary_key + source_primary_key = @reflection.source_reflection.active_record_primary_key + if @reflection.source_reflection.options[:as] column = "#{@reflection.source_reflection.options[:as]}_type" conditions << -- cgit v1.2.3 From 30176f28a41681c7607eed39d03501327869d40c Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 31 Jan 2011 13:21:03 +0000 Subject: Add :bulk => true option to change_table --- activerecord/CHANGELOG | 13 ++ .../abstract/schema_statements.rb | 90 +++++++++----- .../connection_adapters/abstract_adapter.rb | 4 + .../connection_adapters/mysql_adapter.rb | 114 ++++++++++++----- .../active_record/migration/command_recorder.rb | 20 ++- .../test/cases/migration/command_recorder_test.rb | 2 +- activerecord/test/cases/migration_test.rb | 138 +++++++++++++++++++++ 7 files changed, 316 insertions(+), 65 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index d84fe655a7..d1124801df 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,18 @@ *Rails 3.1.0 (unreleased)* +* Add :bulk => true option to change_table to make all the schema changes defined in change_table block using a single ALTER statement. [Pratik Naik] + + Example: + + change_table(:users, :bulk => true) do |t| + t.string :company_name + t.change :birthdate, :datetime + end + + This will now result in: + + ALTER TABLE `users` ADD COLUMN `company_name` varchar(255), CHANGE `updated_at` `updated_at` datetime DEFAULT NULL + * Removed support for accessing attributes on a has_and_belongs_to_many join table. This has been documented as deprecated behaviour since April 2006. Please use has_many :through instead. [Jon Leighton] 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 5b9c48bafa..3ec7dd02a4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -176,6 +176,13 @@ module ActiveRecord # # Other column alterations here # end # + # The +options+ hash can include the following keys: + # [:bulk] + # Set this to true to make this a bulk alter query, such as + # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ... + # + # Defaults to false. + # # ===== Examples # ====== Add a column # change_table(:suppliers) do |t| @@ -224,8 +231,14 @@ module ActiveRecord # # See also Table for details on # all of the various column transformation - def change_table(table_name) - yield Table.new(table_name, self) + def change_table(table_name, options = {}) + if supports_bulk_alter? && options[:bulk] + recorder = ActiveRecord::Migration::CommandRecorder.new(self) + yield Table.new(table_name, recorder) + bulk_change_table(table_name, recorder.commands) + else + yield Table.new(table_name, self) + end end # Renames a table. @@ -253,10 +266,7 @@ module ActiveRecord # remove_column(:suppliers, :qualification) # remove_columns(:suppliers, :qualification, :experience) def remove_column(table_name, *column_names) - raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? - column_names.flatten.each do |column_name| - execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}" - end + columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" } end alias :remove_columns :remove_column @@ -327,25 +337,8 @@ module ActiveRecord # # Note: SQLite doesn't support index length def add_index(table_name, column_name, options = {}) - column_names = Array.wrap(column_name) - index_name = index_name(table_name, :column => column_names) - - if Hash === options # legacy support, since this param was a string - index_type = options[:unique] ? "UNIQUE" : "" - index_name = options[:name].to_s if options.key?(:name) - else - index_type = options - end - - if index_name.length > index_name_length - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" - end - if index_name_exists?(table_name, index_name, false) - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" - end - quoted_column_names = quoted_columns_for_index(column_names, options).join(", ") - - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})" + index_name, index_type, index_columns = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})" end # Remove the given index from the table. @@ -359,11 +352,7 @@ module ActiveRecord # Remove the index named by_branch_party in the accounts table. # remove_index :accounts, :name => :by_branch_party def remove_index(table_name, options = {}) - index_name = index_name(table_name, options) - unless index_name_exists?(table_name, index_name, true) - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" - end - remove_index!(table_name, index_name) + remove_index!(table_name, index_name_for_remove(table_name, options)) end def remove_index!(table_name, index_name) #:nodoc: @@ -469,7 +458,7 @@ module ActiveRecord end def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - if native = native_database_types[type] + if native = native_database_types[type.to_sym] column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup if type == :decimal # ignore limit, use precision and scale @@ -537,6 +526,45 @@ module ActiveRecord options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + def add_index_options(table_name, column_name, options = {}) + column_names = Array.wrap(column_name) + index_name = index_name(table_name, :column => column_names) + + if Hash === options # legacy support, since this param was a string + index_type = options[:unique] ? "UNIQUE" : "" + index_name = options[:name].to_s if options.key?(:name) + else + index_type = options + end + + if index_name.length > index_name_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" + end + if index_name_exists?(table_name, index_name, false) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" + end + index_columns = quoted_columns_for_index(column_names, options).join(", ") + + [index_name, index_type, index_columns] + end + + def index_name_for_remove(table_name, options = {}) + index_name = index_name(table_name, options) + + unless index_name_exists?(table_name, index_name, true) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + + index_name + end + + def columns_for_remove(table_name, *column_names) + column_names = column_names.flatten + + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank? + column_names.map {|column_name| quote_column_name(column_name) } + end + private def table_definition TableDefinition.new(self) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 5ff5813699..79631a6570 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -77,6 +77,10 @@ module ActiveRecord false end + def supports_bulk_alter? + false + end + # Does this adapter support savepoints? PostgreSQL and MySQL do, # SQLite < 3.6.8 does not. def supports_savepoints? diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 47acf0b254..15488cee52 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -203,6 +203,10 @@ module ActiveRecord ADAPTER_NAME end + def supports_bulk_alter? #:nodoc: + true + end + # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ def supports_statement_cache? @@ -547,11 +551,23 @@ module ActiveRecord execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" end + def bulk_change_table(table_name, operations) #:nodoc: + sqls = operations.map do |command, args| + table, arguments = args.shift, args + method = :"#{command}_sql" + + if respond_to?(method) + send(method, table, *arguments) + else + raise "Unknown method called : #{method}(#{arguments.inspect})" + end + end.flatten.join(", ") + + execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") + end + def add_column(table_name, column_name, type, options = {}) - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - add_column_position!(add_column_sql, options) - execute(add_column_sql) + execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") end def change_column_default(table_name, column_name, default) #:nodoc: @@ -570,34 +586,11 @@ module ActiveRecord end def change_column(table_name, column_name, type, options = {}) #:nodoc: - column = column_for(table_name, column_name) - - unless options_include_default?(options) - options[:default] = column.default - end - - unless options.has_key?(:null) - options[:null] = column.null - end - - change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(change_column_sql, options) - add_column_position!(change_column_sql, options) - execute(change_column_sql) + execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") end def rename_column(table_name, column_name, new_column_name) #:nodoc: - options = {} - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - options[:null] = column.null - else - raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" - end - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] - rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" - add_column_options!(rename_column_sql, options) - execute(rename_column_sql) + execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") end # Maps logical Rails types to MySQL-specific data types. @@ -680,6 +673,69 @@ module ActiveRecord end end + def add_column_sql(table_name, column_name, type, options = {}) + add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_position!(add_column_sql, options) + add_column_sql + end + + def remove_column_sql(table_name, *column_names) + columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" } + end + alias :remove_columns_sql :remove_column + + def change_column_sql(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null + end + + change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + change_column_sql + end + + def rename_column_sql(table_name, column_name, new_column_name) + options = {} + + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + options[:null] = column.null + else + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + rename_column_sql + end + + def add_index_sql(table_name, column_name, options = {}) + index_name, index_type, index_columns = add_index_options(table_name, column_name, options) + "ADD #{index_type} INDEX #{index_name} (#{index_columns})" + end + + def remove_index_sql(table_name, options = {}) + index_name = index_name_for_remove(table_name, options) + "DROP INDEX #{index_name}" + end + + def add_timestamps_sql(table_name) + [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] + end + + def remove_timestamps_sql(table_name) + [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + end + private def connect encoding = @config[:encoding] diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index d7e481905a..c9d57ce812 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -40,7 +40,7 @@ module ActiveRecord @commands.reverse.map { |name, args| method = :"invert_#{name}" raise IrreversibleMigration unless respond_to?(method, true) - __send__(method, args) + send(method, args) } end @@ -48,12 +48,16 @@ module ActiveRecord super || delegate.respond_to?(*args) end - def send(method, *args) # :nodoc: - return super unless respond_to?(method) - record(method, args) + [:create_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method| + class_eval <<-EOV, __FILE__, __LINE__ + 1 + def #{method}(*args) + record(:"#{method}", args) + end + EOV end private + def invert_create_table(args) [:drop_table, args] end @@ -86,6 +90,14 @@ module ActiveRecord def invert_add_timestamps(args) [:remove_timestamps, args] end + + # Forwards any missing method call to the \target. + def method_missing(method, *args, &block) + @delegate.send(method, *args, &block) + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}") + end + end end end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index ea2292dda5..ae531ebb4c 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -16,7 +16,7 @@ module ActiveRecord def test_send_calls_super assert_raises(NoMethodError) do - @recorder.send(:create_table, :horses) + @recorder.send(:non_existing_method, :horses) end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index a5a9965c3a..c2a80b02b6 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1923,6 +1923,144 @@ if ActiveRecord::Base.connection.supports_migrations? end end + class AlterTableMigrationsTest < ActiveRecord::TestCase + def setup + @connection = Person.connection + @connection.create_table(:delete_me, :force => true) {|t| } + end + + def teardown + Person.connection.drop_table(:delete_me) rescue nil + end + + def test_adding_multiple_columns + assert_queries(1) do + with_bulk_change_table do |t| + t.column :name, :string + t.string :qualification, :experience + t.integer :age, :default => 0 + t.date :birthdate + t.timestamps + end + end + + assert_equal 8, columns.size + [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } + assert_equal 0, column(:age).default + end + + def test_removing_columns + with_bulk_change_table do |t| + t.string :qualification, :experience + end + + [:qualification, :experience].each {|c| assert column(c) } + + assert_queries(1) do + with_bulk_change_table do |t| + t.remove :qualification, :experience + t.string :qualification_experience + end + end + + [:qualification, :experience].each {|c| assert ! column(c) } + assert column(:qualification_experience) + end + + def test_adding_indexes + with_bulk_change_table do |t| + t.string :username + t.string :name + t.integer :age + end + + # Adding an index fires a query everytime to check if an index already exists or not + assert_queries(3) do + with_bulk_change_table do |t| + t.index :username, :unique => true, :name => :awesome_username_index + t.index [:name, :age] + end + end + + assert_equal 2, indexes.size + + name_age_index = index(:index_delete_me_on_name_and_age) + assert_equal ['name', 'age'].sort, name_age_index.columns.sort + assert ! name_age_index.unique + + assert index(:awesome_username_index).unique + end + + def test_removing_index + with_bulk_change_table do |t| + t.string :name + t.index :name + end + + assert index(:index_delete_me_on_name) + + assert_queries(3) do + with_bulk_change_table do |t| + t.remove_index :name + t.index :name, :name => :new_name_index, :unique => true + end + end + + assert ! index(:index_delete_me_on_name) + + new_name_index = index(:new_name_index) + assert new_name_index.unique + end + + def test_changing_columns + with_bulk_change_table do |t| + t.string :name + t.date :birthdate + end + + assert ! column(:name).default + assert_equal :date, column(:birthdate).type + + assert_queries(1) do + with_bulk_change_table do |t| + t.change :name, :string, :default => 'NONAME' + t.change :birthdate, :datetime + end + end + + assert_equal 'NONAME', column(:name).default + assert_equal :datetime, column(:birthdate).type + end + + protected + + def with_bulk_change_table + # Reset columns/indexes cache as we're changing the table + @columns = @indexes = nil + + Person.connection.change_table(:delete_me, :bulk => true) do |t| + yield t + end + end + + def column(name) + columns.detect {|c| c.name == name.to_s } + end + + def columns + @columns ||= Person.connection.columns('delete_me') + end + + def index(name) + indexes.detect {|i| i.name == name.to_s } + end + + def indexes + @indexes ||= Person.connection.indexes('delete_me') + end + + end + class CopyMigrationsTest < ActiveRecord::TestCase def setup end -- cgit v1.2.3 From 9666b6a625c9fef555c2a18df6721fed2000f131 Mon Sep 17 00:00:00 2001 From: Pratik Naik Date: Mon, 31 Jan 2011 14:10:51 +0000 Subject: Run BulkAlterTableMigrationsTest only when the adapter supports them --- activerecord/test/cases/migration_test.rb | 196 +++++++++++++++--------------- 1 file changed, 99 insertions(+), 97 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index c2a80b02b6..6f0f73e3bd 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1923,141 +1923,143 @@ if ActiveRecord::Base.connection.supports_migrations? end end - class AlterTableMigrationsTest < ActiveRecord::TestCase - def setup - @connection = Person.connection - @connection.create_table(:delete_me, :force => true) {|t| } - end - - def teardown - Person.connection.drop_table(:delete_me) rescue nil - end + if ActiveRecord::Base.connection.supports_bulk_alter? + class BulkAlterTableMigrationsTest < ActiveRecord::TestCase + def setup + @connection = Person.connection + @connection.create_table(:delete_me, :force => true) {|t| } + end - def test_adding_multiple_columns - assert_queries(1) do - with_bulk_change_table do |t| - t.column :name, :string - t.string :qualification, :experience - t.integer :age, :default => 0 - t.date :birthdate - t.timestamps - end + def teardown + Person.connection.drop_table(:delete_me) rescue nil end - assert_equal 8, columns.size - [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } - assert_equal 0, column(:age).default - end + def test_adding_multiple_columns + assert_queries(1) do + with_bulk_change_table do |t| + t.column :name, :string + t.string :qualification, :experience + t.integer :age, :default => 0 + t.date :birthdate + t.timestamps + end + end - def test_removing_columns - with_bulk_change_table do |t| - t.string :qualification, :experience + assert_equal 8, columns.size + [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } + assert_equal 0, column(:age).default end - [:qualification, :experience].each {|c| assert column(c) } - - assert_queries(1) do + def test_removing_columns with_bulk_change_table do |t| - t.remove :qualification, :experience - t.string :qualification_experience + t.string :qualification, :experience end - end - [:qualification, :experience].each {|c| assert ! column(c) } - assert column(:qualification_experience) - end + [:qualification, :experience].each {|c| assert column(c) } - def test_adding_indexes - with_bulk_change_table do |t| - t.string :username - t.string :name - t.integer :age + assert_queries(1) do + with_bulk_change_table do |t| + t.remove :qualification, :experience + t.string :qualification_experience + end + end + + [:qualification, :experience].each {|c| assert ! column(c) } + assert column(:qualification_experience) end - # Adding an index fires a query everytime to check if an index already exists or not - assert_queries(3) do + def test_adding_indexes with_bulk_change_table do |t| - t.index :username, :unique => true, :name => :awesome_username_index - t.index [:name, :age] + t.string :username + t.string :name + t.integer :age end - end - assert_equal 2, indexes.size + # Adding an index fires a query everytime to check if an index already exists or not + assert_queries(3) do + with_bulk_change_table do |t| + t.index :username, :unique => true, :name => :awesome_username_index + t.index [:name, :age] + end + end - name_age_index = index(:index_delete_me_on_name_and_age) - assert_equal ['name', 'age'].sort, name_age_index.columns.sort - assert ! name_age_index.unique + assert_equal 2, indexes.size - assert index(:awesome_username_index).unique - end + name_age_index = index(:index_delete_me_on_name_and_age) + assert_equal ['name', 'age'].sort, name_age_index.columns.sort + assert ! name_age_index.unique - def test_removing_index - with_bulk_change_table do |t| - t.string :name - t.index :name + assert index(:awesome_username_index).unique end - assert index(:index_delete_me_on_name) - - assert_queries(3) do + def test_removing_index with_bulk_change_table do |t| - t.remove_index :name - t.index :name, :name => :new_name_index, :unique => true + t.string :name + t.index :name end - end - assert ! index(:index_delete_me_on_name) + assert index(:index_delete_me_on_name) - new_name_index = index(:new_name_index) - assert new_name_index.unique - end + assert_queries(3) do + with_bulk_change_table do |t| + t.remove_index :name + t.index :name, :name => :new_name_index, :unique => true + end + end - def test_changing_columns - with_bulk_change_table do |t| - t.string :name - t.date :birthdate - end + assert ! index(:index_delete_me_on_name) - assert ! column(:name).default - assert_equal :date, column(:birthdate).type + new_name_index = index(:new_name_index) + assert new_name_index.unique + end - assert_queries(1) do + def test_changing_columns with_bulk_change_table do |t| - t.change :name, :string, :default => 'NONAME' - t.change :birthdate, :datetime + t.string :name + t.date :birthdate end - end - assert_equal 'NONAME', column(:name).default - assert_equal :datetime, column(:birthdate).type - end + assert ! column(:name).default + assert_equal :date, column(:birthdate).type - protected + assert_queries(1) do + with_bulk_change_table do |t| + t.change :name, :string, :default => 'NONAME' + t.change :birthdate, :datetime + end + end + + assert_equal 'NONAME', column(:name).default + assert_equal :datetime, column(:birthdate).type + end - def with_bulk_change_table - # Reset columns/indexes cache as we're changing the table - @columns = @indexes = nil + protected - Person.connection.change_table(:delete_me, :bulk => true) do |t| - yield t + def with_bulk_change_table + # Reset columns/indexes cache as we're changing the table + @columns = @indexes = nil + + Person.connection.change_table(:delete_me, :bulk => true) do |t| + yield t + end end - end - def column(name) - columns.detect {|c| c.name == name.to_s } - end + def column(name) + columns.detect {|c| c.name == name.to_s } + end - def columns - @columns ||= Person.connection.columns('delete_me') - end + def columns + @columns ||= Person.connection.columns('delete_me') + end - def index(name) - indexes.detect {|i| i.name == name.to_s } - end + def index(name) + indexes.detect {|i| i.name == name.to_s } + end - def indexes - @indexes ||= Person.connection.indexes('delete_me') - end + def indexes + @indexes ||= Person.connection.indexes('delete_me') + end + end # AlterTableMigrationsTest end -- cgit v1.2.3 From 57bc25c5f8129f57b08a2dc7c319b86778dd8a40 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Sun, 9 Jan 2011 10:15:05 -0800 Subject: Use run_callbacks; the generated _run__callbacks method is not a public interface. Signed-off-by: Santiago Pastorino --- activerecord/lib/active_record/base.rb | 6 +++--- activerecord/lib/active_record/callbacks.rb | 10 +++++----- .../connection_adapters/abstract/connection_pool.rb | 2 +- activerecord/lib/active_record/transactions.rb | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 00324b14f2..220d861a27 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1400,7 +1400,7 @@ MSG self.attributes = attributes unless attributes.nil? result = yield self if block_given? - _run_initialize_callbacks + run_callbacks :initialize result end @@ -1437,8 +1437,8 @@ MSG @aggregation_cache = {} @readonly = @destroyed = @marked_for_destruction = false @new_record = false - _run_find_callbacks - _run_initialize_callbacks + run_callbacks :find + run_callbacks :initialize end # Specifies how the record is dumped by +Marshal+. diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 47428cfd0f..8f137e3b21 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -237,25 +237,25 @@ module ActiveRecord end def destroy #:nodoc: - _run_destroy_callbacks { super } + run_callbacks(:destroy) { super } end def touch(*) #:nodoc: - _run_touch_callbacks { super } + run_callbacks(:touch) { super } end private def create_or_update #:nodoc: - _run_save_callbacks { super } + run_callbacks(:save) { super } end def create #:nodoc: - _run_create_callbacks { super } + run_callbacks(:create) { super } end def update(*) #:nodoc: - _run_update_callbacks { super } + run_callbacks(:update) { super } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index cffa2387de..54f70c59f8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -212,7 +212,7 @@ module ActiveRecord # calling +checkout+ on this pool. def checkin(conn) @connection_mutex.synchronize do - conn.send(:_run_checkin_callbacks) do + conn.run_callbacks :checkin do @checked_out.delete conn @queue.signal end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 48d2f7c9a9..45a4425944 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -259,7 +259,7 @@ module ActiveRecord # Call the after_commit callbacks def committed! #:nodoc: - _run_commit_callbacks + run_callbacks :commit ensure clear_transaction_record_state end @@ -267,7 +267,7 @@ module ActiveRecord # Call the after rollback callbacks. The restore_state argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state = false) #:nodoc: - _run_rollback_callbacks + run_callbacks :rollback ensure restore_transaction_record_state(force_restore_state) end -- cgit v1.2.3 From 4ccf8319bb3b6e9f5df507b4e9b58c649a3adc34 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 31 Jan 2011 13:57:52 -0800 Subject: expand mulasgn for enhancing readability --- .../connection_adapters/abstract/schema_definitions.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'activerecord') 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 bc8a6e9cd6..50b1a3f632 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -28,12 +28,15 @@ module ActiveRecord # It will be mapped to one of the standard Rails SQL types in the type attribute. # +null+ determines if this column allows +NULL+ values. def initialize(name, default, sql_type = nil, null = true) - @name, @sql_type, @null = name, sql_type, null - @limit, @precision, @scale = extract_limit(sql_type), extract_precision(sql_type), extract_scale(sql_type) - @type = simplified_type(sql_type) - @default = extract_default(default) - - @primary = nil + @name = name + @sql_type = sql_type + @null = null + @limit = extract_limit(sql_type) + @precision = extract_precision(sql_type) + @scale = extract_scale(sql_type) + @type = simplified_type(sql_type) + @default = extract_default(default) + @primary = nil end # Returns +true+ if the column is either of type string or text. -- cgit v1.2.3 From 5a6f651a6a093f69f00fa713648213fb86748297 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Tue, 1 Feb 2011 08:37:43 +0100 Subject: FIX not using _on_create or _on_update callbacks only _create and _update --- activerecord/lib/active_record/callbacks.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 64ffcb11fe..87b77313bb 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -31,7 +31,7 @@ module ActiveRecord # # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the # Active Record life cycle. The sequence for calling Base#save for an existing record is similar, - # except that each _on_create callback is replaced by the corresponding _on_update callback. + # except that each _create callback is replaced by the corresponding _update callback. # # Examples: # class CreditCard < ActiveRecord::Base -- cgit v1.2.3 From 817e37013610c8e8866197594d5e408b4d5daec5 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Thu, 16 Dec 2010 18:31:31 +0900 Subject: Make before_type_cast available for datetime fields [#3973 state:committed] Signed-off-by: Santiago Pastorino --- .../attribute_methods/time_zone_conversion.rb | 5 ++-- activerecord/test/cases/attribute_methods_test.rb | 31 +++++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index dc2785b6bf..a72eecb50e 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -40,12 +40,13 @@ module ActiveRecord def define_method_attribute=(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 - def #{attr_name}=(time) + def #{attr_name}=(original_time) + time = original_time.dup unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end time = time.in_time_zone rescue nil if time - write_attribute(:#{attr_name}, time) + write_attribute(:#{attr_name}, (time || original_time)) end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index a29e4349d6..06adb33ffc 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -116,24 +116,23 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - unless current_adapter?(:Mysql2Adapter) - def test_read_attributes_before_type_cast_on_datetime - developer = Developer.find(:first) - # Oracle adapter returns Time before type cast - unless current_adapter?(:OracleAdapter) - assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s - else - assert_equal developer.created_at.to_s(:db) , developer.attributes_before_type_cast["created_at"].to_s(:db) + def test_read_attributes_before_type_cast_on_datetime + developer = Developer.find(:first) + if current_adapter?(:Mysql2Adapter) + # Mysql2 keeps the value in Time instance + assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s(:db) + else + assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s + end - developer.created_at = "345643456" - assert_equal developer.created_at_before_type_cast, "345643456" - assert_equal developer.created_at, nil + developer.created_at = "345643456" - developer.created_at = "2010-03-21 21:23:32" - assert_equal developer.created_at_before_type_cast.to_s, "2010-03-21 21:23:32" - assert_equal developer.created_at, Time.parse("2010-03-21 21:23:32") - end - end + assert_equal developer.created_at_before_type_cast, "345643456" + assert_equal developer.created_at, nil + + developer.created_at = "2010-03-21 21:23:32" + assert_equal developer.created_at_before_type_cast.to_s, "2010-03-21 21:23:32" + assert_equal developer.created_at, Time.parse("2010-03-21 21:23:32") end def test_hash_content -- cgit v1.2.3 From 6bd9fac1e301d57765073e1f7a17e46972428205 Mon Sep 17 00:00:00 2001 From: Glenn Vanderburg Date: Fri, 28 Jan 2011 18:16:44 -0600 Subject: Propagate association extensions to scopes called on the association. Signed-off-by: Santiago Pastorino --- activerecord/lib/active_record/associations/association_proxy.rb | 3 +++ activerecord/test/cases/associations/extension_test.rb | 5 +++++ activerecord/test/models/comment.rb | 1 + 3 files changed, 9 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 8e16246e80..84f868ec43 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -188,6 +188,9 @@ module ActiveRecord if select = select_value scope = scope.select(select) end + if Relation === scope + scope = scope.extending(*Array.wrap(@reflection.options[:extend])) + end scope.where(construct_owner_conditions) end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index efaab8569e..e1f5b16eca 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -29,6 +29,11 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal projects(:action_controller), developers(:david).projects_extended_by_name_and_block.find_most_recent assert_equal projects(:active_record), developers(:david).projects_extended_by_name_and_block.find_least_recent end + + def test_extension_with_scopes + assert_equal comments(:greetings), posts(:welcome).comments.offset(1).find_most_recent + assert_equal comments(:greetings), posts(:welcome).comments.not_again.find_most_recent + end def test_marshalling_extensions david = developers(:david) diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index a9aa0afced..ff533717cc 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -1,6 +1,7 @@ class Comment < ActiveRecord::Base scope :limit_by, lambda {|l| limit(l) } scope :containing_the_letter_e, :conditions => "comments.body LIKE '%e%'" + scope :not_again, where("comments.body NOT LIKE '%again%'") scope :for_first_post, :conditions => { :post_id => 1 } scope :for_first_author, :joins => :post, -- cgit v1.2.3 From 451ad38bb2eafbaff48697f6d38f27cbc9cc6e9e Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Tue, 1 Feb 2011 17:20:24 -0200 Subject: scope is always a Relation --- activerecord/lib/active_record/associations/association_proxy.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 84f868ec43..07fff7f7d7 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -188,9 +188,7 @@ module ActiveRecord if select = select_value scope = scope.select(select) end - if Relation === scope - scope = scope.extending(*Array.wrap(@reflection.options[:extend])) - end + scope = scope.extending(*Array.wrap(@reflection.options[:extend])) scope.where(construct_owner_conditions) end -- cgit v1.2.3 From 2f62084d428e4806963e660c405a8b8da8f44e68 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 31 Jan 2011 14:24:49 -0800 Subject: dry up our case / when statements --- .../abstract/schema_definitions.rb | 65 +++++++++++----------- 1 file changed, 31 insertions(+), 34 deletions(-) (limited to 'activerecord') 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 50b1a3f632..e1a39feb07 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -56,52 +56,49 @@ module ActiveRecord # Returns the Ruby class that corresponds to the abstract data type. def klass case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime then Time - when :date then Date - when :timestamp then Time - when :time then Time - when :text, :string then String - when :binary then String - when :boolean then Object + when :integer then Fixnum + when :float then Float + when :decimal then BigDecimal + when :datetime, :timestamp, :time then Time + when :date then Date + when :text, :string, :binary then String + when :boolean then Object end end # Casts value (which is a String) to an appropriate instance. def type_cast(value) return nil if value.nil? + klass = self.class + case type - when :string then value - when :text then value - when :integer then value.to_i rescue value ? 1 : 0 - when :float then value.to_f - when :decimal then self.class.value_to_decimal(value) - when :datetime then self.class.string_to_time(value) - when :timestamp then self.class.string_to_time(value) - when :time then self.class.string_to_dummy_time(value) - when :date then self.class.string_to_date(value) - when :binary then self.class.binary_to_string(value) - when :boolean then self.class.value_to_boolean(value) - else value + when :string, :text then value + when :integer then value.to_i rescue value ? 1 : 0 + when :float then value.to_f + when :decimal then klass.value_to_decimal(value) + when :datetime, :timestamp then klass.string_to_time(value) + when :time then klass.string_to_dummy_time(value) + when :date then klass.string_to_date(value) + when :binary then klass.binary_to_string(value) + when :boolean then klass.value_to_boolean(value) + else value end end def type_cast_code(var_name) + klass = self.class.name + case type - when :string then var_name - when :text then var_name - when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" - when :float then "#{var_name}.to_f" - when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" - when :datetime then "#{self.class.name}.string_to_time(#{var_name})" - when :timestamp then "#{self.class.name}.string_to_time(#{var_name})" - when :time then "#{self.class.name}.string_to_dummy_time(#{var_name})" - when :date then "#{self.class.name}.string_to_date(#{var_name})" - when :binary then "#{self.class.name}.binary_to_string(#{var_name})" - when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" - else var_name + when :string, :text then var_name + when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" + when :float then "#{var_name}.to_f" + when :decimal then "#{klass}.value_to_decimal(#{var_name})" + when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" + when :time then "#{klass}.string_to_dummy_time(#{var_name})" + when :date then "#{klass}.string_to_date(#{var_name})" + when :binary then "#{klass}.binary_to_string(#{var_name})" + when :boolean then "#{klass}.value_to_boolean(#{var_name})" + else var_name end end -- cgit v1.2.3 From 6d5e3b86d581c109ad60fe771de0ac852f1af259 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 31 Jan 2011 14:38:20 -0800 Subject: namespace test so we can dry up constant lookup --- activerecord/test/cases/column_definition_test.rb | 194 +++++++++++----------- 1 file changed, 99 insertions(+), 95 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index cc6a6b44f2..f87f819812 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -1,121 +1,125 @@ require "cases/helper" -class ColumnDefinitionTest < ActiveRecord::TestCase - def setup - @adapter = ActiveRecord::ConnectionAdapters::AbstractAdapter.new(nil) - def @adapter.native_database_types - {:string => "varchar"} - end - end - - # Avoid column definitions in create table statements like: - # `title` varchar(255) DEFAULT NULL - def test_should_not_include_default_clause_when_default_is_null - column = ActiveRecord::ConnectionAdapters::Column.new("title", nil, "varchar(20)") - column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new( - @adapter, column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) - assert_equal "title varchar(20)", column_def.to_sql - end - - def test_should_include_default_clause_when_default_is_present - column = ActiveRecord::ConnectionAdapters::Column.new("title", "Hello", "varchar(20)") - column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new( - @adapter, column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) - assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, column_def.to_sql - end - - def test_should_specify_not_null_if_null_option_is_false - column = ActiveRecord::ConnectionAdapters::Column.new("title", "Hello", "varchar(20)", false) - column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new( - @adapter, column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) - assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql - end - - if current_adapter?(:MysqlAdapter) - def test_should_set_default_for_mysql_binary_data_types - binary_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", "a", "binary(1)") - assert_equal "a", binary_column.default +module ActiveRecord + module ConnectionAdapters + class ColumnDefinitionTest < ActiveRecord::TestCase + def setup + @adapter = AbstractAdapter.new(nil) + def @adapter.native_database_types + {:string => "varchar"} + end + end - varbinary_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", "a", "varbinary(1)") - assert_equal "a", varbinary_column.default - end + # Avoid column definitions in create table statements like: + # `title` varchar(255) DEFAULT NULL + def test_should_not_include_default_clause_when_default_is_null + column = Column.new("title", nil, "varchar(20)") + column_def = ColumnDefinition.new( + @adapter, column.name, "string", + column.limit, column.precision, column.scale, column.default, column.null) + assert_equal "title varchar(20)", column_def.to_sql + end - def test_should_not_set_default_for_blob_and_text_data_types - assert_raise ArgumentError do - ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", "a", "blob") + def test_should_include_default_clause_when_default_is_present + column = Column.new("title", "Hello", "varchar(20)") + column_def = ColumnDefinition.new( + @adapter, column.name, "string", + column.limit, column.precision, column.scale, column.default, column.null) + assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, column_def.to_sql end - assert_raise ArgumentError do - ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", "Hello", "text") + def test_should_specify_not_null_if_null_option_is_false + column = Column.new("title", "Hello", "varchar(20)", false) + column_def = ColumnDefinition.new( + @adapter, column.name, "string", + column.limit, column.precision, column.scale, column.default, column.null) + assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql end - text_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", nil, "text") - assert_equal nil, text_column.default + if current_adapter?(:MysqlAdapter) + def test_should_set_default_for_mysql_binary_data_types + binary_column = MysqlColumn.new("title", "a", "binary(1)") + assert_equal "a", binary_column.default - not_null_text_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", nil, "text", false) - assert_equal "", not_null_text_column.default - end + varbinary_column = MysqlColumn.new("title", "a", "varbinary(1)") + assert_equal "a", varbinary_column.default + end - def test_has_default_should_return_false_for_blog_and_test_data_types - blob_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", nil, "blob") - assert !blob_column.has_default? + def test_should_not_set_default_for_blob_and_text_data_types + assert_raise ArgumentError do + MysqlColumn.new("title", "a", "blob") + end - text_column = ActiveRecord::ConnectionAdapters::MysqlColumn.new("title", nil, "text") - assert !text_column.has_default? - end - end + assert_raise ArgumentError do + MysqlColumn.new("title", "Hello", "text") + end - if current_adapter?(:Mysql2Adapter) - def test_should_set_default_for_mysql_binary_data_types - binary_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", "a", "binary(1)") - assert_equal "a", binary_column.default + text_column = MysqlColumn.new("title", nil, "text") + assert_equal nil, text_column.default - varbinary_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", "a", "varbinary(1)") - assert_equal "a", varbinary_column.default - end + not_null_text_column = MysqlColumn.new("title", nil, "text", false) + assert_equal "", not_null_text_column.default + end - def test_should_not_set_default_for_blob_and_text_data_types - assert_raise ArgumentError do - ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", "a", "blob") - end + def test_has_default_should_return_false_for_blog_and_test_data_types + blob_column = MysqlColumn.new("title", nil, "blob") + assert !blob_column.has_default? - assert_raise ArgumentError do - ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", "Hello", "text") + text_column = MysqlColumn.new("title", nil, "text") + assert !text_column.has_default? + end end - text_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", nil, "text") - assert_equal nil, text_column.default + if current_adapter?(:Mysql2Adapter) + def test_should_set_default_for_mysql_binary_data_types + binary_column = Mysql2Column.new("title", "a", "binary(1)") + assert_equal "a", binary_column.default - not_null_text_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", nil, "text", false) - assert_equal "", not_null_text_column.default - end + varbinary_column = Mysql2Column.new("title", "a", "varbinary(1)") + assert_equal "a", varbinary_column.default + end - def test_has_default_should_return_false_for_blog_and_test_data_types - blob_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", nil, "blob") - assert !blob_column.has_default? + def test_should_not_set_default_for_blob_and_text_data_types + assert_raise ArgumentError do + Mysql2Column.new("title", "a", "blob") + end - text_column = ActiveRecord::ConnectionAdapters::Mysql2Column.new("title", nil, "text") - assert !text_column.has_default? - end - end + assert_raise ArgumentError do + Mysql2Column.new("title", "Hello", "text") + end - if current_adapter?(:PostgreSQLAdapter) - def test_bigint_column_should_map_to_integer - bigint_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new('number', nil, "bigint") - assert_equal :integer, bigint_column.type - end + text_column = Mysql2Column.new("title", nil, "text") + assert_equal nil, text_column.default - def test_smallint_column_should_map_to_integer - smallint_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new('number', nil, "smallint") - assert_equal :integer, smallint_column.type - end + not_null_text_column = Mysql2Column.new("title", nil, "text", false) + assert_equal "", not_null_text_column.default + end + + def test_has_default_should_return_false_for_blog_and_test_data_types + blob_column = Mysql2Column.new("title", nil, "blob") + assert !blob_column.has_default? - def test_uuid_column_should_map_to_string - uuid_column = ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new('unique_id', nil, "uuid") - assert_equal :string, uuid_column.type + text_column = Mysql2Column.new("title", nil, "text") + assert !text_column.has_default? + end + end + + if current_adapter?(:PostgreSQLAdapter) + def test_bigint_column_should_map_to_integer + bigint_column = PostgreSQLColumn.new('number', nil, "bigint") + assert_equal :integer, bigint_column.type + end + + def test_smallint_column_should_map_to_integer + smallint_column = PostgreSQLColumn.new('number', nil, "smallint") + assert_equal :integer, smallint_column.type + end + + def test_uuid_column_should_map_to_string + uuid_column = PostgreSQLColumn.new('unique_id', nil, "uuid") + assert_equal :string, uuid_column.type + end + end end end end -- cgit v1.2.3 From a7c2f6be3050b035865a48accb102568c961c1bf Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 31 Jan 2011 14:47:21 -0800 Subject: coders can be assigned to columns --- .../connection_adapters/abstract/schema_definitions.rb | 5 ++++- activerecord/test/cases/column_definition_test.rb | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) (limited to 'activerecord') 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 e1a39feb07..e2582f7f15 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -17,7 +17,9 @@ module ActiveRecord end attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale - attr_accessor :primary + attr_accessor :primary, :coder + + alias :encoded? :coder # Instantiates a new column in the table. # @@ -37,6 +39,7 @@ module ActiveRecord @type = simplified_type(sql_type) @default = extract_default(default) @primary = nil + @coder = nil end # Returns +true+ if the column is either of type string or text. diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index f87f819812..a80deee93e 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -10,6 +10,20 @@ module ActiveRecord end end + def test_can_set_coder + column = Column.new("title", nil, "varchar(20)") + column.coder = YAML + assert_equal YAML, column.coder + end + + def test_encoded? + column = Column.new("title", nil, "varchar(20)") + assert !column.encoded? + + column.coder = YAML + assert column.encoded? + end + # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null -- cgit v1.2.3 From 65f11ff7894ace0e762921a36489de46bd22c724 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 31 Jan 2011 15:10:32 -0800 Subject: column will use coder to typecase value when it is available --- .../connection_adapters/abstract/schema_definitions.rb | 2 ++ activerecord/test/cases/column_definition_test.rb | 6 ++++++ 2 files changed, 8 insertions(+) (limited to 'activerecord') 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 e2582f7f15..d115ef8fb6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -72,6 +72,8 @@ module ActiveRecord # Casts value (which is a String) to an appropriate instance. def type_cast(value) return nil if value.nil? + return coder.load(value) if encoded? + klass = self.class case type diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index a80deee93e..d1dddd4c2c 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -24,6 +24,12 @@ module ActiveRecord assert column.encoded? end + def test_type_case_coded_column + column = Column.new("title", nil, "varchar(20)") + column.coder = YAML + assert_equal "hello", column.type_cast("--- hello") + end + # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null -- cgit v1.2.3 From 0171de00b7a21b9f2866f600fe2aca3152608e33 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 31 Jan 2011 16:32:15 -0800 Subject: moving AR::ConnectionAdapters::Column to its own file --- .../abstract/schema_definitions.rb | 263 -------------------- .../connection_adapters/abstract_adapter.rb | 1 + .../active_record/connection_adapters/column.rb | 268 +++++++++++++++++++++ 3 files changed, 269 insertions(+), 263 deletions(-) create mode 100644 activerecord/lib/active_record/connection_adapters/column.rb (limited to 'activerecord') 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 d115ef8fb6..7ac48c6646 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -6,269 +6,6 @@ require 'bigdecimal/util' module ActiveRecord module ConnectionAdapters #:nodoc: - # An abstract definition of a column in a table. - class Column - TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set - FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set - - module Format - ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ - ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ - end - - attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale - attr_accessor :primary, :coder - - alias :encoded? :coder - - # Instantiates a new column in the table. - # - # +name+ is the column's name, such as supplier_id in supplier_id int(11). - # +default+ is the type-casted default value, such as +new+ in sales_stage varchar(20) default 'new'. - # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in - # company_name varchar(60). - # It will be mapped to one of the standard Rails SQL types in the type attribute. - # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type = nil, null = true) - @name = name - @sql_type = sql_type - @null = null - @limit = extract_limit(sql_type) - @precision = extract_precision(sql_type) - @scale = extract_scale(sql_type) - @type = simplified_type(sql_type) - @default = extract_default(default) - @primary = nil - @coder = nil - end - - # Returns +true+ if the column is either of type string or text. - def text? - type == :string || type == :text - end - - # Returns +true+ if the column is either of type integer, float or decimal. - def number? - type == :integer || type == :float || type == :decimal - end - - def has_default? - !default.nil? - end - - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime, :timestamp, :time then Time - when :date then Date - when :text, :string, :binary then String - when :boolean then Object - end - end - - # Casts value (which is a String) to an appropriate instance. - def type_cast(value) - return nil if value.nil? - return coder.load(value) if encoded? - - klass = self.class - - case type - when :string, :text then value - when :integer then value.to_i rescue value ? 1 : 0 - when :float then value.to_f - when :decimal then klass.value_to_decimal(value) - when :datetime, :timestamp then klass.string_to_time(value) - when :time then klass.string_to_dummy_time(value) - when :date then klass.string_to_date(value) - when :binary then klass.binary_to_string(value) - when :boolean then klass.value_to_boolean(value) - else value - end - end - - def type_cast_code(var_name) - klass = self.class.name - - case type - when :string, :text then var_name - when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" - when :float then "#{var_name}.to_f" - when :decimal then "#{klass}.value_to_decimal(#{var_name})" - when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" - when :time then "#{klass}.string_to_dummy_time(#{var_name})" - when :date then "#{klass}.string_to_date(#{var_name})" - when :binary then "#{klass}.binary_to_string(#{var_name})" - when :boolean then "#{klass}.value_to_boolean(#{var_name})" - else var_name - end - end - - # Returns the human name of the column name. - # - # ===== Examples - # Column.new('sales_stage', ...).human_name # => 'Sales stage' - def human_name - Base.human_attribute_name(@name) - end - - def extract_default(default) - type_cast(default) - end - - # Used to convert from Strings to BLOBs - def string_to_binary(value) - self.class.string_to_binary(value) - end - - class << self - # Used to convert from Strings to BLOBs - def string_to_binary(value) - value - end - - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value - end - - def string_to_date(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_date(string) || fallback_string_to_date(string) - end - - def string_to_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - def string_to_dummy_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - string_to_time "2000-01-01 #{string}" - end - - # convert something to a boolean - def value_to_boolean(value) - if value.is_a?(String) && value.blank? - nil - else - TRUE_VALUES.include?(value) - end - end - - # convert something to a BigDecimal - def value_to_decimal(value) - # Using .class is faster than .is_a? and - # subclasses of BigDecimal will be handled - # in the else clause - if value.class == BigDecimal - value - elsif value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - - protected - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i - end - - def new_date(year, mon, mday) - if year && year != 0 - Date.new(year, mon, mday) rescue nil - end - end - - def new_time(year, mon, mday, hour, min, sec, microsec) - # Treat 0000-00-00 00:00:00 as nil. - return nil if year.nil? || year == 0 - - Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - - def fast_string_to_date(string) - if string =~ Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Format::ISO_DATETIME - microsec = ($7.to_f * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def fallback_string_to_time(string) - time_hash = Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - - private - def extract_limit(sql_type) - $1.to_i if sql_type =~ /\((.*)\)/ - end - - def extract_precision(sql_type) - $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i - end - - def extract_scale(sql_type) - case sql_type - when /^(numeric|decimal|number)\((\d+)\)/i then 0 - when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i - end - end - - def simplified_type(field_type) - case field_type - when /int/i - :integer - when /float|double/i - :float - when /decimal|numeric|number/i - extract_scale(field_type) == 0 ? :integer : :decimal - when /datetime/i - :datetime - when /timestamp/i - :timestamp - when /time/i - :time - when /date/i - :date - when /clob/i, /text/i - :text - when /blob/i, /binary/i - :binary - when /char/i, /string/i - :string - when /boolean/i - :boolean - end - end - end - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 79631a6570..3a3a73fc42 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,6 +4,7 @@ require 'bigdecimal/util' require 'active_support/core_ext/benchmark' # TODO: Autoload these files +require 'active_record/connection_adapters/column' require 'active_record/connection_adapters/abstract/schema_definitions' require 'active_record/connection_adapters/abstract/schema_statements' require 'active_record/connection_adapters/abstract/database_statements' diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb new file mode 100644 index 0000000000..beb06ea622 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -0,0 +1,268 @@ +module ActiveRecord + # :stopdoc: + module ConnectionAdapters + # An abstract definition of a column in a table. + class Column + TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set + FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set + + module Format + ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ + ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ + end + + attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale + attr_accessor :primary, :coder + + alias :encoded? :coder + + # Instantiates a new column in the table. + # + # +name+ is the column's name, such as supplier_id in supplier_id int(11). + # +default+ is the type-casted default value, such as +new+ in sales_stage varchar(20) default 'new'. + # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in + # company_name varchar(60). + # It will be mapped to one of the standard Rails SQL types in the type attribute. + # +null+ determines if this column allows +NULL+ values. + def initialize(name, default, sql_type = nil, null = true) + @name = name + @sql_type = sql_type + @null = null + @limit = extract_limit(sql_type) + @precision = extract_precision(sql_type) + @scale = extract_scale(sql_type) + @type = simplified_type(sql_type) + @default = extract_default(default) + @primary = nil + @coder = nil + end + + # Returns +true+ if the column is either of type string or text. + def text? + type == :string || type == :text + end + + # Returns +true+ if the column is either of type integer, float or decimal. + def number? + type == :integer || type == :float || type == :decimal + end + + def has_default? + !default.nil? + end + + # Returns the Ruby class that corresponds to the abstract data type. + def klass + case type + when :integer then Fixnum + when :float then Float + when :decimal then BigDecimal + when :datetime, :timestamp, :time then Time + when :date then Date + when :text, :string, :binary then String + when :boolean then Object + end + end + + # Casts value (which is a String) to an appropriate instance. + def type_cast(value) + return nil if value.nil? + return coder.load(value) if encoded? + + klass = self.class + + case type + when :string, :text then value + when :integer then value.to_i rescue value ? 1 : 0 + when :float then value.to_f + when :decimal then klass.value_to_decimal(value) + when :datetime, :timestamp then klass.string_to_time(value) + when :time then klass.string_to_dummy_time(value) + when :date then klass.string_to_date(value) + when :binary then klass.binary_to_string(value) + when :boolean then klass.value_to_boolean(value) + else value + end + end + + def type_cast_code(var_name) + klass = self.class.name + + case type + when :string, :text then var_name + when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" + when :float then "#{var_name}.to_f" + when :decimal then "#{klass}.value_to_decimal(#{var_name})" + when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})" + when :time then "#{klass}.string_to_dummy_time(#{var_name})" + when :date then "#{klass}.string_to_date(#{var_name})" + when :binary then "#{klass}.binary_to_string(#{var_name})" + when :boolean then "#{klass}.value_to_boolean(#{var_name})" + else var_name + end + end + + # Returns the human name of the column name. + # + # ===== Examples + # Column.new('sales_stage', ...).human_name # => 'Sales stage' + def human_name + Base.human_attribute_name(@name) + end + + def extract_default(default) + type_cast(default) + end + + # Used to convert from Strings to BLOBs + def string_to_binary(value) + self.class.string_to_binary(value) + end + + class << self + # Used to convert from Strings to BLOBs + def string_to_binary(value) + value + end + + # Used to convert from BLOBs to Strings + def binary_to_string(value) + value + end + + def string_to_date(string) + return string unless string.is_a?(String) + return nil if string.empty? + + fast_string_to_date(string) || fallback_string_to_date(string) + end + + def string_to_time(string) + return string unless string.is_a?(String) + return nil if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + def string_to_dummy_time(string) + return string unless string.is_a?(String) + return nil if string.empty? + + string_to_time "2000-01-01 #{string}" + end + + # convert something to a boolean + def value_to_boolean(value) + if value.is_a?(String) && value.blank? + nil + else + TRUE_VALUES.include?(value) + end + end + + # convert something to a BigDecimal + def value_to_decimal(value) + # Using .class is faster than .is_a? and + # subclasses of BigDecimal will be handled + # in the else clause + if value.class == BigDecimal + value + elsif value.respond_to?(:to_d) + value.to_d + else + value.to_s.to_d + end + end + + protected + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i + end + + def new_date(year, mon, mday) + if year && year != 0 + Date.new(year, mon, mday) rescue nil + end + end + + def new_time(year, mon, mday, hour, min, sec, microsec) + # Treat 0000-00-00 00:00:00 as nil. + return nil if year.nil? || year == 0 + + Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + + def fast_string_to_date(string) + if string =~ Format::ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ Format::ISO_DATETIME + microsec = ($7.to_f * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def fallback_string_to_time(string) + time_hash = Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end + + private + def extract_limit(sql_type) + $1.to_i if sql_type =~ /\((.*)\)/ + end + + def extract_precision(sql_type) + $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i + end + + def extract_scale(sql_type) + case sql_type + when /^(numeric|decimal|number)\((\d+)\)/i then 0 + when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i + end + end + + def simplified_type(field_type) + case field_type + when /int/i + :integer + when /float|double/i + :float + when /decimal|numeric|number/i + extract_scale(field_type) == 0 ? :integer : :decimal + when /datetime/i + :datetime + when /timestamp/i + :timestamp + when /time/i + :time + when /date/i + :date + when /clob/i, /text/i + :text + when /blob/i, /binary/i + :binary + when /char/i, /string/i + :string + when /boolean/i + :boolean + end + end + end + end + # :startdoc: +end -- cgit v1.2.3 From 3cc2b77dc1cb4c1e5cfac68c7828e35a27415e0d Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 1 Feb 2011 09:34:21 -0800 Subject: adding a YAML Column coder for YAML serialization to db columns --- activerecord/lib/active_record.rb | 4 ++ .../lib/active_record/coders/yaml_column.rb | 34 ++++++++++++++++ activerecord/test/cases/coders/yaml_column_test.rb | 45 ++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 activerecord/lib/active_record/coders/yaml_column.rb create mode 100644 activerecord/test/cases/coders/yaml_column_test.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index c80bce2849..5afb97803e 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -81,6 +81,10 @@ module ActiveRecord autoload :Validations end + module Coders + autoload :YAMLColumn, 'active_record/coders/yaml_column' + end + module AttributeMethods extend ActiveSupport::Autoload diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb new file mode 100644 index 0000000000..9b0df119ef --- /dev/null +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -0,0 +1,34 @@ +module ActiveRecord + # :stopdoc: + module Coders + class YAMLColumn + RESCUE_ERRORS = [ ArgumentError ] + + if defined?(Psych) && defined?(Psych::SyntaxError) + RESCUE_ERRORS << Psych::SyntaxError + end + + attr_accessor :object_class + + def initialize(object_class = Object) + @object_class = object_class + end + + def load(yaml) + return yaml unless yaml.is_a?(String) && yaml =~ /^---/ + begin + obj = YAML::load(yaml) + + unless obj.is_a?(object_class) || obj.nil? + raise SerializationTypeMismatch, + "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" + end + + rescue *RESCUE_ERRORS + yaml + end + end + end + end + # :startdoc +end diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb new file mode 100644 index 0000000000..f85f11b57f --- /dev/null +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -0,0 +1,45 @@ +require "cases/helper" + +module ActiveRecord + module Coders + class YAMLColumnTest < ActiveRecord::TestCase + def test_initialize_takes_class + coder = YAMLColumn.new(Object) + assert_equal Object, coder.object_class + end + + def test_type_mismatch_on_different_classes + coder = YAMLColumn.new(Array) + assert_raises(SerializationTypeMismatch) do + coder.load "--- foo" + end + end + + def test_nil_is_ok + coder = YAMLColumn.new + assert_nil coder.load "--- " + end + + def test_nil_is_ok_with_different_class + coder = YAMLColumn.new SerializationTypeMismatch + assert_nil coder.load "--- " + end + + def test_returns_string_unless_starts_with_dash + coder = YAMLColumn.new + assert_equal 'foo', coder.load("foo") + end + + def test_load_handles_other_classes + coder = YAMLColumn.new + assert_equal [], coder.load([]) + end + + def test_load_swallows_yaml_exceptions + coder = YAMLColumn.new + bad_yaml = '--- {' + assert_equal bad_yaml, coder.load(bad_yaml) + end + end + end +end -- cgit v1.2.3 From 69600a9f97d2f678972500d8a741edc745833718 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 1 Feb 2011 10:37:53 -0800 Subject: avoid column lookup on subclasses, keep column info cached as table_name => column_list --- activerecord/lib/active_record/base.rb | 10 ++++++++-- activerecord/lib/active_record/session_store.rb | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 220d861a27..1e762a287d 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -671,7 +671,11 @@ module ActiveRecord #:nodoc: # Returns a hash of column objects for the table associated with this class. def columns_hash - @columns_hash ||= Hash[columns.map { |column| [column.name, column] }] + @@columns_cache[table_name] ||= Hash[columns.map { |column| [column.name, column] }] + end + + def columns_hash=(value) + @@columns_cache[table_name] = value end # Returns an array of column names as strings. @@ -728,7 +732,8 @@ module ActiveRecord #:nodoc: def reset_column_information connection.clear_cache! undefine_attribute_methods - @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil + self.columns_hash = nil + @column_names = @columns = @content_columns = @dynamic_methods_hash = @inheritance_column = nil @arel_engine = @relation = @arel_table = nil end @@ -1376,6 +1381,7 @@ MSG quoted_value end end + @@columns_cache = {} public # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index 3400fd6ade..68d9f89edd 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -59,10 +59,12 @@ module ActiveRecord end def drop_table! + self.columns_hash = nil connection.drop_table table_name end def create_table! + self.columns_hash = nil connection.create_table(table_name) do |t| t.string session_id_column, :limit => 255 t.text data_column_name -- cgit v1.2.3 From a6cf6ec98b58dc2a7d0586ccb6ef970d93f0bafc Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 1 Feb 2011 11:07:02 -0800 Subject: move the coders to the serialized_attributes hash --- activerecord/lib/active_record/attribute_methods/read.rb | 10 +++------- activerecord/lib/active_record/base.rb | 12 ++++-------- activerecord/lib/active_record/coders/yaml_column.rb | 7 ++++++- activerecord/lib/active_record/validations/uniqueness.rb | 6 ++++-- 4 files changed, 17 insertions(+), 18 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index ff088200a1..895c16b9b3 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -106,14 +106,10 @@ module ActiveRecord # Returns the unserialized object of the attribute. def unserialize_attribute(attr_name) - unserialized_object = object_from_yaml(@attributes[attr_name]) + coder = self.class.serialized_attributes[attr_name] + unserialized_object = coder.load(@attributes[attr_name]) - if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil? - @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object - else - raise SerializationTypeMismatch, - "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}" - end + @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object end private diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1e762a287d..42226f83ea 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -536,7 +536,7 @@ module ActiveRecord #:nodoc: # serialize :preferences # end def serialize(attr_name, class_name = Object) - serialized_attributes[attr_name.to_s] = class_name + serialized_attributes[attr_name.to_s] = Coders::YAMLColumn.new(class_name) end # Guesses the table name (in forced lower-case) based on the name of the class in the @@ -1738,8 +1738,9 @@ MSG if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) value = read_attribute(name) - if !value.nil? && self.class.serialized_attributes.key?(name) - value = YAML.dump value + coder = self.class.serialized_attributes[name] + if !value.nil? && coder + value = coder.dump value end attrs[self.class.arel_table[name]] = value end @@ -1865,11 +1866,6 @@ MSG end end - def object_from_yaml(string) - return string unless string.is_a?(String) && string =~ /^---/ - YAML::load(string) rescue string - end - def populate_with_current_scope_attributes if scope = self.class.send(:current_scoped_methods) create_with = scope.scope_for_create diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 9b0df119ef..fcecc11aba 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -14,16 +14,21 @@ module ActiveRecord @object_class = object_class end + def dump(obj) + YAML.dump obj + end + def load(yaml) return yaml unless yaml.is_a?(String) && yaml =~ /^---/ begin - obj = YAML::load(yaml) + obj = YAML.load(yaml) unless obj.is_a?(object_class) || obj.nil? raise SerializationTypeMismatch, "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" end + obj rescue *RESCUE_ERRORS yaml end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index e6a2b40403..a96796f9ff 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -15,8 +15,10 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) - if value && record.class.serialized_attributes.key?(attribute.to_s) - value = YAML.dump value + coder = record.class.serialized_attributes[attribute.to_s] + + if value && coder + value = coder.dump value end sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value) -- cgit v1.2.3 From ee34b4cf346975d0aef7f26ef47ee2e4f3e13c37 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 1 Feb 2011 11:30:09 -0800 Subject: share column cache among subclasses, only look up columns per AR::Base subclass once --- activerecord/lib/active_record/base.rb | 28 ++++++++++++---------- activerecord/lib/active_record/session_store.rb | 4 ++-- .../test/cases/session_store/session_test.rb | 1 + activerecord/test/models/contact.rb | 4 ++++ 4 files changed, 22 insertions(+), 15 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 42226f83ea..f66b84935c 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -662,20 +662,16 @@ module ActiveRecord #:nodoc: # Returns an array of column objects for the table associated with this class. def columns - unless defined?(@columns) && @columns - @columns = connection.columns(table_name, "#{name} Columns") - @columns.each { |column| column.primary = column.name == primary_key } - end - @columns + @@columns[table_name] ||= connection.columns( + table_name, "#{name} Columns" + ).tap { |columns| + columns.each { |column| column.primary = column.name == primary_key } + } end # Returns a hash of column objects for the table associated with this class. def columns_hash - @@columns_cache[table_name] ||= Hash[columns.map { |column| [column.name, column] }] - end - - def columns_hash=(value) - @@columns_cache[table_name] = value + @@columns_hash[table_name] ||= Hash[columns.map { |column| [column.name, column] }] end # Returns an array of column names as strings. @@ -732,11 +728,16 @@ module ActiveRecord #:nodoc: def reset_column_information connection.clear_cache! undefine_attribute_methods - self.columns_hash = nil - @column_names = @columns = @content_columns = @dynamic_methods_hash = @inheritance_column = nil + reset_column_cache + @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil @arel_engine = @relation = @arel_table = nil end + def reset_column_cache # :nodoc: + @@columns.delete table_name + @@columns_hash.delete table_name + end + def attribute_method?(attribute) super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, ''))) end @@ -1381,7 +1382,8 @@ MSG quoted_value end end - @@columns_cache = {} + @@columns_hash = {} + @@columns = {} public # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index 68d9f89edd..e3342f046f 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -59,12 +59,12 @@ module ActiveRecord end def drop_table! - self.columns_hash = nil + reset_column_cache connection.drop_table table_name end def create_table! - self.columns_hash = nil + reset_column_cache connection.create_table(table_name) do |t| t.string session_id_column, :limit => 255 t.text data_column_name diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb index f906bda8c3..cee5ddd003 100644 --- a/activerecord/test/cases/session_store/session_test.rb +++ b/activerecord/test/cases/session_store/session_test.rb @@ -60,6 +60,7 @@ module ActiveRecord end def test_loaded? + Session.create_table! s = Session.new assert !s.loaded?, 'session is not loaded' end diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index 975a885331..5bbe7ebb12 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -1,4 +1,8 @@ class Contact < ActiveRecord::Base + def self.columns + @columns + end + # mock out self.columns so no pesky db is needed for these tests def self.column(name, sql_type = nil, options = {}) @columns ||= [] -- cgit v1.2.3 From ebe485fd8ec80a1a9b86516bc6f74bc5bbba3476 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 1 Feb 2011 11:41:14 -0800 Subject: serialize can take an arbitrary code object --- activerecord/lib/active_record/base.rb | 8 +++++++- activerecord/test/cases/base_test.rb | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f66b84935c..5310b55a92 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -536,7 +536,13 @@ module ActiveRecord #:nodoc: # serialize :preferences # end def serialize(attr_name, class_name = Object) - serialized_attributes[attr_name.to_s] = Coders::YAMLColumn.new(class_name) + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + serialized_attributes[attr_name.to_s] = coder end # Guesses the table name (in forced lower-case) based on the name of the class in the diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index a58d5dec81..7c677d55ca 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1018,6 +1018,27 @@ class BasicsTest < ActiveRecord::TestCase assert_equal topic.content, false end + def test_serialize_with_coder + coder = Class.new { + # Identity + def load(thing) + thing + end + + # base 64 + def dump(thing) + [thing].pack('m') + end + }.new + + Topic.serialize(:content, coder) + s = 'hello world' + topic = Topic.new(:content => s) + assert topic.save + topic = topic.reload + assert_equal [s].pack('m'), topic.content + end + def test_quote author_name = "\\ \001 ' \n \\n \"" topic = Topic.create('author_name' => author_name) -- cgit v1.2.3 From a0fac7192241f3242af410ca16e6dd43b933c98e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 1 Feb 2011 14:25:33 -0800 Subject: store the serialized column values in the @attributes hash --- .../lib/active_record/attribute_methods/write.rb | 3 ++- activerecord/lib/active_record/base.rb | 21 ++++++++++++------ .../lib/active_record/validations/uniqueness.rb | 6 ------ activerecord/test/cases/base_test.rb | 25 ++++++++++++++++++++++ 4 files changed, 41 insertions(+), 14 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 6a593a7e0e..5eba94ec66 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -23,7 +23,8 @@ module ActiveRecord if (column = column_for_attribute(attr_name)) && column.number? @attributes[attr_name] = convert_number_column_value(value) else - @attributes[attr_name] = value + coder = self.class.serialized_attributes[attr_name] + @attributes[attr_name] = coder ? coder.dump(value) : value end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 5310b55a92..6b82b827b0 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1739,18 +1739,25 @@ MSG # Returns a copy of the attributes hash where all the values have been safely quoted for use in # an Arel insert/update method. def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) - attrs = {} + attrs = {} + klass = self.class + arel_table = klass.arel_table + attribute_names.each do |name| if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) - value = read_attribute(name) - coder = self.class.serialized_attributes[name] - if !value.nil? && coder - value = coder.dump value - end - attrs[self.class.arel_table[name]] = value + value = if klass.serialized_attributes[name] + @attributes[name] + else + # FIXME: we need @attributes to be used consistently. + # If the values stored in @attributes were already type + # casted, this code could be simplified + read_attribute(name) + end + + attrs[arel_table[name]] = value end end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index a96796f9ff..76110b73e4 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -15,12 +15,6 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) - coder = record.class.serialized_attributes[attribute.to_s] - - if value && coder - value = coder.dump value - end - sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value) relation = finder_class.unscoped.where(sql, *params) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7c677d55ca..5cbc52732b 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1037,6 +1037,31 @@ class BasicsTest < ActiveRecord::TestCase assert topic.save topic = topic.reload assert_equal [s].pack('m'), topic.content + ensure + Topic.serialize(:content) + end + + def test_serialize_with_bcrypt_coder + crypt_coder = Class.new { + def load(thing) + return unless thing + BCrypt::Password.new thing + end + + def dump(thing) + BCrypt::Password.create(thing).to_s + end + }.new + + Topic.serialize(:content, crypt_coder) + password = 'password' + topic = Topic.new(:content => password) + assert topic.save + topic = topic.reload + assert_kind_of BCrypt::Password, topic.content + assert_equal(true, topic.content == password, 'password should equal') + ensure + Topic.serialize(:content) end def test_quote -- cgit v1.2.3 From 5b42e9660201fc721075d2bfbe13edb0014dbde2 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 1 Feb 2011 15:23:55 -0800 Subject: make sure de-serialization happens on object instantiation --- activerecord/lib/active_record/attribute_methods/read.rb | 2 +- activerecord/lib/active_record/attribute_methods/write.rb | 3 +-- activerecord/lib/active_record/base.rb | 10 ++++++++-- activerecord/lib/active_record/validations/uniqueness.rb | 6 ++++++ activerecord/test/cases/serialization_test.rb | 6 ++++++ 5 files changed, 22 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 895c16b9b3..ab86d8bad1 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -54,7 +54,7 @@ module ActiveRecord # Define read method for serialized attribute. def define_read_method_for_serialized_attribute(attr_name) - access_code = "@attributes_cache['#{attr_name}'] ||= unserialize_attribute('#{attr_name}')" + access_code = "@attributes_cache['#{attr_name}'] ||= @attributes['#{attr_name}']" generated_attribute_methods.module_eval("def _#{attr_name}; #{access_code}; end; alias #{attr_name} _#{attr_name}", __FILE__, __LINE__) end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 5eba94ec66..6a593a7e0e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -23,8 +23,7 @@ module ActiveRecord if (column = column_for_attribute(attr_name)) && column.number? @attributes[attr_name] = convert_number_column_value(value) else - coder = self.class.serialized_attributes[attr_name] - @attributes[attr_name] = coder ? coder.dump(value) : value + @attributes[attr_name] = value end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 6b82b827b0..04b479bdb2 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1446,6 +1446,12 @@ MSG # post.title # => 'hello world' def init_with(coder) @attributes = coder['attributes'] + + (@attributes.keys & self.class.serialized_attributes.keys).each do |key| + coder = self.class.serialized_attributes[key] + @attributes[key] = coder.load @attributes[key] + end + @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} @association_cache = {} @aggregation_cache = {} @@ -1748,8 +1754,8 @@ MSG if include_readonly_attributes || (!include_readonly_attributes && !self.class.readonly_attributes.include?(name)) - value = if klass.serialized_attributes[name] - @attributes[name] + value = if coder = klass.serialized_attributes[name] + coder.dump @attributes[name] else # FIXME: we need @attributes to be used consistently. # If the values stored in @attributes were already type diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 76110b73e4..a96796f9ff 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -15,6 +15,12 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) + coder = record.class.serialized_attributes[attribute.to_s] + + if value && coder + value = coder.dump value + end + sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value) relation = finder_class.unscoped.where(sql, *params) diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 25dbcc9fc2..677d659f39 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -23,6 +23,12 @@ class SerializationTest < ActiveRecord::TestCase @contact = Contact.new(@contact_attributes) end + def test_serialized_init_with + topic = Topic.allocate + topic.init_with('attributes' => { 'content' => '--- foo' }) + assert_equal 'foo', topic.content + end + def test_to_xml xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema -- cgit v1.2.3 From 4e390720178889c773adb12ea1f073bc035fbe67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81omnicki?= Date: Tue, 1 Feb 2011 23:27:26 +0100 Subject: Configurable generation of add_index for references columns Signed-off-by: Santiago Pastorino --- .../lib/rails/generators/active_record/model/model_generator.rb | 1 + .../lib/rails/generators/active_record/model/templates/migration.rb | 2 ++ 2 files changed, 3 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index c75abd043c..f7caa43ac8 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -10,6 +10,7 @@ module ActiveRecord class_option :migration, :type => :boolean class_option :timestamps, :type => :boolean class_option :parent, :type => :string, :desc => "The parent class for the generated model" + class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns" def create_migration_file return unless options[:migration] && options[:parent].nil? diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb index cd2552d9b8..e573e908a7 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb @@ -9,8 +9,10 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% end -%> end +<% if options[:indexes] %> <% attributes.select {|attr| attr.reference? }.each do |attribute| -%> add_index :<%= table_name %>, :<%= attribute.name %>_id <% end -%> +<% end %> end end -- cgit v1.2.3 From 6d40d527e896038767272eca3a6357e6998b985e Mon Sep 17 00:00:00 2001 From: Franck Verrot Date: Tue, 25 Jan 2011 23:20:57 +0100 Subject: Test private method timestamp_attributes_for_create Signed-off-by: Santiago Pastorino --- activerecord/test/cases/timestamp_test.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 70c098bc6d..1970bf0a5d 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -140,4 +140,9 @@ class TimestampTest < ActiveRecord::TestCase ensure Toy.belongs_to :pet end + + def test_timestamp_attributes_for_create + toy = Toy.first + assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on] + end end -- cgit v1.2.3 From 253f5a15f42720ddc2cf6cd7e982fe3e0dc65f52 Mon Sep 17 00:00:00 2001 From: Franck Verrot Date: Tue, 25 Jan 2011 23:21:33 +0100 Subject: Test private method timestamp_attributes_for_update Signed-off-by: Santiago Pastorino --- activerecord/test/cases/timestamp_test.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 1970bf0a5d..26c04c47b9 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -145,4 +145,9 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on] end + + def test_timestamp_attributes_for_update + toy = Toy.first + assert_equal toy.send(:timestamp_attributes_for_update), [:updated_at, :updated_on] + end end -- cgit v1.2.3 From 598b32c5812e428f639c9a80b352aec54f059a36 Mon Sep 17 00:00:00 2001 From: Franck Verrot Date: Tue, 25 Jan 2011 23:23:19 +0100 Subject: Test private method all_timestamp_attributes Signed-off-by: Santiago Pastorino --- activerecord/test/cases/timestamp_test.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 26c04c47b9..006266536c 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -150,4 +150,9 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal toy.send(:timestamp_attributes_for_update), [:updated_at, :updated_on] end + + def test_all_timestamp_attributes + toy = Toy.first + assert_equal toy.send(:all_timestamp_attributes), [:created_at, :created_on, :updated_at, :updated_on] + end end -- cgit v1.2.3 From 5178e641750d4e3f8419c8e6cf3ab7e7cb48a880 Mon Sep 17 00:00:00 2001 From: Franck Verrot Date: Tue, 25 Jan 2011 23:27:27 +0100 Subject: Added timestamp_attributes_for_create_in_model Signed-off-by: Santiago Pastorino --- activerecord/lib/active_record/timestamp.rb | 4 ++++ activerecord/test/cases/timestamp_test.rb | 5 +++++ 2 files changed, 9 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 5617adea1f..38cc2c9694 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -66,6 +66,10 @@ module ActiveRecord self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) end + def timestamp_attributes_for_create_in_model + timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) } + end + def timestamp_attributes_for_update_in_model timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) } end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 006266536c..428dfedef2 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -155,4 +155,9 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal toy.send(:all_timestamp_attributes), [:created_at, :created_on, :updated_at, :updated_on] end + + def test_timestamp_attributes_for_create_in_model + toy = Toy.first + assert_equal toy.send(:timestamp_attributes_for_create_in_model), [:created_at] + end end -- cgit v1.2.3 From 47315760bc893a48738e6592a0cbb3fef506bcd7 Mon Sep 17 00:00:00 2001 From: Franck Verrot Date: Tue, 25 Jan 2011 23:28:31 +0100 Subject: Test timestamp_attributes_for_update_in_model that was already in place Signed-off-by: Santiago Pastorino --- activerecord/test/cases/timestamp_test.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 428dfedef2..62e3a084d0 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -160,4 +160,9 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal toy.send(:timestamp_attributes_for_create_in_model), [:created_at] end + + def test_timestamp_attributes_for_update_in_model + toy = Toy.first + assert_equal toy.send(:timestamp_attributes_for_update_in_model), [:updated_at] + end end -- cgit v1.2.3 From a5b03e9c7af8f539764a66f9bd51b7ebbcb9f57d Mon Sep 17 00:00:00 2001 From: Franck Verrot Date: Tue, 25 Jan 2011 23:33:55 +0100 Subject: Implement and test private method all_timestamp_attributes_in_model Signed-off-by: Santiago Pastorino --- activerecord/lib/active_record/timestamp.rb | 4 ++++ activerecord/test/cases/timestamp_test.rb | 5 +++++ 2 files changed, 9 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 38cc2c9694..65d9d1fb19 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -74,6 +74,10 @@ module ActiveRecord timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) } end + def all_timestamp_attributes_in_model + timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model + end + def timestamp_attributes_for_update #:nodoc: [:updated_at, :updated_on] end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 62e3a084d0..1c21f0f3b6 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -165,4 +165,9 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal toy.send(:timestamp_attributes_for_update_in_model), [:updated_at] end + + def test_all_timestamp_attributes_in_model + toy = Toy.first + assert_equal toy.send(:all_timestamp_attributes_in_model), [:created_at, :updated_at] + end end -- cgit v1.2.3 From 8dcacd0cc7fe634ed0bedbc21dfbb0da46298686 Mon Sep 17 00:00:00 2001 From: Franck Verrot Date: Tue, 25 Jan 2011 23:36:08 +0100 Subject: Refactor clear_timestamp_attributes to use the newly created all_timestamp_attributes_in_model Signed-off-by: Santiago Pastorino --- activerecord/lib/active_record/base.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 04b479bdb2..c592490c84 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1898,11 +1898,9 @@ MSG # Clear attributes and changed_attributes def clear_timestamp_attributes - %w(created_at created_on updated_at updated_on).each do |attribute_name| - if has_attribute?(attribute_name) - self[attribute_name] = nil - changed_attributes.delete(attribute_name) - end + all_timestamp_attributes_in_model.each do |attribute_name| + self[attribute_name] = nil + changed_attributes.delete(attribute_name) end end end -- cgit v1.2.3 From 9e23835cb8f6b9284d635e52f7e374563ea51042 Mon Sep 17 00:00:00 2001 From: Raimonds Simanovskis Date: Wed, 2 Feb 2011 17:45:12 +0100 Subject: fix for test_read_attributes_before_type_cast_on_datetime - Oracle adapter also returns Time value --- activerecord/test/cases/attribute_methods_test.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 06adb33ffc..7e3e204626 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -118,8 +118,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_read_attributes_before_type_cast_on_datetime developer = Developer.find(:first) - if current_adapter?(:Mysql2Adapter) - # Mysql2 keeps the value in Time instance + if current_adapter?(:Mysql2Adapter, :OracleAdapter) + # Mysql2 and Oracle adapters keep the value in Time instance assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s(:db) else assert_equal developer.created_at.to_s(:db), developer.attributes_before_type_cast["created_at"].to_s -- cgit v1.2.3 From 70b9db173e958944fcf61ac8514408fba9ef0c9a Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 2 Feb 2011 10:17:39 -0800 Subject: adding mysql2 adapter --- .../connection_adapters/mysql2_adapter.rb | 657 +++++++++++++++++++++ 1 file changed, 657 insertions(+) create mode 100644 activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb new file mode 100644 index 0000000000..5bd593ef18 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -0,0 +1,657 @@ +# encoding: utf-8 + +require 'mysql2' unless defined? Mysql2 + +module ActiveRecord + class Base + def self.mysql2_connection(config) + config[:username] = 'root' if config[:username].nil? + + if Mysql2::Client.const_defined? :FOUND_ROWS + config[:flags] = Mysql2::Client::FOUND_ROWS + end + + client = Mysql2::Client.new(config.symbolize_keys) + options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] + ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + end + end + + module ConnectionAdapters + class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: + end + + class Mysql2Column < Column + BOOL = "tinyint(1)" + def extract_default(default) + if sql_type =~ /blob/i || type == :text + if default.blank? + return null ? nil : '' + else + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + elsif missing_default_forged_as_empty_string?(default) + nil + else + super + end + end + + def has_default? + return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns + super + end + + # Returns the Ruby class that corresponds to the abstract data type. + def klass + case type + when :integer then Fixnum + when :float then Float + when :decimal then BigDecimal + when :datetime then Time + when :date then Date + when :timestamp then Time + when :time then Time + when :text, :string then String + when :binary then String + when :boolean then Object + end + end + + def type_cast(value) + return nil if value.nil? + case type + when :string then value + when :text then value + when :integer then value.to_i rescue value ? 1 : 0 + when :float then value.to_f # returns self if it's already a Float + when :decimal then self.class.value_to_decimal(value) + when :datetime, :timestamp then value.class == Time ? value : self.class.string_to_time(value) + when :time then value.class == Time ? value : self.class.string_to_dummy_time(value) + when :date then value.class == Date ? value : self.class.string_to_date(value) + when :binary then value + when :boolean then self.class.value_to_boolean(value) + else value + end + end + + def type_cast_code(var_name) + case type + when :string then var_name + when :text then var_name + when :integer then "#{var_name}.to_i rescue #{var_name} ? 1 : 0" + when :float then "#{var_name}.to_f" + when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" + when :datetime, :timestamp then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_time(#{var_name})" + when :time then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_dummy_time(#{var_name})" + when :date then "#{var_name}.class == Date ? #{var_name} : #{self.class.name}.string_to_date(#{var_name})" + when :binary then nil + when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" + else var_name + end + end + + private + def simplified_type(field_type) + return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) + return :string if field_type =~ /enum/i or field_type =~ /set/i + return :integer if field_type =~ /year/i + return :binary if field_type =~ /bit/i + super + end + + def extract_limit(sql_type) + case sql_type + when /blob|text/i + case sql_type + when /tiny/i + 255 + when /medium/i + 16777215 + when /long/i + 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases + else + super # we could return 65535 here, but we leave it undecorated by default + end + when /^bigint/i; 8 + when /^int/i; 4 + when /^mediumint/i; 3 + when /^smallint/i; 2 + when /^tinyint/i; 1 + else + super + end + end + + # MySQL misreports NOT NULL column default when none is given. + # We can't detect this for columns which may have a legitimate '' + # default (string) but we can for others (integer, datetime, boolean, + # and the rest). + # + # Test whether the column has default '', is not null, and is not + # a type allowing default ''. + def missing_default_forged_as_empty_string?(default) + type != :string && !null && default == '' + end + end + + class Mysql2Adapter < AbstractAdapter + cattr_accessor :emulate_booleans + self.emulate_booleans = true + + ADAPTER_NAME = 'Mysql2' + PRIMARY = "PRIMARY" + + LOST_CONNECTION_ERROR_MESSAGES = [ + "Server shutdown in progress", + "Broken pipe", + "Lost connection to MySQL server during query", + "MySQL server has gone away" ] + + QUOTED_TRUE, QUOTED_FALSE = '1', '0' + + NATIVE_DATABASE_TYPES = { + :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "int", :limit => 4 }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "tinyint", :limit => 1 } + } + + def initialize(connection, logger, connection_options, config) + super(connection, logger) + @connection_options, @config = connection_options, config + @quoted_column_names, @quoted_table_names = {}, {} + configure_connection + end + + def adapter_name + ADAPTER_NAME + end + + def supports_migrations? + true + end + + def supports_primary_key? + true + end + + def supports_savepoints? + true + end + + def native_database_types + NATIVE_DATABASE_TYPES + end + + # QUOTING ================================================== + + def quote(value, column = nil) + if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) + s = column.class.string_to_binary(value).unpack("H*")[0] + "x'#{s}'" + elsif value.kind_of?(BigDecimal) + value.to_s("F") + else + super + end + end + + def quote_column_name(name) #:nodoc: + @quoted_column_names[name] ||= "`#{name}`" + end + + def quote_table_name(name) #:nodoc: + @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') + end + + def quote_string(string) + @connection.escape(string) + end + + def quoted_true + QUOTED_TRUE + end + + def quoted_false + QUOTED_FALSE + end + + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity(&block) #:nodoc: + old = select_value("SELECT @@FOREIGN_KEY_CHECKS") + + begin + update("SET FOREIGN_KEY_CHECKS = 0") + yield + ensure + update("SET FOREIGN_KEY_CHECKS = #{old}") + end + end + + # CONNECTION MANAGEMENT ==================================== + + def active? + return false unless @connection + @connection.query 'select 1' + true + rescue Mysql2::Error + false + end + + def reconnect! + disconnect! + connect + end + + # this is set to true in 2.3, but we don't want it to be + def requires_reloading? + false + end + + def disconnect! + unless @connection.nil? + @connection.close + @connection = nil + end + end + + def reset! + disconnect! + connect + end + + # DATABASE STATEMENTS ====================================== + + # FIXME: re-enable the following once a "better" query_cache solution is in core + # + # The overrides below perform much better than the originals in AbstractAdapter + # because we're able to take advantage of mysql2's lazy-loading capabilities + # + # # Returns a record hash with the column names as keys and column values + # # as values. + # def select_one(sql, name = nil) + # result = execute(sql, name) + # result.each(:as => :hash) do |r| + # return r + # end + # end + # + # # Returns a single value from a record + # def select_value(sql, name = nil) + # result = execute(sql, name) + # if first = result.first + # first.first + # end + # end + # + # # Returns an array of the values of the first column in a select: + # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] + # def select_values(sql, name = nil) + # execute(sql, name).map { |row| row.first } + # end + + # Returns an array of arrays containing the field values. + # Order is the same as that returned by +columns+. + def select_rows(sql, name = nil) + execute(sql, name).to_a + end + + # Executes the SQL statement in the context of this connection. + def execute(sql, name = nil) + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone + if name == :skip_logging + @connection.query(sql) + else + log(sql, name) { @connection.query(sql) } + end + rescue ActiveRecord::StatementInvalid => exception + if exception.message.split(":").first =~ /Packets out of order/ + raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." + else + raise + end + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + super + id_value || @connection.last_id + end + alias :create :insert_sql + + def update_sql(sql, name = nil) + super + @connection.affected_rows + end + + def begin_db_transaction + execute "BEGIN" + rescue Exception + # Transactions aren't supported + end + + def commit_db_transaction + execute "COMMIT" + rescue Exception + # Transactions aren't supported + end + + def rollback_db_transaction + execute "ROLLBACK" + rescue Exception + # Transactions aren't supported + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + def add_limit_offset!(sql, options) + limit, offset = options[:limit], options[:offset] + if limit && offset + sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}" + elsif limit + sql << " LIMIT #{sanitize_limit(limit)}" + elsif offset + sql << " OFFSET #{offset.to_i}" + end + sql + end + + # SCHEMA STATEMENTS ======================================== + + def structure_dump + if supports_views? + sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" + else + sql = "SHOW TABLES" + end + + select_all(sql).inject("") do |structure, table| + table.delete('Table_type') + structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" + end + end + + def recreate_database(name, options = {}) + drop_database(name) + create_database(name, options) + end + + # Create a new MySQL database with optional :charset and :collation. + # Charset defaults to utf8. + # + # Example: + # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' + # create_database 'matt_development' + # create_database 'matt_development', :charset => :big5 + def create_database(name, options = {}) + if options[:collation] + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" + else + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" + end + end + + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS `#{name}`" + end + + def current_database + select_value 'SELECT DATABASE() as db' + end + + # Returns the database character set. + def charset + show_variable 'character_set_database' + end + + # Returns the database collation strategy. + def collation + show_variable 'collation_database' + end + + def tables(name = nil) + tables = [] + execute("SHOW TABLES", name).each do |field| + tables << field.first + end + tables + end + + def drop_table(table_name, options = {}) + super(table_name, options) + end + + def indexes(table_name, name = nil) + indexes = [] + current_index = nil + result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) + result.each(:symbolize_keys => true, :as => :hash) do |row| + if current_index != row[:Key_name] + next if row[:Key_name] == PRIMARY # skip the primary key + current_index = row[:Key_name] + indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], []) + end + + indexes.last.columns << row[:Column_name] + indexes.last.lengths << row[:Sub_part] + end + indexes + end + + def columns(table_name, name = nil) + sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" + columns = [] + result = execute(sql, :skip_logging) + result.each(:symbolize_keys => true, :as => :hash) { |field| + columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") + } + columns + end + + def create_table(table_name, options = {}) + super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) + end + + def rename_table(table_name, new_name) + execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" + end + + def add_column(table_name, column_name, type, options = {}) + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_position!(add_column_sql, options) + execute(add_column_sql) + end + + def change_column_default(table_name, column_name, default) + column = column_for(table_name, column_name) + change_column table_name, column_name, column.sql_type, :default => default + end + + def change_column_null(table_name, column_name, null, default = nil) + column = column_for(table_name, column_name) + + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + + change_column table_name, column_name, column.sql_type, :null => null + end + + def change_column(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null + end + + change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) + options = {} + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + options[:null] = column.null + else + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + execute(rename_column_sql) + end + + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + return super unless type.to_s == 'integer' + + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + end + + def show_variable(name) + variables = select_all("SHOW VARIABLES LIKE '#{name}'") + variables.first['Value'] unless variables.empty? + end + + def pk_and_sequence_for(table) + keys = [] + result = execute("describe #{quote_table_name(table)}") + result.each(:symbolize_keys => true, :as => :hash) do |row| + keys << row[:Field] if row[:Key] == "PRI" + end + keys.length == 1 ? [keys.first, nil] : nil + end + + # Returns just a table's primary key + def primary_key(table) + pk_and_sequence = pk_and_sequence_for(table) + pk_and_sequence && pk_and_sequence.first + end + + def case_sensitive_equality_operator + "= BINARY" + end + + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + + protected + def quoted_columns_for_index(column_names, options = {}) + length = options[:length] if options.is_a?(Hash) + + quoted_column_names = case length + when Hash + column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } + when Fixnum + column_names.map {|name| "#{quote_column_name(name)}(#{length})"} + else + column_names.map {|name| quote_column_name(name) } + end + end + + def translate_exception(exception, message) + return super unless exception.respond_to?(:error_number) + + case exception.error_number + when 1062 + RecordNotUnique.new(message, exception) + when 1452 + InvalidForeignKey.new(message, exception) + else + super + end + end + + private + def connect + @connection = Mysql2::Client.new(@config) + configure_connection + end + + def configure_connection + @connection.query_options.merge!(:as => :array) + + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + variable_assignments = ['SQL_AUTO_IS_NULL=0'] + encoding = @config[:encoding] + + # make sure we set the encoding + variable_assignments << "NAMES '#{encoding}'" if encoding + + # increase timeout so mysql server doesn't disconnect us + wait_timeout = @config[:wait_timeout] + wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) + variable_assignments << "@@wait_timeout = #{wait_timeout}" + + execute("SET #{variable_assignments.join(', ')}", :skip_logging) + end + + # Returns an array of record hashes with the column names as keys and + # column values as values. + def select(sql, name = nil) + execute(sql, name).each(:as => :hash) + end + + def supports_views? + version[0] >= 5 + end + + def version + @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + end + + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + end + end + end +end -- cgit v1.2.3 From 12b2f8b040aeb7a69150dff7534b82ef633be331 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Wed, 2 Feb 2011 16:19:52 -0200 Subject: Delete blank lines in migration generator --- .../lib/rails/generators/active_record/model/templates/migration.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb index e573e908a7..4f81a52fd0 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb @@ -8,11 +8,10 @@ class <%= migration_class_name %> < ActiveRecord::Migration t.timestamps <% end -%> end - -<% if options[:indexes] %> +<% if options[:indexes] -%> <% attributes.select {|attr| attr.reference? }.each do |attribute| -%> add_index :<%= table_name %>, :<%= attribute.name %>_id <% end -%> -<% end %> +<% end -%> end end -- cgit v1.2.3 From 3fc598095e8e560a29cbefec643d991cfe0131c8 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 2 Feb 2011 10:25:29 -0800 Subject: just require mysql2 --- activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 5bd593ef18..babdf4cb58 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -require 'mysql2' unless defined? Mysql2 +require 'mysql2' module ActiveRecord class Base -- cgit v1.2.3 From 8bfa8e7cbea1fb65b180260c55a7f146efbd5b05 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 2 Feb 2011 10:25:38 -0800 Subject: this method should never return nil --- activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index babdf4cb58..a04fc01d6f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -85,7 +85,7 @@ module ActiveRecord when :datetime, :timestamp then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_time(#{var_name})" when :time then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_dummy_time(#{var_name})" when :date then "#{var_name}.class == Date ? #{var_name} : #{self.class.name}.string_to_date(#{var_name})" - when :binary then nil + when :binary then var_name when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" else var_name end -- cgit v1.2.3 From 351331fb343cdf17baa078943898ded6caff9d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20=C3=81lvarez?= Date: Thu, 3 Feb 2011 00:26:39 +0100 Subject: Make serialized columns with explicit object_type return a new instance of the object instead of nil --- activerecord/lib/active_record/base.rb | 24 ++++++++++++++++++---- .../lib/active_record/coders/yaml_column.rb | 2 ++ activerecord/test/cases/base_test.rb | 19 +++++++++++++++++ activerecord/test/cases/coders/yaml_column_test.rb | 5 +++-- 4 files changed, 44 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 0941700803..e0957e2849 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -249,6 +249,17 @@ module ActiveRecord #:nodoc: # user = User.create(:preferences => %w( one two three )) # User.find(user.id).preferences # raises SerializationTypeMismatch # + # When you specify a class option, the default value for that attribute will be a new + # instance of that class. + # + # class User < ActiveRecord::Base + # serialize :preferences, OpenStruct + # end + # + # user = User.new + # user.preferences.theme_color = "red" + # + # # == Single table inheritance # # Active Record allows inheritance by storing the name of the class in a column that by @@ -1409,6 +1420,7 @@ MSG @changed_attributes = {} ensure_proper_type + set_serialized_attributes populate_with_current_scope_attributes self.attributes = attributes unless attributes.nil? @@ -1447,10 +1459,7 @@ MSG def init_with(coder) @attributes = coder['attributes'] - (@attributes.keys & self.class.serialized_attributes.keys).each do |key| - coder = self.class.serialized_attributes[key] - @attributes[key] = coder.load @attributes[key] - end + set_serialized_attributes @attributes_cache, @previously_changed, @changed_attributes = {}, {}, {} @association_cache = {} @@ -1461,6 +1470,13 @@ MSG run_callbacks :initialize end + def set_serialized_attributes + (@attributes.keys & self.class.serialized_attributes.keys).each do |key| + coder = self.class.serialized_attributes[key] + @attributes[key] = coder.load @attributes[key] + end + end + # Specifies how the record is dumped by +Marshal+. # # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index fcecc11aba..fb59d9fb07 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -19,6 +19,7 @@ module ActiveRecord end def load(yaml) + return object_class.new if object_class != Object && yaml.nil? return yaml unless yaml.is_a?(String) && yaml =~ /^---/ begin obj = YAML.load(yaml) @@ -27,6 +28,7 @@ module ActiveRecord raise SerializationTypeMismatch, "Attribute was supposed to be a #{object_class}, but was a #{obj.class}" end + obj ||= object_class.new if object_class != Object obj rescue *RESCUE_ERRORS diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 5cbc52732b..a255c07957 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1002,6 +1002,25 @@ class BasicsTest < ActiveRecord::TestCase Topic.serialize(:content) end + def test_serialized_default_class + Topic.serialize(:content, Hash) + topic = Topic.new + assert_equal Hash, topic.content.class + assert_equal Hash, topic.read_attribute(:content).class + topic.content["beer"] = "MadridRb" + assert topic.save + topic.reload + assert_equal Hash, topic.content.class + assert_equal "MadridRb", topic.content["beer"] + ensure + Topic.serialize(:content) + end + + def test_serialized_no_default_class_for_object + topic = Topic.new + assert_nil topic.content + end + def test_serialized_boolean_value_true Topic.serialize(:content) topic = Topic.new(:content => true) diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index f85f11b57f..c7dcc21809 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -1,3 +1,4 @@ + require "cases/helper" module ActiveRecord @@ -20,9 +21,9 @@ module ActiveRecord assert_nil coder.load "--- " end - def test_nil_is_ok_with_different_class + def test_returns_new_with_different_class coder = YAMLColumn.new SerializationTypeMismatch - assert_nil coder.load "--- " + assert_equal SerializationTypeMismatch, coder.load("--- ").class end def test_returns_string_unless_starts_with_dash -- cgit v1.2.3 From a5d8f0be006e5b77d9a82b59407c8d0585f87d41 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 3 Feb 2011 09:09:27 -0800 Subject: this method should be private --- activerecord/lib/active_record/base.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index e0957e2849..e4a425627f 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1470,13 +1470,6 @@ MSG run_callbacks :initialize end - def set_serialized_attributes - (@attributes.keys & self.class.serialized_attributes.keys).each do |key| - coder = self.class.serialized_attributes[key] - @attributes[key] = coder.load @attributes[key] - end - end - # Specifies how the record is dumped by +Marshal+. # # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this @@ -1740,6 +1733,13 @@ MSG private + def set_serialized_attributes + (@attributes.keys & self.class.serialized_attributes.keys).each do |key| + coder = self.class.serialized_attributes[key] + @attributes[key] = coder.load @attributes[key] + end + end + # Sets the attribute used for single table inheritance to this class name if this is not the # ActiveRecord::Base descendant. # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to -- cgit v1.2.3 From 1a15fda02159487371a0cb4c36311345dec7b46b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 3 Feb 2011 11:12:07 -0800 Subject: reduce cache misses on STI subclasses --- activerecord/lib/active_record/base.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index e4a425627f..8750e226b9 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -747,12 +747,13 @@ module ActiveRecord #:nodoc: undefine_attribute_methods reset_column_cache @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @relation = @arel_table = nil + @arel_engine = @relation = nil end def reset_column_cache # :nodoc: @@columns.delete table_name @@columns_hash.delete table_name + @@arel_tables.delete table_name end def attribute_method?(attribute) @@ -851,7 +852,7 @@ module ActiveRecord #:nodoc: end def arel_table - @arel_table ||= Arel::Table.new(table_name, arel_engine) + @@arel_tables[table_name] ||= Arel::Table.new(table_name, arel_engine) end def arel_engine @@ -1401,6 +1402,7 @@ MSG end @@columns_hash = {} @@columns = {} + @@arel_tables = {} public # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with -- cgit v1.2.3 From d65e3b481e72e8c76818a94353e9ac315c7c0272 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 3 Feb 2011 11:50:43 -0800 Subject: ARel only requires the connection from the AR class. Simply return the AR class rather than jump through hoops and store ivars --- activerecord/lib/active_record/base.rb | 10 +---- .../abstract/connection_specification.rb | 48 +++++++++++----------- activerecord/test/cases/multiple_db_test.rb | 6 +-- 3 files changed, 29 insertions(+), 35 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 8750e226b9..4e743ec826 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -747,7 +747,7 @@ module ActiveRecord #:nodoc: undefine_attribute_methods reset_column_cache @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @arel_engine = @relation = nil + @relation = nil end def reset_column_cache # :nodoc: @@ -856,13 +856,7 @@ module ActiveRecord #:nodoc: end def arel_engine - @arel_engine ||= begin - if self == ActiveRecord::Base - ActiveRecord::Base - else - connection_handler.connection_pools[name] ? self : superclass.arel_engine - end - end + self end # Returns a scope for this class without taking into account the default_scope. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 3716937689..5a6d13e64b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -50,34 +50,34 @@ module ActiveRecord # may be returned on an error. def self.establish_connection(spec = nil) case spec - when nil - raise AdapterNotSpecified unless defined?(Rails.env) - establish_connection(Rails.env) - when ConnectionSpecification - self.connection_handler.establish_connection(name, spec) - when Symbol, String - if configuration = configurations[spec.to_s] - establish_connection(configuration) - else - raise AdapterNotSpecified, "#{spec} database is not configured" - end + when nil + raise AdapterNotSpecified unless defined?(Rails.env) + establish_connection(Rails.env) + when ConnectionSpecification + self.connection_handler.establish_connection(name, spec) + when Symbol, String + if configuration = configurations[spec.to_s] + establish_connection(configuration) else - spec = spec.symbolize_keys - unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end + raise AdapterNotSpecified, "#{spec} database is not configured" + end + else + spec = spec.symbolize_keys + unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end - begin - require "active_record/connection_adapters/#{spec[:adapter]}_adapter" - rescue LoadError => e - raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" - end + begin + require "active_record/connection_adapters/#{spec[:adapter]}_adapter" + rescue LoadError => e + raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" + end - adapter_method = "#{spec[:adapter]}_connection" - unless respond_to?(adapter_method) - raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" - end + adapter_method = "#{spec[:adapter]}_connection" + unless respond_to?(adapter_method) + raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" + end - remove_connection - establish_connection(ConnectionSpecification.new(spec, adapter_method)) + remove_connection + establish_connection(ConnectionSpecification.new(spec, adapter_method)) end end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index bd51388e05..3daf81c828 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -84,8 +84,8 @@ class MultipleDbTest < ActiveRecord::TestCase assert_equal "Ruby Developer", Entrant.find(1).name end - def test_arel_table_engines - assert_not_equal Entrant.arel_engine, Course.arel_engine - assert_equal Entrant.arel_engine, Bird.arel_engine + def test_connections + assert_not_equal Entrant.connection, Course.connection + assert_equal Entrant.connection, Bird.connection end end -- cgit v1.2.3 From 95d5d9b6c48c08f1fba0c77ecbc97b62b2603824 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Thu, 3 Feb 2011 15:07:03 -0500 Subject: The type_cast_calculated_value method will trust DB types before casting to a BigDecimal. [#6365 state:committed] Signed-off-by: Santiago Pastorino --- activerecord/lib/active_record/relation/calculations.rb | 2 +- activerecord/test/cases/calculations_test.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index b75a65e3ca..abc4c54109 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -285,7 +285,7 @@ module ActiveRecord case operation when 'count' then value.to_i when 'sum' then type_cast_using_column(value || '0', column) - when 'average' then value.try(:to_d) + when 'average' then value.respond_to?(:to_d) ? value.to_d : value else type_cast_using_column(value, column) end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 644c9cb528..3121f1615d 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -28,6 +28,12 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 3.5, value end + def test_should_return_integer_average_if_db_returns_such + Account.connection.stubs :select_value => 3 + value = Account.average(:id) + assert_equal 3, value + end + def test_should_return_nil_as_average assert_nil NumericData.average(:bank_balance) end -- cgit v1.2.3 From 23a3ba426067d3d38259ef4e1d234cbb82c8aea2 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 3 Feb 2011 14:04:21 -0800 Subject: Revert "ARel only requires the connection from the AR class. Simply return the AR class rather than jump through hoops and store ivars" This reverts commit d65e3b481e72e8c76818a94353e9ac315c7c0272. --- activerecord/lib/active_record/base.rb | 10 ++++- .../abstract/connection_specification.rb | 48 +++++++++++----------- activerecord/test/cases/multiple_db_test.rb | 6 +-- 3 files changed, 35 insertions(+), 29 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 4e743ec826..8750e226b9 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -747,7 +747,7 @@ module ActiveRecord #:nodoc: undefine_attribute_methods reset_column_cache @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil - @relation = nil + @arel_engine = @relation = nil end def reset_column_cache # :nodoc: @@ -856,7 +856,13 @@ module ActiveRecord #:nodoc: end def arel_engine - self + @arel_engine ||= begin + if self == ActiveRecord::Base + ActiveRecord::Base + else + connection_handler.connection_pools[name] ? self : superclass.arel_engine + end + end end # Returns a scope for this class without taking into account the default_scope. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 5a6d13e64b..3716937689 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -50,34 +50,34 @@ module ActiveRecord # may be returned on an error. def self.establish_connection(spec = nil) case spec - when nil - raise AdapterNotSpecified unless defined?(Rails.env) - establish_connection(Rails.env) - when ConnectionSpecification - self.connection_handler.establish_connection(name, spec) - when Symbol, String - if configuration = configurations[spec.to_s] - establish_connection(configuration) + when nil + raise AdapterNotSpecified unless defined?(Rails.env) + establish_connection(Rails.env) + when ConnectionSpecification + self.connection_handler.establish_connection(name, spec) + when Symbol, String + if configuration = configurations[spec.to_s] + establish_connection(configuration) + else + raise AdapterNotSpecified, "#{spec} database is not configured" + end else - raise AdapterNotSpecified, "#{spec} database is not configured" - end - else - spec = spec.symbolize_keys - unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end + spec = spec.symbolize_keys + unless spec.key?(:adapter) then raise AdapterNotSpecified, "database configuration does not specify adapter" end - begin - require "active_record/connection_adapters/#{spec[:adapter]}_adapter" - rescue LoadError => e - raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" - end + begin + require "active_record/connection_adapters/#{spec[:adapter]}_adapter" + rescue LoadError => e + raise "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e})" + end - adapter_method = "#{spec[:adapter]}_connection" - unless respond_to?(adapter_method) - raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" - end + adapter_method = "#{spec[:adapter]}_connection" + unless respond_to?(adapter_method) + raise AdapterNotFound, "database configuration specifies nonexistent #{spec[:adapter]} adapter" + end - remove_connection - establish_connection(ConnectionSpecification.new(spec, adapter_method)) + remove_connection + establish_connection(ConnectionSpecification.new(spec, adapter_method)) end end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 3daf81c828..bd51388e05 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -84,8 +84,8 @@ class MultipleDbTest < ActiveRecord::TestCase assert_equal "Ruby Developer", Entrant.find(1).name end - def test_connections - assert_not_equal Entrant.connection, Course.connection - assert_equal Entrant.connection, Bird.connection + def test_arel_table_engines + assert_not_equal Entrant.arel_engine, Course.arel_engine + assert_equal Entrant.arel_engine, Bird.arel_engine end end -- cgit v1.2.3 From 7423a71fc02c0ca3bf37b94e16a1322c0caaa6fd Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 3 Feb 2011 15:35:34 -0800 Subject: allow AR caches to be cleared, clear them on class reloading --- activerecord/lib/active_record/base.rb | 6 ++++++ activerecord/lib/active_record/railtie.rb | 1 + activerecord/test/cases/base_test.rb | 8 ++++++++ 3 files changed, 15 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 8750e226b9..f8ae855e28 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -750,6 +750,12 @@ module ActiveRecord #:nodoc: @arel_engine = @relation = nil end + def clear_cache! # :nodoc: + @@columns.clear + @@columns_hash.clear + @@arel_tables.clear + end + def reset_column_cache # :nodoc: @@columns.delete table_name @@columns_hash.delete table_name diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 2accf0a48f..72687c9ca3 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -72,6 +72,7 @@ module ActiveRecord ActiveSupport.on_load(:active_record) do ActionDispatch::Reloader.to_cleanup do ActiveRecord::Base.clear_reloadable_connections! + ActiveRecord::Base.clear_cache! end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index a255c07957..68adeff882 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1553,6 +1553,14 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_clear_cache! + # preheat cache + c1 = Post.columns + ActiveRecord::Base.clear_cache! + c2 = Post.columns + assert_not_equal c1, c2 + end + def test_default_scope_is_reset Object.const_set :UnloadablePost, Class.new(ActiveRecord::Base) UnloadablePost.table_name = 'posts' -- cgit v1.2.3 From a88071e92afa383e3eaa3369948c4fe960c25b03 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 3 Feb 2011 15:48:34 -0800 Subject: just use the superclass implementation --- .../connection_adapters/mysql2_adapter.rb | 49 ---------------------- 1 file changed, 49 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index a04fc01d6f..b054d5ba73 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -42,55 +42,6 @@ module ActiveRecord super end - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime then Time - when :date then Date - when :timestamp then Time - when :time then Time - when :text, :string then String - when :binary then String - when :boolean then Object - end - end - - def type_cast(value) - return nil if value.nil? - case type - when :string then value - when :text then value - when :integer then value.to_i rescue value ? 1 : 0 - when :float then value.to_f # returns self if it's already a Float - when :decimal then self.class.value_to_decimal(value) - when :datetime, :timestamp then value.class == Time ? value : self.class.string_to_time(value) - when :time then value.class == Time ? value : self.class.string_to_dummy_time(value) - when :date then value.class == Date ? value : self.class.string_to_date(value) - when :binary then value - when :boolean then self.class.value_to_boolean(value) - else value - end - end - - def type_cast_code(var_name) - case type - when :string then var_name - when :text then var_name - when :integer then "#{var_name}.to_i rescue #{var_name} ? 1 : 0" - when :float then "#{var_name}.to_f" - when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" - when :datetime, :timestamp then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_time(#{var_name})" - when :time then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_dummy_time(#{var_name})" - when :date then "#{var_name}.class == Date ? #{var_name} : #{self.class.name}.string_to_date(#{var_name})" - when :binary then var_name - when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" - else var_name - end - end - private def simplified_type(field_type) return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) -- cgit v1.2.3 From dde8933d60d1c1890fef21237f0f328e0e34382b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 3 Feb 2011 15:54:31 -0800 Subject: add_limit_offset! is deprecated --- activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index b054d5ba73..aaa2b33fcf 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -327,6 +327,7 @@ module ActiveRecord end sql end + deprecate :add_limit_offset! # SCHEMA STATEMENTS ======================================== -- cgit v1.2.3 From 89962d82f1f4c3d61d5851a4b0bca771357c6351 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 3 Feb 2011 16:00:59 -0800 Subject: refactor a bunch of return / if to a case / when --- .../active_record/connection_adapters/column.rb | 44 +++++++++++----------- .../connection_adapters/mysql2_adapter.rb | 12 ++++-- 2 files changed, 30 insertions(+), 26 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index beb06ea622..4e3d8a096f 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -238,28 +238,28 @@ module ActiveRecord def simplified_type(field_type) case field_type - when /int/i - :integer - when /float|double/i - :float - when /decimal|numeric|number/i - extract_scale(field_type) == 0 ? :integer : :decimal - when /datetime/i - :datetime - when /timestamp/i - :timestamp - when /time/i - :time - when /date/i - :date - when /clob/i, /text/i - :text - when /blob/i, /binary/i - :binary - when /char/i, /string/i - :string - when /boolean/i - :boolean + when /int/i + :integer + when /float|double/i + :float + when /decimal|numeric|number/i + extract_scale(field_type) == 0 ? :integer : :decimal + when /datetime/i + :datetime + when /timestamp/i + :timestamp + when /time/i + :time + when /date/i + :date + when /clob/i, /text/i + :text + when /blob/i, /binary/i + :binary + when /char/i, /string/i + :string + when /boolean/i + :boolean end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index aaa2b33fcf..acf1832938 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -45,10 +45,14 @@ module ActiveRecord private def simplified_type(field_type) return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) - return :string if field_type =~ /enum/i or field_type =~ /set/i - return :integer if field_type =~ /year/i - return :binary if field_type =~ /bit/i - super + + case field_type + when /enum/i, /set/i then :string + when /year/i then :integer + when /bit/i then :binary + else + super + end end def extract_limit(sql_type) -- cgit v1.2.3 From ac15647bf0e6ed85714dee4e2b14b2e7e6f29320 Mon Sep 17 00:00:00 2001 From: Gabriel Horner Date: Thu, 3 Feb 2011 23:51:06 -0500 Subject: keep options titles consistent to "Options" --- activerecord/lib/active_record/associations.rb | 2 +- activerecord/lib/active_record/relation/finder_methods.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 891ac52f8a..285ce32e21 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -912,7 +912,7 @@ module ActiveRecord # * Firm#clients.create (similar to c = Client.new("firm_id" => id); c.save; c) # The declaration can also include an options hash to specialize the behavior of the association. # - # === Supported options + # === Options # [:class_name] # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So has_many :products will by default be linked diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index e447de92a4..7f32e5538e 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -19,7 +19,7 @@ module ActiveRecord # # All approaches accept an options hash as their last parameter. # - # ==== Parameters + # ==== Options # # * :conditions - An SQL fragment like "administrator = 1", ["user_name = ?", username], # or ["user_name = :user_name", { :user_name => user_name }]. See conditions in the intro. -- cgit v1.2.3 From 94175c0e39ae9fc8d22dc2c2c55f16c1d92f4090 Mon Sep 17 00:00:00 2001 From: Kevin Skoglund Date: Fri, 4 Feb 2011 09:33:06 -0500 Subject: Improve regex in rake db:migrate status [#5940 state:resolved] [#5940 state:committed] Signed-off-by: Santiago Pastorino --- activerecord/lib/active_record/railties/databases.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 9924755ddf..49d4b8f76b 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -193,7 +193,7 @@ db_namespace = namespace :db do file_list = [] Dir.foreach(File.join(Rails.root, 'db', 'migrate')) do |file| # only files matching "20091231235959_some_name.rb" pattern - if match_data = /(\d{14})_(.+)\.rb/.match(file) + if match_data = /^(\d{14})_(.+)\.rb$/.match(file) status = db_list.delete(match_data[1]) ? 'up' : 'down' file_list << [status, match_data[1], match_data[2]] end -- cgit v1.2.3 From df077604865b12b119be0259575675f45b958524 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 4 Feb 2011 13:34:41 -0800 Subject: introduce a fake AR adapter for mocking database return values --- .../connection_adapters/fake_adapter.rb | 36 ++++++++++++++++++++++ activerecord/test/models/contact.rb | 12 +++++--- 2 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 activerecord/test/active_record/connection_adapters/fake_adapter.rb (limited to 'activerecord') diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb new file mode 100644 index 0000000000..1c2942170e --- /dev/null +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -0,0 +1,36 @@ +module ActiveRecord + class Base + def self.fake_connection(config) + ConnectionAdapters::FakeAdapter.new nil, logger + end + end + + module ConnectionAdapters + class FakeAdapter < AbstractAdapter + attr_accessor :tables, :primary_keys + + def initialize(connection, logger) + super + @tables = [] + @primary_keys = {} + @columns = Hash.new { |h,k| h[k] = [] } + end + + def primary_key(table) + @primary_keys[table] + end + + def merge_column(table_name, name, sql_type = nil, options = {}) + @columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new( + name.to_s, + options[:default], + sql_type.to_s, + options[:null]) + end + + def columns(table_name, message) + @columns[table_name] + end + end + end +end diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index 5bbe7ebb12..e081eee661 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -1,12 +1,14 @@ class Contact < ActiveRecord::Base - def self.columns - @columns - end + establish_connection(:adapter => 'fake') + + connection.tables = ['contacts'] + connection.primary_keys = { + 'contacts' => 'id' + } # mock out self.columns so no pesky db is needed for these tests def self.column(name, sql_type = nil, options = {}) - @columns ||= [] - @columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, options[:default], sql_type.to_s, options[:null]) + connection.merge_column('contacts', name, sql_type, options) end column :name, :string -- cgit v1.2.3 From 909588d964bf27f20142a0b4d57890114a8d4a7a Mon Sep 17 00:00:00 2001 From: Edward Faulkner Date: Fri, 4 Feb 2011 15:34:44 -0500 Subject: Fixing ordering of HABTM association deletion [#6191 state:resolved] Signed-off-by: Santiago Pastorino --- activerecord/lib/active_record/associations.rb | 2 +- .../has_and_belongs_to_many_associations_test.rb | 2 +- activerecord/test/cases/habtm_destroy_order_test.rb | 17 +++++++++++++++++ activerecord/test/models/lesson.rb | 11 +++++++++++ activerecord/test/models/student.rb | 3 +++ activerecord/test/schema/schema.rb | 13 +++++++++++++ 6 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 activerecord/test/cases/habtm_destroy_order_test.rb create mode 100644 activerecord/test/models/lesson.rb create mode 100644 activerecord/test/models/student.rb (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 891ac52f8a..d30362b93e 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1450,8 +1450,8 @@ module ActiveRecord include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 def destroy # def destroy - #{reflection.name}.clear # posts.clear super # super + #{reflection.name}.clear # posts.clear end # end RUBY } 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 30730c7094..55b611ca92 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 @@ -365,7 +365,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_removing_associations_on_destroy david = DeveloperWithBeforeDestroyRaise.find(1) assert !david.projects.empty? - assert_raise(RuntimeError) { david.destroy } + david.destroy assert david.projects.empty? assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty? end diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb new file mode 100644 index 0000000000..15598392e2 --- /dev/null +++ b/activerecord/test/cases/habtm_destroy_order_test.rb @@ -0,0 +1,17 @@ +require "cases/helper" +require "models/lesson" +require "models/student" + +class HabtmDestroyOrderTest < ActiveRecord::TestCase + test "may not delete a lesson with students" do + sicp = Lesson.new(:name => "SICP") + ben = Student.new(:name => "Ben Bitdiddle") + sicp.students << ben + sicp.save! + assert_raises LessonError do + assert_no_difference('Lesson.count') do + sicp.destroy + end + end + end +end diff --git a/activerecord/test/models/lesson.rb b/activerecord/test/models/lesson.rb new file mode 100644 index 0000000000..4c88153068 --- /dev/null +++ b/activerecord/test/models/lesson.rb @@ -0,0 +1,11 @@ +class LessonError < Exception +end + +class Lesson < ActiveRecord::Base + has_and_belongs_to_many :students + before_destroy :ensure_no_students + + def ensure_no_students + raise LessonError unless students.empty? + end +end diff --git a/activerecord/test/models/student.rb b/activerecord/test/models/student.rb new file mode 100644 index 0000000000..f459f2a9a3 --- /dev/null +++ b/activerecord/test/models/student.rb @@ -0,0 +1,3 @@ +class Student < ActiveRecord::Base + has_and_belongs_to_many :lessons +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 5f9bb7ee41..326c336317 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -279,6 +279,15 @@ ActiveRecord::Schema.define do t.integer :version, :null => false, :default => 0 end + create_table :lessons, :force => true do |t| + t.string :name + end + + create_table :lessons_students, :id => false, :force => true do |t| + t.references :lesson + t.references :student + end + create_table :line_items, :force => true do |t| t.integer :invoice_id t.integer :amount @@ -509,6 +518,10 @@ ActiveRecord::Schema.define do t.string :sponsorable_type end + create_table :students, :force => true do |t| + t.string :name + end + create_table :subscribers, :force => true, :id => false do |t| t.string :nick, :null => false t.string :name -- cgit v1.2.3 From 5f3cf4244de4fd62049d08df4e6bf63c945ab90e Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 4 Feb 2011 10:19:02 -0800 Subject: connection pool can cache column, table, and primary key information --- .../abstract/connection_pool.rb | 34 +++++++++++++++++++++ activerecord/test/cases/connection_pool_test.rb | 35 ++++++++++++++++++++++ 2 files changed, 69 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 54f70c59f8..bafd79d614 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -58,6 +58,7 @@ module ActiveRecord # before giving up and raising a timeout error (default 5 seconds). class ConnectionPool attr_reader :spec, :connections + attr_reader :columns, :columns_hash, :primary_keys # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -81,6 +82,39 @@ module ActiveRecord @connections = [] @checked_out = [] + + @columns = Hash.new do |h, table_name| + h[table_name] = with_connection do |conn| + conn.columns(table_name, "#{table_name} Columns") + end + end + + @columns_hash = Hash.new do |h, table_name| + h[table_name] = Hash[columns[table_name].map { |col| + [col.name, col] + }] + end + + @primary_keys = Hash.new do |h, table_name| + h[table_name] = with_connection do |conn| + if conn.table_exists?(table_name) + conn.primary_key(table_name) + else + 'id' + end + end + end + end + + # Clears out internal caches: + # + # * columns + # * columns_hash + # * primary_keys + def clear_cache! + @columns.clear + @columns_hash.clear + @primary_keys.clear end # Retrieve the connection associated with the current thread, or call diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 2e18117895..27fc3cab49 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -3,6 +3,41 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class ConnectionPoolTest < ActiveRecord::TestCase + def setup + # Keep a duplicate pool so we do not bother others + @pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec + end + + def test_pool_caches_columns + columns = @pool.columns['posts'] + assert_equal columns, @pool.columns['posts'] + end + + def test_pool_caches_columns_hash + columns_hash = @pool.columns_hash['posts'] + assert_equal columns_hash, @pool.columns_hash['posts'] + end + + def test_clearing_cache + @pool.columns['posts'] + @pool.columns_hash['posts'] + @pool.primary_keys['posts'] + + @pool.clear_cache! + + assert_equal 0, @pool.columns.size + assert_equal 0, @pool.columns_hash.size + assert_equal 0, @pool.primary_keys.size + end + + def test_primary_key + assert_equal 'id', @pool.primary_keys['posts'] + end + + def test_primary_key_for_non_existent_table + assert_equal 'id', @pool.primary_keys['omgponies'] + end + def test_clear_stale_cached_connections! pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec -- cgit v1.2.3 From 0cd42864e3dcba520980f952be9a09c01beddb03 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 4 Feb 2011 12:15:30 -0800 Subject: making sure primary key is set on the columns --- .../connection_adapters/abstract/connection_pool.rb | 10 +++++++++- activerecord/test/cases/connection_pool_test.rb | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index bafd79d614..db745e325f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -85,7 +85,15 @@ module ActiveRecord @columns = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| - conn.columns(table_name, "#{table_name} Columns") + + # Fetch a list of columns + conn.columns(table_name, "#{table_name} Columns").tap do |columns| + + # set primary key information + columns.each do |column| + column.primary = column.name == primary_keys[table_name] + end + end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 27fc3cab49..0727e86705 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -38,6 +38,15 @@ module ActiveRecord assert_equal 'id', @pool.primary_keys['omgponies'] end + def test_primary_key_is_set_on_columns + posts_columns = @pool.columns_hash['posts'] + assert posts_columns['id'].primary + + (posts_columns.keys - ['id']).each do |key| + assert !posts_columns[key].primary + end + end + def test_clear_stale_cached_connections! pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec -- cgit v1.2.3 From c94651f8c822f7c0778c03eb36bee5ca19f35911 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 4 Feb 2011 14:52:05 -0800 Subject: almost fisted --- activerecord/lib/active_record/base.rb | 26 +++++----------------- .../abstract/connection_pool.rb | 7 ++++++ activerecord/lib/active_record/session_store.rb | 4 ++-- .../has_and_belongs_to_many_associations_test.rb | 4 ++-- 4 files changed, 17 insertions(+), 24 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index f8ae855e28..effb17b2ff 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -679,16 +679,12 @@ module ActiveRecord #:nodoc: # Returns an array of column objects for the table associated with this class. def columns - @@columns[table_name] ||= connection.columns( - table_name, "#{name} Columns" - ).tap { |columns| - columns.each { |column| column.primary = column.name == primary_key } - } + connection_pool.columns[table_name] end # Returns a hash of column objects for the table associated with this class. def columns_hash - @@columns_hash[table_name] ||= Hash[columns.map { |column| [column.name, column] }] + connection_pool.columns_hash[table_name] end # Returns an array of column names as strings. @@ -745,21 +741,14 @@ module ActiveRecord #:nodoc: def reset_column_information connection.clear_cache! undefine_attribute_methods - reset_column_cache + connection_pool.clear_table_cache!(table_name) if table_exists? + @column_names = @content_columns = @dynamic_methods_hash = @inheritance_column = nil @arel_engine = @relation = nil end def clear_cache! # :nodoc: - @@columns.clear - @@columns_hash.clear - @@arel_tables.clear - end - - def reset_column_cache # :nodoc: - @@columns.delete table_name - @@columns_hash.delete table_name - @@arel_tables.delete table_name + connection_pool.clear_cache! end def attribute_method?(attribute) @@ -858,7 +847,7 @@ module ActiveRecord #:nodoc: end def arel_table - @@arel_tables[table_name] ||= Arel::Table.new(table_name, arel_engine) + Arel::Table.new(table_name, arel_engine) end def arel_engine @@ -1406,9 +1395,6 @@ MSG quoted_value end end - @@columns_hash = {} - @@columns = {} - @@arel_tables = {} public # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index db745e325f..63cdce5e4d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -125,6 +125,13 @@ module ActiveRecord @primary_keys.clear end + # Clear out internal caches for table with +table_name+ + def clear_table_cache!(table_name) + @columns.delete table_name + @columns_hash.delete table_name + @primary_keys.delete table_name + end + # Retrieve the connection associated with the current thread, or call # #checkout to obtain one if necessary. # diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index e3342f046f..7e77aefb21 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -59,12 +59,12 @@ module ActiveRecord end def drop_table! - reset_column_cache + connection_pool.clear_table_cache!(table_name) connection.drop_table table_name end def create_table! - reset_column_cache + connection_pool.clear_table_cache!(table_name) connection.create_table(table_name) do |t| t.string session_id_column, :limit => 255 t.text data_column_name 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 55b611ca92..49cfac2170 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 @@ -757,10 +757,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david = Developer.find(1) # clear cache possibly created by other tests david.projects.reset_column_information - assert_queries(0) { david.projects.columns; david.projects.columns } + assert_queries(1) { david.projects.columns; david.projects.columns } # and again to verify that reset_column_information clears the cache correctly david.projects.reset_column_information - assert_queries(0) { david.projects.columns; david.projects.columns } + assert_queries(1) { david.projects.columns; david.projects.columns } end def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause -- cgit v1.2.3 From acccb72cb12ab55bb01c3dce32f54f4a59ebec6c Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 4 Feb 2011 15:54:32 -0800 Subject: column cache now lives on the connection pool --- .../connection_adapters/abstract/connection_pool.rb | 6 +++++- .../has_and_belongs_to_many_associations_test.rb | 4 ++-- activerecord/test/cases/connection_pool_test.rb | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 63cdce5e4d..b9bfad2bd1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -57,6 +57,7 @@ module ActiveRecord # * +wait_timeout+: number of seconds to block and wait for a connection # before giving up and raising a timeout error (default 5 seconds). class ConnectionPool + attr_accessor :automatic_reconnect attr_reader :spec, :connections attr_reader :columns, :columns_hash, :primary_keys @@ -82,6 +83,7 @@ module ActiveRecord @connections = [] @checked_out = [] + @automatic_reconnect = true @columns = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| @@ -281,6 +283,8 @@ module ActiveRecord end def checkout_new_connection + raise ConnectionNotEstablished unless @automatic_reconnect + c = new_connection @connections << c checkout_and_verify(c) @@ -379,7 +383,7 @@ module ActiveRecord pool = @connection_pools[klass.name] return nil unless pool - @connection_pools.delete_if { |key, value| value == pool } + pool.automatic_reconnect = false pool.disconnect! pool.spec.config end 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 49cfac2170..55b611ca92 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 @@ -757,10 +757,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david = Developer.find(1) # clear cache possibly created by other tests david.projects.reset_column_information - assert_queries(1) { david.projects.columns; david.projects.columns } + assert_queries(0) { david.projects.columns; david.projects.columns } # and again to verify that reset_column_information clears the cache correctly david.projects.reset_column_information - assert_queries(1) { david.projects.columns; david.projects.columns } + assert_queries(0) { david.projects.columns; david.projects.columns } end def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 0727e86705..55ac1bc406 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -99,6 +99,26 @@ module ActiveRecord end.join() end + + def test_automatic_reconnect= + pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec + assert pool.automatic_reconnect + assert pool.connection + + pool.disconnect! + assert pool.connection + + pool.disconnect! + pool.automatic_reconnect = false + + assert_raises(ConnectionNotEstablished) do + pool.connection + end + + assert_raises(ConnectionNotEstablished) do + pool.with_connection + end + end end end end -- cgit v1.2.3 From 59f7780a3454a14054d1d33d9b6e31192ab2e58b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 4 Feb 2011 18:08:31 -0800 Subject: adjust query counts to be consistent across databases, make sure database log the same things --- .../connection_adapters/abstract/connection_pool.rb | 14 +++++++++----- .../active_record/connection_adapters/mysql_adapter.rb | 2 +- .../connection_adapters/postgresql_adapter.rb | 4 ++-- .../connection_adapters/sqlite_adapter.rb | 18 +++++++++++++++--- activerecord/lib/active_record/result.rb | 4 ++++ .../has_and_belongs_to_many_associations_test.rb | 9 ++++++--- activerecord/test/cases/helper.rb | 2 +- activerecord/test/cases/migration_test.rb | 5 ++++- 8 files changed, 42 insertions(+), 16 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index b9bfad2bd1..4475019e0e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -85,6 +85,12 @@ module ActiveRecord @checked_out = [] @automatic_reconnect = true + @tables = Hash.new do |h, table_name| + with_connection do |conn| + h[table_name] = conn.table_exists?(table_name) + end + end + @columns = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| @@ -107,11 +113,7 @@ module ActiveRecord @primary_keys = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| - if conn.table_exists?(table_name) - conn.primary_key(table_name) - else - 'id' - end + @tables[table_name] ? conn.primary_key(table_name) : 'id' end end end @@ -121,10 +123,12 @@ module ActiveRecord # * columns # * columns_hash # * primary_keys + # * tables def clear_cache! @columns.clear @columns_hash.clear @primary_keys.clear + @tables.clear end # Clear out internal caches for table with +table_name+ diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 15488cee52..cdf1ebfee4 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -537,7 +537,7 @@ module ActiveRecord def columns(table_name, name = nil)#:nodoc: sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] - result = execute(sql, :skip_logging) + result = execute(sql) result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } result.free columns diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index a4b1aa7154..46c0f3fafe 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -786,7 +786,7 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = query(<<-end_sql, 'PK and serial sequence')[0] + result = exec_query(<<-end_sql, 'PK and serial sequence').rows.first SELECT attr.attname, seq.relname FROM pg_class seq, pg_attribute attr, @@ -1071,7 +1071,7 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: - query <<-end_sql + exec_query(<<-end_sql).rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index b04383d5bf..f650a1bc74 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -152,8 +152,11 @@ module ActiveRecord # Don't cache statements without bind values if binds.empty? - stmt = @connection.prepare(sql) - cols = stmt.columns + stmt = @connection.prepare(sql) + cols = stmt.columns + records = stmt.to_a + stmt.close + stmt = records else cache = @statements[sql] ||= { :stmt => @connection.prepare(sql) @@ -233,6 +236,15 @@ module ActiveRecord def columns(table_name, name = nil) #:nodoc: table_structure(table_name).map do |field| + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/ + field["dflt_value"] = $1.gsub(/''/, "'") + when /^"(.*)"$/ + field["dflt_value"] = $1.gsub(/""/, '"') + end + SQLiteColumn.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) end end @@ -334,7 +346,7 @@ module ActiveRecord end def table_structure(table_name) - structure = @connection.table_info(quote_table_name(table_name)) + structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})").to_hash raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? structure end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 47a5ac2700..0465b21e88 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -20,6 +20,10 @@ module ActiveRecord hash_rows.each { |row| yield row } end + def to_hash + hash_rows + end + private def hash_rows @hash_rows ||= @rows.map { |row| 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 55b611ca92..126b767d06 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 @@ -757,10 +757,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david = Developer.find(1) # clear cache possibly created by other tests david.projects.reset_column_information - assert_queries(0) { david.projects.columns; david.projects.columns } - # and again to verify that reset_column_information clears the cache correctly + + # One query for columns, one for primary key + assert_queries(2) { david.projects.columns; david.projects.columns } + + ## and again to verify that reset_column_information clears the cache correctly david.projects.reset_column_information - assert_queries(0) { david.projects.columns; david.projects.columns } + assert_queries(2) { david.projects.columns; david.projects.columns } end def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 2cc993b6ed..97bb631d2d 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -50,7 +50,7 @@ ensure end ActiveRecord::Base.connection.class.class_eval do - IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/] + IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/] # FIXME: this needs to be refactored so specific database can add their own # ignored SQL. This ignored SQL is for Oracle. diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 6f0f73e3bd..9d7c49768b 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -2022,7 +2022,10 @@ if ActiveRecord::Base.connection.supports_migrations? assert ! column(:name).default assert_equal :date, column(:birthdate).type - assert_queries(1) do + # One query for columns (delete_me table) + # One query for primary key (delete_me table) + # One query to do the bulk change + assert_queries(3) do with_bulk_change_table do |t| t.change :name, :string, :default => 'NONAME' t.change :birthdate, :datetime -- cgit v1.2.3 From c56e314866dff5d72c2a90e5e785c9a12d73d22b Mon Sep 17 00:00:00 2001 From: Brian Morearty Date: Sat, 5 Feb 2011 09:07:00 -0800 Subject: Updates to ActiveRecord::Timestamp documentation. Change ActiveRecord::Base.xyz to config.active_record.xyz in docs. Remove from code samples. Update skip_time_zone_conversion_for_attributes code sample: put the call in the model class. Clarify that skip_time_zone_conversion_for_attributes skips converion when reading. --- activerecord/lib/active_record/timestamp.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 65d9d1fb19..1511c71ffc 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -9,24 +9,26 @@ module ActiveRecord # # Timestamping can be turned off by setting: # - # ActiveRecord::Base.record_timestamps = false + # config.active_record.record_timestamps = false # # Timestamps are in the local timezone by default but you can use UTC by setting: # - # ActiveRecord::Base.default_timezone = :utc + # config.active_record.default_timezone = :utc # # == Time Zone aware attributes # # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code. # - # ActiveRecord::Base.time_zone_aware_attributes = true + # config.active_record.time_zone_aware_attributes = true # # This feature can easily be turned off by assigning value false . # - # If your attributes are time zone aware and you desire to skip time zone conversion for certain - # attributes then you can do following: + # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone + # when reading certain attributes then you can do following: # - # Topic.skip_time_zone_conversion_for_attributes = [:written_on] + # class Topic < ActiveRecord::Base + # self.skip_time_zone_conversion_for_attributes = [:written_on] + # end module Timestamp extend ActiveSupport::Concern -- cgit v1.2.3 From a3f5d7159d00a0c7c7d79d15652028ac13df30af Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 12 Jan 2011 15:18:45 -0800 Subject: fix db:fixtures:load with FIXTURES specified [#6061 state:resolved] Signed-off-by: Santiago Pastorino --- activerecord/lib/active_record/railties/databases.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 49d4b8f76b..ff36814684 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -296,8 +296,8 @@ db_namespace = namespace :db do base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir["#{fixtures_dir}/**/*.{yml,csv}"]).each do |fixture_file| - Fixtures.create_fixtures(fixtures_dir, fixture_file[(fixtures_dir.size + 1)..-5]) + (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.{yml,csv}"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| + Fixtures.create_fixtures(fixtures_dir, fixture_file) end end -- cgit v1.2.3 From 40aefb93018277ee7cafc5529b16d7b6df8aa4dd Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Mon, 7 Feb 2011 08:29:06 +0900 Subject: avoid nil.dup Signed-off-by: Santiago Pastorino --- .../lib/active_record/attribute_methods/time_zone_conversion.rb | 2 +- activerecord/test/cases/attribute_methods_test.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index a72eecb50e..76218d2a73 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -41,7 +41,7 @@ module ActiveRecord if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body, line = <<-EOV, __LINE__ + 1 def #{attr_name}=(original_time) - time = original_time.dup + time = original_time.dup unless original_time.nil? unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 7e3e204626..c343dd7918 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -460,6 +460,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_write_nil_to_time_attributes + in_time_zone "Pacific Time (US & Canada)" do + record = @target.new + record.written_on = nil + assert_nil record.written_on + end + end + def test_time_attributes_are_retrieved_in_current_time_zone in_time_zone "Pacific Time (US & Canada)" do utc_time = Time.utc(2008, 1, 1) -- cgit v1.2.3 From 65e08cfb4fdf30a38162477dc8b644c6fad74d93 Mon Sep 17 00:00:00 2001 From: Akira Matsuda Date: Mon, 7 Feb 2011 19:51:52 +0900 Subject: do not to_s where you are testing that a string value is stored for the before_type_cast Signed-off-by: Santiago Pastorino --- activerecord/test/cases/attribute_methods_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index c343dd7918..88eaea62e8 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -131,7 +131,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal developer.created_at, nil developer.created_at = "2010-03-21 21:23:32" - assert_equal developer.created_at_before_type_cast.to_s, "2010-03-21 21:23:32" + assert_equal developer.created_at_before_type_cast, "2010-03-21 21:23:32" assert_equal developer.created_at, Time.parse("2010-03-21 21:23:32") end -- cgit v1.2.3 From 0de661d6c74172a9fedcced6a4e99d007df953ef Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 7 Feb 2011 09:25:57 -0800 Subject: the connection pool caches table_exists? calls --- .../abstract/connection_pool.rb | 22 ++++++++++++++-------- activerecord/test/cases/associations/eager_test.rb | 2 ++ 2 files changed, 16 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 4475019e0e..b1754e61df 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -59,7 +59,7 @@ module ActiveRecord class ConnectionPool attr_accessor :automatic_reconnect attr_reader :spec, :connections - attr_reader :columns, :columns_hash, :primary_keys + attr_reader :columns, :columns_hash, :primary_keys, :tables # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification # object which describes database connection information (e.g. adapter, @@ -84,12 +84,7 @@ module ActiveRecord @connections = [] @checked_out = [] @automatic_reconnect = true - - @tables = Hash.new do |h, table_name| - with_connection do |conn| - h[table_name] = conn.table_exists?(table_name) - end - end + @tables = {} @columns = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| @@ -113,11 +108,22 @@ module ActiveRecord @primary_keys = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| - @tables[table_name] ? conn.primary_key(table_name) : 'id' + table_exists?(table_name) ? conn.primary_key(table_name) : 'id' end end end + # A cached lookup for table existence + def table_exists?(name) + return true if @tables.key? name + + with_connection do |conn| + conn.tables.each { |table| @tables[table] = true } + end + + @tables.key? name + end + # Clears out internal caches: # # * columns diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index e11f1009dc..3074648d59 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -851,6 +851,8 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_loading_with_conditions_on_join_model_preloads + Author.columns + authors = assert_queries(2) do Author.find(:all, :include => :author_address, :joins => :comments, :conditions => "posts.title like 'Welcome%'") end -- cgit v1.2.3 From 9f773d66b5c52510ff5fdd27238e1763d29bcb91 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 7 Feb 2011 14:34:39 -0800 Subject: mysql2 should log these sql statements --- activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index acf1832938..74dd7e8491 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -418,7 +418,7 @@ module ActiveRecord def columns(table_name, name = nil) sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] - result = execute(sql, :skip_logging) + result = execute(sql) result.each(:symbolize_keys => true, :as => :hash) { |field| columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") } -- cgit v1.2.3 From 1193709cd693099488353c20f2c7fadcf9cd6d05 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 7 Feb 2011 14:35:00 -0800 Subject: removing some freedom patches. use notification system to count sql queries --- activerecord/test/cases/helper.rb | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 97bb631d2d..499b30b4e8 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -49,28 +49,29 @@ ensure ActiveRecord::Base.default_timezone = old_zone end -ActiveRecord::Base.connection.class.class_eval do - IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/] +module ActiveRecord + class SQLCounter + IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/] - # FIXME: this needs to be refactored so specific database can add their own - # ignored SQL. This ignored SQL is for Oracle. - IGNORED_SQL.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from ((all|user)_tab_columns|(all|user)_triggers|(all|user)_constraints)/im] + # FIXME: this needs to be refactored so specific database can add their own + # ignored SQL. This ignored SQL is for Oracle. + IGNORED_SQL.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from ((all|user)_tab_columns|(all|user)_triggers|(all|user)_constraints)/im] - def execute_with_query_record(sql, name = nil, &block) - $queries_executed ||= [] - $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r } - execute_without_query_record(sql, name, &block) - end + def initialize + $queries_executed = [] + end - alias_method_chain :execute, :query_record + def call(name, start, finish, message_id, values) + sql = values[:sql] - def exec_query_with_query_record(sql, name = nil, binds = [], &block) - $queries_executed ||= [] - $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r } - exec_query_without_query_record(sql, name, binds, &block) + # FIXME: this seems bad. we should probably have a better way to indicate + # the query was cached + unless 'CACHE' == values[:name] + $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r } + end + end end - - alias_method_chain :exec_query, :query_record + ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) end ActiveRecord::Base.connection.class.class_eval { -- cgit v1.2.3 From 30bba95a048af7f64724fe2450ad14967581a456 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 7 Feb 2011 15:12:21 -0800 Subject: update ignored SQL for oracle --- activerecord/test/cases/helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 499b30b4e8..8c001096cc 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -55,7 +55,7 @@ module ActiveRecord # FIXME: this needs to be refactored so specific database can add their own # ignored SQL. This ignored SQL is for Oracle. - IGNORED_SQL.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from ((all|user)_tab_columns|(all|user)_triggers|(all|user)_constraints)/im] + IGNORED_SQL.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im] def initialize $queries_executed = [] -- cgit v1.2.3 From 5f1ea2a26b6e29f235e132d565b53f12e0234c66 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 7 Feb 2011 15:28:49 -0800 Subject: we do not use this method, so delete --- activerecord/test/cases/helper.rb | 12 ------------ 1 file changed, 12 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 8c001096cc..1019ea1dda 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -74,18 +74,6 @@ module ActiveRecord ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new) end -ActiveRecord::Base.connection.class.class_eval { - attr_accessor :column_calls - - def columns_with_calls(*args) - @column_calls ||= 0 - @column_calls += 1 - columns_without_calls(*args) - end - - alias_method_chain :columns, :calls -} - unless ENV['FIXTURE_DEBUG'] module ActiveRecord::TestFixtures::ClassMethods def try_to_load_dependency_with_silence(*args) -- cgit v1.2.3 From d55406d2e991056b08f69eb68bcf9b17da807b6c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 30 Jan 2011 19:07:08 +0000 Subject: Make record.association.destroy(*records) on habtm and hm:t only delete records in the join table. This is to make the destroy method more consistent across the different types of associations. For more details see the CHANGELOG entry. --- activerecord/CHANGELOG | 19 ++ .../associations/association_collection.rb | 8 +- .../has_and_belongs_to_many_association.rb | 2 +- .../associations/has_many_association.rb | 22 +- .../associations/has_many_through_association.rb | 21 +- .../has_and_belongs_to_many_associations_test.rb | 30 ++- .../has_many_through_associations_test.rb | 12 +- .../test/cases/autosave_association_test.rb | 241 +++++++++++++++------ 8 files changed, 248 insertions(+), 107 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index d1124801df..aca81b0077 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,24 @@ *Rails 3.1.0 (unreleased)* +* Changed the behaviour of association.destroy for has_and_belongs_to_many and has_many :through. + From now on, 'destroy' or 'delete' on an association will be taken to mean 'get rid of the link', + not (necessarily) 'get rid of the associated records'. + + Previously, has_and_belongs_to_many.destroy(*records) would destroy the records themselves. It + would not delete any records in the join table. Now, it deletes the records in the join table. + + Previously, has_many_through.destroy(*records) would destroy the records themselves, and the + records in the join table. [Note: This has not always been the case; previous version of Rails + only deleted the records themselves.] Now, it destroys only the records in the join table. + + Note that this change is backwards-incompatible to an extent, but there is unfortunately no + way to 'deprecate' it before changing it. The change is being made in order to have + consistency as to the meaning of 'destroy' or 'delete' across the different types of associations. + + If you wish to destroy the records themselves, you can do records.association.each(&:destroy) + + [Jon Leighton] + * Add :bulk => true option to change_table to make all the schema changes defined in change_table block using a single ALTER statement. [Pratik Naik] Example: diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 2811f53424..9bd59132f5 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -192,7 +192,7 @@ module ActiveRecord def destroy(*records) records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)} remove_records(records) do |_records, old_records| - old_records.each { |record| record.destroy } + delete_records(old_records, :destroy) if old_records.any? end load_target @@ -462,6 +462,12 @@ module ActiveRecord end end + # Delete the given records from the association, using one of the methods :destroy, + # :delete_all or :nullify. The default method used is given by the :dependent option. + def delete_records(records, method = @reflection.options[:dependent]) + raise NotImplementedError + end + def callback(method, record) callbacks_for(method).each do |callback| case callback diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index 3329a4af8e..d70f326e55 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -40,7 +40,7 @@ module ActiveRecord load_target.size end - def delete_records(records) + def delete_records(records, method = nil) if sql = @reflection.options[:delete_sql] records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } else diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index caefd14ee3..aff955dd5f 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -54,20 +54,20 @@ module ActiveRecord end # Deletes the records according to the :dependent option. - def delete_records(records) - case @reflection.options[:dependent] - when :destroy - records.each { |r| r.destroy } - when :delete_all - @reflection.klass.delete(records.map { |r| r.id }) - else - updates = { @reflection.foreign_key => nil } - conditions = { @reflection.association_primary_key => records.map { |r| r.id } } + def delete_records(records, method = @reflection.options[:dependent]) + case method + when :destroy + records.each { |r| r.destroy } + when :delete_all + @reflection.klass.delete(records.map { |r| r.id }) + else + updates = { @reflection.foreign_key => nil } + conditions = { @reflection.association_primary_key => records.map { |r| r.id } } - scoped.where(conditions).update_all(updates) + scoped.where(conditions).update_all(updates) end - if has_cached_counter? && @reflection.options[:dependent] != :destroy + if has_cached_counter? && method != :destroy @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) end end 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 d5b901beff..3174ea6373 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -8,13 +8,6 @@ module ActiveRecord alias_method :new, :build - def destroy(*records) - transaction do - delete_records(records.flatten) - super - end - end - # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been # loaded and calling collection.size if it has. If it's more likely than not that the collection does # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer @@ -51,10 +44,18 @@ module ActiveRecord end # TODO - add dependent option support - def delete_records(records) + def delete_records(records, method = @reflection.options[:dependent]) through_association = @owner.send(@reflection.through_reflection.name) - records.each do |associate| - through_association.where(construct_join_attributes(associate)).delete_all + + case method + when :destroy + records.each do |record| + through_association.where(construct_join_attributes(record)).destroy_all + end + else + records.each do |record| + through_association.where(construct_join_attributes(record)).delete_all + end end end 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 126b767d06..5dd2c9861e 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 @@ -372,27 +372,34 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_destroying david = Developer.find(1) - active_record = Project.find(1) + project = Project.find(1) david.projects.reload assert_equal 2, david.projects.size - assert_equal 3, active_record.developers.size + assert_equal 3, project.developers.size - assert_difference "Project.count", -1 do - david.projects.destroy(active_record) + assert_no_difference "Project.count" do + david.projects.destroy(project) end + join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id} AND project_id = #{project.id}") + assert join_records.empty? + assert_equal 1, david.reload.projects.size assert_equal 1, david.projects(true).size end - def test_destroying_array + def test_destroying_many david = Developer.find(1) david.projects.reload + projects = Project.all - assert_difference "Project.count", -Project.count do - david.projects.destroy(Project.find(:all)) + assert_no_difference "Project.count" do + david.projects.destroy(*projects) end + join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}") + assert join_records.empty? + assert_equal 0, david.reload.projects.size assert_equal 0, david.projects(true).size end @@ -401,7 +408,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david = Developer.find(1) david.projects.reload assert !david.projects.empty? - david.projects.destroy_all + + assert_no_difference "Project.count" do + david.projects.destroy_all + end + + join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}") + assert join_records.empty? + assert david.projects.empty? assert david.projects(true).empty? end 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 96f4597726..6830478107 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -128,8 +128,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_destroy_association - assert_difference ["Person.count", "Reader.count"], -1 do - posts(:welcome).people.destroy(people(:michael)) + assert_no_difference "Person.count" do + assert_difference "Reader.count", -1 do + posts(:welcome).people.destroy(people(:michael)) + end end assert posts(:welcome).reload.people.empty? @@ -137,8 +139,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_destroy_all - assert_difference ["Person.count", "Reader.count"], -1 do - posts(:welcome).people.destroy_all + assert_no_difference "Person.count" do + assert_difference "Reader.count", -1 do + posts(:welcome).people.destroy_all + end end assert posts(:welcome).reload.people.empty? diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 11c0c5b0ef..c2f2609c9e 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -689,110 +689,207 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase assert_equal 'NewName', @parrot.reload.name end - # has_many & has_and_belongs_to - %w{ parrots birds }.each do |association_name| - define_method("test_should_destroy_#{association_name}_as_part_of_the_save_transaction_if_they_were_marked_for_destroyal") do - 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") } + def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction + 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") } - assert !@pirate.send(association_name).any? { |child| child.marked_for_destruction? } + assert !@pirate.birds.any? { |child| child.marked_for_destruction? } - @pirate.send(association_name).each { |child| child.mark_for_destruction } - klass = @pirate.send(association_name).first.class - ids = @pirate.send(association_name).map(&:id) + @pirate.birds.each { |child| child.mark_for_destruction } + klass = @pirate.birds.first.class + ids = @pirate.birds.map(&:id) - assert @pirate.send(association_name).all? { |child| child.marked_for_destruction? } - ids.each { |id| assert klass.find_by_id(id) } + assert @pirate.birds.all? { |child| child.marked_for_destruction? } + ids.each { |id| assert klass.find_by_id(id) } - @pirate.save - assert @pirate.reload.send(association_name).empty? - ids.each { |id| assert_nil klass.find_by_id(id) } + @pirate.save + assert @pirate.reload.birds.empty? + ids.each { |id| assert_nil klass.find_by_id(id) } + end + + def test_should_skip_validation_on_has_many_if_marked_for_destruction + 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") } + + @pirate.birds.each { |bird| bird.name = '' } + assert !@pirate.valid? + + @pirate.birds.each do |bird| + bird.mark_for_destruction + bird.expects(:valid?).never end + assert_difference("Bird.count", -2) { @pirate.save! } + end + + def test_should_skip_validation_on_has_many_if_destroyed + @pirate.birds.create!(:name => "birds_1") - define_method("test_should_skip_validation_on_the_#{association_name}_association_if_marked_for_destruction") do - 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") } - children = @pirate.send(association_name) + @pirate.birds.each { |bird| bird.name = '' } + assert !@pirate.valid? + + @pirate.birds.each { |bird| bird.destroy } + assert @pirate.valid? + end - children.each { |child| child.name = '' } - assert !@pirate.valid? + def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many + @pirate.birds.create!(:name => "birds_1") + + @pirate.birds.each { |bird| bird.mark_for_destruction } + assert @pirate.save + + @pirate.birds.each { |bird| bird.expects(:destroy).never } + assert @pirate.save + end - children.each do |child| - child.mark_for_destruction - child.expects(:valid?).never + def test_should_rollback_destructions_if_an_exception_occurred_while_saving_has_many + 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") } + before = @pirate.birds.map { |c| c.mark_for_destruction ; c } + + # Stub the destroy method of the the second child to raise an exception + class << before.last + def destroy(*args) + super + raise 'Oh noes!' end - assert_difference("#{association_name.classify}.count", -2) { @pirate.save! } end - define_method("test_should_skip_validation_on_the_#{association_name}_association_if_destroyed") do - @pirate.send(association_name).create!(:name => "#{association_name}_1") - children = @pirate.send(association_name) + assert_raise(RuntimeError) { assert !@pirate.save } + assert_equal before, @pirate.reload.birds + end + + # Add and remove callbacks tests for association collections. + %w{ method proc }.each do |callback_type| + define_method("test_should_run_add_callback_#{callback_type}s_for_has_many") do + association_name_with_callbacks = "birds_with_#{callback_type}_callbacks" + + pirate = Pirate.new(:catchphrase => "Arr") + pirate.send(association_name_with_callbacks).build(:name => "Crowe the One-Eyed") - children.each { |child| child.name = '' } - assert !@pirate.valid? + expected = [ + "before_adding_#{callback_type}_bird_", + "after_adding_#{callback_type}_bird_" + ] - children.each { |child| child.destroy } - assert @pirate.valid? + assert_equal expected, pirate.ship_log end - define_method("test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_#{association_name}") do - @pirate.send(association_name).create!(:name => "#{association_name}_1") - children = @pirate.send(association_name) + define_method("test_should_run_remove_callback_#{callback_type}s_for_has_many") do + association_name_with_callbacks = "birds_with_#{callback_type}_callbacks" - children.each { |child| child.mark_for_destruction } - assert @pirate.save - children.each { |child| child.expects(:destroy).never } - assert @pirate.save + @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed") + @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction } + child_id = @pirate.send(association_name_with_callbacks).first.id + + @pirate.ship_log.clear + @pirate.save + + expected = [ + "before_removing_#{callback_type}_bird_#{child_id}", + "after_removing_#{callback_type}_bird_#{child_id}" + ] + + assert_equal expected, @pirate.ship_log end + end - define_method("test_should_rollback_destructions_if_an_exception_occurred_while_saving_#{association_name}") do - 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") } - before = @pirate.send(association_name).map { |c| c.mark_for_destruction ; c } + def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction + 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") } - # Stub the destroy method of the the second child to raise an exception - class << before.last - def destroy(*args) - super - raise 'Oh noes!' - end - end + assert !@pirate.parrots.any? { |parrot| parrot.marked_for_destruction? } + @pirate.parrots.each { |parrot| parrot.mark_for_destruction } - assert_raise(RuntimeError) { assert !@pirate.save } - assert_equal before, @pirate.reload.send(association_name) + assert_no_difference "Parrot.count" do + @pirate.save end - # Add and remove callbacks tests for association collections. - %w{ method proc }.each do |callback_type| - define_method("test_should_run_add_callback_#{callback_type}s_for_#{association_name}") do - association_name_with_callbacks = "#{association_name}_with_#{callback_type}_callbacks" + assert @pirate.reload.parrots.empty? - pirate = Pirate.new(:catchphrase => "Arr") - pirate.send(association_name_with_callbacks).build(:name => "Crowe the One-Eyed") + join_records = Pirate.connection.select_all("SELECT * FROM parrots_pirates WHERE pirate_id = #{@pirate.id}") + assert join_records.empty? + end - expected = [ - "before_adding_#{callback_type}_#{association_name.singularize}_", - "after_adding_#{callback_type}_#{association_name.singularize}_" - ] + def test_should_skip_validation_on_habtm_if_marked_for_destruction + 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") } - assert_equal expected, pirate.ship_log - end + @pirate.parrots.each { |parrot| parrot.name = '' } + assert !@pirate.valid? + + @pirate.parrots.each do |parrot| + parrot.mark_for_destruction + parrot.expects(:valid?).never + end - define_method("test_should_run_remove_callback_#{callback_type}s_for_#{association_name}") do - association_name_with_callbacks = "#{association_name}_with_#{callback_type}_callbacks" + @pirate.save! + assert @pirate.reload.parrots.empty? + end + + def test_should_skip_validation_on_habtm_if_destroyed + @pirate.parrots.create!(:name => "parrots_1") - @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed") - @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction } - child_id = @pirate.send(association_name_with_callbacks).first.id + @pirate.parrots.each { |parrot| parrot.name = '' } + assert !@pirate.valid? - @pirate.ship_log.clear - @pirate.save + @pirate.parrots.each { |parrot| parrot.destroy } + assert @pirate.valid? + end + + def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm + @pirate.parrots.create!(:name => "parrots_1") + + @pirate.parrots.each { |parrot| parrot.mark_for_destruction } + assert @pirate.save + + assert_queries(1) do + assert @pirate.save + end + end - expected = [ - "before_removing_#{callback_type}_#{association_name.singularize}_#{child_id}", - "after_removing_#{callback_type}_#{association_name.singularize}_#{child_id}" - ] + def test_should_rollback_destructions_if_an_exception_occurred_while_saving_habtm + 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") } + before = @pirate.parrots.map { |c| c.mark_for_destruction ; c } - assert_equal expected, @pirate.ship_log + class << @pirate.parrots + def destroy(*args) + super + raise 'Oh noes!' end end + + assert_raise(RuntimeError) { assert !@pirate.save } + assert_equal before, @pirate.reload.parrots + end + + # Add and remove callbacks tests for association collections. + %w{ method proc }.each do |callback_type| + define_method("test_should_run_add_callback_#{callback_type}s_for_habtm") do + association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks" + + pirate = Pirate.new(:catchphrase => "Arr") + pirate.send(association_name_with_callbacks).build(:name => "Crowe the One-Eyed") + + expected = [ + "before_adding_#{callback_type}_parrot_", + "after_adding_#{callback_type}_parrot_" + ] + + assert_equal expected, pirate.ship_log + end + + define_method("test_should_run_remove_callback_#{callback_type}s_for_habtm") do + association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks" + + @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed") + @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction } + child_id = @pirate.send(association_name_with_callbacks).first.id + + @pirate.ship_log.clear + @pirate.save + + expected = [ + "before_removing_#{callback_type}_parrot_#{child_id}", + "after_removing_#{callback_type}_parrot_#{child_id}" + ] + + assert_equal expected, @pirate.ship_log + end end end -- cgit v1.2.3 From 05bcb8cecc8573f28ad080839233b4bb9ace07be Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Tue, 1 Feb 2011 22:56:04 +0000 Subject: Support the :dependent option on has_many :through associations. For historical and practical reasons, :delete_all is the default deletion strategy employed by association.delete(*records), despite the fact that the default strategy is :nullify for regular has_many. Also, this only works at all if the source reflection is a belongs_to. For other situations, you should directly modify the through association. --- activerecord/CHANGELOG | 8 ++ activerecord/lib/active_record/associations.rb | 3 +- .../associations/has_many_through_association.rb | 17 ++-- .../associations/through_association.rb | 33 +++++-- .../has_many_through_associations_test.rb | 100 +++++++++++++++++++++ activerecord/test/models/person.rb | 27 +++++- activerecord/test/models/reference.rb | 12 +++ activerecord/test/schema/schema.rb | 1 + 8 files changed, 182 insertions(+), 19 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index aca81b0077..e49915a8cd 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,13 @@ *Rails 3.1.0 (unreleased)* +* Support the :dependent option on has_many :through associations. For historical and practical + reasons, :delete_all is the default deletion strategy employed by association.delete(*records), + despite the fact that the default strategy is :nullify for regular has_many. Also, this only + works at all if the source reflection is a belongs_to. For other situations, you should directly + modify the through association. + + [Jon Leighton] + * Changed the behaviour of association.destroy for has_and_belongs_to_many and has_many :through. From now on, 'destroy' or 'delete' on an association will be taken to mean 'get rid of the link', not (necessarily) 'get rid of the associated records'. diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 398936b3d8..b90838a52b 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1606,12 +1606,11 @@ module ActiveRecord send(reflection.name).each do |o| # No point in executing the counter update since we're going to destroy the parent anyway counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym - if(o.respond_to? counter_method) then + if o.respond_to?(counter_method) class << o self end.send(:define_method, counter_method, Proc.new {}) end - o.destroy end end 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 3174ea6373..c98ac79dc0 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -43,19 +43,18 @@ module ActiveRecord end end - # TODO - add dependent option support - def delete_records(records, method = @reflection.options[:dependent]) - through_association = @owner.send(@reflection.through_reflection.name) + def deletion_scope(records) + @owner.send(@reflection.through_reflection.name).where(construct_join_attributes(*records)) + end + def delete_records(records, method = @reflection.options[:dependent]) case method when :destroy - records.each do |record| - through_association.where(construct_join_attributes(record)).destroy_all - end + deletion_scope(records).destroy_all + when :nullify + deletion_scope(records).update_all(@reflection.source_reflection.foreign_key => nil) else - records.each do |record| - through_association.where(construct_join_attributes(record)).delete_all - end + deletion_scope(records).delete_all end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index c840a16160..4ae0669c96 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -74,21 +74,40 @@ module ActiveRecord right.create_on(right.create_and(conditions))) end - # Construct attributes for :through pointing to owner and associate. - def construct_join_attributes(associate) - # TODO: revisit this to allow it for deletion, supposing dependent option is supported - raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro) + # Construct attributes for :through pointing to owner and associate. This is used by the + # methods which create and delete records on the association. + # + # We only support indirectly modifying through associations which has a belongs_to source. + # This is the "has_many :tags, :through => :taggings" situation, where the join model + # typically has a belongs_to on both side. In other words, associations which could also + # be represented as has_and_belongs_to_many associations. + # + # We do not support creating/deleting records on the association where the source has + # some other type, because this opens up a whole can of worms, and in basically any + # situation it is more natural for the user to just create or modify their join records + # directly as required. + def construct_join_attributes(*records) + if @reflection.source_reflection.macro != :belongs_to + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) + end join_attributes = { @reflection.source_reflection.foreign_key => - associate.send(@reflection.source_reflection.association_primary_key) + records.map { |record| + record.send(@reflection.source_reflection.association_primary_key) + } } if @reflection.options[:source_type] - join_attributes.merge!(@reflection.source_reflection.foreign_type => associate.class.base_class.name) + join_attributes[@reflection.source_reflection.foreign_type] = + records.map { |record| record.class.base_class.name } end - join_attributes + if records.count == 1 + Hash[join_attributes.map { |k, v| [k, v.first] }] + else + join_attributes + end end # The reason that we are operating directly on the scope here (rather than passing 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 6830478107..2aaf5750ba 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -155,6 +155,106 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_delete_through_belongs_to_with_dependent_nullify + Reference.make_comments = true + + person = people(:michael) + job = jobs(:magician) + reference = Reference.where(:job_id => job.id, :person_id => person.id).first + + assert_no_difference ['Job.count', 'Reference.count'] do + assert_difference 'person.jobs.count', -1 do + person.jobs_with_dependent_nullify.delete(job) + end + end + + assert_equal nil, reference.reload.job_id + ensure + Reference.make_comments = false + end + + def test_delete_through_belongs_to_with_dependent_delete_all + Reference.make_comments = true + + person = people(:michael) + job = jobs(:magician) + + # Make sure we're not deleting everything + assert person.jobs.count >= 2 + + assert_no_difference 'Job.count' do + assert_difference ['person.jobs.count', 'Reference.count'], -1 do + person.jobs_with_dependent_delete_all.delete(job) + end + end + + # Check that the destroy callback on Reference did not run + assert_equal nil, person.reload.comments + ensure + Reference.make_comments = false + end + + def test_delete_through_belongs_to_with_dependent_destroy + Reference.make_comments = true + + person = people(:michael) + job = jobs(:magician) + + # Make sure we're not deleting everything + assert person.jobs.count >= 2 + + assert_no_difference 'Job.count' do + assert_difference ['person.jobs.count', 'Reference.count'], -1 do + person.jobs_with_dependent_destroy.delete(job) + end + end + + # Check that the destroy callback on Reference ran + assert_equal "Reference destroyed", person.reload.comments + ensure + Reference.make_comments = false + end + + def test_belongs_to_with_dependent_destroy + person = PersonWithDependentDestroyJobs.find(1) + + # Create a reference which is not linked to a job. This should not be destroyed. + person.references.create! + + assert_no_difference 'Job.count' do + assert_difference 'Reference.count', -person.jobs.count do + person.destroy + end + end + end + + def test_belongs_to_with_dependent_delete_all + person = PersonWithDependentDeleteAllJobs.find(1) + + # Create a reference which is not linked to a job. This should not be destroyed. + person.references.create! + + assert_no_difference 'Job.count' do + assert_difference 'Reference.count', -person.jobs.count do + person.destroy + end + end + end + + def test_belongs_to_with_dependent_nullify + person = PersonWithDependentNullifyJobs.find(1) + + references = person.references.to_a + + assert_no_difference ['Reference.count', 'Job.count'] do + person.destroy + end + + references.each do |reference| + assert_equal nil, reference.reload.job_id + end + end + def test_replace_association assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)} diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index bee89de042..a18b9e44df 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -6,10 +6,14 @@ class Person < ActiveRecord::Base has_many :references has_many :bad_references has_many :fixed_bad_references, :conditions => { :favourite => true }, :class_name => 'BadReference' - has_many :jobs, :through => :references has_one :favourite_reference, :class_name => 'Reference', :conditions => ['favourite=?', true] has_many :posts_with_comments_sorted_by_comment_id, :through => :readers, :source => :post, :include => :comments, :order => 'comments.id' + has_many :jobs, :through => :references + has_many :jobs_with_dependent_destroy, :source => :job, :through => :references, :dependent => :destroy + has_many :jobs_with_dependent_delete_all, :source => :job, :through => :references, :dependent => :delete_all + has_many :jobs_with_dependent_nullify, :source => :job, :through => :references, :dependent => :nullify + belongs_to :primary_contact, :class_name => 'Person' has_many :agents, :class_name => 'Person', :foreign_key => 'primary_contact_id' has_many :agents_of_agents, :through => :agents, :source => :agents @@ -18,3 +22,24 @@ class Person < ActiveRecord::Base scope :males, :conditions => { :gender => 'M' } scope :females, :conditions => { :gender => 'F' } end + +class PersonWithDependentDestroyJobs < ActiveRecord::Base + self.table_name = 'people' + + has_many :references, :foreign_key => :person_id + has_many :jobs, :source => :job, :through => :references, :dependent => :destroy +end + +class PersonWithDependentDeleteAllJobs < ActiveRecord::Base + self.table_name = 'people' + + has_many :references, :foreign_key => :person_id + has_many :jobs, :source => :job, :through => :references, :dependent => :delete_all +end + +class PersonWithDependentNullifyJobs < ActiveRecord::Base + self.table_name = 'people' + + has_many :references, :foreign_key => :person_id + has_many :jobs, :source => :job, :through => :references, :dependent => :nullify +end diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index 4a17c936f5..06c4f79ef3 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -1,6 +1,18 @@ class Reference < ActiveRecord::Base belongs_to :person belongs_to :job + + class << self + attr_accessor :make_comments + end + + before_destroy :make_comments + + def make_comments + if self.class.make_comments + person.update_attributes :comments => "Reference destroyed" + end + end end class BadReference < ActiveRecord::Base diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 326c336317..09c7b7ba63 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -425,6 +425,7 @@ ActiveRecord::Schema.define do t.string :gender, :limit => 1 t.references :number1_fan t.integer :lock_version, :null => false, :default => 0 + t.string :comments end create_table :pets, :primary_key => :pet_id ,:force => true do |t| -- cgit v1.2.3 From 52f09eac5b3d297021ef726e04ec19f6011cb302 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 5 Feb 2011 13:13:49 +0000 Subject: Correctly update counter caches on deletion for has_many :through [#2824 state:resolved]. Also fixed a bunch of other counter cache bugs in the process, as once I fixed this one others started appearing like nobody's business. --- .../associations/has_many_association.rb | 54 ++++++++++++++++------ .../associations/has_many_through_association.rb | 29 ++++++++++-- .../associations/has_many_associations_test.rb | 22 +++++---- .../has_many_through_associations_test.rb | 44 +++++++++++++++++- activerecord/test/fixtures/posts.yml | 2 + activerecord/test/models/post.rb | 5 ++ activerecord/test/schema/schema.rb | 4 ++ 7 files changed, 130 insertions(+), 30 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index aff955dd5f..732f63713e 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -45,30 +45,54 @@ module ActiveRecord [@reflection.options[:limit], count].compact.min end - def has_cached_counter? - @owner.attribute_present?(cached_counter_attribute_name) + def has_cached_counter?(reflection = @reflection) + @owner.attribute_present?(cached_counter_attribute_name(reflection)) end - def cached_counter_attribute_name - "#{@reflection.name}_count" + def cached_counter_attribute_name(reflection = @reflection) + "#{reflection.name}_count" + end + + def update_counter(difference, reflection = @reflection) + if has_cached_counter?(reflection) + counter = cached_counter_attribute_name(reflection) + @owner.class.update_counters(@owner.id, counter => difference) + @owner[counter] += difference + @owner.changed_attributes.delete(counter) # eww + end + end + + # This shit is nasty. We need to avoid the following situation: + # + # * An associated record is deleted via record.destroy + # * Hence the callbacks run, and they find a belongs_to on the record with a + # :counter_cache options which points back at our @owner. So they update the + # counter cache. + # * In which case, we must make sure to *not* update the counter cache, or else + # it will be decremented twice. + # + # Hence this method. + def inverse_updates_counter_cache?(reflection = @reflection) + counter_name = cached_counter_attribute_name(reflection) + reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection| + inverse_reflection.counter_cache_column == counter_name + } end # Deletes the records according to the :dependent option. def delete_records(records, method = @reflection.options[:dependent]) - case method - when :destroy + if method == :destroy records.each { |r| r.destroy } - when :delete_all - @reflection.klass.delete(records.map { |r| r.id }) + update_counter(-records.length) unless inverse_updates_counter_cache? else - updates = { @reflection.foreign_key => nil } - conditions = { @reflection.association_primary_key => records.map { |r| r.id } } - - scoped.where(conditions).update_all(updates) - end + keys = records.map { |r| r[@reflection.association_primary_key] } + scope = scoped.where(@reflection.association_primary_key => keys) - if has_cached_counter? && method != :destroy - @owner.class.update_counters(@owner.id, cached_counter_attribute_name => -records.size) + if method == :delete_all + update_counter(-scope.delete_all) + else + update_counter(-scope.update_all(@reflection.foreign_key => nil)) + end end end 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 c98ac79dc0..040de7e98f 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -31,6 +31,9 @@ module ActiveRecord through_association = @owner.send(@reflection.through_reflection.name) through_association.create!(construct_join_attributes(record)) + + update_counter(1) + true end private @@ -43,19 +46,35 @@ module ActiveRecord end end - def deletion_scope(records) - @owner.send(@reflection.through_reflection.name).where(construct_join_attributes(*records)) + def update_through_counter?(method) + case method + when :destroy + !inverse_updates_counter_cache?(@reflection.through_reflection) + when :nullify + false + else + true + end end def delete_records(records, method = @reflection.options[:dependent]) + through = @owner.send(:association_proxy, @reflection.through_reflection.name) + scope = through.scoped.where(construct_join_attributes(*records)) + case method when :destroy - deletion_scope(records).destroy_all + count = scope.destroy_all.length when :nullify - deletion_scope(records).update_all(@reflection.source_reflection.foreign_key => nil) + count = scope.update_all(@reflection.source_reflection.foreign_key => nil) else - deletion_scope(records).delete_all + count = scope.delete_all end + + if @reflection.through_reflection.macro == :has_many && update_through_counter?(method) + update_counter(-count, @reflection.through_reflection) + end + + update_counter(-count) end def find_target diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index e36124a055..23b777ac6d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -630,7 +630,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal topic.replies.to_a.size, topic.replies_count end - def test_deleting_updates_counter_cache_without_dependent_destroy + def test_deleting_updates_counter_cache_without_dependent_option post = posts(:welcome) assert_difference "post.reload.taggings_count", -1 do @@ -640,16 +640,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_delete_all post = posts(:welcome) - - # Manually update the count as the tagging will have been added to the taggings association, - # rather than to the taggings_with_delete_all one (which is just a 'shadow' of the former) - post.update_attribute(:taggings_with_delete_all_count, post.taggings_with_delete_all.to_a.count) + post.update_attribute(:taggings_with_delete_all_count, post.taggings_count) assert_difference "post.reload.taggings_with_delete_all_count", -1 do post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first) end end + def test_deleting_updates_counter_cache_with_dependent_destroy + post = posts(:welcome) + post.update_attribute(:taggings_with_destroy_count, post.taggings_count) + + assert_difference "post.reload.taggings_with_destroy_count", -1 do + post.taggings_with_destroy.delete(post.taggings_with_destroy.first) + end + end + def test_deleting_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") @@ -701,9 +707,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_clearing_updates_counter_cache topic = Topic.first - topic.replies.clear - topic.reload - assert_equal 0, topic.replies_count + assert_difference 'topic.reload.replies_count', -1 do + topic.replies.clear + end end def test_clearing_a_dependent_association_collection 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 2aaf5750ba..c58068ef75 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -25,8 +25,8 @@ require 'models/membership' require 'models/club' class HasManyThroughAssociationsTest < ActiveRecord::TestCase - fixtures :posts, :readers, :people, :comments, :authors, :categories, - :owners, :pets, :toys, :jobs, :references, :companies, :members, + fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, + :owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses, :subscribers, :books, :subscriptions, :developers, :categorizations # Dummies to force column loads so query counts are clean. @@ -255,6 +255,37 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end + def test_update_counter_caches_on_delete + post = posts(:welcome) + tag = post.tags.create!(:name => 'doomed') + + assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do + posts(:welcome).tags.delete(tag) + end + end + + def test_update_counter_caches_on_delete_with_dependent_destroy + post = posts(:welcome) + tag = post.tags.create!(:name => 'doomed') + post.update_attribute(:tags_with_destroy_count, post.tags.count) + + assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do + posts(:welcome).tags_with_destroy.delete(tag) + end + end + + def test_update_counter_caches_on_delete_with_dependent_nullify + post = posts(:welcome) + tag = post.tags.create!(:name => 'doomed') + post.update_attribute(:tags_with_nullify_count, post.tags.count) + + assert_no_difference 'post.reload.taggings_count' do + assert_difference 'post.reload.tags_with_nullify_count', -1 do + posts(:welcome).tags_with_nullify.delete(tag) + end + end + end + def test_replace_association assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)} @@ -671,4 +702,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal true, club.reload.membership.favourite end + + def test_deleting_from_has_many_through_a_belongs_to_should_not_try_to_update_counter + post = posts(:welcome) + address = author_addresses(:david_address) + + assert post.author_addresses.include?(address) + post.author_addresses.delete(address) + assert post[:author_count].nil? + end end diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml index f817493190..07069a064f 100644 --- a/activerecord/test/fixtures/posts.yml +++ b/activerecord/test/fixtures/posts.yml @@ -5,6 +5,7 @@ welcome: body: Such a lovely day comments_count: 2 taggings_count: 1 + tags_count: 1 type: Post thinking: @@ -14,6 +15,7 @@ thinking: body: Like I hopefully always am comments_count: 1 taggings_count: 1 + tags_count: 1 type: SpecialPost authorless: diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 1c95d30d6b..fd8cd2244a 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -39,6 +39,7 @@ class Post < ActiveRecord::Base has_many :author_favorites, :through => :author has_many :author_categorizations, :through => :author, :source => :categorizations + has_many :author_addresses, :through => :author has_one :very_special_comment has_one :very_special_comment_with_post, :class_name => "VerySpecialComment", :include => :post @@ -57,6 +58,10 @@ class Post < ActiveRecord::Base end has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all + has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy + + has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy + has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify has_many :misc_tags, :through => :taggings, :source => :tag, :conditions => "tags.name = 'Misc'" has_many :funky_tags, :through => :taggings, :source => :tag diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 09c7b7ba63..665a4fe914 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -455,6 +455,10 @@ ActiveRecord::Schema.define do t.integer :comments_count, :default => 0 t.integer :taggings_count, :default => 0 t.integer :taggings_with_delete_all_count, :default => 0 + t.integer :taggings_with_destroy_count, :default => 0 + t.integer :tags_count, :default => 0 + t.integer :tags_with_destroy_count, :default => 0 + t.integer :tags_with_nullify_count, :default => 0 end create_table :price_estimates, :force => true do |t| -- cgit v1.2.3 From d9870d92f733f0ef4452a0b6df338ed9dbcc05b3 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sat, 5 Feb 2011 13:18:27 +0000 Subject: This string should continue --- activerecord/lib/active_record/associations/association_collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 9bd59132f5..acb9fe7ff8 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -290,7 +290,7 @@ module ActiveRecord unless concat(other_array - @target) @target = original_target - raise RecordNotSaved, "Failed to replace #{@reflection.name} because one or more of the " + raise RecordNotSaved, "Failed to replace #{@reflection.name} because one or more of the " \ "new records could not be saved." end end -- cgit v1.2.3 From e62b57647258fad34129975c5a264d19af2dbbe8 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Sun, 6 Feb 2011 22:14:16 +0000 Subject: Refactor the implementations of AssociatioCollection#delete and #destroy to be more consistent with each other, and to stop passing blocks around, thus making the execution easier to follow. --- .../associations/association_collection.rb | 26 +++++++++------------- .../has_and_belongs_to_many_association.rb | 2 +- .../associations/has_many_association.rb | 2 +- .../associations/has_many_through_association.rb | 2 +- .../test/cases/autosave_association_test.rb | 2 +- 5 files changed, 15 insertions(+), 19 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index acb9fe7ff8..3e3237c348 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -178,10 +178,7 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - remove_records(records) do |_records, old_records| - delete_records(old_records) if old_records.any? - _records.each { |record| @target.delete(record) } - end + delete_or_destroy(records, @reflection.options[:dependent]) end # Destroy +records+ and remove them from this association calling @@ -190,12 +187,8 @@ module ActiveRecord # Note that this method will _always_ remove records from the database # ignoring the +:dependent+ option. def destroy(*records) - records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)} - remove_records(records) do |_records, old_records| - delete_records(old_records, :destroy) if old_records.any? - end - - load_target + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + delete_or_destroy(records, :destroy) end def create(attrs = {}) @@ -450,21 +443,24 @@ module ActiveRecord add_record_to_target_with_callbacks(record, &block) end - def remove_records(*records) + def delete_or_destroy(records, method) records = records.flatten records.each { |record| raise_on_type_mismatch(record) } + existing_records = records.reject { |r| r.new_record? } transaction do records.each { |record| callback(:before_remove, record) } - old_records = records.reject { |r| r.new_record? } - yield(records, old_records) + + delete_records(existing_records, method) if existing_records.any? + records.each { |record| @target.delete(record) } + records.each { |record| callback(:after_remove, record) } end end # Delete the given records from the association, using one of the methods :destroy, - # :delete_all or :nullify. The default method used is given by the :dependent option. - def delete_records(records, method = @reflection.options[:dependent]) + # :delete_all or :nullify (or nil, in which case a default is used). + def delete_records(records, method) raise NotImplementedError end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index d70f326e55..b0ccab2de6 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -40,7 +40,7 @@ module ActiveRecord load_target.size end - def delete_records(records, method = nil) + def delete_records(records, method) if sql = @reflection.options[:delete_sql] records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } else diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 732f63713e..0b246587c0 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -80,7 +80,7 @@ module ActiveRecord end # Deletes the records according to the :dependent option. - def delete_records(records, method = @reflection.options[:dependent]) + def delete_records(records, method) if method == :destroy records.each { |r| r.destroy } update_counter(-records.length) unless inverse_updates_counter_cache? 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 040de7e98f..81dd373f7b 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -57,7 +57,7 @@ module ActiveRecord end end - def delete_records(records, method = @reflection.options[:dependent]) + def delete_records(records, method) through = @owner.send(:association_proxy, @reflection.through_reflection.name) scope = through.scoped.where(construct_join_attributes(*records)) diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index c2f2609c9e..dd2ce786aa 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -837,7 +837,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.parrots.each { |parrot| parrot.mark_for_destruction } assert @pirate.save - assert_queries(1) do + assert_no_queries do assert @pirate.save end end -- cgit v1.2.3 From 4f7bdc8f74d48300a6dc13413a43e65dca5c8384 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 7 Feb 2011 00:00:15 +0000 Subject: Documentation for recent refinements to association deletion --- activerecord/lib/active_record/associations.rb | 78 +++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b90838a52b..a2db592c7d 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -232,10 +232,9 @@ module ActiveRecord # others.empty? | X | X | X # others.clear | X | X | X # others.delete(other,other,...) | X | X | X - # others.delete_all | X | X | + # others.delete_all | X | X | X # others.destroy_all | X | X | X # others.find(*args) | X | X | X - # others.find_first | X | | # others.exists? | X | X | X # others.uniq | X | X | X # others.reset | X | X | X @@ -833,6 +832,73 @@ module ActiveRecord # * does not work with :polymorphic associations. # * for +belongs_to+ associations +has_many+ inverse associations are ignored. # + # == Deleting from associations + # + # === Dependent associations + # + # +has_many+, +has_one+ and +belongs_to+ associations support the :dependent option. + # This allows you to specify that associated records should be deleted when the owner is + # deleted. + # + # For example: + # + # class Author + # has_many :posts, :dependent => :destroy + # end + # Author.find(1).destroy # => Will destroy all of the author's posts, too + # + # The :dependent option can have different values which specify how the deletion + # is done. For more information, see the documentation for this option on the different + # specific association types. + # + # === Delete or destroy? + # + # +has_many+ and +has_and_belongs_to_many+ associations have the methods destroy, + # delete, destroy_all and delete_all. + # + # For +has_and_belongs_to_many+, delete and destroy are the same: they + # cause the records in the join table to be removed. + # + # For +has_many+, destroy will always call the destroy method of the + # record(s) being removed so that callbacks are run. However delete will either + # do the deletion according to the strategy specified by the :dependent option, or + # if no :dependent option is given, then it will follow the default strategy. + # The default strategy is :nullify (set the foreign keys to nil), except for + # +has_many+ :through, where the default strategy is delete_all (delete + # the join records, without running their callbacks). + # + # There is also a clear method which is the same as delete_all, except that + # it returns the association rather than the records which have been deleted. + # + # === What gets deleted? + # + # There is a potential pitfall here: +has_and_belongs_to_many+ and +has_many+ :through + # associations have records in join tables, as well as the associated records. So when we + # call one of these deletion methods, what exactly should be deleted? + # + # The answer is that it is assumed that deletion on an association is about removing the + # link between the owner and the associated object(s), rather than necessarily the + # associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+ + # :through, the join records will be deleted, but the associated records won't. + # + # This makes sense if you think about it: if you were to call post.tags.delete(Tag.find_by_name('food')) + # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself + # to be removed from the database. + # + # However, there are examples where this strategy doesn't make sense. For example, suppose + # a person has many projects, and each project has many tasks. If we deleted one of a person's + # tasks, we would probably not want the project to be deleted. In this scenario, the delete method + # won't actually work: it can only be used if the association on the join model is a + # +belongs_to+. In other situations you are expected to perform operations directly on + # either the associated records or the :through association. + # + # With a regular +has_many+ there is no distinction between the "associated records" + # and the "link", so there is only one choice for what gets deleted. + # + # With +has_and_belongs_to_many+ and +has_many+ :through, if you want to delete the + # associated records themselves, you can always do something along the lines of + # person.tasks.each(&:destroy). + # # == Type safety with ActiveRecord::AssociationTypeMismatch # # If you attempt to assign an object to an association that doesn't match the inferred @@ -857,6 +923,10 @@ module ActiveRecord # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # Objects will be in addition destroyed if they're associated with :dependent => :destroy, # and deleted if they're associated with :dependent => :delete_all. + # + # If the :through option is used, then the join records are deleted (rather than + # nullified) by default, but you can specify :dependent => :destroy or + # :dependent => :nullify to override this. # [collection=objects] # Replaces the collections content by deleting and adding objects as appropriate. If the :through # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is @@ -940,7 +1010,9 @@ module ActiveRecord # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to # :restrict this object cannot be deleted if it has any associated object. # - # *Warning:* This option is ignored when used with :through option. + # If using with the :through option, the association on the join model must be + # a +belongs_to+, and the records which get deleted are the join records, rather than + # the associated records. # # [:finder_sql] # Specify a complete SQL statement to fetch the association. This is a good way to go for complex -- cgit v1.2.3 From 2b4de6621f75b49c30c03ddd78076f7204cc9577 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 7 Feb 2011 16:09:33 -0800 Subject: require tag since we need it for this test --- activerecord/test/cases/associations/eager_load_nested_include_test.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index 8957586189..2cf9f89c3c 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/post' +require 'models/tag' require 'models/author' require 'models/comment' require 'models/category' -- cgit v1.2.3 From 08ef06dbf1273fc09d6e23b7d8727928717e8004 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 7 Feb 2011 16:25:22 -0800 Subject: just return the record from insert_record, use truthiness for comparisons --- .../lib/active_record/associations/has_many_through_association.rb | 4 ++-- activerecord/lib/active_record/autosave_association.rb | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'activerecord') 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 81dd373f7b..f299f51466 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -26,14 +26,14 @@ module ActiveRecord def insert_record(record, force = true, validate = true) if record.new_record? - return false unless save_record(record, force, validate) + return unless save_record(record, force, validate) end through_association = @owner.send(@reflection.through_reflection.name) through_association.create!(construct_join_attributes(record)) update_counter(1) - true + record end private diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 9c7bb67479..c52af23fbd 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -306,6 +306,8 @@ module ActiveRecord records.each do |record| next if record.destroyed? + saved = true + if autosave && record.marked_for_destruction? association.destroy(record) elsif autosave != false && (@new_record_before_save || record.new_record?) @@ -318,7 +320,7 @@ module ActiveRecord saved = record.save(:validate => false) end - raise ActiveRecord::Rollback if saved == false + raise ActiveRecord::Rollback unless saved end end -- cgit v1.2.3 From ac86923fcaac9d07d1f7a313e841d9c68b08411c Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 7 Feb 2011 19:53:22 -0800 Subject: no more faker, rbench, or addressable --- activerecord/examples/performance.rb | 197 ++++++++++++++++------------------- 1 file changed, 88 insertions(+), 109 deletions(-) (limited to 'activerecord') diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index c4ce361b5d..63822731d5 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -1,31 +1,9 @@ -#!/usr/bin/env ruby -KU - TIMES = (ENV['N'] || 10000).to_i -require 'rubygems' - -gem 'addressable', '~>2.0' -gem 'faker', '~>0.3.1' -gem 'rbench', '~>0.2.3' -require 'addressable/uri' -require 'faker' -require 'rbench' - -require File.expand_path("../../../load_paths", __FILE__) +require 'rubygems' require "active_record" -conn = { :adapter => 'mysql', - :database => 'activerecord_unittest', - :username => 'rails', :password => '', - :encoding => 'utf8' } - -conn[:socket] = Pathname.glob(%w[ - /opt/local/var/run/mysql5/mysqld.sock - /tmp/mysqld.sock - /tmp/mysql.sock - /var/mysql/mysql.sock - /var/run/mysqld/mysqld.sock -]).find { |path| path.socket? }.to_s +conn = { :adapter => 'sqlite3', :database => ':memory:' } ActiveRecord::Base.establish_connection(conn) @@ -55,125 +33,126 @@ class Exhibit < ActiveRecord::Base def self.feel(exhibits) exhibits.each { |e| e.feel } end end -sqlfile = File.expand_path("../performance.sql", __FILE__) - -if File.exists?(sqlfile) - mysql_bin = %w[mysql mysql5].detect { |bin| `which #{bin}`.length > 0 } - `#{mysql_bin} -u #{conn[:username]} #{"-p#{conn[:password]}" unless conn[:password].blank?} #{conn[:database]} < #{sqlfile}` -else - puts 'Generating data...' - - # pre-compute the insert statements and fake data compilation, - # so the benchmarks below show the actual runtime for the execute - # method, minus the setup steps - - # Using the same paragraph for all exhibits because it is very slow - # to generate unique paragraphs for all exhibits. - notes = Faker::Lorem.paragraphs.join($/) - today = Date.today - - puts 'Inserting 10,000 users and exhibits...' - 10_000.times do - user = User.create( - :created_at => today, - :name => Faker::Name.name, - :email => Faker::Internet.email - ) - - Exhibit.create( - :created_at => today, - :name => Faker::Company.name, - :user => user, - :notes => notes - ) - end - - mysqldump_bin = %w[mysqldump mysqldump5].detect { |bin| `which #{bin}`.length > 0 } - `#{mysqldump_bin} -u #{conn[:username]} #{"-p#{conn[:password]}" unless conn[:password].blank?} #{conn[:database]} exhibits users > #{sqlfile}` +puts 'Generating data...' + +module ActiveRecord + class Faker + LOREM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse non aliquet diam. Curabitur vel urna metus, quis malesuada elit. Integer consequat tincidunt felis. Etiam non erat dolor. Vivamus imperdiet nibh sit amet diam eleifend id posuere diam malesuada. Mauris at accumsan sem. Donec id lorem neque. Fusce erat lorem, ornare eu congue vitae, malesuada quis neque. Maecenas vel urna a velit pretium fermentum. Donec tortor enim, tempor venenatis egestas a, tempor sed ipsum. Ut arcu justo, faucibus non imperdiet ac, interdum at diam. Pellentesque ipsum enim, venenatis ut iaculis vitae, varius vitae sem. Sed rutrum quam ac elit euismod bibendum. Donec ultricies ultricies magna, at lacinia libero mollis aliquam. Sed ac arcu in tortor elementum tincidunt vel interdum sem. Curabitur eget erat arcu. Praesent eget eros leo. Nam magna enim, sollicitudin vehicula scelerisque in, vulputate ut libero. Praesent varius tincidunt commodo".split + def self.name + LOREM.grep(/^\w*$/).sort_by { rand }.first(2).join ' ' + end + + def self.email + LOREM.grep(/^\w*$/).sort_by { rand }.first(2).join('@') + ".com" + end + end +end + +# pre-compute the insert statements and fake data compilation, +# so the benchmarks below show the actual runtime for the execute +# method, minus the setup steps + +# Using the same paragraph for all exhibits because it is very slow +# to generate unique paragraphs for all exhibits. +notes = ActiveRecord::Faker::LOREM.join ' ' +today = Date.today + +puts 'Inserting 10,000 users and exhibits...' +10_000.times do + user = User.create( + :created_at => today, + :name => ActiveRecord::Faker.name, + :email => ActiveRecord::Faker.email + ) + + Exhibit.create( + :created_at => today, + :name => ActiveRecord::Faker.name, + :user => user, + :notes => notes + ) end -RBench.run(TIMES) do - column :times - column :ar +require 'benchmark' - report 'Model#id', (TIMES * 100).ceil do - ar_obj = Exhibit.find(1) +Benchmark.bm(46) do |x| + ar_obj = Exhibit.find(1) + attrs = { :name => 'sam' } + attrs_first = { :name => 'sam' } + attrs_second = { :name => 'tom' } + exhibit = { + :name => ActiveRecord::Faker.name, + :notes => notes, + :created_at => Date.today + } - ar { ar_obj.id } + x.report("Model#id (x#{(TIMES * 100).ceil})") do + (TIMES * 100).ceil.times { ar_obj.id } end - report 'Model.new (instantiation)' do - ar { Exhibit.new } + x.report 'Model.new (instantiation)' do + TIMES.times { Exhibit.new } end - report 'Model.new (setting attributes)' do - attrs = { :name => 'sam' } - ar { Exhibit.new(attrs) } + x.report 'Model.new (setting attributes)' do + TIMES.times { Exhibit.new(attrs) } end - report 'Model.first' do - ar { Exhibit.first.look } + x.report 'Model.first' do + TIMES.times { Exhibit.first.look } end - report 'Model.all limit(100)', (TIMES / 10).ceil do - ar { Exhibit.look Exhibit.limit(100) } + x.report("Model.all limit(100) (x#{(TIMES / 10).ceil})") do + (TIMES / 10).ceil.times { Exhibit.look Exhibit.limit(100) } end - report 'Model.all limit(100) with relationship', (TIMES / 10).ceil do - ar { Exhibit.feel Exhibit.limit(100).includes(:user) } + x.report "Model.all limit(100) with relationship (x#{(TIMES / 10).ceil})" do + (TIMES / 10).ceil.times { Exhibit.feel Exhibit.limit(100).includes(:user) } end - report 'Model.all limit(10,000)', (TIMES / 1000).ceil do - ar { Exhibit.look Exhibit.limit(10000) } + x.report "Model.all limit(10,000) x(#{(TIMES / 1000).ceil})" do + (TIMES / 1000).ceil.times { Exhibit.look Exhibit.limit(10000) } end - exhibit = { - :name => Faker::Company.name, - :notes => Faker::Lorem.paragraphs.join($/), - :created_at => Date.today - } - - report 'Model.create' do - ar { Exhibit.create(exhibit) } + x.report 'Model.create' do + TIMES.times { Exhibit.create(exhibit) } end - report 'Resource#attributes=' do - attrs_first = { :name => 'sam' } - attrs_second = { :name => 'tom' } - ar { exhibit = Exhibit.new(attrs_first); exhibit.attributes = attrs_second } + x.report 'Resource#attributes=' do + TIMES.times { + exhibit = Exhibit.new(attrs_first) + exhibit.attributes = attrs_second + } end - report 'Resource#update' do - ar { Exhibit.first.update_attributes(:name => 'bob') } + x.report 'Resource#update' do + TIMES.times { Exhibit.first.update_attributes(:name => 'bob') } end - report 'Resource#destroy' do - ar { Exhibit.first.destroy } + x.report 'Resource#destroy' do + TIMES.times { Exhibit.first.destroy } end - report 'Model.transaction' do - ar { Exhibit.transaction { Exhibit.new } } + x.report 'Model.transaction' do + TIMES.times { Exhibit.transaction { Exhibit.new } } end - report 'Model.find(id)' do + x.report 'Model.find(id)' do id = Exhibit.first.id - ar { Exhibit.find(id) } + TIMES.times { Exhibit.find(id) } end - report 'Model.find_by_sql' do - ar { Exhibit.find_by_sql("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}").first } + x.report 'Model.find_by_sql' do + TIMES.times { + Exhibit.find_by_sql("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}").first + } end - report 'Model.log', (TIMES * 10) do - ar { Exhibit.connection.send(:log, "hello", "world") {} } + x.report "Model.log x(#{TIMES * 10})" do + (TIMES * 10).times { Exhibit.connection.send(:log, "hello", "world") {} } end - report 'AR.execute(query)', (TIMES / 2) do - ar { ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}") } + x.report "AR.execute(query) (#{TIMES / 2})" do + (TIMES / 2).times { ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}") } end - - summary 'Total' end - -ActiveRecord::Migration.drop_table "exhibits" -ActiveRecord::Migration.drop_table "users" -- cgit v1.2.3 From 9643243204fab063630380f42fcd4b8160044104 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 3 Feb 2011 20:57:14 +0800 Subject: make set_table_name take effect immediately --- activerecord/lib/active_record/base.rb | 3 +++ activerecord/test/cases/base_test.rb | 11 +++++++++++ activerecord/test/models/joke.rb | 4 ++++ activerecord/test/schema/schema.rb | 4 ++++ 4 files changed, 22 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index effb17b2ff..2d5c166075 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -636,6 +636,9 @@ module ActiveRecord #:nodoc: def set_table_name(value = nil, &block) @quoted_table_name = nil define_attr_method :table_name, value, &block + + @arel_table = Arel::Table.new(table_name, :engine => arel_engine) + @relation = Relation.new(self, arel_table) end alias :table_name= :set_table_name diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 68adeff882..1fa5d2ac5f 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -20,6 +20,7 @@ require 'models/warehouse_thing' require 'models/parrot' require 'models/loose_person' require 'models/edge' +require 'models/joke' require 'rexml/document' require 'active_support/core_ext/exception' @@ -1156,6 +1157,16 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "bar", k.table_name end + def test_switching_between_table_name + assert_difference("GoodJoke.count") do + Joke.set_table_name "cold_jokes" + Joke.create + + Joke.set_table_name "funny_jokes" + Joke.create + end + end + def test_quoted_table_name_after_set_table_name klass = Class.new(ActiveRecord::Base) diff --git a/activerecord/test/models/joke.rb b/activerecord/test/models/joke.rb index 3978abc2ba..d7f01e59e6 100644 --- a/activerecord/test/models/joke.rb +++ b/activerecord/test/models/joke.rb @@ -1,3 +1,7 @@ class Joke < ActiveRecord::Base set_table_name 'funny_jokes' end + +class GoodJoke < ActiveRecord::Base + set_table_name 'funny_jokes' +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 665a4fe914..b1763ff431 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -229,6 +229,10 @@ ActiveRecord::Schema.define do t.string :name end + create_table :cold_jokes, :force => true do |t| + t.string :name + end + create_table :goofy_string_id, :force => true, :id => false do |t| t.string :id, :null => false t.string :info -- cgit v1.2.3 From cd440236ad8a14d7430f65f1f4a878d5e04d6e22 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 8 Feb 2011 11:31:46 -0800 Subject: this test requires the job model, so we should require it --- activerecord/test/cases/locking_test.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 2a72838d06..3ce65a5b6b 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -1,6 +1,7 @@ require 'thread' require "cases/helper" require 'models/person' +require 'models/job' require 'models/reader' require 'models/legacy_thing' require 'models/reference' -- cgit v1.2.3 From 8ce57652b224c01d474ef20b27ea3c3838534467 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 8 Feb 2011 13:13:04 -0800 Subject: ignore max identifier length queries from pg --- activerecord/test/cases/helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 1019ea1dda..7e9007ef73 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -51,7 +51,7 @@ end module ActiveRecord class SQLCounter - IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/] + IGNORED_SQL = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/] # FIXME: this needs to be refactored so specific database can add their own # ignored SQL. This ignored SQL is for Oracle. -- cgit v1.2.3 From 0b58a7ff420d7ef4b643c521a62be7259dd2f5cb Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 7 Dec 2010 09:49:37 -0800 Subject: limit() should sanitize limit values This fixes CVE-2011-0448 --- .../abstract/database_statements.rb | 30 +++++++++---------- .../lib/active_record/relation/query_methods.rb | 2 +- activerecord/test/cases/base_test.rb | 34 ++++++++++++++++++++++ 3 files changed, 50 insertions(+), 16 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 01e53b46c8..5f30b972f5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -276,6 +276,21 @@ module ActiveRecord "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" end + # Sanitizes the given LIMIT parameter in order to prevent SQL injection. + # + # +limit+ may be anything that can evaluate to a string via #to_s. It + # should look like an integer, or a comma-delimited list of integers. + # + # Returns the sanitized limit parameter, either as an integer, or as a + # string which contains a comma-delimited list of integers. + def sanitize_limit(limit) + if limit.to_s =~ /,/ + Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',') + else + Integer(limit) + end + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. @@ -299,21 +314,6 @@ module ActiveRecord update_sql(sql, name) end - # Sanitizes the given LIMIT parameter in order to prevent SQL injection. - # - # +limit+ may be anything that can evaluate to a string via #to_s. It - # should look like an integer, or a comma-delimited list of integers. - # - # Returns the sanitized limit parameter, either as an integer, or as a - # string which contains a comma-delimited list of integers. - def sanitize_limit(limit) - if limit.to_s =~ /,/ - limit.to_s.split(',').map{ |i| i.to_i }.join(',') - else - limit.to_i - end - end - # Send a rollback message to all records after they have been rolled back. If rollback # is false, only rollback records since the last save point. def rollback_transaction_records(rollback) #:nodoc diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 2cbb103eb9..6a905b8588 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -171,7 +171,7 @@ module ActiveRecord arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty? - arel.take(@limit_value) if @limit_value + arel.take(connection.sanitize_limit(@limit_value)) if @limit_value arel.skip(@offset_value) if @offset_value arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 1fa5d2ac5f..1730d9fb56 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -54,6 +54,40 @@ class BasicsTest < ActiveRecord::TestCase assert_nil Edge.primary_key end + def test_limit_with_comma + assert_nothing_raised do + Topic.limit("1,2").all + end + end + + def test_limit_without_comma + assert_nothing_raised do + assert_equal 1, Topic.limit("1").all.length + end + + assert_nothing_raised do + assert_equal 1, Topic.limit(1).all.length + end + end + + def test_invalid_limit + assert_raises(ArgumentError) do + Topic.limit("asdfadf").all + end + end + + def test_limit_should_sanitize_sql_injection_for_limit_without_comas + assert_raises(ArgumentError) do + Topic.limit("1 select * from schema").all + end + end + + def test_limit_should_sanitize_sql_injection_for_limit_with_comas + assert_raises(ArgumentError) do + Topic.limit("1, 7 procedure help()").all + end + end + def test_select_symbol topic_ids = Topic.select(:id).map(&:id).sort assert_equal Topic.find(:all).map(&:id).sort, topic_ids -- cgit v1.2.3 From 1c6f4562d788cd5b63889b9597bc1765a1bd75e0 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 8 Feb 2011 16:01:16 -0800 Subject: primary keys should not be cleared on cache clear, fixing oracle tests --- activerecord/lib/active_record/attribute_methods/primary_key.rb | 1 + .../connection_adapters/abstract/connection_pool.rb | 2 -- activerecord/lib/active_record/relation.rb | 9 ++++++++- activerecord/test/cases/base_test.rb | 5 +++++ activerecord/test/cases/connection_pool_test.rb | 4 +--- 5 files changed, 15 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 978cd7fbe3..fcdd31ddea 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -56,6 +56,7 @@ module ActiveRecord @primary_key ||= '' self.original_primary_key = @primary_key value &&= value.to_s + connection_pool.primary_keys[table_name] = value self.primary_key = block_given? ? instance_eval(&block) : value end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index b1754e61df..4297c26413 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -128,12 +128,10 @@ module ActiveRecord # # * columns # * columns_hash - # * primary_keys # * tables def clear_cache! @columns.clear @columns_hash.clear - @primary_keys.clear @tables.clear end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 1441e9750e..852f4077f2 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -32,7 +32,14 @@ module ActiveRecord def insert(values) im = arel.compile_insert values im.into @table - primary_key_value = primary_key && Hash === values ? values[table[primary_key]] : nil + + primary_key_value = nil + + if primary_key && Hash === values + primary_key_value = values[values.keys.find { |k| + k.name == primary_key + }] + end @klass.connection.insert( im.to_sql, diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 1730d9fb56..20a16cc8ac 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -50,6 +50,11 @@ class Boolean < ActiveRecord::Base; end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts + def test_columns_should_obey_set_primary_key + pk = Subscriber.columns.find { |x| x.name == 'nick' } + assert pk.primary, 'nick should be primary key' + end + def test_primary_key_with_no_id assert_nil Edge.primary_key end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 55ac1bc406..0c545b7e9d 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -18,16 +18,14 @@ module ActiveRecord assert_equal columns_hash, @pool.columns_hash['posts'] end - def test_clearing_cache + def test_clearing_column_cache @pool.columns['posts'] @pool.columns_hash['posts'] - @pool.primary_keys['posts'] @pool.clear_cache! assert_equal 0, @pool.columns.size assert_equal 0, @pool.columns_hash.size - assert_equal 0, @pool.primary_keys.size end def test_primary_key -- cgit v1.2.3 From 5046120b97f4ce8ab08ff7e65c87cfb35f95dba6 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Tue, 8 Feb 2011 16:44:52 -0800 Subject: comma limits do not make sense on oracle or pg --- activerecord/test/cases/base_test.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 20a16cc8ac..2cbff6a5e1 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -59,9 +59,11 @@ class BasicsTest < ActiveRecord::TestCase assert_nil Edge.primary_key end - def test_limit_with_comma - assert_nothing_raised do - Topic.limit("1,2").all + unless current_adapter?(:PostgreSQLAdapter) || current_adapter?(:OracleAdapter) + def test_limit_with_comma + assert_nothing_raised do + Topic.limit("1,2").all + end end end -- cgit v1.2.3 From 56fb3b1594d97fa00dd8d8d95f1d5bf7c30380ee Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Wed, 9 Feb 2011 11:07:25 -0500 Subject: Allow limit values to accept an ARel SQL literal. --- .../connection_adapters/abstract/database_statements.rb | 10 +++++++--- activerecord/test/cases/base_test.rb | 10 ++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 5f30b972f5..7e3f58a411 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -278,13 +278,17 @@ module ActiveRecord # Sanitizes the given LIMIT parameter in order to prevent SQL injection. # - # +limit+ may be anything that can evaluate to a string via #to_s. It - # should look like an integer, or a comma-delimited list of integers. + # The +limit+ may be anything that can evaluate to a string via #to_s. It + # should look like an integer, or a comma-delimited list of integers, or + # an Arel SQL literal. # + # Returns Integer and Arel::Nodes::SqlLiteral limits as is. # Returns the sanitized limit parameter, either as an integer, or as a # string which contains a comma-delimited list of integers. def sanitize_limit(limit) - if limit.to_s =~ /,/ + if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral) + limit + elsif limit.to_s =~ /,/ Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',') else Integer(limit) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 2cbff6a5e1..5b162f4f6f 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -59,7 +59,7 @@ class BasicsTest < ActiveRecord::TestCase assert_nil Edge.primary_key end - unless current_adapter?(:PostgreSQLAdapter) || current_adapter?(:OracleAdapter) + unless current_adapter?(:PostgreSQLAdapter,:OracleAdapter,:SQLServerAdapter) def test_limit_with_comma assert_nothing_raised do Topic.limit("1,2").all @@ -94,7 +94,13 @@ class BasicsTest < ActiveRecord::TestCase Topic.limit("1, 7 procedure help()").all end end - + + unless current_adapter?(:MysqlAdapter) + def test_limit_should_allow_sql_literal + assert_equal 1, Topic.limit(Arel.sql('2-1')).all.length + end + end + def test_select_symbol topic_ids = Topic.select(:id).map(&:id).sort assert_equal Topic.find(:all).map(&:id).sort, topic_ids -- cgit v1.2.3 From c567ccbb1738969345b29297e40a9aee87a97391 Mon Sep 17 00:00:00 2001 From: Raimonds Simanovskis Date: Wed, 9 Feb 2011 13:11:06 +0200 Subject: bugfix for serialized_attributes to be class specific previously serialized_attributes were kept as class attribute of ActiveRecord::Base - if some attribute was defined as serialized in one subclass then it was serialized in all other subclasses as well (if it had the same name) --- activerecord/lib/active_record/base.rb | 4 +++- activerecord/test/cases/attribute_methods_test.rb | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 2d5c166075..16bdd7bb58 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -553,7 +553,9 @@ module ActiveRecord #:nodoc: Coders::YAMLColumn.new(class_name) end - serialized_attributes[attr_name.to_s] = coder + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) end # Guesses the table name (in forced lower-case) based on the name of the class in the diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 88eaea62e8..dfacf58da8 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -8,6 +8,7 @@ require 'models/topic' require 'models/company' require 'models/category' require 'models/reply' +require 'models/contact' class AttributeMethodsTest < ActiveRecord::TestCase fixtures :topics, :developers, :companies, :computers @@ -609,6 +610,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase Object.send(:undef_method, :title) # remove test method from object end + def test_list_of_serialized_attributes + assert_equal %w(content), Topic.serialized_attributes.keys + assert_equal %w(preferences), Contact.serialized_attributes.keys + end private def cached_columns -- cgit v1.2.3 From d3b2596884d884b72d50de2b7ced6097df670736 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 9 Feb 2011 11:50:53 -0800 Subject: use parenthesis so limit works on all dbs --- activerecord/test/cases/base_test.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 5b162f4f6f..ef6b741acd 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -94,13 +94,11 @@ class BasicsTest < ActiveRecord::TestCase Topic.limit("1, 7 procedure help()").all end end - - unless current_adapter?(:MysqlAdapter) - def test_limit_should_allow_sql_literal - assert_equal 1, Topic.limit(Arel.sql('2-1')).all.length - end + + def test_limit_should_allow_sql_literal + assert_equal 1, Topic.limit(Arel.sql('(2 - 1)')).all.length end - + def test_select_symbol topic_ids = Topic.select(:id).map(&:id).sort assert_equal Topic.find(:all).map(&:id).sort, topic_ids -- cgit v1.2.3 From 8bc464c8090f2929917ecd2d9476c914aa6da787 Mon Sep 17 00:00:00 2001 From: Franck Verrot Date: Sat, 27 Nov 2010 11:14:26 +0100 Subject: The optimistic lock column should be increased when calling touch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Santiago Pastorino and José Ignacio Costa --- activerecord/lib/active_record/persistence.rb | 7 +++++++ activerecord/test/cases/locking_test.rb | 8 ++++++++ activerecord/test/schema/schema.rb | 1 + 3 files changed, 16 insertions(+) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b05957981d..3fd67078b4 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -224,6 +224,7 @@ module ActiveRecord def touch(name = nil) attributes = timestamp_attributes_for_update_in_model attributes << name if name + unless attributes.empty? current_time = current_time_from_proper_timezone changes = {} @@ -232,6 +233,12 @@ module ActiveRecord changes[column.to_s] = write_attribute(column.to_s, current_time) end + if locking_enabled? + lock_col = self.class.locking_column.to_s + previous_value = send(lock_col).to_i + changes[lock_col] = write_attribute(lock_col, previous_value + 1) + end + @changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key self.class.update_all(changes, { primary_key => self[primary_key] }) == 1 diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 3ce65a5b6b..636a709924 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -99,6 +99,14 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 1, p1.lock_version end + def test_touch_existing_lock + p1 = Person.find(1) + assert_equal 0, p1.lock_version + + p1.touch + assert_equal 1, p1.lock_version + end + def test_lock_column_name_existing t1 = LegacyThing.find(1) diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index b1763ff431..0b3865fc78 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -430,6 +430,7 @@ ActiveRecord::Schema.define do t.references :number1_fan t.integer :lock_version, :null => false, :default => 0 t.string :comments + t.timestamps end create_table :pets, :primary_key => :pet_id ,:force => true do |t| -- cgit v1.2.3 From 9d8fdfec38a145e3f5074fd8dc0216630c268e32 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Wed, 9 Feb 2011 15:24:32 +0100 Subject: removed some duplication from LH issue 5505 regarding AR touch and optimistic locking [#5505 state:resolved] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Santiago Pastorino and José Ignacio Costa --- activerecord/lib/active_record/locking/optimistic.rb | 14 ++++++++++---- activerecord/lib/active_record/persistence.rb | 6 +----- 2 files changed, 11 insertions(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index e5065de7fb..6b2b1ebafe 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -58,6 +58,12 @@ module ActiveRecord end private + def increment_lock + lock_col = self.class.locking_column + previous_lock_value = send(lock_col).to_i + send(lock_col + '=', previous_lock_value + 1) + end + def attributes_from_column_definition result = super @@ -78,8 +84,8 @@ module ActiveRecord return 0 if attribute_names.empty? lock_col = self.class.locking_column - previous_value = send(lock_col).to_i - send(lock_col + '=', previous_value + 1) + previous_lock_value = send(lock_col).to_i + increment_lock attribute_names += [lock_col] attribute_names.uniq! @@ -89,7 +95,7 @@ module ActiveRecord stmt = relation.where( relation.table[self.class.primary_key].eq(quoted_id).and( - relation.table[lock_col].eq(quote_value(previous_value)) + relation.table[lock_col].eq(quote_value(previous_lock_value)) ) ).arel.compile_update(arel_attributes_values(false, false, attribute_names)) @@ -103,7 +109,7 @@ module ActiveRecord # If something went wrong, revert the version. rescue Exception - send(lock_col + '=', previous_value) + send(lock_col + '=', previous_lock_value) raise end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 3fd67078b4..4ccb7461a1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -233,11 +233,7 @@ module ActiveRecord changes[column.to_s] = write_attribute(column.to_s, current_time) end - if locking_enabled? - lock_col = self.class.locking_column.to_s - previous_value = send(lock_col).to_i - changes[lock_col] = write_attribute(lock_col, previous_value + 1) - end + changes[self.class.locking_column] = increment_lock if locking_enabled? @changed_attributes.except!(*changes.keys) primary_key = self.class.primary_key -- cgit v1.2.3 From 5548e47adbb6dda320cd09ecb696c568896aa6f1 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 9 Feb 2011 13:35:26 -0800 Subject: rawr, mysql, mysql2, why do you hate me. :'( --- activerecord/test/cases/base_test.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index ef6b741acd..6a61dcdb47 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -95,8 +95,10 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_limit_should_allow_sql_literal - assert_equal 1, Topic.limit(Arel.sql('(2 - 1)')).all.length + unless current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) + def test_limit_should_allow_sql_literal + assert_equal 1, Topic.limit(Arel.sql('2-1')).all.length + end end def test_select_symbol -- cgit v1.2.3 From c560c8b2ba2af2ff1942e40b8eff8287354bf7d8 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Wed, 9 Feb 2011 15:15:37 -0800 Subject: log method takes an option list of bind values --- .../active_record/connection_adapters/abstract_adapter.rb | 12 +++++++----- .../lib/active_record/connection_adapters/mysql_adapter.rb | 2 +- .../active_record/connection_adapters/postgresql_adapter.rb | 2 +- .../lib/active_record/connection_adapters/sqlite_adapter.rb | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 3a3a73fc42..0f44baa2fe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -209,11 +209,13 @@ module ActiveRecord protected - def log(sql, name = "SQL") - @instrumenter.instrument("sql.active_record", - :sql => sql, :name => name, :connection_id => object_id) do - yield - end + def log(sql, name = "SQL", binds = []) + @instrumenter.instrument( + "sql.active_record", + :sql => sql, + :name => name, + :connection_id => object_id, + :binds => binds) { yield } rescue Exception => e message = "#{e.class.name}: #{e.message}: #{sql}" @logger.debug message if @logger diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index cdf1ebfee4..368c5b2023 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -331,7 +331,7 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - log(sql, name) do + log(sql, name, binds) do result = nil cache = {} diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 46c0f3fafe..20be4a22ea 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -532,7 +532,7 @@ module ActiveRecord def exec_query(sql, name = 'SQL', binds = []) return exec_no_cache(sql, name) if binds.empty? - log(sql, name) do + log(sql, name, binds) do unless @statements.key? sql nextkey = "a#{@statements.length + 1}" @connection.prepare nextkey, sql diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index f650a1bc74..9ee6b88ab6 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -148,7 +148,7 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== def exec_query(sql, name = nil, binds = []) - log(sql, name) do + log(sql, name, binds) do # Don't cache statements without bind values if binds.empty? -- cgit v1.2.3 From 028016ede382de80b9e91a7e13cd5372c2afd4bd Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 10 Feb 2011 09:56:50 -0800 Subject: test cases for bind parameter logging --- activerecord/test/cases/bind_parameter_test.rb | 56 ++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 activerecord/test/cases/bind_parameter_test.rb (limited to 'activerecord') diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb new file mode 100644 index 0000000000..4532ab6213 --- /dev/null +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -0,0 +1,56 @@ +require 'cases/helper' +require 'models/topic' + +module ActiveRecord + class BindParameterTest < ActiveRecord::TestCase + class LogListener + attr_accessor :calls + + def initialize + @calls = [] + end + + def call(*args) + calls << args + end + end + + fixtures :topics + + def setup + super + @connection = ActiveRecord::Base.connection + @listener = LogListener.new + @pk = Topic.columns.find { |c| c.primary } + ActiveSupport::Notifications.subscribe('sql.active_record', @listener) + end + + def teardown + ActiveSupport::Notifications.unsubscribe(@listener) + end + + def test_binds_are_logged + # FIXME: use skip with minitest + return unless @connection.supports_statement_cache? + + sub = @connection.substitute_for(@pk, []) + binds = [[@pk, 1]] + sql = "select * from topics where id = #{sub}" + + @connection.exec_query(sql, 'SQL', binds) + + message = @listener.calls.find { |args| args[4][:sql] == sql } + assert_equal binds, message[4][:binds] + end + + def test_find_one_uses_binds + # FIXME: use skip with minitest + return unless @connection.supports_statement_cache? + + Topic.find(1) + binds = [[@pk, 1]] + message = @listener.calls.find { |args| args[4][:binds] == binds } + assert message, 'expected a message with binds' + end + end +end -- cgit v1.2.3 From 2f49cd91b7fdf18a559216fa725d039a5cd78ff1 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 10 Feb 2011 13:34:33 -0800 Subject: bind parameters are logged to debug log --- activerecord/lib/active_record/log_subscriber.rb | 16 +++++++--- activerecord/test/cases/bind_parameter_test.rb | 38 ++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index c7ae12977a..a08b3562d0 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -22,8 +22,16 @@ module ActiveRecord self.class.runtime += event.duration return unless logger.debug? - name = '%s (%.1fms)' % [event.payload[:name], event.duration] - sql = event.payload[:sql].squeeze(' ') + payload = event.payload + name = '%s (%.1fms)' % [payload[:name], event.duration] + sql = payload[:sql].squeeze(' ') + binds = nil + + unless (payload[:binds] || []).empty? + binds = " {" + payload[:binds].map { |col,v| + "#{col.name.inspect} => #{v.inspect}" + }.join(", ") + "}" + end if odd? name = color(name, CYAN, true) @@ -32,7 +40,7 @@ module ActiveRecord name = color(name, MAGENTA, true) end - debug " #{name} #{sql}" + debug " #{name} #{sql}#{binds}" end def odd? @@ -45,4 +53,4 @@ module ActiveRecord end end -ActiveRecord::LogSubscriber.attach_to :active_record \ No newline at end of file +ActiveRecord::LogSubscriber.attach_to :active_record diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 4532ab6213..83001f44f9 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -3,6 +3,8 @@ require 'models/topic' module ActiveRecord class BindParameterTest < ActiveRecord::TestCase + fixtures :topics + class LogListener attr_accessor :calls @@ -15,8 +17,6 @@ module ActiveRecord end end - fixtures :topics - def setup super @connection = ActiveRecord::Base.connection @@ -52,5 +52,39 @@ module ActiveRecord message = @listener.calls.find { |args| args[4][:binds] == binds } assert message, 'expected a message with binds' end + + def test_logs_bind_vars + # FIXME: use skip with minitest + return unless @connection.supports_statement_cache? + + pk = Topic.columns.find { |x| x.primary } + + payload = { + :name => 'SQL', + :sql => 'select * from topics where id = ?', + :binds => [[pk, 10]] + } + event = ActiveSupport::Notifications::Event.new( + 'foo', + Time.now, + Time.now, + 123, + payload) + + logger = Class.new(ActiveRecord::LogSubscriber) { + attr_reader :debugs + def initialize + super + @debugs = [] + end + + def debug str + @debugs << str + end + }.new + + logger.sql event + assert_match("{#{pk.name.inspect} => #{10.inspect}}", logger.debugs.first) + end end end -- cgit v1.2.3 From 61b69338b27e3d865d4ca87269a7c12a74ea32da Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 10 Feb 2011 13:54:29 -0800 Subject: simplify bind parameter logging --- activerecord/lib/active_record/log_subscriber.rb | 4 +--- activerecord/test/cases/bind_parameter_test.rb | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index a08b3562d0..8855798bde 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -28,9 +28,7 @@ module ActiveRecord binds = nil unless (payload[:binds] || []).empty? - binds = " {" + payload[:binds].map { |col,v| - "#{col.name.inspect} => #{v.inspect}" - }.join(", ") + "}" + binds = " #{Hash[payload[:binds].map { |col,v| [col.name, v] }]}" end if odd? diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 83001f44f9..eb2991437b 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -84,7 +84,7 @@ module ActiveRecord }.new logger.sql event - assert_match("{#{pk.name.inspect} => #{10.inspect}}", logger.debugs.first) + assert_match({pk.name => 10}.inspect, logger.debugs.first) end end end -- cgit v1.2.3 From 339ad0d0020cf7173ef7debe32f3cbb8809de5ca Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Thu, 10 Feb 2011 14:17:09 -0800 Subject: fixing tests on 1.8, using a list of lists because order is important --- activerecord/lib/active_record/log_subscriber.rb | 4 +++- activerecord/test/cases/bind_parameter_test.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 8855798bde..afadbf03ef 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -28,7 +28,9 @@ module ActiveRecord binds = nil unless (payload[:binds] || []).empty? - binds = " #{Hash[payload[:binds].map { |col,v| [col.name, v] }]}" + binds = " " + payload[:binds].map { |col,v| + [col.name, v] + }.inspect end if odd? diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index eb2991437b..19383bb06b 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -84,7 +84,7 @@ module ActiveRecord }.new logger.sql event - assert_match({pk.name => 10}.inspect, logger.debugs.first) + assert_match([[pk.name, 10]].inspect, logger.debugs.first) end end end -- cgit v1.2.3 From 83dd6e1e620e0350c5999bd35d75251374aceaea Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 10:03:37 -0800 Subject: favor composition over inheritence. use AS::OrderedHash rather than omap --- activerecord/lib/active_record/fixtures.rb | 43 +++++++++++++++++------------- activerecord/test/cases/fixtures_test.rb | 2 +- 2 files changed, 25 insertions(+), 20 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 216c691833..c8adb809b0 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -12,15 +12,7 @@ require 'active_support/dependencies' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/logger' - -if RUBY_VERSION < '1.9' - module YAML #:nodoc: - class Omap #:nodoc: - def keys; map { |k, v| k } end - def values; map { |k, v| v } end - end - end -end +require 'active_support/ordered_hash' if defined? ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: @@ -452,7 +444,7 @@ class FixturesFileNotFound < StandardError; end # # Any fixture labeled "DEFAULTS" is safely ignored. -class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) +class Fixtures MAX_ID = 2 ** 30 - 1 DEFAULT_FILTER_RE = /\.ya?ml$/ @@ -552,9 +544,10 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) Zlib.crc32(label.to_s) % MAX_ID end - attr_reader :table_name, :name + attr_reader :table_name, :name, :fixtures def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) + @fixtures = ActiveSupport::OrderedHash.new @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter @name = table_name # preserve fixture base name @class_name = class_name || @@ -565,6 +558,22 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) read_fixture_files end + def [](x) + fixtures[x] + end + + def []=(k,v) + fixtures[k] = v + end + + def each(&block) + fixtures.each(&block) + end + + def size + fixtures.size + end + def delete_existing_fixtures @connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete' end @@ -574,18 +583,14 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) now = now.to_s(:db) # allow a standard key to be used for doing defaults in YAML - if is_a?(Hash) - delete('DEFAULTS') - else - delete(assoc('DEFAULTS')) - end + fixtures.delete('DEFAULTS') # track any join tables we need to insert later habtm_fixtures = Hash.new do |h, habtm| h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil) end - each do |label, fixture| + fixtures.each do |label, fixture| row = fixture.to_hash if model_class && model_class < ActiveRecord::Base @@ -725,7 +730,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)" end - self[name] = Fixture.new(data, model_class, @connection) + fixtures[name] = Fixture.new(data, model_class, @connection) end end end @@ -738,7 +743,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) reader.each do |row| data = {} row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } - self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection) + fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection) end end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 864a7a2acc..6dbdf4ca54 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -245,7 +245,7 @@ if Account.connection.respond_to?(:reset_pk_sequence!) def test_create_fixtures_resets_sequences_when_not_cached @instances.each do |instance| - max_id = create_fixtures(instance.class.table_name).inject(0) do |_max_id, (_, fixture)| + max_id = create_fixtures(instance.class.table_name).fixtures.inject(0) do |_max_id, (_, fixture)| fixture_id = fixture['id'].to_i fixture_id > _max_id ? fixture_id : _max_id end -- cgit v1.2.3 From fb09d025429d1c1f05a705a231edb9938ccff9b0 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 14:33:03 -0800 Subject: refactor fixtures to do less work in the constructor --- activerecord/lib/active_record/fixtures.rb | 33 +++++++++++++++++++++--------- activerecord/test/cases/helper.rb | 2 +- 2 files changed, 24 insertions(+), 11 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index c8adb809b0..59dbe8651e 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -450,6 +450,12 @@ class Fixtures @@all_cached_fixtures = {} + def self.find_table_name(table_name) # :nodoc: + ActiveRecord::Base.pluralize_table_names ? + table_name.to_s.singularize.camelize : + table_name.to_s.camelize + end + def self.reset_cache(connection = nil) connection ||= ActiveRecord::Base.connection @@all_cached_fixtures[connection.object_id] = {} @@ -547,14 +553,16 @@ class Fixtures attr_reader :table_name, :name, :fixtures def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) - @fixtures = ActiveSupport::OrderedHash.new - @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter - @name = table_name # preserve fixture base name - @class_name = class_name || - (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize) - @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" - @table_name = class_name.table_name if class_name.respond_to?(:table_name) - @connection = class_name.connection if class_name.respond_to?(:connection) + @fixtures = ActiveSupport::OrderedHash.new + @connection = connection + @table_name = table_name + @fixture_path = fixture_path + @file_filter = file_filter + @name = table_name # preserve fixture base name + @class_name = class_name + @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" + @table_name = class_name.table_name if class_name.respond_to?(:table_name) + @connection = class_name.connection if class_name.respond_to?(:connection) read_fixture_files end @@ -587,7 +595,10 @@ class Fixtures # track any join tables we need to insert later habtm_fixtures = Hash.new do |h, habtm| - h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil) + h[habtm] = HabtmFixtures.new( + @connection, + habtm.options[:join_table], + Fixtures.find_table_name(habtm.options[:join_table]), nil) end fixtures.each do |label, fixture| @@ -843,7 +854,9 @@ module ActiveRecord self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false - self.fixture_class_names = {} + self.fixture_class_names = Hash.new do |h, table_name| + h[table_name] = Fixtures.find_table_name(table_name) + end end module ClassMethods diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 7e9007ef73..be508e46f8 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -94,7 +94,7 @@ class ActiveSupport::TestCase self.use_transactional_fixtures = true def create_fixtures(*table_names, &block) - Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, {}, &block) + Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, table_names, fixture_class_names, &block) end end -- cgit v1.2.3 From 77a6e2558475b80481739c091a873bf39f46cf3b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 15:01:51 -0800 Subject: use hash defaults to dry up ||= calls --- activerecord/lib/active_record/fixtures.rb | 49 ++++++++++++++---------------- 1 file changed, 22 insertions(+), 27 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 59dbe8651e..aa8a746d52 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -448,7 +448,7 @@ class Fixtures MAX_ID = 2 ** 30 - 1 DEFAULT_FILTER_RE = /\.ya?ml$/ - @@all_cached_fixtures = {} + @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } def self.find_table_name(table_name) # :nodoc: ActiveRecord::Base.pluralize_table_names ? @@ -458,11 +458,10 @@ class Fixtures def self.reset_cache(connection = nil) connection ||= ActiveRecord::Base.connection - @@all_cached_fixtures[connection.object_id] = {} + @@all_cached_fixtures.delete connection.object_id end def self.cache_for_connection(connection) - @@all_cached_fixtures[connection.object_id] ||= {} @@all_cached_fixtures[connection.object_id] end @@ -486,13 +485,11 @@ class Fixtures def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true) object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures if load_instances - ActiveRecord::Base.silence do - fixtures.each do |name, fixture| - begin - object.instance_variable_set "@#{name}", fixture.find - rescue FixtureClassNotFound - nil - end + fixtures.each do |name, fixture| + begin + object.instance_variable_set "@#{name}", fixture.find + rescue FixtureClassNotFound + nil end end end @@ -515,30 +512,28 @@ class Fixtures table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) } unless table_names_to_fetch.empty? - ActiveRecord::Base.silence do - connection.disable_referential_integrity do - fixtures_map = {} + connection.disable_referential_integrity do + fixtures_map = {} - fixtures = table_names_to_fetch.map do |table_name| - fixtures_map[table_name] = Fixtures.new(connection, table_name.tr('/', '_'), class_names[table_name.tr('/', '_').to_sym], File.join(fixtures_directory, table_name)) - end + fixtures = table_names_to_fetch.map do |table_name| + fixtures_map[table_name] = Fixtures.new(connection, table_name.tr('/', '_'), class_names[table_name.tr('/', '_').to_sym], File.join(fixtures_directory, table_name)) + end - all_loaded_fixtures.update(fixtures_map) + all_loaded_fixtures.update(fixtures_map) - connection.transaction(:requires_new => true) do - fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } - fixtures.each { |fixture| fixture.insert_fixtures } + connection.transaction(:requires_new => true) do + fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } + fixtures.each { |fixture| fixture.insert_fixtures } - # Cap primary key sequences to max(pk). - if connection.respond_to?(:reset_pk_sequence!) - table_names.each do |table_name| - connection.reset_pk_sequence!(table_name.tr('/', '_')) - end + # Cap primary key sequences to max(pk). + if connection.respond_to?(:reset_pk_sequence!) + table_names.each do |table_name| + connection.reset_pk_sequence!(table_name.tr('/', '_')) end end - - cache_fixtures(connection, fixtures_map) end + + cache_fixtures(connection, fixtures_map) end end cached_fixtures(connection, table_names) -- cgit v1.2.3 From ca938c0fc90d28032005356b019db0d04b5439ee Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 15:37:05 -0800 Subject: fixing variable names --- activerecord/lib/active_record/fixtures.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index aa8a746d52..34f27a107f 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -509,14 +509,20 @@ class Fixtures table_names.each { |n| class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') } connection = block_given? ? yield : ActiveRecord::Base.connection - table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) } + files_to_read = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) } - unless table_names_to_fetch.empty? + unless files_to_read.empty? connection.disable_referential_integrity do fixtures_map = {} - fixtures = table_names_to_fetch.map do |table_name| - fixtures_map[table_name] = Fixtures.new(connection, table_name.tr('/', '_'), class_names[table_name.tr('/', '_').to_sym], File.join(fixtures_directory, table_name)) + fixtures = files_to_read.map do |path| + table_name = path.tr '/', '_' + + fixtures_map[path] = Fixtures.new( + connection, + table_name, + class_names[table_name.to_sym], + File.join(fixtures_directory, path)) end all_loaded_fixtures.update(fixtures_map) -- cgit v1.2.3 From 634424665422bc885c5e3f168908ea58e1f3c811 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 16:09:34 -0800 Subject: create_fixtures() should always return a list --- activerecord/lib/active_record/fixtures.rb | 8 ++++---- activerecord/test/cases/fixtures_test.rb | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 34f27a107f..9634cebcf5 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -471,11 +471,10 @@ class Fixtures def self.cached_fixtures(connection, keys_to_fetch = nil) if keys_to_fetch - fixtures = cache_for_connection(connection).values_at(*keys_to_fetch) + cache_for_connection(connection).values_at(*keys_to_fetch) else - fixtures = cache_for_connection(connection).values + cache_for_connection(connection).values end - fixtures.size > 1 ? fixtures : fixtures.first end def self.cache_fixtures(connection, fixtures_map) @@ -554,13 +553,14 @@ class Fixtures attr_reader :table_name, :name, :fixtures def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) - @fixtures = ActiveSupport::OrderedHash.new @connection = connection @table_name = table_name @fixture_path = fixture_path @file_filter = file_filter @name = table_name # preserve fixture base name @class_name = class_name + + @fixtures = ActiveSupport::OrderedHash.new @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" @table_name = class_name.table_name if class_name.respond_to?(:table_name) @connection = class_name.connection if class_name.respond_to?(:connection) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 6dbdf4ca54..ee51692f93 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -35,7 +35,7 @@ class FixturesTest < ActiveRecord::TestCase def test_clean_fixtures FIXTURES.each do |name| fixtures = nil - assert_nothing_raised { fixtures = create_fixtures(name) } + assert_nothing_raised { fixtures = create_fixtures(name).first } assert_kind_of(Fixtures, fixtures) fixtures.each { |_name, fixture| fixture.each { |key, value| @@ -53,7 +53,7 @@ class FixturesTest < ActiveRecord::TestCase end def test_attributes - topics = create_fixtures("topics") + topics = create_fixtures("topics").first assert_equal("The First Topic", topics["first"]["title"]) assert_nil(topics["second"]["author_email_address"]) end @@ -127,7 +127,7 @@ class FixturesTest < ActiveRecord::TestCase end def test_instantiation - topics = create_fixtures("topics") + topics = create_fixtures("topics").first assert_kind_of Topic, topics["first"].find end @@ -245,7 +245,7 @@ if Account.connection.respond_to?(:reset_pk_sequence!) def test_create_fixtures_resets_sequences_when_not_cached @instances.each do |instance| - max_id = create_fixtures(instance.class.table_name).fixtures.inject(0) do |_max_id, (_, fixture)| + max_id = create_fixtures(instance.class.table_name).first.fixtures.inject(0) do |_max_id, (_, fixture)| fixture_id = fixture['id'].to_i fixture_id > _max_id ? fixture_id : _max_id end @@ -509,7 +509,7 @@ class FasterFixturesTest < ActiveRecord::TestCase fixtures :categories, :authors def load_extra_fixture(name) - fixture = create_fixtures(name) + fixture = create_fixtures(name).first assert fixture.is_a?(Fixtures) @loaded_fixtures[fixture.table_name] = fixture end -- cgit v1.2.3 From af5b1db965d986f563d19d5dd23df9925b43d5fb Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 16:18:37 -0800 Subject: create fixtures always returns a list, so build a hash from the list --- activerecord/lib/active_record/fixtures.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 9634cebcf5..fba05b16d7 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -955,8 +955,7 @@ module ActiveRecord if @@already_loaded_fixtures[self.class] @loaded_fixtures = @@already_loaded_fixtures[self.class] else - load_fixtures - @@already_loaded_fixtures[self.class] = @loaded_fixtures + @@already_loaded_fixtures[self.class] = load_fixtures end ActiveRecord::Base.connection.increment_open_transactions ActiveRecord::Base.connection.transaction_joinable = false @@ -989,15 +988,8 @@ module ActiveRecord private def load_fixtures - @loaded_fixtures = {} fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) - unless fixtures.nil? - if fixtures.instance_of?(Fixtures) - @loaded_fixtures[fixtures.name] = fixtures - else - fixtures.each { |f| @loaded_fixtures[f.name] = f } - end - end + @loaded_fixtures = Hash[fixtures.map { |f| [f.name, f] }] end # for pre_loaded_fixtures, only require the classes once. huge speed improvement -- cgit v1.2.3 From e0802fa1766ab70d04a2d52f2c79f0ca818d6674 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 16:22:38 -0800 Subject: avoid side effects from method calls, localize ivar assignment --- activerecord/lib/active_record/fixtures.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index fba05b16d7..962698b2fd 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -955,7 +955,8 @@ module ActiveRecord if @@already_loaded_fixtures[self.class] @loaded_fixtures = @@already_loaded_fixtures[self.class] else - @@already_loaded_fixtures[self.class] = load_fixtures + @loaded_fixtures = load_fixtures + @@already_loaded_fixtures[self.class] = @loaded_fixtures end ActiveRecord::Base.connection.increment_open_transactions ActiveRecord::Base.connection.transaction_joinable = false @@ -964,7 +965,7 @@ module ActiveRecord else Fixtures.reset_cache @@already_loaded_fixtures[self.class] = nil - load_fixtures + @loaded_fixtures = load_fixtures end # Instantiate fixtures for every test if requested. @@ -989,7 +990,7 @@ module ActiveRecord private def load_fixtures fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) - @loaded_fixtures = Hash[fixtures.map { |f| [f.name, f] }] + Hash[fixtures.map { |f| [f.name, f] }] end # for pre_loaded_fixtures, only require the classes once. huge speed improvement -- cgit v1.2.3 From 61fc3094799f82fa4f4c33b5e02656cf6a0126ab Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 16:26:48 -0800 Subject: clear cache for all connections when resetting --- activerecord/lib/active_record/fixtures.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 962698b2fd..2cef5570e1 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -456,13 +456,12 @@ class Fixtures table_name.to_s.camelize end - def self.reset_cache(connection = nil) - connection ||= ActiveRecord::Base.connection - @@all_cached_fixtures.delete connection.object_id + def self.reset_cache + @@all_cached_fixtures.clear end def self.cache_for_connection(connection) - @@all_cached_fixtures[connection.object_id] + @@all_cached_fixtures[connection] end def self.fixture_is_cached?(connection, table_name) -- cgit v1.2.3 From 0ebbf6be8a5aa9b5c817b3ee0dfdd8135c525ac7 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 16:35:15 -0800 Subject: calculate model class on construction --- activerecord/lib/active_record/fixtures.rb | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 2cef5570e1..caf1aedf09 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -549,7 +549,7 @@ class Fixtures Zlib.crc32(label.to_s) % MAX_ID end - attr_reader :table_name, :name, :fixtures + attr_reader :table_name, :name, :fixtures, :model_class def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) @connection = connection @@ -561,8 +561,16 @@ class Fixtures @fixtures = ActiveSupport::OrderedHash.new @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}" - @table_name = class_name.table_name if class_name.respond_to?(:table_name) - @connection = class_name.connection if class_name.respond_to?(:connection) + + # Should be an AR::Base type class + if class_name.is_a?(Class) + @table_name = class_name.table_name + @connection = class_name.connection + @model_class = class_name + else + @model_class = class_name.constantize rescue nil + end + read_fixture_files end @@ -675,19 +683,6 @@ class Fixtures def read_fixture_files; end end - def model_class - unless defined?(@model_class) - @model_class = - if @class_name.nil? || @class_name.is_a?(Class) - @class_name - else - @class_name.constantize rescue nil - end - end - - @model_class - end - def primary_key_name @primary_key_name ||= model_class && model_class.primary_key end -- cgit v1.2.3 From 24166c2603d8e706a0f44b05875b32fbf46b48cf Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 16:52:23 -0800 Subject: database quoting should take care of this, no need to gsub --- activerecord/lib/active_record/fixtures.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index caf1aedf09..56bfd2aff4 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -816,7 +816,7 @@ class Fixture #:nodoc: def value_list cols = (model_class && model_class < ActiveRecord::Base) ? model_class.columns_hash : {} @fixture.map do |key, value| - @connection.quote(value, cols[key]).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r") + @connection.quote(value, cols[key]) end.join(', ') end -- cgit v1.2.3 From a5b61269f3bb08ba270e15c043dc64329f1e10d7 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 17:02:53 -0800 Subject: Fixtures class constantizes this value, so no need to do it twice --- activerecord/lib/active_record/fixtures.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 56bfd2aff4..6eeb012aa1 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -788,9 +788,9 @@ class Fixture #:nodoc: attr_reader :model_class def initialize(fixture, model_class, connection = ActiveRecord::Base.connection) - @connection = connection - @fixture = fixture - @model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil + @connection = connection + @fixture = fixture + @model_class = model_class end def class_name -- cgit v1.2.3 From 55a4fb5e705671058ffe6d9af965742d560c31e5 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 17:14:23 -0800 Subject: fixture should quack like a hash --- .../connection_adapters/abstract/database_statements.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 7e3f58a411..5c1ce173c8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -261,7 +261,15 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - execute "INSERT INTO #{quote_table_name(table_name)} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert' + columns = Hash[columns(table_name).map { |c| [c.name, c] }] + + key_list = [] + value_list = fixture.map do |name, value| + key_list << quote_column_name(name) + quote(value, columns[name]) + end + + execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert' end def empty_insert_statement_value -- cgit v1.2.3 From bc3c3453d485a098c91d96b6f0a8e2c9a1e9b17b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 17:20:53 -0800 Subject: convert fixtures to a list of hashes to insert --- activerecord/lib/active_record/fixtures.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 6eeb012aa1..546c49b3c3 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -609,7 +609,7 @@ class Fixtures Fixtures.find_table_name(habtm.options[:join_table]), nil) end - fixtures.each do |label, fixture| + rows = fixtures.map do |label, fixture| row = fixture.to_hash if model_class && model_class < ActiveRecord::Base @@ -668,7 +668,11 @@ class Fixtures end end - @connection.insert_fixture(fixture, @table_name) + row + end + + rows.each do |row| + @connection.insert_fixture(row, table_name) end # insert any HABTM join tables we discovered -- cgit v1.2.3 From 2b353acac319fdba5bd1dafda5f13ea7a62666aa Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 17:45:09 -0800 Subject: Fixture class no longer needs a reference to the database connection --- activerecord/lib/active_record/fixtures.rb | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 546c49b3c3..413e0b3088 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -661,7 +661,7 @@ class Fixtures join_fixtures["#{label}_#{target}"] = Fixture.new( { association.foreign_key => row[primary_key_name], association.association_foreign_key => Fixtures.identify(target) }, - nil, @connection) + nil) end end end @@ -740,7 +740,7 @@ class Fixtures raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)" end - fixtures[name] = Fixture.new(data, model_class, @connection) + fixtures[name] = Fixture.new(data, model_class) end end end @@ -753,7 +753,7 @@ class Fixtures reader.each do |row| data = {} row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } - fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection) + fixtures["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class) end end @@ -791,8 +791,7 @@ class Fixture #:nodoc: attr_reader :model_class - def initialize(fixture, model_class, connection = ActiveRecord::Base.connection) - @connection = connection + def initialize(fixture, model_class) @fixture = fixture @model_class = model_class end @@ -813,17 +812,6 @@ class Fixture #:nodoc: @fixture end - def key_list - @fixture.keys.map { |column_name| @connection.quote_column_name(column_name) }.join(', ') - end - - def value_list - cols = (model_class && model_class < ActiveRecord::Base) ? model_class.columns_hash : {} - @fixture.map do |key, value| - @connection.quote(value, cols[key]) - end.join(', ') - end - def find if model_class model_class.find(self[model_class.primary_key]) -- cgit v1.2.3 From 5b0695a8cba1f2f2dc88cb9777fc6e7bbe24da01 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Fri, 11 Feb 2011 17:53:55 -0800 Subject: key habtm fixtures off table name --- activerecord/lib/active_record/fixtures.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 413e0b3088..4459b711e6 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -602,11 +602,11 @@ class Fixtures fixtures.delete('DEFAULTS') # track any join tables we need to insert later - habtm_fixtures = Hash.new do |h, habtm| - h[habtm] = HabtmFixtures.new( + habtm_fixtures = Hash.new do |h, path| + h[path] = HabtmFixtures.new( @connection, - habtm.options[:join_table], - Fixtures.find_table_name(habtm.options[:join_table]), nil) + path, + Fixtures.find_table_name(path), nil) end rows = fixtures.map do |label, fixture| @@ -655,7 +655,7 @@ class Fixtures when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - join_fixtures = habtm_fixtures[association] + join_fixtures = habtm_fixtures[association.options[:join_table]] targets.each do |target| join_fixtures["#{label}_#{target}"] = Fixture.new( -- cgit v1.2.3 From d72add9c20778e253f90fbfe587eae0e205c40ad Mon Sep 17 00:00:00 2001 From: Ernie Miller Date: Sat, 12 Feb 2011 13:59:53 -0500 Subject: Fix table name collision due to incorrect alias count on certain joins. [#6423 state:committed] Signed-off-by: Santiago Pastorino --- .../class_methods/join_dependency/join_association.rb | 8 ++++---- .../test/cases/associations/inner_join_association_test.rb | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb index 3fea24ebf8..856d826d67 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -82,12 +82,12 @@ module ActiveRecord connection = active_record.connection name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" - table_index = aliases[name] + 1 - name = name[0, connection.table_alias_length-3] + "_#{table_index}" if table_index > 1 + aliases[name] += 1 + name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1 + else + aliases[name] += 1 end - aliases[name] += 1 - name end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index da2a81e98a..8f32902c19 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -16,6 +16,13 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase assert_equal authors(:david), result.first end + def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations + assert_nothing_raised do + sql = Person.joins(:agents => {:agents => :agents}).joins(:agents => {:agents => {:primary_contact => :agents}}).to_sql + assert_match(/agents_people_4/i, sql) + end + end + def test_construct_finder_sql_ignores_empty_joins_hash sql = Author.joins({}).to_sql assert_no_match(/JOIN/i, sql) -- cgit v1.2.3 From 7eb554bdb13d3469712b847add2ab0054f0698c7 Mon Sep 17 00:00:00 2001 From: Brian Lopez Date: Sat, 12 Feb 2011 16:54:27 -0800 Subject: switch over to Mysql2::Client#ping for the mysql2 connection check --- activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 74dd7e8491..7bad511c64 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -197,10 +197,7 @@ module ActiveRecord def active? return false unless @connection - @connection.query 'select 1' - true - rescue Mysql2::Error - false + @connection.ping end def reconnect! -- cgit v1.2.3 From fbd917f50a6046d02dd6a64ccfb1aed0cbce68d8 Mon Sep 17 00:00:00 2001 From: Ernie Miller Date: Thu, 10 Feb 2011 14:03:25 -0500 Subject: Remove Relation#& alias for Relation#merge --- .../lib/active_record/associations/association_proxy.rb | 2 +- .../lib/active_record/associations/through_association.rb | 2 +- activerecord/lib/active_record/relation/spawn_methods.rb | 2 -- activerecord/test/cases/method_scoping_test.rb | 2 +- activerecord/test/cases/relation_scoping_test.rb | 4 ++-- activerecord/test/cases/relations_test.rb | 14 +++++++------- 6 files changed, 12 insertions(+), 14 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 07fff7f7d7..2832f49c23 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -166,7 +166,7 @@ module ActiveRecord end def scoped - target_scope & @association_scope + target_scope.merge(@association_scope) end protected diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 4ae0669c96..ab593d09b9 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -6,7 +6,7 @@ module ActiveRecord protected def target_scope - super & @reflection.through_reflection.klass.scoped + super.merge(@reflection.through_reflection.klass.scoped) end def association_scope diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 69a7642ec5..4150e36a9a 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -61,8 +61,6 @@ module ActiveRecord merged_relation end - alias :& :merge - # Removes from the query the condition(s) specified in +skips+. # # Example: diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index e3ba65b4fa..7e8383da9e 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -227,7 +227,7 @@ class MethodScopingTest < ActiveRecord::TestCase end def test_scoped_create_with_join_and_merge - (Comment.where(:body => "but Who's Buying?").joins(:post) & Post.where(:body => 'Peace Sells...')).with_scope do + Comment.where(:body => "but Who's Buying?").joins(:post).merge(Post.where(:body => 'Peace Sells...')).with_scope do assert_equal({:body => "but Who's Buying?"}, Comment.scoped.scope_for_create) end end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 1bdf3136d4..cda2850b02 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -488,8 +488,8 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_create_with_merge - aaron = (PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20) & - PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new + aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge( + PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new assert_equal 20, aaron.salary assert_equal 'Aaron', aaron.name diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 5018b16b67..184e7a2b9e 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -545,17 +545,17 @@ class RelationTest < ActiveRecord::TestCase end def test_relation_merging - devs = Developer.where("salary >= 80000") & Developer.limit(2) & Developer.order('id ASC').where("id < 3") + devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3")) assert_equal [developers(:david), developers(:jamis)], devs.to_a - dev_with_count = Developer.limit(1) & Developer.order('id DESC') & Developer.select('developers.*') + dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*')) assert_equal [developers(:poor_jamis)], dev_with_count.to_a end def test_relation_merging_with_eager_load relations = [] - relations << (Post.order('comments.id DESC') & Post.eager_load(:last_comment) & Post.scoped) - relations << (Post.eager_load(:last_comment) & Post.order('comments.id DESC') & Post.scoped) + relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.scoped) + relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.scoped) relations.each do |posts| post = posts.find { |p| p.id == 1 } @@ -564,18 +564,18 @@ class RelationTest < ActiveRecord::TestCase end def test_relation_merging_with_locks - devs = Developer.lock.where("salary >= 80000").order("id DESC") & Developer.limit(2) + devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) assert_present devs.locked end def test_relation_merging_with_preload - [Post.scoped & Post.preload(:author), Post.preload(:author) & Post.scoped].each do |posts| + [Post.scoped.merge(Post.preload(:author)), Post.preload(:author).merge(Post.scoped)].each do |posts| assert_queries(2) { assert posts.first.author } end end def test_relation_merging_with_joins - comments = Comment.joins(:post).where(:body => 'Thank you for the welcome') & Post.where(:body => 'Such a lovely day') + comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day')) assert_equal 1, comments.count end -- cgit v1.2.3 From 9143032a108bd41121337c82416ba90f460d8214 Mon Sep 17 00:00:00 2001 From: Santiago Pastorino Date: Sun, 13 Feb 2011 02:38:31 -0200 Subject: Add missing require --- activerecord/test/cases/associations/inner_join_association_test.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 8f32902c19..e2228228a3 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -4,6 +4,7 @@ require 'models/comment' require 'models/author' require 'models/category' require 'models/categorization' +require 'models/person' require 'models/tagging' require 'models/tag' -- cgit v1.2.3 From a7e19b30ca71f62af516675023659be061b2b70a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 11 Feb 2011 22:22:19 +0000 Subject: Add interpolation of association conditions back in, in the form of proc { ... } rather than instance_eval-ing strings --- activerecord/CHANGELOG | 23 ++++++++++++++++++++-- .../lib/active_record/association_preload.rb | 12 +++++++++-- .../associations/association_collection.rb | 8 +++----- .../associations/association_proxy.rb | 11 ++++++++--- .../join_dependency/join_association.rb | 4 ++++ .../has_and_belongs_to_many_association.rb | 4 ++-- .../associations/through_association.rb | 4 ++-- activerecord/lib/active_record/base.rb | 6 ------ activerecord/test/cases/associations/eager_test.rb | 8 ++++++++ .../has_and_belongs_to_many_associations_test.rb | 2 +- .../associations/has_many_associations_test.rb | 1 - .../has_many_through_associations_test.rb | 7 +++++++ .../associations/has_one_associations_test.rb | 8 ++++++++ activerecord/test/cases/base_test.rb | 6 ------ activerecord/test/models/company.rb | 15 ++++++-------- activerecord/test/models/post.rb | 7 +++++++ activerecord/test/models/project.rb | 17 ++++++++-------- activerecord/test/models/tagging.rb | 1 + 18 files changed, 97 insertions(+), 47 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index e49915a8cd..72bbeeec61 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -50,8 +50,27 @@ (for example, add_name_to_users) use the reversible migration's `change` method instead of the ordinary `up` and `down` methods. [Prem Sichanugrist] -* Removed support for interpolated SQL conditions. Please use scoping -along with attribute conditionals as a replacement. +* Removed support for interpolating string SQL conditions on associations. Instead, you should + use a proc, like so: + + Before: + + has_many :things, :conditions => 'foo = #{bar}' + + After: + + has_many :things, :conditions => proc { "foo = #{bar}" } + + Inside the proc, 'self' is the object which is the owner of the association, unless you are + eager loading the association, in which case 'self' is the class which the association is within. + + You can have any "normal" conditions inside the proc, so the following will work too: + + has_many :things, :conditions => proc { ["foo = ?", bar] } + + Previously :insert_sql and :delete_sql on has_and_belongs_to_many association allowed you to call + 'record' to get the record being inserted or deleted. This is now passed as an argument to + the proc. * Added ActiveRecord::Base#has_secure_password (via ActiveModel::SecurePassword) to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH]. Example: diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index b83c00e9f8..52d2c49836 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -399,10 +399,18 @@ module ActiveRecord end end + def process_conditions(conditions, klass = self) + if conditions.respond_to?(:to_proc) + conditions = instance_eval(&conditions) + end + + klass.send(:sanitize_sql, conditions) + end + def append_conditions(reflection, preload_options) [ - ("(#{reflection.sanitized_conditions})" if reflection.sanitized_conditions), - ("(#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]), + ('(' + process_conditions(reflection.options[:conditions], reflection.klass) + ')' if reflection.options[:conditions]), + ('(' + process_conditions(preload_options[:conditions]) + ')' if preload_options[:conditions]), ].compact.map { |x| Arel.sql x } end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 3e3237c348..8e006e4d9d 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -374,17 +374,15 @@ module ActiveRecord def custom_counter_sql if @reflection.options[:counter_sql] - counter_sql = @reflection.options[:counter_sql] + interpolate(@reflection.options[:counter_sql]) else # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ - counter_sql = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + interpolate(@reflection.options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } end - - interpolate_sql(counter_sql) end def custom_finder_sql - interpolate_sql(@reflection.options[:finder_sql]) + interpolate(@reflection.options[:finder_sql]) end def find_target diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 2832f49c23..fc03a4b619 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -184,7 +184,8 @@ module ActiveRecord def association_scope scope = target_klass.unscoped scope = scope.create_with(creation_attributes) - scope = scope.apply_finder_options(@reflection.options.slice(:conditions, :readonly, :include)) + scope = scope.apply_finder_options(@reflection.options.slice(:readonly, :include)) + scope = scope.where(interpolate(@reflection.options[:conditions])) if select = select_value scope = scope.select(select) end @@ -240,8 +241,12 @@ module ActiveRecord !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass end - def interpolate_sql(sql, record = nil) - @owner.send(:interpolate_sql, sql, record) + def interpolate(sql, record = nil) + if sql.respond_to?(:to_proc) + @owner.send(:instance_exec, record, &sql) + else + sql + end end def select_value diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb index 856d826d67..aaa475109e 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -108,6 +108,10 @@ module ActiveRecord end def process_conditions(conditions, table_name) + if conditions.respond_to?(:to_proc) + conditions = instance_eval(&conditions) + end + Arel.sql(sanitize_sql(conditions, table_name)) end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index b0ccab2de6..b5ec45159b 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -17,7 +17,7 @@ module ActiveRecord end if @reflection.options[:insert_sql] - @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) + @owner.connection.insert(interpolate(@reflection.options[:insert_sql], record)) else stmt = join_table.compile_insert( join_table[@reflection.foreign_key] => @owner.id, @@ -42,7 +42,7 @@ module ActiveRecord def delete_records(records, method) if sql = @reflection.options[:delete_sql] - records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } + records.each { |record| @owner.connection.delete(interpolate(sql, record)) } else relation = join_table stmt = relation.where(relation[@reflection.foreign_key].eq(@owner.id). diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index ab593d09b9..0550bae408 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -119,14 +119,14 @@ module ActiveRecord scope = scope.where(@reflection.through_reflection.klass.send(:type_condition)) end - scope = scope.where(@reflection.source_reflection.options[:conditions]) + scope = scope.where(interpolate(@reflection.source_reflection.options[:conditions])) scope.where(through_conditions) end # If there is a hash of conditions then we make sure the keys are scoped to the # through table name if left ambiguous. def through_conditions - conditions = @reflection.through_reflection.options[:conditions] + conditions = interpolate(@reflection.through_reflection.options[:conditions]) if conditions.is_a?(Hash) Hash[conditions.map { |key, value| diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 16bdd7bb58..3d48ab89ac 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1790,12 +1790,6 @@ MSG self.class.connection.quote(value, column) end - # Interpolate custom SQL string in instance context. - # Optional record argument is meant for custom insert_sql. - def interpolate_sql(sql, record = nil) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) - end - # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 3074648d59..42dd612083 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -668,6 +668,14 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal people(:david, :susan), Person.find(:all, :include => [:readers, :primary_contact, :number1_fan], :conditions => "number1_fans_people.first_name like 'M%'", :order => 'people.id', :limit => 2, :offset => 0) end + def test_preload_with_interpolation + post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id) + assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + + post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id) + assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + end + def test_polymorphic_type_condition post = Post.find(posts(:thinking).id, :include => :taggings) assert post.taggings.include?(taggings(:thinking_general)) 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 5dd2c9861e..dc382c3007 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 @@ -72,7 +72,7 @@ class DeveloperWithCounterSQL < ActiveRecord::Base :join_table => "developers_projects", :association_foreign_key => "project_id", :foreign_key => "developer_id", - :counter_sql => 'SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}' + :counter_sql => proc { "SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}" } end class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 23b777ac6d..ad774eb9ce 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -279,7 +279,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_counting_using_finder_sql assert_equal 2, Firm.find(4).clients_using_sql.count - assert_equal 2, Firm.find(4).clients_using_multiline_sql.count end def test_belongs_to_sanity 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 c58068ef75..73b1d6c500 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -711,4 +711,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post.author_addresses.delete(address) assert post[:author_count].nil? end + + def test_interpolated_conditions + post = posts(:welcome) + assert !post.tags.empty? + assert_equal post.tags, post.interpolated_tags + assert_equal post.tags, post.interpolated_tags_2 + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index dbf6dfe20d..6b73a35905 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -243,6 +243,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase firm.destroy end + def test_finding_with_interpolated_condition + firm = Firm.find(:first) + superior = firm.clients.create(:name => 'SuperiorCo') + superior.rating = 10 + superior.save + assert_equal 10, firm.clients_with_interpolated_conditions.first.rating + end + def test_assignment_before_child_saved firm = Firm.find(1) firm.account = a = Account.new("credit_limit" => 1000) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 6a61dcdb47..0ad20bb9bc 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1295,12 +1295,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal res6, res7 end - def test_interpolate_sql - assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo@bar') } - assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo bar) baz') } - assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo bar} baz') } - end - def test_scoped_find_conditions scoped_developers = Developer.send(:with_scope, :find => { :conditions => 'salary > 90000' }) do Developer.find(:all, :conditions => 'id < 5') diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 3e219fbe4a..e0b30efd51 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -48,19 +48,16 @@ class Firm < Company has_many :dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :destroy has_many :exclusively_dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all has_many :limited_clients, :class_name => "Client", :limit => 1 + has_many :clients_with_interpolated_conditions, :class_name => "Client", :conditions => proc { "rating > #{rating}" } has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id" has_many :clients_like_ms_with_hash_conditions, :conditions => { :name => 'Microsoft' }, :class_name => "Client", :order => "id" - has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' - has_many :clients_using_multiline_sql, :class_name => "Client", :finder_sql => ' - SELECT - companies.* - FROM companies WHERE companies.client_of = #{id}' + has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" } has_many :clients_using_counter_sql, :class_name => "Client", - :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}', - :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = #{id}' + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " }, + :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" } has_many :clients_using_zero_counter_sql, :class_name => "Client", - :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}', - :counter_sql => 'SELECT 0 FROM companies WHERE client_of = #{id}' + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }, + :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" } has_many :no_clients_using_counter_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000', :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000' diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index fd8cd2244a..5f29196790 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -41,6 +41,9 @@ class Post < ActiveRecord::Base has_many :author_categorizations, :through => :author, :source => :categorizations has_many :author_addresses, :through => :author + has_many :comments_with_interpolated_conditions, :class_name => 'Comment', + :conditions => proc { ["#{"#{aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome'] } + has_one :very_special_comment has_one :very_special_comment_with_post, :class_name => "VerySpecialComment", :include => :post has_many :special_comments @@ -57,6 +60,10 @@ class Post < ActiveRecord::Base end end + has_many :interpolated_taggings, :class_name => 'Tagging', :as => :taggable, :conditions => proc { "1 = #{1}" } + has_many :interpolated_tags, :through => :taggings + has_many :interpolated_tags_2, :through => :interpolated_taggings, :source => :tag + has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 8a53a8f803..efe1ce67da 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -7,14 +7,15 @@ class Project < ActiveRecord::Base has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true has_and_belongs_to_many :developers_named_david_with_hash_conditions, :class_name => "Developer", :conditions => { :name => 'David' }, :uniq => true has_and_belongs_to_many :salaried_developers, :class_name => "Developer", :conditions => "salary > 0" - has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => 'SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id' - has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => ' - SELECT - t.*, j.* - FROM - developers_projects j, - developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id' - has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => "DELETE FROM developers_projects WHERE project_id = \#{id} AND developer_id = \#{record.id}" + has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => proc { "SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" } + has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => proc { + "SELECT + t.*, j.* + FROM + developers_projects j, + developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" + } + has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => proc { |record| "DELETE FROM developers_projects WHERE project_id = #{id} AND developer_id = #{record.id}" } has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || ''}"}, :after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || ''}"}, :before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"}, diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index 33ffc623d7..231d2b5890 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -6,6 +6,7 @@ class Tagging < ActiveRecord::Base belongs_to :tag, :include => :tagging belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id' belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' + belongs_to :interpolated_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => proc { "1 = #{1}" } belongs_to :taggable, :polymorphic => true, :counter_cache => true has_many :things, :through => :taggable end -- cgit v1.2.3 From eab5fb49f839bf19da4c31b35142f9e05d05ed2e Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 11 Feb 2011 22:32:21 +0000 Subject: Fix test/cases/connection_pool_test.rb for sqlite3 in-memory db --- activerecord/test/cases/connection_pool_test.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'activerecord') diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 0c545b7e9d..7ac14fa8d6 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -6,6 +6,16 @@ module ActiveRecord def setup # Keep a duplicate pool so we do not bother others @pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec + + if in_memory_db? + # Separate connections to an in-memory database create an entirely new database, + # with an empty schema etc, so we just stub out this schema on the fly. + @pool.with_connection do |connection| + connection.create_table :posts do |t| + t.integer :cololumn + end + end + end end def test_pool_caches_columns -- cgit v1.2.3 From 7ce7ae0c8bd94e7f96c1448183b83da85ef91edb Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 14 Feb 2011 00:14:16 +0000 Subject: Get rid of AssociationCollection#save_record --- .../associations/association_collection.rb | 21 +++++++-------------- .../has_and_belongs_to_many_association.rb | 8 +++----- .../associations/has_many_association.rb | 4 ++-- .../associations/has_many_through_association.rb | 17 +++++++++++++---- .../lib/active_record/autosave_association.rb | 2 +- 5 files changed, 26 insertions(+), 26 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 8e006e4d9d..4ead67b282 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -197,16 +197,15 @@ module ActiveRecord else create_record(attrs) do |record| yield(record) if block_given? - insert_record(record, false) + insert_record(record) end end end - def create!(attrs = {}) - create_record(attrs) do |record| - yield(record) if block_given? - insert_record(record, true) - end + def create!(attrs = {}, &block) + record = create(attrs, &block) + Array.wrap(record).each(&:save!) + record end # Returns the size of the collection by executing a SELECT COUNT(*) @@ -419,17 +418,11 @@ module ActiveRecord end + existing end - # Do the relevant stuff to insert the given record into the association collection. The - # force param specifies whether or not an exception should be raised on failure. The - # validate param specifies whether validation should be performed (if force is false). - def insert_record(record, force = true, validate = true) + # Do the relevant stuff to insert the given record into the association collection. + def insert_record(record, validate = true) raise NotImplementedError end - def save_record(record, force, validate) - force ? record.save! : record.save(:validate => validate) - end - def create_record(attributes, &block) ensure_owner_is_persisted! transaction { build_record(attributes, &block) } diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index b5ec45159b..b9c9919e7a 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -11,10 +11,8 @@ module ActiveRecord protected - def insert_record(record, force = true, validate = true) - if record.new_record? - return false unless save_record(record, force, validate) - end + def insert_record(record, validate = true) + return if record.new_record? && !record.save(:validate => validate) if @reflection.options[:insert_sql] @owner.connection.insert(interpolate(@reflection.options[:insert_sql], record)) @@ -27,7 +25,7 @@ module ActiveRecord @owner.connection.insert stmt.to_sql end - true + record end def association_scope diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 0b246587c0..543b073393 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -8,9 +8,9 @@ module ActiveRecord class HasManyAssociation < AssociationCollection #:nodoc: protected - def insert_record(record, force = false, validate = true) + def insert_record(record, validate = true) set_owner_attributes(record) - save_record(record, force, validate) + record.save(:validate => validate) end private 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 f299f51466..9f74d57c4d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -22,12 +22,21 @@ module ActiveRecord end end + def <<(*records) + unless @owner.new_record? + records.flatten.each do |record| + raise_on_type_mismatch(record) + record.save! if record.new_record? + end + end + + super + end + protected - def insert_record(record, force = true, validate = true) - if record.new_record? - return unless save_record(record, force, validate) - end + def insert_record(record, validate = true) + return if record.new_record? && !record.save(:validate => validate) through_association = @owner.send(@reflection.through_reflection.name) through_association.create!(construct_join_attributes(record)) diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index c52af23fbd..95357ee5dc 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -312,7 +312,7 @@ module ActiveRecord association.destroy(record) elsif autosave != false && (@new_record_before_save || record.new_record?) if autosave - saved = association.send(:insert_record, record, false, false) + saved = association.send(:insert_record, record, false) else association.send(:insert_record, record) end -- cgit v1.2.3 From b93d2189c4cf370927b9796e0008034a5268214f Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 14 Feb 2011 00:34:54 +0000 Subject: Get rid of create_record as it is not only used in one place --- .../active_record/associations/association_collection.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 4ead67b282..ff4c2b51ac 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -195,9 +195,13 @@ module ActiveRecord if attrs.is_a?(Array) attrs.collect { |attr| create(attr) } else - create_record(attrs) do |record| - yield(record) if block_given? - insert_record(record) + ensure_owner_is_persisted! + + transaction do + build_record(attrs) do |record| + yield(record) if block_given? + insert_record(record) + end end end end @@ -423,11 +427,6 @@ module ActiveRecord raise NotImplementedError end - def create_record(attributes, &block) - ensure_owner_is_persisted! - transaction { build_record(attributes, &block) } - end - def build_record(attributes, &block) attributes = scoped.scope_for_create.merge(attributes) record = @reflection.build_association(attributes) -- cgit v1.2.3 From 686418c65754701b9cdc213de4b6905f465fdc60 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 14 Feb 2011 00:44:10 +0000 Subject: Move create and create! next to build --- .../associations/association_collection.rb | 42 +++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index ff4c2b51ac..e391d9cce6 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -66,6 +66,27 @@ module ActiveRecord end end + def create(attrs = {}) + if attrs.is_a?(Array) + attrs.collect { |attr| create(attr) } + else + ensure_owner_is_persisted! + + transaction do + build_record(attrs) do |record| + yield(record) if block_given? + insert_record(record) + end + end + end + end + + def create!(attrs = {}, &block) + record = create(attrs, &block) + Array.wrap(record).each(&:save!) + record + end + # Add +records+ to this association. Returns +self+ so method calls may be chained. # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. def <<(*records) @@ -191,27 +212,6 @@ module ActiveRecord delete_or_destroy(records, :destroy) end - def create(attrs = {}) - if attrs.is_a?(Array) - attrs.collect { |attr| create(attr) } - else - ensure_owner_is_persisted! - - transaction do - build_record(attrs) do |record| - yield(record) if block_given? - insert_record(record) - end - end - end - end - - def create!(attrs = {}, &block) - record = create(attrs, &block) - Array.wrap(record).each(&:save!) - record - end - # Returns the size of the collection by executing a SELECT COUNT(*) # query if the collection hasn't been loaded, and calling # collection.size if it has. -- cgit v1.2.3 From 641a3068ea11c20e09358d6911404a265da32a9f Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 14 Feb 2011 00:58:34 +0000 Subject: Don't pass the block through build_record --- .../associations/association_collection.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index e391d9cce6..a850cf39cd 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -59,21 +59,21 @@ module ActiveRecord if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } else - build_record(attributes) do |record| - block.call(record) if block_given? + add_record_to_target_with_callbacks(build_record(attributes)) do |record| + yield(record) if block_given? set_owner_attributes(record) end end end - def create(attrs = {}) - if attrs.is_a?(Array) - attrs.collect { |attr| create(attr) } + def create(attributes = {}) + if attributes.is_a?(Array) + attributes.collect { |attr| create(attr) } else ensure_owner_is_persisted! transaction do - build_record(attrs) do |record| + add_record_to_target_with_callbacks(build_record(attributes)) do |record| yield(record) if block_given? insert_record(record) end @@ -427,10 +427,8 @@ module ActiveRecord raise NotImplementedError end - def build_record(attributes, &block) - attributes = scoped.scope_for_create.merge(attributes) - record = @reflection.build_association(attributes) - add_record_to_target_with_callbacks(record, &block) + def build_record(attributes) + @reflection.build_association(scoped.scope_for_create.merge(attributes)) end def delete_or_destroy(records, method) -- cgit v1.2.3 From db03308451497dd6c7fa6e531b378f63f0781e7c Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 14 Feb 2011 01:02:42 +0000 Subject: Rename add_record_to_target_with_callbacks to add_to_target --- .../lib/active_record/associations/association_collection.rb | 8 ++++---- activerecord/lib/active_record/nested_attributes.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index a850cf39cd..b8187b1c6a 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -59,7 +59,7 @@ module ActiveRecord if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } else - add_record_to_target_with_callbacks(build_record(attributes)) do |record| + add_to_target(build_record(attributes)) do |record| yield(record) if block_given? set_owner_attributes(record) end @@ -73,7 +73,7 @@ module ActiveRecord ensure_owner_is_persisted! transaction do - add_record_to_target_with_callbacks(build_record(attributes)) do |record| + add_to_target(build_record(attributes)) do |record| yield(record) if block_given? insert_record(record) end @@ -96,7 +96,7 @@ module ActiveRecord transaction do records.flatten.each do |record| raise_on_type_mismatch(record) - add_record_to_target_with_callbacks(record) do |r| + add_to_target(record) do |r| result &&= insert_record(record) unless @owner.new_record? end end @@ -351,7 +351,7 @@ module ActiveRecord target end - def add_record_to_target_with_callbacks(record) + def add_to_target(record) callback(:before_add, record) yield(record) if block_given? @target ||= [] unless loaded? diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 16023defe3..fc68d7254a 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -413,7 +413,7 @@ module ActiveRecord if target_record existing_record = target_record else - association.send(:add_record_to_target_with_callbacks, existing_record) + association.send(:add_to_target, existing_record) end end -- cgit v1.2.3 From c9b685e681ea5851c2baaaec780fcc9c6a9e2775 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 14 Feb 2011 01:07:48 +0000 Subject: @target should always be an array --- activerecord/lib/active_record/associations/association_collection.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index b8187b1c6a..8b5e600c96 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -354,12 +354,13 @@ module ActiveRecord def add_to_target(record) callback(:before_add, record) yield(record) if block_given? - @target ||= [] unless loaded? + if @reflection.options[:uniq] && index = @target.index(record) @target[index] = record else @target << record end + callback(:after_add, record) set_inverse_instance(record) record -- cgit v1.2.3 From 5d6d669bfe1e480dd4d0cc5042b7faba4b469846 Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 14 Feb 2011 01:19:42 +0000 Subject: Inline ensure_owner_is_persisted! as it is only called from one place --- .../lib/active_record/associations/association_collection.rb | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 8b5e600c96..888ebdf7af 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -67,11 +67,13 @@ module ActiveRecord end def create(attributes = {}) + unless @owner.persisted? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" + end + if attributes.is_a?(Array) attributes.collect { |attr| create(attr) } else - ensure_owner_is_persisted! - transaction do add_to_target(build_record(attributes)) do |record| yield(record) if block_given? @@ -471,12 +473,6 @@ module ActiveRecord @owner.class.send(full_callback_name.to_sym) || [] end - def ensure_owner_is_persisted! - unless @owner.persisted? - raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" - end - end - # Should we deal with assoc.first or assoc.last by issuing an independent query to # the database, or by getting the target, and then taking the first/last item from that? # -- cgit v1.2.3 From b9ea751d0e56bd00d341766977a607ed3f7ddd0f Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Mon, 14 Feb 2011 01:40:00 +0000 Subject: Add a transaction wrapper in add_to_target. This means that #build will now also use a transaction. IMO this is reasonable given that the before_add and after_add callbacks might do anything, and this great consistency allows us to abstract out the duplicate code from #build and #create. --- .../associations/association_collection.rb | 54 +++++++++++----------- 1 file changed, 27 insertions(+), 27 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 888ebdf7af..ca350f51c9 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -56,31 +56,15 @@ module ActiveRecord end def build(attributes = {}, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| build(attr, &block) } - else - add_to_target(build_record(attributes)) do |record| - yield(record) if block_given? - set_owner_attributes(record) - end - end + build_or_create(attributes, :build, &block) end - def create(attributes = {}) + def create(attributes = {}, &block) unless @owner.persisted? raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" end - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr) } - else - transaction do - add_to_target(build_record(attributes)) do |record| - yield(record) if block_given? - insert_record(record) - end - end - end + build_or_create(attributes, :create, &block) end def create!(attrs = {}, &block) @@ -354,17 +338,20 @@ module ActiveRecord end def add_to_target(record) - callback(:before_add, record) - yield(record) if block_given? + transaction do + callback(:before_add, record) + yield(record) if block_given? - if @reflection.options[:uniq] && index = @target.index(record) - @target[index] = record - else - @target << record + if @reflection.options[:uniq] && index = @target.index(record) + @target[index] = record + else + @target << record + end + + callback(:after_add, record) + set_inverse_instance(record) end - callback(:after_add, record) - set_inverse_instance(record) record end @@ -425,6 +412,19 @@ module ActiveRecord end + existing end + def build_or_create(attributes, method) + records = Array.wrap(attributes).map do |attrs| + record = build_record(attrs) + + add_to_target(record) do + yield(record) if block_given? + insert_record(record) if method == :create + end + end + + attributes.is_a?(Array) ? records : records.first + end + # Do the relevant stuff to insert the given record into the association collection. def insert_record(record, validate = true) raise NotImplementedError -- cgit v1.2.3 From 6f4e3ffd0f42c92a00536e7e1356be3e8cf37639 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 14 Feb 2011 10:04:53 -0800 Subject: HabtmFixtures class is no longer needed --- activerecord/lib/active_record/fixtures.rb | 53 +++++++++++++----------------- 1 file changed, 22 insertions(+), 31 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 4459b711e6..feca1e0035 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -590,8 +590,8 @@ class Fixtures fixtures.size end - def delete_existing_fixtures - @connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete' + def delete_existing_fixtures(table = table_name) + @connection.delete "DELETE FROM #{@connection.quote_table_name(table)}", 'Fixture Delete' end def insert_fixtures @@ -602,12 +602,7 @@ class Fixtures fixtures.delete('DEFAULTS') # track any join tables we need to insert later - habtm_fixtures = Hash.new do |h, path| - h[path] = HabtmFixtures.new( - @connection, - path, - Fixtures.find_table_name(path), nil) - end + habtm_fixtures = Hash.new { |h,k| h[k] = [] } rows = fixtures.map do |label, fixture| row = fixture.to_hash @@ -655,14 +650,11 @@ class Fixtures when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) - join_fixtures = habtm_fixtures[association.options[:join_table]] - - targets.each do |target| - join_fixtures["#{label}_#{target}"] = Fixture.new( - { association.foreign_key => row[primary_key_name], - association.association_foreign_key => Fixtures.identify(target) }, - nil) - end + table_name = association.options[:join_table] + habtm_fixtures[table_name].concat targets.map { |target| + { association.foreign_key => row[primary_key_name], + association.association_foreign_key => Fixtures.identify(target) } + } end end end @@ -675,18 +667,19 @@ class Fixtures @connection.insert_fixture(row, table_name) end + habtm_fixtures.keys.each do |table| + delete_existing_fixtures(table) + end + # insert any HABTM join tables we discovered - habtm_fixtures.values.each do |fixture| - fixture.delete_existing_fixtures - fixture.insert_fixtures + habtm_fixtures.each do |table, fixtures| + fixtures.each do |row| + @connection.insert_fixture(row, table) + end end end private - class HabtmFixtures < ::Fixtures #:nodoc: - def read_fixture_files; end - end - def primary_key_name @primary_key_name ||= model_class && model_class.primary_key end @@ -789,7 +782,7 @@ class Fixture #:nodoc: class FormatError < FixtureError #:nodoc: end - attr_reader :model_class + attr_reader :model_class, :fixture def initialize(fixture, model_class) @fixture = fixture @@ -797,24 +790,22 @@ class Fixture #:nodoc: end def class_name - @model_class.name if @model_class + model_class.name if model_class end def each - @fixture.each { |item| yield item } + fixture.each { |item| yield item } end def [](key) - @fixture[key] + fixture[key] end - def to_hash - @fixture - end + alias :to_hash :fixture def find if model_class - model_class.find(self[model_class.primary_key]) + model_class.find(fixture[model_class.primary_key]) else raise FixtureClassNotFound, "No class attached to find." end -- cgit v1.2.3 From 2487aab99a0c1767502c9f635102607fb4ded05d Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 14 Feb 2011 11:25:19 -0800 Subject: fixtures will return a list of tables that may be effected, delete existing fixtures will delete those tables --- activerecord/lib/active_record/fixtures.rb | 31 ++++++++++++++++++++++++------ activerecord/test/cases/fixtures_test.rb | 5 +++++ 2 files changed, 30 insertions(+), 6 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index feca1e0035..154c2d022c 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -590,8 +590,31 @@ class Fixtures fixtures.size end - def delete_existing_fixtures(table = table_name) - @connection.delete "DELETE FROM #{@connection.quote_table_name(table)}", 'Fixture Delete' + def delete_existing_fixtures + tables.each do |table| + @connection.delete "DELETE FROM #{@connection.quote_table_name(table)}", 'Fixture Delete' + end + end + + # Return a list of tables this fixture effect. This is typically the +table_name+ + # along with any habtm tables specified via Foxy Fixtures. + def tables + [table_name] + fixtures.values.map { |fixture| + row = fixture.to_hash + + # If STI is used, find the correct subclass for association reflection + associations = [] + if model_class && model_class < ActiveRecord::Base + reflection_class = row[inheritance_column_name].constantize rescue model_class + associations = reflection_class.reflect_on_all_associations + end + + foxy_habtms = associations.find_all { |assoc| + assoc.macro == :has_and_belongs_to_many && row.key?(assoc.name.to_s) + } + + foxy_habtms.map { |assoc| assoc.options[:join_table] } + }.flatten.uniq end def insert_fixtures @@ -667,10 +690,6 @@ class Fixtures @connection.insert_fixture(row, table_name) end - habtm_fixtures.keys.each do |table| - delete_existing_fixtures(table) - end - # insert any HABTM join tables we discovered habtm_fixtures.each do |table, fixtures| fixtures.each do |row| diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index ee51692f93..2a5d6b2beb 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -188,6 +188,11 @@ class FixturesTest < ActiveRecord::TestCase end end + def test_tables + fixtures = Fixtures.new(Parrot.connection, 'parrots', 'Parrot', FIXTURES_ROOT + "/parrots") + assert_equal %w{parrots parrots_treasures}, fixtures.tables + end + def test_yml_file_in_subdirectory assert_equal(categories(:sub_special_1).name, "A special category in a subdir file") assert_equal(categories(:sub_special_1).class, SpecialCategory) -- cgit v1.2.3 From ca0501676500471981806993e7ccd7536f7943f0 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 14 Feb 2011 12:01:22 -0800 Subject: extract rows that should be inserted to a method --- activerecord/lib/active_record/fixtures.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 154c2d022c..75c5daaaae 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -617,7 +617,9 @@ class Fixtures }.flatten.uniq end - def insert_fixtures + # Return 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 rows now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now now = now.to_s(:db) @@ -625,9 +627,9 @@ class Fixtures fixtures.delete('DEFAULTS') # track any join tables we need to insert later - habtm_fixtures = Hash.new { |h,k| h[k] = [] } + rows = Hash.new { |h,table| h[table] = [] } - rows = fixtures.map do |label, fixture| + rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash if model_class && model_class < ActiveRecord::Base @@ -674,7 +676,7 @@ class Fixtures if (targets = row.delete(association.name.to_s)) targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) table_name = association.options[:join_table] - habtm_fixtures[table_name].concat targets.map { |target| + rows[table_name].concat targets.map { |target| { association.foreign_key => row[primary_key_name], association.association_foreign_key => Fixtures.identify(target) } } @@ -685,15 +687,13 @@ class Fixtures row end + rows + end - rows.each do |row| - @connection.insert_fixture(row, table_name) - end - - # insert any HABTM join tables we discovered - habtm_fixtures.each do |table, fixtures| - fixtures.each do |row| - @connection.insert_fixture(row, table) + def insert_fixtures + rows.each do |table_name, rows| + rows.each do |row| + @connection.insert_fixture(row, table_name) end end end -- cgit v1.2.3 From fd81c700ec4ff7a8170e5497ecc351d76fa50b84 Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 14 Feb 2011 14:53:42 -0800 Subject: extract database activity out of Fixtures instances --- activerecord/lib/active_record/fixtures.rb | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 75c5daaaae..97d814eb71 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -504,8 +504,12 @@ class Fixtures def self.create_fixtures(fixtures_directory, table_names, class_names = {}) table_names = [table_names].flatten.map { |n| n.to_s } - table_names.each { |n| class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') } - connection = block_given? ? yield : ActiveRecord::Base.connection + table_names.each { |n| + class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') + } + + # FIXME: Apparently JK uses this. + connection = block_given? ? yield : ActiveRecord::Base.connection files_to_read = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) } @@ -513,7 +517,7 @@ class Fixtures connection.disable_referential_integrity do fixtures_map = {} - fixtures = files_to_read.map do |path| + fixture_files = files_to_read.map do |path| table_name = path.tr '/', '_' fixtures_map[path] = Fixtures.new( @@ -526,8 +530,20 @@ class Fixtures all_loaded_fixtures.update(fixtures_map) connection.transaction(:requires_new => true) do - fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } - fixtures.each { |fixture| fixture.insert_fixtures } + fixture_files.each do |ff| + conn = ff.model_class.respond_to?(:connection) ? ff.model_class.connection : connection + table_rows = ff.table_rows + + table_rows.keys.each do |table| + conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' + end + + table_rows.each do |table_name,rows| + rows.each do |row| + conn.insert_fixture(row, table_name) + end + end + end # Cap primary key sequences to max(pk). if connection.respond_to?(:reset_pk_sequence!) @@ -619,7 +635,7 @@ class Fixtures # Return 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 rows + def table_rows now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now now = now.to_s(:db) @@ -691,7 +707,7 @@ class Fixtures end def insert_fixtures - rows.each do |table_name, rows| + table_rows.each do |table_name, rows| rows.each do |row| @connection.insert_fixture(row, table_name) end -- cgit v1.2.3 From f9ea47736e270152c264bb5f8fdbfaa1d04fe82f Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 14 Feb 2011 14:55:01 -0800 Subject: remove unused methods --- activerecord/lib/active_record/fixtures.rb | 35 ------------------------------ activerecord/test/cases/fixtures_test.rb | 5 ----- 2 files changed, 40 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 97d814eb71..3876e57210 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -606,33 +606,6 @@ class Fixtures fixtures.size end - def delete_existing_fixtures - tables.each do |table| - @connection.delete "DELETE FROM #{@connection.quote_table_name(table)}", 'Fixture Delete' - end - end - - # Return a list of tables this fixture effect. This is typically the +table_name+ - # along with any habtm tables specified via Foxy Fixtures. - def tables - [table_name] + fixtures.values.map { |fixture| - row = fixture.to_hash - - # If STI is used, find the correct subclass for association reflection - associations = [] - if model_class && model_class < ActiveRecord::Base - reflection_class = row[inheritance_column_name].constantize rescue model_class - associations = reflection_class.reflect_on_all_associations - end - - foxy_habtms = associations.find_all { |assoc| - assoc.macro == :has_and_belongs_to_many && row.key?(assoc.name.to_s) - } - - foxy_habtms.map { |assoc| assoc.options[:join_table] } - }.flatten.uniq - end - # Return 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 @@ -706,14 +679,6 @@ class Fixtures rows end - def insert_fixtures - table_rows.each do |table_name, rows| - rows.each do |row| - @connection.insert_fixture(row, table_name) - end - end - end - private def primary_key_name @primary_key_name ||= model_class && model_class.primary_key diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 2a5d6b2beb..ee51692f93 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -188,11 +188,6 @@ class FixturesTest < ActiveRecord::TestCase end end - def test_tables - fixtures = Fixtures.new(Parrot.connection, 'parrots', 'Parrot', FIXTURES_ROOT + "/parrots") - assert_equal %w{parrots parrots_treasures}, fixtures.tables - end - def test_yml_file_in_subdirectory assert_equal(categories(:sub_special_1).name, "A special category in a subdir file") assert_equal(categories(:sub_special_1).class, SpecialCategory) -- cgit v1.2.3 From e4dac4750d4aed2356a6d76c11c592f031a47bdd Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 14 Feb 2011 16:01:47 -0800 Subject: removing irrelevant test --- activerecord/lib/active_record/fixtures.rb | 1 + .../test/cases/adapters/mysql/reserved_word_test.rb | 18 ------------------ 2 files changed, 1 insertion(+), 18 deletions(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 3876e57210..04bdd20c36 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -568,6 +568,7 @@ class Fixtures attr_reader :table_name, :name, :fixtures, :model_class def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) + raise if file_filter != DEFAULT_FILTER_RE @connection = connection @table_name = table_name @fixture_path = fixture_path diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index b5c938b14a..43015098c9 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -78,24 +78,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true self.use_transactional_fixtures = false - #fixtures :group - - def test_fixtures - f = create_test_fixtures :select, :distinct, :group, :values, :distincts_selects - - assert_nothing_raised { - f.each do |x| - x.delete_existing_fixtures - end - } - - assert_nothing_raised { - f.each do |x| - x.insert_fixtures - end - } - end - #activerecord model class with reserved-word table name def test_activerecord_model create_test_fixtures :select, :distinct, :group, :values, :distincts_selects -- cgit v1.2.3 From 02fc45a057878c49b6397900e5dcf828fdb8c09d Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 14 Feb 2011 16:48:18 -0800 Subject: remove accidental raise! --- activerecord/lib/active_record/fixtures.rb | 1 - 1 file changed, 1 deletion(-) (limited to 'activerecord') diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 04bdd20c36..3876e57210 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -568,7 +568,6 @@ class Fixtures attr_reader :table_name, :name, :fixtures, :model_class def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE) - raise if file_filter != DEFAULT_FILTER_RE @connection = connection @table_name = table_name @fixture_path = fixture_path -- cgit v1.2.3 From c9f1ab5365319e087e1b010a3f90626a2b8f080b Mon Sep 17 00:00:00 2001 From: Aaron Patterson Date: Mon, 14 Feb 2011 17:10:14 -0800 Subject: bad tests are bad --- .../test/cases/adapters/mysql2/reserved_word_test.rb | 18 ------------------ 1 file changed, 18 deletions(-) (limited to 'activerecord') diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 90d8b0d923..1efa7deaeb 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -78,24 +78,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true self.use_transactional_fixtures = false - #fixtures :group - - def test_fixtures - f = create_test_fixtures :select, :distinct, :group, :values, :distincts_selects - - assert_nothing_raised { - f.each do |x| - x.delete_existing_fixtures - end - } - - assert_nothing_raised { - f.each do |x| - x.insert_fixtures - end - } - end - #activerecord model class with reserved-word table name def test_activerecord_model create_test_fixtures :select, :distinct, :group, :values, :distincts_selects -- cgit v1.2.3