aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorCarl Lerche & Yehuda Katz <wycats@gmail.com>2009-04-13 15:18:45 -0700
committerCarl Lerche & Yehuda Katz <wycats@gmail.com>2009-04-13 15:18:45 -0700
commit906aebceedb95d8caa6db6314bc90f605bdfaf2b (patch)
tree5abc86bb6709b20df7cb5f4d1750b27c641dca4b /activerecord
parent2036d3ba75da1a0f3061bf5a33c89e2b2eaff420 (diff)
parentc877857d59554d78dbf45f5f9fcaafb8badec4e2 (diff)
downloadrails-906aebceedb95d8caa6db6314bc90f605bdfaf2b.tar.gz
rails-906aebceedb95d8caa6db6314bc90f605bdfaf2b.tar.bz2
rails-906aebceedb95d8caa6db6314bc90f605bdfaf2b.zip
Bring abstract_controller up to date with rails/master
Resolved all the conflicts since 2.3.0 -> HEAD. Following is a list of commits that could not be applied cleanly or are obviated with the abstract_controller refactor. They all need to be revisited to ensure that fixes made in 2.3 do not reappear in 3.0: 2259ecf368e6a6715966f69216e3ee86bf1a82a7 AR not available * This will be reimplemented with ActionORM or equivalent 06182ea02e92afad579998aa80144588e8865ac3 implicitly rendering a js response should not use the default layout [#1844 state:resolved] * This will be handled generically 893e9eb99504705419ad6edac14d00e71cef5f12 Improve view rendering performance in development mode and reinstate template recompiling in production [#1909 state:resolved] * We will need to reimplement rails-dev-boost on top of the refactor; the changes here are very implementation specific and cannot be cleanly applied. The following commits are implicated: 199e750d46c04970b5e7684998d09405648ecbd4 3942cb406e1d5db0ac00e03153809cc8dc4cc4db f8ea9f85d4f1e3e6f3b5d895bef6b013aa4b0690 e3b166aab37ddc2fbab030b146eb61713b91bf55 ae9f258e03c9fd5088da12c1c6cd216cc89a01f7 44423126c6f6133a1d9cf1d0832b527e8711d40f 0cb020b4d6d838025859bd60fb8151c8e21b8e84 workaround for picking layouts based on wrong view_paths [#1974 state:resolved] * The specifics of this commit no longer apply. Since it is a two-line commit, we will reimplement this change. 8c5cc66a831aadb159f3daaffa4208064c30af0e make action_controller/layouts pick templates from the current instance's view_paths instead of the class view_paths [#1974 state:resolved] * This does not apply at all. It should be trivial to apply the feature to the reimplemented ActionController::Base. 87e8b162463f13bd50d27398f020769460a770e3 fix HTML fallback for explicit templates [#2052 state:resolved] * There were a number of patches related to this that simply compounded each other. Basically none of them apply cleanly, and the underlying issue needs to be revisited. After discussing the underlying problem with Koz, we will defer these fixes for further discussion.
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG15
-rw-r--r--activerecord/Rakefile6
-rw-r--r--activerecord/lib/active_record.rb3
-rwxr-xr-xactiverecord/lib/active_record/associations.rb312
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb57
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb10
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb13
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb1
-rw-r--r--activerecord/lib/active_record/autosave_association.rb349
-rwxr-xr-xactiverecord/lib/active_record/base.rb98
-rw-r--r--activerecord/lib/active_record/batches.rb81
-rw-r--r--activerecord/lib/active_record/calculations.rb26
-rw-r--r--activerecord/lib/active_record/callbacks.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb64
-rw-r--r--activerecord/lib/active_record/fixtures.rb103
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb33
-rw-r--r--activerecord/lib/active_record/migration.rb6
-rw-r--r--activerecord/lib/active_record/named_scope.rb30
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb329
-rw-r--r--activerecord/lib/active_record/reflection.rb7
-rw-r--r--activerecord/lib/active_record/serializers/json_serializer.rb19
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb18
-rw-r--r--activerecord/lib/active_record/session_store.rb14
-rw-r--r--activerecord/lib/active_record/test_case.rb14
-rw-r--r--activerecord/lib/active_record/transactions.rb2
-rw-r--r--activerecord/lib/active_record/validations.rb28
-rw-r--r--activerecord/lib/active_record/version.rb2
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb166
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb29
-rw-r--r--activerecord/test/cases/associations/eager_test.rb8
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb47
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb232
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb39
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb131
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb20
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb12
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb14
-rw-r--r--activerecord/test/cases/autosave_association_test.rb901
-rwxr-xr-xactiverecord/test/cases/base_test.rb44
-rw-r--r--activerecord/test/cases/batches_test.rb61
-rw-r--r--activerecord/test/cases/calculations_test.rb55
-rw-r--r--activerecord/test/cases/callbacks_test.rb4
-rw-r--r--activerecord/test/cases/connection_pool_test.rb25
-rw-r--r--activerecord/test/cases/datatype_test_postgresql.rb1
-rw-r--r--activerecord/test/cases/dirty_test.rb4
-rw-r--r--activerecord/test/cases/finder_test.rb116
-rw-r--r--activerecord/test/cases/fixtures_test.rb34
-rw-r--r--activerecord/test/cases/helper.rb13
-rw-r--r--activerecord/test/cases/inheritance_test.rb4
-rw-r--r--activerecord/test/cases/locking_test.rb28
-rw-r--r--activerecord/test/cases/method_scoping_test.rb46
-rw-r--r--activerecord/test/cases/migration_test.rb490
-rw-r--r--activerecord/test/cases/named_scope_test.rb55
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb509
-rw-r--r--activerecord/test/cases/query_cache_test.rb4
-rw-r--r--activerecord/test/cases/reflection_test.rb16
-rw-r--r--activerecord/test/cases/schema_test_postgresql.rb44
-rw-r--r--activerecord/test/cases/transactions_test.rb49
-rw-r--r--activerecord/test/cases/validations_i18n_test.rb580
-rw-r--r--activerecord/test/cases/validations_test.rb61
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb13
-rw-r--r--activerecord/test/fixtures/toys.yml4
-rw-r--r--activerecord/test/models/bird.rb3
-rw-r--r--activerecord/test/models/company.rb2
-rw-r--r--activerecord/test/models/event.rb3
-rw-r--r--activerecord/test/models/owner.rb3
-rw-r--r--activerecord/test/models/parrot.rb2
-rw-r--r--activerecord/test/models/pet.rb3
-rw-r--r--activerecord/test/models/pirate.rb56
-rw-r--r--activerecord/test/models/post.rb6
-rw-r--r--activerecord/test/models/reply.rb6
-rw-r--r--activerecord/test/models/ship.rb9
-rw-r--r--activerecord/test/models/ship_part.rb5
-rw-r--r--activerecord/test/models/topic.rb12
-rw-r--r--activerecord/test/models/toy.rb4
-rw-r--r--activerecord/test/schema/schema.rb20
82 files changed, 4197 insertions, 1532 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 507e37ac3b..c73ac4649e 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,4 +1,17 @@
-*2.3.0/3.0*
+*2.3.2 [Final] (March 15, 2009)*
+
+* Added ActiveRecord::Base.find_each and ActiveRecord::Base.find_in_batches for batch processing [DHH/Jamis Buck]
+
+* Added that ActiveRecord::Base.exists? can be called with no arguments #1817 [Scott Taylor]
+
+* Add Support for updating deeply nested models from a single form. #1202 [Eloy Duran]
+
+ class Book < ActiveRecord::Base
+ has_one :author
+ has_many :pages
+
+ accepts_nested_attributes_for :author, :pages
+ end
* Make after_save callbacks fire only if the record was successfully saved. #1735 [Michael Lovitt]
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 1c7e2603ee..b50008c971 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -30,7 +30,9 @@ desc 'Run mysql, sqlite, and postgresql tests by default'
task :default => :test
desc 'Run mysql, sqlite, and postgresql tests'
-task :test => %w(test_mysql test_sqlite3 test_postgresql)
+task :test => defined?(JRUBY_VERSION) ?
+ %w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) :
+ %w(test_mysql test_sqlite3 test_postgresql)
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb )
Rake::TestTask.new("test_#{adapter}") { |t|
@@ -175,7 +177,7 @@ spec = Gem::Specification.new do |s|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
end
- s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD)
+ s.add_dependency('activesupport', '= 2.3.2' + PKG_BUILD)
s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index e1265b7e1e..2f8c5c712f 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -46,7 +46,9 @@ module ActiveRecord
autoload :AssociationPreload, 'active_record/association_preload'
autoload :Associations, 'active_record/associations'
autoload :AttributeMethods, 'active_record/attribute_methods'
+ autoload :AutosaveAssociation, 'active_record/autosave_association'
autoload :Base, 'active_record/base'
+ autoload :Batches, 'active_record/batches'
autoload :Calculations, 'active_record/calculations'
autoload :Callbacks, 'active_record/callbacks'
autoload :Dirty, 'active_record/dirty'
@@ -55,6 +57,7 @@ module ActiveRecord
autoload :Migration, 'active_record/migration'
autoload :Migrator, 'active_record/migration'
autoload :NamedScope, 'active_record/named_scope'
+ autoload :NestedAttributes, 'active_record/nested_attributes'
autoload :Observing, 'active_record/observer'
autoload :QueryCache, 'active_record/query_cache'
autoload :Reflection, 'active_record/reflection'
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 8b51a38f48..6d25b36aea 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -22,7 +22,7 @@ module ActiveRecord
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
- super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '}?")
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
end
end
@@ -51,6 +51,12 @@ module ActiveRecord
end
end
+ class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
+ def initialize(reflection)
+ super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
+ end
+ end
+
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
@@ -65,7 +71,7 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods for documentation.
module Associations # :nodoc:
- # These classes will be loaded when associatoins are created.
+ # 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 :AssociationProxy, 'active_record/associations/association_proxy'
@@ -88,6 +94,18 @@ module ActiveRecord
end unless self.new_record?
end
+ private
+ # Gets the specified association instance if it responds to :loaded?, nil otherwise.
+ def association_instance_get(name)
+ association = instance_variable_get("@#{name}")
+ association if association.respond_to?(:loaded?)
+ end
+
+ # Set the specified association instance.
+ def association_instance_set(name, association)
+ instance_variable_set("@#{name}", association)
+ end
+
# Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like
# "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are
# specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own <tt>attr*</tt>
@@ -121,41 +139,40 @@ module ActiveRecord
# | | belongs_to |
# generated methods | belongs_to | :polymorphic | has_one
# ----------------------------------+------------+--------------+---------
- # #other | X | X | X
- # #other=(other) | X | X | X
- # #build_other(attributes={}) | X | | X
- # #create_other(attributes={}) | X | | X
- # #other.create!(attributes={}) | | | X
- # #other.nil? | X | X |
+ # other | X | X | X
+ # other=(other) | X | X | X
+ # build_other(attributes={}) | X | | X
+ # create_other(attributes={}) | X | | X
+ # other.create!(attributes={}) | | | X
#
# ===Collection associations (one-to-many / many-to-many)
# | | | has_many
# generated methods | habtm | has_many | :through
# ----------------------------------+-------+----------+----------
- # #others | X | X | X
- # #others=(other,other,...) | X | X | X
- # #other_ids | X | X | X
- # #other_ids=(id,id,...) | X | X | X
- # #others<< | X | X | X
- # #others.push | X | X | X
- # #others.concat | X | X | X
- # #others.build(attributes={}) | X | X | X
- # #others.create(attributes={}) | X | X | X
- # #others.create!(attributes={}) | X | X | X
- # #others.size | X | X | X
- # #others.length | X | X | X
- # #others.count | X | X | X
- # #others.sum(args*,&block) | X | X | X
- # #others.empty? | X | X | X
- # #others.clear | X | X | X
- # #others.delete(other,other,...) | X | X | X
- # #others.delete_all | 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
+ # others | X | X | X
+ # others=(other,other,...) | X | X | X
+ # other_ids | X | X | X
+ # other_ids=(id,id,...) | X | X | X
+ # others<< | X | X | X
+ # others.push | X | X | X
+ # others.concat | X | X | X
+ # others.build(attributes={}) | X | X | X
+ # others.create(attributes={}) | X | X | X
+ # others.create!(attributes={}) | X | X | X
+ # others.size | X | X | X
+ # others.length | X | X | X
+ # others.count | X | X | X
+ # others.sum(args*,&block) | X | X | X
+ # others.empty? | X | X | X
+ # others.clear | X | X | X
+ # others.delete(other,other,...) | X | X | X
+ # others.delete_all | 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
#
# == Cardinality and associations
#
@@ -256,6 +273,10 @@ module ActiveRecord
# You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
# aware of, mostly involving the saving of associated objects.
#
+ # Unless you enable the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
+ # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association,
+ # in which case the members are always saved.
+ #
# === 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
@@ -752,6 +773,9 @@ module ActiveRecord
# If true, all the associated objects are readonly through the association.
# [:validate]
# If false, don't validate the associated objects when saving the parent object. true by default.
+ # [:autosave]
+ # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
+ #
# Option examples:
# has_many :comments, :order => "posted_on"
# has_many :comments, :include => :author
@@ -768,11 +792,7 @@ module ActiveRecord
# 'ORDER BY p.first_name'
def has_many(association_id, options = {}, &extension)
reflection = create_has_many_reflection(association_id, options, &extension)
-
configure_dependency_for_has_many(reflection)
-
- add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
- add_multiple_associated_save_callbacks(reflection.name)
add_association_callbacks(reflection.name, reflection.options)
if options[:through]
@@ -794,8 +814,6 @@ module ActiveRecord
# [association=(associate)]
# Assigns the associate object, extracts the primary key, sets it as the foreign key,
# and saves the associate object.
- # [association.nil?]
- # Returns +true+ if there is no associated object.
# [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
@@ -814,7 +832,6 @@ module ActiveRecord
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
# * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
- # * <tt>Account#beneficiary.nil?</tt>
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
#
@@ -857,14 +874,16 @@ module ActiveRecord
# [:source]
# Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be
# inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a
- # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
+ # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
# [:source_type]
# Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
- # association is a polymorphic +belongs_to+.
+ # association is a polymorphic +belongs_to+.
# [:readonly]
# If true, the associated object is readonly through the association.
# [:validate]
# If false, don't validate the associated object when saving the parent object. +false+ by default.
+ # [:autosave]
+ # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
@@ -881,25 +900,9 @@ module ActiveRecord
association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation)
else
reflection = create_has_one_reflection(association_id, options)
-
- ivar = "@#{reflection.name}"
-
- method_name = "has_one_after_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- if !association.nil? && (new_record? || association.new_record? || association[reflection.primary_key_name] != id)
- association[reflection.primary_key_name] = id
- association.save(true)
- end
- end
- after_save method_name
-
- add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
association_accessor_methods(reflection, HasOneAssociation)
association_constructor_method(:build, reflection, HasOneAssociation)
association_constructor_method(:create, reflection, HasOneAssociation)
-
configure_dependency_for_has_one(reflection)
end
end
@@ -916,8 +919,6 @@ module ActiveRecord
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
# Assigns the associate object, extracts the primary key, and sets it as the foreign key.
- # [association.nil?]
- # Returns +true+ if there is no associated object.
# [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.
@@ -935,7 +936,6 @@ module ActiveRecord
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
- # * <tt>Post#author.nil?</tt>
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
@@ -979,6 +979,8 @@ module ActiveRecord
# If true, the associated object is readonly through the association.
# [:validate]
# If false, don't validate the associated objects when saving the parent object. +false+ by default.
+ # [:autosave]
+ # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
@@ -991,54 +993,17 @@ module ActiveRecord
def belongs_to(association_id, options = {})
reflection = create_belongs_to_reflection(association_id, options)
- ivar = "@#{reflection.name}"
-
if reflection.options[:polymorphic]
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
-
- method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- if association && association.target
- if association.new_record?
- association.save(true)
- end
-
- if association.updated?
- self[reflection.primary_key_name] = association.id
- self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
- end
- end
- end
- before_save method_name
else
association_accessor_methods(reflection, BelongsToAssociation)
association_constructor_method(:build, reflection, BelongsToAssociation)
association_constructor_method(:create, reflection, BelongsToAssociation)
-
- method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- if !association.nil?
- if association.new_record?
- association.save(true)
- end
-
- if association.updated?
- self[reflection.primary_key_name] = association.id
- end
- end
- end
- before_save method_name
end
# Create the callbacks to update counter cache
if options[:counter_cache]
- cache_column = options[:counter_cache] == true ?
- "#{self.to_s.demodulize.underscore.pluralize}_count" :
- options[:counter_cache]
+ cache_column = reflection.counter_cache_column
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
define_method(method_name) do
@@ -1059,8 +1024,6 @@ module ActiveRecord
)
end
- add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
-
configure_dependency_for_belongs_to(reflection)
end
@@ -1075,6 +1038,22 @@ module ActiveRecord
# but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the
# custom <tt>:join_table</tt> option if you need to.
#
+ # The join table should not have a primary key or a model associated with it. You must manually generate the
+ # join table with a migration such as this:
+ #
+ # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
+ # def self.up
+ # create_table :developers_projects, :id => false do |t|
+ # t.integer :developer_id
+ # t.integer :project_id
+ # end
+ # end
+ #
+ # def self.down
+ # drop_table :developers_projects
+ # 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
@@ -1151,11 +1130,12 @@ module ActiveRecord
# [:foreign_key]
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_and_belongs_to_many+ association
- # will use "person_id" as the default <tt>:foreign_key</tt>.
+ # to Project will use "person_id" as the default <tt>:foreign_key</tt>.
# [:association_foreign_key]
- # Specify the association foreign key used for the association. By default this is
- # guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is Project,
- # the +has_and_belongs_to_many+ association will use "project_id" as the default <tt>:association_foreign_key</tt>.
+ # Specify the foreign key used for the association on the receiving side of the association.
+ # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
+ # So if a Person class makes a +has_and_belongs_to_many+ association to Project,
+ # the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
# [:conditions]
# Specify the conditions that the associated object must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used.
@@ -1196,6 +1176,8 @@ module ActiveRecord
# If true, all the associated objects are readonly through the association.
# [:validate]
# If false, don't validate the associated objects when saving the parent object. +true+ by default.
+ # [:autosave]
+ # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.
#
# Option examples:
# has_and_belongs_to_many :projects
@@ -1207,9 +1189,6 @@ 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)
-
- add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
- add_multiple_associated_save_callbacks(reflection.name)
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
# Don't use a before_destroy callback since users' before_destroy
@@ -1243,33 +1222,30 @@ module ActiveRecord
end
def association_accessor_methods(reflection, association_proxy_class)
- ivar = "@#{reflection.name}"
-
define_method(reflection.name) do |*params|
force_reload = params.first unless params.empty?
-
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
if association.nil? || force_reload
association = association_proxy_class.new(self, reflection)
retval = association.reload
if retval.nil? and association_proxy_class == BelongsToAssociation
- instance_variable_set(ivar, nil)
+ association_instance_set(reflection.name, nil)
return nil
end
- instance_variable_set(ivar, association)
+ association_instance_set(reflection.name, association)
end
association.target.nil? ? nil : association
end
define_method("loaded_#{reflection.name}?") do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
association && association.loaded?
end
define_method("#{reflection.name}=") do |new_value|
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
if association.nil? || association.target != new_value
association = association_proxy_class.new(self, reflection)
@@ -1280,7 +1256,7 @@ module ActiveRecord
self.send(reflection.name, new_value)
else
association.replace(new_value)
- instance_variable_set(ivar, new_value.nil? ? nil : association)
+ association_instance_set(reflection.name, new_value.nil? ? nil : association)
end
end
@@ -1288,20 +1264,18 @@ module ActiveRecord
return if target.nil? and association_proxy_class == BelongsToAssociation
association = association_proxy_class.new(self, reflection)
association.target = target
- instance_variable_set(ivar, association)
+ association_instance_set(reflection.name, association)
end
end
def collection_reader_method(reflection, association_proxy_class)
define_method(reflection.name) do |*params|
- ivar = "@#{reflection.name}"
-
force_reload = params.first unless params.empty?
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
- unless association.respond_to?(:loaded?)
+ unless association
association = association_proxy_class.new(self, reflection)
- instance_variable_set(ivar, association)
+ association_instance_set(reflection.name, association)
end
association.reload if force_reload
@@ -1336,86 +1310,15 @@ module ActiveRecord
end
end
- def add_single_associated_validation_callbacks(association_name)
- method_name = "validate_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- association = instance_variable_get("@#{association_name}")
- if !association.nil?
- errors.add association_name unless association.target.nil? || association.valid?
- end
- end
-
- validate method_name
- end
-
- def add_multiple_associated_validation_callbacks(association_name)
- method_name = "validate_associated_records_for_#{association_name}".to_sym
- ivar = "@#{association_name}"
-
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- if association.respond_to?(:loaded?)
- if new_record?
- association
- elsif association.loaded?
- association.select { |record| record.new_record? }
- else
- association.target.select { |record| record.new_record? }
- end.each do |record|
- errors.add association_name unless record.valid?
- end
- end
- end
-
- validate method_name
- end
-
- def add_multiple_associated_save_callbacks(association_name)
- ivar = "@#{association_name}"
-
- method_name = "before_save_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- @new_record_before_save = new_record?
- true
- end
- before_save method_name
-
- method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym
- define_method(method_name) do
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
-
- records_to_save = if @new_record_before_save
- association
- elsif association.respond_to?(:loaded?) && association.loaded?
- association.select { |record| record.new_record? }
- elsif association.respond_to?(:loaded?) && !association.loaded?
- association.target.select { |record| record.new_record? }
- else
- []
- end
- records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank?
-
- # reconstruct the SQL queries now that we know the owner's id
- association.send(:construct_sql) if association.respond_to?(:construct_sql)
- end
-
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create method_name
- after_update method_name
- end
-
def association_constructor_method(constructor, reflection, association_proxy_class)
define_method("#{constructor}_#{reflection.name}") do |*params|
- ivar = "@#{reflection.name}"
-
attributees = params.first unless params.empty?
replace_existing = params[1].nil? ? true : params[1]
- association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
+ association = association_instance_get(reflection.name)
- if association.nil?
+ unless association
association = association_proxy_class.new(self, reflection)
- instance_variable_set(ivar, association)
+ association_instance_set(reflection.name, association)
end
if association_proxy_class == HasOneAssociation
@@ -1627,6 +1530,10 @@ module ActiveRecord
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
+
+ if reflection.association_foreign_key == reflection.primary_key_name
+ raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection)
+ end
reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
@@ -1949,9 +1856,10 @@ module ActiveRecord
def construct(parent, associations, joins, row)
case associations
when Symbol, String
- while (join = joins.shift).reflection.name.to_s != associations.to_s
- raise ConfigurationError, "Not Enough Associations" if joins.empty?
- end
+ join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name }
+ raise(ConfigurationError, "No such association") if join.nil?
+
+ joins.delete(join)
construct_association(parent, join, row)
when Array
associations.each do |association|
@@ -1959,7 +1867,11 @@ module ActiveRecord
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
- association = construct_association(parent, joins.shift, row)
+ join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name }
+ raise(ConfigurationError, "No such association") if join.nil?
+
+ association = construct_association(parent, join, row)
+ joins.delete(join)
construct(association, associations[name], joins, row) if association
end
else
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index 0fefec1216..3aef1b21e9 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -60,7 +60,7 @@ module ActiveRecord
@reflection.klass.find(*args)
end
end
-
+
# Fetches the first one using SQL if possible.
def first(*args)
if fetch_first_or_last_using_find?(args)
@@ -143,6 +143,8 @@ module ActiveRecord
end
# Remove all records from this association
+ #
+ # See delete for more info.
def delete_all
load_target
delete(@target)
@@ -186,7 +188,6 @@ module ActiveRecord
end
end
-
# Removes +records+ from this association calling +before_remove+ and
# +after_remove+ callbacks.
#
@@ -195,22 +196,25 @@ 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)
- records = flatten_deeper(records)
- records.each { |record| raise_on_type_mismatch(record) }
-
- transaction do
- records.each { |record| callback(:before_remove, record) }
-
- old_records = records.reject {|r| r.new_record? }
+ remove_records(records) do |records, old_records|
delete_records(old_records) if old_records.any?
-
- records.each do |record|
- @target.delete(record)
- callback(:after_remove, record)
- end
+ records.each { |record| @target.delete(record) }
end
end
+ # Destroy +records+ and remove them from this association calling
+ # +before_remove+ and +after_remove+ callbacks.
+ #
+ # Note that this method will _always_ remove records from the database
+ # ignoring the +:dependent+ option.
+ def destroy(*records)
+ remove_records(records) do |records, old_records|
+ old_records.each { |record| record.destroy }
+ end
+
+ load_target
+ end
+
# Removes all records from this association. Returns +self+ so method calls may be chained.
def clear
return self if length.zero? # forces load_target if it hasn't happened already
@@ -223,15 +227,16 @@ module ActiveRecord
self
end
-
- def destroy_all
- transaction do
- each { |record| record.destroy }
- end
+ # Destory all the records from this association.
+ #
+ # See destroy for more info.
+ def destroy_all
+ load_target
+ destroy(@target)
reset_target!
end
-
+
def create(attrs = {})
if attrs.is_a?(Array)
attrs.collect { |attr| create(attr) }
@@ -431,6 +436,18 @@ module ActiveRecord
record
end
+ def remove_records(*records)
+ records = flatten_deeper(records)
+ records.each { |record| raise_on_type_mismatch(record) }
+
+ transaction do
+ records.each { |record| callback(:before_remove, record) }
+ old_records = records.reject { |r| r.new_record? }
+ yield(records, old_records)
+ records.each { |record| callback(:after_remove, record) }
+ end
+ end
+
def callback(method, record)
callbacks_for(method).each do |callback|
ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
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 a5cc3bf091..af9ce3dfb2 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
@@ -28,12 +28,12 @@ module ActiveRecord
load_target.size
end
- def insert_record(record, force=true)
+ def insert_record(record, force = true, validate = true)
if record.new_record?
if force
record.save!
else
- return false unless record.save
+ return false unless record.save(validate)
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 3348079e9d..a2cbabfe0c 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -56,9 +56,9 @@ module ActiveRecord
"#{@reflection.name}_count"
end
- def insert_record(record)
+ def insert_record(record, force = false, validate = true)
set_belongs_to_association_for(record)
- record.save
+ force ? record.save! : record.save(validate)
end
# Deletes the records according to the <tt>:dependent</tt> 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 2eeeb28d1f..1c091e7d5a 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -23,8 +23,8 @@ module ActiveRecord
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 less SELECT query if you use length.
+ # 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 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?
@@ -47,12 +47,12 @@ module ActiveRecord
options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil?
end
- def insert_record(record, force=true)
+ def insert_record(record, force = true, validate = true)
if record.new_record?
if force
record.save!
else
- return false unless record.save
+ return false unless record.save(validate)
end
end
through_reflection = @reflection.through_reflection
@@ -150,7 +150,7 @@ module ActiveRecord
end
else
reflection_primary_key = @reflection.source_reflection.primary_key_name
- source_primary_key = @reflection.klass.primary_key
+ source_primary_key = @reflection.through_reflection.klass.primary_key
if @reflection.source_reflection.options[:as]
polymorphic_join = "AND %s.%s = %s" % [
@reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 960323004d..b92cbbdeab 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -29,8 +29,17 @@ module ActiveRecord
unless @target.nil? || @target == obj
if dependent? && !dont_save
- @target.destroy unless @target.new_record?
- @owner.clear_association_cache
+ case @reflection.options[:dependent]
+ when :delete
+ @target.delete unless @target.new_record?
+ @owner.clear_association_cache
+ when :destroy
+ @target.destroy unless @target.new_record?
+ @owner.clear_association_cache
+ when :nullify
+ @target[@reflection.primary_key_name] = nil
+ @target.save unless @owner.new_record? || @target.new_record?
+ end
else
@target[@reflection.primary_key_name] = nil
@target.save unless @owner.new_record? || @target.new_record?
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 177d156834..3ffc48941c 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -324,6 +324,7 @@ module ActiveRecord
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
+ return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
!value.blank?
end
elsif column.number?
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
new file mode 100644
index 0000000000..741aa2acbe
--- /dev/null
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -0,0 +1,349 @@
+module ActiveRecord
+ # AutosaveAssociation is a module that takes care of automatically saving
+ # your associations when the parent is saved. In addition to saving, it
+ # also destroys any associations that were marked for destruction.
+ # (See mark_for_destruction and marked_for_destruction?)
+ #
+ # Saving of the parent, its associations, and the destruction of marked
+ # associations, all happen inside 1 transaction. This should never leave the
+ # database in an inconsistent state after, for instance, mass assigning
+ # attributes and saving them.
+ #
+ # If validations for any of the associations fail, their error messages will
+ # be applied to the parent.
+ #
+ # Note that it also means that associations marked for destruction won't
+ # be destroyed directly. They will however still be marked for destruction.
+ #
+ # === One-to-one Example
+ #
+ # Consider a Post model with one Author:
+ #
+ # class Post
+ # has_one :author, :autosave => true
+ # end
+ #
+ # Saving changes to the parent and its associated model can now be performed
+ # automatically _and_ atomically:
+ #
+ # post = Post.find(1)
+ # post.title # => "The current global position of migrating ducks"
+ # post.author.name # => "alloy"
+ #
+ # post.title = "On the migration of ducks"
+ # post.author.name = "Eloy Duran"
+ #
+ # post.save
+ # post.reload
+ # post.title # => "On the migration of ducks"
+ # post.author.name # => "Eloy Duran"
+ #
+ # Destroying an associated model, as part of the parent's save action, is as
+ # simple as marking it for destruction:
+ #
+ # post.author.mark_for_destruction
+ # post.author.marked_for_destruction? # => true
+ #
+ # Note that the model is _not_ yet removed from the database:
+ # id = post.author.id
+ # Author.find_by_id(id).nil? # => false
+ #
+ # post.save
+ # post.reload.author # => nil
+ #
+ # Now it _is_ removed from the database:
+ # Author.find_by_id(id).nil? # => true
+ #
+ # === One-to-many Example
+ #
+ # Consider a Post model with many Comments:
+ #
+ # class Post
+ # has_many :comments, :autosave => true
+ # end
+ #
+ # Saving changes to the parent and its associated model can now be performed
+ # automatically _and_ atomically:
+ #
+ # post = Post.find(1)
+ # post.title # => "The current global position of migrating ducks"
+ # post.comments.first.body # => "Wow, awesome info thanks!"
+ # post.comments.last.body # => "Actually, your article should be named differently."
+ #
+ # post.title = "On the migration of ducks"
+ # post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
+ #
+ # post.save
+ # post.reload
+ # post.title # => "On the migration of ducks"
+ # post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
+ #
+ # Destroying one of the associated models members, as part of the parent's
+ # save action, is as simple as marking it for destruction:
+ #
+ # post.comments.last.mark_for_destruction
+ # post.comments.last.marked_for_destruction? # => true
+ # post.comments.length # => 2
+ #
+ # Note that the model is _not_ yet removed from the database:
+ # id = post.comments.last.id
+ # Comment.find_by_id(id).nil? # => false
+ #
+ # post.save
+ # post.reload.comments.length # => 1
+ #
+ # Now it _is_ removed from the database:
+ # Comment.find_by_id(id).nil? # => true
+ #
+ # === Validation
+ #
+ # Validation is performed on the parent as usual, but also on all autosave
+ # enabled associations. If any of the associations fail validation, its
+ # error messages will be applied on the parents errors object and validation
+ # of the parent will fail.
+ #
+ # Consider a Post model with Author which validates the presence of its name
+ # attribute:
+ #
+ # class Post
+ # has_one :author, :autosave => true
+ # end
+ #
+ # class Author
+ # validates_presence_of :name
+ # end
+ #
+ # post = Post.find(1)
+ # post.author.name = ''
+ # post.save # => false
+ # post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
+ #
+ # No validations will be performed on the associated models when validations
+ # are skipped for the parent:
+ #
+ # post = Post.find(1)
+ # post.author.name = ''
+ # post.save(false) # => true
+ module AutosaveAssociation
+ ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
+
+ def self.included(base)
+ base.class_eval do
+ base.extend(ClassMethods)
+ alias_method_chain :reload, :autosave_associations
+
+ ASSOCIATION_TYPES.each do |type|
+ base.send("valid_keys_for_#{type}_association") << :autosave
+ end
+ end
+ end
+
+ module ClassMethods
+ private
+
+ # def belongs_to(name, options = {})
+ # super
+ # add_autosave_association_callbacks(reflect_on_association(name))
+ # end
+ ASSOCIATION_TYPES.each do |type|
+ module_eval %{
+ def #{type}(name, options = {})
+ super
+ add_autosave_association_callbacks(reflect_on_association(name))
+ end
+ }
+ end
+
+ # Adds a validate and save callback for the association as specified by
+ # the +reflection+.
+ def add_autosave_association_callbacks(reflection)
+ save_method = "autosave_associated_records_for_#{reflection.name}"
+ validation_method = "validate_associated_records_for_#{reflection.name}"
+ validate validation_method
+
+ case reflection.macro
+ when :has_many, :has_and_belongs_to_many
+ before_save :before_save_collection_association
+
+ define_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+
+ define_method(validation_method) { validate_collection_association(reflection) }
+ else
+ case reflection.macro
+ when :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ after_save save_method
+ when :belongs_to
+ define_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
+ define_method(validation_method) { validate_single_association(reflection) }
+ end
+ end
+ end
+
+ # Reloads the attributes of the object as usual and removes a mark for destruction.
+ def reload_with_autosave_associations(options = nil)
+ @marked_for_destruction = false
+ reload_without_autosave_associations(options)
+ end
+
+ # Marks this record to be destroyed as part of the parents save transaction.
+ # This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
+ #
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
+ def mark_for_destruction
+ @marked_for_destruction = true
+ end
+
+ # Returns whether or not this record will be destroyed as part of the parents save transaction.
+ #
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
+ def marked_for_destruction?
+ @marked_for_destruction
+ end
+
+ private
+
+ # Returns the record for an association collection that should be validated
+ # or saved. If +autosave+ is +false+ only new records will be returned,
+ # unless the parent is/was a new record itself.
+ def associated_records_to_validate_or_save(association, new_record, autosave)
+ if new_record
+ association
+ elsif association.loaded?
+ autosave ? association : association.select { |record| record.new_record? }
+ else
+ autosave ? association.target : association.target.select { |record| record.new_record? }
+ end
+ end
+
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+ # turned on for the association specified by +reflection+.
+ def validate_single_association(reflection)
+ if reflection.options[:validate] == true || reflection.options[:autosave] == true
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
+ association_valid?(reflection, association)
+ end
+ end
+ end
+
+ # Validate the associated records if <tt>:validate</tt> or
+ # <tt>:autosave</tt> is turned on for the association specified by
+ # +reflection+.
+ def validate_collection_association(reflection)
+ if reflection.options[:validate] != false && association = association_instance_get(reflection.name)
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
+ records.each { |record| association_valid?(reflection, record) }
+ end
+ end
+ end
+
+ # Returns whether or not the association is valid and applies any errors to
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
+ # enabled records if they're marked_for_destruction?.
+ def association_valid?(reflection, association)
+ unless valid = association.valid?
+ if reflection.options[:autosave]
+ unless association.marked_for_destruction?
+ association.errors.each do |attribute, message|
+ attribute = "#{reflection.name}_#{attribute}"
+ errors.add(attribute, message) unless errors.on(attribute)
+ end
+ end
+ else
+ errors.add(reflection.name)
+ end
+ end
+ valid
+ end
+
+ # Is used as a before_save callback to check while saving a collection
+ # association whether or not the parent was a new record before saving.
+ def before_save_collection_association
+ @new_record_before_save = new_record?
+ true
+ end
+
+ # Saves any new associated records, or all loaded autosave associations if
+ # <tt>:autosave</tt> is enabled on the association.
+ #
+ # In addition, it destroys all children that were marked for destruction
+ # with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ autosave = reflection.options[:autosave]
+
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+ records.each do |record|
+ if autosave && record.marked_for_destruction?
+ association.destroy(record)
+ elsif @new_record_before_save || record.new_record?
+ if autosave
+ association.send(:insert_record, record, false, false)
+ else
+ association.send(:insert_record, record)
+ end
+ elsif autosave
+ record.save(false)
+ end
+ end
+ end
+
+ # reconstruct the SQL queries now that we know the owner's id
+ association.send(:construct_sql) if association.respond_to?(:construct_sql)
+ end
+ end
+
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_has_one_association(reflection)
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
+ if reflection.options[:autosave] && association.marked_for_destruction?
+ association.destroy
+ elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || reflection.options[:autosave]
+ association[reflection.primary_key_name] = id
+ association.save(false)
+ end
+ end
+ end
+
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_belongs_to_association(reflection)
+ if association = association_instance_get(reflection.name)
+ if reflection.options[:autosave] && association.marked_for_destruction?
+ association.destroy
+ else
+ association.save(false) if association.new_record? || reflection.options[:autosave]
+
+ if association.updated?
+ self[reflection.primary_key_name] = association.id
+ # TODO: Removing this code doesn't seem to matter…
+ if reflection.options[:polymorphic]
+ self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
+ end
+ end
+ end
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 0efccb66ee..9943a7014a 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -416,7 +416,7 @@ module ActiveRecord #:nodoc:
end
@@subclasses = {}
-
+
##
# :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
@@ -661,9 +661,8 @@ module ActiveRecord #:nodoc:
connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
-
# Returns true if a record exists in the table that matches the +id+ or
- # conditions given, or false otherwise. The argument can take four forms:
+ # conditions given, or false otherwise. The argument can take five forms:
#
# * Integer - Finds the record with this primary key.
# * String - Finds the record with a primary key corresponding to this
@@ -672,6 +671,7 @@ module ActiveRecord #:nodoc:
# (such as <tt>['color = ?', 'red']</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
# (such as <tt>{:color => 'red'}</tt>).
+ # * No args - Returns false if the table is empty, true otherwise.
#
# For more information about specifying conditions as a Hash or Array,
# see the Conditions section in the introduction to ActiveRecord::Base.
@@ -685,7 +685,8 @@ module ActiveRecord #:nodoc:
# Person.exists?('5')
# Person.exists?(:name => "David")
# Person.exists?(['name LIKE ?', "%#{query}%"])
- def exists?(id_or_conditions)
+ # Person.exists?
+ def exists?(id_or_conditions = {})
connection.select_all(
construct_finder_sql(
:select => "#{quoted_table_name}.#{primary_key}",
@@ -735,12 +736,12 @@ module ActiveRecord #:nodoc:
# ==== Parameters
#
# * +id+ - This should be the id or an array of ids to be updated.
- # * +attributes+ - This should be a Hash of attributes to be set on the object, or an array of Hashes.
+ # * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes.
#
# ==== Examples
#
# # Updating one record:
- # Person.update(15, { :user_name => 'Samuel', :group => 'expert' })
+ # Person.update(15, :user_name => 'Samuel', :group => 'expert')
#
# # Updating multiple records:
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
@@ -809,25 +810,28 @@ module ActiveRecord #:nodoc:
# Updates all records with details given if they match a set of conditions supplied, limits and order can
# also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
- # database. It does not instantiate the involved models and it does not trigger Active Record callbacks.
+ # database. It does not instantiate the involved models and it does not trigger Active Record callbacks
+ # or validations.
#
# ==== Parameters
#
- # * +updates+ - A string of column and value pairs that will be set on any records that match conditions. This creates the SET clause of the generated SQL.
- # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info.
+ # * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
+ # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. See conditions in the intro.
# * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
#
# ==== Examples
#
- # # Update all billing objects with the 3 different attributes given
- # Billing.update_all( "category = 'authorized', approved = 1, author = 'David'" )
+ # # Update all customers with the given attributes
+ # Customer.update_all :wants_email => true
#
- # # Update records that match our conditions
- # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'" )
+ # # Update all books with 'Rails' in their title
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'"
#
- # # Update records that match our conditions but limit it to 5 ordered by date
- # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'",
- # :order => 'created_at', :limit => 5 )
+ # # Update all avatars migrated more than a week ago
+ # Avatar.update_all ['migrated_at = ?, Time.now.utc], ['migrated_at > ?', 1.week.ago]
+ #
+ # # Update all books that match our conditions, but limit it to 5 ordered by date
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
def update_all(updates, conditions = nil, options = {})
sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} "
@@ -884,7 +888,8 @@ module ActiveRecord #:nodoc:
# Deletes the records matching +conditions+ without instantiating the records first, and hence not
# calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that
# goes straight to the database, much more efficient than +destroy_all+. Be careful with relations
- # though, in particular <tt>:dependent</tt> rules defined on associations are not honored.
+ # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns
+ # the number of rows affected.
#
# ==== Parameters
#
@@ -1000,7 +1005,6 @@ module ActiveRecord #:nodoc:
update_counters(id, counter_name => -1)
end
-
# Attributes named in this macro are protected from mass-assignment,
# such as <tt>new(attributes)</tt>,
# <tt>update_attributes(attributes)</tt>, or
@@ -1101,7 +1105,6 @@ module ActiveRecord #:nodoc:
read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {})
end
-
# Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending
# directly from ActiveRecord::Base. So if the hierarchy looks like: Reply < Message < ActiveRecord::Base, then Message is used
# to guess the table name even when called on Reply. The rules used to do the guess are handled by the Inflector class
@@ -1344,7 +1347,7 @@ module ActiveRecord #:nodoc:
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
end
- def self_and_descendents_from_active_record#nodoc:
+ def self_and_descendants_from_active_record#nodoc:
klass = self
classes = [klass]
while klass != klass.base_class
@@ -1364,7 +1367,7 @@ module ActiveRecord #:nodoc:
# module now.
# Specify +options+ with additional translating options.
def human_attribute_name(attribute_key_name, options = {})
- defaults = self_and_descendents_from_active_record.map do |klass|
+ defaults = self_and_descendants_from_active_record.map do |klass|
:"#{klass.name.underscore}.#{attribute_key_name}"
end
defaults << options[:default] if options[:default]
@@ -1379,7 +1382,7 @@ module ActiveRecord #:nodoc:
# Default scope of the translation is activerecord.models
# Specify +options+ with additional translating options.
def human_name(options = {})
- defaults = self_and_descendents_from_active_record.map do |klass|
+ defaults = self_and_descendants_from_active_record.map do |klass|
:"#{klass.name.underscore}"
end
defaults << self.name.humanize
@@ -1414,7 +1417,6 @@ module ActiveRecord #:nodoc:
end
end
-
def quote_value(value, column = nil) #:nodoc:
connection.quote(value,column)
end
@@ -1483,7 +1485,7 @@ module ActiveRecord #:nodoc:
elsif match = DynamicScopeMatch.match(method_id)
return true if all_attributes_exists?(match.attribute_names)
end
-
+
super
end
@@ -1534,7 +1536,7 @@ module ActiveRecord #:nodoc:
end
def reverse_sql_order(order_query)
- reversed_query = order_query.split(/,/).each { |s|
+ reversed_query = order_query.to_s.split(/,/).each { |s|
if s.match(/\s(asc|ASC)$/)
s.gsub!(/\s(asc|ASC)$/, ' DESC')
elsif s.match(/\s(desc|DESC)$/)
@@ -1687,7 +1689,7 @@ module ActiveRecord #:nodoc:
def construct_finder_sql(options)
scope = scope(:find)
sql = "SELECT #{options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))} "
- sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
+ sql << "FROM #{options[:from] || (scope && scope[:from]) || quoted_table_name} "
add_joins!(sql, options[:joins], scope)
add_conditions!(sql, options[:conditions], scope)
@@ -1742,7 +1744,9 @@ module ActiveRecord #:nodoc:
scoped_order = scope[:order] if scope
if order
sql << " ORDER BY #{order}"
- sql << ", #{scoped_order}" if scoped_order
+ if scoped_order && scoped_order != order
+ sql << ", #{scoped_order}"
+ end
else
sql << " ORDER BY #{scoped_order}" if scoped_order
end
@@ -1751,12 +1755,12 @@ module ActiveRecord #:nodoc:
def add_group!(sql, group, having, scope = :auto)
if group
sql << " GROUP BY #{group}"
- sql << " HAVING #{having}" if having
+ sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having
else
scope = scope(:find) if :auto == scope
if scope && (scoped_group = scope[:group])
sql << " GROUP BY #{scoped_group}"
- sql << " HAVING #{scoped_having}" if (scoped_having = scope[:having])
+ sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having]
end
end
end
@@ -1990,12 +1994,16 @@ module ActiveRecord #:nodoc:
attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) }
end
- def attribute_condition(argument)
+ def attribute_condition(quoted_column_name, argument)
case argument
- when nil then "IS ?"
- when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)"
- when Range then "BETWEEN ? AND ?"
- else "= ?"
+ when nil then "#{quoted_column_name} IS ?"
+ when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "#{quoted_column_name} IN (?)"
+ when Range then if argument.exclude_end?
+ "#{quoted_column_name} >= ? AND #{quoted_column_name} < ?"
+ else
+ "#{quoted_column_name} BETWEEN ? AND ?"
+ end
+ else "#{quoted_column_name} = ?"
end
end
@@ -2007,7 +2015,6 @@ module ActiveRecord #:nodoc:
end
end
-
# Defines an "attribute" method (like +inheritance_column+ or
# +table_name+). A new (class) method will be created with the
# given name. If a value is specified, the new method will
@@ -2104,7 +2111,7 @@ module ActiveRecord #:nodoc:
end
# Merge scopings
- if action == :merge && current_scoped_methods
+ if [:merge, :reverse_merge].include?(action) && current_scoped_methods
method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)|
case hash[method]
when Hash
@@ -2126,7 +2133,11 @@ module ActiveRecord #:nodoc:
end
end
else
- hash[method] = hash[method].merge(params)
+ if action == :reverse_merge
+ hash[method] = hash[method].merge(params)
+ else
+ hash[method] = params.merge(hash[method])
+ end
end
else
hash[method] = params
@@ -2136,7 +2147,6 @@ module ActiveRecord #:nodoc:
end
self.scoped_methods << method_scoping
-
begin
yield
ensure
@@ -2167,7 +2177,7 @@ module ActiveRecord #:nodoc:
# Test whether the given method and optional key are scoped.
def scoped?(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
- !key || scope.has_key?(key)
+ !key || !scope[key].nil?
end
end
@@ -2305,7 +2315,7 @@ module ActiveRecord #:nodoc:
table_name = connection.quote_table_name(table_name)
end
- "#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}"
+ attribute_condition("#{table_name}.#{connection.quote_column_name(attr)}", value)
else
sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s))
end
@@ -2742,7 +2752,6 @@ module ActiveRecord #:nodoc:
assign_multiparameter_attributes(multi_parameter_attributes)
end
-
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
def attributes
self.attribute_names.inject({}) do |attrs, name|
@@ -3136,7 +3145,12 @@ module ActiveRecord #:nodoc:
include Dirty
include Callbacks, Observing, Timestamp
include Associations, AssociationPreload, NamedScope
- include Aggregations, Transactions, Reflection, Calculations, Serialization
+
+ # AutosaveAssociation needs to be included before Transactions, because we want
+ # #save_with_autosave_associations to be wrapped inside a transaction.
+ include AutosaveAssociation, NestedAttributes
+
+ include Aggregations, Transactions, Reflection, Batches, Calculations, Serialization
end
end
diff --git a/activerecord/lib/active_record/batches.rb b/activerecord/lib/active_record/batches.rb
new file mode 100644
index 0000000000..5a6cecd4ad
--- /dev/null
+++ b/activerecord/lib/active_record/batches.rb
@@ -0,0 +1,81 @@
+module ActiveRecord
+ module Batches # :nodoc:
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ # When processing large numbers of records, it's often a good idea to do
+ # so in batches to prevent memory ballooning.
+ module ClassMethods
+ # Yields each record that was found by the find +options+. The find is
+ # performed by find_in_batches with a batch size of 1000 (or as
+ # specified by the <tt>:batch_size</tt> option).
+ #
+ # Example:
+ #
+ # Person.find_each(:conditions => "age > 21") do |person|
+ # person.party_all_night!
+ # end
+ #
+ # Note: This method is only intended to use for batch processing of
+ # large amounts of records that wouldn't fit in memory all at once. If
+ # you just need to loop over less than 1000 records, it's probably
+ # better just to use the regular find methods.
+ def find_each(options = {})
+ find_in_batches(options) do |records|
+ records.each { |record| yield record }
+ end
+
+ self
+ end
+
+ # Yields each batch of records that was found by the find +options+ as
+ # an array. The size of each batch is set by the <tt>:batch_size</tt>
+ # option; the default is 1000.
+ #
+ # You can control the starting point for the batch processing by
+ # supplying the <tt>:start</tt> option. This is especially useful if you
+ # want multiple workers dealing with the same processing queue. You can
+ # make worker 1 handle all the records between id 0 and 10,000 and
+ # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
+ # option on that worker).
+ #
+ # It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # work. This also mean that this method only works with integer-based
+ # primary keys. You can't set the limit either, that's used to control
+ # the the batch sizes.
+ #
+ # Example:
+ #
+ # Person.find_in_batches(:conditions => "age > 21") do |group|
+ # sleep(50) # Make sure it doesn't get too crowded in there!
+ # group.each { |person| person.party_all_night! }
+ # end
+ def find_in_batches(options = {})
+ raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order]
+ raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit]
+
+ start = options.delete(:start).to_i
+ batch_size = options.delete(:batch_size) || 1000
+
+ with_scope(:find => options.merge(:order => batch_order, :limit => batch_size)) do
+ records = find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ])
+
+ while records.any?
+ yield records
+
+ break if records.size < batch_size
+ records = find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", records.last.id ])
+ end
+ end
+ end
+
+
+ private
+ def batch_order
+ "#{table_name}.#{primary_key} ASC"
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index b239c03284..f077818d3b 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -141,22 +141,30 @@ module ActiveRecord
def construct_count_options_from_args(*args)
options = {}
column_name = :all
-
+
# We need to handle
# count()
# count(:column_name=:all)
# count(options={})
# count(column_name=:all, options={})
+ # selects specified by scopes
case args.size
+ when 0
+ column_name = scope(:find)[:select] if scope(:find)
when 1
- args[0].is_a?(Hash) ? options = args[0] : column_name = args[0]
+ if args[0].is_a?(Hash)
+ column_name = scope(:find)[:select] if scope(:find)
+ options = args[0]
+ else
+ column_name = args[0]
+ end
when 2
column_name, options = args
else
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
- end if args.size > 0
-
- [column_name, options]
+ end
+
+ [column_name || :all, options]
end
def construct_calculation_sql(operation, column_name, options) #:nodoc:
@@ -214,13 +222,15 @@ module ActiveRecord
end
if options[:group] && options[:having]
+ having = sanitize_sql_for_conditions(options[:having])
+
# FrontBase requires identifiers in the HAVING clause and chokes on function calls
if connection.adapter_name == 'FrontBase'
- options[:having].downcase!
- options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
+ having.downcase!
+ having.gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias)
end
- sql << " HAVING #{options[:having]} "
+ sql << " HAVING #{having} "
end
sql << " ORDER BY #{options[:order]} " if options[:order]
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 88958f4583..e375037b5b 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -104,6 +104,37 @@ module ActiveRecord
# The callback objects have methods named after the callback called with the record as the only parameter, such as:
#
# class BankAccount < ActiveRecord::Base
+ # before_save EncryptionWrapper.new
+ # after_save EncryptionWrapper.new
+ # after_initialize EncryptionWrapper.new
+ # end
+ #
+ # class EncryptionWrapper
+ # def before_save(record)
+ # record.credit_card_number = encrypt(record.credit_card_number)
+ # end
+ #
+ # def after_save(record)
+ # record.credit_card_number = decrypt(record.credit_card_number)
+ # end
+ #
+ # alias_method :after_find, :after_save
+ #
+ # private
+ # def encrypt(value)
+ # # Secrecy is committed
+ # end
+ #
+ # def decrypt(value)
+ # # Secrecy is unveiled
+ # end
+ # end
+ #
+ # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
+ # a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other
+ # initialization data such as the name of the attribute to work with:
+ #
+ # class BankAccount < ActiveRecord::Base
# before_save EncryptionWrapper.new("credit_card_number")
# after_save EncryptionWrapper.new("credit_card_number")
# after_initialize EncryptionWrapper.new("credit_card_number")
@@ -115,11 +146,11 @@ module ActiveRecord
# end
#
# def before_save(record)
- # record.credit_card_number = encrypt(record.credit_card_number)
+ # record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
# end
#
# def after_save(record)
- # record.credit_card_number = decrypt(record.credit_card_number)
+ # record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
# end
#
# alias_method :after_find, :after_save
@@ -134,9 +165,6 @@ module ActiveRecord
# end
# end
#
- # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has
- # a method by the name of the callback messaged.
- #
# The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string",
# which will then be evaluated within the binding of the callback. Example:
#
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 901b17124c..aac84cc5f4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -351,5 +351,21 @@ module ActiveRecord
retrieve_connection_pool klass.superclass
end
end
+
+ class ConnectionManagement
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ @app.call(env)
+ ensure
+ # Don't return connection (and peform implicit rollback) if
+ # this request is a part of integration test
+ unless env.key?("rack.test")
+ ActiveRecord::Base.clear_active_connections!
+ end
+ end
+ end
end
end
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 273f823e7f..24c734cddb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -8,6 +8,7 @@ module ActiveRecord
# 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/
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 913bb521ca..ec204d0f03 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -392,9 +392,28 @@ module ActiveRecord
quote_string(s)
end
+ # Checks the following cases:
+ #
+ # - table_name
+ # - "table.name"
+ # - schema_name.table_name
+ # - schema_name."table.name"
+ # - "schema.name".table_name
+ # - "schema.name"."table.name"
+ def quote_table_name(name)
+ schema, name_part = extract_pg_identifier_from_name(name.to_s)
+
+ unless name_part
+ quote_column_name(schema)
+ else
+ table_name, name_part = extract_pg_identifier_from_name(name_part)
+ "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
+ end
+ end
+
# Quotes column names for use in SQL queries.
def quote_column_name(name) #:nodoc:
- %("#{name}")
+ PGconn.quote_ident(name.to_s)
end
# Quote date/time values for use in SQL input. Includes microseconds
@@ -1045,6 +1064,16 @@ module ActiveRecord
ORDER BY a.attnum
end_sql
end
+
+ def extract_pg_identifier_from_name(name)
+ match_data = name[0,1] == '"' ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
+
+ if match_data
+ rest = name[match_data[0].length..-1]
+ rest = rest[1..-1] if rest[0,1] == "."
+ [match_data[1], (rest.length > 0 ? rest : nil)]
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index cc9c46505f..75420f69aa 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -18,7 +18,7 @@ module ActiveRecord
db.busy_timeout(config[:timeout]) unless config[:timeout].nil?
- ConnectionAdapters::SQLite3Adapter.new(db, logger)
+ ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index 5390f49f04..afd6472db8 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -17,9 +17,9 @@ module ActiveRecord
# "Downgrade" deprecated sqlite API
if SQLite.const_defined?(:Version)
- ConnectionAdapters::SQLite2Adapter.new(db, logger)
+ ConnectionAdapters::SQLite2Adapter.new(db, logger, config)
else
- ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger)
+ ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config)
end
end
end
@@ -72,10 +72,31 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLiteAdapter < AbstractAdapter
+ class Version
+ include Comparable
+
+ def initialize(version_string)
+ @version = version_string.split('.').map(&:to_i)
+ end
+
+ def <=>(version_string)
+ @version <=> version_string.split('.').map(&:to_i)
+ end
+ end
+
+ def initialize(connection, logger, config)
+ super(connection, logger)
+ @config = config
+ end
+
def adapter_name #:nodoc:
'SQLite'
end
+ def supports_ddl_transactions?
+ sqlite_version >= '2.0.0'
+ end
+
def supports_migrations? #:nodoc:
true
end
@@ -83,6 +104,10 @@ module ActiveRecord
def requires_reloading?
true
end
+
+ def supports_add_column?
+ sqlite_version >= '3.1.6'
+ end
def disconnect!
super
@@ -164,7 +189,6 @@ module ActiveRecord
catch_schema_changes { @connection.rollback }
end
-
# SELECT ... FOR UPDATE is redundant since the table is locked.
def add_lock!(sql, options) #:nodoc:
sql
@@ -213,14 +237,20 @@ module ActiveRecord
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
end
+ # See: http://www.sqlite.org/lang_altertable.html
+ # SQLite has an additional restriction on the ALTER TABLE statement
+ def valid_alter_table_options( type, options)
+ type.to_sym != :primary_key
+ end
+
def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
- raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
+ if supports_add_column? && valid_alter_table_options( type, options )
+ super(table_name, column_name, type, options)
+ else
+ alter_table(table_name) do |definition|
+ definition.column(column_name, type, options)
+ end
end
-
- super(table_name, column_name, type, options)
- # See last paragraph on http://www.sqlite.org/lang_altertable.html
- execute "VACUUM"
end
def remove_column(table_name, *column_names) #:nodoc:
@@ -380,7 +410,7 @@ module ActiveRecord
end
def sqlite_version
- @sqlite_version ||= select_value('select sqlite_version(*)')
+ @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)'))
end
def default_primary_key_type
@@ -393,23 +423,9 @@ module ActiveRecord
end
class SQLite2Adapter < SQLiteAdapter # :nodoc:
- def supports_count_distinct? #:nodoc:
- false
- end
-
def rename_table(name, new_name)
move_table(name, new_name)
end
-
- def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if @connection.respond_to?(:transaction_active?) && @connection.transaction_active?
- raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction'
- end
-
- alter_table(table_name) do |definition|
- definition.column(column_name, type, options)
- end
- end
end
class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc:
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 0131d9fac5..c6501113bf 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -21,13 +21,17 @@ else
end
end
-# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors:
+# Fixtures are a way of organizing data that you want to test against; in short, sample data.
+#
+# = Fixture formats
+#
+# Fixtures come in 3 flavors:
#
# 1. YAML fixtures
# 2. CSV fixtures
# 3. Single-file fixtures
#
-# = YAML fixtures
+# == YAML fixtures
#
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
@@ -65,9 +69,9 @@ end
# parent_id: 1
# title: Child
#
-# = CSV fixtures
+# == CSV fixtures
#
-# Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
+# Fixtures can also be kept in the Comma Separated Value (CSV) format. Akin to YAML fixtures, CSV fixtures are stored
# in a single file, but instead end with the <tt>.csv</tt> file extension
# (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>).
#
@@ -90,7 +94,7 @@ end
# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
# have existing data somewhere already.
#
-# = Single-file fixtures
+# == Single-file fixtures
#
# This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
@@ -113,65 +117,53 @@ end
# name => Ruby on Rails
# url => http://www.rubyonrails.org
#
-# = Using Fixtures
+# = Using fixtures in testcases
#
# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the
# fixtures, but first let's take a look at a sample unit test:
#
-# require 'web_site'
+# require 'test_helper'
#
# class WebSiteTest < ActiveSupport::TestCase
-# def test_web_site_count
+# test "web_site_count" do
# assert_equal 2, WebSite.count
# end
# end
#
-# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail. Here's the
-# easiest way to add fixtures to the database:
-#
-# ...
-# class WebSiteTest < ActiveSupport::TestCase
-# fixtures :web_sites # add more by separating the symbols with commas
-# ...
-#
-# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger
-# the testing environment to automatically load the appropriate fixtures into the database before each test.
+# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, so this test will succeed.
+# The testing environment will automatically load the all fixtures into the database before each test.
# To ensure consistent data, the environment deletes the fixtures before running the load.
#
-# In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
-# of the test case. It is named after the symbol... so, in our example, there would be a hash available called
-# <tt>@web_sites</tt>. This is where the "fixture name" comes into play.
-#
-# On top of that, each record is automatically "found" (using <tt>Model.find(id)</tt>) and placed in the instance variable of its name.
-# So for the YAML fixtures, we'd get <tt>@rubyonrails</tt> and <tt>@google</tt>, which could be interrogated using regular Active Record semantics:
+# In addition to being available in the database, the fixture's data may also be accessed by
+# using a special dynamic method, which has the same name as the model, and accepts the
+# name of the fixture to instantiate:
#
-# # test if the object created from the fixture data has the same attributes as the data itself
-# def test_find
-# assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
+# test "find" do
+# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
# end
#
-# As seen above, the data hash created from the YAML fixtures would have <tt>@web_sites["rubyonrails"]["url"]</tt> return
-# "http://www.rubyonrails.org" and <tt>@web_sites["google"]["name"]</tt> would return "Google". The same fixtures, but loaded
-# from a CSV fixture file, would be accessible via <tt>@web_sites["web_site_1"]["name"] == "Ruby on Rails"</tt> and have the individual
-# fixtures available as instance variables <tt>@web_site_1</tt> and <tt>@web_site_2</tt>.
+# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the following tests:
#
-# If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options.
+# test "find_alt_method_1" do
+# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
+# end
#
-# - to completely disable instantiated fixtures:
-# self.use_instantiated_fixtures = false
+# test "find_alt_method_2" do
+# assert_equal "Ruby on Rails", @rubyonrails.news
+# end
#
-# - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
-# self.use_instantiated_fixtures = :no_instances
+# In order to use these methods to access fixtured data within your testcases, you must specify one of the
+# following in your <tt>ActiveSupport::TestCase</tt>-derived class:
#
-# Even if auto-instantiated fixtures are disabled, you can still access them
-# by name via special dynamic methods. Each method has the same name as the
-# model, and accepts the name of the fixture to instantiate:
+# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
+# self.use_instantiated_fixtures = true
#
-# fixtures :web_sites
+# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
+# self.use_instantiated_fixtures = :no_instances
#
-# def test_find
-# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
-# end
+# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
+# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
+# large sets of fixtured data.
#
# = Dynamic fixtures with ERb
#
@@ -194,21 +186,17 @@ end
# = Transactional fixtures
#
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
-# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
#
# class FooTest < ActiveSupport::TestCase
# self.use_transactional_fixtures = true
-# self.use_instantiated_fixtures = false
-#
-# fixtures :foos
#
-# def test_godzilla
+# test "godzilla" do
# assert !Foo.find(:all).empty?
# Foo.destroy_all
# assert Foo.find(:all).empty?
# end
#
-# def test_godzilla_aftermath
+# test "godzilla aftermath" do
# assert !Foo.find(:all).empty?
# end
# end
@@ -220,24 +208,25 @@ end
# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
#
# When *not* to use transactional fixtures:
-# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
-# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
-# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
-# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
-# Use InnoDB, MaxDB, or NDB instead.
+#
+# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
+# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
+# the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
+# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
+# Use InnoDB, MaxDB, or NDB instead.
#
# = Advanced YAML Fixtures
#
# YAML fixtures that don't specify an ID get some extra features:
#
-# * Stable, autogenerated ID's
+# * Stable, autogenerated IDs
# * Label references for associations (belongs_to, has_one, has_many)
# * HABTM associations as inline lists
# * Autofilled timestamp columns
# * Fixture label interpolation
# * Support for YAML defaults
#
-# == Stable, autogenerated ID's
+# == Stable, autogenerated IDs
#
# Here, have a monkey fixture:
#
@@ -292,7 +281,7 @@ end
#
# Add a few more monkeys and pirates and break this into multiple files,
# and it gets pretty hard to keep track of what's going on. Let's
-# use labels instead of ID's:
+# use labels instead of IDs:
#
# ### in pirates.yml
#
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index ff9899d032..7fa7e267d8 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -23,6 +23,16 @@ module ActiveRecord
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
+ # Optimistic locking will also check for stale data when objects are destroyed. Example:
+ #
+ # p1 = Person.find(1)
+ # p2 = Person.find(1)
+ #
+ # p1.first_name = "Michael"
+ # p1.save
+ #
+ # p2.destroy # Raises a ActiveRecord::StaleObjectError
+ #
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
@@ -39,6 +49,7 @@ module ActiveRecord
base.lock_optimistically = true
base.alias_method_chain :update, :lock
+ base.alias_method_chain :destroy, :lock
base.alias_method_chain :attributes_from_column_definition, :lock
class << base
@@ -98,6 +109,28 @@ module ActiveRecord
end
end
+ def destroy_with_lock #:nodoc:
+ return destroy_without_lock unless locking_enabled?
+
+ unless new_record?
+ lock_col = self.class.locking_column
+ previous_value = send(lock_col).to_i
+
+ affected_rows = connection.delete(
+ "DELETE FROM #{self.class.quoted_table_name} " +
+ "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
+ "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
+ "#{self.class.name} Destroy"
+ )
+
+ unless affected_rows == 1
+ raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object"
+ end
+ end
+
+ freeze
+ end
+
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 15350cf1e1..657acd6dc0 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -338,6 +338,10 @@ module ActiveRecord
self.verbose = save
end
+ def connection
+ ActiveRecord::Base.connection
+ end
+
def method_missing(method, *arguments, &block)
arg_list = arguments.map(&:inspect) * ', '
@@ -345,7 +349,7 @@ module ActiveRecord
unless arguments.empty? || method == :execute
arguments[0] = Migrator.proper_table_name(arguments.first)
end
- ActiveRecord::Base.connection.send(method, *arguments, &block)
+ connection.send(method, *arguments, &block)
end
end
end
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index 989b2a1ec5..1f3ef300f2 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -1,11 +1,12 @@
module ActiveRecord
module NamedScope
- # All subclasses of ActiveRecord::Base have two named \scopes:
- # * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and
+ # All subclasses of ActiveRecord::Base have one named scope:
# * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
#
# These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
# intermediate values (scopes) around as first-class objects is convenient.
+ #
+ # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
def self.included(base)
base.class_eval do
extend ClassMethods
@@ -88,7 +89,12 @@ module ActiveRecord
when Hash
options
when Proc
- options.call(*args)
+ case parent_scope
+ when Scope
+ with_scope(:find => parent_scope.proxy_options) { options.call(*args) }
+ else
+ options.call(*args)
+ end
end, &block)
end
(class << self; self end).instance_eval do
@@ -98,9 +104,9 @@ module ActiveRecord
end
end
end
-
+
class Scope
- attr_reader :proxy_scope, :proxy_options
+ attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
[].methods.each do |m|
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
@@ -111,8 +117,12 @@ module ActiveRecord
delegate :scopes, :with_scope, :to => :proxy_scope
def initialize(proxy_scope, options, &block)
+ options ||= {}
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
extend Module.new(&block) if block_given?
+ unless Scope === proxy_scope
+ @current_scoped_methods_when_defined = proxy_scope.send(:current_scoped_methods)
+ end
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
end
@@ -166,9 +176,15 @@ module ActiveRecord
if scopes.include?(method)
scopes[method].call(self, *args)
else
- with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do
+ with_scope({:find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do
method = :new if method == :build
- proxy_scope.send(method, *args, &block)
+ if current_scoped_methods_when_defined
+ with_scope current_scoped_methods_when_defined do
+ proxy_scope.send(method, *args, &block)
+ end
+ else
+ proxy_scope.send(method, *args, &block)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
new file mode 100644
index 0000000000..e3122d195a
--- /dev/null
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -0,0 +1,329 @@
+module ActiveRecord
+ module NestedAttributes #:nodoc:
+ def self.included(base)
+ base.extend(ClassMethods)
+ base.class_inheritable_accessor :reject_new_nested_attributes_procs, :instance_writer => false
+ base.reject_new_nested_attributes_procs = {}
+ end
+
+ # == Nested Attributes
+ #
+ # Nested attributes allow you to save attributes on associated records
+ # through the parent. By default nested attribute updating is turned off,
+ # you can enable it using the accepts_nested_attributes_for class method.
+ # When you enable nested attributes an attribute writer is defined on
+ # the model.
+ #
+ # The attribute writer is named after the association, which means that
+ # in the following example, two new methods are added to your model:
+ # <tt>author_attributes=(attributes)</tt> and
+ # <tt>pages_attributes=(attributes)</tt>.
+ #
+ # class Book < ActiveRecord::Base
+ # has_one :author
+ # has_many :pages
+ #
+ # accepts_nested_attributes_for :author, :pages
+ # end
+ #
+ # Note that the <tt>:autosave</tt> option is automatically enabled on every
+ # association that accepts_nested_attributes_for is used for.
+ #
+ # === One-to-one
+ #
+ # Consider a Member model that has one Avatar:
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar
+ # end
+ #
+ # Enabling nested attributes on a one-to-one association allows you to
+ # create the member and avatar in one go:
+ #
+ # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
+ # member = Member.create(params)
+ # member.avatar.id # => 2
+ # member.avatar.icon # => 'smiling'
+ #
+ # It also allows you to update the avatar through the member:
+ #
+ # params = { :member' => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
+ # member.update_attributes params['member']
+ # member.avatar.icon # => 'sad'
+ #
+ # By default you will only be able to set and update attributes on the
+ # associated model. If you want to destroy the associated model through the
+ # attributes hash, you have to enable it first using the
+ # <tt>:allow_destroy</tt> option.
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar, :allow_destroy => true
+ # end
+ #
+ # Now, when you add the <tt>_delete</tt> key to the attributes hash, with a
+ # value that evaluates to +true+, you will destroy the associated model:
+ #
+ # member.avatar_attributes = { :id => '2', :_delete => '1' }
+ # member.avatar.marked_for_destruction? # => true
+ # member.save
+ # member.avatar #=> nil
+ #
+ # Note that the model will _not_ be destroyed until the parent is saved.
+ #
+ # === One-to-many
+ #
+ # Consider a member that has a number of posts:
+ #
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts
+ # end
+ #
+ # You can now set or update attributes on an associated post model through
+ # the attribute hash.
+ #
+ # For each hash that does _not_ have an <tt>id</tt> key a new record will
+ # be instantiated, unless the hash also contains a <tt>_delete</tt> key
+ # that evaluates to +true+.
+ #
+ # params = { :member => {
+ # :name => 'joe', :posts_attributes => [
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
+ # { :title => 'The egalitarian assumption of the modern citizen' },
+ # { :title => '', :_delete => '1' } # this will be ignored
+ # ]
+ # }}
+ #
+ # member = Member.create(params['member'])
+ # member.posts.length # => 2
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
+ #
+ # You may also set a :reject_if proc to silently ignore any new record
+ # hashes if they fail to pass your criteria. For example, the previous
+ # example could be rewritten as:
+ #
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
+ # end
+ #
+ # params = { :member => {
+ # :name => 'joe', :posts_attributes => [
+ # { :title => 'Kari, the awesome Ruby documentation browser!' },
+ # { :title => 'The egalitarian assumption of the modern citizen' },
+ # { :title => '' } # this will be ignored because of the :reject_if proc
+ # ]
+ # }}
+ #
+ # member = Member.create(params['member'])
+ # member.posts.length # => 2
+ # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
+ # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
+ #
+ # If the hash contains an <tt>id</tt> key that matches an already
+ # associated record, the matching record will be modified:
+ #
+ # member.attributes = {
+ # :name => 'Joe',
+ # :posts_attributes => [
+ # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
+ # { :id => 2, :title => '[UPDATED] other post' }
+ # ]
+ # }
+ #
+ # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!'
+ # member.posts.second.title # => '[UPDATED] other post'
+ #
+ # By default the associated records are protected from being destroyed. If
+ # you want to destroy any of the associated records through the attributes
+ # hash, you have to enable it first using the <tt>:allow_destroy</tt>
+ # option. This will allow you to also use the <tt>_delete</tt> key to
+ # destroy existing records:
+ #
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, :allow_destroy => true
+ # end
+ #
+ # params = { :member => {
+ # :posts_attributes => [{ :id => '2', :_delete => '1' }]
+ # }}
+ #
+ # member.attributes = params['member']
+ # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
+ # member.posts.length #=> 2
+ # member.save
+ # member.posts.length # => 1
+ #
+ # === Saving
+ #
+ # All changes to models, including the destruction of those marked for
+ # destruction, are saved and destroyed automatically and atomically when
+ # the parent model is saved. This happens inside the transaction initiated
+ # by the parents save method. See ActiveRecord::AutosaveAssociation.
+ module ClassMethods
+ # Defines an attributes writer for the specified association(s). If you
+ # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
+ # will need to add the attribute writer to the allowed list.
+ #
+ # Supported options:
+ # [:allow_destroy]
+ # If true, destroys any members from the attributes hash with a
+ # <tt>_delete</tt> key and a value that evaluates to +true+
+ # (eg. 1, '1', true, or 'true'). This option is off by default.
+ # [:reject_if]
+ # Allows you to specify a Proc that checks whether a record should be
+ # built for a certain attribute hash. The hash is passed to the Proc
+ # and the Proc should return either +true+ or +false+. When no Proc
+ # is specified a record will be built for all attribute hashes that
+ # do not have a <tt>_delete</tt> that evaluates to true.
+ #
+ # Examples:
+ # # creates avatar_attributes=
+ # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
+ # # creates avatar_attributes= and posts_attributes=
+ # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
+ def accepts_nested_attributes_for(*attr_names)
+ options = { :allow_destroy => false }
+ options.update(attr_names.extract_options!)
+ options.assert_valid_keys(:allow_destroy, :reject_if)
+
+ attr_names.each do |association_name|
+ if reflection = reflect_on_association(association_name)
+ type = case reflection.macro
+ when :has_one, :belongs_to
+ :one_to_one
+ when :has_many, :has_and_belongs_to_many
+ :collection
+ end
+
+ reflection.options[:autosave] = true
+ self.reject_new_nested_attributes_procs[association_name.to_sym] = options[:reject_if]
+
+ # def pirate_attributes=(attributes)
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
+ # end
+ class_eval %{
+ def #{association_name}_attributes=(attributes)
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
+ end
+ }, __FILE__, __LINE__
+ else
+ raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
+ end
+ end
+ end
+ end
+
+ # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
+ # used in conjunction with fields_for to build a form element for the
+ # destruction of this association.
+ #
+ # See ActionView::Helpers::FormHelper::fields_for for more info.
+ def _delete
+ marked_for_destruction?
+ end
+
+ private
+
+ # Attribute hash keys that should not be assigned as normal attributes.
+ # These hash keys are nested attributes implementation details.
+ UNASSIGNABLE_KEYS = %w{ id _delete }
+
+ # Assigns the given attributes to the association.
+ #
+ # If the given attributes include an <tt>:id</tt> that matches the existing
+ # record’s id, then the existing record will be modified. Otherwise a new
+ # record will be built.
+ #
+ # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
+ # <tt>:_delete</tt> key set to a truthy value, then the existing record
+ # will be marked for destruction.
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
+ attributes = attributes.stringify_keys
+
+ if attributes['id'].blank?
+ unless reject_new_record?(association_name, attributes)
+ send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS))
+ end
+ elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
+ assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
+ end
+ end
+
+ # Assigns the given attributes to the collection association.
+ #
+ # Hashes with an <tt>:id</tt> value matching an existing associated record
+ # will update that record. Hashes without an <tt>:id</tt> value will build
+ # a new record for the association. Hashes with a matching <tt>:id</tt>
+ # value and a <tt>:_delete</tt> key set to a truthy value will mark the
+ # matched record for destruction.
+ #
+ # For example:
+ #
+ # assign_nested_attributes_for_collection_association(:people, {
+ # '1' => { :id => '1', :name => 'Peter' },
+ # '2' => { :name => 'John' },
+ # '3' => { :id => '2', :_delete => true }
+ # })
+ #
+ # Will update the name of the Person with ID 1, build a new associated
+ # person with the name `John', and mark the associatied Person with ID 2
+ # for destruction.
+ #
+ # Also accepts an Array of attribute hashes:
+ #
+ # assign_nested_attributes_for_collection_association(:people, [
+ # { :id => '1', :name => 'Peter' },
+ # { :name => 'John' },
+ # { :id => '2', :_delete => true }
+ # ])
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
+ end
+
+ if attributes_collection.is_a? Hash
+ attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
+ end
+
+ attributes_collection.each do |attributes|
+ attributes = attributes.stringify_keys
+
+ if attributes['id'].blank?
+ unless reject_new_record?(association_name, attributes)
+ send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
+ end
+ elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
+ assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
+ end
+ end
+ end
+
+ # Updates a record with the +attributes+ or marks it for destruction if
+ # +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
+ def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
+ if has_delete_flag?(attributes) && allow_destroy
+ record.mark_for_destruction
+ else
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
+ end
+ end
+
+ # Determines if a hash contains a truthy _delete key.
+ def has_delete_flag?(hash)
+ ConnectionAdapters::Column.value_to_boolean hash['_delete']
+ end
+
+ # Determines if a new record should be build by checking for
+ # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
+ # association and evaluates to +true+.
+ def reject_new_record?(association_name, attributes)
+ has_delete_flag?(attributes) ||
+ self.class.reject_new_nested_attributes_procs[association_name].try(:call, attributes)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 1937abdc83..2d4c1d5507 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -65,6 +65,11 @@ module ActiveRecord
def reflect_on_association(association)
reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
end
+
+ # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
+ def reflect_on_all_autosave_associations
+ reflections.values.select { |reflection| reflection.options[:autosave] }
+ end
end
@@ -192,7 +197,7 @@ module ActiveRecord
def counter_cache_column
if options[:counter_cache] == true
- "#{active_record.name.underscore.pluralize}_count"
+ "#{active_record.name.demodulize.underscore.pluralize}_count"
elsif options[:counter_cache]
options[:counter_cache]
end
diff --git a/activerecord/lib/active_record/serializers/json_serializer.rb b/activerecord/lib/active_record/serializers/json_serializer.rb
index 419b45d475..1fd65ed006 100644
--- a/activerecord/lib/active_record/serializers/json_serializer.rb
+++ b/activerecord/lib/active_record/serializers/json_serializer.rb
@@ -8,6 +8,25 @@ module ActiveRecord #:nodoc:
# Returns a JSON string representing the model. Some configuration is
# available through +options+.
#
+ # The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the
+ # top-level behavior of to_json. In a new Rails application, it is set to
+ # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>,
+ # to_json will emit a single root node named after the object's type. For example:
+ #
+ # konata = User.find(1)
+ # ActiveRecord::Base.include_root_in_json = true
+ # konata.to_json
+ # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
+ # "created_at": "2006/08/01", "awesome": true} }
+ #
+ # ActiveRecord::Base.include_root_in_json = false
+ # konata.to_json
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
+ # "created_at": "2006/08/01", "awesome": true}
+ #
+ # The remainder of the examples in this section assume include_root_in_json is set to
+ # <tt>false</tt>.
+ #
# Without any +options+, the returned JSON string will include all
# the model's attributes. For example:
#
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 4749823b94..fa75874603 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -231,16 +231,22 @@ module ActiveRecord #:nodoc:
def add_associations(association, records, opts)
if records.is_a?(Enumerable)
tag = reformat_name(association.to_s)
+ type = options[:skip_types] ? {} : {:type => "array"}
+
if records.empty?
- builder.tag!(tag, :type => :array)
+ builder.tag!(tag, type)
else
- builder.tag!(tag, :type => :array) do
+ builder.tag!(tag, type) do
association_name = association.to_s.singularize
records.each do |record|
- record.to_xml opts.merge(
- :root => association_name,
- :type => (record.class.to_s.underscore == association_name ? nil : record.class.name)
- )
+ if options[:skip_types]
+ record_type = {}
+ else
+ record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
+ record_type = {:type => record_class}
+ end
+
+ record.to_xml opts.merge(:root => association_name).merge(record_type)
end
end
end
diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb
index 74d91f129e..21471da419 100644
--- a/activerecord/lib/active_record/session_store.rb
+++ b/activerecord/lib/active_record/session_store.rb
@@ -99,7 +99,7 @@ module ActiveRecord
define_method(:session_id=) { |session_id| self.sessid = session_id }
else
def self.find_by_session_id(session_id)
- find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
+ find :first, :conditions => {:session_id=>session_id}
end
end
end
@@ -184,7 +184,7 @@ module ActiveRecord
# Look up a session by id and unmarshal its data if found.
def find_by_session_id(session_id)
- if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
+ if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}")
new(:session_id => session_id, :marshaled_data => record['data'])
end
end
@@ -287,8 +287,7 @@ module ActiveRecord
def get_session(env, sid)
Base.silence do
sid ||= generate_sid
- session = @@session_class.find_by_session_id(sid)
- session ||= @@session_class.new(:session_id => sid, :data => {})
+ session = find_session(sid)
env[SESSION_RECORD_KEY] = session
[sid, session.data]
end
@@ -296,7 +295,7 @@ module ActiveRecord
def set_session(env, sid, session_data)
Base.silence do
- record = env[SESSION_RECORD_KEY]
+ record = env[SESSION_RECORD_KEY] ||= find_session(sid)
record.data = session_data
return false unless record.save
@@ -310,5 +309,10 @@ module ActiveRecord
return true
end
+
+ def find_session(id)
+ @@session_class.find_by_session_id(id) ||
+ @@session_class.new(:session_id => id, :data => {})
+ end
end
end
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index 149b93203e..8c6abaaccb 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -27,6 +27,7 @@ module ActiveRecord
$queries_executed = []
yield
ensure
+ %w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) }
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
end
@@ -48,5 +49,18 @@ module ActiveRecord
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.establish_connection(@connection)
end
+
+ def with_kcode(kcode)
+ if RUBY_VERSION < '1.9'
+ orig_kcode, $KCODE = $KCODE, kcode
+ begin
+ yield
+ ensure
+ $KCODE = orig_kcode
+ end
+ else
+ yield
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 0b6e52c79b..b059eb7f6f 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -175,6 +175,8 @@ module ActiveRecord
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
# end
+ #
+ # Note that "TRUNCATE" is also a MySQL DDL statement!
module ClassMethods
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(options = {}, &block)
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 6d750accb0..d2d12b80c9 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -89,7 +89,7 @@ module ActiveRecord
message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
- defaults = @base.class.self_and_descendents_from_active_record.map do |klass|
+ defaults = @base.class.self_and_descendants_from_active_record.map do |klass|
[ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
:"models.#{klass.name.underscore}.#{message}" ]
end
@@ -575,6 +575,8 @@ module ActiveRecord
# Get range option and value.
option = range_options.first
option_value = options[range_options.first]
+ key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
+ custom_message = options[:message] || options[key]
case option
when :within, :in
@@ -583,9 +585,9 @@ module ActiveRecord
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin
- record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
+ record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => option_value.begin)
elsif value.size > option_value.end
- record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end)
+ record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => option_value.end)
end
end
when :is, :minimum, :maximum
@@ -593,13 +595,10 @@ module ActiveRecord
# Declare different validations per option.
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
- message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
unless !value.nil? and value.size.method(validity_checks[option])[option_value]
- key = message_options[option]
- custom_message = options[:message] || options[key]
record.errors.add(attr, key, :default => custom_message, :count => option_value)
end
end
@@ -721,20 +720,20 @@ module ActiveRecord
# class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
- is_text_column = finder_class.columns_hash[attr_name.to_s].text?
+ column = finder_class.columns_hash[attr_name.to_s]
if value.nil?
comparison_operator = "IS ?"
- elsif is_text_column
+ elsif column.text?
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
- value = value.to_s
+ value = column.limit ? value.to_s[0, column.limit] : value.to_s
else
comparison_operator = "= ?"
end
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
- if value.nil? || (configuration[:case_sensitive] || !is_text_column)
+ if value.nil? || (configuration[:case_sensitive] || !column.text?)
condition_sql = "#{sql_attribute} #{comparison_operator}"
condition_params = [value]
else
@@ -745,7 +744,7 @@ module ActiveRecord
if scope = configuration[:scope]
Array(scope).map do |scope_item|
scope_value = record.send(scope_item)
- condition_sql << " AND #{record.class.quoted_table_name}.#{scope_item} #{attribute_condition(scope_value)}"
+ condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
condition_params << scope_value
end
end
@@ -803,7 +802,7 @@ module ActiveRecord
# Validates whether the value of the specified attribute is available in a particular enumerable object.
#
# class Person < ActiveRecord::Base
- # validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!"
+ # validates_inclusion_of :gender, :in => %w( m f )
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list"
# end
@@ -1041,6 +1040,11 @@ module ActiveRecord
errors.empty?
end
+ # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise.
+ def invalid?
+ !valid?
+ end
+
# Returns the Errors object that holds all information about attribute error messages.
def errors
@errors ||= Errors.new(self)
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 6ac4bdc905..852807b4c5 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -2,7 +2,7 @@ module ActiveRecord
module VERSION #:nodoc:
MAJOR = 2
MINOR = 3
- TINY = 0
+ TINY = 2
STRING = [MAJOR, MINOR, TINY].join('.')
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 40a8503980..13a78a1890 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -154,6 +154,23 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 0, Topic.find(t2.id).replies.size
end
+ def test_belongs_to_reassign_with_namespaced_models_and_counters
+ t1 = Web::Topic.create("title" => "t1")
+ t2 = Web::Topic.create("title" => "t2")
+ r1 = Web::Reply.new("title" => "r1", "content" => "r1")
+ r1.topic = t1
+
+ assert r1.save
+ assert_equal 1, Web::Topic.find(t1.id).replies.size
+ assert_equal 0, Web::Topic.find(t2.id).replies.size
+
+ r1.topic = Web::Topic.find(t2.id)
+
+ assert r1.save
+ assert_equal 0, Web::Topic.find(t1.id).replies.size
+ assert_equal 1, Web::Topic.find(t2.id).replies.size
+ end
+
def test_belongs_to_counter_after_save
topic = Topic.create!(:title => "monday night")
topic.replies.create!(:title => "re: monday night", :content => "football")
@@ -190,19 +207,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count")
end
- def test_assignment_before_parent_saved
- client = Client.find(:first)
- apple = Firm.new("name" => "Apple")
- client.firm = apple
- assert_equal apple, client.firm
- assert apple.new_record?
- assert client.save
- assert apple.save
- assert !apple.new_record?
- assert_equal apple, client.firm
- assert_equal apple, client.firm(true)
- end
-
def test_assignment_before_child_saved
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
@@ -215,19 +219,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal firm, final_cut.firm(true)
end
- def test_assignment_before_either_saved
- final_cut = Client.new("name" => "Final Cut")
- apple = Firm.new("name" => "Apple")
- final_cut.firm = apple
- assert final_cut.new_record?
- assert apple.new_record?
- assert final_cut.save
- assert !final_cut.new_record?
- assert !apple.new_record?
- assert_equal apple, final_cut.firm
- assert_equal apple, final_cut.firm(true)
- end
-
def test_new_record_with_foreign_key_but_no_object
c = Client.new("firm_id" => 1)
assert_equal Firm.find(:first), c.firm_with_basic_id
@@ -274,90 +265,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 17, reply.replies.size
end
- def test_store_two_association_with_one_save
- num_orders = Order.count
- num_customers = Customer.count
- order = Order.new
-
- customer1 = order.billing = Customer.new
- customer2 = order.shipping = Customer.new
- assert order.save
- assert_equal customer1, order.billing
- assert_equal customer2, order.shipping
-
- order.reload
-
- assert_equal customer1, order.billing
- assert_equal customer2, order.shipping
-
- assert_equal num_orders +1, Order.count
- assert_equal num_customers +2, Customer.count
- end
-
-
- def test_store_association_in_two_relations_with_one_save
- num_orders = Order.count
- num_customers = Customer.count
- order = Order.new
-
- customer = order.billing = order.shipping = Customer.new
- assert order.save
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- order.reload
-
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- assert_equal num_orders +1, Order.count
- assert_equal num_customers +1, Customer.count
- end
-
- def test_store_association_in_two_relations_with_one_save_in_existing_object
- num_orders = Order.count
- num_customers = Customer.count
- order = Order.create
-
- customer = order.billing = order.shipping = Customer.new
- assert order.save
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- order.reload
-
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- assert_equal num_orders +1, Order.count
- assert_equal num_customers +1, Customer.count
- end
-
- def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values
- num_orders = Order.count
- num_customers = Customer.count
- order = Order.create
-
- customer = order.billing = order.shipping = Customer.new
- assert order.save
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- order.reload
-
- customer = order.billing = order.shipping = Customer.new
-
- assert order.save
- order.reload
-
- assert_equal customer, order.billing
- assert_equal customer, order.shipping
-
- assert_equal num_orders +1, Order.count
- assert_equal num_customers +2, Customer.count
- end
-
-
def test_association_assignment_sticks
post = Post.find(:first)
@@ -410,32 +317,29 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal nil, sponsor.sponsorable_id
end
- def test_save_fails_for_invalid_belongs_to
- assert log = AuditLog.create(:developer_id=>0,:message=>"")
-
- log.developer = Developer.new
- assert !log.developer.valid?
- assert !log.valid?
- assert !log.save
- assert_equal "is invalid", log.errors.on("developer")
- end
-
- def test_save_succeeds_for_invalid_belongs_to_with_validate_false
- assert log = AuditLog.create(:developer_id=>0,:message=>"")
-
- log.unvalidated_developer = Developer.new
- assert !log.unvalidated_developer.valid?
- assert log.valid?
- assert log.save
- end
-
def test_belongs_to_proxy_should_not_respond_to_private_methods
- assert_raises(NoMethodError) { companies(:first_firm).private_method }
- assert_raises(NoMethodError) { companies(:second_client).firm.private_method }
+ assert_raise(NoMethodError) { companies(:first_firm).private_method }
+ assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
end
def test_belongs_to_proxy_should_respond_to_private_methods_via_send
companies(:first_firm).send(:private_method)
companies(:second_client).firm.send(:private_method)
end
+
+ def test_save_of_record_with_loaded_belongs_to
+ @account = companies(:first_firm).account
+
+ assert_nothing_raised do
+ Account.find(@account.id).save!
+ Account.find(@account.id, :include => :firm).save!
+ end
+
+ @account.firm.delete
+
+ assert_nothing_raised do
+ Account.find(@account.id).save!
+ Account.find(@account.id, :include => :firm).save!
+ end
+ end
end
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 12dec5ccd1..1b2e0fc11e 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -1,4 +1,9 @@
require 'cases/helper'
+require 'models/author'
+require 'models/post'
+require 'models/comment'
+require 'models/category'
+require 'models/categorization'
module Remembered
def self.included(base)
@@ -99,3 +104,27 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
end
end
end
+
+class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
+ def setup
+ @davey_mcdave = Author.create(:name => 'Davey McDave')
+ @first_post = @davey_mcdave.posts.create(:title => 'Davey Speaks', :body => 'Expressive wordage')
+ @first_comment = @first_post.comments.create(:body => 'Inflamatory doublespeak')
+ @first_categorization = @davey_mcdave.categorizations.create(:category => Category.first, :post => @first_post)
+ end
+
+ def teardown
+ @davey_mcdave.destroy
+ @first_post.destroy
+ @first_comment.destroy
+ @first_categorization.destroy
+ end
+
+ def test_missing_data_in_a_nested_include_should_not_cause_errors_when_constructing_objects
+ assert_nothing_raised do
+ # @davey_mcdave doesn't have any author_favorites
+ includes = {:posts => :comments, :categorizations => :category, :author_favorites => :favorite_author }
+ Author.all :include => includes, :conditions => {:authors => {:name => @davey_mcdave.name}}, :order => 'categories.name'
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 14099d4176..40723814c5 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -549,16 +549,16 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_invalid_association_reference
- assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=> :monkeys )
}
- assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ :monkeys ])
}
- assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
post = Post.find(6, :include=>[ 'monkeys' ])
}
- assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
+ assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
post = Post.find(6, :include=>[ :monkeys, :elephants ])
}
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 1e3b423471..5e8b2cadfc 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
@@ -381,6 +381,33 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
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)
+ david.projects.reload
+ assert_equal 2, david.projects.size
+ assert_equal 3, active_record.developers.size
+
+ assert_difference "Project.count", -1 do
+ david.projects.destroy(active_record)
+ end
+
+ assert_equal 1, david.reload.projects.size
+ assert_equal 1, david.projects(true).size
+ end
+
+ def test_destroying_array
+ david = Developer.find(1)
+ david.projects.reload
+
+ assert_difference "Project.count", -Project.count do
+ david.projects.destroy(Project.find(:all))
+ end
+
+ assert_equal 0, david.reload.projects.size
+ assert_equal 0, david.projects(true).size
+ end
+
def test_destroy_all
david = Developer.find(1)
david.projects.reload
@@ -616,7 +643,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_updating_attributes_on_rich_associations
david = projects(:action_controller).developers.first
david.name = "DHH"
- assert_raises(ActiveRecord::ReadOnlyRecord) { david.save! }
+ assert_raise(ActiveRecord::ReadOnlyRecord) { david.save! }
end
def test_updating_attributes_on_rich_associations_with_limited_find_from_reflection
@@ -740,6 +767,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developer, project.developers.find(:first)
assert_equal project, developer.projects.find(:first)
end
+
+ def test_self_referential_habtm_without_foreign_key_set_should_raise_exception
+ assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) {
+ Member.class_eval do
+ has_and_belongs_to_many :friends, :class_name => "Member", :join_table => "member_friends"
+ end
+ }
+ end
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
@@ -767,12 +802,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, developer.projects.count
end
- uses_mocha 'mocking Post.transaction' do
- def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Post.expects(:transaction)
- Category.find(:first).posts.transaction do
- # nothing
- end
+ def test_association_proxy_transaction_method_starts_transaction_in_association_class
+ Post.expects(:transaction)
+ Category.find(:first).posts.transaction do
+ # nothing
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 428fb50013..30edf79a26 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -70,6 +70,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, companies(:first_firm).limited_clients.find(:all, :limit => nil).size
end
+ def test_dynamic_find_last_without_specified_order
+ assert_equal companies(:second_client), companies(:first_firm).unsorted_clients.find_last_by_type('Client')
+ end
+
def test_dynamic_find_should_respect_association_order
assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find(:first, :conditions => "type = 'Client'")
assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
@@ -176,7 +180,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_ids
firm = Firm.find(:first)
- assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find }
+ assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find }
client = firm.clients.find(2)
assert_kind_of Client, client
@@ -190,7 +194,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, client_ary.size
assert_equal client, client_ary.first
- assert_raises(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
+ assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
def test_find_string_ids_when_using_finder_sql
@@ -215,6 +219,45 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, firm.clients.find(:all, :conditions => "name = 'Summit'").length
end
+ def test_find_each
+ firm = companies(:first_firm)
+
+ assert ! firm.clients.loaded?
+
+ assert_queries(3) do
+ firm.clients.find_each(:batch_size => 1) {|c| assert_equal firm.id, c.firm_id }
+ end
+
+ assert ! firm.clients.loaded?
+ end
+
+ def test_find_each_with_conditions
+ firm = companies(:first_firm)
+
+ assert_queries(2) do
+ firm.clients.find_each(:batch_size => 1, :conditions => {:name => "Microsoft"}) do |c|
+ assert_equal firm.id, c.firm_id
+ assert_equal "Microsoft", c.name
+ end
+ end
+
+ assert ! firm.clients.loaded?
+ end
+
+ def test_find_in_batches
+ firm = companies(:first_firm)
+
+ assert ! firm.clients.loaded?
+
+ assert_queries(2) do
+ firm.clients.find_in_batches(:batch_size => 2) do |clients|
+ clients.each {|c| assert_equal firm.id, c.firm_id }
+ end
+ end
+
+ assert ! firm.clients.loaded?
+ end
+
def test_find_all_sanitized
firm = Firm.find(:first)
summit = firm.clients.find(:all, :conditions => "name = 'Summit'")
@@ -238,7 +281,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_in_collection
assert_equal Client.find(2).name, companies(:first_firm).clients.find(2).name
- assert_raises(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
+ assert_raise(ActiveRecord::RecordNotFound) { companies(:first_firm).clients.find(6) }
end
def test_find_grouped
@@ -278,36 +321,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_create_with_bang_on_has_many_when_parent_is_new_raises
- assert_raises(ActiveRecord::RecordNotSaved) do
+ assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create! :name=>"Whoever"
end
end
def test_regular_create_on_has_many_when_parent_is_new_raises
- assert_raises(ActiveRecord::RecordNotSaved) do
+ assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create :name=>"Whoever"
end
end
def test_create_with_bang_on_has_many_raises_when_record_not_saved
- assert_raises(ActiveRecord::RecordInvalid) do
+ assert_raise(ActiveRecord::RecordInvalid) do
firm = Firm.find(:first)
firm.plain_clients.create!
end
end
def test_create_with_bang_on_habtm_when_parent_is_new_raises
- assert_raises(ActiveRecord::RecordNotSaved) do
+ assert_raise(ActiveRecord::RecordNotSaved) do
Developer.new("name" => "Aredridel").projects.create!
end
end
def test_adding_a_mismatch_class
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << nil }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).clients_of_firm << Topic.find(1) }
end
def test_adding_a_collection
@@ -317,81 +360,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, companies(:first_firm).clients_of_firm(true).size
end
- def test_adding_before_save
- no_of_firms = Firm.count
- no_of_clients = Client.count
-
- new_firm = Firm.new("name" => "A New Firm, Inc")
- c = Client.new("name" => "Apple")
-
- new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
- assert_equal 1, new_firm.clients_of_firm.size
- new_firm.clients_of_firm << c
- assert_equal 2, new_firm.clients_of_firm.size
-
- assert_equal no_of_firms, Firm.count # Firm was not saved to database.
- assert_equal no_of_clients, Client.count # Clients were not saved to database.
- assert new_firm.save
- assert !new_firm.new_record?
- assert !c.new_record?
- assert_equal new_firm, c.firm
- assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
- assert_equal no_of_clients+2, Client.count # Clients were saved to database.
-
- assert_equal 2, new_firm.clients_of_firm.size
- assert_equal 2, new_firm.clients_of_firm(true).size
- end
-
- def test_invalid_adding
- firm = Firm.find(1)
- assert !(firm.clients_of_firm << c = Client.new)
- assert c.new_record?
- assert !firm.valid?
- assert !firm.save
- assert c.new_record?
- end
-
- def test_invalid_adding_before_save
- no_of_firms = Firm.count
- no_of_clients = Client.count
- new_firm = Firm.new("name" => "A New Firm, Inc")
- new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
- assert c.new_record?
- assert !c.valid?
- assert !new_firm.valid?
- assert !new_firm.save
- assert c.new_record?
- assert new_firm.new_record?
- end
-
- def test_invalid_adding_with_validate_false
- firm = Firm.find(:first)
- client = Client.new
- firm.unvalidated_clients_of_firm << client
-
- assert firm.valid?
- assert !client.valid?
- assert firm.save
- assert client.new_record?
- end
-
- def test_valid_adding_with_validate_false
- no_of_clients = Client.count
-
- firm = Firm.find(:first)
- client = Client.new("name" => "Apple")
-
- assert firm.valid?
- assert client.valid?
- assert client.new_record?
-
- firm.unvalidated_clients_of_firm << client
-
- assert firm.save
- assert !client.new_record?
- assert_equal no_of_clients+1, Client.count
- end
-
def test_build
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
@@ -400,10 +368,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "Another Client", new_client.name
assert new_client.new_record?
assert_equal new_client, company.clients_of_firm.last
- company.name += '-changed'
- assert_queries(2) { assert company.save }
- assert !new_client.new_record?
- assert_equal 2, company.clients_of_firm(true).size
end
def test_collection_size_after_building
@@ -428,11 +392,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_many
company = companies(:first_firm)
new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
-
assert_equal 2, new_clients.size
- company.name += '-changed'
- assert_queries(3) { assert company.save }
- assert_equal 3, company.clients_of_firm(true).size
end
def test_build_followed_by_save_does_not_load_target
@@ -463,10 +423,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "Another Client", new_client.name
assert new_client.new_record?
assert_equal new_client, company.clients_of_firm.last
- company.name += '-changed'
- assert_queries(2) { assert company.save }
- assert !new_client.new_record?
- assert_equal 2, company.clients_of_firm(true).size
end
def test_build_many_via_block
@@ -480,10 +436,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, new_clients.size
assert_equal "changed", new_clients.first.name
assert_equal "changed", new_clients.last.name
-
- company.name += '-changed'
- assert_queries(3) { assert company.save }
- assert_equal 3, company.clients_of_firm(true).size
end
def test_create_without_loading_association
@@ -501,16 +453,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, first_firm.clients_of_firm.size
end
- def test_invalid_build
- new_client = companies(:first_firm).clients_of_firm.build
- assert new_client.new_record?
- assert !new_client.valid?
- assert_equal new_client, companies(:first_firm).clients_of_firm.last
- assert !companies(:first_firm).save
- assert new_client.new_record?
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
- end
-
def test_create
force_signal37_to_load_all_clients_of_firm
new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
@@ -703,7 +645,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_invalid_belongs_to_dependent_option_raises_exception
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
Author.belongs_to :special_author_address, :dependent => :nullify
end
end
@@ -729,13 +671,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_type_mismatch
david = Developer.find(1)
david.projects.reload
- assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(1) }
end
def test_deleting_self_type_mismatch
david = Developer.find(1)
david.projects.reload
- assert_raises(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { david.projects.delete(Project.find(1).developers) }
+ end
+
+ def test_destroying
+ force_signal37_to_load_all_clients_of_firm
+
+ assert_difference "Client.count", -1 do
+ companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first)
+ end
+
+ assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_destroying_a_collection
+ 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
+
+ assert_difference "Client.count", -2 do
+ companies(:first_firm).clients_of_firm.destroy([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]])
+ end
+
+ assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_destroy_all
@@ -843,15 +809,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert !firm.clients.include?(:first_client)
end
- def test_replace_on_new_object
- firm = Firm.new("name" => "New Firm")
- firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
- assert firm.save
- firm.reload
- assert_equal 2, firm.clients.length
- assert firm.clients.include?(Client.find_by_name("New Client"))
- end
-
def test_get_ids
assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids
end
@@ -879,15 +836,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert company.clients_using_sql.loaded?
end
- def test_assign_ids
- firm = Firm.new("name" => "Apple")
- firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
- firm.save
- firm.reload
- assert_equal 2, firm.clients.length
- assert firm.clients.include?(companies(:second_client))
- end
-
def test_assign_ids_ignoring_blanks
firm = Firm.create!(:name => 'Apple')
firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, '']
@@ -910,16 +858,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection, &block) }
end
-
- def test_assign_ids_for_through_a_belongs_to
- post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!")
- post.person_ids = [people(:david).id, people(:michael).id]
- post.save
- post.reload
- assert_equal 2, post.people.length
- assert post.people.include?(people(:david))
- end
-
def test_dynamic_find_should_respect_association_order_for_through
assert_equal Comment.find(10), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'")
assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type('SpecialComment')
@@ -1097,12 +1035,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = old
end
- uses_mocha 'mocking Comment.transaction' do
- def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Comment.expects(:transaction)
- Post.find(:first).comments.transaction do
- # nothing
- end
+ def test_association_proxy_transaction_method_starts_transaction_in_association_class
+ Comment.expects(:transaction)
+ Post.find(:first).comments.transaction do
+ # nothing
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 ad6a5d6840..97efca7891 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -1,14 +1,19 @@
require "cases/helper"
require 'models/post'
require 'models/person'
+require 'models/reference'
+require 'models/job'
require 'models/reader'
require 'models/comment'
require 'models/tag'
require 'models/tagging'
require 'models/author'
+require 'models/owner'
+require 'models/pet'
+require 'models/toy'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
- fixtures :posts, :readers, :people, :comments, :authors
+ fixtures :posts, :readers, :people, :comments, :authors, :owners, :pets, :toys
def test_associate_existing
assert_queries(2) { posts(:thinking);people(:david) }
@@ -87,6 +92,24 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert posts(:welcome).reload.people(true).empty?
end
+ def test_destroy_association
+ assert_difference "Person.count", -1 do
+ posts(:welcome).people.destroy(people(:michael))
+ end
+
+ assert posts(:welcome).reload.people.empty?
+ assert posts(:welcome).people(true).empty?
+ end
+
+ def test_destroy_all
+ assert_difference "Person.count", -1 do
+ posts(:welcome).people.destroy_all
+ end
+
+ assert posts(:welcome).reload.people.empty?
+ assert posts(:welcome).people(true).empty?
+ end
+
def test_replace_association
assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
@@ -228,12 +251,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert !person.posts.loaded?
end
- uses_mocha 'mocking Tag.transaction' do
- def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Tag.expects(:transaction)
- Post.find(:first).tags.transaction do
- # nothing
- end
+ def test_association_proxy_transaction_method_starts_transaction_in_association_class
+ Tag.expects(:transaction)
+ Post.find(:first).tags.transaction do
+ # nothing
end
end
@@ -251,4 +272,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
author.author_favorites.create(:favorite_author_id => 3)
assert_equal post.author.author_favorites, post.author_favorites
end
+
+ def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
+ assert_equal 1, owners(:blackbeard).toys.count
+ 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 14032a67c0..1ddb3f49bf 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -59,8 +59,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_type_mismatch
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
- assert_raises(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
+ assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
end
def test_natural_assignment
@@ -76,7 +76,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).save
assert_nil companies(:first_firm).account
# account is dependent, therefore is destroyed when reference to owner is lost
- assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
+ assert_raise(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
+ end
+
+ def test_nullification_on_association_change
+ firm = companies(:rails_core)
+ old_account_id = firm.account.id
+ firm.account = Account.new
+ # 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_changecalls_delete
+ companies(:first_firm).deletable_account = Account.new
+ assert_equal [], Account.destroyed_account_ids[companies(:first_firm).id]
+ end
+
+ def test_association_change_calls_destroy
+ companies(:first_firm).account = Account.new
+ assert_equal [companies(:first_firm).id], Account.destroyed_account_ids[companies(:first_firm).id]
end
def test_natural_assignment_to_already_associated_record
@@ -193,28 +211,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal account, firm.account
end
- def test_build_before_child_saved
- firm = Firm.find(1)
-
- account = firm.account.build("credit_limit" => 1000)
- assert_equal account, firm.account
- assert account.new_record?
- assert firm.save
- assert_equal account, firm.account
- assert !account.new_record?
- end
-
- def test_build_before_either_saved
- firm = Firm.new("name" => "GlobalMegaCorp")
-
- firm.account = account = Account.new("credit_limit" => 1000)
- assert_equal account, firm.account
- assert account.new_record?
- assert firm.save
- assert_equal account, firm.account
- assert !account.new_record?
- end
-
def test_failing_build_association
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
@@ -253,16 +249,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
firm.destroy
end
- def test_assignment_before_parent_saved
- firm = Firm.new("name" => "GlobalMegaCorp")
- firm.account = a = Account.find(1)
- assert firm.new_record?
- assert_equal a, firm.account
- assert firm.save
- assert_equal a, firm.account
- assert_equal a, firm.account(true)
- end
-
def test_finding_with_interpolated_condition
firm = Firm.find(:first)
superior = firm.clients.create(:name => 'SuperiorCo')
@@ -279,61 +265,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal a, firm.account
assert_equal a, firm.account(true)
end
-
- def test_save_fails_for_invalid_has_one
- firm = Firm.find(:first)
- assert firm.valid?
-
- firm.account = Account.new
-
- assert !firm.account.valid?
- assert !firm.valid?
- assert !firm.save
- assert_equal "is invalid", firm.errors.on("account")
- end
-
-
- def test_save_succeeds_for_invalid_has_one_with_validate_false
- firm = Firm.find(:first)
- assert firm.valid?
-
- firm.unvalidated_account = Account.new
-
- assert !firm.unvalidated_account.valid?
- assert firm.valid?
- assert firm.save
- end
-
- def test_assignment_before_either_saved
- firm = Firm.new("name" => "GlobalMegaCorp")
- firm.account = a = Account.new("credit_limit" => 1000)
- assert firm.new_record?
- assert a.new_record?
- assert_equal a, firm.account
- assert firm.save
- assert !firm.new_record?
- assert !a.new_record?
- assert_equal a, firm.account
- assert_equal a, firm.account(true)
- end
-
- def test_not_resaved_when_unchanged
- firm = Firm.find(:first, :include => :account)
- firm.name += '-changed'
- assert_queries(1) { firm.save! }
-
- firm = Firm.find(:first)
- firm.account = Account.find(:first)
- assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
-
- firm = Firm.find(:first).clone
- firm.account = Account.find(:first)
- assert_queries(2) { firm.save! }
-
- firm = Firm.find(:first).clone
- firm.account = Account.find(:first).clone
- assert_queries(2) { firm.save! }
- end
def test_save_still_works_after_accessing_nil_has_one
jp = Company.new :name => 'Jaded Pixel'
@@ -350,8 +281,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_proxy_should_not_respond_to_private_methods
- assert_raises(NoMethodError) { accounts(:signals37).private_method }
- assert_raises(NoMethodError) { companies(:first_firm).account.private_method }
+ assert_raise(NoMethodError) { accounts(:signals37).private_method }
+ assert_raise(NoMethodError) { companies(:first_firm).account.private_method }
end
def test_has_one_proxy_should_respond_to_private_methods_via_send
@@ -359,4 +290,20 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).account.send(:private_method)
end
+ def test_save_of_record_with_loaded_has_one
+ @firm = companies(:first_firm)
+ assert_not_nil @firm.account
+
+ assert_nothing_raised do
+ Firm.find(@firm.id).save!
+ Firm.find(@firm.id, :include => :account).save!
+ end
+
+ @firm.account.destroy
+
+ assert_nothing_raised do
+ Firm.find(@firm.id).save!
+ Firm.find(@firm.id, :include => :account).save!
+ end
+ 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 f65d76e2ce..12c598751b 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -115,8 +115,8 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_through_proxy_should_not_respond_to_private_methods
- assert_raises(NoMethodError) { clubs(:moustache_club).private_method }
- assert_raises(NoMethodError) { @member.club.private_method }
+ assert_raise(NoMethodError) { clubs(:moustache_club).private_method }
+ assert_raise(NoMethodError) { @member.club.private_method }
end
def test_has_one_through_proxy_should_respond_to_private_methods_via_send
@@ -173,4 +173,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil assert_no_queries { @new_detail.member_type }
end
+ def test_save_of_record_with_loaded_has_one_through
+ @club = @member.club
+ assert_not_nil @club.sponsored_member
+
+ assert_nothing_raised do
+ Club.find(@club.id).save!
+ Club.find(@club.id, :include => :sponsored_member).save!
+ end
+
+ @club.sponsor.destroy
+
+ assert_nothing_raised do
+ Club.find(@club.id).save!
+ Club.find(@club.id, :include => :sponsored_member).save!
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 7a0427aabc..b1060d01af 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -510,13 +510,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert !author.comments.loaded?
end
- uses_mocha('has_many_through_collection_size_uses_counter_cache_if_it_exists') do
- 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)
- assert_equal 100, author.comments.size
- assert !author.comments.loaded?
- end
+ 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)
+ assert_equal 100, author.comments.size
+ assert !author.comments.loaded?
end
def test_adding_junk_to_has_many_through_should_raise_type_mismatch
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 77ee8d8fc4..17ed302465 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -56,6 +56,18 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal myobj, topic.content
end
+ def test_typecast_attribute_from_select_to_false
+ topic = Topic.create(:title => 'Budget')
+ topic = Topic.find(:first, :select => "topics.*, 1=2 as is_test")
+ assert !topic.is_test?
+ end
+
+ def test_typecast_attribute_from_select_to_true
+ topic = Topic.create(:title => 'Budget')
+ topic = Topic.find(:first, :select => "topics.*, 2=2 as is_test")
+ assert topic.is_test?
+ end
+
def test_kernel_methods_not_implemented_in_activerecord
%w(test name display y).each do |method|
assert !ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined"
@@ -88,7 +100,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
%w(save create_or_update).each do |method|
klass = Class.new ActiveRecord::Base
klass.class_eval "def #{method}() 'defined #{method}' end"
- assert_raises ActiveRecord::DangerousAttributeError do
+ assert_raise ActiveRecord::DangerousAttributeError do
klass.instance_method_already_implemented?(method)
end
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
new file mode 100644
index 0000000000..436f50d395
--- /dev/null
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -0,0 +1,901 @@
+require 'cases/helper'
+require 'models/bird'
+require 'models/company'
+require 'models/customer'
+require 'models/developer'
+require 'models/order'
+require 'models/parrot'
+require 'models/person'
+require 'models/pirate'
+require 'models/post'
+require 'models/reader'
+require 'models/ship'
+require 'models/ship_part'
+require 'models/treasure'
+
+class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
+ def test_autosave_should_be_a_valid_option_for_has_one
+ assert base.valid_keys_for_has_one_association.include?(:autosave)
+ end
+
+ def test_autosave_should_be_a_valid_option_for_belongs_to
+ assert base.valid_keys_for_belongs_to_association.include?(:autosave)
+ end
+
+ def test_autosave_should_be_a_valid_option_for_has_many
+ assert base.valid_keys_for_has_many_association.include?(:autosave)
+ end
+
+ def test_autosave_should_be_a_valid_option_for_has_and_belongs_to_many
+ assert base.valid_keys_for_has_and_belongs_to_many_association.include?(:autosave)
+ end
+
+ private
+
+ def base
+ ActiveRecord::Base
+ end
+end
+
+class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
+ def test_save_fails_for_invalid_has_one
+ firm = Firm.find(:first)
+ assert firm.valid?
+
+ firm.account = Account.new
+
+ assert !firm.account.valid?
+ assert !firm.valid?
+ assert !firm.save
+ assert_equal "is invalid", firm.errors.on("account")
+ end
+
+ def test_save_succeeds_for_invalid_has_one_with_validate_false
+ firm = Firm.find(:first)
+ assert firm.valid?
+
+ firm.unvalidated_account = Account.new
+
+ assert !firm.unvalidated_account.valid?
+ assert firm.valid?
+ assert firm.save
+ end
+
+ def test_build_before_child_saved
+ firm = Firm.find(1)
+
+ account = firm.account.build("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.new_record?
+ assert firm.save
+ assert_equal account, firm.account
+ assert !account.new_record?
+ end
+
+ def test_build_before_either_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+
+ firm.account = account = Account.new("credit_limit" => 1000)
+ assert_equal account, firm.account
+ assert account.new_record?
+ assert firm.save
+ assert_equal account, firm.account
+ assert !account.new_record?
+ end
+
+ def test_assignment_before_parent_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = a = Account.find(1)
+ assert firm.new_record?
+ assert_equal a, firm.account
+ assert firm.save
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
+
+ def test_assignment_before_either_saved
+ firm = Firm.new("name" => "GlobalMegaCorp")
+ firm.account = a = Account.new("credit_limit" => 1000)
+ assert firm.new_record?
+ assert a.new_record?
+ assert_equal a, firm.account
+ assert firm.save
+ assert !firm.new_record?
+ assert !a.new_record?
+ assert_equal a, firm.account
+ assert_equal a, firm.account(true)
+ end
+
+ def test_not_resaved_when_unchanged
+ firm = Firm.find(:first, :include => :account)
+ firm.name += '-changed'
+ assert_queries(1) { firm.save! }
+
+ firm = Firm.find(:first)
+ firm.account = Account.find(:first)
+ assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
+
+ firm = Firm.find(:first).clone
+ firm.account = Account.find(:first)
+ assert_queries(2) { firm.save! }
+
+ firm = Firm.find(:first).clone
+ firm.account = Account.find(:first).clone
+ assert_queries(2) { firm.save! }
+ end
+end
+
+class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
+ def test_save_fails_for_invalid_belongs_to
+ assert log = AuditLog.create(:developer_id => 0, :message => "")
+
+ log.developer = Developer.new
+ assert !log.developer.valid?
+ assert !log.valid?
+ assert !log.save
+ assert_equal "is invalid", log.errors.on("developer")
+ end
+
+ def test_save_succeeds_for_invalid_belongs_to_with_validate_false
+ assert log = AuditLog.create(:developer_id => 0, :message=> "")
+
+ log.unvalidated_developer = Developer.new
+ assert !log.unvalidated_developer.valid?
+ assert log.valid?
+ assert log.save
+ end
+
+ def test_assignment_before_parent_saved
+ client = Client.find(:first)
+ apple = Firm.new("name" => "Apple")
+ client.firm = apple
+ assert_equal apple, client.firm
+ assert apple.new_record?
+ assert client.save
+ assert apple.save
+ assert !apple.new_record?
+ assert_equal apple, client.firm
+ assert_equal apple, client.firm(true)
+ end
+
+ def test_assignment_before_either_saved
+ final_cut = Client.new("name" => "Final Cut")
+ apple = Firm.new("name" => "Apple")
+ final_cut.firm = apple
+ assert final_cut.new_record?
+ assert apple.new_record?
+ assert final_cut.save
+ assert !final_cut.new_record?
+ assert !apple.new_record?
+ assert_equal apple, final_cut.firm
+ assert_equal apple, final_cut.firm(true)
+ end
+
+ def test_store_two_association_with_one_save
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.new
+
+ customer1 = order.billing = Customer.new
+ customer2 = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer1, order.billing
+ assert_equal customer2, order.shipping
+
+ order.reload
+
+ assert_equal customer1, order.billing
+ assert_equal customer2, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +2, Customer.count
+ end
+
+ def test_store_association_in_two_relations_with_one_save
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.new
+
+ customer = order.billing = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ order.reload
+
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +1, Customer.count
+ end
+
+ def test_store_association_in_two_relations_with_one_save_in_existing_object
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.create
+
+ customer = order.billing = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ order.reload
+
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +1, Customer.count
+ end
+
+ def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values
+ num_orders = Order.count
+ num_customers = Customer.count
+ order = Order.create
+
+ customer = order.billing = order.shipping = Customer.new
+ assert order.save
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ order.reload
+
+ customer = order.billing = order.shipping = Customer.new
+
+ assert order.save
+ order.reload
+
+ assert_equal customer, order.billing
+ assert_equal customer, order.shipping
+
+ assert_equal num_orders +1, Order.count
+ assert_equal num_customers +2, Customer.count
+ end
+end
+
+class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
+ fixtures :companies, :people
+
+ def test_invalid_adding
+ firm = Firm.find(1)
+ assert !(firm.clients_of_firm << c = Client.new)
+ assert c.new_record?
+ assert !firm.valid?
+ assert !firm.save
+ assert c.new_record?
+ end
+
+ def test_invalid_adding_before_save
+ no_of_firms = Firm.count
+ no_of_clients = Client.count
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
+ assert c.new_record?
+ assert !c.valid?
+ assert !new_firm.valid?
+ assert !new_firm.save
+ assert c.new_record?
+ assert new_firm.new_record?
+ end
+
+ def test_invalid_adding_with_validate_false
+ firm = Firm.find(:first)
+ client = Client.new
+ firm.unvalidated_clients_of_firm << client
+
+ assert firm.valid?
+ assert !client.valid?
+ assert firm.save
+ assert client.new_record?
+ end
+
+ def test_valid_adding_with_validate_false
+ no_of_clients = Client.count
+
+ firm = Firm.find(:first)
+ client = Client.new("name" => "Apple")
+
+ assert firm.valid?
+ assert client.valid?
+ assert client.new_record?
+
+ firm.unvalidated_clients_of_firm << client
+
+ assert firm.save
+ assert !client.new_record?
+ assert_equal no_of_clients+1, Client.count
+ end
+
+ def test_invalid_build
+ new_client = companies(:first_firm).clients_of_firm.build
+ assert new_client.new_record?
+ assert !new_client.valid?
+ assert_equal new_client, companies(:first_firm).clients_of_firm.last
+ assert !companies(:first_firm).save
+ assert new_client.new_record?
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ end
+
+ def test_adding_before_save
+ no_of_firms = Firm.count
+ no_of_clients = Client.count
+
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ c = Client.new("name" => "Apple")
+
+ new_firm.clients_of_firm.push Client.new("name" => "Natural Company")
+ assert_equal 1, new_firm.clients_of_firm.size
+ new_firm.clients_of_firm << c
+ assert_equal 2, new_firm.clients_of_firm.size
+
+ assert_equal no_of_firms, Firm.count # Firm was not saved to database.
+ assert_equal no_of_clients, Client.count # Clients were not saved to database.
+ assert new_firm.save
+ assert !new_firm.new_record?
+ assert !c.new_record?
+ assert_equal new_firm, c.firm
+ assert_equal no_of_firms+1, Firm.count # Firm was saved to database.
+ assert_equal no_of_clients+2, Client.count # Clients were saved to database.
+
+ assert_equal 2, new_firm.clients_of_firm.size
+ assert_equal 2, new_firm.clients_of_firm(true).size
+ end
+
+ def test_assign_ids
+ firm = Firm.new("name" => "Apple")
+ firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
+ firm.save
+ firm.reload
+ assert_equal 2, firm.clients.length
+ assert firm.clients.include?(companies(:second_client))
+ end
+
+ def test_assign_ids_for_through_a_belongs_to
+ post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!")
+ post.person_ids = [people(:david).id, people(:michael).id]
+ post.save
+ post.reload
+ assert_equal 2, post.people.length
+ assert post.people.include?(people(:david))
+ end
+
+ def test_build_before_save
+ company = companies(:first_firm)
+ new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
+ assert !company.clients_of_firm.loaded?
+
+ company.name += '-changed'
+ assert_queries(2) { assert company.save }
+ assert !new_client.new_record?
+ assert_equal 2, company.clients_of_firm(true).size
+ end
+
+ def test_build_many_before_save
+ company = companies(:first_firm)
+ new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
+
+ company.name += '-changed'
+ assert_queries(3) { assert company.save }
+ assert_equal 3, company.clients_of_firm(true).size
+ end
+
+ def test_build_via_block_before_save
+ company = companies(:first_firm)
+ new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } }
+ assert !company.clients_of_firm.loaded?
+
+ company.name += '-changed'
+ assert_queries(2) { assert company.save }
+ assert !new_client.new_record?
+ assert_equal 2, company.clients_of_firm(true).size
+ end
+
+ def test_build_many_via_block_before_save
+ company = companies(:first_firm)
+ new_clients = assert_no_queries do
+ company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
+ client.name = "changed"
+ end
+ end
+
+ company.name += '-changed'
+ assert_queries(3) { assert company.save }
+ assert_equal 3, company.clients_of_firm(true).size
+ end
+
+ def test_replace_on_new_object
+ firm = Firm.new("name" => "New Firm")
+ firm.clients = [companies(:second_client), Client.new("name" => "New Client")]
+ assert firm.save
+ firm.reload
+ assert_equal 2, firm.clients.length
+ assert firm.clients.include?(Client.find_by_name("New Client"))
+ end
+end
+
+class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
+ end
+
+ # reload
+ def test_a_marked_for_destruction_record_should_not_be_be_marked_after_reload
+ @pirate.mark_for_destruction
+ @pirate.ship.mark_for_destruction
+
+ assert !@pirate.reload.marked_for_destruction?
+ assert !@pirate.ship.marked_for_destruction?
+ end
+
+ # has_one
+ def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
+ assert !@pirate.ship.marked_for_destruction?
+
+ @pirate.ship.mark_for_destruction
+ id = @pirate.ship.id
+
+ assert @pirate.ship.marked_for_destruction?
+ assert Ship.find_by_id(id)
+
+ @pirate.save
+ assert_nil @pirate.reload.ship
+ assert_nil Ship.find_by_id(id)
+ end
+
+ def test_should_skip_validation_on_a_child_association_if_marked_for_destruction
+ @pirate.ship.name = ''
+ assert !@pirate.valid?
+
+ @pirate.ship.mark_for_destruction
+ assert_difference('Ship.count', -1) { @pirate.save! }
+ end
+
+ def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_child
+ # Stub the save method of the @pirate.ship instance to destroy and then raise an exception
+ class << @pirate.ship
+ def save(*args)
+ super
+ destroy
+ raise 'Oh noes!'
+ end
+ end
+
+ assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_not_nil @pirate.reload.ship
+ end
+
+ # belongs_to
+ def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
+ assert !@ship.pirate.marked_for_destruction?
+
+ @ship.pirate.mark_for_destruction
+ id = @ship.pirate.id
+
+ assert @ship.pirate.marked_for_destruction?
+ assert Pirate.find_by_id(id)
+
+ @ship.save
+ assert_nil @ship.reload.pirate
+ assert_nil Pirate.find_by_id(id)
+ end
+
+ def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction
+ @ship.pirate.catchphrase = ''
+ assert !@ship.valid?
+
+ @ship.pirate.mark_for_destruction
+ assert_difference('Pirate.count', -1) { @ship.save! }
+ end
+
+ def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_parent
+ # Stub the save method of the @ship.pirate instance to destroy and then raise an exception
+ class << @ship.pirate
+ def save(*args)
+ super
+ destroy
+ raise 'Oh noes!'
+ end
+ end
+
+ assert_raise(RuntimeError) { assert !@ship.save }
+ assert_not_nil @ship.reload.pirate
+ 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}") }
+
+ assert !@pirate.send(association_name).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)
+
+ assert @pirate.send(association_name).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) }
+ end
+
+ 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)
+
+ children.each { |child| child.name = '' }
+ assert !@pirate.valid?
+
+ children.each { |child| child.mark_for_destruction }
+ assert_difference("#{association_name.classify}.count", -2) { @pirate.save! }
+ 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 }
+
+ # Stub the save method of the first child to destroy and the second to raise an exception
+ class << before.first
+ def save(*args)
+ super
+ destroy
+ end
+ end
+ class << before.last
+ def save(*args)
+ super
+ raise 'Oh noes!'
+ end
+ end
+
+ assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_equal before, @pirate.reload.send(association_name)
+ 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"
+
+ pirate = Pirate.new(:catchphrase => "Arr")
+ pirate.send(association_name_with_callbacks).build(:name => "Crowe the One-Eyed")
+
+ expected = [
+ "before_adding_#{callback_type}_#{association_name.singularize}_<new>",
+ "after_adding_#{callback_type}_#{association_name.singularize}_<new>"
+ ]
+
+ assert_equal expected, pirate.ship_log
+ 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.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}_#{association_name.singularize}_#{child_id}",
+ "after_removing_#{callback_type}_#{association_name.singularize}_#{child_id}"
+ ]
+
+ assert_equal expected, @pirate.ship_log
+ end
+ end
+ end
+end
+
+class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
+ end
+
+ def test_should_still_work_without_an_associated_model
+ @ship.destroy
+ @pirate.reload.catchphrase = "Arr"
+ @pirate.save
+ assert 'Arr', @pirate.reload.catchphrase
+ end
+
+ def test_should_automatically_save_the_associated_model
+ @pirate.ship.name = 'The Vile Insanity'
+ @pirate.save
+ assert_equal 'The Vile Insanity', @pirate.reload.ship.name
+ end
+
+ def test_should_automatically_save_bang_the_associated_model
+ @pirate.ship.name = 'The Vile Insanity'
+ @pirate.save!
+ assert_equal 'The Vile Insanity', @pirate.reload.ship.name
+ end
+
+ def test_should_automatically_validate_the_associated_model
+ @pirate.ship.name = ''
+ assert !@pirate.valid?
+ assert !@pirate.errors.on(:ship_name).blank?
+ end
+
+ def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
+ @pirate.ship.name = nil
+ @pirate.catchphrase = nil
+ assert !@pirate.valid?
+ assert !@pirate.errors.on(:ship_name).blank?
+ assert !@pirate.errors.on(:catchphrase).blank?
+ end
+
+ def test_should_still_allow_to_bypass_validations_on_the_associated_model
+ @pirate.catchphrase = ''
+ @pirate.ship.name = ''
+ @pirate.save(false)
+ assert_equal ['', ''], [@pirate.reload.catchphrase, @pirate.ship.name]
+ end
+
+ def test_should_allow_to_bypass_validations_on_associated_models_at_any_depth
+ 2.times { |i| @pirate.ship.parts.create!(:name => "part #{i}") }
+
+ @pirate.catchphrase = ''
+ @pirate.ship.name = ''
+ @pirate.ship.parts.each { |part| part.name = '' }
+ @pirate.save(false)
+
+ values = [@pirate.reload.catchphrase, @pirate.ship.name, *@pirate.ship.parts.map(&:name)]
+ assert_equal ['', '', '', ''], values
+ end
+
+ def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
+ @pirate.ship.name = ''
+ assert_raise(ActiveRecord::RecordInvalid) do
+ @pirate.save!
+ end
+ end
+
+ def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
+ before = [@pirate.catchphrase, @pirate.ship.name]
+
+ @pirate.catchphrase = 'Arr'
+ @pirate.ship.name = 'The Vile Insanity'
+
+ # Stub the save method of the @pirate.ship instance to raise an exception
+ class << @pirate.ship
+ def save(*args)
+ super
+ raise 'Oh noes!'
+ end
+ end
+
+ assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name]
+ end
+
+ def test_should_not_load_the_associated_model
+ assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! }
+ end
+end
+
+class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @ship = Ship.create(:name => 'Nights Dirty Lightning')
+ @pirate = @ship.create_pirate(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ end
+
+ def test_should_still_work_without_an_associated_model
+ @pirate.destroy
+ @ship.reload.name = "The Vile Insanity"
+ @ship.save
+ assert 'The Vile Insanity', @ship.reload.name
+ end
+
+ def test_should_automatically_save_the_associated_model
+ @ship.pirate.catchphrase = 'Arr'
+ @ship.save
+ assert_equal 'Arr', @ship.reload.pirate.catchphrase
+ end
+
+ def test_should_automatically_save_bang_the_associated_model
+ @ship.pirate.catchphrase = 'Arr'
+ @ship.save!
+ assert_equal 'Arr', @ship.reload.pirate.catchphrase
+ end
+
+ def test_should_automatically_validate_the_associated_model
+ @ship.pirate.catchphrase = ''
+ assert !@ship.valid?
+ assert !@ship.errors.on(:pirate_catchphrase).blank?
+ end
+
+ def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
+ @ship.name = nil
+ @ship.pirate.catchphrase = nil
+ assert !@ship.valid?
+ assert !@ship.errors.on(:name).blank?
+ assert !@ship.errors.on(:pirate_catchphrase).blank?
+ end
+
+ def test_should_still_allow_to_bypass_validations_on_the_associated_model
+ @ship.pirate.catchphrase = ''
+ @ship.name = ''
+ @ship.save(false)
+ assert_equal ['', ''], [@ship.reload.name, @ship.pirate.catchphrase]
+ end
+
+ def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
+ @ship.pirate.catchphrase = ''
+ assert_raise(ActiveRecord::RecordInvalid) do
+ @ship.save!
+ end
+ end
+
+ def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
+ before = [@ship.pirate.catchphrase, @ship.name]
+
+ @ship.pirate.catchphrase = 'Arr'
+ @ship.name = 'The Vile Insanity'
+
+ # Stub the save method of the @ship.pirate instance to raise an exception
+ class << @ship.pirate
+ def save(*args)
+ super
+ raise 'Oh noes!'
+ end
+ end
+
+ assert_raise(RuntimeError) { assert !@ship.save }
+ # TODO: Why does using reload on @ship looses the associated pirate?
+ assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name]
+ end
+
+ def test_should_not_load_the_associated_model
+ assert_queries(1) { @ship.name = 'The Vile Insanity'; @ship.save! }
+ end
+end
+
+module AutosaveAssociationOnACollectionAssociationTests
+ def test_should_automatically_save_the_associated_models
+ new_names = ['Grace OMalley', 'Privateers Greed']
+ @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
+
+ @pirate.save
+ assert_equal new_names, @pirate.reload.send(@association_name).map(&:name)
+ end
+
+ def test_should_automatically_save_bang_the_associated_models
+ new_names = ['Grace OMalley', 'Privateers Greed']
+ @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
+
+ @pirate.save!
+ assert_equal new_names, @pirate.reload.send(@association_name).map(&:name)
+ end
+
+ def test_should_automatically_validate_the_associated_models
+ @pirate.send(@association_name).each { |child| child.name = '' }
+
+ assert !@pirate.valid?
+ assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
+ assert @pirate.errors.on(@association_name).blank?
+ end
+
+ def test_should_not_use_default_invalid_error_on_associated_models
+ @pirate.send(@association_name).build(:name => '')
+
+ assert !@pirate.valid?
+ assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
+ assert @pirate.errors.on(@association_name).blank?
+ end
+
+ def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
+ @pirate.send(@association_name).each { |child| child.name = '' }
+ @pirate.catchphrase = nil
+
+ assert !@pirate.valid?
+ assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name")
+ assert !@pirate.errors.on(:catchphrase).blank?
+ end
+
+ def test_should_allow_to_bypass_validations_on_the_associated_models_on_update
+ @pirate.catchphrase = ''
+ @pirate.send(@association_name).each { |child| child.name = '' }
+
+ assert @pirate.save(false)
+ assert_equal ['', '', ''], [
+ @pirate.reload.catchphrase,
+ @pirate.send(@association_name).first.name,
+ @pirate.send(@association_name).last.name
+ ]
+ end
+
+ def test_should_validation_the_associated_models_on_create
+ assert_no_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count") do
+ 2.times { @pirate.send(@association_name).build }
+ @pirate.save(true)
+ end
+ end
+
+ def test_should_allow_to_bypass_validations_on_the_associated_models_on_create
+ assert_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count", +2) do
+ 2.times { @pirate.send(@association_name).build }
+ @pirate.save(false)
+ end
+ end
+
+ def test_should_rollback_any_changes_if_an_exception_occurred_while_saving
+ before = [@pirate.catchphrase, *@pirate.send(@association_name).map(&:name)]
+ new_names = ['Grace OMalley', 'Privateers Greed']
+
+ @pirate.catchphrase = 'Arr'
+ @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
+
+ # Stub the save method of the first child instance to raise an exception
+ class << @pirate.send(@association_name).first
+ def save(*args)
+ super
+ raise 'Oh noes!'
+ end
+ end
+
+ assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)]
+ end
+
+ def test_should_still_raise_an_ActiveRecordRecord_Invalid_exception_if_we_want_that
+ @pirate.send(@association_name).each { |child| child.name = '' }
+ assert_raise(ActiveRecord::RecordInvalid) do
+ @pirate.save!
+ end
+ end
+
+ def test_should_not_load_the_associated_models_if_they_were_not_loaded_yet
+ assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! }
+
+ @pirate.send(@association_name).class # hack to load the target
+
+ assert_queries(3) do
+ @pirate.catchphrase = 'Yarr'
+ new_names = ['Grace OMalley', 'Privateers Greed']
+ @pirate.send(@association_name).each_with_index { |child, i| child.name = new_names[i] }
+ @pirate.save!
+ end
+ end
+end
+
+class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @association_name = :birds
+
+ @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @child_1 = @pirate.birds.create(:name => 'Posideons Killer')
+ @child_2 = @pirate.birds.create(:name => 'Killer bandita Dionne')
+ end
+
+ include AutosaveAssociationOnACollectionAssociationTests
+end
+
+class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @association_name = :parrots
+ @habtm = true
+
+ @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @child_1 = @pirate.parrots.create(:name => 'Posideons Killer')
+ @child_2 = @pirate.parrots.create(:name => 'Killer bandita Dionne')
+ end
+
+ include AutosaveAssociationOnACollectionAssociationTests
+end \ No newline at end of file
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 973bb567bd..99d77961fc 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -424,8 +424,8 @@ class BasicsTest < ActiveRecord::TestCase
def test_non_attribute_access_and_assignment
topic = Topic.new
assert !topic.respond_to?("mumbo")
- assert_raises(NoMethodError) { topic.mumbo }
- assert_raises(NoMethodError) { topic.mumbo = 5 }
+ assert_raise(NoMethodError) { topic.mumbo }
+ assert_raise(NoMethodError) { topic.mumbo = 5 }
end
def test_preserving_date_objects
@@ -490,7 +490,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_record_not_found_exception
- assert_raises(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) }
+ assert_raise(ActiveRecord::RecordNotFound) { topicReloaded = Topic.find(99999) }
end
def test_initialize_with_attributes
@@ -848,7 +848,7 @@ class BasicsTest < ActiveRecord::TestCase
client.delete
assert client.frozen?
assert_kind_of Firm, client.firm
- assert_raises(ActiveSupport::FrozenObjectError) { client.name = "something else" }
+ assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
end
def test_destroy_new_record
@@ -862,7 +862,7 @@ class BasicsTest < ActiveRecord::TestCase
client.destroy
assert client.frozen?
assert_kind_of Firm, client.firm
- assert_raises(ActiveSupport::FrozenObjectError) { client.name = "something else" }
+ assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
end
def test_update_attribute
@@ -910,8 +910,8 @@ class BasicsTest < ActiveRecord::TestCase
def test_mass_assignment_should_raise_exception_if_accessible_and_protected_attribute_writers_are_both_used
topic = TopicWithProtectedContentAndAccessibleAuthorName.new
- assert_raises(RuntimeError) { topic.attributes = { "author_name" => "me" } }
- assert_raises(RuntimeError) { topic.attributes = { "content" => "stuff" } }
+ assert_raise(RuntimeError) { topic.attributes = { "author_name" => "me" } }
+ assert_raise(RuntimeError) { topic.attributes = { "content" => "stuff" } }
end
def test_mass_assignment_protection
@@ -949,7 +949,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_mass_assigning_invalid_attribute
firm = Firm.new
- assert_raises(ActiveRecord::UnknownAttributeError) do
+ assert_raise(ActiveRecord::UnknownAttributeError) do
firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 }
end
end
@@ -1402,7 +1402,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_sql_injection_via_find
- assert_raises(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
Topic.find("123456 OR id > 0")
end
end
@@ -1755,6 +1755,13 @@ class BasicsTest < ActiveRecord::TestCase
end
end
+ def test_scoped_find_with_group_and_having
+ developers = Developer.with_scope(:find => { :group => 'salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do
+ Developer.find(:all)
+ end
+ assert_equal 3, developers.size
+ end
+
def test_find_last
last = Developer.find :last
assert_equal last, Developer.find(:first, :order => 'id desc')
@@ -1783,6 +1790,11 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal last, Developer.find(:all, :order => 'developers.name, developers.salary DESC').last
end
+ def test_find_symbol_ordered_last
+ last = Developer.find :last, :order => :salary
+ assert_equal last, Developer.find(:all, :order => :salary).last
+ end
+
def test_find_scoped_ordered_last
last_developer = Developer.with_scope(:find => { :order => 'developers.salary ASC' }) do
Developer.find(:last)
@@ -2092,18 +2104,4 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal custom_datetime, parrot[attribute]
end
end
-
- private
- def with_kcode(kcode)
- if RUBY_VERSION < '1.9'
- orig_kcode, $KCODE = $KCODE, kcode
- begin
- yield
- ensure
- $KCODE = orig_kcode
- end
- else
- yield
- end
- end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
new file mode 100644
index 0000000000..5009a90846
--- /dev/null
+++ b/activerecord/test/cases/batches_test.rb
@@ -0,0 +1,61 @@
+require 'cases/helper'
+require 'models/post'
+
+class EachTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def setup
+ @posts = Post.all(:order => "id asc")
+ @total = Post.count
+ end
+
+ def test_each_should_excecute_one_query_per_batch
+ assert_queries(Post.count + 1) do
+ Post.find_each(:batch_size => 1) do |post|
+ assert_kind_of Post, post
+ end
+ end
+ end
+
+ def test_each_should_raise_if_the_order_is_set
+ assert_raise(RuntimeError) do
+ Post.find_each(:order => "title") { |post| post }
+ end
+ end
+
+ def test_each_should_raise_if_the_limit_is_set
+ assert_raise(RuntimeError) do
+ Post.find_each(:limit => 1) { |post| post }
+ end
+ end
+
+ def test_find_in_batches_should_return_batches
+ assert_queries(Post.count + 1) do
+ Post.find_in_batches(:batch_size => 1) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
+ end
+ end
+ end
+
+ def test_find_in_batches_should_start_from_the_start_option
+ assert_queries(Post.count) do
+ Post.find_in_batches(:batch_size => 1, :start => 2) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
+ end
+ end
+ end
+
+ def test_find_in_batches_shouldnt_excute_query_unless_needed
+ post_count = Post.count
+
+ assert_queries(2) do
+ Post.find_in_batches(:batch_size => post_count) {|batch| assert_kind_of Array, batch }
+ end
+
+ assert_queries(1) do
+ Post.find_in_batches(:batch_size => post_count + 1) {|batch| assert_kind_of Array, batch }
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 080f6a7007..56dcdea110 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -92,6 +92,14 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, c[2]
end
+ def test_should_group_by_summed_field_having_sanitized_condition
+ c = Account.sum(:credit_limit, :group => :firm_id,
+ :having => ['sum(credit_limit) > ?', 50])
+ assert_nil c[1]
+ assert_equal 105, c[6]
+ assert_equal 60, c[2]
+ end
+
def test_should_group_by_summed_association
c = Account.sum(:credit_limit, :group => :firm)
assert_equal 50, c[companies(:first_firm)]
@@ -156,25 +164,23 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 1, c[companies(:first_client)]
end
- uses_mocha 'group_by_non_numeric_foreign_key_association' do
- def test_should_group_by_association_with_non_numeric_foreign_key
- ActiveRecord::Base.connection.expects(:select_all).returns([{"count_all" => 1, "firm_id" => "ABC"}])
+ def test_should_group_by_association_with_non_numeric_foreign_key
+ ActiveRecord::Base.connection.expects(:select_all).returns([{"count_all" => 1, "firm_id" => "ABC"}])
- firm = mock()
- firm.expects(:id).returns("ABC")
- firm.expects(:class).returns(Firm)
- Company.expects(:find).with(["ABC"]).returns([firm])
+ firm = mock()
+ firm.expects(:id).returns("ABC")
+ firm.expects(:class).returns(Firm)
+ Company.expects(:find).with(["ABC"]).returns([firm])
- column = mock()
- column.expects(:name).at_least_once.returns(:firm_id)
- column.expects(:type_cast).with("ABC").returns("ABC")
- Account.expects(:columns).at_least_once.returns([column])
+ column = mock()
+ column.expects(:name).at_least_once.returns(:firm_id)
+ column.expects(:type_cast).with("ABC").returns("ABC")
+ Account.expects(:columns).at_least_once.returns([column])
- c = Account.count(:all, :group => :firm)
- first_key = c.keys.first
- assert_equal Firm, first_key.class
- assert_equal 1, c[first_key]
- end
+ c = Account.count(:all, :group => :firm)
+ first_key = c.keys.first
+ assert_equal Firm, first_key.class
+ assert_equal 1, c[first_key]
end
def test_should_calculate_grouped_association_with_foreign_key_option
@@ -249,8 +255,8 @@ class CalculationsTest < ActiveRecord::TestCase
Company.send(:validate_calculation_options, :count, :include => true)
end
- assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
- assert_raises(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) }
+ assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
+ assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :count, :foo => :bar) }
end
def test_should_count_selected_field_with_include
@@ -258,6 +264,19 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 4, Account.count(:distinct => true, :include => :firm, :select => :credit_limit)
end
+ def test_should_count_scoped_select
+ Account.update_all("credit_limit = NULL")
+ assert_equal 0, Account.scoped(:select => "credit_limit").count
+ end
+
+ def test_should_count_scoped_select_with_options
+ Account.update_all("credit_limit = NULL")
+ Account.last.update_attribute('credit_limit', 49)
+ Account.first.update_attribute('credit_limit', 51)
+
+ assert_equal 1, Account.scoped(:select => "credit_limit").count(:conditions => ['credit_limit >= 50'])
+ end
+
def test_should_count_manual_select_with_include
assert_equal 6, Account.count(:select => "DISTINCT accounts.id", :include => :firm)
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 33b1ea034d..95fddaeef6 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -352,13 +352,13 @@ class CallbacksTest < ActiveRecord::TestCase
david = ImmutableDeveloper.find(1)
assert david.valid?
assert !david.save
- assert_raises(ActiveRecord::RecordNotSaved) { david.save! }
+ assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
david = ImmutableDeveloper.find(1)
david.salary = 10_000_000
assert !david.valid?
assert !david.save
- assert_raises(ActiveRecord::RecordInvalid) { david.save! }
+ assert_raise(ActiveRecord::RecordInvalid) { david.save! }
someone = CallbackCancellationDeveloper.find(1)
someone.cancel_before_save = true
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
new file mode 100644
index 0000000000..cc9b2a45f4
--- /dev/null
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -0,0 +1,25 @@
+require "cases/helper"
+
+class ConnectionManagementTest < ActiveRecord::TestCase
+ def setup
+ @env = {}
+ @app = stub('App')
+ @management = ActiveRecord::ConnectionAdapters::ConnectionManagement.new(@app)
+
+ @connections_cleared = false
+ ActiveRecord::Base.stubs(:clear_active_connections!).with { @connections_cleared = true }
+ end
+
+ test "clears active connections after each call" do
+ @app.expects(:call).with(@env)
+ @management.call(@env)
+ assert @connections_cleared
+ end
+
+ test "doesn't clear active connections when running in a test case" do
+ @env['rack.test'] = true
+ @app.expects(:call).with(@env)
+ @management.call(@env)
+ assert !@connections_cleared
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/cases/datatype_test_postgresql.rb b/activerecord/test/cases/datatype_test_postgresql.rb
index bff092b5d7..88fb6f7384 100644
--- a/activerecord/test/cases/datatype_test_postgresql.rb
+++ b/activerecord/test/cases/datatype_test_postgresql.rb
@@ -26,6 +26,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
+ @connection.execute("set lc_monetary = 'C'")
@connection.execute("INSERT INTO postgresql_arrays (commission_by_quarter, nicknames) VALUES ( '{35000,21000,18000,17000}', '{foo,bar,baz}' )")
@first_array = PostgresqlArray.find(1)
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 1c9e281cc0..ac95bac4ad 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -166,7 +166,7 @@ class DirtyTest < ActiveRecord::TestCase
def test_association_assignment_changes_foreign_key
pirate = Pirate.create!(:catchphrase => 'jarl')
- pirate.parrot = Parrot.create!
+ pirate.parrot = Parrot.create!(:name => 'Lorre')
assert pirate.changed?
assert_equal %w(parrot_id), pirate.changed
end
@@ -228,7 +228,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.new
pirate.parrot_id = 1
- assert_raises(ActiveRecord::RecordInvalid) { pirate.save! }
+ assert_raise(ActiveRecord::RecordInvalid) { pirate.save! }
check_pirate_after_save_failure(pirate)
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index d4d770b04e..d8778957c0 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -94,7 +94,16 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(NoMethodError) { Topic.exists?([1,2]) }
end
-
+
+ def test_exists_returns_true_with_one_record_and_no_args
+ assert Topic.exists?
+ end
+
+ def test_does_not_exist_with_empty_table_and_no_args_given
+ Topic.delete_all
+ assert !Topic.exists?
+ end
+
def test_exists_with_aggregate_having_three_mappings
existing_address = customers(:david).address
assert Customer.exists?(:address => existing_address)
@@ -137,7 +146,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_ids_missing_one
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, 2, 45) }
end
def test_find_all_with_limit
@@ -182,6 +191,13 @@ class FinderTest < ActiveRecord::TestCase
assert developers.all? { |developer| developer.salary > 10000 }
end
+ def test_find_with_group_and_sanitized_having
+ developers = Developer.find(:all, :group => "salary", :having => ["sum(salary) > ?", 10000], :select => "salary")
+ assert_equal 3, developers.size
+ assert_equal 3, developers.map(&:salary).uniq.size
+ assert developers.all? { |developer| developer.salary > 10000 }
+ end
+
def test_find_with_entire_select_statement
topics = Topic.find_by_sql "SELECT * FROM topics WHERE author_name = 'Mary'"
@@ -220,7 +236,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_unexisting_record_exception_handling
- assert_raises(ActiveRecord::RecordNotFound) {
+ assert_raise(ActiveRecord::RecordNotFound) {
Topic.find(1).parent
}
@@ -229,7 +245,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_only_some_columns
topic = Topic.find(1, :select => "author_name")
- assert_raises(ActiveRecord::MissingAttributeError) {topic.title}
+ assert_raise(ActiveRecord::MissingAttributeError) {topic.title}
assert_equal "David", topic.author_name
assert !topic.attribute_present?("title")
#assert !topic.respond_to?("title")
@@ -251,22 +267,22 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_array_conditions
assert Topic.find(1, :conditions => ["approved = ?", false])
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) }
end
def test_find_on_hash_conditions
assert Topic.find(1, :conditions => { :approved => false })
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) }
end
def test_find_on_hash_conditions_with_explicit_table_name
assert Topic.find(1, :conditions => { 'topics.approved' => false })
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { 'topics.approved' => true }) }
end
def test_find_on_hash_conditions_with_hashed_table_name
assert Topic.find(1, :conditions => {:topics => { :approved => false }})
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => {:topics => { :approved => true }}) }
end
def test_find_with_hash_conditions_on_joined_table
@@ -284,7 +300,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_hash_conditions_with_explicit_table_name_and_aggregate
david = customers(:david)
assert Customer.find(david.id, :conditions => { 'customers.name' => david.name, :address => david.address })
- assert_raises(ActiveRecord::RecordNotFound) {
+ assert_raise(ActiveRecord::RecordNotFound) {
Customer.find(david.id, :conditions => { 'customers.name' => david.name + "1", :address => david.address })
}
end
@@ -295,7 +311,13 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_hash_conditions_with_range
assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1..2 }).map(&:id).sort
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :id => 2..3 }) }
+ end
+
+ def test_find_on_hash_conditions_with_end_exclusive_range
+ assert_equal [1,2,3], Topic.find(:all, :conditions => { :id => 1..3 }).map(&:id).sort
+ assert_equal [1,2], Topic.find(:all, :conditions => { :id => 1...3 }).map(&:id).sort
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(3, :conditions => { :id => 2...3 }) }
end
def test_find_on_hash_conditions_with_multiple_ranges
@@ -305,9 +327,9 @@ class FinderTest < ActiveRecord::TestCase
def test_find_on_multiple_hash_conditions
assert Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false })
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) }
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
end
def test_condition_interpolation
@@ -331,7 +353,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_hash_condition_find_malformed
- assert_raises(ActiveRecord::StatementInvalid) {
+ assert_raise(ActiveRecord::StatementInvalid) {
Company.find(:first, :conditions => { :id => 2, :dhh => true })
}
end
@@ -400,10 +422,10 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"])
assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!' OR 1=1"])
assert_kind_of Time, Topic.find(:first, :conditions => ["id = ?", 1]).written_on
- assert_raises(ActiveRecord::PreparedStatementInvalid) {
+ assert_raise(ActiveRecord::PreparedStatementInvalid) {
Company.find(:first, :conditions => ["id=? AND name = ?", 2])
}
- assert_raises(ActiveRecord::PreparedStatementInvalid) {
+ assert_raise(ActiveRecord::PreparedStatementInvalid) {
Company.find(:first, :conditions => ["id=?", 2, 3, 4])
}
end
@@ -420,11 +442,11 @@ class FinderTest < ActiveRecord::TestCase
def test_bind_arity
assert_nothing_raised { bind '' }
- assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
- assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?' }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' }
assert_nothing_raised { bind '?', 1 }
- assert_raises(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
end
def test_named_bind_variables
@@ -507,21 +529,19 @@ class FinderTest < ActiveRecord::TestCase
assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
end
- uses_mocha('test_dynamic_finder_should_go_through_the_find_class_method') do
- def test_dynamic_finders_should_go_through_the_find_class_method
- Topic.expects(:find).with(:first, :conditions => { :title => 'The First Topic!' })
- Topic.find_by_title("The First Topic!")
+ def test_dynamic_finders_should_go_through_the_find_class_method
+ Topic.expects(:find).with(:first, :conditions => { :title => 'The First Topic!' })
+ Topic.find_by_title("The First Topic!")
- Topic.expects(:find).with(:last, :conditions => { :title => 'The Last Topic!' })
- Topic.find_last_by_title("The Last Topic!")
+ Topic.expects(:find).with(:last, :conditions => { :title => 'The Last Topic!' })
+ Topic.find_last_by_title("The Last Topic!")
- Topic.expects(:find).with(:all, :conditions => { :title => 'A Topic.' })
- Topic.find_all_by_title("A Topic.")
+ Topic.expects(:find).with(:all, :conditions => { :title => 'A Topic.' })
+ Topic.find_all_by_title("A Topic.")
- Topic.expects(:find).with(:first, :conditions => { :title => 'Does not exist yet for sure!' }).times(2)
- Topic.find_or_initialize_by_title('Does not exist yet for sure!')
- Topic.find_or_create_by_title('Does not exist yet for sure!')
- end
+ Topic.expects(:find).with(:first, :conditions => { :title => 'Does not exist yet for sure!' }).times(2)
+ Topic.find_or_initialize_by_title('Does not exist yet for sure!')
+ Topic.find_or_create_by_title('Does not exist yet for sure!')
end
def test_find_by_one_attribute
@@ -531,7 +551,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_by_one_attribute_bang
assert_equal topics(:first), Topic.find_by_title!("The First Topic")
- assert_raises(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
end
def test_find_by_one_attribute_caches_dynamic_finder
@@ -612,14 +632,14 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_one_missing_attribute
- assert_raises(NoMethodError) { Topic.find_by_undertitle("The First Topic!") }
+ assert_raise(NoMethodError) { Topic.find_by_undertitle("The First Topic!") }
end
def test_find_by_invalid_method_syntax
- assert_raises(NoMethodError) { Topic.fail_to_find_by_title("The First Topic") }
- assert_raises(NoMethodError) { Topic.find_by_title?("The First Topic") }
- assert_raises(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") }
- assert_raises(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") }
+ assert_raise(NoMethodError) { Topic.fail_to_find_by_title("The First Topic") }
+ assert_raise(NoMethodError) { Topic.find_by_title?("The First Topic") }
+ assert_raise(NoMethodError) { Topic.fail_to_find_or_create_by_title("Nonexistent Title") }
+ assert_raise(NoMethodError) { Topic.find_or_create_by_title?("Nonexistent Title") }
end
def test_find_by_two_attributes
@@ -641,8 +661,8 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_last_by_invalid_method_syntax
- assert_raises(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
- assert_raises(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
+ assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
+ assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
end
def test_find_last_by_one_attribute_with_several_options
@@ -650,7 +670,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_last_by_one_missing_attribute
- assert_raises(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") }
+ assert_raise(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") }
end
def test_find_last_by_two_attributes
@@ -903,16 +923,16 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_bad_sql
- assert_raises(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
+ assert_raise(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" }
end
def test_find_with_invalid_params
- assert_raises(ArgumentError) { Topic.find :first, :join => "It should be `joins'" }
- assert_raises(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" }
+ assert_raise(ArgumentError) { Topic.find :first, :join => "It should be `joins'" }
+ assert_raise(ArgumentError) { Topic.find :first, :conditions => '1 = 1', :join => "It should be `joins'" }
end
def test_dynamic_finder_with_invalid_params
- assert_raises(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
+ assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
end
def test_find_all_with_limit
@@ -1044,6 +1064,14 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [0, 1, 1], posts.map(&:author_id).sort
end
+ def test_finder_with_scoped_from
+ all_topics = Topic.all
+
+ Topic.with_scope(:find => { :from => 'fake_topics' }) do
+ assert_equal all_topics, Topic.all(:from => 'topics')
+ end
+ end
+
protected
def bind(statement, *vars)
if vars.first.is_a?(Hash)
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index ed2915b023..252bf4ff61 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -151,7 +151,7 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_dirty_dirty_yaml_file
- assert_raises(Fixture::FormatError) do
+ assert_raise(Fixture::FormatError) do
Fixtures.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses")
end
end
@@ -265,12 +265,10 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase
assert_raise(StandardError) { topics([:first, :second]) }
end
- uses_mocha 'reloading_fixtures_through_accessor_methods' do
- def test_reloading_fixtures_through_accessor_methods
- assert_equal "The First Topic", topics(:first).title
- @loaded_fixtures['topics']['first'].expects(:find).returns(stub(:title => "Fresh Topic!"))
- assert_equal "Fresh Topic!", topics(:first, true).title
- end
+ def test_reloading_fixtures_through_accessor_methods
+ assert_equal "The First Topic", topics(:first).title
+ @loaded_fixtures['topics']['first'].expects(:find).returns(stub(:title => "Fresh Topic!"))
+ assert_equal "Fresh Topic!", topics(:first, true).title
end
end
@@ -422,7 +420,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def test_raises_error
- assert_raises FixtureClassNotFound do
+ assert_raise FixtureClassNotFound do
funny_jokes(:a_joke)
end
end
@@ -639,17 +637,15 @@ class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase
end
class FixtureLoadingTest < ActiveRecord::TestCase
- uses_mocha 'reloading_fixtures_through_accessor_methods' do
- def test_logs_message_for_failed_dependency_load
- ActiveRecord::TestCase.expects(:require_dependency).with(:does_not_exist).raises(LoadError)
- ActiveRecord::Base.logger.expects(:warn)
- ActiveRecord::TestCase.try_to_load_dependency(:does_not_exist)
- end
+ def test_logs_message_for_failed_dependency_load
+ ActiveRecord::TestCase.expects(:require_dependency).with(:does_not_exist).raises(LoadError)
+ ActiveRecord::Base.logger.expects(:warn)
+ ActiveRecord::TestCase.try_to_load_dependency(:does_not_exist)
+ end
- def test_does_not_logs_message_for_successful_dependency_load
- ActiveRecord::TestCase.expects(:require_dependency).with(:works_out_fine)
- ActiveRecord::Base.logger.expects(:warn).never
- ActiveRecord::TestCase.try_to_load_dependency(:works_out_fine)
- end
+ def test_does_not_logs_message_for_successful_dependency_load
+ ActiveRecord::TestCase.expects(:require_dependency).with(:works_out_fine)
+ ActiveRecord::Base.logger.expects(:warn).never
+ ActiveRecord::TestCase.try_to_load_dependency(:works_out_fine)
end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 24ce35e2e2..1ec52ac24d 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -2,7 +2,11 @@ $:.unshift(File.dirname(__FILE__) + '/../../lib')
$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib')
require 'config'
+
+require 'rubygems'
require 'test/unit'
+gem 'mocha', '>= 0.9.5'
+require 'mocha'
require 'active_record'
require 'active_record/test_case'
@@ -24,15 +28,6 @@ def current_adapter?(*types)
end
end
-def uses_mocha(description)
- require 'rubygems'
- gem 'mocha', '>= 0.9.3'
- require 'mocha'
- yield
-rescue LoadError
- $stderr.puts "Skipping #{description} tests. `gem install mocha` and try again."
-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/]
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 3f59eb9706..eae5a60829 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -68,7 +68,7 @@ class InheritanceTest < ActiveRecord::TestCase
if current_adapter?(:SybaseAdapter)
Company.connection.execute "SET IDENTITY_INSERT companies OFF"
end
- assert_raises(ActiveRecord::SubclassNotFound) { Company.find(100) }
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.find(100) }
end
def test_inheritance_find
@@ -124,7 +124,7 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_finding_incorrect_type_data
- assert_raises(ActiveRecord::RecordNotFound) { Firm.find(2) }
+ assert_raise(ActiveRecord::RecordNotFound) { Firm.find(2) }
assert_nothing_raised { Firm.find(1) }
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 077cac7747..e177235591 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -35,7 +35,25 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
- assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
+ end
+
+ def test_lock_destroy
+ p1 = Person.find(1)
+ p2 = Person.find(1)
+ assert_equal 0, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ p1.first_name = 'stu'
+ p1.save!
+ assert_equal 1, p1.lock_version
+ assert_equal 0, p2.lock_version
+
+ assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
+
+ assert p1.destroy
+ assert_equal true, p1.frozen?
+ assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
end
def test_lock_repeating
@@ -50,9 +68,9 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
- assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
p2.first_name = 'sue2'
- assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_new
@@ -71,7 +89,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, p2.lock_version
p2.first_name = 'sue'
- assert_raises(ActiveRecord::StaleObjectError) { p2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
def test_lock_new_with_nil
@@ -95,7 +113,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, t2.version
t2.tps_report_number = 800
- assert_raises(ActiveRecord::StaleObjectError) { t2.save! }
+ assert_raise(ActiveRecord::StaleObjectError) { t2.save! }
end
def test_lock_column_is_mass_assignable
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index 71e2ce8790..3c34cdeade 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -262,6 +262,15 @@ class NestedScopingTest < ActiveRecord::TestCase
end
end
+ def test_merge_inner_scope_has_priority
+ Developer.with_scope(:find => { :limit => 5 }) do
+ Developer.with_scope(:find => { :limit => 10 }) do
+ merged_option = Developer.instance_eval('current_scoped_methods')[:find]
+ assert_equal({ :limit => 10 }, merged_option)
+ end
+ end
+ end
+
def test_replace_options
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do
@@ -369,8 +378,10 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_merged_scoped_find
poor_jamis = developers(:poor_jamis)
Developer.with_scope(:find => { :conditions => "salary < 100000" }) do
- Developer.with_scope(:find => { :offset => 1 }) do
- assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
+ Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do
+ assert_sql /ORDER BY id asc / do
+ assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
+ end
end
end
end
@@ -400,6 +411,29 @@ class NestedScopingTest < ActiveRecord::TestCase
end
end
+ def test_nested_scoped_create
+ comment = nil
+ Comment.with_scope(:create => { :post_id => 1}) do
+ Comment.with_scope(:create => { :post_id => 2}) do
+ assert_equal({ :post_id => 2 }, Comment.send(:current_scoped_methods)[:create])
+ comment = Comment.create :body => "Hey guys, nested scopes are broken. Please fix!"
+ end
+ end
+ assert_equal 2, comment.post_id
+ end
+
+ def test_nested_exclusive_scope_for_create
+ comment = nil
+ Comment.with_scope(:create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do
+ Comment.with_exclusive_scope(:create => { :post_id => 1 }) do
+ assert_equal({ :post_id => 1 }, Comment.send(:current_scoped_methods)[:create])
+ comment = Comment.create :body => "Hey guys"
+ end
+ end
+ assert_equal 1, comment.post_id
+ assert_equal 'Hey guys', comment.body
+ end
+
def test_merged_scoped_find_on_blank_conditions
[nil, " ", [], {}].each do |blank|
Developer.with_scope(:find => {:conditions => blank}) do
@@ -523,7 +557,6 @@ class HasManyScopingTest< ActiveRecord::TestCase
end
end
-
class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
fixtures :posts, :categories, :categories_posts
@@ -549,7 +582,6 @@ class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
end
end
-
class DefaultScopingTest < ActiveRecord::TestCase
fixtures :developers
@@ -577,7 +609,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
# Scopes added on children should append to parent scope
expected_klass_scope = [{ :create => {}, :find => { :order => 'salary DESC' }}, { :create => {}, :find => {} }]
assert_equal expected_klass_scope, klass.send(:scoped_methods)
-
+
# Parent should still have the original scope
assert_equal scope, DeveloperOrderedBySalary.send(:scoped_methods)
end
@@ -597,7 +629,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_named_scope
- expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
+ expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary }
received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.salary }
assert_equal expected, received
end
@@ -620,7 +652,6 @@ end
=begin
# We disabled the scoping for has_one and belongs_to as we can't think of a proper use case
-
class BelongsToScopingTest< ActiveRecord::TestCase
fixtures :comments, :posts
@@ -640,7 +671,6 @@ class BelongsToScopingTest< ActiveRecord::TestCase
end
-
class HasOneScopingTest< ActiveRecord::TestCase
fixtures :comments, :posts
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 2ec3d40332..16861f21b1 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -93,6 +93,30 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
+ def testing_table_with_only_foo_attribute
+ Person.connection.create_table :testings, :id => false do |t|
+ t.column :foo, :string
+ end
+
+ yield Person.connection
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+ protected :testing_table_with_only_foo_attribute
+
+ def test_create_table_without_id
+ testing_table_with_only_foo_attribute do |connection|
+ assert_equal connection.columns(:testings).size, 1
+ end
+ end
+
+ def test_add_column_with_primary_key_attribute
+ testing_table_with_only_foo_attribute do |connection|
+ assert_nothing_raised { connection.add_column :testings, :id, :primary_key }
+ assert_equal connection.columns(:testings).size, 2
+ end
+ end
+
def test_create_table_adds_id
Person.connection.create_table :testings do |t|
t.column :foo, :string
@@ -111,7 +135,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
Person.connection.execute "insert into testings (foo) values (NULL)"
end
ensure
@@ -219,22 +243,20 @@ if ActiveRecord::Base.connection.supports_migrations?
ActiveRecord::Base.primary_key_prefix_type = nil
end
- uses_mocha('test_create_table_with_force_true_does_not_drop_nonexisting_table') do
- def test_create_table_with_force_true_does_not_drop_nonexisting_table
- if Person.connection.table_exists?(:testings2)
- Person.connection.drop_table :testings2
- end
+ def test_create_table_with_force_true_does_not_drop_nonexisting_table
+ if Person.connection.table_exists?(:testings2)
+ Person.connection.drop_table :testings2
+ end
- # using a copy as we need the drop_table method to
- # continue to work for the ensure block of the test
- temp_conn = Person.connection.dup
- temp_conn.expects(:drop_table).never
- temp_conn.create_table :testings2, :force => true do |t|
- t.column :foo, :string
- end
- ensure
- Person.connection.drop_table :testings2 rescue nil
+ # using a copy as we need the drop_table method to
+ # continue to work for the ensure block of the test
+ temp_conn = Person.connection.dup
+ temp_conn.expects(:drop_table).never
+ temp_conn.create_table :testings2, :force => true do |t|
+ t.column :foo, :string
end
+ ensure
+ Person.connection.drop_table :testings2 rescue nil
end
def test_create_table_with_timestamps_should_create_datetime_columns
@@ -280,7 +302,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
Person.connection.add_column :testings, :bar, :string, :null => false
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
Person.connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
end
ensure
@@ -299,7 +321,7 @@ if ActiveRecord::Base.connection.supports_migrations?
Person.connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter)
assert_nothing_raised {Person.connection.add_column :testings, :bar, :string, :null => false, :default => "default" }
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
unless current_adapter?(:OpenBaseAdapter)
Person.connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
else
@@ -547,7 +569,7 @@ if ActiveRecord::Base.connection.supports_migrations?
else
ActiveRecord::ActiveRecordError
end
- assert_raises(exception) do
+ assert_raise(exception) do
Person.connection.rename_column "hats", "nonexistent", "should_fail"
end
ensure
@@ -797,7 +819,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal "hello world", Reminder.find(:first).content
WeNeedReminders.down
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
end
def test_add_table_with_decimals
@@ -858,7 +880,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
GiveMeBigNumbers.down
- assert_raises(ActiveRecord::StatementInvalid) { BigNumber.find(:first) }
+ assert_raise(ActiveRecord::StatementInvalid) { BigNumber.find(:first) }
end
def test_migrator
@@ -878,7 +900,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal 0, ActiveRecord::Migrator.current_version
Person.reset_column_information
assert !Person.column_methods_hash.include?(:last_name)
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
end
def test_migrator_one_up
@@ -930,11 +952,11 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal(0, ActiveRecord::Migrator.current_version)
end
- if current_adapter?(:PostgreSQLAdapter)
+ if ActiveRecord::Base.connection.supports_ddl_transactions?
def test_migrator_one_up_with_exception_and_rollback
assert !Person.column_methods_hash.include?(:last_name)
- e = assert_raises(StandardError) do
+ e = assert_raise(StandardError) do
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/broken", 100)
end
@@ -947,20 +969,20 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_finds_migrations
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations
- [['1', 'people_have_last_names'],
- ['2', 'we_need_reminders'],
- ['3', 'innocent_jointable']].each_with_index do |pair, i|
- migrations[i].version == pair.first
- migrations[1].name == pair.last
+
+ [[1, 'PeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ assert_equal migrations[i].version, pair.first
+ assert_equal migrations[i].name, pair.last
end
end
def test_finds_pending_migrations
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_2", 1)
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/interleaved/pass_2").pending_migrations
+
assert_equal 1, migrations.size
- migrations[0].version == '3'
- migrations[0].name == 'innocent_jointable'
+ assert_equal migrations[0].version, 3
+ assert_equal migrations[0].name, 'InnocentJointable'
end
def test_only_loads_pending_migrations
@@ -1109,7 +1131,7 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal "hello world", Reminder.find(:first).content
WeNeedReminders.down
- assert_raises(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
+ assert_raise(ActiveRecord::StatementInvalid) { Reminder.find(:first) }
ensure
ActiveRecord::Base.table_name_prefix = ''
ActiveRecord::Base.table_name_suffix = ''
@@ -1139,13 +1161,13 @@ if ActiveRecord::Base.connection.supports_migrations?
end
def test_migrator_with_duplicates
- assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
+ assert_raise(ActiveRecord::DuplicateMigrationVersionError) do
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate", nil)
end
end
def test_migrator_with_duplicate_names
- assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
+ assert_raise(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/duplicate_names", nil)
end
end
@@ -1161,7 +1183,7 @@ if ActiveRecord::Base.connection.supports_migrations?
# table name is 29 chars, the standard sequence name will
# be 33 chars and fail
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
begin
Person.connection.create_table :table_with_name_thats_just_ok do |t|
t.column :foo, :string, :null => false
@@ -1188,7 +1210,7 @@ if ActiveRecord::Base.connection.supports_migrations?
end
# confirm the custom sequence got dropped
- assert_raises(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::StatementInvalid) do
Person.connection.execute("select suitably_short_seq.nextval from dual")
end
end
@@ -1203,277 +1225,271 @@ if ActiveRecord::Base.connection.supports_migrations?
end
- uses_mocha 'Sexy migration tests' do
- class SexyMigrationsTest < ActiveRecord::TestCase
- def test_references_column_type_adds_id
- with_new_table do |t|
- t.expects(:column).with('customer_id', :integer, {})
- t.references :customer
- end
+ class SexyMigrationsTest < ActiveRecord::TestCase
+ def test_references_column_type_adds_id
+ with_new_table do |t|
+ t.expects(:column).with('customer_id', :integer, {})
+ t.references :customer
end
+ end
- def test_references_column_type_with_polymorphic_adds_type
- with_new_table do |t|
- t.expects(:column).with('taggable_type', :string, {})
- t.expects(:column).with('taggable_id', :integer, {})
- t.references :taggable, :polymorphic => true
- end
+ def test_references_column_type_with_polymorphic_adds_type
+ with_new_table do |t|
+ t.expects(:column).with('taggable_type', :string, {})
+ t.expects(:column).with('taggable_id', :integer, {})
+ t.references :taggable, :polymorphic => true
end
+ end
- def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag
- with_new_table do |t|
- t.expects(:column).with('taggable_type', :string, {:null => false})
- t.expects(:column).with('taggable_id', :integer, {:null => false})
- t.references :taggable, :polymorphic => true, :null => false
- end
+ def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag
+ with_new_table do |t|
+ t.expects(:column).with('taggable_type', :string, {:null => false})
+ t.expects(:column).with('taggable_id', :integer, {:null => false})
+ t.references :taggable, :polymorphic => true, :null => false
end
+ end
- def test_belongs_to_works_like_references
- with_new_table do |t|
- t.expects(:column).with('customer_id', :integer, {})
- t.belongs_to :customer
- end
+ def test_belongs_to_works_like_references
+ with_new_table do |t|
+ t.expects(:column).with('customer_id', :integer, {})
+ t.belongs_to :customer
end
+ end
- def test_timestamps_creates_updated_at_and_created_at
- with_new_table do |t|
- t.expects(:column).with(:created_at, :datetime, kind_of(Hash))
- t.expects(:column).with(:updated_at, :datetime, kind_of(Hash))
- t.timestamps
- end
+ def test_timestamps_creates_updated_at_and_created_at
+ with_new_table do |t|
+ t.expects(:column).with(:created_at, :datetime, kind_of(Hash))
+ t.expects(:column).with(:updated_at, :datetime, kind_of(Hash))
+ t.timestamps
end
+ end
- def test_integer_creates_integer_column
- with_new_table do |t|
- t.expects(:column).with(:foo, 'integer', {})
- t.expects(:column).with(:bar, 'integer', {})
- t.integer :foo, :bar
- end
+ def test_integer_creates_integer_column
+ with_new_table do |t|
+ t.expects(:column).with(:foo, 'integer', {})
+ t.expects(:column).with(:bar, 'integer', {})
+ t.integer :foo, :bar
end
+ end
- def test_string_creates_string_column
- with_new_table do |t|
- t.expects(:column).with(:foo, 'string', {})
- t.expects(:column).with(:bar, 'string', {})
- t.string :foo, :bar
- end
+ def test_string_creates_string_column
+ with_new_table do |t|
+ t.expects(:column).with(:foo, 'string', {})
+ t.expects(:column).with(:bar, 'string', {})
+ t.string :foo, :bar
end
+ end
- protected
- def with_new_table
- Person.connection.create_table :delete_me, :force => true do |t|
- yield t
- end
- ensure
- Person.connection.drop_table :delete_me rescue nil
+ protected
+ def with_new_table
+ Person.connection.create_table :delete_me, :force => true do |t|
+ yield t
end
+ ensure
+ Person.connection.drop_table :delete_me rescue nil
+ end
- end # SexyMigrationsTest
- end # uses_mocha
+ end # SexyMigrationsTest
- uses_mocha 'ChangeTable migration tests' do
- class ChangeTableMigrationsTest < ActiveRecord::TestCase
- def setup
- @connection = Person.connection
- @connection.create_table :delete_me, :force => true do |t|
- end
+ class ChangeTableMigrationsTest < ActiveRecord::TestCase
+ def setup
+ @connection = Person.connection
+ @connection.create_table :delete_me, :force => true do |t|
end
+ end
- def teardown
- Person.connection.drop_table :delete_me rescue nil
- end
+ def teardown
+ Person.connection.drop_table :delete_me rescue nil
+ end
- def test_references_column_type_adds_id
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
- t.references :customer
- end
+ def test_references_column_type_adds_id
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
+ t.references :customer
end
+ end
- def test_remove_references_column_type_removes_id
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, 'customer_id')
- t.remove_references :customer
- end
+ def test_remove_references_column_type_removes_id
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'customer_id')
+ t.remove_references :customer
end
+ end
- def test_add_belongs_to_works_like_add_references
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
- t.belongs_to :customer
- end
+ def test_add_belongs_to_works_like_add_references
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'customer_id', :integer, {})
+ t.belongs_to :customer
end
+ end
- def test_remove_belongs_to_works_like_remove_references
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, 'customer_id')
- t.remove_belongs_to :customer
- end
+ def test_remove_belongs_to_works_like_remove_references
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'customer_id')
+ t.remove_belongs_to :customer
end
+ end
- def test_references_column_type_with_polymorphic_adds_type
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {})
- @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {})
- t.references :taggable, :polymorphic => true
- end
+ def test_references_column_type_with_polymorphic_adds_type
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {})
+ @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {})
+ t.references :taggable, :polymorphic => true
end
+ end
- def test_remove_references_column_type_with_polymorphic_removes_type
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
- @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
- t.remove_references :taggable, :polymorphic => true
- end
+ def test_remove_references_column_type_with_polymorphic_removes_type
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
+ t.remove_references :taggable, :polymorphic => true
end
+ end
- def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false})
- @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false})
- t.references :taggable, :polymorphic => true, :null => false
- end
+ def test_references_column_type_with_polymorphic_and_options_null_is_false_adds_table_flag
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, 'taggable_type', :string, {:null => false})
+ @connection.expects(:add_column).with(:delete_me, 'taggable_id', :integer, {:null => false})
+ t.references :taggable, :polymorphic => true, :null => false
end
+ end
- def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
- @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
- t.remove_references :taggable, :polymorphic => true, :null => false
- end
+ def test_remove_references_column_type_with_polymorphic_and_options_null_is_false_removes_table_flag
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_type')
+ @connection.expects(:remove_column).with(:delete_me, 'taggable_id')
+ t.remove_references :taggable, :polymorphic => true, :null => false
end
+ end
- def test_timestamps_creates_updated_at_and_created_at
- with_change_table do |t|
- @connection.expects(:add_timestamps).with(:delete_me)
- t.timestamps
- end
+ def test_timestamps_creates_updated_at_and_created_at
+ with_change_table do |t|
+ @connection.expects(:add_timestamps).with(:delete_me)
+ t.timestamps
end
+ end
- def test_remove_timestamps_creates_updated_at_and_created_at
- with_change_table do |t|
- @connection.expects(:remove_timestamps).with(:delete_me)
- t.remove_timestamps
- end
+ def test_remove_timestamps_creates_updated_at_and_created_at
+ with_change_table do |t|
+ @connection.expects(:remove_timestamps).with(:delete_me)
+ t.remove_timestamps
end
+ end
- def string_column
- if current_adapter?(:PostgreSQLAdapter)
- "character varying(255)"
- else
- 'varchar(255)'
- end
+ def string_column
+ if current_adapter?(:PostgreSQLAdapter)
+ "character varying(255)"
+ else
+ 'varchar(255)'
end
+ end
- def integer_column
- if current_adapter?(:MysqlAdapter)
- 'int(11)'
- else
- 'integer'
- end
+ def integer_column
+ if current_adapter?(:MysqlAdapter)
+ 'int(11)'
+ else
+ 'integer'
end
+ end
- def test_integer_creates_integer_column
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {})
- @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {})
- t.integer :foo, :bar
- end
+ def test_integer_creates_integer_column
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :foo, integer_column, {})
+ @connection.expects(:add_column).with(:delete_me, :bar, integer_column, {})
+ t.integer :foo, :bar
end
+ end
- def test_string_creates_string_column
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, :foo, string_column, {})
- @connection.expects(:add_column).with(:delete_me, :bar, string_column, {})
- t.string :foo, :bar
- end
+ def test_string_creates_string_column
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :foo, string_column, {})
+ @connection.expects(:add_column).with(:delete_me, :bar, string_column, {})
+ t.string :foo, :bar
end
+ end
- def test_column_creates_column
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, :bar, :integer, {})
- t.column :bar, :integer
- end
+ def test_column_creates_column
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :bar, :integer, {})
+ t.column :bar, :integer
end
+ end
- def test_column_creates_column_with_options
- with_change_table do |t|
- @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false})
- t.column :bar, :integer, :null => false
- end
+ def test_column_creates_column_with_options
+ with_change_table do |t|
+ @connection.expects(:add_column).with(:delete_me, :bar, :integer, {:null => false})
+ t.column :bar, :integer, :null => false
end
+ end
- def test_index_creates_index
- with_change_table do |t|
- @connection.expects(:add_index).with(:delete_me, :bar, {})
- t.index :bar
- end
+ def test_index_creates_index
+ with_change_table do |t|
+ @connection.expects(:add_index).with(:delete_me, :bar, {})
+ t.index :bar
end
+ end
- def test_index_creates_index_with_options
- with_change_table do |t|
- @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true})
- t.index :bar, :unique => true
- end
+ def test_index_creates_index_with_options
+ with_change_table do |t|
+ @connection.expects(:add_index).with(:delete_me, :bar, {:unique => true})
+ t.index :bar, :unique => true
end
+ end
- def test_change_changes_column
- with_change_table do |t|
- @connection.expects(:change_column).with(:delete_me, :bar, :string, {})
- t.change :bar, :string
- end
+ def test_change_changes_column
+ with_change_table do |t|
+ @connection.expects(:change_column).with(:delete_me, :bar, :string, {})
+ t.change :bar, :string
end
+ end
- def test_change_changes_column_with_options
- with_change_table do |t|
- @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true})
- t.change :bar, :string, :null => true
- end
+ def test_change_changes_column_with_options
+ with_change_table do |t|
+ @connection.expects(:change_column).with(:delete_me, :bar, :string, {:null => true})
+ t.change :bar, :string, :null => true
end
+ end
- def test_change_default_changes_column
- with_change_table do |t|
- @connection.expects(:change_column_default).with(:delete_me, :bar, :string)
- t.change_default :bar, :string
- end
+ def test_change_default_changes_column
+ with_change_table do |t|
+ @connection.expects(:change_column_default).with(:delete_me, :bar, :string)
+ t.change_default :bar, :string
end
+ end
- def test_remove_drops_single_column
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, [:bar])
- t.remove :bar
- end
+ def test_remove_drops_single_column
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, [:bar])
+ t.remove :bar
end
+ end
- def test_remove_drops_multiple_columns
- with_change_table do |t|
- @connection.expects(:remove_column).with(:delete_me, [:bar, :baz])
- t.remove :bar, :baz
- end
+ def test_remove_drops_multiple_columns
+ with_change_table do |t|
+ @connection.expects(:remove_column).with(:delete_me, [:bar, :baz])
+ t.remove :bar, :baz
end
+ end
- def test_remove_index_removes_index_with_options
- with_change_table do |t|
- @connection.expects(:remove_index).with(:delete_me, {:unique => true})
- t.remove_index :unique => true
- end
+ def test_remove_index_removes_index_with_options
+ with_change_table do |t|
+ @connection.expects(:remove_index).with(:delete_me, {:unique => true})
+ t.remove_index :unique => true
end
+ end
- def test_rename_renames_column
- with_change_table do |t|
- @connection.expects(:rename_column).with(:delete_me, :bar, :baz)
- t.rename :bar, :baz
- end
+ def test_rename_renames_column
+ with_change_table do |t|
+ @connection.expects(:rename_column).with(:delete_me, :bar, :baz)
+ t.rename :bar, :baz
end
+ end
- protected
- def with_change_table
- Person.connection.change_table :delete_me do |t|
- yield t
- end
+ protected
+ def with_change_table
+ Person.connection.change_table :delete_me do |t|
+ yield t
end
-
- end # ChangeTable test
- end # uses_mocha
-
+ end
+ end
end
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index e1e27fa130..ae6a54a5bd 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -15,7 +15,7 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal Topic.find(:all), Topic.base
assert_equal Topic.find(:all), Topic.base.to_a
assert_equal Topic.find(:first), Topic.base.first
- assert_equal Topic.find(:all), Topic.base.each { |i| i }
+ assert_equal Topic.find(:all), Topic.base.map { |i| i }
end
def test_found_items_are_cached
@@ -99,6 +99,12 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal topics_written_before_the_second, Topic.written_before(topics(:second).written_on)
end
+ def test_procedural_scopes_returning_nil
+ all_topics = Topic.find(:all)
+
+ assert_equal all_topics, Topic.written_before(nil)
+ end
+
def test_scopes_with_joins
address = author_addresses(:david_address)
posts_with_authors_at_address = Post.find(
@@ -142,6 +148,15 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal authors(:david).comments & Comment.containing_the_letter_e, authors(:david).comments.containing_the_letter_e
end
+ def test_named_scopes_honor_current_scopes_from_when_defined
+ assert !Post.ranked_by_comments.limit(5).empty?
+ assert !authors(:david).posts.ranked_by_comments.limit(5).empty?
+ assert_not_equal Post.ranked_by_comments.limit(5), authors(:david).posts.ranked_by_comments.limit(5)
+ assert_not_equal Post.top(5), authors(:david).posts.top(5)
+ assert_equal authors(:david).posts.ranked_by_comments.limit(5), authors(:david).posts.top(5)
+ assert_equal Post.ranked_by_comments.limit(5), Post.top(5)
+ end
+
def test_active_records_have_scope_named__all__
assert !Topic.find(:all).empty?
@@ -238,7 +253,7 @@ class NamedScopeTest < ActiveRecord::TestCase
topic = Topic.approved.create!({})
assert topic.approved
end
-
+
def test_should_build_with_proxy_options_chained
topic = Topic.approved.by_lifo.build({})
assert topic.approved
@@ -278,15 +293,21 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal post.comments.size, Post.scoped(:joins => join).scoped(:joins => join, :conditions => "posts.id = #{post.id}").size
end
- def test_chanining_should_use_latest_conditions_when_creating
- post1 = Topic.rejected.approved.new
- assert post1.approved?
+ def test_chaining_should_use_latest_conditions_when_creating
+ post = Topic.rejected.new
+ assert !post.approved?
+
+ post = Topic.rejected.approved.new
+ assert post.approved?
- post2 = Topic.approved.rejected.new
- assert ! post2.approved?
+ post = Topic.approved.rejected.new
+ assert !post.approved?
+
+ post = Topic.approved.rejected.approved.new
+ assert post.approved?
end
- def test_chanining_should_use_latest_conditions_when_searching
+ def test_chaining_should_use_latest_conditions_when_searching
# Normal hash conditions
assert_equal Topic.all(:conditions => {:approved => true}), Topic.rejected.approved.all
assert_equal Topic.all(:conditions => {:approved => false}), Topic.approved.rejected.all
@@ -297,6 +318,24 @@ class NamedScopeTest < ActiveRecord::TestCase
# Nested hash conditions with different keys
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).all.uniq
end
+
+ def test_methods_invoked_within_scopes_should_respect_scope
+ assert_equal [], Topic.approved.by_rejected_ids.proxy_options[:conditions][:id]
+ end
+
+ def test_named_scopes_batch_finders
+ assert_equal 3, Topic.approved.count
+
+ assert_queries(4) do
+ Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
+ end
+
+ assert_queries(2) do
+ Topic.approved.find_in_batches(:batch_size => 2) do |group|
+ group.each {|t| assert t.approved? }
+ end
+ end
+ end
end
class DynamicScopeMatchTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
new file mode 100644
index 0000000000..cd6277c24b
--- /dev/null
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -0,0 +1,509 @@
+require "cases/helper"
+require "models/pirate"
+require "models/ship"
+require "models/bird"
+require "models/parrot"
+require "models/treasure"
+
+module AssertRaiseWithMessage
+ def assert_raise_with_message(expected_exception, expected_message)
+ begin
+ error_raised = false
+ yield
+ rescue expected_exception => error
+ error_raised = true
+ actual_message = error.message
+ end
+ assert error_raised
+ assert_equal expected_message, actual_message
+ end
+end
+
+class TestNestedAttributesInGeneral < ActiveRecord::TestCase
+ include AssertRaiseWithMessage
+
+ def teardown
+ Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ end
+
+ def test_base_should_have_an_empty_reject_new_nested_attributes_procs
+ assert_equal Hash.new, ActiveRecord::Base.reject_new_nested_attributes_procs
+ end
+
+ def test_should_add_a_proc_to_reject_new_nested_attributes_procs
+ [:parrots, :birds].each do |name|
+ assert_instance_of Proc, Pirate.reject_new_nested_attributes_procs[name]
+ end
+ end
+
+ def test_should_raise_an_ArgumentError_for_non_existing_associations
+ assert_raise_with_message ArgumentError, "No association found for name `honesty'. Has it been defined yet?" do
+ Pirate.accepts_nested_attributes_for :honesty
+ end
+ end
+
+ def test_should_disable_allow_destroy_by_default
+ Pirate.accepts_nested_attributes_for :ship
+
+ pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.create_ship(:name => 'Nights Dirty Lightning')
+
+ assert_no_difference('Ship.count') do
+ pirate.update_attributes(:ship_attributes => { '_delete' => true })
+ end
+ end
+
+ def test_a_model_should_respond_to_underscore_delete_and_return_if_it_is_marked_for_destruction
+ ship = Ship.create!(:name => 'Nights Dirty Lightning')
+ assert !ship._delete
+ ship.mark_for_destruction
+ assert ship._delete
+ end
+end
+
+class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
+ def setup
+ @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
+ end
+
+ def test_should_define_an_attribute_writer_method_for_the_association
+ assert_respond_to @pirate, :ship_attributes=
+ end
+
+ def test_should_build_a_new_record_if_there_is_no_id
+ @ship.destroy
+ @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
+
+ assert @pirate.ship.new_record?
+ assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ end
+
+ def test_should_not_build_a_new_record_if_there_is_no_id_and_delete_is_truthy
+ @ship.destroy
+ @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_delete => '1' }
+
+ assert_nil @pirate.ship
+ end
+
+ def test_should_not_build_a_new_record_if_a_reject_if_proc_returns_false
+ @ship.destroy
+ @pirate.reload.ship_attributes = {}
+
+ assert_nil @pirate.ship
+ end
+
+ def test_should_replace_an_existing_record_if_there_is_no_id
+ @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger' }
+
+ assert @pirate.ship.new_record?
+ assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ assert_equal 'Nights Dirty Lightning', @ship.name
+ end
+
+ def test_should_not_replace_an_existing_record_if_there_is_no_id_and_delete_is_truthy
+ @pirate.reload.ship_attributes = { :name => 'Davy Jones Gold Dagger', :_delete => '1' }
+
+ assert_equal @ship, @pirate.ship
+ assert_equal 'Nights Dirty Lightning', @pirate.ship.name
+ end
+
+ def test_should_modify_an_existing_record_if_there_is_a_matching_id
+ @pirate.reload.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
+
+ assert_equal @ship, @pirate.ship
+ assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ end
+
+ def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
+ @pirate.reload.ship_attributes = { 'id' => @ship.id, 'name' => 'Davy Jones Gold Dagger' }
+
+ assert_equal @ship, @pirate.ship
+ assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ end
+
+ def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
+ @ship.stubs(:id).returns('ABC1X')
+ @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' }
+
+ assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ end
+
+ def test_should_delete_an_existing_record_if_there_is_a_matching_id_and_delete_is_truthy
+ @pirate.ship.destroy
+ [1, '1', true, 'true'].each do |truth|
+ @pirate.reload.create_ship(:name => 'Mister Pablo')
+ assert_difference('Ship.count', -1) do
+ @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_delete => truth })
+ end
+ end
+ end
+
+ def test_should_not_delete_an_existing_record_if_delete_is_not_truthy
+ [nil, '0', 0, 'false', false].each do |not_truth|
+ assert_no_difference('Ship.count') do
+ @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_delete => not_truth })
+ end
+ end
+ end
+
+ def test_should_not_delete_an_existing_record_if_allow_destroy_is_false
+ Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
+
+ assert_no_difference('Ship.count') do
+ @pirate.update_attribute(:ship_attributes, { :id => @pirate.ship.id, :_delete => '1' })
+ end
+
+ Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ end
+
+ def test_should_also_work_with_a_HashWithIndifferentAccess
+ @pirate.ship_attributes = HashWithIndifferentAccess.new(:id => @ship.id, :name => 'Davy Jones Gold Dagger')
+
+ assert !@pirate.ship.new_record?
+ assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
+ end
+
+ def test_should_work_with_update_attributes_as_well
+ @pirate.update_attributes({ :catchphrase => 'Arr', :ship_attributes => { :id => @ship.id, :name => 'Mister Pablo' } })
+ @pirate.reload
+
+ assert_equal 'Arr', @pirate.catchphrase
+ assert_equal 'Mister Pablo', @pirate.ship.name
+ end
+
+ def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
+ assert_no_difference('Ship.count') do
+ @pirate.attributes = { :ship_attributes => { :id => @ship.id, :_delete => '1' } }
+ end
+ assert_difference('Ship.count', -1) do
+ @pirate.save
+ end
+ end
+
+ def test_should_automatically_enable_autosave_on_the_association
+ assert Pirate.reflect_on_association(:ship).options[:autosave]
+ end
+end
+
+class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
+ def setup
+ @ship = Ship.new(:name => 'Nights Dirty Lightning')
+ @pirate = @ship.build_pirate(:catchphrase => 'Aye')
+ @ship.save!
+ end
+
+ def test_should_define_an_attribute_writer_method_for_the_association
+ assert_respond_to @ship, :pirate_attributes=
+ end
+
+ def test_should_build_a_new_record_if_there_is_no_id
+ @pirate.destroy
+ @ship.reload.pirate_attributes = { :catchphrase => 'Arr' }
+
+ assert @ship.pirate.new_record?
+ assert_equal 'Arr', @ship.pirate.catchphrase
+ end
+
+ def test_should_not_build_a_new_record_if_there_is_no_id_and_delete_is_truthy
+ @pirate.destroy
+ @ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_delete => '1' }
+
+ assert_nil @ship.pirate
+ end
+
+ def test_should_not_build_a_new_record_if_a_reject_if_proc_returns_false
+ @pirate.destroy
+ @ship.reload.pirate_attributes = {}
+
+ assert_nil @ship.pirate
+ end
+
+ def test_should_replace_an_existing_record_if_there_is_no_id
+ @ship.reload.pirate_attributes = { :catchphrase => 'Arr' }
+
+ assert @ship.pirate.new_record?
+ assert_equal 'Arr', @ship.pirate.catchphrase
+ assert_equal 'Aye', @pirate.catchphrase
+ end
+
+ def test_should_not_replace_an_existing_record_if_there_is_no_id_and_delete_is_truthy
+ @ship.reload.pirate_attributes = { :catchphrase => 'Arr', :_delete => '1' }
+
+ assert_equal @pirate, @ship.pirate
+ assert_equal 'Aye', @ship.pirate.catchphrase
+ end
+
+ def test_should_modify_an_existing_record_if_there_is_a_matching_id
+ @ship.reload.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
+
+ assert_equal @pirate, @ship.pirate
+ assert_equal 'Arr', @ship.pirate.catchphrase
+ end
+
+ def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
+ @ship.reload.pirate_attributes = { 'id' => @pirate.id, 'catchphrase' => 'Arr' }
+
+ assert_equal @pirate, @ship.pirate
+ assert_equal 'Arr', @ship.pirate.catchphrase
+ end
+
+ def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id
+ @pirate.stubs(:id).returns('ABC1X')
+ @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' }
+
+ assert_equal 'Arr', @ship.pirate.catchphrase
+ end
+
+ def test_should_delete_an_existing_record_if_there_is_a_matching_id_and_delete_is_truthy
+ @ship.pirate.destroy
+ [1, '1', true, 'true'].each do |truth|
+ @ship.reload.create_pirate(:catchphrase => 'Arr')
+ assert_difference('Pirate.count', -1) do
+ @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_delete => truth })
+ end
+ end
+ end
+
+ def test_should_not_delete_an_existing_record_if_delete_is_not_truthy
+ [nil, '0', 0, 'false', false].each do |not_truth|
+ assert_no_difference('Pirate.count') do
+ @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_delete => not_truth })
+ end
+ end
+ end
+
+ def test_should_not_delete_an_existing_record_if_allow_destroy_is_false
+ Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
+
+ assert_no_difference('Pirate.count') do
+ @ship.update_attribute(:pirate_attributes, { :id => @ship.pirate.id, :_delete => '1' })
+ end
+
+ Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ end
+
+ def test_should_work_with_update_attributes_as_well
+ @ship.update_attributes({ :name => 'Mister Pablo', :pirate_attributes => { :catchphrase => 'Arr' } })
+ @ship.reload
+
+ assert_equal 'Mister Pablo', @ship.name
+ assert_equal 'Arr', @ship.pirate.catchphrase
+ end
+
+ def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
+ assert_no_difference('Pirate.count') do
+ @ship.attributes = { :pirate_attributes => { :id => @ship.pirate.id, '_delete' => true } }
+ end
+ assert_difference('Pirate.count', -1) { @ship.save }
+ end
+
+ def test_should_automatically_enable_autosave_on_the_association
+ assert Ship.reflect_on_association(:pirate).options[:autosave]
+ end
+end
+
+module NestedAttributesOnACollectionAssociationTests
+ include AssertRaiseWithMessage
+
+ def test_should_define_an_attribute_writer_method_for_the_association
+ assert_respond_to @pirate, association_setter
+ end
+
+ def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
+ @alternate_params[association_getter].stringify_keys!
+ @pirate.update_attributes @alternate_params
+ assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
+ end
+
+ def test_should_take_an_array_and_assign_the_attributes_to_the_associated_models
+ @pirate.send(association_setter, @alternate_params[association_getter].values)
+ @pirate.save
+ assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
+ end
+
+ def test_should_also_work_with_a_HashWithIndifferentAccess
+ @pirate.send(association_setter, HashWithIndifferentAccess.new('foo' => HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley')))
+ @pirate.save
+ assert_equal 'Grace OMalley', @child_1.reload.name
+ end
+
+ def test_should_take_a_hash_and_assign_the_attributes_to_the_associated_models
+ @pirate.attributes = @alternate_params
+ assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
+ assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
+ end
+
+ def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
+ @child_1.stubs(:id).returns('ABC1X')
+ @child_2.stubs(:id).returns('ABC2X')
+
+ @pirate.attributes = {
+ association_getter => [
+ { :id => @child_1.id, :name => 'Grace OMalley' },
+ { :id => @child_2.id, :name => 'Privateers Greed' }
+ ]
+ }
+
+ assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name]
+ end
+
+ def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing
+ @pirate.send(@association_name).destroy_all
+ @pirate.reload.attributes = {
+ association_getter => { 'foo' => { :name => 'Grace OMalley' }, 'bar' => { :name => 'Privateers Greed' }}
+ }
+
+ assert @pirate.send(@association_name).first.new_record?
+ assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
+
+ assert @pirate.send(@association_name).last.new_record?
+ assert_equal 'Privateers Greed', @pirate.send(@association_name).last.name
+ end
+
+ def test_should_not_assign_delete_key_to_a_record
+ assert_nothing_raised ActiveRecord::UnknownAttributeError do
+ @pirate.send(association_setter, { 'foo' => { '_delete' => '0' }})
+ end
+ end
+
+ def test_should_ignore_new_associated_records_with_truthy_delete_attribute
+ @pirate.send(@association_name).destroy_all
+ @pirate.reload.attributes = {
+ association_getter => {
+ 'foo' => { :name => 'Grace OMalley' },
+ 'bar' => { :name => 'Privateers Greed', '_delete' => '1' }
+ }
+ }
+
+ assert_equal 1, @pirate.send(@association_name).length
+ assert_equal 'Grace OMalley', @pirate.send(@association_name).first.name
+ end
+
+ def test_should_ignore_new_associated_records_if_a_reject_if_proc_returns_false
+ @alternate_params[association_getter]['baz'] = {}
+ assert_no_difference("@pirate.send(@association_name).length") do
+ @pirate.attributes = @alternate_params
+ end
+ end
+
+ def test_should_sort_the_hash_by_the_keys_before_building_new_associated_models
+ attributes = ActiveSupport::OrderedHash.new
+ attributes['123726353'] = { :name => 'Grace OMalley' }
+ attributes['2'] = { :name => 'Privateers Greed' } # 2 is lower then 123726353
+ @pirate.send(association_setter, attributes)
+
+ assert_equal ['Posideons Killer', 'Killer bandita Dionne', 'Privateers Greed', 'Grace OMalley'].to_set, @pirate.send(@association_name).map(&:name).to_set
+ end
+
+ def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed
+ assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) }
+ assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, ActiveSupport::OrderedHash.new) }
+
+ assert_raise_with_message ArgumentError, 'Hash or Array expected, got String ("foo")' do
+ @pirate.send(association_setter, "foo")
+ end
+ end
+
+ def test_should_work_with_update_attributes_as_well
+ @pirate.update_attributes(:catchphrase => 'Arr',
+ association_getter => { 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' }})
+
+ assert_equal 'Grace OMalley', @child_1.reload.name
+ end
+
+ def test_should_update_existing_records_and_add_new_ones_that_have_no_id
+ @alternate_params[association_getter]['baz'] = { :name => 'Buccaneers Servant' }
+ assert_difference('@pirate.send(@association_name).count', +1) do
+ @pirate.update_attributes @alternate_params
+ end
+ assert_equal ['Grace OMalley', 'Privateers Greed', 'Buccaneers Servant'].to_set, @pirate.reload.send(@association_name).map(&:name).to_set
+ end
+
+ def test_should_be_possible_to_destroy_a_record
+ ['1', 1, 'true', true].each do |true_variable|
+ record = @pirate.reload.send(@association_name).create!(:name => 'Grace OMalley')
+ @pirate.send(association_setter,
+ @alternate_params[association_getter].merge('baz' => { :id => record.id, '_delete' => true_variable })
+ )
+
+ assert_difference('@pirate.send(@association_name).count', -1) do
+ @pirate.save
+ end
+ end
+ end
+
+ def test_should_not_destroy_the_associated_model_with_a_non_truthy_argument
+ [nil, '', '0', 0, 'false', false].each do |false_variable|
+ @alternate_params[association_getter]['foo']['_delete'] = false_variable
+ assert_no_difference('@pirate.send(@association_name).count') do
+ @pirate.update_attributes(@alternate_params)
+ end
+ end
+ end
+
+ def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
+ assert_no_difference('@pirate.send(@association_name).count') do
+ @pirate.send(association_setter, @alternate_params[association_getter].merge('baz' => { :id => @child_1.id, '_delete' => true }))
+ end
+ assert_difference('@pirate.send(@association_name).count', -1) { @pirate.save }
+ end
+
+ def test_should_automatically_enable_autosave_on_the_association
+ assert Pirate.reflect_on_association(@association_name).options[:autosave]
+ end
+
+ private
+
+ def association_setter
+ @association_setter ||= "#{@association_name}_attributes=".to_sym
+ end
+
+ def association_getter
+ @association_getter ||= "#{@association_name}_attributes".to_sym
+ end
+end
+
+class TestNestedAttributesOnAHasManyAssociation < ActiveRecord::TestCase
+ def setup
+ @association_type = :has_many
+ @association_name = :birds
+
+ @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @pirate.birds.create!(:name => 'Posideons Killer')
+ @pirate.birds.create!(:name => 'Killer bandita Dionne')
+
+ @child_1, @child_2 = @pirate.birds
+
+ @alternate_params = {
+ :birds_attributes => {
+ 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' },
+ 'bar' => { :id => @child_2.id, :name => 'Privateers Greed' }
+ }
+ }
+ end
+
+ include NestedAttributesOnACollectionAssociationTests
+end
+
+class TestNestedAttributesOnAHasAndBelongsToManyAssociation < ActiveRecord::TestCase
+ def setup
+ @association_type = :has_and_belongs_to_many
+ @association_name = :parrots
+
+ @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ @pirate.parrots.create!(:name => 'Posideons Killer')
+ @pirate.parrots.create!(:name => 'Killer bandita Dionne')
+
+ @child_1, @child_2 = @pirate.parrots
+
+ @alternate_params = {
+ :parrots_attributes => {
+ 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' },
+ 'bar' => { :id => @child_2.id, :name => 'Privateers Greed' }
+ }
+ }
+ end
+
+ include NestedAttributesOnACollectionAssociationTests
+end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 171d0e6dae..f90a66d1dc 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -55,8 +55,6 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
-uses_mocha 'QueryCacheExpiryTest' do
-
class QueryCacheExpiryTest < ActiveRecord::TestCase
fixtures :tasks, :posts, :categories, :categories_posts
@@ -123,5 +121,3 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
end
end
end
-
-end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index e0ed3e5886..db64bbb806 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -4,6 +4,7 @@ require 'models/customer'
require 'models/company'
require 'models/company_in_module'
require 'models/subscriber'
+require 'models/pirate'
class ReflectionTest < ActiveRecord::TestCase
fixtures :topics, :customers, :companies, :subscribers
@@ -91,6 +92,15 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal Money, Customer.reflect_on_aggregation(:balance).klass
end
+ def test_reflect_on_all_autosave_associations
+ expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] }
+ received = Pirate.reflect_on_all_autosave_associations
+
+ assert !received.empty?
+ assert_not_equal Pirate.reflect_on_all_associations.length, received.length
+ assert_equal expected, received
+ end
+
def test_has_many_reflection
reflection_for_clients = ActiveRecord::Reflection::AssociationReflection.new(:has_many, :clients, { :order => "id", :dependent => :destroy }, Firm)
@@ -160,9 +170,9 @@ class ReflectionTest < ActiveRecord::TestCase
def test_reflection_of_all_associations
# FIXME these assertions bust a lot
- assert_equal 26, Firm.reflect_on_all_associations.size
- assert_equal 20, Firm.reflect_on_all_associations(:has_many).size
- assert_equal 6, Firm.reflect_on_all_associations(:has_one).size
+ assert_equal 28, Firm.reflect_on_all_associations.size
+ assert_equal 21, Firm.reflect_on_all_associations(:has_many).size
+ assert_equal 7, 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/schema_test_postgresql.rb b/activerecord/test/cases/schema_test_postgresql.rb
index 336a38765c..2d36bd0b22 100644
--- a/activerecord/test/cases/schema_test_postgresql.rb
+++ b/activerecord/test/cases/schema_test_postgresql.rb
@@ -18,9 +18,22 @@ class SchemaTest < ActiveRecord::TestCase
'moment timestamp without time zone default now()'
]
+ class Thing1 < ActiveRecord::Base
+ set_table_name "test_schema.things"
+ end
+
+ class Thing2 < ActiveRecord::Base
+ set_table_name "test_schema2.things"
+ end
+
+ class Thing3 < ActiveRecord::Base
+ set_table_name 'test_schema."things.table"'
+ end
+
def setup
@connection = ActiveRecord::Base.connection
@connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
+ @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})"
@connection.execute "CREATE SCHEMA #{SCHEMA2_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
@connection.execute "CREATE INDEX #{INDEX_A_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_A_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_A_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_A_COLUMN});"
@@ -47,6 +60,37 @@ class SchemaTest < ActiveRecord::TestCase
end
end
+
+ def test_proper_encoding_of_table_name
+ assert_equal '"table_name"', @connection.quote_table_name('table_name')
+ assert_equal '"table.name"', @connection.quote_table_name('"table.name"')
+ assert_equal '"schema_name"."table_name"', @connection.quote_table_name('schema_name.table_name')
+ assert_equal '"schema_name"."table.name"', @connection.quote_table_name('schema_name."table.name"')
+ assert_equal '"schema.name"."table_name"', @connection.quote_table_name('"schema.name".table_name')
+ assert_equal '"schema.name"."table.name"', @connection.quote_table_name('"schema.name"."table.name"')
+ end
+
+ def test_classes_with_qualified_schema_name
+ assert_equal 0, Thing1.count
+ assert_equal 0, Thing2.count
+ assert_equal 0, Thing3.count
+
+ Thing1.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ assert_equal 1, Thing1.count
+ assert_equal 0, Thing2.count
+ assert_equal 0, Thing3.count
+
+ Thing2.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ assert_equal 1, Thing1.count
+ assert_equal 1, Thing2.count
+ assert_equal 0, Thing3.count
+
+ Thing3.create(:id => 1, :name => "thing1", :email => "thing1@localhost", :moment => Time.now)
+ assert_equal 1, Thing1.count
+ assert_equal 1, Thing2.count
+ assert_equal 1, Thing3.count
+ end
+
def test_raise_on_unquoted_schema_name
assert_raise(ActiveRecord::StatementInvalid) do
with_schema_search_path '$user,public'
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 4a07a8bb1d..f6533b5396 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -214,7 +214,7 @@ class TransactionTest < ActiveRecord::TestCase
end
def test_invalid_keys_for_transaction
- assert_raises ArgumentError do
+ assert_raise ArgumentError do
Topic.transaction :nested => true do
end
end
@@ -306,17 +306,15 @@ class TransactionTest < ActiveRecord::TestCase
assert_equal "Three", @three
end if Topic.connection.supports_savepoints?
- uses_mocha 'mocking connection.commit_db_transaction' do
- def test_rollback_when_commit_raises
- Topic.connection.expects(:begin_db_transaction)
- Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
- Topic.connection.expects(:outside_transaction?).returns(false)
- Topic.connection.expects(:rollback_db_transaction)
+ def test_rollback_when_commit_raises
+ Topic.connection.expects(:begin_db_transaction)
+ Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
+ Topic.connection.expects(:outside_transaction?).returns(false)
+ Topic.connection.expects(:rollback_db_transaction)
- assert_raise RuntimeError do
- Topic.transaction do
- # do nothing
- end
+ assert_raise RuntimeError do
+ Topic.transaction do
+ # do nothing
end
end
end
@@ -330,14 +328,12 @@ class TransactionTest < ActiveRecord::TestCase
assert Topic.connection.outside_transaction?
end
- uses_mocha 'mocking connection.rollback_db_transaction' do
- def test_rollback_wont_be_executed_if_no_transaction_active
- assert_raise RuntimeError do
- Topic.transaction do
- Topic.connection.rollback_db_transaction
- Topic.connection.expects(:rollback_db_transaction).never
- raise "Rails doesn't scale!"
- end
+ def test_rollback_wont_be_executed_if_no_transaction_active
+ assert_raise RuntimeError do
+ Topic.transaction do
+ Topic.connection.rollback_db_transaction
+ Topic.connection.expects(:rollback_db_transaction).never
+ raise "Rails doesn't scale!"
end
end
end
@@ -353,7 +349,7 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- def test_sqlite_add_column_in_transaction_raises_statement_invalid
+ def test_sqlite_add_column_in_transaction
return true unless current_adapter?(:SQLite3Adapter, :SQLiteAdapter)
# Test first if column creation/deletion works correctly when no
@@ -372,10 +368,15 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.column_names.include?('stuff')
end
- # Test now inside a transaction: add_column should raise a StatementInvalid
- Topic.transaction do
- assert_raises(ActiveRecord::StatementInvalid) { Topic.connection.add_column('topics', 'stuff', :string) }
- raise ActiveRecord::Rollback
+ if Topic.connection.supports_ddl_transactions?
+ assert_nothing_raised do
+ Topic.transaction { Topic.connection.add_column('topics', 'stuff', :string) }
+ end
+ else
+ Topic.transaction do
+ assert_raise(ActiveRecord::StatementInvalid) { Topic.connection.add_column('topics', 'stuff', :string) }
+ raise ActiveRecord::Rollback
+ end
end
end
diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb
index e893a704f1..66982346e9 100644
--- a/activerecord/test/cases/validations_i18n_test.rb
+++ b/activerecord/test/cases/validations_i18n_test.rb
@@ -71,359 +71,353 @@ class ActiveRecordValidationsI18nTests < ActiveSupport::TestCase
end
# ActiveRecord::Errors
- uses_mocha 'ActiveRecord::Errors' do
+ def test_errors_generate_message_translates_custom_model_attribute_key
- def test_errors_generate_message_translates_custom_model_attribute_key
-
- I18n.expects(:translate).with(
- :topic,
- { :count => 1,
- :default => ['Topic'],
- :scope => [:activerecord, :models]
- }
- ).returns('Topic')
+ I18n.expects(:translate).with(
+ :topic,
+ { :count => 1,
+ :default => ['Topic'],
+ :scope => [:activerecord, :models]
+ }
+ ).returns('Topic')
- I18n.expects(:translate).with(
- :"topic.title",
- { :count => 1,
- :default => ['Title'],
- :scope => [:activerecord, :attributes]
- }
- ).returns('Title')
+ I18n.expects(:translate).with(
+ :"topic.title",
+ { :count => 1,
+ :default => ['Title'],
+ :scope => [:activerecord, :attributes]
+ }
+ ).returns('Title')
+
+ I18n.expects(:translate).with(
+ :"models.topic.attributes.title.invalid",
+ :value => nil,
+ :scope => [:activerecord, :errors],
+ :default => [
+ :"models.topic.invalid",
+ 'default from class def error 1',
+ :"messages.invalid"],
+ :attribute => "Title",
+ :model => "Topic"
+ ).returns('default from class def error 1')
+
+ @topic.errors.generate_message :title, :invalid, :default => 'default from class def error 1'
+ end
+
+ def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti
+
+ I18n.expects(:translate).with(
+ :reply,
+ { :count => 1,
+ :default => [:topic, 'Reply'],
+ :scope => [:activerecord, :models]
+ }
+ ).returns('Reply')
- I18n.expects(:translate).with(
+ I18n.expects(:translate).with(
+ :"reply.title",
+ { :count => 1,
+ :default => [:'topic.title', 'Title'],
+ :scope => [:activerecord, :attributes]
+ }
+ ).returns('Title')
+
+ I18n.expects(:translate).with(
+ :"models.reply.attributes.title.invalid",
+ :value => nil,
+ :scope => [:activerecord, :errors],
+ :default => [
+ :"models.reply.invalid",
:"models.topic.attributes.title.invalid",
- :value => nil,
- :scope => [:activerecord, :errors],
- :default => [
- :"models.topic.invalid",
- 'default from class def error 1',
- :"messages.invalid"],
- :attribute => "Title",
- :model => "Topic"
- ).returns('default from class def error 1')
-
- @topic.errors.generate_message :title, :invalid, :default => 'default from class def error 1'
- end
-
- def test_errors_generate_message_translates_custom_model_attribute_keys_with_sti
-
- I18n.expects(:translate).with(
- :reply,
- { :count => 1,
- :default => [:topic, 'Reply'],
- :scope => [:activerecord, :models]
- }
- ).returns('Reply')
+ :"models.topic.invalid",
+ 'default from class def',
+ :"messages.invalid"],
+ :model => 'Reply',
+ :attribute => 'Title'
+ ).returns("default from class def")
- I18n.expects(:translate).with(
- :"reply.title",
- { :count => 1,
- :default => [:'topic.title', 'Title'],
- :scope => [:activerecord, :attributes]
- }
- ).returns('Title')
-
- I18n.expects(:translate).with(
- :"models.reply.attributes.title.invalid",
- :value => nil,
- :scope => [:activerecord, :errors],
- :default => [
- :"models.reply.invalid",
- :"models.topic.attributes.title.invalid",
- :"models.topic.invalid",
- 'default from class def',
- :"messages.invalid"],
- :model => 'Reply',
- :attribute => 'Title'
- ).returns("default from class def")
-
- Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def'
+ Reply.new.errors.generate_message :title, :invalid, :default => 'default from class def'
- end
+ end
- def test_errors_add_on_empty_generates_message
- @topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil})
- @topic.errors.add_on_empty :title
- end
+ def test_errors_add_on_empty_generates_message
+ @topic.errors.expects(:generate_message).with(:title, :empty, {:default => nil})
+ @topic.errors.add_on_empty :title
+ end
- def test_errors_add_on_empty_generates_message_with_custom_default_message
- @topic.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'})
- @topic.errors.add_on_empty :title, 'custom'
- end
+ def test_errors_add_on_empty_generates_message_with_custom_default_message
+ @topic.errors.expects(:generate_message).with(:title, :empty, {:default => 'custom'})
+ @topic.errors.add_on_empty :title, 'custom'
+ end
- def test_errors_add_on_blank_generates_message
- @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil})
- @topic.errors.add_on_blank :title
- end
+ def test_errors_add_on_blank_generates_message
+ @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil})
+ @topic.errors.add_on_blank :title
+ end
- def test_errors_add_on_blank_generates_message_with_custom_default_message
- @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'})
- @topic.errors.add_on_blank :title, 'custom'
- end
+ def test_errors_add_on_blank_generates_message_with_custom_default_message
+ @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'})
+ @topic.errors.add_on_blank :title, 'custom'
+ end
- def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
- @topic.errors.instance_variable_set :@errors, { 'title' => ['empty'] }
- I18n.expects(:translate).with(:"topic.title", :default => ['Title'], :scope => [:activerecord, :attributes], :count => 1).returns('Title')
- @topic.errors.full_messages :locale => 'en'
- end
+ def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
+ @topic.errors.instance_variable_set :@errors, { 'title' => ['empty'] }
+ I18n.expects(:translate).with(:"topic.title", :default => ['Title'], :scope => [:activerecord, :attributes], :count => 1).returns('Title')
+ @topic.errors.full_messages :locale => 'en'
end
# ActiveRecord::Validations
- uses_mocha 'ActiveRecord::Validations' do
- # validates_confirmation_of w/ mocha
-
- def test_validates_confirmation_of_generates_message
- Topic.validates_confirmation_of :title
- @topic.title_confirmation = 'foo'
- @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil})
- @topic.valid?
- end
+ # validates_confirmation_of w/ mocha
+ def test_validates_confirmation_of_generates_message
+ Topic.validates_confirmation_of :title
+ @topic.title_confirmation = 'foo'
+ @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => nil})
+ @topic.valid?
+ end
- def test_validates_confirmation_of_generates_message_with_custom_default_message
- Topic.validates_confirmation_of :title, :message => 'custom'
- @topic.title_confirmation = 'foo'
- @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'})
- @topic.valid?
- end
+ def test_validates_confirmation_of_generates_message_with_custom_default_message
+ Topic.validates_confirmation_of :title, :message => 'custom'
+ @topic.title_confirmation = 'foo'
+ @topic.errors.expects(:generate_message).with(:title, :confirmation, {:default => 'custom'})
+ @topic.valid?
+ end
- # validates_acceptance_of w/ mocha
+ # validates_acceptance_of w/ mocha
- def test_validates_acceptance_of_generates_message
- Topic.validates_acceptance_of :title, :allow_nil => false
- @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => nil})
- @topic.valid?
- end
+ def test_validates_acceptance_of_generates_message
+ Topic.validates_acceptance_of :title, :allow_nil => false
+ @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => nil})
+ @topic.valid?
+ end
- def test_validates_acceptance_of_generates_message_with_custom_default_message
- Topic.validates_acceptance_of :title, :message => 'custom', :allow_nil => false
- @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'})
- @topic.valid?
- end
+ def test_validates_acceptance_of_generates_message_with_custom_default_message
+ Topic.validates_acceptance_of :title, :message => 'custom', :allow_nil => false
+ @topic.errors.expects(:generate_message).with(:title, :accepted, {:default => 'custom'})
+ @topic.valid?
+ end
- # validates_presence_of w/ mocha
+ # validates_presence_of w/ mocha
- def test_validates_presence_of_generates_message
- Topic.validates_presence_of :title
- @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil})
- @topic.valid?
- end
+ def test_validates_presence_of_generates_message
+ Topic.validates_presence_of :title
+ @topic.errors.expects(:generate_message).with(:title, :blank, {:default => nil})
+ @topic.valid?
+ end
- def test_validates_presence_of_generates_message_with_custom_default_message
- Topic.validates_presence_of :title, :message => 'custom'
- @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'})
- @topic.valid?
- end
+ def test_validates_presence_of_generates_message_with_custom_default_message
+ Topic.validates_presence_of :title, :message => 'custom'
+ @topic.errors.expects(:generate_message).with(:title, :blank, {:default => 'custom'})
+ @topic.valid?
+ end
- def test_validates_length_of_within_generates_message_with_title_too_short
- Topic.validates_length_of :title, :within => 3..5
- @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil})
- @topic.valid?
- end
+ def test_validates_length_of_within_generates_message_with_title_too_short
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil})
+ @topic.valid?
+ end
- def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message
- Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom'
- @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message
+ Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom'
+ @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'})
+ @topic.valid?
+ end
- def test_validates_length_of_within_generates_message_with_title_too_long
- Topic.validates_length_of :title, :within => 3..5
- @topic.title = 'this title is too long'
- @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil})
- @topic.valid?
- end
+ def test_validates_length_of_within_generates_message_with_title_too_long
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.title = 'this title is too long'
+ @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil})
+ @topic.valid?
+ end
- def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message
- Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom'
- @topic.title = 'this title is too long'
- @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message
+ Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom'
+ @topic.title = 'this title is too long'
+ @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'})
+ @topic.valid?
+ end
- # validates_length_of :within w/ mocha
+ # validates_length_of :within w/ mocha
- def test_validates_length_of_within_generates_message_with_title_too_short
- Topic.validates_length_of :title, :within => 3..5
- @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil})
- @topic.valid?
- end
+ def test_validates_length_of_within_generates_message_with_title_too_short
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => nil})
+ @topic.valid?
+ end
- def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message
- Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom'
- @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_length_of_within_generates_message_with_title_too_short_and_custom_default_message
+ Topic.validates_length_of :title, :within => 3..5, :too_short => 'custom'
+ @topic.errors.expects(:generate_message).with(:title, :too_short, {:count => 3, :default => 'custom'})
+ @topic.valid?
+ end
- def test_validates_length_of_within_generates_message_with_title_too_long
- Topic.validates_length_of :title, :within => 3..5
- @topic.title = 'this title is too long'
- @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil})
- @topic.valid?
- end
+ def test_validates_length_of_within_generates_message_with_title_too_long
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.title = 'this title is too long'
+ @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => nil})
+ @topic.valid?
+ end
- def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message
- Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom'
- @topic.title = 'this title is too long'
- @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_length_of_within_generates_message_with_title_too_long_and_custom_default_message
+ Topic.validates_length_of :title, :within => 3..5, :too_long => 'custom'
+ @topic.title = 'this title is too long'
+ @topic.errors.expects(:generate_message).with(:title, :too_long, {:count => 5, :default => 'custom'})
+ @topic.valid?
+ end
- # validates_length_of :is w/ mocha
+ # validates_length_of :is w/ mocha
- def test_validates_length_of_is_generates_message
- Topic.validates_length_of :title, :is => 5
- @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil})
- @topic.valid?
- end
+ def test_validates_length_of_is_generates_message
+ Topic.validates_length_of :title, :is => 5
+ @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => nil})
+ @topic.valid?
+ end
- def test_validates_length_of_is_generates_message_with_custom_default_message
- Topic.validates_length_of :title, :is => 5, :message => 'custom'
- @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_length_of_is_generates_message_with_custom_default_message
+ Topic.validates_length_of :title, :is => 5, :message => 'custom'
+ @topic.errors.expects(:generate_message).with(:title, :wrong_length, {:count => 5, :default => 'custom'})
+ @topic.valid?
+ end
- # validates_uniqueness_of w/ mocha
+ # validates_uniqueness_of w/ mocha
- def test_validates_uniqueness_of_generates_message
- Topic.validates_uniqueness_of :title
- @topic.title = unique_topic.title
- @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'})
- @topic.valid?
- end
+ def test_validates_uniqueness_of_generates_message
+ Topic.validates_uniqueness_of :title
+ @topic.title = unique_topic.title
+ @topic.errors.expects(:generate_message).with(:title, :taken, {:default => nil, :value => 'unique!'})
+ @topic.valid?
+ end
- def test_validates_uniqueness_of_generates_message_with_custom_default_message
- Topic.validates_uniqueness_of :title, :message => 'custom'
- @topic.title = unique_topic.title
- @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'})
- @topic.valid?
- end
+ def test_validates_uniqueness_of_generates_message_with_custom_default_message
+ Topic.validates_uniqueness_of :title, :message => 'custom'
+ @topic.title = unique_topic.title
+ @topic.errors.expects(:generate_message).with(:title, :taken, {:default => 'custom', :value => 'unique!'})
+ @topic.valid?
+ end
- # validates_format_of w/ mocha
+ # validates_format_of w/ mocha
- def test_validates_format_of_generates_message
- Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
- @topic.title = '72x'
- @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil})
- @topic.valid?
- end
+ def test_validates_format_of_generates_message
+ Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
+ @topic.title = '72x'
+ @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => nil})
+ @topic.valid?
+ end
- def test_validates_format_of_generates_message_with_custom_default_message
- Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom'
- @topic.title = '72x'
- @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_format_of_generates_message_with_custom_default_message
+ Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/, :message => 'custom'
+ @topic.title = '72x'
+ @topic.errors.expects(:generate_message).with(:title, :invalid, {:value => '72x', :default => 'custom'})
+ @topic.valid?
+ end
- # validates_inclusion_of w/ mocha
+ # validates_inclusion_of w/ mocha
- def test_validates_inclusion_of_generates_message
- Topic.validates_inclusion_of :title, :in => %w(a b c)
- @topic.title = 'z'
- @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil})
- @topic.valid?
- end
+ def test_validates_inclusion_of_generates_message
+ Topic.validates_inclusion_of :title, :in => %w(a b c)
+ @topic.title = 'z'
+ @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => nil})
+ @topic.valid?
+ end
- def test_validates_inclusion_of_generates_message_with_custom_default_message
- Topic.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom'
- @topic.title = 'z'
- @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_inclusion_of_generates_message_with_custom_default_message
+ Topic.validates_inclusion_of :title, :in => %w(a b c), :message => 'custom'
+ @topic.title = 'z'
+ @topic.errors.expects(:generate_message).with(:title, :inclusion, {:value => 'z', :default => 'custom'})
+ @topic.valid?
+ end
- # validates_exclusion_of w/ mocha
+ # validates_exclusion_of w/ mocha
- def test_validates_exclusion_of_generates_message
- Topic.validates_exclusion_of :title, :in => %w(a b c)
- @topic.title = 'a'
- @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil})
- @topic.valid?
- end
+ def test_validates_exclusion_of_generates_message
+ Topic.validates_exclusion_of :title, :in => %w(a b c)
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => nil})
+ @topic.valid?
+ end
- def test_validates_exclusion_of_generates_message_with_custom_default_message
- Topic.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom'
- @topic.title = 'a'
- @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_exclusion_of_generates_message_with_custom_default_message
+ Topic.validates_exclusion_of :title, :in => %w(a b c), :message => 'custom'
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :exclusion, {:value => 'a', :default => 'custom'})
+ @topic.valid?
+ end
- # validates_numericality_of without :only_integer w/ mocha
+ # validates_numericality_of without :only_integer w/ mocha
- def test_validates_numericality_of_generates_message
- Topic.validates_numericality_of :title
- @topic.title = 'a'
- @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil})
- @topic.valid?
- end
+ def test_validates_numericality_of_generates_message
+ Topic.validates_numericality_of :title
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil})
+ @topic.valid?
+ end
- def test_validates_numericality_of_generates_message_with_custom_default_message
- Topic.validates_numericality_of :title, :message => 'custom'
- @topic.title = 'a'
- @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_numericality_of_generates_message_with_custom_default_message
+ Topic.validates_numericality_of :title, :message => 'custom'
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
+ @topic.valid?
+ end
- # validates_numericality_of with :only_integer w/ mocha
+ # validates_numericality_of with :only_integer w/ mocha
- def test_validates_numericality_of_only_integer_generates_message
- Topic.validates_numericality_of :title, :only_integer => true
- @topic.title = 'a'
- @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil})
- @topic.valid?
- end
+ def test_validates_numericality_of_only_integer_generates_message
+ Topic.validates_numericality_of :title, :only_integer => true
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => nil})
+ @topic.valid?
+ end
- def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message
- Topic.validates_numericality_of :title, :only_integer => true, :message => 'custom'
- @topic.title = 'a'
- @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_numericality_of_only_integer_generates_message_with_custom_default_message
+ Topic.validates_numericality_of :title, :only_integer => true, :message => 'custom'
+ @topic.title = 'a'
+ @topic.errors.expects(:generate_message).with(:title, :not_a_number, {:value => 'a', :default => 'custom'})
+ @topic.valid?
+ end
- # validates_numericality_of :odd w/ mocha
+ # validates_numericality_of :odd w/ mocha
- def test_validates_numericality_of_odd_generates_message
- Topic.validates_numericality_of :title, :only_integer => true, :odd => true
- @topic.title = 0
- @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil})
- @topic.valid?
- end
+ def test_validates_numericality_of_odd_generates_message
+ Topic.validates_numericality_of :title, :only_integer => true, :odd => true
+ @topic.title = 0
+ @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => nil})
+ @topic.valid?
+ end
- def test_validates_numericality_of_odd_generates_message_with_custom_default_message
- Topic.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom'
- @topic.title = 0
- @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_numericality_of_odd_generates_message_with_custom_default_message
+ Topic.validates_numericality_of :title, :only_integer => true, :odd => true, :message => 'custom'
+ @topic.title = 0
+ @topic.errors.expects(:generate_message).with(:title, :odd, {:value => 0, :default => 'custom'})
+ @topic.valid?
+ end
- # validates_numericality_of :less_than w/ mocha
+ # validates_numericality_of :less_than w/ mocha
- def test_validates_numericality_of_less_than_generates_message
- Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
- @topic.title = 1
- @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil})
- @topic.valid?
- end
+ def test_validates_numericality_of_less_than_generates_message
+ Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
+ @topic.title = 1
+ @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => nil})
+ @topic.valid?
+ end
- def test_validates_numericality_of_odd_generates_message_with_custom_default_message
- Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom'
- @topic.title = 1
- @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'})
- @topic.valid?
- end
+ def test_validates_numericality_of_odd_generates_message_with_custom_default_message
+ Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0, :message => 'custom'
+ @topic.title = 1
+ @topic.errors.expects(:generate_message).with(:title, :less_than, {:value => 1, :count => 0, :default => 'custom'})
+ @topic.valid?
+ end
- # validates_associated w/ mocha
+ # validates_associated w/ mocha
- def test_validates_associated_generates_message
- Topic.validates_associated :replies
- replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil})
- replied_topic.valid?
- end
+ def test_validates_associated_generates_message
+ Topic.validates_associated :replies
+ replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil})
+ replied_topic.valid?
+ end
- def test_validates_associated_generates_message_with_custom_default_message
- Topic.validates_associated :replies
- replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil})
- replied_topic.valid?
- end
+ def test_validates_associated_generates_message_with_custom_default_message
+ Topic.validates_associated :replies
+ replied_topic.errors.expects(:generate_message).with(:replies, :invalid, {:value => replied_topic.replies, :default => nil})
+ replied_topic.valid?
end
# validates_confirmation_of w/o mocha
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 380d8ac260..c20f5ae63e 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -8,6 +8,7 @@ require 'models/warehouse_thing'
require 'models/guid'
require 'models/owner'
require 'models/pet'
+require 'models/event'
# The following methods in Topic are used in test_conditional_validation_*
class Topic
@@ -113,8 +114,8 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_invalid_record_exception
- assert_raises(ActiveRecord::RecordInvalid) { Reply.create! }
- assert_raises(ActiveRecord::RecordInvalid) { Reply.new.save! }
+ assert_raise(ActiveRecord::RecordInvalid) { Reply.create! }
+ assert_raise(ActiveRecord::RecordInvalid) { Reply.new.save! }
begin
r = Reply.new
@@ -126,13 +127,13 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_exception_on_create_bang_many
- assert_raises(ActiveRecord::RecordInvalid) do
+ assert_raise(ActiveRecord::RecordInvalid) do
Reply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
end
end
def test_exception_on_create_bang_with_block
- assert_raises(ActiveRecord::RecordInvalid) do
+ assert_raise(ActiveRecord::RecordInvalid) do
Reply.create!({ "title" => "OK" }) do |r|
r.content = nil
end
@@ -140,7 +141,7 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_exception_on_create_bang_many_with_block
- assert_raises(ActiveRecord::RecordInvalid) do
+ assert_raise(ActiveRecord::RecordInvalid) do
Reply.create!([{ "title" => "OK" }, { "title" => "Wrong Create" }]) do |r|
r.content = nil
end
@@ -149,7 +150,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_scoped_create_without_attributes
Reply.with_scope(:create => {}) do
- assert_raises(ActiveRecord::RecordInvalid) { Reply.create! }
+ assert_raise(ActiveRecord::RecordInvalid) { Reply.create! }
end
end
@@ -169,7 +170,7 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal person.first_name, "Mary", "should be ok when no attributes are passed to create!"
end
end
- end
+ end
def test_single_error_per_attr_iteration
r = Reply.new
@@ -530,6 +531,14 @@ class ValidationsTest < ActiveRecord::TestCase
end
end
+ def test_validate_uniqueness_with_limit
+ # Event.title is limited to 5 characters
+ e1 = Event.create(:title => "abcde")
+ assert e1.valid?, "Could not create an event with a unique, 5 character title"
+ e2 = Event.create(:title => "abcdefgh")
+ assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique"
+ end
+
def test_validate_straight_inheritance_uniqueness
w1 = IneptWizard.create(:name => "Rincewind", :city => "Ankh-Morpork")
assert w1.valid?, "Saving w1"
@@ -958,6 +967,19 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal "boo 5", t.errors["title"]
end
+ def test_validates_length_of_custom_errors_for_in
+ Topic.validates_length_of(:title, :in => 10..20, :message => "hoo {{count}}")
+ t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 10", t.errors["title"]
+
+ t = Topic.create("title" => "uhohuhohuhohuhohuhohuhohuhohuhoh", "content" => "whatever")
+ assert !t.valid?
+ assert t.errors.on(:title)
+ assert_equal "hoo 20", t.errors["title"]
+ end
+
def test_validates_length_of_custom_errors_for_maximum_with_too_long
Topic.validates_length_of( :title, :maximum=>5, :too_long=>"hoo {{count}}" )
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
@@ -1408,6 +1430,17 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal "can't be blank", t.errors.on("title").first
end
+ def test_invalid_should_be_the_opposite_of_valid
+ Topic.validates_presence_of :title
+
+ t = Topic.new
+ assert t.invalid?
+ assert t.errors.invalid?(:title)
+
+ t.title = 'Things are going to change'
+ assert !t.invalid?
+ end
+
# previous implementation of validates_presence_of eval'd the
# string with the wrong binding, this regression test is to
# ensure that it works correctly
@@ -1429,20 +1462,6 @@ class ValidationsTest < ActiveRecord::TestCase
t.author_name = "Hubert J. Farnsworth"
assert t.valid?, "A topic with an important title and author should be valid"
end
-
- private
- def with_kcode(kcode)
- if RUBY_VERSION < '1.9'
- orig_kcode, $KCODE = $KCODE, kcode
- begin
- yield
- ensure
- $KCODE = orig_kcode
- end
- else
- yield
- end
- end
end
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index 39c6ea820d..b49997669e 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -38,11 +38,15 @@ class XmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<CreatedAt}, @xml
end
+ def test_should_allow_skipped_types
+ @xml = Contact.new(:age => 25).to_xml :skip_types => true
+ assert %r{<age>25</age>}.match(@xml)
+ end
+
def test_should_include_yielded_additions
@xml = Contact.new.to_xml do |xml|
xml.creator "David"
end
-
assert_match %r{<creator>David</creator>}, @xml
end
end
@@ -145,6 +149,13 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert_match %r{<hello-post type="StiPost">}, xml
end
+ def test_included_associations_should_skip_types
+ xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true
+ assert_match %r{<hello-posts>}, xml
+ assert_match %r{<hello-post>}, xml
+ assert_match %r{<hello-post>}, xml
+ end
+
def test_methods_are_called_on_object
xml = authors(:david).to_xml :methods => :label, :indent => 0
assert_match %r{<label>.*</label>}, xml
diff --git a/activerecord/test/fixtures/toys.yml b/activerecord/test/fixtures/toys.yml
new file mode 100644
index 0000000000..037e335e0a
--- /dev/null
+++ b/activerecord/test/fixtures/toys.yml
@@ -0,0 +1,4 @@
+bone:
+ toy_id: 1
+ name: Bone
+ pet_id: 1
diff --git a/activerecord/test/models/bird.rb b/activerecord/test/models/bird.rb
new file mode 100644
index 0000000000..341d2eeffc
--- /dev/null
+++ b/activerecord/test/models/bird.rb
@@ -0,0 +1,3 @@
+class Bird < ActiveRecord::Base
+ validates_presence_of :name
+end \ No newline at end of file
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 3b27a9e272..02a775f9ef 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -37,6 +37,7 @@ 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' )"
+ 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"
has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
@@ -69,6 +70,7 @@ class Firm < Company
has_one :account_with_select, :foreign_key => "firm_id", :select => "id, firm_id", :class_name=>'Account'
has_one :readonly_account, :foreign_key => "firm_id", :class_name => "Account", :readonly => true
has_one :account_using_primary_key, :primary_key => "firm_id", :class_name => "Account"
+ has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete
end
class DependentFirm < Company
diff --git a/activerecord/test/models/event.rb b/activerecord/test/models/event.rb
new file mode 100644
index 0000000000..99fa0feeb7
--- /dev/null
+++ b/activerecord/test/models/event.rb
@@ -0,0 +1,3 @@
+class Event < ActiveRecord::Base
+ validates_uniqueness_of :title
+end \ No newline at end of file
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index dbaf2ce688..5760b991ec 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -1,4 +1,5 @@
class Owner < ActiveRecord::Base
set_primary_key :owner_id
has_many :pets
-end \ No newline at end of file
+ has_many :toys, :through => :pets
+end
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index b9431fd1c0..4a7ed52636 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -4,6 +4,8 @@ class Parrot < ActiveRecord::Base
has_and_belongs_to_many :treasures
has_many :loots, :as => :looter
alias_attribute :title, :name
+
+ validates_presence_of :name
end
class LiveParrot < Parrot
diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb
index 889ce46f33..dc1a3c5e94 100644
--- a/activerecord/test/models/pet.rb
+++ b/activerecord/test/models/pet.rb
@@ -1,4 +1,5 @@
class Pet < ActiveRecord::Base
set_primary_key :pet_id
belongs_to :owner
-end \ No newline at end of file
+ has_many :toys
+end
diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb
index 51c8183dee..238917bf30 100644
--- a/activerecord/test/models/pirate.rb
+++ b/activerecord/test/models/pirate.rb
@@ -1,9 +1,63 @@
class Pirate < ActiveRecord::Base
belongs_to :parrot
has_and_belongs_to_many :parrots
- has_many :treasures, :as => :looter
+ has_and_belongs_to_many :parrots_with_method_callbacks, :class_name => "Parrot",
+ :before_add => :log_before_add,
+ :after_add => :log_after_add,
+ :before_remove => :log_before_remove,
+ :after_remove => :log_after_remove
+ has_and_belongs_to_many :parrots_with_proc_callbacks, :class_name => "Parrot",
+ :before_add => proc {|p,pa| p.ship_log << "before_adding_proc_parrot_#{pa.id || '<new>'}"},
+ :after_add => proc {|p,pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}"},
+ :before_remove => proc {|p,pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}"},
+ :after_remove => proc {|p,pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}"}
+ has_many :treasures, :as => :looter
has_many :treasure_estimates, :through => :treasures, :source => :price_estimates
+ # These both have :autosave enabled because accepts_nested_attributes_for is used on them.
+ has_one :ship
+ has_many :birds
+ has_many :birds_with_method_callbacks, :class_name => "Bird",
+ :before_add => :log_before_add,
+ :after_add => :log_after_add,
+ :before_remove => :log_before_remove,
+ :after_remove => :log_after_remove
+ has_many :birds_with_proc_callbacks, :class_name => "Bird",
+ :before_add => proc {|p,b| p.ship_log << "before_adding_proc_bird_#{b.id || '<new>'}"},
+ :after_add => proc {|p,b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}"},
+ :before_remove => proc {|p,b| p.ship_log << "before_removing_proc_bird_#{b.id}"},
+ :after_remove => proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.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 :parrots_with_method_callbacks, :parrots_with_proc_callbacks,
+ :birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true
+
validates_presence_of :catchphrase
+
+ def ship_log
+ @ship_log ||= []
+ end
+
+ private
+ def log_before_add(record)
+ log(record, "before_adding_method")
+ end
+
+ def log_after_add(record)
+ log(record, "after_adding_method")
+ end
+
+ def log_before_remove(record)
+ log(record, "before_removing_method")
+ end
+
+ def log_after_remove(record)
+ log(record, "after_removing_method")
+ end
+
+ def log(record, callback)
+ ship_log << "#{callback}_#{record.class.name.downcase}_#{record.id || '<new>'}"
+ end
end
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 388fff8fba..374e536a5b 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -1,5 +1,7 @@
class Post < ActiveRecord::Base
named_scope :containing_the_letter_a, :conditions => "body LIKE '%a%'"
+ named_scope :ranked_by_comments, :order => "comments_count DESC"
+ named_scope :limit, lambda {|limit| {:limit => limit} }
named_scope :with_authors_at_address, lambda { |address| {
:conditions => [ 'authors.author_address_id = ?', address.id ],
:joins => 'JOIN authors ON authors.id = posts.author_id'
@@ -68,6 +70,10 @@ class Post < ActiveRecord::Base
:before_remove => lambda {|owner, reader| log(:removed, :before, reader.first_name) },
:after_remove => lambda {|owner, reader| log(:removed, :after, reader.first_name) }
+ def self.top(limit)
+ ranked_by_comments.limit(limit)
+ end
+
def self.reset_log
@log = []
end
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index 812bc1f535..1c990acab6 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -37,3 +37,9 @@ end
class SillyReply < Reply
belongs_to :reply, :foreign_key => "parent_id", :counter_cache => :replies_count
end
+
+module Web
+ class Reply < Web::Topic
+ belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true, :class_name => 'Web::Topic'
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb
index 05b09fc1b9..06759d64b8 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -1,3 +1,10 @@
class Ship < ActiveRecord::Base
self.record_timestamps = false
-end \ No newline at end of file
+
+ belongs_to :pirate
+ has_many :parts, :class_name => 'ShipPart', :autosave => true
+
+ accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+
+ validates_presence_of :name
+end
diff --git a/activerecord/test/models/ship_part.rb b/activerecord/test/models/ship_part.rb
new file mode 100644
index 0000000000..0a606db239
--- /dev/null
+++ b/activerecord/test/models/ship_part.rb
@@ -0,0 +1,5 @@
+class ShipPart < ActiveRecord::Base
+ belongs_to :ship
+
+ validates_presence_of :name
+end \ No newline at end of file
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 08bb24ed03..51012d22ed 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -1,7 +1,9 @@
class Topic < ActiveRecord::Base
named_scope :base
named_scope :written_before, lambda { |time|
- { :conditions => ['written_on < ?', time] }
+ if time
+ { :conditions => ['written_on < ?', time] }
+ end
}
named_scope :approved, :conditions => {:approved => true}
named_scope :rejected, :conditions => {:approved => false}
@@ -33,6 +35,8 @@ class Topic < ActiveRecord::Base
end
named_scope :named_extension, :extend => NamedExtension
named_scope :multiple_extensions, :extend => [MultipleExtensionTwo, MultipleExtensionOne]
+
+ named_scope :by_rejected_ids, lambda {{ :conditions => { :id => all(:conditions => {:approved => false}).map(&:id) } }}
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
serialize :content
@@ -69,3 +73,9 @@ class Topic < ActiveRecord::Base
end
end
end
+
+module Web
+ class Topic < ActiveRecord::Base
+ has_many :replies, :dependent => :destroy, :foreign_key => "parent_id", :class_name => 'Web::Reply'
+ end
+end \ No newline at end of file
diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb
new file mode 100644
index 0000000000..79a88db0da
--- /dev/null
+++ b/activerecord/test/models/toy.rb
@@ -0,0 +1,4 @@
+class Toy < ActiveRecord::Base
+ set_primary_key :toy_id
+ belongs_to :pet
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index d44faf04cc..ea848a2940 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -55,6 +55,11 @@ ActiveRecord::Schema.define do
t.binary :data
end
+ create_table :birds, :force => true do |t|
+ t.string :name
+ t.integer :pirate_id
+ end
+
create_table :books, :force => true do |t|
t.column :name, :string
end
@@ -150,6 +155,10 @@ ActiveRecord::Schema.define do
t.integer :course_id, :null => false
end
+ create_table :events, :force => true do |t|
+ t.string :title, :limit => 5
+ end
+
create_table :funny_jokes, :force => true do |t|
t.string :name
end
@@ -356,12 +365,18 @@ ActiveRecord::Schema.define do
create_table :ships, :force => true do |t|
t.string :name
+ t.integer :pirate_id
t.datetime :created_at
t.datetime :created_on
t.datetime :updated_at
t.datetime :updated_on
end
+ create_table :ship_parts, :force => true do |t|
+ t.string :name
+ t.integer :ship_id
+ end
+
create_table :sponsors, :force => true do |t|
t.integer :club_id
t.integer :sponsorable_id
@@ -410,6 +425,11 @@ ActiveRecord::Schema.define do
t.column :taggings_count, :integer, :default => 0
end
+ create_table :toys, :primary_key => :toy_id ,:force => true do |t|
+ t.string :name
+ t.integer :pet_id, :integer
+ end
+
create_table :treasures, :force => true do |t|
t.column :name, :string
t.column :looter_id, :integer