diff options
Diffstat (limited to 'activerecord')
18 files changed, 116 insertions, 47 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a0680cd86f..c7928e503b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -8,7 +8,7 @@ parentheses (eg. `($1.25) # => -1.25`)). Fixes #11899. - * Yves Senn* + *Yves Senn* * Stop interpreting SQL 'string' columns as :string type because there is no common STRING datatype in SQL. @@ -24,7 +24,6 @@ Example: record = User.new - record.logged_in_from_ip # is type of an inet or a cidr # Before: diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 8eec4f56af..e1fa5225b5 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -50,8 +50,8 @@ module ActiveRecord # Checks whether record is different to the current target, without loading it def different_target?(record) - if record.nil? - owner[reflection.foreign_key] + if record.nil? + owner[reflection.foreign_key] else record.id != owner[reflection.foreign_key] end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 2317e34bc0..6cc9d6c079 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -85,9 +85,11 @@ module ActiveRecord def initialize(records, associations, preload_scope = nil) @records = Array.wrap(records).compact.uniq @associations = Array.wrap(associations) - @preload_scope = preload_scope || Relation.create(nil, nil) + @preload_scope = preload_scope || NULL_RELATION end + NULL_RELATION = Struct.new(:values).new({}) + def run unless records.empty? associations.each { |association| preload(association) } diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 10238555f0..02dc464536 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -42,7 +42,6 @@ module ActiveRecord scope.first.tap { |record| set_inverse_instance(record) } end - # Implemented by subclasses def replace(record) raise NotImplementedError, "Subclasses must implement a replace(record) method" end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index b06add096f..04e3dd49e7 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -18,6 +18,7 @@ require 'arel' require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' +require 'active_record/relation/delegation' module ActiveRecord #:nodoc: # = Active Record @@ -290,6 +291,7 @@ module ActiveRecord #:nodoc: extend Translation extend DynamicMatchers extend Explain + extend Delegation::DelegateCache include Persistence include ReadonlyAttributes diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index f428f160cf..5245fc130a 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -422,7 +422,6 @@ module ActiveRecord def valid_inverse_reflection?(reflection) reflection && klass.name == reflection.active_record.name && - klass.primary_key == reflection.active_record_primary_key && can_find_inverse_of_automatically?(reflection) end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 52a538e5b5..27c04b0952 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -161,8 +161,7 @@ module ActiveRecord result = result.map do |attributes| values = klass.initialize_attributes(attributes).values - iter = columns.each - values.map { |value| iter.next.type_cast value } + columns.zip(values).map { |column, value| column.type_cast value } end columns.one? ? result.map!(&:first) : result end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index b6f80ac5c7..e28938f9d0 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,8 +1,33 @@ -require 'thread' -require 'thread_safe' +require 'active_support/concern' module ActiveRecord module Delegation # :nodoc: + module DelegateCache + def relation_delegate_class(klass) # :nodoc: + @relation_delegate_cache[klass] + end + + def initialize_relation_delegate_cache # :nodoc: + @relation_delegate_cache = cache = {} + [ + ActiveRecord::Relation, + ActiveRecord::Associations::CollectionProxy, + ActiveRecord::AssociationRelation + ].each do |klass| + delegate = Class.new(klass) { + include ClassSpecificRelation + } + const_set klass.name.gsub('::', '_'), delegate + cache[klass] = delegate + end + end + + def inherited(child_class) + child_class.initialize_relation_delegate_cache + super + end + end + extend ActiveSupport::Concern # This module creates compiled delegation methods dynamically at runtime, which makes @@ -71,32 +96,14 @@ module ActiveRecord end module ClassMethods # :nodoc: - @@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2) - def create(klass, *args) relation_class_for(klass).new(klass, *args) end private - # Cache the constants in @@subclasses because looking them up via const_get - # make instantiation significantly slower. + def relation_class_for(klass) - if klass && (klass_name = klass.name) - my_cache = @@subclasses.compute_if_absent(self) { ThreadSafe::Cache.new } - # This hash is keyed by klass.name to avoid memory leaks in development mode - my_cache.compute_if_absent(klass_name) do - # Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name - subclass_name = "#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}" - - if const_defined?(subclass_name) - const_get(subclass_name) - else - const_set(subclass_name, Class.new(self) { include ClassSpecificRelation }) - end - end - else - ActiveRecord::Relation - end + klass.relation_delegate_class(self) end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index c08158d38b..530c47d0d0 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -107,11 +107,11 @@ module ActiveRecord bind_values = filter_binds(lhs_binds, removed) + rhs_binds conn = relation.klass.connection - bviter = bind_values.each.with_index + bv_index = 0 where_values.map! do |node| if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right - (column, _), i = bviter.next - substitute = conn.substitute_at column, i + substitute = conn.substitute_at(bind_values[bv_index].first, bv_index) + bv_index += 1 Arel::Nodes::Equality.new(node.left, substitute) else node diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index a5d6aad3f0..01fec31544 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -100,11 +100,7 @@ module ActiveRecord elsif default_scopes.any? evaluate_default_scope do default_scopes.inject(relation) do |default_scope, scope| - if !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(unscoped { scope.call }) - else - default_scope.merge(scope) - end + default_scope.merge(unscoped { scope.call }) end end end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index a534f0e56a..1122f8b9a1 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -52,6 +52,11 @@ module ActiveRecord c = Column.new(nil, nil, 'text') assert_equal "'666'", @conn.quote(fixnum, c) end + + def test_quote_time_usec + assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) + assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) + end end 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 941e851aae..c00cae5750 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -28,7 +28,8 @@ require 'models/club' class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, :owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses, - :subscribers, :books, :subscriptions, :developers, :categorizations, :essays + :subscribers, :books, :subscriptions, :developers, :categorizations, :essays, + :categories_posts # Dummies to force column loads so query counts are clean. def setup @@ -36,6 +37,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create :person_id => 0, :post_id => 0 end + def test_pk_is_not_required_for_join + post = Post.includes(:scategories).first + post2 = Post.includes(:categories).first + + assert_operator post.categories.length, :>, 0 + assert_equal post2.categories, post.categories + end + def test_include? person = Person.new post = Post.new diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 9b1abc3b81..cf3c07845c 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -371,7 +371,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase prev_default_scope = Club.default_scopes [:includes, :preload, :joins, :eager_load].each do |q| - Club.default_scopes = [Club.send(q, :category)] + Club.default_scopes = [proc { Club.send(q, :category) }] assert_equal categories(:general), members(:groucho).reload.club_category end ensure diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index c91cf89f6d..dc2de7b523 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1324,6 +1324,35 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 1, post.comments.length end + def test_marshal_between_processes + skip "fork isn't supported" unless Process.respond_to?(:fork) + + # Define a new model to ensure there are no caches + if self.class.const_defined?("Post", false) + flunk "there should be no post constant" + end + + self.class.const_set("Post", Class.new(ActiveRecord::Base) { + has_many :comments + }) + + rd, wr = IO.pipe + + ActiveRecord::Base.connection_handler.clear_all_connections! + + fork do + rd.close + post = Post.new + post.comments.build + wr.write Marshal.dump(post) + wr.close + end + + wr.close + assert Marshal.load rd.read + rd.close + end + def test_marshalling_new_record_round_trip_with_associations post = Post.new post.comments.build diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index f73e449610..1e6ccecfab 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -126,7 +126,7 @@ module ActiveRecord def test_dup_with_default_scope prev_default_scopes = Topic.default_scopes - Topic.default_scopes = [Topic.where(:approved => true)] + Topic.default_scopes = [proc { Topic.where(:approved => true) }] topic = Topic.new(:approved => false) assert !topic.dup.approved?, "should not be overridden by default scopes" ensure diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index f99801c437..a695087426 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -9,6 +9,10 @@ module ActiveRecord fixtures :posts, :comments, :authors class FakeKlass < Struct.new(:table_name, :name) + extend ActiveRecord::Delegation::DelegateCache + + inherited self + def self.connection Post.connection end @@ -76,8 +80,8 @@ module ActiveRecord end def test_table_name_delegates_to_klass - relation = Relation.new FakeKlass.new('foo'), :b - assert_equal 'foo', relation.table_name + relation = Relation.new FakeKlass.new('posts'), :b + assert_equal 'posts', relation.table_name end def test_scope_for_create @@ -177,8 +181,12 @@ module ActiveRecord end test 'merging a hash interpolates conditions' do - klass = stub_everything - klass.stubs(:sanitize_sql).with(['foo = ?', 'bar']).returns('foo = bar') + klass = Class.new(FakeKlass) do + def self.sanitize_sql(args) + raise unless args == ['foo = ?', 'bar'] + 'foo = bar' + end + end relation = Relation.new(klass, :b) relation.merge!(where: ['foo = ?', 'bar']) @@ -210,6 +218,9 @@ module ActiveRecord class RelationMutationTest < ActiveSupport::TestCase class FakeKlass < Struct.new(:table_name, :name) + extend ActiveRecord::Delegation::DelegateCache + inherited self + def arel_table Post.arel_table end @@ -217,6 +228,10 @@ module ActiveRecord def connection Post.connection end + + def relation_delegate_class(klass) + self.class.relation_delegate_class(klass) + end end def relation diff --git a/activerecord/test/fixtures/tasks.yml b/activerecord/test/fixtures/tasks.yml index 402ca85faf..c38b32b0e5 100644 --- a/activerecord/test/fixtures/tasks.yml +++ b/activerecord/test/fixtures/tasks.yml @@ -1,4 +1,4 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html first_task: id: 1 starting: 2005-03-30t06:30:00.00+01:00 diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 93a7a2073c..25fb2cffe2 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -1,4 +1,10 @@ class Post < ActiveRecord::Base + class CategoryPost < ActiveRecord::Base + self.table_name = "categories_posts" + belongs_to :category + belongs_to :post + end + module NamedExtension def author 'lifo' @@ -72,6 +78,8 @@ class Post < ActiveRecord::Base has_many :special_comments_ratings, :through => :special_comments, :source => :ratings has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings + has_many :category_posts, :class_name => 'CategoryPost' + has_many :scategories, through: :category_posts, source: :category has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' |