diff options
Diffstat (limited to 'activerecord')
20 files changed, 162 insertions, 40 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index e9e97f5d62..69cf1193b6 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,18 @@ ## Rails 4.0.0 (unreleased) ## +* Implemented ActiveRecord::Relation#none method + + The `none` method returns a chainable relation with zero records + (an instance of the NullRelation class). + + Any subsequent condition chained to the returned relation will continue + generating an empty relation and will not fire any query to the database. + + *Juanjo Bazán* + +* Added the `ActiveRecord::NullRelation` class implementing the null + object pattern for the Relation class. *Juanjo Bazán* + * Added deprecation for the `:dependent => :restrict` association option. Please note: diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 78e958442f..73c8a06ab7 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -43,6 +43,7 @@ module ActiveRecord autoload :AutosaveAssociation autoload :Relation + autoload :NullRelation autoload_under 'relation' do autoload :QueryMethods diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index ba2fcf5ed7..c3fa4a05fd 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -58,9 +58,9 @@ module ActiveRecord::Associations::Builder def dependent_restrict_deprecation_warning if dependent_restrict_raises? - msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`."\ - "Instead, it will add an error on the model. To fix this warning, make sure your code" \ - "isn't relying on a `DeleteRestrictionError` and then add" \ + msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`. "\ + "Instead, it will add an error on the model. To fix this warning, make sure your code " \ + "isn't relying on a `DeleteRestrictionError` and then add " \ "`config.active_record.dependent_restrict_raises = false` to your application config." ActiveSupport::Deprecation.warn msg end @@ -80,5 +80,5 @@ module ActiveRecord::Associations::Builder end end end - end + end end 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 52f09efd53..6ba64bb88f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -65,18 +65,24 @@ module ActiveRecord end private - def cache_sql(sql, binds) - result = - if @query_cache[sql].key?(binds) - ActiveSupport::Notifications.instrument("sql.active_record", - :sql => sql, :binds => binds, :name => "CACHE", :connection_id => object_id) - @query_cache[sql][binds] - else - @query_cache[sql][binds] = yield - end + def cache_sql(sql, binds) + result = + if @query_cache[sql].key?(binds) + ActiveSupport::Notifications.instrument("sql.active_record", + :sql => sql, :binds => binds, :name => "CACHE", :connection_id => object_id) + @query_cache[sql][binds] + else + @query_cache[sql][binds] = yield + end + # FIXME: we should guarantee that all cached items are Result + # objects. Then we can avoid this conditional + if ActiveRecord::Result === result + result.dup + else result.collect { |row| row.dup } end + end 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 6086c32dbe..c1332fde1a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -225,7 +225,7 @@ module ActiveRecord # column values as values. def select(sql, name = nil, binds = []) binds = binds.dup - exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a + exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name) end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e432c5af32..5905242747 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -408,7 +408,7 @@ module ActiveRecord def select(sql, name = nil, binds = []) @connection.query_with_result = true - rows = exec_query(sql, name, binds).to_a + rows = exec_query(sql, name, binds) @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index dbfb375ba8..1d8e5d813a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1249,7 +1249,7 @@ module ActiveRecord # Executes a SELECT query and returns the results, performing any data type # conversions that are required to be performed here instead of in PostgreSQLColumn. def select(sql, name = nil, binds = []) - exec_query(sql, name, binds).to_a + exec_query(sql, name, binds) end def select_raw(sql, name = nil) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 69750a911d..0520fc8b62 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -454,7 +454,7 @@ module ActiveRecord protected def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds).to_a + exec_query(sql, name, binds) end def table_structure(table_name) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index e643c0d437..4d73cdd37a 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -40,11 +40,13 @@ module ActiveRecord # This locking mechanism will function inside a single Ruby process. To make it work across all # web requests, the recommended approach is to add +lock_version+ as a hidden field to your form. # - # You must ensure that your database schema defaults the +lock_version+ column to 0. - # # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>. - # To override the name of the +lock_version+ column, invoke the <tt>set_locking_column</tt> method. - # This method uses the same syntax as <tt>set_table_name</tt> + # To override the name of the +lock_version+ column, set the <tt>locking_column</tt> class attribute: + # + # class Person < ActiveRecord::Base + # self.locking_column = :lock_person + # end + # module Optimistic extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb new file mode 100644 index 0000000000..60c37ac2b7 --- /dev/null +++ b/activerecord/lib/active_record/null_relation.rb @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +module ActiveRecord + # = Active Record Null Relation + class NullRelation < Relation + def exec_queries + @records = [] + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 94e34e1bd4..5945b05190 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,7 +8,7 @@ module ActiveRecord delegate :find_each, :find_in_batches, :to => :scoped delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :references, :to => :scoped + :having, :create_with, :uniq, :references, :none, :to => :scoped delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped # Executes a custom SQL query against your database and returns all the results. The results will @@ -35,7 +35,8 @@ module ActiveRecord # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...] def find_by_sql(sql, binds = []) logging_query_plan do - connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) + result_set.map { |record| instantiate(record) } end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index a8ae7208fc..b6d762c2e2 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -196,6 +196,39 @@ module ActiveRecord relation end + # Returns a chainable relation with zero records, specifically an + # instance of the NullRelation class. + # + # The returned NullRelation inherits from Relation and implements the + # Null Object pattern so it is an object with defined null behavior: + # it always returns an empty array of records and does not query the database. + # + # Any subsequent condition chained to the returned relation will continue + # generating an empty relation and will not fire any query to the database. + # + # This is useful in scenarios where you need a chainable response to a method + # or a scope that could return zero results. + # + # For example: + # + # @posts = current_user.visible_posts.where(:name => params[:name]) + # # => the visible_posts method is expected to return a chainable Relation + # + # def visible_posts + # case role + # when 'Country Manager' + # Post.where(:country => country) + # when 'Reviewer' + # Post.published + # when 'Bad User' + # Post.none # => returning [] instead breaks the previous code + # end + # end + # + def none + NullRelation.new(@klass, @table) + end + def readonly(value = true) relation = clone relation.readonly_value = value diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 9ceab2eabc..60a2e90e23 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -24,6 +24,31 @@ module ActiveRecord hash_rows end + alias :map! :map + alias :collect! :map + + def empty? + rows.empty? + end + + def to_ary + hash_rows + end + + def [](idx) + hash_rows[idx] + end + + def last + hash_rows.last + end + + def initialize_copy(other) + @columns = columns.dup + @rows = rows.dup + @hash_rows = nil + end + private def hash_rows @hash_rows ||= @rows.map { |row| diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 367f87031c..ab1e821aab 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1144,7 +1144,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_restrict - # ActiveRecord::Base.dependent_restrict_raises = true, by default + option_before = ActiveRecord::Base.dependent_restrict_raises + ActiveRecord::Base.dependent_restrict_raises = true firm = RestrictedFirm.create!(:name => 'restrict') firm.companies.create(:name => 'child') @@ -1153,13 +1154,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } assert RestrictedFirm.exists?(:name => 'restrict') assert firm.companies.exists?(:name => 'child') + ensure + ActiveRecord::Base.dependent_restrict_raises = option_before end def test_restrict_when_dependent_restrict_raises_config_set_to_false - # ActiveRecord::Base.dependent_restrict_raises = true, by default - + option_before = ActiveRecord::Base.dependent_restrict_raises ActiveRecord::Base.dependent_restrict_raises = false - # add an error on the model instead of raising ActiveRecord::DeleteRestrictionError firm = RestrictedFirm.create!(:name => 'restrict') firm.companies.create(:name => 'child') @@ -1174,7 +1175,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert RestrictedFirm.exists?(:name => 'restrict') assert firm.companies.exists?(:name => 'child') ensure - ActiveRecord::Base.dependent_restrict_raises = true + ActiveRecord::Base.dependent_restrict_raises = option_before end def test_included_in_collection @@ -1279,7 +1280,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_get_ids_for_ordered_association - assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_rating_ids + assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids end def test_assign_ids_ignoring_blanks @@ -1680,9 +1681,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_building_has_many_association_with_restrict_dependency + option_before = ActiveRecord::Base.dependent_restrict_raises + ActiveRecord::Base.dependent_restrict_raises = true + klass = Class.new(ActiveRecord::Base) - - assert_deprecated { klass.has_many :companies, :dependent => :restrict } + + assert_deprecated { klass.has_many :companies, :dependent => :restrict } assert_not_deprecated { klass.has_many :companies } + ensure + ActiveRecord::Base.dependent_restrict_raises = option_before 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 7c6736fb95..37be6a279d 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -157,7 +157,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end def test_dependence_with_restrict - # ActiveRecord::Base.dependent_restrict_raises = true, by default + option_before = ActiveRecord::Base.dependent_restrict_raises + ActiveRecord::Base.dependent_restrict_raises = true firm = RestrictedFirm.create!(:name => 'restrict') firm.create_account(:credit_limit => 10) @@ -167,13 +168,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy } assert RestrictedFirm.exists?(:name => 'restrict') assert firm.account.present? + ensure + ActiveRecord::Base.dependent_restrict_raises = option_before end def test_dependence_with_restrict_with_dependent_restrict_raises_config_set_to_false - # ActiveRecord::Base.dependent_restrict_raises = true, by default - + option_before = ActiveRecord::Base.dependent_restrict_raises ActiveRecord::Base.dependent_restrict_raises = false - # adds an error on the model instead of raising ActiveRecord::DeleteRestrictionError firm = RestrictedFirm.create!(:name => 'restrict') firm.create_account(:credit_limit => 10) @@ -187,7 +188,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert RestrictedFirm.exists?(:name => 'restrict') assert firm.account.present? ensure - ActiveRecord::Base.dependent_restrict_raises = true + ActiveRecord::Base.dependent_restrict_raises = option_before end def test_successful_build_association @@ -484,9 +485,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end def test_building_has_one_association_with_dependent_restrict + option_before = ActiveRecord::Base.dependent_restrict_raises + ActiveRecord::Base.dependent_restrict_raises = true + klass = Class.new(ActiveRecord::Base) - + assert_deprecated { klass.has_one :account, :dependent => :restrict } assert_not_deprecated { klass.has_one :account } + ensure + ActiveRecord::Base.dependent_restrict_raises = option_before end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 00a6d97928..6c3e4fc6d0 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1104,7 +1104,10 @@ class BasicsTest < ActiveRecord::TestCase # TODO: extend defaults tests to other databases! if current_adapter?(:PostgreSQLAdapter) def test_default + tz = Default.default_timezone + Default.default_timezone = :local default = Default.new + Default.default_timezone = tz # fixed dates / times assert_equal Date.new(2004, 1, 1), default.fixed_date diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index b4598ab32a..734d017b6e 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -22,6 +22,9 @@ ActiveSupport::Deprecation.debug = true # Enable Identity Map only when ENV['IM'] is set to "true" ActiveRecord::IdentityMap.enabled = (ENV['IM'] == "true") +# Avoid deprecation warning setting dependent_restrict_raises to false. The default is true +ActiveRecord::Base.dependent_restrict_raises = false + # Connect to the database ARTest.connect diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 297fb56570..16f05f2198 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -189,8 +189,8 @@ class ReflectionTest < ActiveRecord::TestCase def test_reflection_of_all_associations # FIXME these assertions bust a lot - assert_equal 38, Firm.reflect_on_all_associations.size - assert_equal 28, Firm.reflect_on_all_associations(:has_many).size + assert_equal 39, Firm.reflect_on_all_associations.size + assert_equal 29, 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/relations_test.rb b/activerecord/test/cases/relations_test.rb index 5e19465253..0471d03f3b 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -215,6 +215,19 @@ class RelationTest < ActiveRecord::TestCase assert_equal [2, 4, 6, 8, 10], even_ids.sort end + def test_none + assert_no_queries do + assert_equal [], Developer.none + assert_equal [], Developer.scoped.none + end + end + + def test_none_chainable + assert_no_queries do + assert_equal [], Developer.none.where(:name => 'David') + end + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 764f1c1df5..fbdfaa2c29 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -45,7 +45,7 @@ class Firm < Company has_many :unsorted_clients_with_symbol, :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" - has_many :clients_ordered_by_rating, :order => "rating", :class_name => "Client" + has_many :clients_ordered_by_name, :order => "name", :class_name => "Client" has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false 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 |