aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
authorHongli Lai (Phusion) <hongli@phusion.nl>2008-09-04 23:01:40 +0200
committerHongli Lai (Phusion) <hongli@phusion.nl>2008-09-04 23:01:40 +0200
commitc480c1db1f302ab28a255c5423326e51d27ec5ed (patch)
treeac2afe4deb5ea436d53e421c14650bc627480f45 /activerecord
parent08704c442d15b16511214731dd94108b737ef407 (diff)
parentd7bd01f543d18e37f9c353d847bda3456bc337c3 (diff)
downloadrails-c480c1db1f302ab28a255c5423326e51d27ec5ed.tar.gz
rails-c480c1db1f302ab28a255c5423326e51d27ec5ed.tar.bz2
rails-c480c1db1f302ab28a255c5423326e51d27ec5ed.zip
Merge branch 'master' of git@github.com:lifo/docrails
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG8
-rw-r--r--activerecord/lib/active_record.rb22
-rw-r--r--activerecord/lib/active_record/association_preload.rb8
-rwxr-xr-x[-rw-r--r--]activerecord/lib/active_record/associations.rb99
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb29
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb18
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb14
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb12
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb4
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb6
-rwxr-xr-xactiverecord/lib/active_record/base.rb257
-rw-r--r--activerecord/lib/active_record/calculations.rb19
-rw-r--r--activerecord/lib/active_record/callbacks.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb281
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb242
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb43
-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.rb8
-rwxr-xr-x[-rw-r--r--]activerecord/lib/active_record/connection_adapters/abstract_adapter.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb19
-rw-r--r--activerecord/lib/active_record/dirty.rb5
-rw-r--r--activerecord/lib/active_record/dynamic_finder_match.rb40
-rw-r--r--activerecord/lib/active_record/fixtures.rb2
-rw-r--r--activerecord/lib/active_record/locale/en-US.rb25
-rw-r--r--activerecord/lib/active_record/locale/en-US.yml33
-rw-r--r--activerecord/lib/active_record/migration.rb47
-rw-r--r--activerecord/lib/active_record/named_scope.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.rb16
-rw-r--r--activerecord/lib/active_record/validations.rb140
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb12
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb42
-rw-r--r--activerecord/test/cases/associations/eager_test.rb19
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb54
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb48
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb24
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb10
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb49
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb7
-rw-r--r--activerecord/test/cases/associations_test.rb4
-rw-r--r--activerecord/test/cases/base_test.rb14
-rw-r--r--activerecord/test/cases/column_definition_test.rb8
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--activerecord/test/cases/dirty_test.rb36
-rw-r--r--activerecord/test/cases/finder_test.rb56
-rw-r--r--activerecord/test/cases/helper.rb1
-rw-r--r--activerecord/test/cases/i18n_test.rb41
-rw-r--r--activerecord/test/cases/locking_test.rb7
-rw-r--r--activerecord/test/cases/method_scoping_test.rb81
-rw-r--r--activerecord/test/cases/migration_test.rb57
-rw-r--r--activerecord/test/cases/named_scope_test.rb33
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb87
-rw-r--r--activerecord/test/cases/query_cache_test.rb5
-rw-r--r--activerecord/test/cases/reflection_test.rb4
-rw-r--r--activerecord/test/cases/threaded_connections_test.rb48
-rw-r--r--activerecord/test/cases/transactions_test.rb73
-rw-r--r--activerecord/test/cases/validations_i18n_test.rb566
-rw-r--r--activerecord/test/cases/validations_test.rb20
-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--activerecord/test/models/developer.rb2
-rw-r--r--activerecord/test/models/project.rb2
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb4
-rw-r--r--activerecord/test/schema/schema.rb2
69 files changed, 2054 insertions, 894 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index 90be9b700a..58d0669770 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,13 @@
*Edge*
+* Connection pooling. #936 [Nick Sieger]
+
+* Merge scoped :joins together instead of overwriting them. May expose scoping bugs in your code! #501 [Andrew White]
+
+* 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]
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 17a7949959..a6bbd6fc82 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
@@ -81,7 +77,5 @@ require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/schema_dumper'
-I18n.backend.populate do
- require 'active_record/locale/en-US.rb'
-end
+I18n.load_translations File.dirname(__FILE__) + '/active_record/locale/en-US.yml'
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index c7594809b7..61fa34ac39 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -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
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index eb1281901b..6405071354 100644..100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -878,10 +878,10 @@ module ActiveRecord
method_name = "has_one_after_save_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
+ association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
- if !association.nil? && (new_record? || association.new_record? || association["#{reflection.primary_key_name}"] != id)
- association["#{reflection.primary_key_name}"] = id
+ if !association.nil? && (new_record? || association.new_record? || association[reflection.primary_key_name] != id)
+ association[reflection.primary_key_name] = id
association.save(true)
end
end
@@ -994,7 +994,7 @@ module ActiveRecord
method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
+ association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
if association && association.target
if association.new_record?
@@ -1002,8 +1002,8 @@ module ActiveRecord
end
if association.updated?
- self["#{reflection.primary_key_name}"] = association.id
- self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s
+ self[reflection.primary_key_name] = association.id
+ self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
end
end
end
@@ -1015,7 +1015,7 @@ module ActiveRecord
method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
+ association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
if !association.nil?
if association.new_record?
@@ -1023,7 +1023,7 @@ module ActiveRecord
end
if association.updated?
- self["#{reflection.primary_key_name}"] = association.id
+ self[reflection.primary_key_name] = association.id
end
end
end
@@ -1038,15 +1038,15 @@ module ActiveRecord
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = send("#{reflection.name}")
- association.class.increment_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil?
+ association = send(reflection.name)
+ association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
end
after_create method_name
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = send("#{reflection.name}")
- association.class.decrement_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil?
+ association = send(reflection.name)
+ association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil?
end
before_destroy method_name
@@ -1164,6 +1164,9 @@ module ActiveRecord
# 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
+ # [: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>.
# [:delete_sql]
# Overwrite the default generated SQL statement used to remove links between the associated
# classes with a manual statement.
@@ -1269,10 +1272,9 @@ module ActiveRecord
association.create_through_record(new_value)
self.send(reflection.name, new_value)
else
- association.replace(new_value)
+ association.replace(new_value)
+ instance_variable_set(ivar, new_value.nil? ? nil : association)
end
-
- instance_variable_set(ivar, new_value.nil? ? nil : association)
end
define_method("set_#{reflection.name}_target") do |target|
@@ -1301,7 +1303,11 @@ module ActiveRecord
end
define_method("#{reflection.name.to_s.singularize}_ids") do
- send(reflection.name).map { |record| record.id }
+ if send(reflection.name).loaded?
+ send(reflection.name).map(&:id)
+ else
+ send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map(&:id)
+ end
end
end
@@ -1322,19 +1328,19 @@ module ActiveRecord
end
end
end
-
+
def add_single_associated_validation_callbacks(association_name)
method_name = "validate_associated_records_for_#{association_name}".to_sym
define_method(method_name) do
association = instance_variable_get("@#{association_name}")
if !association.nil?
- errors.add "#{association_name}" unless association.target.nil? || association.valid?
+ errors.add association_name unless association.target.nil? || association.valid?
end
end
-
+
validate method_name
end
-
+
def add_multiple_associated_validation_callbacks(association_name)
method_name = "validate_associated_records_for_#{association_name}".to_sym
ivar = "@#{association_name}"
@@ -1350,7 +1356,7 @@ module ActiveRecord
else
association.target.select { |record| record.new_record? }
end.each do |record|
- errors.add "#{association_name}" unless record.valid?
+ errors.add association_name unless record.valid?
end
end
end
@@ -1370,7 +1376,7 @@ module ActiveRecord
method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym
define_method(method_name) do
- association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
+ association = instance_variable_get(ivar) if instance_variable_defined?(ivar)
records_to_save = if @new_record_before_save
association
@@ -1437,7 +1443,7 @@ module ActiveRecord
when :destroy
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
- send("#{reflection.name}").each { |o| o.destroy }
+ send(reflection.name).each { |o| o.destroy }
end
before_destroy method_name
when :delete_all
@@ -1456,22 +1462,22 @@ module ActiveRecord
when :destroy
method_name = "has_one_dependent_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = send("#{reflection.name}")
+ association = send(reflection.name)
association.destroy unless association.nil?
end
before_destroy method_name
when :delete
method_name = "has_one_dependent_delete_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = send("#{reflection.name}")
+ association = send(reflection.name)
association.class.delete(association.id) unless association.nil?
end
before_destroy method_name
when :nullify
method_name = "has_one_dependent_nullify_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = send("#{reflection.name}")
- association.update_attribute("#{reflection.primary_key_name}", nil) unless association.nil?
+ association = send(reflection.name)
+ association.update_attribute(reflection.primary_key_name, nil) unless association.nil?
end
before_destroy method_name
else
@@ -1486,14 +1492,14 @@ module ActiveRecord
when :destroy
method_name = "belongs_to_dependent_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = send("#{reflection.name}")
+ association = send(reflection.name)
association.destroy unless association.nil?
end
before_destroy method_name
when :delete
method_name = "belongs_to_dependent_delete_for_#{reflection.name}".to_sym
define_method(method_name) do
- association = send("#{reflection.name}")
+ association = send(reflection.name)
association.class.delete(association.id) unless association.nil?
end
before_destroy method_name
@@ -1528,7 +1534,7 @@ module ActiveRecord
create_reflection(:has_one, association_id, options, self)
end
-
+
def create_has_one_through_reflection(association_id, options)
options.assert_valid_keys(
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
@@ -1596,7 +1602,7 @@ module ActiveRecord
sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
- add_joins!(sql, options, scope)
+ add_joins!(sql, options[:joins], scope)
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
@@ -1652,7 +1658,7 @@ module ActiveRecord
if is_distinct
sql << distinct_join_associations.collect { |assoc| assoc.association_join }.join
- add_joins!(sql, options, scope)
+ add_joins!(sql, options[:joins], scope)
end
add_conditions!(sql, options[:conditions], scope)
@@ -1678,19 +1684,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
@@ -1892,6 +1898,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
@@ -1919,7 +1926,7 @@ module ActiveRecord
end
def aliased_primary_key
- "#{ aliased_prefix }_r0"
+ "#{aliased_prefix}_r0"
end
def aliased_table_name
@@ -1931,7 +1938,7 @@ module ActiveRecord
@column_names_with_alias = []
([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
+ @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
end
end
@@ -1968,12 +1975,12 @@ module ActiveRecord
@aliased_prefix = "t#{ join_dependency.joins.size }"
@parent_table_name = parent.active_record.table_name
@aliased_table_name = aliased_table_name_for(table_name)
-
+
if reflection.macro == :has_and_belongs_to_many
@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
@@ -1997,7 +2004,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
@@ -2098,10 +2105,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.sti_name)] 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]
@@ -2111,7 +2116,7 @@ module ActiveRecord
end
protected
-
+
def aliased_table_name_for(name, suffix = nil)
if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{name.downcase}\son}
@join_dependency.table_aliases[name] += 1
@@ -2129,7 +2134,7 @@ module ActiveRecord
name
end
-
+
def pluralize(table_name)
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
end
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index a12d4face4..168443e092 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -141,6 +141,35 @@ module ActiveRecord
end
end
+ # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
+ # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
+ # descendant's +construct_sql+ method will have set :counter_sql automatically.
+ # Otherwise, construct options and pass them with scope to the target class's +count+.
+ def count(*args)
+ if @reflection.options[:counter_sql]
+ @reflection.klass.count_by_sql(@counter_sql)
+ else
+ column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
+ if @reflection.options[:uniq]
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
+ column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
+ options.merge!(:distinct => true)
+ end
+
+ value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
+
+ limit = @reflection.options[:limit]
+ offset = @reflection.options[:offset]
+
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
+ else
+ value
+ end
+ end
+ end
+
+
# Remove +records+ from this association. Does not destroy +records+.
def delete(*records)
records = flatten_deeper(records)
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 78b4c137a7..acdcd14ec8 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -74,8 +74,8 @@ module ActiveRecord
end
# Does the proxy or its \target respond to +symbol+?
- def respond_to?(symbol, include_priv = false)
- proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
+ def respond_to?(*args)
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
end
# Forwards <tt>===</tt> explicitly to the \target because the instance method
@@ -155,18 +155,6 @@ module ActiveRecord
records.map { |record| record.quoted_id }.join(',')
end
- # Interpolates the SQL in <tt>options[key]</tt> and assigns the result
- # back, for any +key+ in +keys+ that's present in +options+.
- #
- # Meant to be used like this:
- #
- # interpolate_sql_options!(@reflection.options, :finder_sql)
- #
- def interpolate_sql_options!(options, *keys)
- keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
- end
-
- # Forwards the call to the owner.
def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record)
end
@@ -261,7 +249,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
# Returns the ID of the owner, quoted if needed.
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..3d689098b5 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,16 +70,24 @@ 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
end
@join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
+
+ if @reflection.options[:counter_sql]
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
+ elsif @reflection.options[:finder_sql]
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
+ else
+ @counter_sql = @finder_sql
+ end
end
def construct_scope
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index f06e69aba3..dda22668c6 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -5,23 +5,6 @@ module ActiveRecord
# If the association has a <tt>:through</tt> option further specialization
# is provided by its child HasManyThroughAssociation.
class HasManyAssociation < AssociationCollection #:nodoc:
- # Count the number of associated records. All arguments are optional.
- def count(*args)
- if @reflection.options[:counter_sql]
- @reflection.klass.count_by_sql(@counter_sql)
- elsif @reflection.options[:finder_sql]
- @reflection.klass.count_by_sql(@finder_sql)
- else
- column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
- options[:conditions] = options[:conditions].blank? ?
- @finder_sql :
- @finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
- options[:include] ||= @reflection.options[:include]
-
- @reflection.klass.count(column_name, options)
- end
- end
-
protected
def owner_quoted_id
if @reflection.options[:primary_key]
@@ -49,8 +32,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..84fa900f46 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -31,16 +31,6 @@ module ActiveRecord
return count
end
- def count(*args)
- column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
- if @reflection.options[:uniq]
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL statement.
- column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
- options.merge!(:distinct => true)
- end
- @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
- end
-
protected
def construct_find_options!(options)
options[:select] = construct_select(options[:select])
@@ -237,7 +227,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 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/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index c846956e1f..b78bd5d931 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -22,6 +22,10 @@ module ActiveRecord
def find_target
super.first
+ end
+
+ def reset_target!
+ @target = nil
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index fab16a4446..0a1baff87d 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -214,7 +214,7 @@ module ActiveRecord
if logger
logger.warn "Exception occurred during reader method compilation."
logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
- logger.warn "#{err.message}"
+ logger.warn err.message
end
end
end
@@ -330,8 +330,8 @@ module ActiveRecord
end
end
- # A Person object with a name attribute can ask <tt>person.respond_to?("name")</tt>,
- # <tt>person.respond_to?("name=")</tt>, and <tt>person.respond_to?("name?")</tt>
+ # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
+ # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
# which will all return +true+.
alias :respond_to_without_attributes? :respond_to?
def respond_to?(method, include_priv = false)
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index b282ea931e..3419aad580 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -452,13 +452,6 @@ module ActiveRecord #:nodoc:
cattr_accessor :default_timezone, :instance_writer => false
@@default_timezone = :local
- # Determines whether to use a connection for each thread, or a single shared connection for all threads.
- # Defaults to false. If you're writing a threaded application, set to true
- # and periodically call verify_active_connections! to clear out connections
- # assigned to stale threads.
- cattr_accessor :allow_concurrency, :instance_writer => false
- @@allow_concurrency = false
-
# Specifies the format to use when dumping the database schema with Rails'
# Rakefile. If :sql, the schema is dumped as (potentially database-
# specific) SQL statements. If :ruby, the schema is dumped as an
@@ -930,12 +923,12 @@ module ActiveRecord #:nodoc:
# To start from an all-closed default and enable attributes as needed,
# have a look at +attr_accessible+.
def attr_protected(*attributes)
- write_inheritable_attribute("attr_protected", Set.new(attributes.map(&:to_s)) + (protected_attributes || []))
+ write_inheritable_attribute(:attr_protected, Set.new(attributes.map(&:to_s)) + (protected_attributes || []))
end
# Returns an array of all the attributes that have been protected from mass-assignment.
def protected_attributes # :nodoc:
- read_inheritable_attribute("attr_protected")
+ read_inheritable_attribute(:attr_protected)
end
# Specifies a white list of model attributes that can be set via
@@ -963,22 +956,22 @@ module ActiveRecord #:nodoc:
# customer.credit_rating = "Average"
# customer.credit_rating # => "Average"
def attr_accessible(*attributes)
- write_inheritable_attribute("attr_accessible", Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
+ write_inheritable_attribute(:attr_accessible, Set.new(attributes.map(&:to_s)) + (accessible_attributes || []))
end
# Returns an array of all the attributes that have been made accessible to mass-assignment.
def accessible_attributes # :nodoc:
- read_inheritable_attribute("attr_accessible")
+ read_inheritable_attribute(:attr_accessible)
end
# Attributes listed as readonly can be set for a new record, but will be ignored in database updates afterwards.
def attr_readonly(*attributes)
- write_inheritable_attribute("attr_readonly", Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
+ write_inheritable_attribute(:attr_readonly, Set.new(attributes.map(&:to_s)) + (readonly_attributes || []))
end
# Returns an array of all the attributes that have been specified as readonly.
def readonly_attributes
- read_inheritable_attribute("attr_readonly")
+ read_inheritable_attribute(:attr_readonly)
end
# If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object,
@@ -1002,7 +995,7 @@ module ActiveRecord #:nodoc:
# Returns a hash of all the attributes that have been specified for serialization as keys and their class restriction as values.
def serialized_attributes
- read_inheritable_attribute("attr_serialized") or write_inheritable_attribute("attr_serialized", {})
+ read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {})
end
@@ -1223,11 +1216,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.
@@ -1322,8 +1350,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
@@ -1517,7 +1545,7 @@ module ActiveRecord #:nodoc:
sql = "SELECT #{options[:select] || (scope && scope[:select]) || ((options[:joins] || (scope && scope[:joins])) && quoted_table_name + '.*') || '*'} "
sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
- add_joins!(sql, options, scope)
+ add_joins!(sql, options[:joins], scope)
add_conditions!(sql, options[:conditions], scope)
add_group!(sql, options[:group], scope)
@@ -1533,6 +1561,22 @@ module ActiveRecord #:nodoc:
(safe_to_array(first) + safe_to_array(second)).uniq
end
+ def merge_joins(first, second)
+ if first.is_a?(String) && second.is_a?(String)
+ "#{first} #{second}"
+ elsif first.is_a?(String) || second.is_a?(String)
+ if first.is_a?(String)
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, second, nil)
+ "#{first} #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join}"
+ else
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, first, nil)
+ "#{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} #{second}"
+ end
+ else
+ (safe_to_array(first) + safe_to_array(second)).uniq
+ end
+ end
+
# Object#to_a is deprecated, though it does have the desired behavior
def safe_to_array(o)
case o
@@ -1588,16 +1632,15 @@ module ActiveRecord #:nodoc:
end
# The optional scope argument is for the current <tt>:find</tt> scope.
- def add_joins!(sql, options, scope = :auto)
+ def add_joins!(sql, joins, scope = :auto)
scope = scope(:find) if :auto == scope
- [(scope && scope[:joins]), options[:joins]].each do |join|
- case join
- when Symbol, Hash, Array
- join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, join, nil)
- sql << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
- else
- sql << " #{join} "
- end
+ merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
+ case merged_joins
+ when Symbol, Hash, Array
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, merged_joins, nil)
+ sql << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
+ when String
+ sql << " #{merged_joins} "
end
end
@@ -1612,10 +1655,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}) "
@@ -1641,88 +1685,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] }
@@ -1752,7 +1775,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
@@ -1867,6 +1890,8 @@ module ActiveRecord #:nodoc:
hash[method][key] = merge_conditions(params[key], hash[method][key])
elsif key == :include && merge
hash[method][key] = merge_includes(hash[method][key], params[key]).uniq
+ elsif key == :joins && merge
+ hash[method][key] = merge_joins(params[key], hash[method][key])
else
hash[method][key] = hash[method][key] || params[key]
end
@@ -1914,22 +1939,11 @@ module ActiveRecord #:nodoc:
end
end
- def thread_safe_scoped_methods #:nodoc:
+ def scoped_methods #:nodoc:
scoped_methods = (Thread.current[:scoped_methods] ||= {})
scoped_methods[self] ||= []
end
- def single_threaded_scoped_methods #:nodoc:
- @scoped_methods ||= []
- end
-
- # pick up the correct scoped_methods version from @@allow_concurrency
- if @@allow_concurrency
- alias_method :scoped_methods, :thread_safe_scoped_methods
- else
- alias_method :scoped_methods, :single_threaded_scoped_methods
- end
-
def current_scoped_methods #:nodoc:
scoped_methods.last
end
@@ -2594,11 +2608,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
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index e765b46cc2..a675af4787 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -186,11 +186,17 @@ module ActiveRecord
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
sql << " FROM #{connection.quote_table_name(table_name)} "
end
+
+ joins = ""
+ add_joins!(joins, options[:joins], scope)
+
if merged_includes.any?
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins)
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
end
- add_joins!(sql, options, scope)
+
+ sql << joins unless joins.blank?
+
add_conditions!(sql, options[:conditions], scope)
add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
@@ -260,7 +266,14 @@ module ActiveRecord
# column_alias_for("count(*)") # => "count_all"
# column_alias_for("count", "id") # => "count_id"
def column_alias_for(*keys)
- connection.table_alias_for(keys.join(' ').downcase.gsub(/\*/, 'all').gsub(/\W+/, ' ').strip.gsub(/ +/, '_'))
+ table_name = keys.join(' ')
+ table_name.downcase!
+ table_name.gsub!(/\*/, 'all')
+ table_name.gsub!(/\W+/, ' ')
+ table_name.strip!
+ table_name.gsub!(/ +/, '_')
+
+ connection.table_alias_for(table_name)
end
def column_for(field)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index d99e183f9e..dd7ae51096 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -172,16 +172,15 @@ module ActiveRecord
#
# == Transactions
#
- # The entire callback chain for +save+ and +destroy+ runs within their transaction, including
- # the <tt>after_*</tt> hooks. Cancellation does not trigger a rollback. To rollback
- # the transaction just raise an exception the same way you do for regular transactions.
- #
- # Note though that such an exception bypasses the regular call chain in Active Record:
- # If ActiveRecord::Rollback is raised both +save+ and +destroy+ return +nil+. On the other
- # hand <tt>save!</tt> does *not* raise ActiveRecord::RecordNotSaved, and does not raise
- # anything else for that matter, <tt>save!</tt> just returns +nil+ in that case.
- # If any other exception is raised it goes up until it reaches the caller, no matter
- # which one of the three actions was being performed.
+ # 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/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
new file mode 100644
index 0000000000..838b0434b0
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -0,0 +1,281 @@
+require 'monitor'
+require 'set'
+
+module ActiveRecord
+ # Raised when a connection could not be obtained within the connection
+ # acquisition timeout period.
+ class ConnectionTimeoutError < ConnectionNotEstablished
+ end
+
+ module ConnectionAdapters
+ # Connection pool base class for managing ActiveRecord database
+ # connections.
+ #
+ # Connections can be obtained and used from a connection pool in several
+ # ways:
+ #
+ # 1. Simply use ActiveRecord::Base.connection as with ActiveRecord 2.1 and
+ # earlier (pre-connection-pooling). Eventually, when you're done with
+ # the connection(s) and wish it to be returned to the pool, you call
+ # ActiveRecord::Base.clear_active_connections!. This will be the
+ # default behavior for ActiveRecord when used in conjunction with
+ # ActionPack's request handling cycle.
+ # 2. Manually check out a connection from the pool with
+ # ActiveRecord::Base.connection_pool.checkout. You are responsible for
+ # returning this connection to the pool when finished by calling
+ # ActiveRecord::Base.connection_pool.checkin(connection).
+ # 3. Use ActiveRecord::Base.connection_pool.with_connection(&block), which
+ # obtains a connection, yields it as the sole argument to the block,
+ # and returns it to the pool after the block completes.
+ #
+ # There are two connection-pooling-related options that you can add to
+ # your database connection configuration:
+ #
+ # * +pool+: number indicating size of connection pool (default 5)
+ # * +wait_timeout+: number of seconds to block and wait for a connection
+ # before giving up and raising a timeout error (default 5 seconds).
+ class ConnectionPool
+ delegate :verification_timeout, :to => "::ActiveRecord::Base"
+ attr_reader :spec
+
+ def initialize(spec)
+ @spec = spec
+ # The cache of reserved connections mapped to threads
+ @reserved_connections = {}
+ # The mutex used to synchronize pool access
+ @connection_mutex = Monitor.new
+ @queue = @connection_mutex.new_cond
+ # default 5 second timeout
+ @timeout = spec.config[:wait_timeout] || 5
+ # default max pool size to 5
+ @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
+ @connections = []
+ @checked_out = []
+ end
+
+ # Retrieve the connection associated with the current thread, or call
+ # #checkout to obtain one if necessary.
+ #
+ # #connection can be called any number of times; the connection is
+ # held in a hash keyed by the thread id.
+ def connection
+ if conn = @reserved_connections[current_connection_id]
+ conn.verify!(verification_timeout)
+ conn
+ else
+ @reserved_connections[current_connection_id] = checkout
+ end
+ end
+
+ # Signal that the thread is finished with the current connection.
+ # #release_connection releases the connection-thread association
+ # and returns the connection to the pool.
+ def release_connection
+ conn = @reserved_connections.delete(current_connection_id)
+ checkin conn if conn
+ end
+
+ # Reserve a connection, and yield it to a block. Ensure the connection is
+ # checked back in when finished.
+ def with_connection
+ conn = checkout
+ yield conn
+ ensure
+ checkin conn
+ end
+
+ # Returns true if a connection has already been opened.
+ def connected?
+ !@connections.empty?
+ end
+
+ # Disconnect all connections in the pool.
+ def disconnect!
+ @reserved_connections.each do |name,conn|
+ checkin conn
+ end
+ @reserved_connections = {}
+ @connections.each do |conn|
+ conn.disconnect!
+ end
+ @connections = []
+ end
+
+ # Clears the cache which maps classes
+ def clear_reloadable_connections!
+ @reserved_connections.each do |name, conn|
+ checkin conn
+ end
+ @reserved_connections = {}
+ @connections.each do |conn|
+ conn.disconnect! if conn.requires_reloading?
+ end
+ @connections = []
+ end
+
+ # Verify active connections and remove and disconnect connections
+ # associated with stale threads.
+ def verify_active_connections! #:nodoc:
+ clear_stale_cached_connections!
+ @connections.each do |connection|
+ connection.verify!(verification_timeout)
+ end
+ end
+
+ # Return any checked-out connections back to the pool by threads that
+ # are no longer alive.
+ def clear_stale_cached_connections!
+ remove_stale_cached_threads!(@reserved_connections) do |name, conn|
+ checkin conn
+ end
+ end
+
+ # Check-out a database connection from the pool.
+ def checkout
+ # Checkout an available connection
+ conn = @connection_mutex.synchronize do
+ if @checked_out.size < @connections.size
+ checkout_existing_connection
+ elsif @connections.size < @size
+ checkout_new_connection
+ end
+ end
+ return conn if conn
+
+ # No connections available; wait for one
+ @connection_mutex.synchronize do
+ if @queue.wait(@timeout)
+ checkout_existing_connection
+ else
+ raise ConnectionTimeoutError, "could not obtain a database connection in a timely fashion"
+ end
+ end
+ end
+
+ # Check-in a database connection back into the pool.
+ def checkin(conn)
+ @connection_mutex.synchronize do
+ conn.run_callbacks :checkin
+ @checked_out.delete conn
+ @queue.signal
+ end
+ end
+
+ synchronize :clear_reloadable_connections!, :verify_active_connections!,
+ :connected?, :disconnect!, :with => :@connection_mutex
+
+ private
+ def new_connection
+ config = spec.config.reverse_merge(:allow_concurrency => true)
+ ActiveRecord::Base.send(spec.adapter_method, config)
+ end
+
+ def current_connection_id #:nodoc:
+ Thread.current.object_id
+ end
+
+ # Remove stale threads from the cache.
+ def remove_stale_cached_threads!(cache, &block)
+ keys = Set.new(cache.keys)
+
+ Thread.list.each do |thread|
+ keys.delete(thread.object_id) if thread.alive?
+ end
+ keys.each do |key|
+ next unless cache.has_key?(key)
+ block.call(key, cache[key])
+ cache.delete(key)
+ end
+ end
+
+ def checkout_new_connection
+ c = new_connection
+ @connections << c
+ checkout_and_verify(c)
+ end
+
+ def checkout_existing_connection
+ c = (@connections - @checked_out).first
+ checkout_and_verify(c)
+ end
+
+ def checkout_and_verify(c)
+ c.run_callbacks :checkout
+ c.verify!(verification_timeout)
+ @checked_out << c
+ c
+ end
+ end
+
+ class ConnectionHandler
+ def initialize(pools = {})
+ @connection_pools = pools
+ end
+
+ def connection_pools
+ @connection_pools ||= {}
+ end
+
+ def establish_connection(name, spec)
+ @connection_pools[name] = ConnectionAdapters::ConnectionPool.new(spec)
+ end
+
+ # Returns any connections in use by the current thread back to the pool,
+ # and also returns connections to the pool cached by threads that are no
+ # longer alive.
+ def clear_active_connections!
+ @connection_pools.each_value do |pool|
+ pool.release_connection
+ pool.clear_stale_cached_connections!
+ end
+ end
+
+ # Clears the cache which maps classes
+ def clear_reloadable_connections!
+ @connection_pools.each_value {|pool| pool.clear_reloadable_connections! }
+ end
+
+ def clear_all_connections!
+ @connection_pools.each_value {|pool| pool.disconnect! }
+ end
+
+ # Verify active connections.
+ def verify_active_connections! #:nodoc:
+ @connection_pools.each_value {|pool| pool.verify_active_connections! }
+ end
+
+ # Locate the connection of the nearest super class. This can be an
+ # active or defined connection: if it is the latter, it will be
+ # opened and set as the active connection for the class it was defined
+ # for (not necessarily the current class).
+ def retrieve_connection(klass) #:nodoc:
+ pool = retrieve_connection_pool(klass)
+ (pool && pool.connection) or raise ConnectionNotEstablished
+ end
+
+ # Returns true if a connection that's accessible to this class has
+ # already been opened.
+ def connected?(klass)
+ retrieve_connection_pool(klass).connected?
+ end
+
+ # Remove the connection for this class. This will close the active
+ # connection and the defined connection (if they exist). The result
+ # can be used as an argument for establish_connection, for easily
+ # re-establishing the connection.
+ def remove_connection(klass)
+ pool = @connection_pools[klass.name]
+ @connection_pools.delete_if { |key, value| value == pool }
+ pool.disconnect! if pool
+ pool.spec.config if pool
+ end
+
+ def retrieve_connection_pool(klass)
+ pool = @connection_pools[klass.name]
+ return pool if pool
+ return nil if ActiveRecord::Base == klass
+ retrieve_connection_pool klass.superclass
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
index 2a8807fb78..417a333aab 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb
@@ -1,5 +1,3 @@
-require 'set'
-
module ActiveRecord
class Base
class ConnectionSpecification #:nodoc:
@@ -14,158 +12,9 @@ module ActiveRecord
cattr_accessor :verification_timeout, :instance_writer => false
@@verification_timeout = 0
- # The class -> [adapter_method, config] map
- @@defined_connections = {}
-
- # The class -> thread id -> adapter cache. (class -> adapter if not allow_concurrency)
- @@active_connections = {}
-
- class << self
- # Retrieve the connection cache.
- def thread_safe_active_connections #:nodoc:
- @@active_connections[Thread.current.object_id] ||= {}
- end
-
- def single_threaded_active_connections #:nodoc:
- @@active_connections
- end
-
- # pick up the right active_connection method from @@allow_concurrency
- if @@allow_concurrency
- alias_method :active_connections, :thread_safe_active_connections
- else
- alias_method :active_connections, :single_threaded_active_connections
- end
-
- # set concurrency support flag (not thread safe, like most of the methods in this file)
- def allow_concurrency=(threaded) #:nodoc:
- logger.debug "allow_concurrency=#{threaded}" if logger
- return if @@allow_concurrency == threaded
- clear_all_cached_connections!
- @@allow_concurrency = threaded
- method_prefix = threaded ? "thread_safe" : "single_threaded"
- sing = (class << self; self; end)
- [:active_connections, :scoped_methods].each do |method|
- sing.send(:alias_method, method, "#{method_prefix}_#{method}")
- end
- log_connections if logger
- end
-
- def active_connection_name #:nodoc:
- @active_connection_name ||=
- if active_connections[name] || @@defined_connections[name]
- name
- elsif self == ActiveRecord::Base
- nil
- else
- superclass.active_connection_name
- end
- end
-
- def clear_active_connection_name #:nodoc:
- @active_connection_name = nil
- subclasses.each { |klass| klass.clear_active_connection_name }
- end
-
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work unrelated
- # to any of the specific Active Records.
- def connection
- if defined?(@active_connection_name) && (conn = active_connections[@active_connection_name])
- conn
- else
- # retrieve_connection sets the cache key.
- conn = retrieve_connection
- active_connections[@active_connection_name] = conn
- end
- end
-
- # Clears the cache which maps classes to connections.
- def clear_active_connections!
- clear_cache!(@@active_connections) do |name, conn|
- conn.disconnect!
- end
- end
-
- # Clears the cache which maps classes
- def clear_reloadable_connections!
- if @@allow_concurrency
- # With concurrent connections @@active_connections is
- # a hash keyed by thread id.
- @@active_connections.each do |thread_id, conns|
- conns.each do |name, conn|
- if conn.requires_reloading?
- conn.disconnect!
- @@active_connections[thread_id].delete(name)
- end
- end
- end
- else
- @@active_connections.each do |name, conn|
- if conn.requires_reloading?
- conn.disconnect!
- @@active_connections.delete(name)
- end
- end
- end
- end
-
- # Verify active connections.
- def verify_active_connections! #:nodoc:
- if @@allow_concurrency
- remove_stale_cached_threads!(@@active_connections) do |name, conn|
- conn.disconnect!
- end
- end
-
- active_connections.each_value do |connection|
- connection.verify!(@@verification_timeout)
- end
- end
-
- private
- def clear_cache!(cache, thread_id = nil, &block)
- if cache
- if @@allow_concurrency
- thread_id ||= Thread.current.object_id
- thread_cache, cache = cache, cache[thread_id]
- return unless cache
- end
-
- cache.each(&block) if block_given?
- cache.clear
- end
- ensure
- if thread_cache && @@allow_concurrency
- thread_cache.delete(thread_id)
- end
- end
-
- # Remove stale threads from the cache.
- def remove_stale_cached_threads!(cache, &block)
- stale = Set.new(cache.keys)
-
- Thread.list.each do |thread|
- stale.delete(thread.object_id) if thread.alive?
- end
-
- stale.each do |thread_id|
- clear_cache!(cache, thread_id, &block)
- end
- end
-
- def clear_all_cached_connections!
- if @@allow_concurrency
- @@active_connections.each_value do |connection_hash_for_thread|
- connection_hash_for_thread.each_value {|conn| conn.disconnect! }
- connection_hash_for_thread.clear
- end
- else
- @@active_connections.each_value {|conn| conn.disconnect! }
- end
- @@active_connections.clear
- end
- end
+ # The connection handler
+ cattr_accessor :connection_handler, :instance_writer => false
+ @@connection_handler = ConnectionAdapters::ConnectionHandler.new
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work that isn't
@@ -208,9 +57,7 @@ module ActiveRecord
raise AdapterNotSpecified unless defined? RAILS_ENV
establish_connection(RAILS_ENV)
when ConnectionSpecification
- clear_active_connection_name
- @active_connection_name = name
- @@defined_connections[name] = spec
+ @@connection_handler.establish_connection(name, spec)
when Symbol, String
if configuration = configurations[spec.to_s]
establish_connection(configuration)
@@ -243,67 +90,42 @@ module ActiveRecord
end
end
- # Locate the connection of the nearest super class. This can be an
- # active or defined connection: if it is the latter, it will be
- # opened and set as the active connection for the class it was defined
- # for (not necessarily the current class).
- def self.retrieve_connection #:nodoc:
- # Name is nil if establish_connection hasn't been called for
- # some class along the inheritance chain up to AR::Base yet.
- if name = active_connection_name
- if conn = active_connections[name]
- # Verify the connection.
- conn.verify!(@@verification_timeout)
- elsif spec = @@defined_connections[name]
- # Activate this connection specification.
- klass = name.constantize
- klass.connection = spec
- conn = active_connections[name]
- end
+ class << self
+ # Deprecated and no longer has any effect.
+ def allow_concurrency
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency has been deprecated and no longer has any effect. Please remove all references to allow_concurrency.")
end
- conn or raise ConnectionNotEstablished
- end
+ # Deprecated and no longer has any effect.
+ def allow_concurrency=(flag)
+ ActiveSupport::Deprecation.warn("ActiveRecord::Base.allow_concurrency= has been deprecated and no longer has any effect. Please remove all references to allow_concurrency=.")
+ end
- # Returns true if a connection that's accessible to this class has already been opened.
- def self.connected?
- active_connections[active_connection_name] ? true : false
- end
+ # Returns the connection currently associated with the class. This can
+ # also be used to "borrow" the connection to do database work unrelated
+ # to any of the specific Active Records.
+ def connection
+ retrieve_connection
+ end
- # Remove the connection for this class. This will close the active
- # connection and the defined connection (if they exist). The result
- # can be used as an argument for establish_connection, for easily
- # re-establishing the connection.
- def self.remove_connection(klass=self)
- spec = @@defined_connections[klass.name]
- konn = active_connections[klass.name]
- @@defined_connections.delete_if { |key, value| value == spec }
- active_connections.delete_if { |key, value| value == konn }
- konn.disconnect! if konn
- spec.config if spec
- end
+ def connection_pool
+ connection_handler.retrieve_connection_pool(self)
+ end
- # Set the connection for the class.
- def self.connection=(spec) #:nodoc:
- if spec.kind_of?(ActiveRecord::ConnectionAdapters::AbstractAdapter)
- active_connections[name] = spec
- elsif spec.kind_of?(ConnectionSpecification)
- config = spec.config.reverse_merge(:allow_concurrency => @@allow_concurrency)
- self.connection = self.send(spec.adapter_method, config)
- elsif spec.nil?
- raise ConnectionNotEstablished
- else
- establish_connection spec
+ def retrieve_connection
+ connection_handler.retrieve_connection(self)
end
- end
- # connection state logging
- def self.log_connections #:nodoc:
- if logger
- logger.info "Defined connections: #{@@defined_connections.inspect}"
- logger.info "Active connections: #{active_connections.inspect}"
- logger.info "Active connection name: #{@active_connection_name}"
+ def connected?
+ connection_handler.connected?(self)
end
+
+ def remove_connection(klass = self)
+ connection_handler.remove_connection(klass)
+ end
+
+ delegate :clear_active_connections!, :clear_reloadable_connections!,
+ :clear_all_connections!,:verify_active_connections!, :to => :connection_handler
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 2afd6064ad..2fc50b9bfa 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -4,7 +4,6 @@ module ActiveRecord
class << self
def included(base)
base.class_eval do
- attr_accessor :query_cache_enabled
alias_method_chain :columns, :query_cache
alias_method_chain :select_all, :query_cache
end
@@ -16,7 +15,7 @@ module ActiveRecord
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__
def #{method_name}_with_query_dirty(*args)
- clear_query_cache if @query_cache_enabled
+ clear_query_cache if query_cache_enabled
#{method_name}_without_query_dirty(*args)
end
@@ -26,22 +25,38 @@ module ActiveRecord
end
end
+ def query_cache_enabled
+ Thread.current['query_cache_enabled']
+ end
+
+ def query_cache_enabled=(flag)
+ Thread.current['query_cache_enabled'] = flag
+ end
+
+ def query_cache
+ Thread.current['query_cache']
+ end
+
+ def query_cache=(cache)
+ Thread.current['query_cache'] = cache
+ end
+
# Enable the query cache within the block.
def cache
- old, @query_cache_enabled = @query_cache_enabled, true
- @query_cache ||= {}
+ old, self.query_cache_enabled = query_cache_enabled, true
+ self.query_cache ||= {}
yield
ensure
clear_query_cache
- @query_cache_enabled = old
+ self.query_cache_enabled = old
end
# Disable the query cache within the block.
def uncached
- old, @query_cache_enabled = @query_cache_enabled, false
+ old, self.query_cache_enabled = query_cache_enabled, false
yield
ensure
- @query_cache_enabled = old
+ self.query_cache_enabled = old
end
# Clears the query cache.
@@ -51,11 +66,11 @@ module ActiveRecord
# the same SQL query and repeatedly return the same result each time, silently
# undermining the randomness you were expecting.
def clear_query_cache
- @query_cache.clear if @query_cache
+ query_cache.clear if query_cache
end
def select_all_with_query_cache(*args)
- if @query_cache_enabled
+ if query_cache_enabled
cache_sql(args.first) { select_all_without_query_cache(*args) }
else
select_all_without_query_cache(*args)
@@ -63,8 +78,8 @@ module ActiveRecord
end
def columns_with_query_cache(*args)
- if @query_cache_enabled
- @query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
+ if query_cache_enabled
+ query_cache["SHOW FIELDS FROM #{args.first}"] ||= columns_without_query_cache(*args)
else
columns_without_query_cache(*args)
end
@@ -73,11 +88,11 @@ module ActiveRecord
private
def cache_sql(sql)
result =
- if @query_cache.has_key?(sql)
+ if query_cache.has_key?(sql)
log_info(sql, "CACHE", 0.0)
- @query_cache[sql]
+ query_cache[sql]
else
- @query_cache[sql] = yield
+ query_cache[sql] = yield
end
if Array === result
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 0f60a91ef1..bececf82a0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -384,12 +384,8 @@ module ActiveRecord
def add_column_options!(sql, options) #:nodoc:
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
# must explicitly 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
+ 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 47dbf5a5f3..005be9d72f 100644..100755
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -7,6 +7,7 @@ require 'active_record/connection_adapters/abstract/schema_definitions'
require 'active_record/connection_adapters/abstract/schema_statements'
require 'active_record/connection_adapters/abstract/database_statements'
require 'active_record/connection_adapters/abstract/quoting'
+require 'active_record/connection_adapters/abstract/connection_pool'
require 'active_record/connection_adapters/abstract/connection_specification'
require 'active_record/connection_adapters/abstract/query_cache'
@@ -24,6 +25,9 @@ module ActiveRecord
class AbstractAdapter
include Quoting, DatabaseStatements, SchemaStatements
include QueryCache
+ include ActiveSupport::Callbacks
+ define_callbacks :checkout, :checkin
+ checkout :reset!
@@row_even = true
def initialize(connection, logger = nil) #:nodoc:
@@ -51,6 +55,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.
@@ -95,14 +106,25 @@ module ActiveRecord
@active = false
end
+ # Reset the state of this connection, directing the DBMS to clear
+ # transactions and other connection-related server-side state. Usually a
+ # database-dependent operation; the default method simply executes a
+ # ROLLBACK and swallows any exceptions which is probably not enough to
+ # ensure the connection is clean.
+ def reset!
+ silence_stderr do # postgres prints on stderr when you do this w/o a txn
+ execute "ROLLBACK" rescue nil
+ end
+ end
+
# Returns true if its safe to reload the connection between requests for development mode.
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading?
false
end
- # Lazily verify this connection, calling <tt>active?</tt> only if it hasn't
- # been called for +timeout+ seconds.
+ # Lazily verify this connection, calling <tt>active?</tt> only if it
+ # hasn't been called for +timeout+ seconds.
def verify!(timeout)
now = Time.now.to_i
if (now - @last_verification) > timeout
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 204ebaa2e2..14c76ac455 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -280,6 +280,16 @@ module ActiveRecord
@connection.close rescue nil
end
+ def reset!
+ if @connection.respond_to?(:change_user)
+ # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
+ # reset the connection is to change the user to the same user.
+ @connection.change_user(@config[:username], @config[:password], @config[:database])
+ configure_connection
+ else
+ super
+ end
+ end
# DATABASE STATEMENTS ======================================
@@ -529,7 +539,11 @@ module ActiveRecord
end
@connection.real_connect(*@connection_options)
+ configure_connection
+ end
+ def configure_connection
+ encoding = @config[:encoding]
execute("SET NAMES '#{encoding}'") if encoding
# By default, MySQL 'where id is null' selects the last inserted id.
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 856435517a..0c2532f21d 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]
@@ -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
@@ -376,7 +380,7 @@ module ActiveRecord
# There are some incorrectly compiled postgres drivers out there
# that don't define PGconn.escape.
self.class.instance_eval do
- undef_method(:quote_string)
+ remove_method(:quote_string)
end
end
quote_string(s)
@@ -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
diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb
index 63bf8c8f5b..7e246e62ca 100644
--- a/activerecord/lib/active_record/dirty.rb
+++ b/activerecord/lib/active_record/dirty.rb
@@ -123,7 +123,10 @@ module ActiveRecord
attr = attr.to_s
# The attribute already has an unsaved change.
- unless changed_attributes.include?(attr)
+ if changed_attributes.include?(attr)
+ old = changed_attributes[attr]
+ changed_attributes.delete(attr) unless field_changed?(attr, old, value)
+ else
old = clone_attribute_value(:read_attribute, attr)
changed_attributes[attr] = old if field_changed?(attr, old, value)
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 622cfc3c3f..114141a646 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -955,7 +955,7 @@ module Test #:nodoc:
ActiveRecord::Base.connection.rollback_db_transaction
ActiveRecord::Base.connection.decrement_open_transactions
end
- ActiveRecord::Base.verify_active_connections!
+ ActiveRecord::Base.clear_active_connections!
end
private
diff --git a/activerecord/lib/active_record/locale/en-US.rb b/activerecord/lib/active_record/locale/en-US.rb
deleted file mode 100644
index b31e13ed3a..0000000000
--- a/activerecord/lib/active_record/locale/en-US.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-I18n.backend.store_translations :'en-US', {
- :active_record => {
- :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 {{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"
- }
- }
-} \ No newline at end of file
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 fd77f27b77..1d843fff28 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -406,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)
@@ -426,7 +432,7 @@ module ActiveRecord
end
def current_version
- self.class.current_version
+ migrated.last || 0
end
def current_migration
@@ -461,14 +467,22 @@ module ActiveRecord
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
@@ -509,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
@@ -531,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 eb887ee550..83043c2c22 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -101,9 +101,9 @@ module ActiveRecord
class Scope
attr_reader :proxy_scope, :proxy_options
-
+ NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set
[].methods.each do |m|
- unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?|respond_to?)/
+ unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
delegate m, :to => :proxy_found
end
end
@@ -136,12 +136,16 @@ module ActiveRecord
end
end
+ def size
+ @found ? @found.length : count
+ end
+
def empty?
@found ? @found.empty? : count.zero?
end
- def respond_to?(method)
- super || @proxy_scope.respond_to?(method)
+ def respond_to?(method, include_private = false)
+ super || @proxy_scope.respond_to?(method, include_private)
end
def any?
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 d1bf26f331..970da701c7 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -91,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:
@@ -118,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 b8b695e529..577e30ec86 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -21,8 +21,8 @@ module ActiveRecord
class << self
def default_error_messages
- ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('active_record.error_messages').")
- I18n.translate 'active_record.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
@@ -38,22 +38,24 @@ module ActiveRecord
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, message = nil)
- message ||= I18n.translate :"active_record.error_messages.invalid"
+ # 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
+ end
# Will add an error message to each of the attributes in +attributes+ that is 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, generate_message(attr, :empty, :default => custom_message)) 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
@@ -61,16 +63,51 @@ module ActiveRecord
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, generate_message(attr, :blank, :default => custom_message)) if value.blank?
+ add(attr, :blank, :default => custom_message) if value.blank?
end
end
- def generate_message(attr, key, options = {})
- msgs = base_classes(@base.class).map{|klass| :"custom.#{klass.name.underscore}.#{attr}.#{key}"}
- msgs << options[:default] if options[:default]
- msgs << key
+ # 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 = {})
+
+ message, options[:default] = options[:default], message if options[:default].is_a?(Symbol)
+
+ 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}"
- I18n.t nil, options.merge(:default => msgs, :scope => [:active_record, :error_messages])
+ 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.
@@ -166,9 +203,9 @@ module ActiveRecord
if attr == "base"
full_messages << message
else
- key = :"active_record.human_attribute_names.#{@base.class.name.underscore.to_sym}.#{attr}"
- attr_name = I18n.translate(key, :locale => options[:locale], :default => @base.class.human_attribute_name(attr))
- full_messages << attr_name + " " + message
+ #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
@@ -219,16 +256,6 @@ module ActiveRecord
end
end
- protected
-
- # TODO maybe this should be on ActiveRecord::Base, maybe #self_and_descendents_from_active_record
- def base_classes(klass)
- classes = [klass]
- while klass != klass.base_class
- classes << klass = klass.superclass
- end
- classes
- end
end
@@ -398,8 +425,7 @@ module ActiveRecord
validates_each(attr_names, configuration) do |record, attr_name, value|
unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
- message = record.errors.generate_message(attr_name, :confirmation, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :confirmation, :default => configuration[:message])
end
end
end
@@ -441,8 +467,7 @@ module ActiveRecord
validates_each(attr_names,configuration) do |record, attr_name, value|
unless value == configuration[:accept]
- message = record.errors.generate_message(attr_name, :accepted, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :accepted, :default => configuration[:message])
end
end
end
@@ -544,11 +569,9 @@ module ActiveRecord
validates_each(attrs, options) do |record, attr, value|
value = options[:tokenizer].call(value) if value.kind_of?(String)
if value.nil? or value.size < option_value.begin
- message = record.errors.generate_message(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
- record.errors.add(attr, message)
+ record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin)
elsif value.size > option_value.end
- message = record.errors.generate_message(attr, :too_long, :default => options[:too_long], :count => option_value.end)
- record.errors.add(attr, message)
+ record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end)
end
end
when :is, :minimum, :maximum
@@ -563,8 +586,7 @@ module ActiveRecord
unless !value.nil? and value.size.method(validity_checks[option])[option_value]
key = message_options[option]
custom_message = options[:message] || options[key]
- message = record.errors.generate_message(attr, key, :default => custom_message, :count => option_value)
- record.errors.add(attr, message)
+ record.errors.add(attr, key, :default => custom_message, :count => option_value)
end
end
end
@@ -629,12 +651,11 @@ module ActiveRecord
if value.nil?
comparison_operator = "IS ?"
- else
+ elsif is_text_column
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
-
- if is_text_column
- value = value.to_s
- end
+ value = value.to_s
+ else
+ comparison_operator = "= ?"
end
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
@@ -644,7 +665,7 @@ module ActiveRecord
condition_params = [value]
else
condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
- condition_params = [value.downcase]
+ condition_params = [value.chars.downcase]
end
if scope = configuration[:scope]
@@ -662,8 +683,7 @@ module ActiveRecord
finder_class.with_exclusive_scope do
if finder_class.exists?([condition_sql, *condition_params])
- message = record.errors.generate_message(attr_name, :taken, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
end
end
end
@@ -701,8 +721,7 @@ module ActiveRecord
validates_each(attr_names, configuration) do |record, attr_name, value|
unless value.to_s =~ configuration[:with]
- message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
end
end
end
@@ -732,12 +751,11 @@ module ActiveRecord
enum = configuration[:in] || configuration[:within]
- 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?")
+ 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|
unless enum.include?(value)
- message = record.errors.generate_message(attr_name, :inclusion, :default => configuration[:message], :value => value)
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
end
end
end
@@ -767,12 +785,11 @@ module ActiveRecord
enum = configuration[:in] || configuration[:within]
- 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?")
+ 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|
if enum.include?(value)
- message = record.errors.generate_message(attr_name, :exclusion, :default => configuration[:message], :value => value)
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
end
end
end
@@ -814,8 +831,7 @@ module ActiveRecord
validates_each(attr_names, configuration) do |record, attr_name, value|
unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v }
- message = record.errors.generate_message(attr_name, :invalid, :default => configuration[:message], :value => value)
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
end
end
end
@@ -864,8 +880,7 @@ module ActiveRecord
if configuration[:only_integer]
unless raw_value.to_s =~ /\A[+-]?\d+\Z/
- message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
raw_value = raw_value.to_i
@@ -873,8 +888,7 @@ module ActiveRecord
begin
raw_value = Kernel.Float(raw_value)
rescue ArgumentError, TypeError
- message = record.errors.generate_message(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
next
end
end
@@ -883,12 +897,10 @@ module ActiveRecord
case option
when :odd, :even
unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
- message = record.errors.generate_message(attr_name, option, :value => raw_value, :default => configuration[:message])
- record.errors.add(attr_name, message)
+ record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
end
else
- message = record.errors.generate_message(attr_name, option, :default => configuration[:message], :value => raw_value, :count => 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/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 1f8a1090eb..8c9ae8a031 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -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_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 58506574f8..e78624a98d 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -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 f71b122ff0..edca3c622b 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
@@ -223,10 +223,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
devel = Developer.find(1)
proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
assert !devel.projects.loaded?
-
+
assert_equal devel.projects.last, proj
assert devel.projects.loaded?
-
+
assert proj.new_record?
devel.save
assert !proj.new_record?
@@ -251,10 +251,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
devel = Developer.find(1)
proj = devel.projects.create("name" => "Projekt")
assert !devel.projects.loaded?
-
+
assert_equal devel.projects.last, proj
assert devel.projects.loaded?
-
+
assert !proj.new_record?
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
@@ -274,10 +274,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_creation_respects_hash_condition
post = categories(:general).post_with_conditions.build(:body => '')
-
+
assert post.save
assert_equal 'Yet Another Testing Title', post.title
-
+
another_post = categories(:general).post_with_conditions.create(:body => '')
assert !another_post.new_record?
@@ -288,7 +288,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
dev = developers(:jamis)
dev.projects << projects(:active_record)
dev.projects << projects(:active_record)
-
+
assert_equal 3, dev.projects.size
assert_equal 1, dev.projects.uniq.size
end
@@ -415,13 +415,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
project.developers.class # force load target
developer = project.developers.first
-
+
assert_no_queries do
assert project.developers.loaded?
assert project.developers.include?(developer)
end
end
-
+
def test_include_checks_if_record_exists_if_target_not_loaded
project = projects(:active_record)
developer = project.developers.first
@@ -450,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
@@ -634,14 +641,29 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal [projects(:active_record).id], developers(:jamis).project_ids
end
+ def test_get_ids_for_loaded_associations
+ developer = developers(:david)
+ developer.projects(true)
+ assert_queries(0) do
+ developer.project_ids
+ developer.project_ids
+ end
+ end
+
+ def test_get_ids_for_unloaded_associations_does_not_load_them
+ developer = developers(:david)
+ assert !developer.projects.loaded?
+ assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort
+ assert !developer.projects.loaded?
+ end
+
def test_assign_ids
developer = Developer.new("name" => "Joe")
developer.project_ids = projects(:active_record, :action_controller).map(&:id)
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
@@ -650,8 +672,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
@@ -698,4 +719,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
# due to Unknown column 'authors.id'
assert Category.find(1).posts_with_authors_sorted_by_author_id.find_by_title('Welcome to the weblog')
end
+
+ def test_counting_on_habtm_association_and_not_array
+ david = Developer.find(1)
+ # Extra parameter just to make sure we aren't falling back to
+ # Array#count in Ruby >=1.8.7, which would raise an ArgumentError
+ assert_nothing_raised { david.projects.count(:all, :conditions => '1=1') }
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index b806e885e1..feac4b002b 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -48,6 +48,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, Firm.find(:first).plain_clients.count(:name)
end
+ def test_counting_with_association_limit
+ firm = companies(:first_firm)
+ assert_equal firm.limited_clients.length, firm.limited_clients.size
+ assert_equal firm.limited_clients.length, firm.limited_clients.count
+ end
+
def test_finding
assert_equal 2, Firm.find(:first).clients.length
end
@@ -378,7 +384,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
assert !company.clients_of_firm.loaded?
-
+
assert_equal "Another Client", new_client.name
assert new_client.new_record?
assert_equal new_client, company.clients_of_firm.last
@@ -395,10 +401,22 @@ 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"}]) }
-
+
assert_equal 2, new_clients.size
company.name += '-changed'
assert_queries(3) { assert company.save }
@@ -637,10 +655,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_creation_respects_hash_condition
ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build
-
+
assert ms_client.save
assert_equal 'Microsoft', ms_client.name
-
+
another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create
assert !another_ms_client.new_record?
@@ -812,6 +830,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids
end
+ def test_get_ids_for_loaded_associations
+ company = companies(:first_firm)
+ company.clients(true)
+ assert_queries(0) do
+ company.client_ids
+ company.client_ids
+ end
+ end
+
+ def test_get_ids_for_unloaded_associations_does_not_load_them
+ company = companies(:first_firm)
+ assert !company.clients.loaded?
+ assert_equal [companies(:first_client).id, companies(:second_client).id], company.client_ids
+ assert !company.clients.loaded?
+ end
+
def test_assign_ids
firm = Firm.new("name" => "Apple")
firm.client_ids = [companies(:first_client).id, companies(:second_client).id]
@@ -882,7 +916,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 4, authors(:david).limited_comments.find(:all, :conditions => "comments.type = 'SpecialComment'", :limit => 9_000).length
assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length
end
-
+
def test_find_all_include_over_the_same_table_for_through
assert_equal 2, people(:michael).posts.find(:all, :include => :people).length
end
@@ -919,13 +953,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_include_loads_collection_if_target_uses_finder_sql
firm = companies(:first_firm)
client = firm.clients_using_sql.first
-
+
firm.reload
assert ! firm.clients_using_sql.loaded?
assert firm.clients_using_sql.include?(client)
assert firm.clients_using_sql.loaded?
end
-
+
def test_include_returns_false_for_non_matching_record_to_verify_scoping
firm = companies(:first_firm)
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 d51a3c7e1c..0be050ec81 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -196,4 +196,28 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# due to Unknown column 'comments.id'
assert Person.find(1).posts_with_comments_sorted_by_comment_id.find_by_title('Welcome to the weblog')
end
+
+ def test_count_with_include_should_alias_join_table
+ assert_equal 2, people(:michael).posts.count(:include => :readers)
+ end
+
+ def test_get_ids
+ assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort
+ end
+
+ def test_get_ids_for_loaded_associations
+ person = people(:michael)
+ person.posts(true)
+ assert_queries(0) do
+ person.post_ids
+ person.post_ids
+ end
+ end
+
+ def test_get_ids_for_unloaded_associations_does_not_load_them
+ person = people(:michael)
+ assert !person.posts.loaded?
+ assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
+ assert !person.posts.loaded?
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 99639849a5..ec06be5eba 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..77e3cb1776 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,39 @@ 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
+
+ def test_uninitialized_has_one_through_should_return_nil_for_unsaved_record
+ assert_nil Member.new.club
+ end
+
+ def test_assigning_association_correctly_assigns_target
+ new_member = Member.create(:name => "Chris")
+ new_member.club = new_club = Club.create(:name => "LRUG")
+ assert_equal new_club, new_member.club.target
+ 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 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/base_test.rb b/activerecord/test/cases/base_test.rb
index 36d30ade5e..ac9081e003 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -138,7 +138,7 @@ class BasicsTest < ActiveRecord::TestCase
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
@@ -616,7 +616,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_update_counter
- category = Category.first
+ category = categories(:general)
assert_nil category.categorizations_count
assert_equal 2, category.categorizations.count
@@ -880,7 +880,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_mass_assignment_protection_against_class_attribute_writers
[:logger, :configurations, :primary_key_prefix_type, :table_name_prefix, :table_name_suffix, :pluralize_table_names, :colorize_logging,
- :default_timezone, :allow_concurrency, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method|
+ :default_timezone, :schema_format, :verification_timeout, :lock_optimistically, :record_timestamps].each do |method|
assert Task.respond_to?(method)
assert Task.respond_to?("#{method}=")
assert Task.new.respond_to?(method)
@@ -1114,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)
@@ -1126,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)
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/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index feb47a15a8..4fe1d79f4d 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -191,6 +191,42 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.changed?
end
+ def test_reverted_changes_are_not_dirty
+ phrase = "shiver me timbers"
+ pirate = Pirate.create!(:catchphrase => phrase)
+ pirate.catchphrase = "*hic*"
+ assert pirate.changed?
+ pirate.catchphrase = phrase
+ assert !pirate.changed?
+ end
+
+ def test_reverted_changes_are_not_dirty_after_multiple_changes
+ phrase = "shiver me timbers"
+ pirate = Pirate.create!(:catchphrase => phrase)
+ 10.times do |i|
+ pirate.catchphrase = "*hic*" * i
+ assert pirate.changed?
+ end
+ assert pirate.changed?
+ pirate.catchphrase = phrase
+ assert !pirate.changed?
+ end
+
+
+ def test_reverted_changes_are_not_dirty_going_from_nil_to_value_and_back
+ pirate = Pirate.create!(:catchphrase => "Yar!")
+
+ pirate.parrot_id = 1
+ assert pirate.changed?
+ assert pirate.parrot_id_changed?
+ assert !pirate.catchphrase_changed?
+
+ pirate.parrot_id = nil
+ assert !pirate.changed?
+ assert !pirate.parrot_id_changed?
+ assert !pirate.catchphrase_changed?
+ end
+
def test_save_should_store_serialized_attributes_even_with_partial_updates
with_partial_updates(Topic) do
topic = Topic.create!(:content => {:a => "a"})
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/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..ea06e377e3
--- /dev/null
+++ b/activerecord/test/cases/i18n_test.rb
@@ -0,0 +1,41 @@
+require "cases/helper"
+require 'models/topic'
+require 'models/reply'
+
+class ActiveRecordI18nTests < Test::Unit::TestCase
+
+ def setup
+ I18n.backend = I18n::Backend::Simple.new
+ end
+
+ def test_translated_model_attributes
+ I18n.backend.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.backend.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.backend.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.backend.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.backend.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.backend.store_translations 'en-US', :activerecord => {:models => {:topic => 'topic model'} }
+ assert_equal 'topic model', Reply.human_name
+ end
+end
+
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 701187223f..bbe8582466 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -210,13 +210,6 @@ unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :OpenBaseAdapter)
def setup
# Avoid introspection queries during tests.
Person.columns; Reader.columns
-
- @allow_concurrency = ActiveRecord::Base.allow_concurrency
- ActiveRecord::Base.allow_concurrency = true
- end
-
- def teardown
- ActiveRecord::Base.allow_concurrency = @allow_concurrency
end
# Test typical find.
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index ee66ac948d..af6fcd32ad 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/author'
require 'models/developer'
require 'models/project'
require 'models/comment'
@@ -6,7 +7,7 @@ require 'models/post'
require 'models/category'
class MethodScopingTest < ActiveRecord::TestCase
- fixtures :developers, :projects, :comments, :posts, :developers_projects
+ fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
def test_set_conditions
Developer.with_scope(:find => { :conditions => 'just a test...' }) do
@@ -97,6 +98,46 @@ class MethodScopingTest < ActiveRecord::TestCase
assert_equal developers(:david).attributes, scoped_developers.first.attributes
end
+ def test_scoped_find_using_new_style_joins
+ scoped_developers = Developer.with_scope(:find => { :joins => :projects }) do
+ Developer.find(:all, :conditions => 'projects.id = 2')
+ end
+ assert scoped_developers.include?(developers(:david))
+ assert !scoped_developers.include?(developers(:jamis))
+ assert_equal 1, scoped_developers.size
+ assert_equal developers(:david).attributes, scoped_developers.first.attributes
+ end
+
+ def test_scoped_find_merges_old_style_joins
+ scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id ' }) do
+ Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1')
+ end
+ assert scoped_authors.include?(authors(:david))
+ assert !scoped_authors.include?(authors(:mary))
+ assert_equal 1, scoped_authors.size
+ assert_equal authors(:david).attributes, scoped_authors.first.attributes
+ end
+
+ def test_scoped_find_merges_new_style_joins
+ scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
+ Author.find(:all, :select => 'DISTINCT authors.*', :joins => :comments, :conditions => 'comments.id = 1')
+ end
+ assert scoped_authors.include?(authors(:david))
+ assert !scoped_authors.include?(authors(:mary))
+ assert_equal 1, scoped_authors.size
+ assert_equal authors(:david).attributes, scoped_authors.first.attributes
+ end
+
+ def test_scoped_find_merges_new_and_old_style_joins
+ scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
+ Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1')
+ end
+ assert scoped_authors.include?(authors(:david))
+ assert !scoped_authors.include?(authors(:mary))
+ assert_equal 1, scoped_authors.size
+ assert_equal authors(:david).attributes, scoped_authors.first.attributes
+ end
+
def test_scoped_count_include
# with the include, will retrieve only developers for the given project
Developer.with_scope(:find => { :include => :projects }) do
@@ -152,7 +193,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
class NestedScopingTest < ActiveRecord::TestCase
- fixtures :developers, :projects, :comments, :posts
+ fixtures :authors, :developers, :projects, :comments, :posts
def test_merge_options
Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do
@@ -357,6 +398,42 @@ class NestedScopingTest < ActiveRecord::TestCase
assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods')
end
end
+
+ def test_nested_scoped_find_merges_old_style_joins
+ scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id' }) do
+ Author.with_scope(:find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do
+ Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1')
+ end
+ end
+ assert scoped_authors.include?(authors(:david))
+ assert !scoped_authors.include?(authors(:mary))
+ assert_equal 1, scoped_authors.size
+ assert_equal authors(:david).attributes, scoped_authors.first.attributes
+ end
+
+ def test_nested_scoped_find_merges_new_style_joins
+ scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
+ Author.with_scope(:find => { :joins => :comments }) do
+ Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1')
+ end
+ end
+ assert scoped_authors.include?(authors(:david))
+ assert !scoped_authors.include?(authors(:mary))
+ assert_equal 1, scoped_authors.size
+ assert_equal authors(:david).attributes, scoped_authors.first.attributes
+ end
+
+ def test_nested_scoped_find_merges_new_and_old_style_joins
+ scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
+ Author.with_scope(:find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do
+ Author.find(:all, :select => 'DISTINCT authors.*', :joins => '', :conditions => 'comments.id = 1')
+ end
+ end
+ assert scoped_authors.include?(authors(:david))
+ assert !scoped_authors.include?(authors(:mary))
+ assert_equal 1, scoped_authors.size
+ assert_equal authors(:david).attributes, scoped_authors.first.attributes
+ end
end
class HasManyScopingTest< ActiveRecord::TestCase
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 920f719995..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'],
@@ -1192,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/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index bd6ec23853..444debd255 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
@@ -51,6 +52,11 @@ class NamedScopeTest < ActiveRecord::TestCase
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)
@@ -238,4 +244,31 @@ 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_rand_should_select_a_random_object_from_proxy
+ assert Topic.approved.rand.is_a?(Topic)
+ 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
+
+ def test_size_should_use_count_when_results_are_not_loaded
+ topics = Topic.base
+ assert_queries(1) do
+ assert_sql(/COUNT/i) { topics.size }
+ end
+ end
+
+ def test_size_should_use_length_when_results_are_loaded
+ topics = Topic.base
+ topics.reload # force load
+ assert_no_queries do
+ topics.size # use loaded (no query)
+ end
+ end
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
new file mode 100644
index 0000000000..078ca1d679
--- /dev/null
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -0,0 +1,87 @@
+require "cases/helper"
+
+class PooledConnectionsTest < ActiveRecord::TestCase
+ def setup
+ super
+ @connection = ActiveRecord::Base.remove_connection
+ end
+
+ def teardown
+ ActiveRecord::Base.clear_all_connections!
+ ActiveRecord::Base.establish_connection(@connection)
+ super
+ end
+
+ def checkout_connections
+ ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :wait_timeout => 0.3}))
+ @connections = []
+ @timed_out = 0
+
+ 4.times do
+ Thread.new do
+ begin
+ @connections << ActiveRecord::Base.connection_pool.checkout
+ rescue ActiveRecord::ConnectionTimeoutError
+ @timed_out += 1
+ end
+ end.join
+ end
+ end
+
+ def test_pooled_connection_checkout
+ checkout_connections
+ assert_equal @connections.length, 2
+ assert_equal @timed_out, 2
+ end
+
+ def checkout_checkin_connections(pool_size, threads)
+ ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :wait_timeout => 0.5}))
+ @connection_count = 0
+ @timed_out = 0
+ threads.times do
+ Thread.new do
+ begin
+ conn = ActiveRecord::Base.connection_pool.checkout
+ sleep 0.1
+ ActiveRecord::Base.connection_pool.checkin conn
+ @connection_count += 1
+ rescue ActiveRecord::ConnectionTimeoutError
+ @timed_out += 1
+ end
+ end.join
+ end
+ end
+
+ def test_pooled_connection_checkin_one
+ checkout_checkin_connections 1, 2
+ assert_equal 2, @connection_count
+ assert_equal 0, @timed_out
+ end
+
+ def test_pooled_connection_checkin_two
+ checkout_checkin_connections 2, 3
+ assert_equal 3, @connection_count
+ assert_equal 0, @timed_out
+ end
+
+ def test_pooled_connection_checkout_existing_first
+ ActiveRecord::Base.establish_connection(@connection.merge({:pool => 1}))
+ conn_pool = ActiveRecord::Base.connection_pool
+ conn = conn_pool.checkout
+ conn_pool.checkin(conn)
+ conn = conn_pool.checkout
+ assert ActiveRecord::ConnectionAdapters::AbstractAdapter === conn
+ conn_pool.checkin(conn)
+ end
+end unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name
+
+class AllowConcurrencyDeprecatedTest < ActiveRecord::TestCase
+ def test_allow_concurrency_is_deprecated
+ assert_deprecated('ActiveRecord::Base.allow_concurrency') do
+ ActiveRecord::Base.allow_concurrency
+ end
+ assert_deprecated('ActiveRecord::Base.allow_concurrency=') do
+ ActiveRecord::Base.allow_concurrency = true
+ end
+ end
+end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index eae2104531..171d0e6dae 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -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/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/threaded_connections_test.rb b/activerecord/test/cases/threaded_connections_test.rb
deleted file mode 100644
index 28f8302367..0000000000
--- a/activerecord/test/cases/threaded_connections_test.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require "cases/helper"
-require 'models/topic'
-require 'models/reply'
-
-unless %w(FrontBase).include? ActiveRecord::Base.connection.adapter_name
- class ThreadedConnectionsTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- fixtures :topics
-
- def setup
- @connection = ActiveRecord::Base.remove_connection
- @connections = []
- @allow_concurrency = ActiveRecord::Base.allow_concurrency
- end
-
- def teardown
- # clear the connection cache
- ActiveRecord::Base.send(:clear_all_cached_connections!)
- # set allow_concurrency to saved value
- ActiveRecord::Base.allow_concurrency = @allow_concurrency
- # reestablish old connection
- ActiveRecord::Base.establish_connection(@connection)
- end
-
- def gather_connections(use_threaded_connections)
- ActiveRecord::Base.allow_concurrency = use_threaded_connections
- ActiveRecord::Base.establish_connection(@connection)
-
- 5.times do
- Thread.new do
- Topic.find :first
- @connections << ActiveRecord::Base.active_connections.values.first
- end.join
- end
- end
-
- def test_threaded_connections
- gather_connections(true)
- assert_equal @connections.uniq.length, 5
- end
-
- def test_unthreaded_connections
- gather_connections(false)
- assert_equal @connections.uniq.length, 1
- end
- end
-end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 06a76eacc3..8383ba58e9 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,21 +269,20 @@ class TransactionTest < ActiveRecord::TestCase
def remove_exception_raising_after_create_callback_to_topic
Topic.class_eval { remove_method :after_create }
end
-end
-if current_adapter?(:PostgreSQLAdapter)
- class ConcurrentTransactionTest < TransactionTest
- def setup
- @allow_concurrency = ActiveRecord::Base.allow_concurrency
- ActiveRecord::Base.allow_concurrency = true
- super
- 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
- def teardown
- super
- ActiveRecord::Base.allow_concurrency = @allow_concurrency
+ 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)
+ class ConcurrentTransactionTest < TransactionTest
# This will cause transactions to overlap and fail unless they are performed on
# separate database connections.
def test_transaction_per_thread
diff --git a/activerecord/test/cases/validations_i18n_test.rb b/activerecord/test/cases/validations_i18n_test.rb
index 86834fe920..090f347a20 100644
--- a/activerecord/test/cases/validations_i18n_test.rb
+++ b/activerecord/test/cases/validations_i18n_test.rb
@@ -6,18 +6,18 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
def setup
reset_callbacks Topic
@topic = Topic.new
- I18n.backend.store_translations('en-US', :active_record => {:error_messages => {:custom => nil}})
+ I18n.backend.store_translations('en-US', :activerecord => {:errors => {:messages => {:custom => nil}}})
end
-
+
def teardown
reset_callbacks Topic
- load 'active_record/locale/en-US.rb'
+ 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")
@@ -25,7 +25,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
topic
end
end
-
+
def reset_callbacks(*models)
models.each do |model|
model.instance_variable_set("@validate_callbacks", ActiveSupport::Callbacks::CallbackChain.new)
@@ -33,28 +33,83 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
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
- global_scope = [:active_record, :error_messages]
- custom_scope = global_scope + [:custom, 'topic', :title]
- I18n.expects(:t).with nil, :scope => [:active_record, :error_messages], :default => [:"custom.topic.title.invalid", 'default from class def', :invalid]
- @topic.errors.generate_message :title, :invalid, :default => 'default from class def'
+ 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
- custom_scope = [:active_record, :error_messages, :custom, 'topic', :title]
- I18n.expects(:t).with nil, :scope => [:active_record, :error_messages], :default => [:"custom.reply.title.invalid", :"custom.topic.title.invalid", 'default from class def', :invalid]
+ 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
@@ -78,12 +133,12 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
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(:"active_record.human_attribute_names.topic.title", :locale => 'en-US', :default => 'Title').returns('Title')
+ @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
-
+ end
+
# ActiveRecord::Validations
uses_mocha 'ActiveRecord::Validations' do
# validates_confirmation_of w/ mocha
@@ -101,7 +156,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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
@@ -115,9 +170,9 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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})
@@ -129,7 +184,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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})
@@ -183,7 +238,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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
@@ -197,23 +252,23 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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})
+ @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'})
+ @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
@@ -229,7 +284,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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
@@ -245,7 +300,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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
@@ -261,7 +316,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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
@@ -277,7 +332,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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
@@ -293,7 +348,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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
@@ -309,7 +364,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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
@@ -325,7 +380,7 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
@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
@@ -340,284 +395,497 @@ class ActiveRecordValidationsI18nTests < Test::Unit::TestCase
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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:confirmation => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:confirmation => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:confirmation => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:accepted => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:accepted => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:accepted => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:blank => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:blank => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:blank => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:too_short => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:too_short => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:too_short => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:wrong_length => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:wrong_length => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:wrong_length => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:wrong_length => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:invalid => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:invalid => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:inclusion => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:inclusion => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:inclusion => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:exclusion => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:exclusion => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:exclusion => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:not_a_number => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:not_a_number => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:not_a_number => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:not_a_number => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:odd => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:odd => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:odd => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:title => {:less_than => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:less_than => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:less_than => 'global 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 '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', :active_record => {:error_messages => {:custom => {:topic => {:replies => {:invalid => 'custom message'}}}}}
- I18n.backend.store_translations 'en-US', :active_record => {:error_messages => {:invalid => 'global message'}}
-
+ 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', :active_record => {:error_messages => {:invalid => 'global message'}}
-
+ 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 \ No newline at end of file
+
+ def test_validations_with_message_symbol_must_translate
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:messages => {:custom_error => "I am a custom error"}}}
+ Topic.validates_presence_of :title, :message => :custom_error
+ @topic.title = nil
+ @topic.valid?
+ assert_equal "I am a custom error", @topic.errors.on(:title)
+ end
+
+ def test_validates_with_message_symbol_must_translate_per_attribute
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:attributes => {:title => {:custom_error => "I am a custom error"}}}}}}
+ Topic.validates_presence_of :title, :message => :custom_error
+ @topic.title = nil
+ @topic.valid?
+ assert_equal "I am a custom error", @topic.errors.on(:title)
+ end
+
+ def test_validates_with_message_symbol_must_translate_per_model
+ I18n.backend.store_translations 'en-US', :activerecord => {:errors => {:models => {:topic => {:custom_error => "I am a custom error"}}}}
+ Topic.validates_presence_of :title, :message => :custom_error
+ @topic.title = nil
+ @topic.valid?
+ assert_equal "I am a custom error", @topic.errors.on(:title)
+ end
+
+ def test_validates_with_message_string
+ Topic.validates_presence_of :title, :message => "I am a custom error"
+ @topic.title = nil
+ @topic.valid?
+ assert_equal "I am a custom error", @topic.errors.on(:title)
+ 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 4b2d28c80b..4999d93a86 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -451,6 +451,18 @@ class ValidationsTest < ActiveRecord::TestCase
t2.title = nil
assert t2.valid?, "should validate with nil"
assert t2.save, "should save with nil"
+
+ with_kcode('UTF8') do
+ t_utf8 = Topic.new("title" => "Я тоже уникальный!")
+ assert t_utf8.save, "Should save t_utf8 as unique"
+
+ # If database hasn't UTF-8 character set, this test fails
+ if Topic.find(t_utf8, :select => 'LOWER(title) AS title').title == "я тоже уникальный!"
+ t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!")
+ assert !t2_utf8.valid?, "Shouldn't be valid"
+ assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique"
+ end
+ end
end
def test_validate_case_sensitive_uniqueness
@@ -1420,8 +1432,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
@@ -1434,8 +1446,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/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/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 487a08f4ab..ab5c7c520b 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -60,7 +60,7 @@ 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|