aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorrick <technoweenie@gmail.com>2008-08-26 11:53:33 -0700
committerrick <technoweenie@gmail.com>2008-08-26 11:53:33 -0700
commit0aef9d1a2651fa0acd2adcd2de308eeb0ec8cdd2 (patch)
tree1a782151632dd80c8a18c3960536bdf8643debe3 /activerecord
parent0a6d75dedd79407376aae1f01302164dfd3e44b6 (diff)
parent229eedfda87a7706dbb5e3e51af8707b3adae375 (diff)
downloadrails-0aef9d1a2651fa0acd2adcd2de308eeb0ec8cdd2.tar.gz
rails-0aef9d1a2651fa0acd2adcd2de308eeb0ec8cdd2.tar.bz2
rails-0aef9d1a2651fa0acd2adcd2de308eeb0ec8cdd2.zip
Merge branch 'master' of git@github.com:rails/rails
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG6
-rw-r--r--[-rwxr-xr-x]activerecord/README0
-rw-r--r--[-rwxr-xr-x]activerecord/Rakefile2
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record.rb23
-rw-r--r--activerecord/lib/active_record/association_preload.rb12
-rwxr-xr-xactiverecord/lib/active_record/associations.rb444
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb2
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb6
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/associations/belongs_to_association.rb0
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb0
-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.rb7
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb2
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/associations/has_one_association.rb4
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/base.rb257
-rw-r--r--activerecord/lib/active_record/calculations.rb2
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/callbacks.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb10
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/connection_adapters/abstract_adapter.rb20
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/connection_adapters/mysql_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb23
-rw-r--r--activerecord/lib/active_record/dirty.rb16
-rw-r--r--activerecord/lib/active_record/dynamic_finder_match.rb40
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/fixtures.rb8
-rw-r--r--activerecord/lib/active_record/locale/en-US.yml33
-rw-r--r--activerecord/lib/active_record/migration.rb95
-rw-r--r--activerecord/lib/active_record/named_scope.rb14
-rw-r--r--activerecord/lib/active_record/observer.rb12
-rw-r--r--activerecord/lib/active_record/reflection.rb2
-rw-r--r--activerecord/lib/active_record/test_case.rb2
-rw-r--r--activerecord/lib/active_record/transactions.rb42
-rw-r--r--[-rwxr-xr-x]activerecord/lib/active_record/validations.rb275
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/associations/belongs_to_associations_test.rb0
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb14
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb36
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb42
-rw-r--r--activerecord/test/cases/associations/eager_test.rb21
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb26
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb32
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb9
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/associations/has_one_associations_test.rb10
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb40
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb7
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/associations_test.rb4
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/attribute_methods_test.rb0
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/base_test.rb45
-rw-r--r--activerecord/test/cases/column_definition_test.rb8
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/deprecated_finder_test.rb0
-rw-r--r--activerecord/test/cases/dirty_test.rb12
-rw-r--r--activerecord/test/cases/finder_test.rb56
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/fixtures_test.rb6
-rw-r--r--activerecord/test/cases/helper.rb1
-rw-r--r--activerecord/test/cases/i18n_test.rb46
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/inheritance_test.rb0
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/lifecycle_test.rb40
-rw-r--r--activerecord/test/cases/method_scoping_test.rb2
-rw-r--r--activerecord/test/cases/migration_test.rb77
-rw-r--r--activerecord/test/cases/multiple_db_test.rb25
-rw-r--r--activerecord/test/cases/named_scope_test.rb43
-rw-r--r--activerecord/test/cases/query_cache_test.rb7
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/readonly_test.rb0
-rw-r--r--activerecord/test/cases/reflection_test.rb4
-rw-r--r--activerecord/test/cases/transactions_test.rb62
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/unconnected_test.rb0
-rw-r--r--activerecord/test/cases/validations_i18n_test.rb858
-rw-r--r--[-rwxr-xr-x]activerecord/test/cases/validations_test.rb21
-rw-r--r--activerecord/test/connections/native_mysql/connection.rb4
-rw-r--r--activerecord/test/migrations/broken/100_migration_that_raises_exception.rb10
-rw-r--r--activerecord/test/models/author.rb3
-rw-r--r--[-rwxr-xr-x]activerecord/test/models/company.rb7
-rw-r--r--activerecord/test/models/developer.rb2
-rw-r--r--activerecord/test/models/project.rb2
-rw-r--r--[-rwxr-xr-x]activerecord/test/models/reply.rb0
-rw-r--r--[-rwxr-xr-x]activerecord/test/models/topic.rb0
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb4
-rw-r--r--activerecord/test/schema/schema.rb3
79 files changed, 2443 insertions, 546 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 95fdd93f65..12295ac799 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,11 @@
*Edge*
+* before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted. #891 [Xavier Noria]
+
+* Transactional migrations for databases which support them. #834 [divoxx, Adam Wiggins, Tarmo Tänav]
+
+* Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin]
+
* change_column_default preserves the not-null constraint. #617 [Tarmo Tänav]
* Fixed that create database statements would always include "DEFAULT NULL" (Nick Sieger) [#334]
diff --git a/activerecord/README b/activerecord/README
index d68eb28a64..d68eb28a64 100755..100644
--- a/activerecord/README
+++ b/activerecord/README
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 60b17e02b9..983528aff7 100755..100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -30,7 +30,7 @@ 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_sqlite test_sqlite3 test_postgresql)
+task :test => %w(test_mysql test_sqlite3 test_postgresql)
for adapter in %w( mysql postgresql sqlite sqlite3 firebird db2 oracle sybase openbase frontbase )
Rake::TestTask.new("test_#{adapter}") { |t|
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index d4f7170305..c47ca486c8 100755..100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -21,17 +21,12 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-$:.unshift(File.dirname(__FILE__)) unless
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
-
-unless defined? ActiveSupport
- active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
- if File.exist?(active_support_path)
- $:.unshift active_support_path
- require 'active_support'
- else
- require 'rubygems'
- gem 'activesupport'
+begin
+ require 'active_support'
+rescue LoadError
+ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
+ if File.directory?(activesupport_path)
+ $:.unshift activesupport_path
require 'active_support'
end
end
@@ -56,6 +51,7 @@ require 'active_record/calculations'
require 'active_record/serialization'
require 'active_record/attribute_methods'
require 'active_record/dirty'
+require 'active_record/dynamic_finder_match'
ActiveRecord::Base.class_eval do
extend ActiveRecord::QueryCache
@@ -80,3 +76,8 @@ end
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/schema_dumper'
+
+I18n.backend.populate do
+ I18n.load_translations File.dirname(__FILE__) + '/active_record/locale/en-US.yml'
+end
+
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index 174ec95de2..61fa34ac39 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -34,7 +34,7 @@ module ActiveRecord
class_to_reflection = {}
# Not all records have the same class, so group then preload
# group on the reflection itself so that if various subclass share the same association then we do not split them
- # unncessarily
+ # unnecessarily
records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records|
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
send("preload_#{reflection.macro}_association", records, reflection, preload_options)
@@ -51,9 +51,7 @@ module ActiveRecord
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
- association_proxy = parent_record.send(reflection_name)
- association_proxy.loaded
- association_proxy.target = associated_record
+ parent_record.send("set_#{reflection_name}_target", associated_record)
end
end
@@ -112,8 +110,8 @@ module ActiveRecord
def preload_has_one_association(records, reflection, preload_options={})
id_to_record_map, ids = construct_id_map(records)
options = reflection.options
+ records.each {|record| record.send("set_#{reflection.name}_target", nil)}
if options[:through]
- records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
through_records = preload_through_records(records, reflection, options[:through])
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.primary_key_name
@@ -126,8 +124,6 @@ module ActiveRecord
end
end
else
- records.each {|record| record.send("set_#{reflection.name}_target", nil)}
-
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
end
end
@@ -252,7 +248,7 @@ module ActiveRecord
table_name = reflection.klass.quoted_table_name
if interface = reflection.options[:as]
- conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'"
+ conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
else
foreign_key = reflection.primary_key_name
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 7ad7802cbc..f915daafba 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -73,6 +73,7 @@ module ActiveRecord
end
end
+ # See ActiveRecord::Associations::ClassMethods for documentation.
module Associations # :nodoc:
def self.included(base)
base.extend(ClassMethods)
@@ -150,6 +151,7 @@ module ActiveRecord
# #others.destroy_all | X | X | X
# #others.find(*args) | X | X | X
# #others.find_first | X | |
+ # #others.exist? | X | X | X
# #others.uniq | X | X | X
# #others.reset | X | X | X
#
@@ -582,12 +584,13 @@ module ActiveRecord
# has_many :clients
# end
#
- # class Company < ActiveRecord::Base; end
+ # class Client < ActiveRecord::Base; end
# end
# end
#
- # When Firm#clients is called, it will in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate
- # with a class in another module scope, this can be done by specifying the complete class name. Example:
+ # When <tt>Firm#clients</tt> is called, it will in turn call <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>.
+ # If you want to associate with a class in another module scope, this can be done by specifying the complete class name.
+ # Example:
#
# module MyApplication
# module Business
@@ -611,31 +614,53 @@ module ActiveRecord
# All of the association macros can be specialized through options. This makes cases more complex than the simple and guessable ones
# possible.
module ClassMethods
- # Adds the following methods for retrieval and query of collections of associated objects:
- # +collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
- # * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects.
+ # Specifies a one-to-many association. The following methods for retrieval and query of
+ # collections of associated objects will be added:
+ #
+ # [collection(force_reload = false)]
+ # Returns an array of all the associated objects.
# An empty array is returned if none are found.
- # * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
- # * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by setting their foreign keys to +NULL+.
+ # [collection<<(object, ...)]
+ # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
+ # [collection.delete(object, ...)]
+ # Removes one or more objects from the collection by setting their foreign keys to +NULL+.
# This will also destroy the objects if they're declared as +belongs_to+ and dependent on this model.
- # * <tt>collection=objects</tt> - Replaces the collections content by deleting and adding objects as appropriate.
- # * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids
- # * <tt>collection_singular_ids=ids</tt> - Replace the collection with the objects identified by the primary keys in +ids+
- # * <tt>collection.clear</tt> - Removes every object from the collection. This destroys the associated objects if they
- # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the database if <tt>:dependent => :delete_all</tt>,
- # otherwise sets their foreign keys to +NULL+.
- # * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects.
- # * <tt>collection.size</tt> - Returns the number of associated objects.
- # * <tt>collection.find</tt> - Finds an associated object according to the same rules as Base.find.
- # * <tt>collection.build(attributes = {}, ...)</tt> - Returns one or more new objects of the collection type that have been instantiated
- # with +attributes+ and linked to this object through a foreign key, but have not yet been saved. *Note:* This only works if an
- # associated object already exists, not if it's +nil+!
- # * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
- # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
- # *Note:* This only works if an associated object already exists, not if it's +nil+!
+ # [collection=objects]
+ # Replaces the collections content by deleting and adding objects as appropriate.
+ # [collection_singular_ids]
+ # Returns an array of the associated objects' ids
+ # [collection_singular_ids=ids]
+ # Replace the collection with the objects identified by the primary keys in +ids+
+ # [collection.clear]
+ # Removes every object from the collection. This destroys the associated objects if they
+ # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the
+ # database if <tt>:dependent => :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
+ # [collection.empty?]
+ # Returns +true+ if there are no associated objects.
+ # [collection.size]
+ # Returns the number of associated objects.
+ # [collection.find(...)]
+ # Finds an associated object according to the same rules as ActiveRecord::Base.find.
+ # [collection.exist?(...)]
+ # Checks whether an associated object with the given conditions exists.
+ # Uses the same rules as ActiveRecord::Base.exists?.
+ # [collection.build(attributes = {}, ...)]
+ # Returns one or more new objects of the collection type that have been instantiated
+ # with +attributes+ and linked to this object through a foreign key, but have not yet
+ # been saved. <b>Note:</b> This only works if an associated object already exists, not if
+ # it's +nil+!
+ # [collection.create(attributes = {})]
+ # Returns a new object of the collection type that has been instantiated
+ # with +attributes+, linked to this object through a foreign key, and that has already
+ # been saved (if it passed the validation). <b>Note:</b> This only works if an associated
+ # object already exists, not if it's +nil+!
+ #
+ # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.)
#
- # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
+ # === Example
+ #
+ # A Firm class declares <tt>has_many :clients</tt>, which will add:
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
@@ -646,54 +671,77 @@ module ActiveRecord
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
+ # * <tt>Firm#clients.exist?(:name => 'ACME')</tt> (similar to <tt>Client.exist?(:name => 'ACME', :firm_id => firm.id)</tt>)
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
# The declaration can also include an options hash to specialize the behavior of the association.
#
- # Options are:
- # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
+ # === Supported options
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_many :products</tt> will by default be linked to the Product class, but
# if the real class name is SpecialProduct, you'll have to specify it with this option.
- # * <tt>:conditions</tt> - Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
+ # [:conditions]
+ # Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from the association are scoped if a hash
# is used. <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
# or <tt>@blog.posts.build</tt>.
- # * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
+ # [:order]
+ # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
# such as <tt>last_name, first_name DESC</tt>.
- # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
+ # [: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_many+ association will use "person_id"
# as the default <tt>:foreign_key</tt>.
- # * <tt>:primary_key</tt> - Specify the method that returns the primary key used for the association. By default this is +id+.
- # * <tt>:dependent</tt> - If set to <tt>:destroy</tt> all the associated objects are destroyed
+ # [:primary_key]
+ # Specify the method that returns the primary key used for the association. By default this is +id+.
+ # [:dependent]
+ # If set to <tt>:destroy</tt> all the associated objects are destroyed
# alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
# objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. *Warning:* This option is ignored when also using
# the <tt>:through</tt> option.
- # * <tt>:finder_sql</tt> - Specify a complete SQL statement to fetch the association. This is a good way to go for complex
+ # [:finder_sql]
+ # Specify a complete SQL statement to fetch the association. This is a good way to go for complex
# associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
- # * <tt>:counter_sql</tt> - Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
+ # [:counter_sql]
+ # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
# specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
- # * <tt>:extend</tt> - Specify a named module for extending the proxy. See "Association extensions".
- # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded.
- # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
- # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
- # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
- # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join
+ # [:extend]
+ # Specify a named module for extending the proxy. See "Association extensions".
+ # [:include]
+ # Specify second-order associations that should be eager loaded when the collection is loaded.
+ # [:group]
+ # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
+ # [:limit]
+ # An integer determining the limit on the number of rows that should be returned.
+ # [:offset]
+ # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
+ # [:select]
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
- # * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
- # * <tt>:through</tt> - Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
+ # [:as]
+ # Specifies a polymorphic interface (See <tt>belongs_to</tt>).
+ # [:through]
+ # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
# or <tt>has_many</tt> association on the join model.
- # * <tt>:source</tt> - Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
+ # [:source]
+ # Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
- # * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
+ # [:source_type]
+ # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
# association is a polymorphic +belongs_to+.
- # * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
- # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
- # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. true by default.
- # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
- #
+ # [:uniq]
+ # If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
+ # [:readonly]
+ # 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.
+ # [:accessible]
+ # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
+
# Option examples:
# has_many :comments, :order => "posted_on"
# has_many :comments, :include => :author
@@ -724,58 +772,91 @@ module ActiveRecord
end
end
- # Adds the following methods for retrieval and query of a single associated object:
- # +association+ is replaced with the symbol passed as the first argument, so
- # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
- # * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
- # * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, sets it as the foreign key,
+ # Specifies a one-to-one association with another class. This method should only be used
+ # if the other class contains the foreign key. If the current class contains the foreign key,
+ # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
+ # on when to use has_one and when to use belongs_to.
+ #
+ # The following methods for retrieval and query of a single associated object will be added:
+ #
+ # [association(force_reload = false)]
+ # Returns the associated object. +nil+ is returned if none is found.
+ # [association=(associate)]
+ # Assigns the associate object, extracts the primary key, sets it as the foreign key,
# and saves the associate object.
- # * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
- # * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
- # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. Note: This ONLY works if
- # an association already exists. It will NOT work if the association is +nil+.
- # * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
- # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
+ # [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. <b>Note:</b> This ONLY works if an association already exists.
+ # It will NOT work if the association is +nil+.
+ # [create_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
+ # with +attributes+, linked to this object through a foreign key, and that
+ # has already been saved (if it passed the validation).
#
- # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add:
+ # (+association+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.)
+ #
+ # === Example
+ #
+ # 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>)
#
+ # === Options
+ #
# The declaration can also include an options hash to specialize the behavior of the association.
#
# Options are:
- # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but
# if the real class name is Person, you'll have to specify it with this option.
- # * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
+ # [:conditions]
+ # Specify the conditions that the associated object must meet in order to be included as a +WHERE+
# SQL fragment, such as <tt>rank = 5</tt>.
- # * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
+ # [:order]
+ # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
# such as <tt>last_name, first_name DESC</tt>.
- # * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
+ # [:dependent]
+ # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to <tt>:nullify</tt>, the associated
# object's foreign key is set to +NULL+. Also, association is assigned.
- # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
+ # [: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_one+ association will use "person_id"
# as the default <tt>:foreign_key</tt>.
- # * <tt>:primary_key</tt> - Specify the method that returns the primary key used for the association. By default this is +id+.
- # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
- # * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
- # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
+ # [:primary_key]
+ # Specify the method that returns the primary key used for the association. By default this is +id+.
+ # [:include]
+ # Specify second-order associations that should be eager loaded when this object is loaded.
+ # [:as]
+ # Specifies a polymorphic interface (See <tt>belongs_to</tt>).
+ # [:select]
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
- # * <tt>:through</tt>: Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
+ # [:through]
+ # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a
# <tt>has_one</tt> or <tt>belongs_to</tt> association on the join model.
- # * <tt>:source</tt> - Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be
+ # [: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>:source_type</tt> - Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
+ # [: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+.
- # * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
- # * <tt>:validate</tt> - If false, don't validate the associated object when saving the parent object. +false+ by default.
- # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
+ # [: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.
+ # [:accessible]
+ # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
#
# Option examples:
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
@@ -815,18 +896,34 @@ module ActiveRecord
end
end
- # Adds the following methods for retrieval and query for a single associated object for which this object holds an id:
- # +association+ is replaced with the symbol passed as the first argument, so
- # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
- # * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
- # * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, and sets it as the foreign key.
- # * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
- # * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
+ # Specifies a one-to-one association with another class. This method should only be used
+ # if this class contains the foreign key. If the other class contains the foreign key,
+ # then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview
+ # on when to use +has_one+ and when to use +belongs_to+.
+ #
+ # Methods will be added for retrieval and query for a single associated object, for which
+ # this object holds an id:
+ #
+ # [association(force_reload = false)]
+ # 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.
- # * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
- # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
+ # [create_association(attributes = {})]
+ # Returns a new object of the associated type that has been instantiated
+ # with +attributes+, linked to this object through a foreign key, and that
+ # has already been saved (if it passed the validation).
+ #
+ # (+association+ is replaced with the symbol passed as the first argument, so
+ # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.)
#
- # Example: A Post class declares <tt>belongs_to :author</tt>, which will add:
+ # === Example
+ #
+ # A Post class declares <tt>belongs_to :author</tt>, which will add:
# * <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>)
@@ -835,23 +932,30 @@ module ActiveRecord
# * <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.
#
- # Options are:
- # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
+ # === Options
+ #
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but
# if the real class name is Person, you'll have to specify it with this option.
- # * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
+ # [: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>.
- # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
+ # [:select]
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
- # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
+ # [:foreign_key]
+ # Specify the foreign key used for the association. By default this is guessed to be the name
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
# will use a foreign key of "favorite_person_id".
- # * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
+ # [:dependent]
+ # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
# <tt>belongs_to</tt> is used in conjunction with a <tt>has_many</tt> relationship on another class because of the potential to leave
# orphaned records behind.
- # * <tt>:counter_cache</tt> - Caches the number of belonging objects on the associate class through the use of +increment_counter+
+ # [:counter_cache]
+ # Caches the number of belonging objects on the associate class through the use of +increment_counter+
# and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's
# destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
# is used on the associate class (such as a Post class). You can also specify a custom counter cache column by providing
@@ -859,13 +963,18 @@ module ActiveRecord
# When creating a counter cache column, the database statement or migration must specify a default value of <tt>0</tt>, failing to do
# this results in a counter with +NULL+ value, which will never increment.
# Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+.
- # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
- # * <tt>:polymorphic</tt> - Specify this association is a polymorphic association by passing +true+.
+ # [:include]
+ # Specify second-order associations that should be eager loaded when this object is loaded.
+ # [:polymorphic]
+ # Specify this association is a polymorphic association by passing +true+.
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
- # * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
- # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default.
- # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
+ # [:readonly]
+ # 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.
+ # [:accessible]
+ # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
#
# Option examples:
# belongs_to :firm, :foreign_key => "client_of"
@@ -951,8 +1060,9 @@ module ActiveRecord
configure_dependency_for_belongs_to(reflection)
end
- # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
- # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project
+ # Specifies a many-to-many relationship with another class. This associates two classes via an
+ # intermediate join table. Unless the join table is explicitly specified as an option, it is
+ # guessed using the lexical order of the class names. So a join between Developer and Project
# will give the default join table name of "developers_projects" because "D" outranks "P". Note that this precedence
# is calculated using the <tt><</tt> operator for String. This means that if the strings are of different lengths,
# and the strings are equal when compared up to the shortest length, then the longer string is considered of higher
@@ -967,28 +1077,48 @@ module ActiveRecord
# associations with attributes to a real join model (see introduction).
#
# Adds the following methods for retrieval and query:
- # +collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
- # * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects.
+ #
+ # [collection(force_reload = false)]
+ # Returns an array of all the associated objects.
# An empty array is returned if none are found.
- # * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by creating associations in the join table
+ # [collection<<(object, ...)]
+ # Adds one or more objects to the collection by creating associations in the join table
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
- # * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by removing their associations from the join table.
+ # [collection.delete(object, ...)]
+ # Removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
- # * <tt>collection=objects</tt> - Replaces the collection's content by deleting and adding objects as appropriate.
- # * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids.
- # * <tt>collection_singular_ids=ids</tt> - Replace the collection by the objects identified by the primary keys in +ids+.
- # * <tt>collection.clear</tt> - Removes every object from the collection. This does not destroy the objects.
- # * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects.
- # * <tt>collection.size</tt> - Returns the number of associated objects.
- # * <tt>collection.find(id)</tt> - Finds an associated object responding to the +id+ and that
+ # [collection=objects]
+ # Replaces the collection's content by deleting and adding objects as appropriate.
+ # [collection_singular_ids]
+ # Returns an array of the associated objects' ids.
+ # [collection_singular_ids=ids]
+ # Replace the collection by the objects identified by the primary keys in +ids+.
+ # [collection.clear]
+ # Removes every object from the collection. This does not destroy the objects.
+ # [collection.empty?]
+ # Returns +true+ if there are no associated objects.
+ # [collection.size]
+ # Returns the number of associated objects.
+ # [collection.find(id)]
+ # Finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
- # * <tt>collection.build(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
+ # Uses the same rules as ActiveRecord::Base.find.
+ # [collection.exist?(...)]
+ # Checks whether an associated object with the given conditions exists.
+ # Uses the same rules as ActiveRecord::Base.exists?.
+ # [collection.build(attributes = {})]
+ # Returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
- # * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
+ # [collection.create(attributes = {})]
+ # Returns a new object of the collection type that has been instantiated
# with +attributes+, linked to this object through the join table, and that has already been saved (if it passed the validation).
#
- # Example: A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
+ # (+collection+ is replaced with the symbol passed as the first argument, so
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.)
+ #
+ # === Example
+ #
+ # A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
# * <tt>Developer#projects</tt>
# * <tt>Developer#projects<<</tt>
# * <tt>Developer#projects.delete</tt>
@@ -999,45 +1129,66 @@ module ActiveRecord
# * <tt>Developer#projects.empty?</tt>
# * <tt>Developer#projects.size</tt>
# * <tt>Developer#projects.find(id)</tt>
+ # * <tt>Developer#clients.exist?(...)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>)
# The declaration may include an options hash to specialize the behavior of the association.
#
- # Options are:
- # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
+ # === Options
+ #
+ # [:class_name]
+ # Specify the class name of the association. Use it only if that name can't be inferred
# from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the
# Project class, but if the real class name is SuperProject, you'll have to specify it with this option.
- # * <tt>:join_table</tt> - Specify the name of the join table if the default based on lexical order isn't what you want.
- # WARNING: If you're overwriting the table name of either class, the +table_name+ method MUST be declared underneath any
- # +has_and_belongs_to_many+ declaration in order to work.
- # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
+ # [:join_table]
+ # Specify the name of the join table if the default based on lexical order isn't what you want.
+ # <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method
+ # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work.
+ # [: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>.
- # * <tt>:association_foreign_key</tt> - Specify the association foreign key used for the association. By default this is
+ # [: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>.
- # * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
+ # [: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.
# <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
# or <tt>@blog.posts.build</tt>.
- # * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
+ # [:order]
+ # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
# such as <tt>last_name, first_name DESC</tt>
- # * <tt>:uniq</tt> - If true, duplicate associated objects will be ignored by accessors and query methods.
- # * <tt>:finder_sql</tt> - Overwrite the default generated SQL statement used to fetch the association with a manual statement
- # * <tt>:delete_sql</tt> - Overwrite the default generated SQL statement used to remove links between the associated
+ # [:uniq]
+ # If true, duplicate associated objects will be ignored by accessors and query methods.
+ # [:finder_sql]
+ # Overwrite the default generated SQL statement used to fetch the association with a manual statement
+ # [:delete_sql]
+ # Overwrite the default generated SQL statement used to remove links between the associated
# classes with a manual statement.
- # * <tt>:insert_sql</tt> - Overwrite the default generated SQL statement used to add links between the associated classes
+ # [:insert_sql]
+ # Overwrite the default generated SQL statement used to add links between the associated classes
# with a manual statement.
- # * <tt>:extend</tt> - Anonymous module for extending the proxy, see "Association extensions".
- # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded.
- # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
- # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
- # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
- # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
+ # [:extend]
+ # Anonymous module for extending the proxy, see "Association extensions".
+ # [:include]
+ # Specify second-order associations that should be eager loaded when the collection is loaded.
+ # [:group]
+ # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
+ # [:limit]
+ # An integer determining the limit on the number of rows that should be returned.
+ # [:offset]
+ # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
+ # [:select]
+ # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
- # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
- # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +true+ by default.
- # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
+ # [:readonly]
+ # 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.
+ # [:accessible<]
+ # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
#
# Option examples:
# has_and_belongs_to_many :projects
@@ -1528,19 +1679,19 @@ module ActiveRecord
else all << cond
end
end
- conditions.join(' ').scan(/([\.\w]+).?\./).flatten
+ conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten
end
def order_tables(options)
order = [options[:order], scope(:find, :order) ].join(", ")
return [] unless order && order.is_a?(String)
- order.scan(/([\.\w]+).?\./).flatten
+ order.scan(/([\.a-zA-Z_]+).?\./).flatten
end
def selects_tables(options)
select = options[:select]
return [] unless select && select.is_a?(String)
- select.scan(/"?([\.\w]+)"?.?\./).flatten
+ select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten
end
# Checks if the conditions reference a table other than the current model table
@@ -1742,6 +1893,7 @@ module ActiveRecord
collection.target.push(association)
when :has_one
return if record.id.to_s != join.parent.record_id(row).to_s
+ return if record.instance_variable_defined?("@#{join.reflection.name}")
association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
record.send("set_#{join.reflection.name}_target", association)
when :belongs_to
@@ -1823,7 +1975,7 @@ module ActiveRecord
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
end
- if reflection.macro == :has_many && reflection.options[:through]
+ if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
end
end
@@ -1847,7 +1999,7 @@ module ActiveRecord
]
when :has_many, :has_one
case
- when reflection.macro == :has_many && reflection.options[:through]
+ when reflection.options[:through]
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
@@ -1883,7 +2035,7 @@ module ActiveRecord
jt_sti_extra = " AND %s.%s = %s" % [
connection.quote_table_name(aliased_join_table_name),
connection.quote_column_name(through_reflection.active_record.inheritance_column),
- through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)]
+ through_reflection.klass.quote_value(through_reflection.klass.sti_name)]
end
when :belongs_to
first_key = primary_key
@@ -1948,10 +2100,8 @@ module ActiveRecord
else
""
end || ''
- join << %(AND %s.%s = %s ) % [
- connection.quote_table_name(aliased_table_name),
- connection.quote_column_name(klass.inheritance_column),
- klass.quote_value(klass.name.demodulize)] unless klass.descends_from_active_record?
+ join << %(AND %s) % [
+ klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record?
[through_reflection, reflection].each do |ref|
join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions]
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index a28be9eed1..9061037b39 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -344,7 +344,7 @@ module ActiveRecord
callback(:before_add, record)
yield(record) if block_given?
@target ||= [] unless loaded?
- @target << record
+ @target << record unless @reflection.options[:uniq] && @target.include?(record)
callback(:after_add, record)
record
end
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 77fc827e11..99b8748a48 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -131,10 +131,6 @@ module ActiveRecord
records.map { |record| record.quoted_id }.join(',')
end
- def interpolate_sql_options!(options, *keys)
- keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
- end
-
def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record)
end
@@ -217,7 +213,7 @@ module ActiveRecord
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
def flatten_deeper(array)
- array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
+ array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
end
def owner_quoted_id
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 7c28cbdd07..7c28cbdd07 100755..100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index d8146daa54..d8146daa54 100755..100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
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 d516d54151..e7e433b6b6 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
@@ -70,10 +70,8 @@ module ActiveRecord
end
def construct_sql
- interpolate_sql_options!(@reflection.options, :finder_sql)
-
if @reflection.options[:finder_sql]
- @finder_sql = @reflection.options[:finder_sql]
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
else
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
@finder_sql << " AND (#{conditions})" if conditions
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index e6fa15c173..ce62127505 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -35,8 +35,11 @@ module ActiveRecord
else
@reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
end
-
- @target = [] and loaded if count == 0
+
+ # If there's nothing in the database and @target has no new records
+ # we are certain the current target is an empty array. This is a
+ # documented side-effect of the method that may avoid an extra SELECT.
+ @target ||= [] and loaded if count == 0
if @reflection.options[:limit]
count = [ @reflection.options[:limit], count ].min
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 e1bfff5923..24b02efc35 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -237,7 +237,7 @@ module ActiveRecord
end
def build_sti_condition
- "#{@reflection.through_reflection.quoted_table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.sti_name)}"
+ @reflection.through_reflection.klass.send(:type_condition)
end
alias_method :sql_conditions, :conditions
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index fdc0fa52c9..18733255d2 100755..100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -21,8 +21,8 @@ module ActiveRecord
def replace(obj, dont_save = false)
load_target
- unless @target.nil?
- if dependent? && !dont_save && @target != obj
+ unless @target.nil? || @target == obj
+ if dependent? && !dont_save
@target.destroy unless @target.new_record?
@owner.clear_association_cache
else
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 962c2b36d9..5c30f80555 100755..100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -6,7 +6,7 @@ module ActiveRecord #:nodoc:
class ActiveRecordError < StandardError
end
- # Raised when the single-table inheritance mechanism failes to locate the subclass
+ # Raised when the single-table inheritance mechanism fails to locate the subclass
# (for example due to improper usage of column that +inheritance_column+ points to).
class SubclassNotFound < ActiveRecordError #:nodoc:
end
@@ -83,8 +83,33 @@ module ActiveRecord #:nodoc:
class ReadOnlyRecord < ActiveRecordError
end
- # Used by Active Record transaction mechanism to distinguish rollback from other exceptional situations.
- # You can use it to roll your transaction back explicitly in the block passed to +transaction+ method.
+ # ActiveRecord::Transactions::ClassMethods.transaction uses this exception
+ # to distinguish a deliberate rollback from other exceptional situations.
+ # Normally, raising an exception will cause the +transaction+ method to rollback
+ # the database transaction *and* pass on the exception. But if you raise an
+ # ActiveRecord::Rollback exception, then the database transaction will be rolled back,
+ # without passing on the exception.
+ #
+ # For example, you could do this in your controller to rollback a transaction:
+ #
+ # class BooksController < ActionController::Base
+ # def create
+ # Book.transaction do
+ # book = Book.new(params[:book])
+ # book.save!
+ # if today_is_friday?
+ # # The system must fail on Friday so that our support department
+ # # won't be out of job. We silently rollback this transaction
+ # # without telling the user.
+ # raise ActiveRecord::Rollback, "Call tech support!"
+ # end
+ # end
+ # # ActiveRecord::Rollback is the only exception that won't be passed on
+ # # by ActiveRecord::Base.transaction, so this line will still be reached
+ # # even on Friday.
+ # redirect_to root_url
+ # end
+ # end
class Rollback < ActiveRecordError
end
@@ -97,7 +122,11 @@ module ActiveRecord #:nodoc:
class MissingAttributeError < NoMethodError
end
- # Raised when an error occured while doing a mass assignment to an attribute through the
+ # Raised when unknown attributes are supplied via mass assignment.
+ class UnknownAttributeError < NoMethodError
+ end
+
+ # Raised when an error occurred while doing a mass assignment to an attribute through the
# <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the
# offending attribute.
class AttributeAssignmentError < ActiveRecordError
@@ -271,7 +300,7 @@ module ActiveRecord #:nodoc:
# # Now 'Bob' exist and is an 'admin'
# User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
#
- # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Protected attributes won't be setted unless they are given in a block. For example:
+ # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Protected attributes won't be set unless they are given in a block. For example:
#
# # No 'Winter' tag exists
# winter = Tag.find_or_initialize_by_name("Winter")
@@ -439,6 +468,10 @@ module ActiveRecord #:nodoc:
cattr_accessor :schema_format , :instance_writer => false
@@schema_format = :ruby
+ # Specify whether or not to use timestamps for migration numbers
+ cattr_accessor :timestamped_migrations , :instance_writer => false
+ @@timestamped_migrations = true
+
# Determine whether to store the full constant name including namespace when using STI
superclass_delegating_accessor :store_full_sti_class
self.store_full_sti_class = false
@@ -724,8 +757,7 @@ module ActiveRecord #:nodoc:
# ==== Attributes
#
# * +updates+ - A String of column and value pairs that will be set on any records that match conditions.
- # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ].
- # See conditions in the intro for more info.
+ # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info.
# * +options+ - Additional options are <tt>:limit</tt> and/or <tt>:order</tt>, see the examples for usage.
#
# ==== Examples
@@ -828,7 +860,7 @@ module ActiveRecord #:nodoc:
def update_counters(id, counters)
updates = counters.inject([]) { |list, (counter_name, increment)|
sign = increment < 0 ? "-" : "+"
- list << "#{connection.quote_column_name(counter_name)} = #{connection.quote_column_name(counter_name)} #{sign} #{increment.abs}"
+ list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}"
}.join(", ")
update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}")
end
@@ -1188,11 +1220,46 @@ module ActiveRecord #:nodoc:
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
end
+ def self_and_descendents_from_active_record#nodoc:
+ klass = self
+ classes = [klass]
+ while klass != klass.base_class
+ classes << klass = klass.superclass
+ end
+ classes
+ rescue
+ # OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column'
+ # Appearantly the method base_class causes some trouble.
+ # It now works for sure.
+ [self]
+ end
+
# Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example:
# Person.human_attribute_name("first_name") # => "First name"
- # Deprecated in favor of just calling "first_name".humanize
- def human_attribute_name(attribute_key_name) #:nodoc:
- attribute_key_name.humanize
+ # This used to be depricated in favor of humanize, but is now preferred, because it automatically uses the I18n
+ # 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|
+ :"#{klass.name.underscore}.#{attribute_key_name}"
+ end
+ defaults << options[:default] if options[:default]
+ defaults.flatten!
+ defaults << attribute_key_name.humanize
+ options[:count] ||= 1
+ I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes]))
+ end
+
+ # Transform the modelname into a more humane format, using I18n.
+ # Defaults to the basic humanize method.
+ # 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|
+ :"#{klass.name.underscore}"
+ end
+ defaults << self.name.humanize
+ I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options))
end
# True if this isn't a concrete subclass needing a STI type condition.
@@ -1287,8 +1354,8 @@ module ActiveRecord #:nodoc:
end
def respond_to?(method_id, include_private = false)
- if match = matches_dynamic_finder?(method_id) || matches_dynamic_finder_with_initialize_or_create?(method_id)
- return true if all_attributes_exists?(extract_attribute_names_from_match(match))
+ if match = DynamicFinderMatch.match(method_id)
+ return true if all_attributes_exists?(match.attribute_names)
end
super
end
@@ -1577,10 +1644,11 @@ module ActiveRecord #:nodoc:
sql << "WHERE #{merged_conditions} " unless merged_conditions.blank?
end
- def type_condition
+ def type_condition(table_alias=nil)
+ quoted_table_alias = self.connection.quote_table_name(table_alias || table_name)
quoted_inheritance_column = connection.quote_column_name(inheritance_column)
- type_condition = subclasses.inject("#{quoted_table_name}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
- condition << "OR #{quoted_table_name}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
+ type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
+ condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
end
" (#{type_condition}) "
@@ -1606,88 +1674,67 @@ module ActiveRecord #:nodoc:
# Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future
# attempts to use it do not run through method_missing.
def method_missing(method_id, *arguments)
- if match = matches_dynamic_finder?(method_id)
- finder = determine_finder(match)
-
- attribute_names = extract_attribute_names_from_match(match)
+ if match = DynamicFinderMatch.match(method_id)
+ attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
-
- self.class_eval %{
- def self.#{method_id}(*args)
- options = args.extract_options!
- attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
- finder_options = { :conditions => attributes }
- validate_find_options(options)
- set_readonly_option!(options)
-
- if options[:conditions]
- with_scope(:find => finder_options) do
- ActiveSupport::Deprecation.silence { send(:#{finder}, options) }
+ if match.finder?
+ finder = match.finder
+ bang = match.bang?
+ self.class_eval %{
+ def self.#{method_id}(*args)
+ options = args.extract_options!
+ attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ finder_options = { :conditions => attributes }
+ validate_find_options(options)
+ set_readonly_option!(options)
+
+ #{'result = ' if bang}if options[:conditions]
+ with_scope(:find => finder_options) do
+ ActiveSupport::Deprecation.silence { send(:#{finder}, options) }
+ end
+ else
+ ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) }
end
- else
- ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) }
- end
- end
- }, __FILE__, __LINE__
- send(method_id, *arguments)
- elsif match = matches_dynamic_finder_with_initialize_or_create?(method_id)
- instantiator = determine_instantiator(match)
- attribute_names = extract_attribute_names_from_match(match)
- super unless all_attributes_exists?(attribute_names)
-
- self.class_eval %{
- def self.#{method_id}(*args)
- guard_protected_attributes = false
-
- if args[0].is_a?(Hash)
- guard_protected_attributes = true
- attributes = args[0].with_indifferent_access
- find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
- else
- find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ #{'result || raise(RecordNotFound)' if bang}
end
+ }, __FILE__, __LINE__
+ send(method_id, *arguments)
+ elsif match.instantiator?
+ instantiator = match.instantiator
+ self.class_eval %{
+ def self.#{method_id}(*args)
+ guard_protected_attributes = false
+
+ if args[0].is_a?(Hash)
+ guard_protected_attributes = true
+ attributes = args[0].with_indifferent_access
+ find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}])
+ else
+ find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args)
+ end
- options = { :conditions => find_attributes }
- set_readonly_option!(options)
+ options = { :conditions => find_attributes }
+ set_readonly_option!(options)
- record = find_initial(options)
+ record = find_initial(options)
- if record.nil?
- record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
- #{'yield(record) if block_given?'}
- #{'record.save' if instantiator == :create}
- record
- else
- record
+ if record.nil?
+ record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
+ #{'yield(record) if block_given?'}
+ #{'record.save' if instantiator == :create}
+ record
+ else
+ record
+ end
end
- end
- }, __FILE__, __LINE__
- send(method_id, *arguments)
+ }, __FILE__, __LINE__
+ send(method_id, *arguments)
+ end
else
super
end
end
- def matches_dynamic_finder?(method_id)
- /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s)
- end
-
- def matches_dynamic_finder_with_initialize_or_create?(method_id)
- /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s)
- end
-
- def determine_finder(match)
- match.captures.first == 'all_by' ? :find_every : :find_initial
- end
-
- def determine_instantiator(match)
- match.captures.first == 'initialize' ? :new : :create
- end
-
- def extract_attribute_names_from_match(match)
- match.captures.last.split('_and_')
- end
-
def construct_attributes_from_arguments(attribute_names, arguments)
attributes = {}
attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] }
@@ -1717,7 +1764,7 @@ module ActiveRecord #:nodoc:
def attribute_condition(argument)
case argument
when nil then "IS ?"
- when Array, ActiveRecord::Associations::AssociationCollection then "IN (?)"
+ when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)"
when Range then "BETWEEN ? AND ?"
else "= ?"
end
@@ -2372,7 +2419,11 @@ module ActiveRecord #:nodoc:
attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes
attributes.each do |k, v|
- k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v)
+ if k.include?("(")
+ multi_parameter_attributes << [ k, v ]
+ else
+ respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
+ end
end
assign_multiparameter_attributes(multi_parameter_attributes)
@@ -2535,11 +2586,14 @@ module ActiveRecord #:nodoc:
end
def convert_number_column_value(value)
- case value
- when FalseClass; 0
- when TrueClass; 1
- when ''; nil
- else value
+ if value == false
+ 0
+ elsif value == true
+ 1
+ elsif value.is_a?(String) && value.blank?
+ nil
+ else
+ value
end
end
@@ -2558,7 +2612,7 @@ module ActiveRecord #:nodoc:
removed_attributes = attributes.keys - safe_attributes.keys
if removed_attributes.any?
- logger.debug "WARNING: Can't mass-assign these protected attributes: #{removed_attributes.join(', ')}"
+ log_protected_attribute_removal(removed_attributes)
end
safe_attributes
@@ -2573,6 +2627,10 @@ module ActiveRecord #:nodoc:
end
end
+ def log_protected_attribute_removal(*attributes)
+ logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}"
+ end
+
# The primary key and inheritance column can never be set by mass-assignment for security reasons.
def attributes_protected_by_default
default = [ self.class.primary_key, self.class.inheritance_column ]
@@ -2586,8 +2644,15 @@ module ActiveRecord #:nodoc:
quoted = {}
connection = self.class.connection
attribute_names.each do |name|
- if column = column_for_attribute(name)
- quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && column.primary
+ if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
+ value = read_attribute(name)
+
+ # We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML.
+ if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))
+ value = value.to_yaml
+ end
+
+ quoted[name] = connection.quote(value, column)
end
end
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index 2ca1a0aaa3..246f87b7a9 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -211,7 +211,7 @@ module ActiveRecord
sql << " ORDER BY #{options[:order]} " if options[:order]
add_limit!(sql, options, scope)
- sql << ')' if use_workaround
+ sql << ') AS #{aggregate_alias}_subquery' if use_workaround
sql
end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 1e385fb128..eec531c514 100755..100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -50,7 +50,7 @@ module ActiveRecord
#
# == Inheritable callback queues
#
- # Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros.
+ # Besides the overwritable callback methods, it's also possible to register callbacks through the use of the callback macros.
# Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance
# hierarchy. Example:
#
@@ -169,6 +169,18 @@ module ActiveRecord
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
# defined as methods on the model, which are called last.
+ #
+ # == Transactions
+ #
+ # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
+ # within a transaction. That includes <tt>after_*</tt> hooks. If everything
+ # goes fine a COMMIT is executed once the chain has been completed.
+ #
+ # If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
+ # can also trigger a ROLLBACK raising an exception in any of the callbacks,
+ # including <tt>after_*</tt> hooks. Note, however, that in that case the client
+ # needs to be aware of it because an ordinary +save+ will raise such exception
+ # instead of quietly returning +false+.
module Callbacks
CALLBACKS = %w(
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 5358491cde..aaf9e2e73f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -149,6 +149,10 @@ module ActiveRecord
"INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)"
end
+ def case_sensitive_equality_operator
+ "="
+ end
+
protected
# Returns an array of record hashes with the column names as keys and
# column values as values.
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 31d6c7942c..75032efe57 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -138,7 +138,11 @@ module ActiveRecord
# convert something to a boolean
def value_to_boolean(value)
- TRUE_VALUES.include?(value)
+ if value.is_a?(String) && value.blank?
+ nil
+ else
+ TRUE_VALUES.include?(value)
+ end
end
# convert something to a BigDecimal
@@ -443,9 +447,10 @@ module ActiveRecord
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <tt>:updated_at</tt> to the table.
- def timestamps
- column(:created_at, :datetime)
- column(:updated_at, :datetime)
+ def timestamps(*args)
+ options = args.extract_options!
+ column(:created_at, :datetime, options)
+ column(:updated_at, :datetime, options)
end
def references(*args)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 7d8530ebef..bececf82a0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -383,13 +383,9 @@ module ActiveRecord
def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
- # must explcitly check for :null to allow change_column to work on migrations
- if options.has_key? :null
- if options[:null] == false
- sql << " NOT NULL"
- else
- sql << " NULL"
- end
+ # must explicitly check for :null to allow change_column to work on migrations
+ if options[:null] == false
+ sql << " NOT NULL"
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index f48b107a2a..6924bb7e6f 100755..100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -51,6 +51,13 @@ module ActiveRecord
true
end
+ # Does this adapter support DDL rollbacks in transactions? That is, would
+ # CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
+ # SQL Server, and others support this. MySQL and others do not.
+ def supports_ddl_transactions?
+ false
+ end
+
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
@@ -118,6 +125,19 @@ module ActiveRecord
@connection
end
+ def open_transactions
+ @open_transactions ||= 0
+ end
+
+ def increment_open_transactions
+ @open_transactions ||= 0
+ @open_transactions += 1
+ end
+
+ def decrement_open_transactions
+ @open_transactions -= 1
+ end
+
def log_info(sql, name, runtime)
if @logger && @logger.debug?
name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})"
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 35b9ed4746..204ebaa2e2 100755..100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -511,6 +511,10 @@ module ActiveRecord
keys.length == 1 ? [keys.first, nil] : nil
end
+ def case_sensitive_equality_operator
+ "= BINARY"
+ end
+
private
def connect
@connection.reconnect = true if @connection.respond_to?(:reconnect=)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 6d16d72dea..55c7da5b4f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -23,8 +23,8 @@ module ActiveRecord
config = config.symbolize_keys
host = config[:host]
port = config[:port] || 5432
- username = config[:username].to_s
- password = config[:password].to_s
+ username = config[:username].to_s if config[:username]
+ password = config[:password].to_s if config[:password]
if config.has_key?(:database)
database = config[:database]
@@ -182,8 +182,8 @@ module ActiveRecord
def self.extract_value_from_default(default)
case default
# Numeric types
- when /\A-?\d+(\.\d*)?\z/
- default
+ when /\A\(?(-?\d+(\.\d*)?\)?)\z/
+ $1
# Character types
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
$1
@@ -335,6 +335,10 @@ module ActiveRecord
postgresql_version >= 80200
end
+ def supports_ddl_transactions?
+ true
+ end
+
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
def table_alias_length
@@ -534,13 +538,13 @@ module ActiveRecord
option_string = options.symbolize_keys.sum do |key, value|
case key
when :owner
- " OWNER = '#{value}'"
+ " OWNER = \"#{value}\""
when :template
- " TEMPLATE = #{value}"
+ " TEMPLATE = \"#{value}\""
when :encoding
" ENCODING = '#{value}'"
when :tablespace
- " TABLESPACE = #{value}"
+ " TABLESPACE = \"#{value}\""
when :connection_limit
" CONNECTION LIMIT = #{value}"
else
@@ -761,7 +765,8 @@ module ActiveRecord
begin
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- rescue ActiveRecord::StatementInvalid
+ rescue ActiveRecord::StatementInvalid => e
+ raise e if postgresql_version > 80000
# This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
begin
begin_db_transaction
@@ -867,7 +872,7 @@ module ActiveRecord
end
private
- # The internal PostgreSQL identifer of the money data type.
+ # The internal PostgreSQL identifier of the money data type.
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
# Connects to a PostgreSQL server and sets up the adapter depending on the
diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb
index a7d767486c..63bf8c8f5b 100644
--- a/activerecord/lib/active_record/dirty.rb
+++ b/activerecord/lib/active_record/dirty.rb
@@ -62,7 +62,7 @@ module ActiveRecord
changed_attributes.keys
end
- # Map of changed attrs => [original value, new value]
+ # Map of changed attrs => [original value, new value].
# person.changes # => {}
# person.name = 'bob'
# person.changes # => { 'name' => ['bill', 'bob'] }
@@ -93,27 +93,27 @@ module ActiveRecord
end
private
- # Map of change attr => original value.
+ # Map of change <tt>attr => original value</tt>.
def changed_attributes
@changed_attributes ||= {}
end
- # Handle *_changed? for method_missing.
+ # Handle <tt>*_changed?</tt> for +method_missing+.
def attribute_changed?(attr)
changed_attributes.include?(attr)
end
- # Handle *_change for method_missing.
+ # Handle <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
end
- # Handle *_was for method_missing.
+ # Handle <tt>*_was</tt> for +method_missing+.
def attribute_was(attr)
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
- # Handle *_will_change! for method_missing.
+ # Handle <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
end
@@ -134,7 +134,9 @@ module ActiveRecord
def update_with_dirty
if partial_updates?
- update_without_dirty(changed)
+ # Serialized attributes should always be written in case they've been
+ # changed in place.
+ update_without_dirty(changed | self.class.serialized_attributes.keys)
else
update_without_dirty
end
diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb
new file mode 100644
index 0000000000..b105b919f5
--- /dev/null
+++ b/activerecord/lib/active_record/dynamic_finder_match.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ class DynamicFinderMatch
+ def self.match(method)
+ df_match = self.new(method)
+ df_match.finder ? df_match : nil
+ end
+
+ def initialize(method)
+ @finder = :find_initial
+ case method.to_s
+ when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
+ @finder = :find_every if $1 == 'all_by'
+ names = $2
+ when /^find_by_([_a-zA-Z]\w*)\!$/
+ @bang = true
+ names = $1
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
+ @instantiator = $1 == 'initialize' ? :new : :create
+ names = $2
+ else
+ @finder = nil
+ end
+ @attribute_names = names && names.split('_and_')
+ end
+
+ attr_reader :finder, :attribute_names, :instantiator
+
+ def finder?
+ !@finder.nil? && @instantiator.nil?
+ end
+
+ def instantiator?
+ @finder == :find_initial && !@instantiator.nil?
+ end
+
+ def bang?
+ @bang
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 17fb9355c4..622cfc3c3f 100755..100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -515,7 +515,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
all_loaded_fixtures.update(fixtures_map)
- connection.transaction(Thread.current['open_transactions'].to_i == 0) do
+ connection.transaction(connection.open_transactions.zero?) do
fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
fixtures.each { |fixture| fixture.insert_fixtures }
@@ -930,7 +930,7 @@ module Test #:nodoc:
load_fixtures
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
- ActiveRecord::Base.send :increment_open_transactions
+ ActiveRecord::Base.connection.increment_open_transactions
ActiveRecord::Base.connection.begin_db_transaction
# Load fixtures for every test.
else
@@ -951,9 +951,9 @@ module Test #:nodoc:
end
# Rollback changes if a transaction is active.
- if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
+ if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0
ActiveRecord::Base.connection.rollback_db_transaction
- Thread.current['open_transactions'] = 0
+ ActiveRecord::Base.connection.decrement_open_transactions
end
ActiveRecord::Base.verify_active_connections!
end
diff --git a/activerecord/lib/active_record/locale/en-US.yml b/activerecord/lib/active_record/locale/en-US.yml
new file mode 100644
index 0000000000..8148f31a81
--- /dev/null
+++ b/activerecord/lib/active_record/locale/en-US.yml
@@ -0,0 +1,33 @@
+en-US:
+ activerecord:
+ errors:
+ # The values :model, :attribute and :value are always available for interpolation
+ # The value :count is available when applicable. Can be used for pluralization.
+ messages:
+ inclusion: "is not included in the list"
+ exclusion: "is reserved"
+ invalid: "is invalid"
+ confirmation: "doesn't match confirmation"
+ accepted: "must be accepted"
+ empty: "can't be empty"
+ blank: "can't be blank"
+ too_long: "is too long (maximum is {{count}} characters)"
+ too_short: "is too short (minimum is {{count}} characters)"
+ wrong_length: "is the wrong length (should be {{count}} characters)"
+ taken: "has already been taken"
+ not_a_number: "is not a number"
+ greater_than: "must be greater than {{count}}"
+ greater_than_or_equal_to: "must be greater than or equal to {{count}}"
+ equal_to: "must be equal to {{count}}"
+ less_than: "must be less than {{count}}"
+ less_than_or_equal_to: "must be less than or equal to {{count}}"
+ odd: "must be odd"
+ even: "must be even"
+ # Append your own errors here or at the model/attributes scope.
+
+ models:
+ # Overrides default messages
+
+ attributes:
+ # Overrides model and default messages.
+
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index e095b3c766..1d843fff28 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -238,6 +238,22 @@ module ActiveRecord
# lower than the current schema version: when migrating up, those
# never-applied "interleaved" migrations will be automatically applied, and
# when migrating down, never-applied "interleaved" migrations will be skipped.
+ #
+ # == Timestamped Migrations
+ #
+ # By default, Rails generates migrations that look like:
+ #
+ # 20080717013526_your_migration_name.rb
+ #
+ # The prefix is a generation timestamp (in UTC).
+ #
+ # If you'd prefer to use numeric prefixes, you can turn timestamped migrations
+ # off by setting:
+ #
+ # config.active_record.timestamped_migrations = false
+ #
+ # In environment.rb.
+ #
class Migration
@@verbose = true
cattr_accessor :verbose
@@ -333,6 +349,27 @@ module ActiveRecord
end
end
+ # MigrationProxy is used to defer loading of the actual migration classes
+ # until they are needed
+ class MigrationProxy
+
+ attr_accessor :name, :version, :filename
+
+ delegate :migrate, :announce, :write, :to=>:migration
+
+ private
+
+ def migration
+ @migration ||= load_migration
+ end
+
+ def load_migration
+ load(filename)
+ name.constantize
+ end
+
+ end
+
class Migrator#:nodoc:
class << self
def migrate(migrations_path, target_version = nil)
@@ -369,11 +406,17 @@ module ActiveRecord
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
end
+ def get_all_versions
+ Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort
+ end
+
def current_version
- version = Base.connection.select_values(
- "SELECT version FROM #{schema_migrations_table_name}"
- ).map(&:to_i).max rescue nil
- version || 0
+ sm_table = schema_migrations_table_name
+ if Base.connection.table_exists?(sm_table)
+ get_all_versions.max || 0
+ else
+ 0
+ end
end
def proper_table_name(name)
@@ -389,7 +432,7 @@ module ActiveRecord
end
def current_version
- self.class.current_version
+ migrated.last || 0
end
def current_migration
@@ -421,17 +464,25 @@ module ActiveRecord
runnable.pop if down? && !target.nil?
runnable.each do |migration|
- Base.logger.info "Migrating to #{migration} (#{migration.version})"
+ Base.logger.info "Migrating to #{migration.name} (#{migration.version})"
# On our way up, we skip migrating the ones we've already migrated
- # On our way down, we skip reverting the ones we've never migrated
next if up? && migrated.include?(migration.version.to_i)
+ # On our way down, we skip reverting the ones we've never migrated
if down? && !migrated.include?(migration.version.to_i)
migration.announce 'never migrated, skipping'; migration.write
- else
- migration.migrate(@direction)
- record_version_state_after_migrating(migration.version)
+ next
+ end
+
+ begin
+ ddl_transaction do
+ migration.migrate(@direction)
+ record_version_state_after_migrating(migration.version)
+ end
+ rescue => e
+ canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
+ raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
end
end
end
@@ -454,11 +505,10 @@ module ActiveRecord
raise DuplicateMigrationNameError.new(name.camelize)
end
- load(file)
-
- klasses << returning(name.camelize.constantize) do |klass|
- class << klass; attr_accessor :version end
- klass.version = version
+ klasses << returning(MigrationProxy.new) do |migration|
+ migration.name = name.camelize
+ migration.version = version
+ migration.filename = file
end
end
@@ -473,17 +523,19 @@ module ActiveRecord
end
def migrated
- sm_table = self.class.schema_migrations_table_name
- Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort
+ @migrated_versions ||= self.class.get_all_versions
end
private
def record_version_state_after_migrating(version)
sm_table = self.class.schema_migrations_table_name
+ @migrated_versions ||= []
if down?
+ @migrated_versions.delete(version.to_i)
Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
else
+ @migrated_versions.push(version.to_i).sort!
Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
end
end
@@ -495,5 +547,14 @@ module ActiveRecord
def down?
@direction == :down
end
+
+ # Wrap the migration in a transaction only if supported by the adapter.
+ def ddl_transaction(&block)
+ if Base.connection.supports_ddl_transactions?
+ Base.transaction { block.call }
+ else
+ block.call
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index 080e3d0f5e..c99c4beca9 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -103,7 +103,7 @@ module ActiveRecord
attr_reader :proxy_scope, :proxy_options
[].methods.each do |m|
- unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/
+ unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?|respond_to?)/
delegate m, :to => :proxy_found
end
end
@@ -140,6 +140,18 @@ module ActiveRecord
@found ? @found.empty? : count.zero?
end
+ def respond_to?(method, include_private = false)
+ super || @proxy_scope.respond_to?(method, include_private)
+ end
+
+ def any?
+ if block_given?
+ proxy_found.any? { |*block_args| yield(*block_args) }
+ else
+ !empty?
+ end
+ end
+
protected
def proxy_found
@found || load_found
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
index 25e0e61c69..b35e407cc1 100644
--- a/activerecord/lib/active_record/observer.rb
+++ b/activerecord/lib/active_record/observer.rb
@@ -20,7 +20,7 @@ module ActiveRecord
# ActiveRecord::Base.observers = Cacher, GarbageCollector
#
# Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is
- # called during startup, and before each development request.
+ # called during startup, and before each development request.
def observers=(*observers)
@observers = observers.flatten
end
@@ -130,11 +130,11 @@ module ActiveRecord
# Observers register themselves in the model class they observe, since it is the class that
# notifies them of events when they occur. As a side-effect, when an observer is loaded its
# corresponding model class is loaded.
- #
+ #
# Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
- # application initializers. Now observers are loaded after application initializers,
+ # application initializers. Now observers are loaded after application initializers,
# so observed models can make use of extensions.
- #
+ #
# If by any chance you are using observed models in the initialization you can still
# load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
# singletons and that call instantiates and registers them.
@@ -189,7 +189,9 @@ module ActiveRecord
def add_observer!(klass)
klass.add_observer(self)
- klass.class_eval 'def after_find() end' unless klass.method_defined?(:after_find)
+ if respond_to?(:after_find) && !klass.method_defined?(:after_find)
+ klass.class_eval 'def after_find() end'
+ end
end
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 3f74c03714..935b1939d8 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -109,7 +109,7 @@ module ActiveRecord
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
# and +other_aggregation+ has an options hash assigned to it.
def ==(other_aggregation)
- name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
+ other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
end
def sanitized_conditions #:nodoc:
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
index ca5591ae35..ffaa41282f 100644
--- a/activerecord/lib/active_record/test_case.rb
+++ b/activerecord/lib/active_record/test_case.rb
@@ -37,7 +37,7 @@ module ActiveRecord
$queries_executed = []
yield
ensure
- assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
+ 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
def assert_no_queries(&block)
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 3b6835762c..81462a2917 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -66,32 +66,24 @@ module ActiveRecord
# will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
# depends on or you can raise exceptions in the callbacks to rollback.
#
- # == Exception handling
+ # == Exception handling and rolling back
#
# Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
- # should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will
- # trigger a ROLLBACK when raised, but not be re-raised by the transaction block.
+ # should be ready to catch those in your application code.
+ #
+ # One exception is the ActiveRecord::Rollback exception, which will trigger a ROLLBACK when raised,
+ # but not be re-raised by the transaction block.
module ClassMethods
+ # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
def transaction(&block)
- increment_open_transactions
+ connection.increment_open_transactions
begin
- connection.transaction(Thread.current['start_db_transaction'], &block)
+ connection.transaction(connection.open_transactions == 1, &block)
ensure
- decrement_open_transactions
+ connection.decrement_open_transactions
end
end
-
- private
- def increment_open_transactions #:nodoc:
- open = Thread.current['open_transactions'] ||= 0
- Thread.current['start_db_transaction'] = open.zero?
- Thread.current['open_transactions'] = open + 1
- end
-
- def decrement_open_transactions #:nodoc:
- Thread.current['open_transactions'] -= 1
- end
end
def transaction(&block)
@@ -99,11 +91,11 @@ module ActiveRecord
end
def destroy_with_transactions #:nodoc:
- transaction { destroy_without_transactions }
+ with_transaction_returning_status(:destroy_without_transactions)
end
def save_with_transactions(perform_validation = true) #:nodoc:
- rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
+ rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
end
def save_with_transactions! #:nodoc:
@@ -126,5 +118,17 @@ module ActiveRecord
end
raise
end
+
+ # Executes +method+ within a transaction and captures its return value as a
+ # status flag. If the status is true the transaction is committed, otherwise
+ # a ROLLBACK is issued. In any case the status flag is returned.
+ def with_transaction_returning_status(method, *args)
+ status = nil
+ transaction do
+ status = send(method, *args)
+ raise ActiveRecord::Rollback unless status
+ end
+ status
+ end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 1035308aa5..8fe4336bbc 100755..100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # Raised by save! and create! when the record is invalid. Use the
+ # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the
# +record+ method to retrieve the record which did not validate.
# begin
# complex_operation_that_calls_save!_internally
@@ -18,69 +18,95 @@ module ActiveRecord
# determine whether the object is in a valid state to be saved. See usage example in Validations.
class Errors
include Enumerable
+
+ class << self
+ def default_error_messages
+ ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages').")
+ I18n.translate 'activerecord.errors.messages'
+ end
+ end
def initialize(base) # :nodoc:
@base, @errors = base, {}
end
- @@default_error_messages = {
- :inclusion => "is not included in the list",
- :exclusion => "is reserved",
- :invalid => "is invalid",
- :confirmation => "doesn't match confirmation",
- :accepted => "must be accepted",
- :empty => "can't be empty",
- :blank => "can't be blank",
- :too_long => "is too long (maximum is %d characters)",
- :too_short => "is too short (minimum is %d characters)",
- :wrong_length => "is the wrong length (should be %d characters)",
- :taken => "has already been taken",
- :not_a_number => "is not a number",
- :greater_than => "must be greater than %d",
- :greater_than_or_equal_to => "must be greater than or equal to %d",
- :equal_to => "must be equal to %d",
- :less_than => "must be less than %d",
- :less_than_or_equal_to => "must be less than or equal to %d",
- :odd => "must be odd",
- :even => "must be even"
- }
-
- # Holds a hash with all the default error messages that can be replaced by your own copy or localizations.
- cattr_accessor :default_error_messages
-
-
# Adds an error to the base object instead of any particular attribute. This is used
# to report errors that don't tie to any specific attribute, but rather to the object
# as a whole. These error messages don't get prepended with any field name when iterating
- # with each_full, so they should be complete sentences.
+ # with +each_full+, so they should be complete sentences.
def add_to_base(msg)
add(:base, msg)
end
- # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
+ # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
# for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
- # If no +msg+ is supplied, "invalid" is assumed.
- def add(attribute, msg = @@default_error_messages[:invalid])
- @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
- @errors[attribute.to_s] << msg
+ # If no +messsage+ is supplied, :invalid is assumed.
+ # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error).
+ def add(attribute, message = nil, options = {})
+ message ||= :invalid
+ message = generate_message(attribute, message, options) if message.is_a?(Symbol)
+ @errors[attribute.to_s] ||= []
+ @errors[attribute.to_s] << message
end
# Will add an error message to each of the attributes in +attributes+ that is empty.
- def add_on_empty(attributes, msg = @@default_error_messages[:empty])
+ def add_on_empty(attributes, custom_message = nil)
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
- is_empty = value.respond_to?("empty?") ? value.empty? : false
- add(attr, msg) unless !value.nil? && !is_empty
+ is_empty = value.respond_to?("empty?") ? value.empty? : false
+ add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty
end
end
# Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
- def add_on_blank(attributes, msg = @@default_error_messages[:blank])
+ def add_on_blank(attributes, custom_message = nil)
for attr in [attributes].flatten
value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
- add(attr, msg) if value.blank?
+ add(attr, :blank, :default => custom_message) if value.blank?
+ end
+ end
+
+ # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>).
+ # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there,
+ # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the
+ # default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name,
+ # translated attribute name and the value are available for interpolation.
+ #
+ # When using inheritence in your models, it will check all the inherited models too, but only if the model itself
+ # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt>
+ # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations:
+ #
+ # <ol>
+ # <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li>
+ # <li><tt>activerecord.errors.models.admin.blank</tt></li>
+ # <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li>
+ # <li><tt>activerecord.errors.models.user.blank</tt></li>
+ # <li><tt>activerecord.errors.messages.blank</tt></li>
+ # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li>
+ # </ol>
+ def generate_message(attribute, message = :invalid, options = {})
+
+ defaults = @base.class.self_and_descendents_from_active_record.map do |klass|
+ [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}",
+ :"models.#{klass.name.underscore}.#{message}" ]
end
+
+ defaults << options.delete(:default)
+ defaults = defaults.compact.flatten << :"messages.#{message}"
+
+ model_name = @base.class.name
+ key = defaults.shift
+ value = @base.respond_to?(attribute) ? @base.send(attribute) : nil
+
+ options = { :default => defaults,
+ :model => @base.class.human_name,
+ :attribute => @base.class.human_attribute_name(attribute.to_s),
+ :value => value,
+ :scope => [:activerecord, :errors]
+ }.merge(options)
+
+ I18n.translate(key, options)
end
# Returns true if the specified +attribute+ has errors associated with it.
@@ -97,7 +123,7 @@ module ActiveRecord
!@errors[attribute.to_s].nil?
end
- # Returns nil, if no errors are associated with the specified +attribute+.
+ # Returns +nil+, if no errors are associated with the specified +attribute+.
# Returns the error message, if one error is associated with the specified +attribute+.
# Returns an array of error messages, if more than one error is associated with the specified +attribute+.
#
@@ -118,7 +144,7 @@ module ActiveRecord
alias :[] :on
- # Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute).
+ # Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>.
def on_base
on(:base)
end
@@ -131,15 +157,15 @@ module ActiveRecord
# end
#
# company = Company.create(:address => '123 First St.')
- # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # =>
- # name - is too short (minimum is 5 characters)
- # name - can't be blank
- # address - can't be blank
+ # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" }
+ # # => name - is too short (minimum is 5 characters)
+ # # name - can't be blank
+ # # address - can't be blank
def each
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
end
- # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
+ # Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned
# through iteration as "First name can't be empty".
#
# class Company < ActiveRecord::Base
@@ -148,10 +174,10 @@ module ActiveRecord
# end
#
# company = Company.create(:address => '123 First St.')
- # company.errors.each_full{|msg| puts msg } # =>
- # Name is too short (minimum is 5 characters)
- # Name can't be blank
- # Address can't be blank
+ # company.errors.each_full{|msg| puts msg }
+ # # => Name is too short (minimum is 5 characters)
+ # # Name can't be blank
+ # # Address can't be blank
def each_full
full_messages.each { |msg| yield msg }
end
@@ -166,22 +192,24 @@ module ActiveRecord
# company = Company.create(:address => '123 First St.')
# company.errors.full_messages # =>
# ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"]
- def full_messages
+ def full_messages(options = {})
full_messages = []
@errors.each_key do |attr|
- @errors[attr].each do |msg|
- next if msg.nil?
+ @errors[attr].each do |message|
+ next unless message
if attr == "base"
- full_messages << msg
+ full_messages << message
else
- full_messages << @base.class.human_attribute_name(attr) + " " + msg
+ #key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}"
+ attr_name = @base.class.human_attribute_name(attr)
+ full_messages << attr_name + ' ' + message
end
end
end
full_messages
- end
+ end
# Returns true if no errors have been added.
def empty?
@@ -209,13 +237,13 @@ module ActiveRecord
# end
#
# company = Company.create(:address => '123 First St.')
- # company.errors.to_xml # =>
- # <?xml version="1.0" encoding="UTF-8"?>
- # <errors>
- # <error>Name is too short (minimum is 5 characters)</error>
- # <error>Name can't be blank</error>
- # <error>Address can't be blank</error>
- # </errors>
+ # company.errors.to_xml
+ # # => <?xml version="1.0" encoding="UTF-8"?>
+ # # <errors>
+ # # <error>Name is too short (minimum is 5 characters)</error>
+ # # <error>Name can't be blank</error>
+ # # <error>Address can't be blank</error>
+ # # </errors>
def to_xml(options={})
options[:root] ||= "errors"
options[:indent] ||= 2
@@ -226,6 +254,7 @@ module ActiveRecord
full_messages.each { |msg| e.error(msg) }
end
end
+
end
@@ -261,7 +290,7 @@ module ActiveRecord
# person.errors.on "phone_number" # => "has invalid format"
# person.errors.each_full { |msg| puts msg }
# # => "Last name can't be empty\n" +
- # "Phone number has invalid format"
+ # # "Phone number has invalid format"
#
# person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
# person.save # => true (and person is now saved in the database)
@@ -300,7 +329,7 @@ module ActiveRecord
:odd => 'odd?', :even => 'even?' }.freeze
# Adds a validation method or block to the class. This is useful when
- # overriding the +validate+ instance method becomes too unwieldly and
+ # overriding the +validate+ instance method becomes too unwieldy and
# you're looking for more descriptive declaration of your validations.
#
# This can be done with a symbol pointing to a method:
@@ -388,13 +417,15 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_confirmation_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
+ unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
+ record.errors.add(attr_name, :confirmation, :default => configuration[:message])
+ end
end
end
@@ -422,7 +453,7 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_acceptance_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" }
+ configuration = { :on => :save, :allow_nil => true, :accept => "1" }
configuration.update(attr_names.extract_options!)
db_cols = begin
@@ -434,7 +465,9 @@ module ActiveRecord
attr_accessor(*names)
validates_each(attr_names,configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept]
+ unless value == configuration[:accept]
+ record.errors.add(attr_name, :accepted, :default => configuration[:message])
+ end
end
end
@@ -461,7 +494,7 @@ module ActiveRecord
# method, proc or string should return or evaluate to a true or false value.
#
def validates_presence_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
# can't use validates_each here, because it cannot cope with nonexistent attributes,
@@ -509,10 +542,7 @@ module ActiveRecord
def validates_length_of(*attrs)
# Merge given options with defaults.
options = {
- :too_long => ActiveRecord::Errors.default_error_messages[:too_long],
- :too_short => ActiveRecord::Errors.default_error_messages[:too_short],
- :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length],
- :tokenizer => lambda {|value| value.split(//)}
+ :tokenizer => lambda {|value| value.split(//)}
}.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.extract_options!.symbolize_keys)
@@ -535,15 +565,12 @@ module ActiveRecord
when :within, :in
raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
- too_short = options[:too_short] % option_value.begin
- too_long = options[:too_long] % option_value.end
-
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)
+ record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
elsif value.size > option_value.end
- record.errors.add(attr, too_long)
+ record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end)
end
end
when :is, :minimum, :maximum
@@ -553,11 +580,13 @@ module ActiveRecord
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
- message = (options[:message] || options[message_options[option]]) % option_value
-
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
- record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
+ 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
end
end
@@ -599,7 +628,7 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_uniqueness_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
+ configuration = { :case_sensitive => true }
configuration.update(attr_names.extract_options!)
validates_each(attr_names,configuration) do |record, attr_name, value|
@@ -617,13 +646,24 @@ module ActiveRecord
# class (which has a database table to query from).
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
- if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?)
- condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}"
+ is_text_column = finder_class.columns_hash[attr_name.to_s].text?
+
+ if value.nil?
+ comparison_operator = "IS ?"
+ elsif is_text_column
+ comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
+ value = 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)
+ condition_sql = "#{sql_attribute} #{comparison_operator}"
condition_params = [value]
else
- # sqlite has case sensitive SELECT query, while MySQL/Postgresql don't.
- # Hence, this is needed only for sqlite.
- condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}"
+ condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
condition_params = [value.downcase]
end
@@ -640,26 +680,10 @@ module ActiveRecord
condition_params << record.send(:id)
end
- results = finder_class.with_exclusive_scope do
- connection.select_all(
- construct_finder_sql(
- :select => "#{connection.quote_column_name(attr_name)}",
- :from => "#{finder_class.quoted_table_name}",
- :conditions => [condition_sql, *condition_params]
- )
- )
- end
-
- unless results.length.zero?
- found = true
-
- # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate
- # column in ruby when case sensitive option
- if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text?
- found = results.any? { |a| a[attr_name.to_s] == value }
+ finder_class.with_exclusive_scope do
+ if finder_class.exists?([condition_sql, *condition_params])
+ record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
end
-
- record.errors.add(attr_name, configuration[:message]) if found
end
end
end
@@ -689,13 +713,15 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_format_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
+ configuration = { :on => :save, :with => nil }
configuration.update(attr_names.extract_options!)
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message] % value) unless value.to_s =~ configuration[:with]
+ unless value.to_s =~ configuration[:with]
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
+ end
end
end
@@ -719,7 +745,7 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_inclusion_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
@@ -727,7 +753,9 @@ module ActiveRecord
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value)
+ unless enum.include?(value)
+ record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
+ end
end
end
@@ -751,7 +779,7 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_exclusion_of(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
enum = configuration[:in] || configuration[:within]
@@ -759,7 +787,9 @@ module ActiveRecord
raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value)
+ if enum.include?(value)
+ record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
+ end
end
end
@@ -795,12 +825,13 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_associated(*attr_names)
- configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
+ configuration = { :on => :save }
configuration.update(attr_names.extract_options!)
validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, configuration[:message]) unless
- (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
+ unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
+ end
end
end
@@ -848,15 +879,15 @@ module ActiveRecord
if configuration[:only_integer]
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
- record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
+ record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
raw_value = raw_value.to_i
else
- begin
+ begin
raw_value = Kernel.Float(raw_value)
rescue ArgumentError, TypeError
- record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
+ record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
end
@@ -864,11 +895,11 @@ module ActiveRecord
numericality_options.each do |option|
case option
when :odd, :even
- record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
+ unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
+ record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
+ end
else
- message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option]
- message = message % configuration[option] if configuration[option]
- record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
+ record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
end
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 9c718c4fef..9c718c4fef 100755..100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 3631be76a0..8c9ae8a031 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -9,7 +9,7 @@ require 'models/topic'
require 'models/reply'
class CascadedEagerLoadingTest < ActiveRecord::TestCase
- fixtures :authors, :mixins, :companies, :posts, :topics
+ fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations
def test_eager_association_loading_with_cascaded_two_levels
authors = Author.find(:all, :include=>{:posts=>:comments}, :order=>"authors.id")
@@ -68,6 +68,18 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
end
+ def test_eager_association_loading_with_has_many_sti_and_subclasses
+ silly = SillyReply.new(:title => "gaga", :content => "boo-boo", :parent_id => 1)
+ silly.parent_id = 1
+ assert silly.save
+
+ topics = Topic.find(:all, :include => :replies, :order => 'topics.id, replies_topics.id')
+ assert_no_queries do
+ assert_equal 2, topics[0].replies.size
+ assert_equal 0, topics[1].replies.size
+ end
+ end
+
def test_eager_association_loading_with_belongs_to_sti
replies = Reply.find(:all, :include => :topic, :order => 'topics.id')
assert replies.include?(topics(:second))
diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
new file mode 100644
index 0000000000..7c470616a5
--- /dev/null
+++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
@@ -0,0 +1,36 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/tagging'
+
+module Namespaced
+ class Post < ActiveRecord::Base
+ set_table_name 'posts'
+ has_one :tagging, :as => :taggable, :class_name => 'Tagging'
+ end
+end
+
+class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
+
+ def setup
+ generate_test_objects
+ end
+
+ def generate_test_objects
+ post = Namespaced::Post.create( :title => 'Great stuff', :body => 'This is not', :author_id => 1 )
+ tagging = Tagging.create( :taggable => post )
+ end
+
+ def test_class_names
+ old = ActiveRecord::Base.store_full_sti_class
+
+ ActiveRecord::Base.store_full_sti_class = false
+ post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging )
+ assert_nil post.tagging
+
+ ActiveRecord::Base.store_full_sti_class = true
+ post = Namespaced::Post.find_by_title( 'Great stuff', :include => :tagging )
+ assert_equal 'Tagging', post.tagging.class.name
+ ensure
+ ActiveRecord::Base.store_full_sti_class = old
+ 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 80cfc84b32..12dec5ccd1 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -1,5 +1,20 @@
require 'cases/helper'
+module Remembered
+ def self.included(base)
+ base.extend ClassMethods
+ base.class_eval do
+ after_create :remember
+ protected
+ def remember; self.class.remembered << self; end
+ end
+ end
+
+ module ClassMethods
+ def remembered; @@remembered ||= []; end
+ def rand; @@remembered.rand; end
+ end
+end
class ShapeExpression < ActiveRecord::Base
belongs_to :shape, :polymorphic => true
@@ -8,26 +23,33 @@ end
class Circle < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
+ include Remembered
end
class Square < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
+ include Remembered
end
class Triangle < ActiveRecord::Base
has_many :shape_expressions, :as => :shape
+ include Remembered
end
class PaintColor < ActiveRecord::Base
has_many :shape_expressions, :as => :paint
belongs_to :non_poly, :foreign_key => "non_poly_one_id", :class_name => "NonPolyOne"
+ include Remembered
end
class PaintTexture < ActiveRecord::Base
has_many :shape_expressions, :as => :paint
belongs_to :non_poly, :foreign_key => "non_poly_two_id", :class_name => "NonPolyTwo"
+ include Remembered
end
class NonPolyOne < ActiveRecord::Base
has_many :paint_colors
+ include Remembered
end
class NonPolyTwo < ActiveRecord::Base
has_many :paint_textures
+ include Remembered
end
@@ -49,23 +71,19 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
end
- # meant to be supplied as an ID, never returns 0
- def rand_simple
- val = (NUM_SIMPLE_OBJS * rand).round
- val == 0 ? 1 : val
- end
-
def generate_test_object_graphs
1.upto(NUM_SIMPLE_OBJS) do
[Circle, Square, Triangle, NonPolyOne, NonPolyTwo].map(&:create!)
end
- 1.upto(NUM_SIMPLE_OBJS) do |i|
- PaintColor.create!(:non_poly_one_id => rand_simple)
- PaintTexture.create!(:non_poly_two_id => rand_simple)
+ 1.upto(NUM_SIMPLE_OBJS) do
+ PaintColor.create!(:non_poly_one_id => NonPolyOne.rand.id)
+ PaintTexture.create!(:non_poly_two_id => NonPolyTwo.rand.id)
end
- 1.upto(NUM_SHAPE_EXPRESSIONS) do |i|
- ShapeExpression.create!(:shape_type => [Circle, Square, Triangle].rand.to_s, :shape_id => rand_simple,
- :paint_type => [PaintColor, PaintTexture].rand.to_s, :paint_id => rand_simple)
+ 1.upto(NUM_SHAPE_EXPRESSIONS) do
+ shape_type = [Circle, Square, Triangle].rand
+ paint_type = [PaintColor, PaintTexture].rand
+ ShapeExpression.create!(:shape_type => shape_type.to_s, :shape_id => shape_type.rand.id,
+ :paint_type => paint_type.to_s, :paint_id => paint_type.rand.id)
end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index f65ada550b..e78624a98d 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -21,7 +21,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
fixtures :posts, :comments, :authors, :categories, :categories_posts,
:companies, :accounts, :tags, :taggings, :people, :readers,
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
- :developers, :projects
+ :developers, :projects, :developers_projects
def test_loading_with_one_association
posts = Post.find(:all, :include => :comments)
@@ -38,6 +38,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal Post.find(1).last_comment, post.last_comment
end
+ def test_loading_with_one_association_with_non_preload
+ posts = Post.find(:all, :include => :last_comment, :order => 'comments.id DESC')
+ post = posts.find { |p| p.id == 1 }
+ assert_equal Post.find(1).last_comment, post.last_comment
+ end
+
def test_loading_conditions_with_or
posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'")
assert_nil posts.detect { |p| p.author_id != authors(:david).id },
@@ -254,9 +260,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_through
- posts_with_comments = people(:michael).posts.find(:all, :include => :comments)
- posts_with_author = people(:michael).posts.find(:all, :include => :author )
- posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ])
+ posts_with_comments = people(:michael).posts.find(:all, :include => :comments, :order => 'posts.id')
+ posts_with_author = people(:michael).posts.find(:all, :include => :author, :order => 'posts.id')
+ posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ], :order => 'posts.id')
assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
@@ -559,6 +565,13 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_nothing_raised { Post.find(:all, :include => 'comments') }
end
+ def test_eager_with_floating_point_numbers
+ assert_queries(2) do
+ # Before changes, the floating point numbers will be interpreted as table names and will cause this to run in one query
+ Comment.find :all, :conditions => "123.456 = 123.456", :include => :post
+ end
+ end
+
def test_preconfigured_includes_with_belongs_to
author = posts(:welcome).author_with_posts
assert_no_queries {assert_equal 5, author.posts.size}
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 b29df68d22..0572418e3b 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
@@ -70,7 +70,7 @@ end
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
- :parrots, :pirates, :treasures, :price_estimates
+ :parrots, :pirates, :treasures, :price_estimates, :tags, :taggings
def test_has_and_belongs_to_many
david = Developer.find(1)
@@ -299,6 +299,17 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, projects(:active_record, :reload).developers.size
end
+ def test_uniq_option_prevents_duplicate_push
+ project = projects(:active_record)
+ project.developers << developers(:jamis)
+ project.developers << developers(:david)
+ assert_equal 3, project.developers.size
+
+ project.developers << developers(:david)
+ project.developers << developers(:jamis)
+ assert_equal 3, project.developers.size
+ end
+
def test_deleting
david = Developer.find(1)
active_record = Project.find(1)
@@ -439,6 +450,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find"
end
+ def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations
+ # interpolate once:
+ assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation"
+ # interpolate again, for a different project id
+ assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation"
+ end
+
def test_find_in_association_with_custom_finder_sql_and_string_id
assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find"
end
@@ -629,8 +647,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer.save
developer.reload
assert_equal 2, developer.projects.length
- assert_equal projects(:active_record), developer.projects[0]
- assert_equal projects(:action_controller), developer.projects[1]
+ assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end
def test_assign_ids_ignoring_blanks
@@ -639,8 +656,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer.save
developer.reload
assert_equal 2, developer.projects.length
- assert_equal projects(:active_record), developer.projects[0]
- assert_equal projects(:action_controller), developer.projects[1]
+ assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort
end
def test_select_limited_ids_list
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index b9c7ec6377..da3c8fb28e 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -14,7 +14,7 @@ require 'models/reader'
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments, :author_addresses,
- :people, :posts
+ :people, :posts, :readers
def setup
Client.destroyed_client_ids.clear
@@ -395,6 +395,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, company.clients_of_firm.size
end
+ def test_collection_size_twice_for_regressions
+ post = posts(:thinking)
+ assert_equal 0, post.readers.size
+ # This test needs a post that has no readers, we assert it to ensure it holds,
+ # but need to reload the post because the very call to #size hides the bug.
+ post.reload
+ post.readers.build
+ size1 = post.readers.size
+ size2 = post.readers.size
+ assert_equal size1, size2
+ end
+
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"}]) }
@@ -999,4 +1011,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert firm.clients.loaded?
end
+ def test_joins_with_namespaced_model_should_use_correct_type
+ old = ActiveRecord::Base.store_full_sti_class
+ ActiveRecord::Base.store_full_sti_class = true
+
+ firm = Namespaced::Firm.create({ :name => 'Some Company' })
+ firm.clients.create({ :name => 'Some Client' })
+
+ stats = Namespaced::Firm.find(firm.id, {
+ :select => "#{Namespaced::Firm.table_name}.id, COUNT(#{Namespaced::Client.table_name}.id) AS num_clients",
+ :joins => :clients,
+ :group => "#{Namespaced::Firm.table_name}.id"
+ })
+ assert_equal 1, stats.num_clients.to_i
+
+ ensure
+ ActiveRecord::Base.store_full_sti_class = old
+ 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 be5170f830..d51a3c7e1c 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -2,15 +2,18 @@ require "cases/helper"
require 'models/post'
require 'models/person'
require 'models/reader'
+require 'models/comment'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
- fixtures :posts, :readers, :people
+ fixtures :posts, :readers, :people, :comments
def test_associate_existing
assert_queries(2) { posts(:thinking);people(:david) }
-
+
+ posts(:thinking).people
+
assert_queries(1) do
- posts(:thinking).people << people(:david)
+ posts(:thinking).people << people(:david)
end
assert_queries(1) do
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 99639849a5..ec06be5eba 100755..100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -79,6 +79,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordNotFound) { Account.find(old_account_id) }
end
+ def test_natural_assignment_to_already_associated_record
+ company = companies(:first_firm)
+ account = accounts(:signals37)
+ assert_equal company.account, account
+ company.account = account
+ company.reload
+ account.reload
+ assert_equal company.account, account
+ end
+
def test_assignment_without_replacement
apple = Firm.create("name" => "Apple")
citibank = Account.create("credit_limit" => 10)
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 3eb66bc941..b61a3711e3 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -44,19 +44,23 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_polymorphic
assert_equal clubs(:moustache_club), @member.sponsor_club
end
-
+
def has_one_through_to_has_many
assert_equal 2, @member.fellow_members.size
end
-
+
def test_has_one_through_eager_loading
- members = Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
+ members = assert_queries(3) do #base table, through table, clubs table
+ Member.find(:all, :include => :club, :conditions => ["name = ?", "Groucho Marx"])
+ end
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].club}
end
-
+
def test_has_one_through_eager_loading_through_polymorphic
- members = Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"])
+ members = assert_queries(3) do #base table, through table, clubs table
+ Member.find(:all, :include => :sponsor_club, :conditions => ["name = ?", "Groucho Marx"])
+ end
assert_equal 1, members.size
assert_not_nil assert_no_queries {members[0].sponsor_club}
end
@@ -71,4 +75,30 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil assert_no_queries {clubs[0].sponsored_member}
end
+ def test_has_one_through_nonpreload_eagerloading
+ members = assert_queries(1) do
+ Member.find(:all, :include => :club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries {members[0].club}
+ end
+
+ def test_has_one_through_nonpreload_eager_loading_through_polymorphic
+ members = assert_queries(1) do
+ Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name') #force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries {members[0].sponsor_club}
+ end
+
+ def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
+ Sponsor.new(:sponsor_club => clubs(:crazy_club), :sponsorable => members(:groucho)).save!
+ members = assert_queries(1) do
+ Member.find(:all, :include => :sponsor_club, :conditions => ["members.name = ?", "Groucho Marx"], :order => 'clubs.name DESC') #force fallback
+ end
+ assert_equal 1, members.size
+ assert_not_nil assert_no_queries { members[0].sponsor_club }
+ assert_equal clubs(:crazy_club), members[0].sponsor_club
+ end
+
end
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 9e79d9c8a1..7a0427aabc 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -694,6 +694,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert ! david.categories.include?(category)
end
+ def test_has_many_through_goes_through_all_sti_classes
+ sub_sti_post = SubStiPost.create!(:title => 'test', :body => 'test', :author_id => 1)
+ new_comment = sub_sti_post.comments.create(:body => 'test')
+
+ assert_equal [9, 10, new_comment.id], authors(:david).sti_post_comments.map(&:id).sort
+ end
+
private
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index 4904feeb7d..0b2731ecd7 100755..100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -27,7 +27,7 @@ require 'models/sponsor'
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
- :computers
+ :computers, :people, :readers
def test_include_with_order_works
assert_nothing_raised {Account.find(:first, :order => 'id', :include => :firm)}
@@ -45,7 +45,7 @@ class AssociationsTest < ActiveRecord::TestCase
assert_equal [], person.readers.find(:all)
person.save!
reader = Reader.create! :person => person, :post => Post.new(:title => "foo", :body => "bar")
- assert_equal [reader], person.readers.find(:all)
+ assert person.readers.find(reader.id)
end
def test_force_reload
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 7999e29264..7999e29264 100755..100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index a4be629fbd..c8111358e3 100755..100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -19,6 +19,7 @@ require 'models/warehouse_thing'
require 'rexml/document'
class Category < ActiveRecord::Base; end
+class Categorization < ActiveRecord::Base; end
class Smarts < ActiveRecord::Base; end
class CreditCard < ActiveRecord::Base
class PinNumber < ActiveRecord::Base
@@ -75,7 +76,7 @@ class TopicWithProtectedContentAndAccessibleAuthorName < ActiveRecord::Base
end
class BasicsTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors
+ fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories
def test_table_exists
assert !NonExistentTable.table_exists?
@@ -130,14 +131,14 @@ class BasicsTest < ActiveRecord::TestCase
def test_read_attributes_before_type_cast
category = Category.new({:name=>"Test categoty", :type => nil})
- category_attrs = {"name"=>"Test categoty", "type" => nil}
+ category_attrs = {"name"=>"Test categoty", "type" => nil, "categorizations_count" => nil}
assert_equal category_attrs , category.attributes_before_type_cast
end
if current_adapter?(:MysqlAdapter)
def test_read_attributes_before_type_cast_on_boolean
bool = Booleantest.create({ "value" => false })
- assert_equal 0, bool.attributes_before_type_cast["value"]
+ assert_equal "0", bool.reload.attributes_before_type_cast["value"]
end
end
@@ -614,6 +615,22 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal -2, Topic.find(2).replies_count
end
+ def test_update_counter
+ category = categories(:general)
+ assert_nil category.categorizations_count
+ assert_equal 2, category.categorizations.count
+
+ Category.update_counters(category.id, "categorizations_count" => category.categorizations.count)
+ category.reload
+ assert_not_nil category.categorizations_count
+ assert_equal 2, category.categorizations_count
+
+ Category.update_counters(category.id, "categorizations_count" => category.categorizations.count)
+ category.reload
+ assert_not_nil category.categorizations_count
+ assert_equal 4, category.categorizations_count
+ end
+
def test_update_all
assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'")
assert_equal "bulk updated!", Topic.find(1).content
@@ -887,6 +904,14 @@ class BasicsTest < ActiveRecord::TestCase
assert_nil keyboard.id
end
+ def test_mass_assigning_invalid_attribute
+ firm = Firm.new
+
+ assert_raises(ActiveRecord::UnknownAttributeError) do
+ firm.attributes = { "id" => 5, "type" => "Client", "i_dont_even_exist" => 20 }
+ end
+ end
+
def test_mass_assignment_protection_on_defaults
firm = Firm.new
firm.attributes = { "id" => 5, "type" => "Client" }
@@ -1089,11 +1114,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_boolean
+ b_nil = Booleantest.create({ "value" => nil })
+ nil_id = b_nil.id
b_false = Booleantest.create({ "value" => false })
false_id = b_false.id
b_true = Booleantest.create({ "value" => true })
true_id = b_true.id
+ b_nil = Booleantest.find(nil_id)
+ assert_nil b_nil.value
b_false = Booleantest.find(false_id)
assert !b_false.value?
b_true = Booleantest.find(true_id)
@@ -1101,11 +1130,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_boolean_cast_from_string
+ b_blank = Booleantest.create({ "value" => "" })
+ blank_id = b_blank.id
b_false = Booleantest.create({ "value" => "0" })
false_id = b_false.id
b_true = Booleantest.create({ "value" => "1" })
true_id = b_true.id
+ b_blank = Booleantest.find(blank_id)
+ assert_nil b_blank.value
b_false = Booleantest.find(false_id)
assert !b_false.value?
b_true = Booleantest.find(true_id)
@@ -1336,6 +1369,12 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal(myobj, topic.content)
end
+ def test_serialized_time_attribute
+ myobj = Time.local(2008,1,1,1,0)
+ topic = Topic.create("content" => myobj).reload
+ assert_equal(myobj, topic.content)
+ end
+
def test_nil_serialized_attribute_with_class_constraint
myobj = MyObject.new('value1', 'value2')
topic = Topic.new
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index 540f42f4b6..98abc8eac8 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -9,13 +9,13 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
end
# Avoid column definitions in create table statements like:
- # `title` varchar(255) DEFAULT NULL NULL
+ # `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
column = ActiveRecord::ConnectionAdapters::Column.new("title", nil, "varchar(20)")
column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new(
@adapter, column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal "title varchar(20) NULL", column_def.to_sql
+ assert_equal "title varchar(20)", column_def.to_sql
end
def test_should_include_default_clause_when_default_is_present
@@ -23,7 +23,7 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
column_def = ActiveRecord::ConnectionAdapters::ColumnDefinition.new(
@adapter, column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello' NULL}, column_def.to_sql
+ assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, column_def.to_sql
end
def test_should_specify_not_null_if_null_option_is_false
@@ -33,4 +33,4 @@ class ColumnDefinitionTest < ActiveRecord::TestCase
column.limit, column.precision, column.scale, column.default, column.null)
assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 2ea85417da..3473b846a0 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -5,7 +5,7 @@ require 'models/entrant'
class DefaultTest < ActiveRecord::TestCase
def test_nil_defaults_for_not_null_columns
column_defaults =
- if current_adapter?(:MysqlAdapter) && Mysql.client_version < 50051
+ if current_adapter?(:MysqlAdapter) && (Mysql.client_version < 50051 || (50100..50122).include?(Mysql.client_version))
{ 'id' => nil, 'name' => '', 'course_id' => nil }
else
{ 'id' => nil, 'name' => nil, 'course_id' => nil }
diff --git a/activerecord/test/cases/deprecated_finder_test.rb b/activerecord/test/cases/deprecated_finder_test.rb
index 2afc91b769..2afc91b769 100755..100644
--- a/activerecord/test/cases/deprecated_finder_test.rb
+++ b/activerecord/test/cases/deprecated_finder_test.rb
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index e5e022050d..feb47a15a8 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -191,6 +191,18 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.changed?
end
+ def test_save_should_store_serialized_attributes_even_with_partial_updates
+ with_partial_updates(Topic) do
+ topic = Topic.create!(:content => {:a => "a"})
+ topic.content[:b] = "b"
+ #assert topic.changed? # Known bug, will fail
+ topic.save!
+ assert_equal "b", topic.content[:b]
+ topic.reload
+ assert_equal "b", topic.content[:b]
+ end
+ end
+
private
def with_partial_updates(klass, on = true)
old = klass.partial_updates?
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index b97db73b68..2ce49ed76f 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -12,6 +12,57 @@ require 'models/customer'
require 'models/job'
require 'models/categorization'
+class DynamicFinderMatchTest < ActiveRecord::TestCase
+ def test_find_no_match
+ assert_nil ActiveRecord::DynamicFinderMatch.match("not_a_finder")
+ end
+
+ def test_find_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert match.finder?
+ assert_equal :find_initial, match.finder
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def find_by_bang
+ match = ActiveRecord::DynamicFinderMatch.match("find_by_age_and_sex_and_location!")
+ assert_not_nil match
+ assert match.finder?
+ assert match.bang?
+ assert_equal :find_initial, match.finder
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def test_find_all_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_all_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert match.finder?
+ assert_equal :find_every, match.finder
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def test_find_or_initialize_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_or_initialize_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert !match.finder?
+ assert match.instantiator?
+ assert_equal :find_initial, match.finder
+ assert_equal :new, match.instantiator
+ assert_equal %w(age sex location), match.attribute_names
+ end
+
+ def test_find_or_create_by
+ match = ActiveRecord::DynamicFinderMatch.match("find_or_create_by_age_and_sex_and_location")
+ assert_not_nil match
+ assert !match.finder?
+ assert match.instantiator?
+ assert_equal :find_initial, match.finder
+ assert_equal :create, match.instantiator
+ assert_equal %w(age sex location), match.attribute_names
+ end
+end
+
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers
@@ -440,6 +491,11 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Topic.find_by_title("The First Topic!")
end
+ 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!") }
+ end
+
def test_find_by_one_attribute_caches_dynamic_finder
# ensure this test can run independently of order
class << Topic; self; end.send(:remove_method, :find_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_by_title' }
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 0ea24868f1..6ba7597f56 100755..100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -461,11 +461,11 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
alias_method :teardown, :blank_teardown
def test_no_rollback_in_teardown_unless_transaction_active
- assert_equal 0, Thread.current['open_transactions']
+ assert_equal 0, ActiveRecord::Base.connection.open_transactions
assert_raise(RuntimeError) { ar_setup_fixtures }
- assert_equal 0, Thread.current['open_transactions']
+ assert_equal 0, ActiveRecord::Base.connection.open_transactions
assert_nothing_raised { ar_teardown_fixtures }
- assert_equal 0, Thread.current['open_transactions']
+ assert_equal 0, ActiveRecord::Base.connection.open_transactions
end
private
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 0530ba9bd9..f30d58546e 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -1,4 +1,5 @@
$:.unshift(File.dirname(__FILE__) + '/../../lib')
+$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib')
require 'config'
require 'test/unit'
diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb
new file mode 100644
index 0000000000..06036733f5
--- /dev/null
+++ b/activerecord/test/cases/i18n_test.rb
@@ -0,0 +1,46 @@
+require "cases/helper"
+require 'models/topic'
+require 'models/reply'
+
+class ActiveRecordI18nTests < Test::Unit::TestCase
+
+ def setup
+ reset_translations
+ end
+
+ def test_translated_model_attributes
+ I18n.store_translations 'en-US', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
+ assert_equal 'topic title attribute', Topic.human_attribute_name('title')
+ end
+
+ def test_translated_model_attributes_with_sti
+ I18n.store_translations 'en-US', :activerecord => {:attributes => {:reply => {:title => 'reply title attribute'} } }
+ assert_equal 'reply title attribute', Reply.human_attribute_name('title')
+ end
+
+ def test_translated_model_attributes_with_sti_fallback
+ I18n.store_translations 'en-US', :activerecord => {:attributes => {:topic => {:title => 'topic title attribute'} } }
+ assert_equal 'topic title attribute', Reply.human_attribute_name('title')
+ end
+
+ def test_translated_model_names
+ I18n.store_translations 'en-US', :activerecord => {:models => {:topic => 'topic model'} }
+ assert_equal 'topic model', Topic.human_name
+ end
+
+ def test_translated_model_names_with_sti
+ I18n.store_translations 'en-US', :activerecord => {:models => {:reply => 'reply model'} }
+ assert_equal 'reply model', Reply.human_name
+ end
+
+ def test_translated_model_names_with_sti_fallback
+ I18n.store_translations 'en-US', :activerecord => {:models => {:topic => 'topic model'} }
+ assert_equal 'topic model', Reply.human_name
+ end
+
+ private
+ def reset_translations
+ I18n.backend = I18n::Backend::Simple.new
+ end
+end
+
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 4fd38bfbc9..4fd38bfbc9 100755..100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb
index 258f7c7a0f..54fb3d8c39 100755..100644
--- a/activerecord/test/cases/lifecycle_test.rb
+++ b/activerecord/test/cases/lifecycle_test.rb
@@ -74,7 +74,7 @@ class MultiObserver < ActiveRecord::Observer
end
class LifecycleTest < ActiveRecord::TestCase
- fixtures :topics, :developers
+ fixtures :topics, :developers, :minimalistics
def test_before_destroy
original_count = Topic.count
@@ -143,7 +143,7 @@ class LifecycleTest < ActiveRecord::TestCase
assert_equal developer.name, multi_observer.record.name
end
- def test_observing_after_find_when_not_defined_on_the_model
+ def test_after_find_can_be_observed_when_its_not_defined_on_the_model
observer = MinimalisticObserver.instance
assert_equal Minimalistic, MinimalisticObserver.observed_class
@@ -151,6 +151,42 @@ class LifecycleTest < ActiveRecord::TestCase
assert_equal minimalistic, observer.minimalistic
end
+ def test_after_find_can_be_observed_when_its_defined_on_the_model
+ observer = TopicObserver.instance
+ assert_equal Topic, TopicObserver.observed_class
+
+ topic = Topic.find(1)
+ assert_equal topic, observer.topic
+ end
+
+ def test_after_find_is_not_created_if_its_not_used
+ # use a fresh class so an observer can't have defined an
+ # after_find on it
+ model_class = Class.new(ActiveRecord::Base)
+ observer_class = Class.new(ActiveRecord::Observer)
+ observer_class.observe(model_class)
+
+ observer = observer_class.instance
+
+ assert !model_class.method_defined?(:after_find)
+ end
+
+ def test_after_find_is_not_clobbered_if_it_already_exists
+ # use a fresh observer class so we can instantiate it (Observer is
+ # a Singleton)
+ model_class = Class.new(ActiveRecord::Base) do
+ def after_find; end
+ end
+ original_method = model_class.instance_method(:after_find)
+ observer_class = Class.new(ActiveRecord::Observer) do
+ def after_find; end
+ end
+ observer_class.observe(model_class)
+
+ observer = observer_class.instance
+ assert_equal original_method, model_class.instance_method(:after_find)
+ end
+
def test_invalid_observer
assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers }
end
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index d6b3e341df..ee66ac948d 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -6,7 +6,7 @@ require 'models/post'
require 'models/category'
class MethodScopingTest < ActiveRecord::TestCase
- fixtures :developers, :projects, :comments, :posts
+ fixtures :developers, :projects, :comments, :posts, :developers_projects
def test_set_conditions
Developer.with_scope(:find => { :conditions => 'just a test...' }) do
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 7ecf755ef8..c1a8da2270 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -237,6 +237,39 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
+ def test_create_table_with_timestamps_should_create_datetime_columns
+ table_name = :testings
+
+ Person.connection.create_table table_name do |t|
+ t.timestamps
+ end
+ created_columns = Person.connection.columns(table_name)
+
+ created_at_column = created_columns.detect {|c| c.name == 'created_at' }
+ updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+
+ assert created_at_column.null
+ assert updated_at_column.null
+ ensure
+ Person.connection.drop_table table_name rescue nil
+ end
+
+ def test_create_table_with_timestamps_should_create_datetime_columns_with_options
+ table_name = :testings
+
+ Person.connection.create_table table_name do |t|
+ t.timestamps :null => false
+ end
+ created_columns = Person.connection.columns(table_name)
+
+ created_at_column = created_columns.detect {|c| c.name == 'created_at' }
+ updated_at_column = created_columns.detect {|c| c.name == 'updated_at' }
+
+ assert !created_at_column.null
+ assert !updated_at_column.null
+ ensure
+ Person.connection.drop_table table_name rescue nil
+ end
# SQL Server, Sybase, and SQLite3 will not allow you to add a NOT NULL
# column to a table without a default value.
@@ -409,10 +442,7 @@ if ActiveRecord::Base.connection.supports_migrations?
ActiveRecord::Migration.add_column :people, :intelligence_quotient, :tinyint
Person.reset_column_information
- Person.create :intelligence_quotient => 300
- jonnyg = Person.find(:first)
- assert_equal 127, jonnyg.intelligence_quotient
- jonnyg.destroy
+ assert_match /tinyint/, Person.columns_hash['intelligence_quotient'].sql_type
ensure
ActiveRecord::Migration.remove_column :people, :intelligence_quotient rescue nil
end
@@ -904,6 +934,21 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal(0, ActiveRecord::Migrator.current_version)
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_migrator_one_up_with_exception_and_rollback
+ assert !Person.column_methods_hash.include?(:last_name)
+
+ e = assert_raises(StandardError) do
+ ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/broken", 100)
+ end
+
+ assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
+
+ Person.reset_column_information
+ assert !Person.column_methods_hash.include?(:last_name)
+ end
+ end
+
def test_finds_migrations
migrations = ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid").migrations
[['1', 'people_have_last_names'],
@@ -922,6 +967,26 @@ if ActiveRecord::Base.connection.supports_migrations?
migrations[0].name == 'innocent_jointable'
end
+ def test_only_loads_pending_migrations
+ # migrate up to 1
+ ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", 1)
+
+ # now unload the migrations that have been defined
+ PeopleHaveLastNames.unloadable
+ ActiveSupport::Dependencies.remove_unloadable_constants!
+
+ ActiveRecord::Migrator.migrate(MIGRATIONS_ROOT + "/valid", nil)
+
+ assert !defined? PeopleHaveLastNames
+
+ %w(WeNeedReminders, InnocentJointable).each do |migration|
+ assert defined? migration
+ end
+
+ ensure
+ load(MIGRATIONS_ROOT + "/valid/1_people_have_last_names.rb")
+ end
+
def test_migrator_interleaved_migrations
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/interleaved/pass_1")
@@ -1172,8 +1237,8 @@ if ActiveRecord::Base.connection.supports_migrations?
def test_timestamps_creates_updated_at_and_created_at
with_new_table do |t|
- t.expects(:column).with(:created_at, :datetime)
- t.expects(:column).with(:updated_at, :datetime)
+ t.expects(:column).with(:created_at, :datetime, kind_of(Hash))
+ t.expects(:column).with(:updated_at, :datetime, kind_of(Hash))
t.timestamps
end
end
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index eb3e43c8ac..7c3e0f2ca6 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -57,4 +57,29 @@ class MultipleDbTest < ActiveRecord::TestCase
assert Course.connection
end
+
+ def test_transactions_across_databases
+ c1 = Course.find(1)
+ e1 = Entrant.find(1)
+
+ begin
+ Course.transaction do
+ Entrant.transaction do
+ c1.name = "Typo"
+ e1.name = "Typo"
+ c1.save
+ e1.save
+ raise "No I messed up."
+ end
+ end
+ rescue
+ # Yup caught it
+ end
+
+ assert_equal "Typo", c1.name
+ assert_equal "Typo", e1.name
+
+ assert_equal "Ruby Development", Course.find(1).name
+ assert_equal "Ruby Developer", Entrant.find(1).name
+ end
end
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index 0c1eb23428..6f6ea1cbe9 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -4,6 +4,7 @@ require 'models/topic'
require 'models/comment'
require 'models/reply'
require 'models/author'
+require 'models/developer'
class NamedScopeTest < ActiveRecord::TestCase
fixtures :posts, :authors, :topics, :comments, :author_addresses
@@ -45,6 +46,17 @@ class NamedScopeTest < ActiveRecord::TestCase
assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count)
end
+ def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
+ assert Topic.approved.respond_to?(:proxy_found)
+ assert Topic.approved.respond_to?(:count)
+ assert Topic.approved.respond_to?(:length)
+ end
+
+ def test_respond_to_respects_include_private_parameter
+ assert !Topic.approved.respond_to?(:load_found)
+ assert Topic.approved.respond_to?(:load_found, true)
+ end
+
def test_subclasses_inherit_scopes
assert Topic.scopes.include?(:base)
@@ -184,6 +196,29 @@ class NamedScopeTest < ActiveRecord::TestCase
end
end
+ def test_any_should_not_load_results
+ topics = Topic.base
+ assert_queries(2) do
+ topics.any? # use count query
+ topics.collect # force load
+ topics.any? # use loaded (no query)
+ end
+ end
+
+ def test_any_should_call_proxy_found_if_using_a_block
+ topics = Topic.base
+ assert_queries(1) do
+ topics.expects(:empty?).never
+ topics.any? { true }
+ end
+ end
+
+ def test_any_should_not_fire_query_if_named_scope_loaded
+ topics = Topic.base
+ topics.collect # force load
+ assert_no_queries { assert topics.any? }
+ end
+
def test_should_build_with_proxy_options
topic = Topic.approved.build({})
assert topic.approved
@@ -209,4 +244,12 @@ class NamedScopeTest < ActiveRecord::TestCase
assert topic.approved
assert_equal 'lifo', topic.author_name
end
+
+ def test_find_all_should_behave_like_select
+ assert_equal Topic.base.select(&:approved), Topic.base.find_all(&:approved)
+ end
+
+ def test_should_use_where_in_query_for_named_scope
+ assert_equal Developer.find_all_by_name('Jamis'), Developer.find_all_by_id(Developer.jamises)
+ end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index dc9eeec281..171d0e6dae 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -58,7 +58,7 @@ end
uses_mocha 'QueryCacheExpiryTest' do
class QueryCacheExpiryTest < ActiveRecord::TestCase
- fixtures :tasks
+ fixtures :tasks, :posts, :categories, :categories_posts
def test_find
Task.connection.expects(:clear_query_cache).times(1)
@@ -116,8 +116,9 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_cache_is_expired_by_habtm_delete
ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
ActiveRecord::Base.cache do
- c = Category.find(:first)
- p = Post.find(:first)
+ c = Category.find(1)
+ p = Post.find(1)
+ assert p.categories.any?
p.categories.delete_all
end
end
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index b921cbdc9c..b921cbdc9c 100755..100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 723062e3b8..4b86e32dbf 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -166,6 +166,10 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
end
+ def test_reflection_should_not_raise_error_when_compared_to_other_object
+ assert_nothing_raised { Firm.reflections[:clients] == Object.new }
+ end
+
private
def assert_reflection(klass, association, options)
assert reflection = klass.reflect_on_association(association)
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 06a76eacc3..af3ee6ddba 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require 'models/topic'
require 'models/reply'
require 'models/developer'
+require 'models/book'
class TransactionTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -86,8 +87,7 @@ class TransactionTest < ActiveRecord::TestCase
assert Topic.find(2).approved?, "Second should still be approved"
end
-
- def test_callback_rollback_in_save
+ def test_raising_exception_in_callback_rollbacks_in_save
add_exception_raising_after_save_callback_to_topic
begin
@@ -102,6 +102,54 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_cancellation_from_before_destroy_rollbacks_in_destroy
+ add_cancelling_before_destroy_with_db_side_effect_to_topic
+ begin
+ nbooks_before_destroy = Book.count
+ status = @first.destroy
+ assert !status
+ assert_nothing_raised(ActiveRecord::RecordNotFound) { @first.reload }
+ assert_equal nbooks_before_destroy, Book.count
+ ensure
+ remove_cancelling_before_destroy_with_db_side_effect_to_topic
+ end
+ end
+
+ def test_cancellation_from_before_filters_rollbacks_in_save
+ %w(validation save).each do |filter|
+ send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ begin
+ nbooks_before_save = Book.count
+ original_author_name = @first.author_name
+ @first.author_name += '_this_should_not_end_up_in_the_db'
+ status = @first.save
+ assert !status
+ assert_equal original_author_name, @first.reload.author_name
+ assert_equal nbooks_before_save, Book.count
+ ensure
+ send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ end
+ end
+ end
+
+ def test_cancellation_from_before_filters_rollbacks_in_save!
+ %w(validation save).each do |filter|
+ send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ begin
+ nbooks_before_save = Book.count
+ original_author_name = @first.author_name
+ @first.author_name += '_this_should_not_end_up_in_the_db'
+ @first.save!
+ flunk
+ rescue => e
+ assert_equal original_author_name, @first.reload.author_name
+ assert_equal nbooks_before_save, Book.count
+ ensure
+ send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
+ end
+ end
+ end
+
def test_callback_rollback_in_create
new_topic = Topic.new(
:title => "A new topic",
@@ -221,6 +269,16 @@ class TransactionTest < ActiveRecord::TestCase
def remove_exception_raising_after_create_callback_to_topic
Topic.class_eval { remove_method :after_create }
end
+
+ %w(validation save destroy).each do |filter|
+ define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
+ Topic.class_eval "def before_#{filter}() Book.create; false end"
+ end
+
+ define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
+ Topic.class_eval "remove_method :before_#{filter}"
+ end
+ end
end
if current_adapter?(:PostgreSQLAdapter)
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index 23ad10f3f9..23ad10f3f9 100755..100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb
new file mode 100644
index 0000000000..43592bcee3
--- /dev/null
+++ b/activerecord/test/cases/validations_i18n_test.rb
@@ -0,0 +1,858 @@
+require "cases/helper"
+require 'models/topic'
+require 'models/reply'
+
+class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
+ def setup
+ reset_callbacks Topic
+ @topic = Topic.new
+ I18n.backend.store_translations('en-US', :activerecord => {:errors => {:messages => {:custom => nil}}})
+ end
+
+ def teardown
+ reset_callbacks Topic
+ I18n.load_translations File.dirname(__FILE__) + '/../../lib/active_record/locale/en-US.yml'
+ end
+
+ def unique_topic
+ @unique ||= Topic.create :title => 'unique!'
+ end
+
+ def replied_topic
+ @replied_topic ||= begin
+ topic = Topic.create(:title => "topic")
+ topic.replies << Reply.new
+ topic
+ end
+ end
+
+ def reset_callbacks(*models)
+ models.each do |model|
+ model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ end
+ end
+
+ def test_default_error_messages_is_deprecated
+ assert_deprecated('ActiveRecord::Errors.default_error_messages') do
+ ActiveRecord::Errors.default_error_messages
+ end
+ end
+
+ # ActiveRecord::Errors
+ uses_mocha 'ActiveRecord::Errors' do
+
+ 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.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(
+ :"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'
+
+ 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_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_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-US'
+ end
+ 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
+
+ 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
+
+ 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
+
+ # 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_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_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_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
+
+ 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_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
+
+ # 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_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
+
+ 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
+
+ # 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_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
+
+ 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
+
+ # 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_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
+
+ 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
+
+ # 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_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
+
+ 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
+
+ # 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_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
+
+ 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
+ end
+
+ # validates_confirmation_of w/o mocha
+
+ def test_validates_confirmation_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:confirmation => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}}
+
+ Topic.validates_confirmation_of :title
+ @topic.title_confirmation = 'foo'
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_confirmation_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:confirmation => 'global message'}}}
+
+ Topic.validates_confirmation_of :title
+ @topic.title_confirmation = 'foo'
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_acceptance_of w/o mocha
+
+ def test_validates_acceptance_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:accepted => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}}
+
+ Topic.validates_acceptance_of :title, :allow_nil => false
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_acceptance_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:accepted => 'global message'}}}
+
+ Topic.validates_acceptance_of :title, :allow_nil => false
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_presence_of w/o mocha
+
+ def test_validates_presence_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:blank => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:blank => 'global message'}}}
+
+ Topic.validates_presence_of :title
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_presence_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:blank => 'global message'}}}
+
+ Topic.validates_presence_of :title
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_length_of :within w/o mocha
+
+ def test_validates_length_of_within_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:too_short => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}}
+
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_length_of_within_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:too_short => 'global message'}}}
+
+ Topic.validates_length_of :title, :within => 3..5
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_length_of :is w/o mocha
+
+ def test_validates_length_of_within_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
+ Topic.validates_length_of :title, :is => 5
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_length_of_within_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
+ Topic.validates_length_of :title, :is => 5
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_uniqueness_of w/o mocha
+
+ def test_validates_length_of_within_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:wrong_length => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
+ Topic.validates_length_of :title, :is => 5
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_length_of_within_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:wrong_length => 'global message'}}}
+
+ Topic.validates_length_of :title, :is => 5
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+
+ # validates_format_of w/o mocha
+
+ def test_validates_format_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:invalid => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
+ Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_format_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
+ Topic.validates_format_of :title, :with => /^[1-9][0-9]*$/
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_inclusion_of w/o mocha
+
+ def test_validates_inclusion_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:inclusion => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}}
+
+ Topic.validates_inclusion_of :title, :in => %w(a b c)
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_inclusion_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:inclusion => 'global message'}}}
+
+ Topic.validates_inclusion_of :title, :in => %w(a b c)
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_exclusion_of w/o mocha
+
+ def test_validates_exclusion_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:exclusion => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}}
+
+ Topic.validates_exclusion_of :title, :in => %w(a b c)
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_exclusion_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:exclusion => 'global message'}}}
+
+ Topic.validates_exclusion_of :title, :in => %w(a b c)
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_numericality_of without :only_integer w/o mocha
+
+ def test_validates_numericality_of_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
+ Topic.validates_numericality_of :title
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_numericality_of_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_numericality_of with :only_integer w/o mocha
+
+ def test_validates_numericality_of_only_integer_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:not_a_number => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_numericality_of_only_integer_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:not_a_number => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true
+ @topic.title = 'a'
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_numericality_of :odd w/o mocha
+
+ def test_validates_numericality_of_odd_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:odd => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:odd => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true, :odd => true
+ @topic.title = 0
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_numericality_of_odd_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:odd => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true, :odd => true
+ @topic.title = 0
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+ # validates_numericality_of :less_than w/o mocha
+
+ def test_validates_numericality_of_less_than_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:less_than => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
+ @topic.title = 1
+ @topic.valid?
+ assert_equal 'custom message', @topic.errors.on(:title)
+ end
+
+ def test_validates_numericality_of_less_than_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:less_than => 'global message'}}}
+
+ Topic.validates_numericality_of :title, :only_integer => true, :less_than => 0
+ @topic.title = 1
+ @topic.valid?
+ assert_equal 'global message', @topic.errors.on(:title)
+ end
+
+
+ # validates_associated w/o mocha
+
+ def test_validates_associated_finds_custom_model_key_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}}
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
+ Topic.validates_associated :replies
+ replied_topic.valid?
+ assert_equal 'custom message', replied_topic.errors.on(:replies)
+ end
+
+ def test_validates_associated_finds_global_default_translation
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
+
+ Topic.validates_associated :replies
+ replied_topic.valid?
+ assert_equal 'global message', replied_topic.errors.on(:replies)
+ end
+end
+
+class ActiveRecordValidationsGenerateMessageI18nTests < Test::Unit::TestCase
+ def setup
+ reset_callbacks Topic
+ @topic = Topic.new
+ I18n.backend.store_translations :'en-US', {
+ :activerecord => {
+ :errors => {
+ :messages => {
+ :inclusion => "is not included in the list",
+ :exclusion => "is reserved",
+ :invalid => "is invalid",
+ :confirmation => "doesn't match confirmation",
+ :accepted => "must be accepted",
+ :empty => "can't be empty",
+ :blank => "can't be blank",
+ :too_long => "is too long (maximum is {{count}} characters)",
+ :too_short => "is too short (minimum is {{count}} characters)",
+ :wrong_length => "is the wrong length (should be {{count}} characters)",
+ :taken => "has already been taken",
+ :not_a_number => "is not a number",
+ :greater_than => "must be greater than {{count}}",
+ :greater_than_or_equal_to => "must be greater than or equal to {{count}}",
+ :equal_to => "must be equal to {{count}}",
+ :less_than => "must be less than {{count}}",
+ :less_than_or_equal_to => "must be less than or equal to {{count}}",
+ :odd => "must be odd",
+ :even => "must be even"
+ }
+ }
+ }
+ }
+ end
+
+ def reset_callbacks(*models)
+ models.each do |model|
+ model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_create_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ model.instance_variable_set("@validate_on_update_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
+ end
+ end
+
+ # validates_inclusion_of: generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value)
+ def test_generate_message_inclusion_with_default_message
+ assert_equal 'is not included in the list', @topic.errors.generate_message(:title, :inclusion, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_inclusion_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :inclusion, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_exclusion_of: generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value)
+ def test_generate_message_exclusion_with_default_message
+ assert_equal 'is reserved', @topic.errors.generate_message(:title, :exclusion, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_exclusion_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :exclusion, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_associated: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
+ # validates_format_of: generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
+ def test_generate_message_invalid_with_default_message
+ assert_equal 'is invalid', @topic.errors.generate_message(:title, :invalid, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_invalid_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :invalid, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_confirmation_of: generate_message(attr_name, :confirmation, :default => configuration[:message])
+ def test_generate_message_confirmation_with_default_message
+ assert_equal "doesn't match confirmation", @topic.errors.generate_message(:title, :confirmation, :default => nil)
+ end
+
+ def test_generate_message_confirmation_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :confirmation, :default => 'custom message')
+ end
+
+ # validates_acceptance_of: generate_message(attr_name, :accepted, :default => configuration[:message])
+ def test_generate_message_accepted_with_default_message
+ assert_equal "must be accepted", @topic.errors.generate_message(:title, :accepted, :default => nil)
+ end
+
+ def test_generate_message_accepted_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :accepted, :default => 'custom message')
+ end
+
+ # add_on_empty: generate_message(attr, :empty, :default => custom_message)
+ def test_generate_message_empty_with_default_message
+ assert_equal "can't be empty", @topic.errors.generate_message(:title, :empty, :default => nil)
+ end
+
+ def test_generate_message_empty_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :empty, :default => 'custom message')
+ end
+
+ # add_on_blank: generate_message(attr, :blank, :default => custom_message)
+ def test_generate_message_blank_with_default_message
+ assert_equal "can't be blank", @topic.errors.generate_message(:title, :blank, :default => nil)
+ end
+
+ def test_generate_message_blank_with_custom_message
+ assert_equal 'custom message', @topic.errors.generate_message(:title, :blank, :default => 'custom message')
+ end
+
+ # validates_length_of: generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end)
+ def test_generate_message_too_long_with_default_message
+ assert_equal "is too long (maximum is 10 characters)", @topic.errors.generate_message(:title, :too_long, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_too_long_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_long, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_length_of: generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
+ def test_generate_message_too_short_with_default_message
+ assert_equal "is too short (minimum is 10 characters)", @topic.errors.generate_message(:title, :too_short, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_too_short_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :too_short, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_length_of: generate_message(attr, key, :default => custom_message, :count => option_value)
+ def test_generate_message_wrong_length_with_default_message
+ assert_equal "is the wrong length (should be 10 characters)", @topic.errors.generate_message(:title, :wrong_length, :default => nil, :count => 10)
+ end
+
+ def test_generate_message_wrong_length_with_custom_message
+ assert_equal 'custom message 10', @topic.errors.generate_message(:title, :wrong_length, :default => 'custom message {{count}}', :count => 10)
+ end
+
+ # validates_uniqueness_of: generate_message(attr_name, :taken, :default => configuration[:message])
+ def test_generate_message_taken_with_default_message
+ assert_equal "has already been taken", @topic.errors.generate_message(:title, :taken, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_taken_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :taken, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
+ def test_generate_message_not_a_number_with_default_message
+ assert_equal "is not a number", @topic.errors.generate_message(:title, :not_a_number, :default => nil, :value => 'title')
+ end
+
+ def test_generate_message_not_a_number_with_custom_message
+ assert_equal 'custom message title', @topic.errors.generate_message(:title, :not_a_number, :default => 'custom message {{value}}', :value => 'title')
+ end
+
+ # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => configuration[:message])
+ def test_generate_message_greater_than_with_default_message
+ assert_equal "must be greater than 10", @topic.errors.generate_message(:title, :greater_than, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_greater_than_or_equal_to_with_default_message
+ assert_equal "must be greater than or equal to 10", @topic.errors.generate_message(:title, :greater_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_equal_to_with_default_message
+ assert_equal "must be equal to 10", @topic.errors.generate_message(:title, :equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_less_than_with_default_message
+ assert_equal "must be less than 10", @topic.errors.generate_message(:title, :less_than, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_less_than_or_equal_to_with_default_message
+ assert_equal "must be less than or equal to 10", @topic.errors.generate_message(:title, :less_than_or_equal_to, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_odd_with_default_message
+ assert_equal "must be odd", @topic.errors.generate_message(:title, :odd, :default => nil, :value => 'title', :count => 10)
+ end
+
+ def test_generate_message_even_with_default_message
+ assert_equal "must be even", @topic.errors.generate_message(:title, :even, :default => nil, :value => 'title', :count => 10)
+ end
+end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 0742e2c632..a40bda2533 100755..100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -477,6 +477,15 @@ class ValidationsTest < ActiveRecord::TestCase
assert_not_equal "has already been taken", t3.errors.on(:title)
end
+ def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer
+ Topic.validates_uniqueness_of(:title, :case_sensitve => true)
+ t = Topic.create!('title' => 101)
+
+ t2 = Topic.new('title' => 101)
+ assert !t2.valid?
+ assert t2.errors.on(:title)
+ end
+
def test_validate_uniqueness_with_non_standard_table_names
i1 = WarehouseThing.create(:value => 1000)
assert !i1.valid?, "i1 should not be valid"
@@ -853,7 +862,9 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_validates_length_with_globally_modified_error_message
- ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d'
+ ActiveSupport::Deprecation.silence do
+ ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d'
+ end
Topic.validates_length_of :title, :minimum => 10
t = Topic.create(:title => 'too short')
assert !t.valid?
@@ -1409,8 +1420,8 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
def test_validates_numericality_of_with_nil_allowed
Topic.validates_numericality_of :approved, :allow_nil => true
- invalid!(BLANK + JUNK)
- valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ invalid!(JUNK)
+ valid!(NIL + BLANK + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
def test_validates_numericality_of_with_integer_only
@@ -1423,8 +1434,8 @@ class ValidatesNumericalityTest < ActiveRecord::TestCase
def test_validates_numericality_of_with_integer_only_and_nil_allowed
Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true
- invalid!(BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY)
- valid!(NIL + INTEGERS)
+ invalid!(JUNK + FLOATS + BIGDECIMAL + INFINITY)
+ valid!(NIL + BLANK + INTEGERS)
end
def test_validates_numericality_with_greater_than
diff --git a/activerecord/test/connections/native_mysql/connection.rb b/activerecord/test/connections/native_mysql/connection.rb
index 1fab444e58..140e06d631 100644
--- a/activerecord/test/connections/native_mysql/connection.rb
+++ b/activerecord/test/connections/native_mysql/connection.rb
@@ -2,9 +2,7 @@ print "Using native MySQL\n"
require_dependency 'models/course'
require 'logger'
-RAILS_DEFAULT_LOGGER = Logger.new('debug.log')
-RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
-ActiveRecord::Base.logger = RAILS_DEFAULT_LOGGER
+ActiveRecord::Base.logger = Logger.new("debug.log")
# GRANT ALL PRIVILEGES ON activerecord_unittest.* to 'rails'@'localhost';
# GRANT ALL PRIVILEGES ON activerecord_unittest2.* to 'rails'@'localhost';
diff --git a/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb b/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
new file mode 100644
index 0000000000..ffb224dad9
--- /dev/null
+++ b/activerecord/test/migrations/broken/100_migration_that_raises_exception.rb
@@ -0,0 +1,10 @@
+class MigrationThatRaisesException < ActiveRecord::Migration
+ def self.up
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+
+ def self.down
+ remove_column "people", "last_name"
+ end
+end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 136dc39cf3..c6aa0293c2 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -32,6 +32,9 @@ class Author < ActiveRecord::Base
has_many :special_posts
has_many :special_post_comments, :through => :special_posts, :source => :comments
+ has_many :sti_posts, :class_name => 'StiPost'
+ has_many :sti_post_comments, :through => :sti_posts, :source => :comments
+
has_many :special_nonexistant_posts, :class_name => "SpecialPost", :conditions => "posts.body = 'nonexistant'"
has_many :special_nonexistant_post_comments, :through => :special_nonexistant_posts, :source => :comments, :conditions => "comments.post_id = 0"
has_many :nonexistant_comments, :through => :posts
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index e6aa810146..cd435948a1 100755..100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -18,6 +18,13 @@ end
module Namespaced
class Company < ::Company
end
+
+ class Firm < ::Company
+ has_many :clients, :class_name => 'Namespaced::Client'
+ end
+
+ class Client < ::Company
+ end
end
class Firm < Company
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 9f26cacdec..c08476f728 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -43,6 +43,8 @@ class Developer < ActiveRecord::Base
has_many :audit_logs
+ named_scope :jamises, :conditions => {:name => 'Jamis'}
+
validates_inclusion_of :salary, :in => 50000..200000
validates_length_of :name, :within => 3..20
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index e1ab89eca5..44c692b5e7 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -7,7 +7,7 @@ class Project < ActiveRecord::Base
has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true
has_and_belongs_to_many :developers_named_david_with_hash_conditions, :class_name => "Developer", :conditions => { :name => 'David' }, :uniq => true
has_and_belongs_to_many :salaried_developers, :class_name => "Developer", :conditions => "salary > 0"
- has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => 'SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id}'
+ has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => 'SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id'
has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => "DELETE FROM developers_projects WHERE project_id = \#{id} AND developer_id = \#{record.id}"
has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"},
:after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"},
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index 812bc1f535..812bc1f535 100755..100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 39ca1bf42a..39ca1bf42a 100755..100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb
index 5ae062c97c..f44c33ae67 100644
--- a/activerecord/test/schema/mysql_specific_schema.rb
+++ b/activerecord/test/schema/mysql_specific_schema.rb
@@ -1,5 +1,5 @@
ActiveRecord::Schema.define do
- create_table :binary_fields, :force => true do |t|
+ create_table :binary_fields, :force => true, :options => 'CHARACTER SET latin1' do |t|
t.binary :tiny_blob, :limit => 255
t.binary :normal_blob, :limit => 65535
t.binary :medium_blob, :limit => 16777215
@@ -9,4 +9,4 @@ ActiveRecord::Schema.define do
t.text :medium_text, :limit => 16777215
t.text :long_text, :limit => 2147483647
end
-end \ No newline at end of file
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 29c91a4464..ab5c7c520b 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -60,12 +60,13 @@ ActiveRecord::Schema.define do
end
create_table :booleantests, :force => true do |t|
- t.integer :value
+ t.boolean :value
end
create_table :categories, :force => true do |t|
t.string :name, :null => false
t.string :type
+ t.integer :categorizations_count
end
create_table :categories_posts, :force => true, :id => false do |t|