aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG108
-rw-r--r--activerecord/lib/active_record.rb151
-rwxr-xr-xactiverecord/lib/active_record/associations.rb137
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb62
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb2
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb44
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb20
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb4
-rw-r--r--activerecord/lib/active_record/autosave_association.rb46
-rwxr-xr-xactiverecord/lib/active_record/base.rb449
-rw-r--r--activerecord/lib/active_record/calculations.rb202
-rw-r--r--activerecord/lib/active_record/callbacks.rb61
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb12
-rwxr-xr-xactiverecord/lib/active_record/connection_adapters/abstract_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb6
-rw-r--r--activerecord/lib/active_record/locale/en.yml3
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb42
-rw-r--r--activerecord/lib/active_record/named_scope.rb38
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb48
-rw-r--r--activerecord/lib/active_record/notifications.rb5
-rw-r--r--activerecord/lib/active_record/railtie.rb71
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb31
-rw-r--r--activerecord/lib/active_record/railties/databases.rake469
-rw-r--r--activerecord/lib/active_record/reflection.rb18
-rw-r--r--activerecord/lib/active_record/relation.rb214
-rw-r--r--activerecord/lib/active_record/relation/calculation_methods.rb177
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb120
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb45
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb141
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb75
-rw-r--r--activerecord/lib/active_record/validations.rb2
-rw-r--r--activerecord/lib/active_record/validations/associated.rb16
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb135
-rw-r--r--activerecord/test/cases/associations/eager_test.rb17
-rw-r--r--activerecord/test/cases/associations/habtm_join_table_test.rb16
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb1
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb22
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb6
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb30
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb292
-rw-r--r--activerecord/test/cases/associations_test.rb10
-rw-r--r--activerecord/test/cases/autosave_association_test.rb60
-rwxr-xr-xactiverecord/test/cases/base_test.rb41
-rw-r--r--activerecord/test/cases/batches_test.rb2
-rw-r--r--activerecord/test/cases/calculations_test.rb20
-rw-r--r--activerecord/test/cases/dirty_test.rb2
-rw-r--r--activerecord/test/cases/finder_test.rb65
-rw-r--r--activerecord/test/cases/helper.rb14
-rw-r--r--activerecord/test/cases/locking_test.rb20
-rw-r--r--activerecord/test/cases/method_scoping_test.rb174
-rw-r--r--activerecord/test/cases/migration_test.rb47
-rw-r--r--activerecord/test/cases/multiple_db_test.rb6
-rw-r--r--activerecord/test/cases/named_scope_test.rb4
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb53
-rw-r--r--activerecord/test/cases/readonly_test.rb41
-rw-r--r--activerecord/test/cases/relations_test.rb495
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb5
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb4
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb13
-rw-r--r--activerecord/test/cases/validations_repair_helper.rb23
-rw-r--r--activerecord/test/cases/validations_test.rb32
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb11
-rw-r--r--activerecord/test/fixtures/edges.yml3
-rw-r--r--activerecord/test/fixtures/faces.yml4
-rw-r--r--activerecord/test/fixtures/interests.yml6
-rw-r--r--activerecord/test/models/face.rb4
-rw-r--r--activerecord/test/models/interest.rb1
-rw-r--r--activerecord/test/models/man.rb2
-rw-r--r--activerecord/test/models/pirate.rb2
-rw-r--r--activerecord/test/models/reply.rb6
-rw-r--r--activerecord/test/models/ship.rb2
-rw-r--r--activerecord/test/schema/schema.rb6
74 files changed, 3150 insertions, 1395 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG
index d8bfb1916d..0cfd8cdc87 100644
--- a/activerecord/CHANGELOG
+++ b/activerecord/CHANGELOG
@@ -1,5 +1,113 @@
*Edge*
+* Add Relation#except. [Pratik Naik]
+
+ one_red_item = Item.where(:colour => 'red').limit(1)
+ all_items = one_red_item.except(:where, :limit)
+
+* Add Relation#delete_all. [Pratik Naik]
+
+ Item.where(:colour => 'red').delete_all
+
+* Add Model.having and Relation#having. [Pratik Naik]
+
+ Developer.group("salary").having("sum(salary) > 10000").select("salary")
+
+* Add Relation#count. [Pratik Naik]
+
+ legends = People.where("age > 100")
+ legends.count
+ legends.count(:age, :distinct => true)
+ legends.select('id').count
+
+* Add Model.readonly and association_collection#readonly finder method. [Pratik Naik]
+
+ Post.readonly.to_a # Load all posts in readonly mode
+ @user.items.readonly(false).to_a # Load all the user items in writable mode
+
+* Add .lock finder method [Pratik Naik]
+
+ User.lock.where(:name => 'lifo').to_a
+
+ old_items = Item.where("age > 100")
+ old_items.lock.each {|i| .. }
+
+* Add Model.from and association_collection#from finder methods [Pratik Naik]
+
+ user = User.scoped
+ user.select('*').from('users, items')
+
+* Add relation.destroy_all [Pratik Naik]
+
+ old_items = Item.where("age > 100")
+ old_items.destroy_all
+
+* Add relation.exists? [Pratik Naik]
+
+ red_items = Item.where(:colours => 'red')
+ red_items.exists?
+ red_items.exists?(1)
+
+* Add find(ids) to relations. [Pratik Naik]
+
+ old_users = User.order("age DESC")
+ old_users.find(1)
+ old_users.find(1, 2, 3)
+
+* Add new finder methods to association collection. [Pratik Naik]
+
+ class User < ActiveRecord::Base
+ has_many :items
+ end
+
+ user = User.first
+ user.items.where(:items => {:colour => 'red'})
+ user.items.select('items.id')
+
+* Add relation.reload to force reloading the records. [Pratik Naik]
+
+ topics = Topic.scoped
+ topics.to_a # force load
+ topics.first # returns a cached record
+ topics.reload
+ topics.first # Fetches a new record from the database
+
+* Rename Model.conditions and relation.conditions to .where. [Pratik Naik]
+
+ Before :
+ User.conditions(:name => 'lifo')
+ User.select('id').conditions(["age > ?", 21])
+
+ Now :
+ User.where(:name => 'lifo')
+ User.select('id').where(["age > ?", 21])
+
+* Add Model.select/group/order/limit/joins/conditions/preload/eager_load class methods returning a lazy relation. [Pratik Naik]
+
+ Examples :
+
+ posts = Post.select('id).order('name') # Returns a lazy relation
+ posts.each {|p| puts p.id } # Fires "select id from posts order by name"
+
+* Model.scoped now returns a relation if invoked without any arguments. [Pratik Naik]
+
+ Example :
+
+ posts = Post.scoped
+ posts.size # Fires "select count(*) from posts" and returns the count
+ posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
+
+* Association inverses for belongs_to, has_one, and has_many. Optimization to reduce database queries. #3533 [Murray Steele]
+
+ # post.comments sets each comment's post without needing to :include
+ class Post < ActiveRecord::Base
+ has_many :comments, :inverse_of => :post
+ end
+
+* MySQL: add_ and change_column support positioning. #3286 [Ben Marini]
+
+* Reset your Active Record counter caches with the reset_counter_cache class method. #1211 [Mike Breen, Gabe da Silveira]
+
* Remove support for SQLite 2. Please upgrade to SQLite 3+ or install the plugin from git://github.com/rails/sqlite2_adapter.git [Pratik Naik]
* PostgreSQL: XML datatype support. #1874 [Leonardo Borges]
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 8195e78826..728dec8925 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -21,89 +21,118 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
-$:.unshift(activesupport_path) if File.directory?(activesupport_path)
-activemodel_path = "#{File.dirname(__FILE__)}/../../activemodel/lib"
-$:.unshift(activemodel_path) if File.directory?(activemodel_path)
+activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
+$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
+
+activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__)
+$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path)
require 'active_support'
require 'active_model'
require 'arel'
module ActiveRecord
- # TODO: Review explicit loads to see if they will automatically be handled by the initializer.
- def self.load_all!
- [Base, DynamicFinderMatch, ConnectionAdapters::AbstractAdapter]
- end
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :VERSION
+
+ autoload :ActiveRecordError, 'active_record/base'
+ autoload :ConnectionNotEstablished, 'active_record/base'
- autoload :VERSION, 'active_record/version'
-
- autoload :ActiveRecordError, 'active_record/base'
- autoload :ConnectionNotEstablished, 'active_record/base'
-
- autoload :Aggregations, 'active_record/aggregations'
- autoload :AssociationPreload, 'active_record/association_preload'
- autoload :Associations, 'active_record/associations'
- autoload :AttributeMethods, 'active_record/attribute_methods'
- autoload :Attributes, 'active_record/attributes'
- autoload :AutosaveAssociation, 'active_record/autosave_association'
- autoload :Relation, 'active_record/relation'
- autoload :Base, 'active_record/base'
- autoload :Batches, 'active_record/batches'
- autoload :Calculations, 'active_record/calculations'
- autoload :Callbacks, 'active_record/callbacks'
- autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
- autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
- autoload :Migration, 'active_record/migration'
- autoload :Migrator, 'active_record/migration'
- autoload :NamedScope, 'active_record/named_scope'
- autoload :NestedAttributes, 'active_record/nested_attributes'
- autoload :Observer, 'active_record/observer'
- autoload :QueryCache, 'active_record/query_cache'
- autoload :Reflection, 'active_record/reflection'
- autoload :Schema, 'active_record/schema'
- autoload :SchemaDumper, 'active_record/schema_dumper'
- autoload :Serialization, 'active_record/serialization'
- autoload :SessionStore, 'active_record/session_store'
- autoload :StateMachine, 'active_record/state_machine'
- autoload :TestCase, 'active_record/test_case'
- autoload :Timestamp, 'active_record/timestamp'
- autoload :Transactions, 'active_record/transactions'
- autoload :Types, 'active_record/types'
- autoload :Validations, 'active_record/validations'
+ autoload :Aggregations
+ autoload :AssociationPreload
+ autoload :Associations
+ autoload :AttributeMethods
+ autoload :Attributes
+ autoload :AutosaveAssociation
+
+ autoload :Relation
+
+ autoload_under 'relation' do
+ autoload :QueryMethods
+ autoload :FinderMethods
+ autoload :CalculationMethods
+ autoload :PredicateBuilder
+ autoload :SpawnMethods
+ end
+
+ autoload :Base
+ autoload :Batches
+ autoload :Calculations
+ autoload :Callbacks
+ autoload :DynamicFinderMatch
+ autoload :DynamicScopeMatch
+ autoload :Migration
+ autoload :Migrator, 'active_record/migration'
+ autoload :NamedScope
+ autoload :NestedAttributes
+ autoload :Observer
+ autoload :QueryCache
+ autoload :Reflection
+ autoload :Schema
+ autoload :SchemaDumper
+ autoload :Serialization
+ autoload :SessionStore
+ autoload :StateMachine
+ autoload :Timestamp
+ autoload :Transactions
+ autoload :Types
+ autoload :Validations
+ end
module AttributeMethods
- autoload :BeforeTypeCast, 'active_record/attribute_methods/before_type_cast'
- autoload :Dirty, 'active_record/attribute_methods/dirty'
- autoload :PrimaryKey, 'active_record/attribute_methods/primary_key'
- autoload :Query, 'active_record/attribute_methods/query'
- autoload :Read, 'active_record/attribute_methods/read'
- autoload :TimeZoneConversion, 'active_record/attribute_methods/time_zone_conversion'
- autoload :Write, 'active_record/attribute_methods/write'
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :BeforeTypeCast
+ autoload :Dirty
+ autoload :PrimaryKey
+ autoload :Query
+ autoload :Read
+ autoload :TimeZoneConversion
+ autoload :Write
+ end
end
module Attributes
- autoload :Aliasing, 'active_record/attributes/aliasing'
- autoload :Store, 'active_record/attributes/store'
- autoload :Typecasting, 'active_record/attributes/typecasting'
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Aliasing
+ autoload :Store
+ autoload :Typecasting
+ end
end
module Type
- autoload :Number, 'active_record/types/number'
- autoload :Object, 'active_record/types/object'
- autoload :Serialize, 'active_record/types/serialize'
- autoload :TimeWithZone, 'active_record/types/time_with_zone'
- autoload :Unknown, 'active_record/types/unknown'
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Number, 'active_record/types/number'
+ autoload :Object, 'active_record/types/object'
+ autoload :Serialize, 'active_record/types/serialize'
+ autoload :TimeWithZone, 'active_record/types/time_with_zone'
+ autoload :Unknown, 'active_record/types/unknown'
+ end
end
module Locking
- autoload :Optimistic, 'active_record/locking/optimistic'
- autoload :Pessimistic, 'active_record/locking/pessimistic'
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Optimistic
+ autoload :Pessimistic
+ end
end
module ConnectionAdapters
- autoload :AbstractAdapter, 'active_record/connection_adapters/abstract_adapter'
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :AbstractAdapter
+ end
end
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index d90dcf090e..149a11eb47 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,9 +1,10 @@
require 'active_support/core_ext/module/delegation'
+require 'active_support/core_ext/enumerable'
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{reflection.class_name})")
+ def initialize(reflection, associated_class = nil)
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
end
end
@@ -60,6 +61,12 @@ module ActiveRecord
end
end
+ class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc:
+ def initialize(reflection)
+ super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).")
+ end
+ end
+
class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
@@ -1316,7 +1323,7 @@ module ActiveRecord
if association.nil? || force_reload
association = association_proxy_class.new(self, reflection)
- retval = association.reload
+ retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload
if retval.nil? and association_proxy_class == BelongsToAssociation
association_instance_set(reflection.name, nil)
return nil
@@ -1361,7 +1368,7 @@ module ActiveRecord
association_instance_set(reflection.name, association)
end
- association.reload if force_reload
+ reflection.klass.uncached { association.reload } if force_reload
association
end
@@ -1373,9 +1380,9 @@ module ActiveRecord
if reflection.through_reflection && reflection.source_reflection.belongs_to?
through = reflection.through_reflection
primary_key = reflection.source_reflection.primary_key_name
- send(through.name).all(:select => "DISTINCT #{through.quoted_table_name}.#{primary_key}").map!(&:"#{primary_key}")
+ send(through.name).select("DISTINCT #{through.quoted_table_name}.#{primary_key}").map!(&:"#{primary_key}")
else
- send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map!(&:id)
+ send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map!(&:id)
end
end
end
@@ -1394,8 +1401,8 @@ module ActiveRecord
end
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
- ids = (new_value || []).reject { |nid| nid.blank? }
- send("#{reflection.name}=", reflection.klass.find(ids))
+ ids = (new_value || []).reject { |nid| nid.blank? }.map(&:to_i)
+ send("#{reflection.name}=", reflection.klass.find(ids).index_by(&:id).values_at(*ids))
end
end
end
@@ -1456,12 +1463,10 @@ module ActiveRecord
after_destroy(method_name)
end
- def find_with_associations(options = {}, join_dependency = nil)
- catch :invalid_query do
- join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
- rows = select_all_rows(options, join_dependency)
- return join_dependency.instantiate(rows)
- end
+ def find_with_associations(options, join_dependency)
+ rows = select_all_rows(options, join_dependency)
+ join_dependency.instantiate(rows)
+ rescue ThrowResult
[]
end
@@ -1480,7 +1485,7 @@ module ActiveRecord
dependent_conditions = []
dependent_conditions << "#{reflection.primary_key_name} = \#{record.#{reflection.name}.send(:owner_quoted_id)}"
dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]
- dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions]
+ dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.table_name) if reflection.options[:conditions]
dependent_conditions << extra_conditions if extra_conditions
dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
dependent_conditions = dependent_conditions.gsub('@', '\@')
@@ -1672,7 +1677,6 @@ module ActiveRecord
def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
options.assert_valid_keys(valid_keys_for_has_and_belongs_to_many_association)
-
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
@@ -1682,6 +1686,9 @@ module ActiveRecord
end
reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
+ if connection.supports_primary_key? && (connection.primary_key(reflection.options[:join_table]) rescue false)
+ raise HasAndBelongsToManyAssociationWithPrimaryKeyError.new(reflection)
+ end
reflection
end
@@ -1696,7 +1703,7 @@ module ActiveRecord
def construct_finder_arel_with_included_associations(options, join_dependency)
scope = scope(:find)
- relation = arel_table((scope && scope[:from]) || options[:from])
+ relation = active_relation
for association in join_dependency.join_associations
relation = association.join_relation(relation)
@@ -1704,11 +1711,13 @@ module ActiveRecord
relation = relation.joins(construct_join(options[:joins], scope)).
select(column_aliases(join_dependency)).
- group(construct_group(options[:group], options[:having], scope)).
+ group(options[:group] || (scope && scope[:group])).
+ having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
- conditions(construct_conditions(options[:conditions], scope))
+ where(construct_conditions(options[:conditions], scope)).
+ from((scope && scope[:from]) || options[:from])
- relation = relation.conditions(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
+ relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections)
relation
@@ -1720,7 +1729,7 @@ module ActiveRecord
def construct_arel_limited_ids_condition(options, join_dependency)
if (ids_array = select_limited_ids_array(options, join_dependency)).empty?
- throw :invalid_query
+ raise ThrowResult
else
Arel::Predicates::In.new(
Arel::SqlLiteral.new("#{connection.quote_table_name table_name}.#{primary_key}"),
@@ -1739,101 +1748,25 @@ module ActiveRecord
def construct_finder_sql_for_association_limiting(options, join_dependency)
scope = scope(:find)
- relation = arel_table(options[:from])
+ relation = active_relation
for association in join_dependency.join_associations
relation = association.join_relation(relation)
end
relation = relation.joins(construct_join(options[:joins], scope)).
- conditions(construct_conditions(options[:conditions], scope)).
- group(construct_group(options[:group], options[:having], scope)).
+ where(construct_conditions(options[:conditions], scope)).
+ group(options[:group] || (scope && scope[:group])).
+ having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
limit(construct_limit(options[:limit], scope)).
offset(construct_limit(options[:offset], scope)).
+ from(options[:from]).
select(connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", construct_order(options[:order], scope(:find)).join(",")))
relation.to_sql
end
- def tables_in_string(string)
- return [] if string.blank?
- string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten
- end
-
- def tables_in_hash(hash)
- return [] if hash.blank?
- tables = hash.map do |key, value|
- if value.is_a?(Hash)
- key.to_s
- else
- tables_in_string(key) if key.is_a?(String)
- end
- end
- tables.flatten.compact
- end
-
- def conditions_tables(options)
- # look in both sets of conditions
- conditions = [scope(:find, :conditions), options[:conditions]].inject([]) do |all, cond|
- case cond
- when nil then all
- when Array then all << tables_in_string(cond.first)
- when Hash then all << tables_in_hash(cond)
- else all << tables_in_string(cond)
- end
- end
- conditions.flatten
- end
-
- def order_tables(options)
- order = [options[:order], scope(:find, :order) ].join(", ")
- return [] unless order && order.is_a?(String)
- tables_in_string(order)
- end
-
- def selects_tables(options)
- select = options[:select]
- return [] unless select && select.is_a?(String)
- tables_in_string(select)
- end
-
- def joined_tables(options)
- scope = scope(:find)
- joins = options[:joins]
- merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
- [table_name] + case merged_joins
- when Symbol, Hash, Array
- if array_of_strings?(merged_joins)
- tables_in_string(merged_joins.join(' '))
- else
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_joins, nil)
- join_dependency.join_associations.collect {|join_association| [join_association.aliased_join_table_name, join_association.aliased_table_name]}.flatten.compact
- end
- else
- tables_in_string(merged_joins)
- end
- end
-
- # Checks if the conditions reference a table other than the current model table
- def include_eager_conditions?(options, tables = nil, joined_tables = nil)
- ((tables || conditions_tables(options)) - (joined_tables || joined_tables(options))).any?
- end
-
- # Checks if the query order references a table other than the current model's table.
- def include_eager_order?(options, tables = nil, joined_tables = nil)
- ((tables || order_tables(options)) - (joined_tables || joined_tables(options))).any?
- end
-
- def include_eager_select?(options, joined_tables = nil)
- (selects_tables(options) - (joined_tables || joined_tables(options))).any?
- end
-
- def references_eager_loaded_tables?(options)
- joined_tables = joined_tables(options)
- include_eager_order?(options, nil, joined_tables) || include_eager_conditions?(options, nil, joined_tables) || include_eager_select?(options, joined_tables)
- end
-
def using_limitable_reflections?(reflections)
reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
end
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index 25e329c0c1..358db6df1d 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -20,7 +20,22 @@ module ActiveRecord
super
construct_sql
end
-
+
+ delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
+
+ def select(select = nil, &block)
+ if block_given?
+ load_target
+ @target.select(&block)
+ else
+ scoped.select(select)
+ end
+ end
+
+ def scoped
+ with_scope(construct_scope) { @reflection.klass.scoped }
+ end
+
def find(*args)
options = args.extract_options!
@@ -37,27 +52,21 @@ module ActiveRecord
load_target.select { |r| ids.include?(r.id) }
end
else
- conditions = "#{@finder_sql}"
- if sanitized_conditions = sanitize_sql(options[:conditions])
- conditions << " AND (#{sanitized_conditions})"
- end
-
- options[:conditions] = conditions
+ merge_options_from_reflection!(options)
+ construct_find_options!(options)
+
+ find_scope = construct_scope[:find].slice(:conditions, :order)
+
+ with_scope(:find => find_scope) do
+ relation = @reflection.klass.send(:construct_finder_arel, options)
- if options[:order] && @reflection.options[:order]
- options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
- elsif @reflection.options[:order]
- options[:order] = @reflection.options[:order]
+ case args.first
+ when :first, :last, :all
+ relation.send(args.first)
+ else
+ relation.find(*args)
+ end
end
-
- # Build options specific to association
- construct_find_options!(options)
-
- merge_options_from_reflection!(options)
-
- # Pass through args exactly as we received them.
- args << options
- @reflection.klass.find(*args)
end
end
@@ -168,7 +177,7 @@ module ActiveRecord
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)
+ column_name, options = @reflection.klass.scoped.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
@@ -383,7 +392,7 @@ module ActiveRecord
loaded if target
target
end
-
+
def method_missing(method, *args)
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
if block_given?
@@ -476,7 +485,14 @@ module ActiveRecord
def callback(method, record)
callbacks_for(method).each do |callback|
- ActiveSupport::DeprecatedCallbacks::Callback.new(method, callback, record).call(@owner, record)
+ case callback
+ when Symbol
+ @owner.send(callback, record)
+ when Proc
+ callback.call(@owner, record)
+ else
+ callback.send(method, @owner, record)
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 7d8f4670fa..022dd2ae9b 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -161,7 +161,7 @@ module ActiveRecord
end
# Forwards the call to the reflection class.
- def sanitize_sql(sql, table_name = @reflection.klass.quoted_table_name)
+ def sanitize_sql(sql, table_name = @reflection.klass.table_name)
@reflection.klass.send(:sanitize_sql, sql, table_name)
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 67e18d692d..f6edd6383c 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -13,6 +13,7 @@ module ActiveRecord
@updated = true
end
+ set_inverse_instance(record, @owner)
loaded
record
end
@@ -22,21 +23,44 @@ module ActiveRecord
end
private
- def find_target
- return nil if association_class.nil?
- if @reflection.options[:conditions]
- association_class.find(
- @owner[@reflection.primary_key_name],
- :select => @reflection.options[:select],
- :conditions => conditions,
- :include => @reflection.options[:include]
- )
+ # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
+ # has_one associations.
+ def we_can_set_the_inverse_on_this?(record)
+ if @reflection.has_inverse?
+ inverse_association = @reflection.polymorphic_inverse_of(record.class)
+ inverse_association && inverse_association.macro == :has_one
else
- association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
+ false
+ end
+ end
+
+ def set_inverse_instance(record, instance)
+ return if record.nil? || !we_can_set_the_inverse_on_this?(record)
+ inverse_relationship = @reflection.polymorphic_inverse_of(record.class)
+ unless inverse_relationship.nil?
+ record.send(:"set_#{inverse_relationship.name}_target", instance)
end
end
+ def find_target
+ return nil if association_class.nil?
+
+ target =
+ if @reflection.options[:conditions]
+ association_class.find(
+ @owner[@reflection.primary_key_name],
+ :select => @reflection.options[:select],
+ :conditions => conditions,
+ :include => @reflection.options[:include]
+ )
+ else
+ association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
+ end
+ set_inverse_instance(target, @owner)
+ target
+ end
+
def foreign_key_present
!@owner[@reflection.primary_key_name].nil?
end
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 c646fe488b..bd05d1014c 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
@@ -1,11 +1,6 @@
module ActiveRecord
module Associations
class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
- def initialize(owner, reflection)
- super
- @primary_key_list = {}
- end
-
def create(attributes = {})
create_record(attributes) { |record| insert_record(record) }
end
@@ -23,9 +18,7 @@ module ActiveRecord
end
def has_primary_key?
- return @has_primary_key unless @has_primary_key.nil?
- @has_primary_key = (@owner.connection.supports_primary_key? &&
- @owner.connection.primary_key(@reflection.options[:join_table]))
+ @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table])
end
protected
@@ -40,11 +33,6 @@ module ActiveRecord
end
def insert_record(record, force = true, validate = true)
- if has_primary_key?
- raise ActiveRecord::ConfigurationError,
- "Primary key is not allowed in a has_and_belongs_to_many join table (#{@reflection.options[:join_table]})."
- end
-
if record.new_record?
if force
record.save!
@@ -56,7 +44,7 @@ module ActiveRecord
if @reflection.options[:insert_sql]
@owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
else
- relation = arel_table(@reflection.options[:join_table])
+ relation = Arel::Table.new(@reflection.options[:join_table])
attributes = columns.inject({}) do |attrs, column|
case column.name.to_s
when @reflection.primary_key_name.to_s
@@ -82,8 +70,8 @@ module ActiveRecord
if sql = @reflection.options[:delete_sql]
records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
else
- relation = arel_table(@reflection.options[:join_table])
- relation.conditions(relation[@reflection.primary_key_name].eq(@owner.id).
+ relation = Arel::Table.new(@reflection.options[:join_table])
+ relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
and(Arel::Predicates::In.new(relation[@reflection.association_foreign_key], records.map(&:id)))
).delete
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index cd31b0e211..d3336cf2d2 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -69,8 +69,8 @@ module ActiveRecord
when :delete_all
@reflection.klass.delete(records.map { |record| record.id })
else
- relation = arel_table(@reflection.table_name)
- relation.conditions(relation[@reflection.primary_key_name].eq(@owner.id).
+ relation = Arel::Table.new(@reflection.table_name)
+ relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
and(Arel::Predicates::In.new(relation[@reflection.klass.primary_key], records.map(&:id)))
).update(relation[@reflection.primary_key_name] => nil)
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index b85a40b2e5..ea769fd48b 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -57,6 +57,7 @@ module ActiveRecord
@target = (AssociationProxy === obj ? obj.target : obj)
end
+ set_inverse_instance(obj, @owner)
@loaded = true
unless @owner.new_record? or obj.nil? or dont_save
@@ -120,10 +121,9 @@ module ActiveRecord
else
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
self.target = record
+ set_inverse_instance(record, @owner)
end
- set_inverse_instance(record, @owner)
-
record
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 8f37fcd515..98ab64537e 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -155,6 +155,13 @@ module ActiveRecord
# Adds a validate and save callback for the association as specified by
# the +reflection+.
+ #
+ # For performance reasons, we don't check whether to validate at runtime,
+ # but instead only define the method and callback when needed. However,
+ # this can change, for instance, when using nested attributes. Since we
+ # don't want the callbacks to get defined multiple times, there are
+ # guards that check if the save or validation methods have already been
+ # defined before actually defining them.
def add_autosave_association_callbacks(reflection)
save_method = "autosave_associated_records_for_#{reflection.name}"
validation_method = "validate_associated_records_for_#{reflection.name}"
@@ -162,28 +169,33 @@ module ActiveRecord
case reflection.macro
when :has_many, :has_and_belongs_to_many
- before_save :before_save_collection_association
+ unless method_defined?(save_method)
+ before_save :before_save_collection_association
- define_method(save_method) { save_collection_association(reflection) }
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create save_method
- after_update save_method
+ define_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+ end
- if force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)
+ if !method_defined?(validation_method) &&
+ (force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false))
define_method(validation_method) { validate_collection_association(reflection) }
validate validation_method
end
else
- case reflection.macro
- when :has_one
- define_method(save_method) { save_has_one_association(reflection) }
- after_save save_method
- when :belongs_to
- define_method(save_method) { save_belongs_to_association(reflection) }
- before_save save_method
+ unless method_defined?(save_method)
+ case reflection.macro
+ when :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ after_save save_method
+ when :belongs_to
+ define_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
end
- if force_validation
+ if !method_defined?(validation_method) && force_validation
define_method(validation_method) { validate_single_association(reflection) }
validate validation_method
end
@@ -221,9 +233,9 @@ module ActiveRecord
if new_record
association
elsif association.loaded?
- autosave ? association : association.select { |record| record.new_record? }
+ autosave ? association : association.find_all { |record| record.new_record? }
else
- autosave ? association.target : association.target.select { |record| record.new_record? }
+ autosave ? association.target : association.target.find_all { |record| record.new_record? }
end
end
@@ -255,7 +267,7 @@ module ActiveRecord
unless valid = association.valid?
if reflection.options[:autosave]
association.errors.each do |attribute, message|
- attribute = "#{reflection.name}_#{attribute}"
+ attribute = "#{reflection.name}.#{attribute}"
errors[attribute] << message if errors[attribute].empty?
end
else
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 056f29f029..ec7725d256 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,4 +1,3 @@
-require 'benchmark'
require 'yaml'
require 'set'
require 'active_support/benchmarkable'
@@ -13,6 +12,7 @@ require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/object/metaclass'
+require 'active_support/core_ext/module/delegation'
module ActiveRecord #:nodoc:
# Generic Active Record exception class.
@@ -68,6 +68,10 @@ module ActiveRecord #:nodoc:
class StatementInvalid < ActiveRecordError
end
+ # Raised when SQL statement is invalid and the application gets a blank result.
+ class ThrowResult < ActiveRecordError
+ end
+
# Parent class for all specific exceptions which wrap database driver exceptions
# provides access to the original exception also.
class WrappedDatabaseException < StatementInvalid
@@ -639,17 +643,20 @@ module ActiveRecord #:nodoc:
# end
def find(*args)
options = args.extract_options!
- validate_find_options(options)
set_readonly_option!(options)
+ relation = construct_finder_arel(options)
+
case args.first
- when :first then find_initial(options)
- when :last then find_last(options)
- when :all then find_every(options)
- else find_from_ids(args, options)
+ when :first, :last, :all
+ relation.send(args.first)
+ else
+ relation.find(*args)
end
end
+ delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
+
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
# same arguments to this method as you can to <tt>find(:first)</tt>.
def first(*args)
@@ -662,26 +669,10 @@ module ActiveRecord #:nodoc:
find(:last, *args)
end
- # Returns an ActiveRecord::Relation object. You can pass in all the same arguments to this method as you can
- # to find(:all).
+ # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the
+ # same arguments to this method as you can to <tt>find(:all)</tt>.
def all(*args)
- options = args.extract_options!
-
- if options.empty? && !scoped?(:find)
- relation = arel_table
- else
- relation = construct_finder_arel(options)
- include_associations = merge_includes(scope(:find, :include), options[:include])
-
- if include_associations.any?
- if references_eager_loaded_tables?(options)
- relation.eager_load(include_associations)
- else
- relation.preload(include_associations)
- end
- end
- end
- relation
+ find(:all, *args)
end
# Executes a custom SQL query against your database and returns all the results. The results will
@@ -735,10 +726,13 @@ module ActiveRecord #:nodoc:
# Person.exists?(:name => "David")
# Person.exists?(['name LIKE ?', "%#{query}%"])
# Person.exists?
- def exists?(id_or_conditions = {})
- find_initial(
- :select => "#{quoted_table_name}.#{primary_key}",
- :conditions => expand_id_conditions(id_or_conditions)) ? true : false
+ def exists?(id_or_conditions = nil)
+ case id_or_conditions
+ when Array, Hash
+ where(id_or_conditions).exists?
+ else
+ scoped.exists?(id_or_conditions)
+ end
end
# Creates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -821,8 +815,8 @@ module ActiveRecord #:nodoc:
#
# # Delete multiple rows
# Todo.delete([2,3,4])
- def delete(id)
- delete_all([ "#{connection.quote_column_name(primary_key)} IN (?)", id ])
+ def delete(id_or_array)
+ active_relation.where(construct_conditions(nil, scope(:find))).delete(id_or_array)
end
# Destroy an object (or multiple objects) that has the given id, the object is instantiated first,
@@ -879,10 +873,10 @@ module ActiveRecord #:nodoc:
def update_all(updates, conditions = nil, options = {})
scope = scope(:find)
- relation = arel_table
+ relation = active_relation
if conditions = construct_conditions(conditions, scope)
- relation = relation.conditions(Arel::SqlLiteral.new(conditions))
+ relation = relation.where(Arel::SqlLiteral.new(conditions))
end
relation = if options.has_key?(:limit) || (scope && scope[:limit])
@@ -923,7 +917,7 @@ module ActiveRecord #:nodoc:
# Person.destroy_all("last_login < '2004-04-04'")
# Person.destroy_all(:status => "inactive")
def destroy_all(conditions = nil)
- find(:all, :conditions => conditions).each { |object| object.destroy }
+ where(conditions).destroy_all
end
# Deletes the records matching +conditions+ without instantiating the records first, and hence not
@@ -944,11 +938,7 @@ module ActiveRecord #:nodoc:
# Both calls delete the affected posts all at once with a single DELETE statement. If you need to destroy dependent
# associations or call your <tt>before_*</tt> or +after_destroy+ callbacks, use the +destroy_all+ method instead.
def delete_all(conditions = nil)
- if conditions
- arel_table.conditions(Arel::SqlLiteral.new(construct_conditions(conditions, scope(:find)))).delete
- else
- arel_table.delete
- end
+ active_relation.where(construct_conditions(conditions, scope(:find))).delete_all
end
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
@@ -967,6 +957,29 @@ module ActiveRecord #:nodoc:
connection.select_value(sql, "#{name} Count").to_i
end
+ # Resets one or more counter caches to their correct value using an SQL
+ # count query. This is useful when adding new counter caches, or if the
+ # counter has been corrupted or modified directly by SQL.
+ #
+ # ==== Parameters
+ #
+ # * +id+ - The id of the object you wish to reset a counter on.
+ # * +counters+ - One or more counter names to reset
+ #
+ # ==== Examples
+ #
+ # # For Post with id #1 records reset the comments_count
+ # Post.reset_counters(1, :comments)
+ def reset_counters(id, *counters)
+ object = find(id)
+ counters.each do |association|
+ child_class = reflect_on_association(association).klass
+ counter_name = child_class.reflect_on_association(self.name.downcase.to_sym).counter_cache_column
+
+ connection.update("UPDATE #{quoted_table_name} SET #{connection.quote_column_name(counter_name)} = #{object.send(association).count} WHERE #{connection.quote_column_name(primary_key)} = #{quote_value(object.id)}", "#{name} UPDATE")
+ end
+ end
+
# A generic "counter updater" implementation, intended primarily to be
# used by increment_counter and decrement_counter, but which may also
# be useful on its own. It simply does a direct SQL update for the record
@@ -1378,7 +1391,8 @@ module ActiveRecord #:nodoc:
# end
def reset_column_information
undefine_attribute_methods
- @arel_table = @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
+ @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil
+ @active_relation = @active_relation_engine = nil
end
def reset_column_information_and_inheritable_attributes_for_all_subclasses#:nodoc:
@@ -1491,130 +1505,25 @@ module ActiveRecord #:nodoc:
"(#{segments.join(') AND (')})" unless segments.empty?
end
-
- def arel_table(table = nil)
- table = table_name if table.blank?
- if @arel_table.nil? || @arel_table.name != table
- @arel_table = Relation.new(self, Arel::Table.new(table))
- end
- @arel_table
+ def active_relation
+ @active_relation ||= Relation.new(self, active_relation_table)
end
- private
- def find_initial(options)
- options.update(:limit => 1)
- find_every(options).first
- end
-
- def find_last(options)
- order = options[:order]
-
- if order
- order = reverse_sql_order(order)
- elsif !scoped?(:find, :order)
- order = "#{table_name}.#{primary_key} DESC"
- end
-
- if scoped?(:find, :order)
- scope = scope(:find)
- original_scoped_order = scope[:order]
- scope[:order] = reverse_sql_order(original_scoped_order)
- end
-
- begin
- find_initial(options.merge({ :order => order }))
- ensure
- scope[:order] = original_scoped_order if original_scoped_order
- end
- end
-
- def reverse_sql_order(order_query)
- order_query.to_s.split(/,/).each { |s|
- if s.match(/\s(asc|ASC)$/)
- s.gsub!(/\s(asc|ASC)$/, ' DESC')
- elsif s.match(/\s(desc|DESC)$/)
- s.gsub!(/\s(desc|DESC)$/, ' ASC')
- else
- s.concat(' DESC')
- end
- }.join(',')
- end
-
- def find_every(options)
- include_associations = merge_includes(scope(:find, :include), options[:include])
-
- if include_associations.any? && references_eager_loaded_tables?(options)
- records = find_with_associations(options)
- else
- records = find_by_sql(construct_finder_sql(options))
- if include_associations.any?
- preload_associations(records, include_associations)
- end
- end
-
- records.each { |record| record.readonly! } if options[:readonly]
-
- records
- end
-
- def find_from_ids(ids, options)
- expects_array = ids.first.kind_of?(Array)
- return ids.first if expects_array && ids.first.empty?
-
- ids = ids.flatten.compact.uniq
-
- case ids.size
- when 0
- raise RecordNotFound, "Couldn't find #{name} without an ID"
- when 1
- result = find_one(ids.first, options)
- expects_array ? [ result ] : result
- else
- find_some(ids, options)
- end
- end
-
- def find_one(id, options)
- conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
- options.update :conditions => "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} = #{quote_value(id,columns_hash[primary_key])}#{conditions}"
-
- # Use find_every(options).first since the primary key condition
- # already ensures we have a single record. Using find_initial adds
- # a superfluous :limit => 1.
- if result = find_every(options).first
- result
- else
- raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}"
- end
- end
-
- def find_some(ids, options)
- conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions]
- ids_list = ids.map { |id| quote_value(id,columns_hash[primary_key]) }.join(',')
- options.update :conditions => "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} IN (#{ids_list})#{conditions}"
-
- result = find_every(options)
-
- # Determine expected size from limit and offset, not just ids.size.
- expected_size =
- if options[:limit] && ids.size > options[:limit]
- options[:limit]
- else
- ids.size
- end
-
- # 11 ids with limit 3, offset 9 should give 2 results.
- if options[:offset] && (ids.size - options[:offset] < expected_size)
- expected_size = ids.size - options[:offset]
- end
+ def active_relation_table(table_name_alias = nil)
+ Arel::Table.new(table_name, :as => table_name_alias, :engine => active_relation_engine)
+ end
- if result.size == expected_size
- result
+ def active_relation_engine
+ @active_relation_engine ||= begin
+ if self == ActiveRecord::Base
+ Arel::Table.engine
else
- raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_list})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
+ connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.active_relation_engine
end
end
+ end
+ private
# Finder methods must instantiate through this method to work with the
# single-table inheritance model that makes it possible to create
# objects of different types from the same table.
@@ -1665,25 +1574,28 @@ module ActiveRecord #:nodoc:
end
def construct_finder_arel(options = {}, scope = scope(:find))
- # TODO add lock to Arel
- relation = arel_table(options[:from]).
+ validate_find_options(options)
+
+ relation = active_relation.
joins(construct_join(options[:joins], scope)).
- conditions(construct_conditions(options[:conditions], scope)).
+ where(construct_conditions(options[:conditions], scope)).
select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))).
- group(construct_group(options[:group], options[:having], scope)).
+ group(options[:group] || (scope && scope[:group])).
+ having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
limit(construct_limit(options[:limit], scope)).
- offset(construct_offset(options[:offset], scope))
+ offset(construct_offset(options[:offset], scope)).
+ from(options[:from]).
+ includes( merge_includes(scope && scope[:include], options[:include]))
+
+ lock = (scope && scope[:lock]) || options[:lock]
+ relation = relation.lock if lock.present?
relation = relation.readonly if options[:readonly]
relation
end
- def construct_finder_sql(options, scope = scope(:find))
- construct_finder_arel(options, scope).to_sql
- end
-
def construct_join(joins, scope)
merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
case merged_joins
@@ -1700,20 +1612,9 @@ module ActiveRecord #:nodoc:
end
end
- def construct_group(group, having, scope)
- sql = ''
- if group
- sql << group.to_s
- sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having
- elsif scope && (scoped_group = scope[:group])
- sql << scoped_group.to_s
- sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having]
- end
- sql
- end
-
def construct_order(order, scope)
orders = []
+
scoped_order = scope[:order] if scope
if order
orders << order
@@ -1721,7 +1622,8 @@ module ActiveRecord #:nodoc:
elsif scoped_order
orders << scoped_order
end
- orders
+
+ orders.reject {|o| o.blank?}
end
def construct_limit(limit, scope)
@@ -1743,7 +1645,7 @@ module ActiveRecord #:nodoc:
# Merges includes so that the result is a valid +include+
def merge_includes(first, second)
- (safe_to_array(first) + safe_to_array(second)).uniq
+ (Array.wrap(first) + Array.wrap(second)).uniq
end
def merge_joins(*joins)
@@ -1755,13 +1657,13 @@ module ActiveRecord #:nodoc:
end
joins.flatten.map{|j| j.strip}.uniq
else
- joins.collect{|j| safe_to_array(j)}.flatten.uniq
+ joins.collect{|j| Array.wrap(j)}.flatten.uniq
end
end
def build_association_joins(joins)
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, joins, nil)
- relation = arel_table.relation
+ relation = active_relation.relation
join_dependency.join_associations.map { |association|
if (association_relation = association.relation).is_a?(Array)
[Arel::InnerJoin.new(relation, association_relation.first, association.association_join.first).joins(relation),
@@ -1772,38 +1674,18 @@ module ActiveRecord #:nodoc:
}.join(" ")
end
- # Object#to_a is deprecated, though it does have the desired behavior
- def safe_to_array(o)
- case o
- when NilClass
- []
- when Array
- o
- else
- [o]
- end
- end
-
def array_of_strings?(o)
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
end
- # The optional scope argument is for the current <tt>:find</tt> scope.
- # The <tt>:lock</tt> option has precedence over a scoped <tt>:lock</tt>.
- def add_lock!(sql, options, scope = :auto)
- scope = scope(:find) if :auto == scope
- options = options.reverse_merge(:lock => scope[:lock]) if scope
- connection.add_lock!(sql, options)
- end
+ def type_condition(table_alias = nil)
+ table = Arel::Table.new(table_name, :engine => active_relation_engine, :as => table_alias)
- 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_alias}.#{quoted_inheritance_column} = '#{sti_name}' " ) do |condition, subclass|
- condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
- end
+ sti_column = table[inheritance_column]
+ condition = sti_column.eq(sti_name)
+ subclasses.each{|subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) }
- " (#{type_condition}) "
+ condition.to_sql
end
# Guesses the table name, but does not decorate it with prefix and suffix information.
@@ -1814,9 +1696,8 @@ module ActiveRecord #:nodoc:
end
# Enables dynamic finders like <tt>find_by_user_name(user_name)</tt> and <tt>find_by_user_name_and_password(user_name, password)</tt>
- # that are turned into <tt>find(:first, :conditions => ["user_name = ?", user_name])</tt> and
- # <tt>find(:first, :conditions => ["user_name = ? AND password = ?", user_name, password])</tt> respectively. Also works for
- # <tt>find(:all)</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>find(:all, :conditions => ["amount = ?", 50])</tt>.
+ # that are turned into <tt>where(:user_name => user_name).first</tt> and <tt>where(:user_name => user_name, :password => :password).first</tt>
+ # respectively. Also works for <tt>all</tt> by using <tt>find_all_by_amount(50)</tt> that is turned into <tt>where(:amount => 50).all</tt>.
#
# It's even possible to use all the additional parameters to +find+. For example, the full interface for +find_all_by_amount+
# is actually <tt>find_all_by_amount(amount, options)</tt>.
@@ -1832,103 +1713,11 @@ module ActiveRecord #:nodoc:
attribute_names = match.attribute_names
super unless all_attributes_exists?(attribute_names)
if match.finder?
- finder = match.finder
- bang = match.bang?
- # def self.find_by_login_and_activated(*args)
- # options = args.extract_options!
- # attributes = construct_attributes_from_arguments(
- # [:login,:activated],
- # args
- # )
- # finder_options = { :conditions => attributes }
- # validate_find_options(options)
- # set_readonly_option!(options)
- #
- # if options[:conditions]
- # with_scope(:find => finder_options) do
- # find(:first, options)
- # end
- # else
- # find(:first, options.merge(finder_options))
- # end
- # end
- 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
- find(:#{finder}, options)
- end
- else
- find(:#{finder}, options.merge(finder_options))
- end
- #{'result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attributes.to_a.collect { |pair| pair.join(\' = \') }.join(\', \')}")' if bang}
- end
- }, __FILE__, __LINE__
- send(method_id, *arguments)
+ options = arguments.extract_options!
+ relation = options.any? ? construct_finder_arel(options) : scoped
+ relation.send :find_by_attributes, match, attribute_names, *arguments
elsif match.instantiator?
- instantiator = match.instantiator
- # def self.find_or_create_by_user_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(*[:user_id])
- # else
- # find_attributes = attributes = construct_attributes_from_arguments([:user_id], args)
- # end
- #
- # options = { :conditions => find_attributes }
- # set_readonly_option!(options)
- #
- # record = find(:first, options)
- #
- # if record.nil?
- # record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) }
- # yield(record) if block_given?
- # record.save
- # record
- # else
- # record
- # end
- # end
- 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)
-
- record = find(:first, 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
- end
- end
- }, __FILE__, __LINE__
- send(method_id, *arguments, &block)
+ scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
end
elsif match = DynamicScopeMatch.match(method_id)
attribute_names = match.attribute_names
@@ -1990,14 +1779,6 @@ module ActiveRecord #:nodoc:
end
end
- # Interpret Array and Hash as conditions and anything else as an id.
- def expand_id_conditions(id_or_conditions)
- case id_or_conditions
- when Array, Hash then id_or_conditions
- else sanitize_sql(primary_key => id_or_conditions)
- end
- end
-
protected
# Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash.
# method_name may be <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>,
@@ -2184,7 +1965,7 @@ module ActiveRecord #:nodoc:
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
# { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
# "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition, table_name = quoted_table_name)
+ def sanitize_sql_for_conditions(condition, table_name = self.table_name)
return nil if condition.blank?
case condition
@@ -2255,30 +2036,12 @@ module ActiveRecord #:nodoc:
# And for value objects on a composed_of relationship:
# { :address => Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
- def sanitize_sql_hash_for_conditions(attrs, default_table_name = quoted_table_name)
+ def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
attrs = expand_hash_conditions_for_aggregates(attrs)
- conditions = attrs.map do |attr, value|
- table_name = default_table_name
-
- unless value.is_a?(Hash)
- attr = attr.to_s
-
- # Extract table name from qualified attribute names.
- if attr.include?('.')
- attr_table_name, attr = attr.split('.', 2)
- attr_table_name = connection.quote_table_name(attr_table_name)
- else
- attr_table_name = table_name
- end
-
- attribute_condition("#{attr_table_name}.#{connection.quote_column_name(attr)}", value)
- else
- sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s))
- end
- end.join(' AND ')
-
- replace_bind_variables(conditions, expand_range_bind_variables(attrs.values))
+ table = Arel::Table.new(default_table_name, active_relation_engine)
+ builder = PredicateBuilder.new(active_relation_engine)
+ builder.build_from_hash(attrs, table).map(&:to_sql).join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
@@ -2543,7 +2306,7 @@ module ActiveRecord #:nodoc:
# be made (since they can't be persisted).
def destroy
unless new_record?
- self.class.arel_table.conditions(self.class.arel_table[self.class.primary_key].eq(id)).delete
+ self.class.active_relation.where(self.class.active_relation[self.class.primary_key].eq(id)).delete_all
end
@destroyed = true
@@ -2830,7 +2593,7 @@ module ActiveRecord #:nodoc:
def update(attribute_names = @attributes.keys)
attributes_with_values = arel_attributes_values(false, false, attribute_names)
return 0 if attributes_with_values.empty?
- self.class.arel_table.conditions(self.class.arel_table[self.class.primary_key].eq(id)).update(attributes_with_values)
+ self.class.active_relation.where(self.class.active_relation[self.class.primary_key].eq(id)).update(attributes_with_values)
end
# Creates a record with values matching those of the instance attributes
@@ -2843,9 +2606,9 @@ module ActiveRecord #:nodoc:
attributes_values = arel_attributes_values
new_id = if attributes_values.empty?
- self.class.arel_table.insert connection.empty_insert_statement_value
+ self.class.active_relation.insert connection.empty_insert_statement_value
else
- self.class.arel_table.insert attributes_values
+ self.class.active_relation.insert attributes_values
end
self.id ||= new_id
@@ -2940,7 +2703,7 @@ module ActiveRecord #:nodoc:
if value && ((self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))) || value.is_a?(Hash) || value.is_a?(Array))
value = value.to_yaml
end
- attrs[self.class.arel_table[name]] = value
+ attrs[self.class.active_relation[name]] = value
end
end
end
diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb
index 40242333e5..20d287faeb 100644
--- a/activerecord/lib/active_record/calculations.rb
+++ b/activerecord/lib/active_record/calculations.rb
@@ -44,7 +44,26 @@ module ActiveRecord
#
# Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead.
def count(*args)
- calculate(:count, *construct_count_options_from_args(*args))
+ case args.size
+ when 0
+ construct_calculation_arel.count
+ when 1
+ if args[0].is_a?(Hash)
+ options = args[0]
+ distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
+ construct_calculation_arel(options).count(options[:select], :distinct => distinct)
+ else
+ construct_calculation_arel.count(args[0])
+ end
+ when 2
+ column_name, options = args
+ distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
+ construct_calculation_arel(options).count(column_name, :distinct => distinct)
+ else
+ raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
+ end
+ rescue ThrowResult
+ 0
end
# Calculates the average value on a given column. The value is returned as
@@ -122,168 +141,63 @@ module ActiveRecord
# Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
- validate_calculation_options(operation, options)
- operation = operation.to_s.downcase
-
- scope = scope(:find)
+ construct_calculation_arel(options).calculate(operation, column_name, options.slice(:distinct))
+ rescue ThrowResult
+ 0
+ end
- merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
+ private
+ def validate_calculation_options(options = {})
+ options.assert_valid_keys(CALCULATIONS_OPTIONS)
+ end
- if operation == "count"
- if merged_includes.any?
- distinct = true
- column_name = options[:select] || primary_key
- end
+ def construct_calculation_arel(options = {})
+ validate_calculation_options(options)
+ options = options.except(:distinct)
- distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i
- distinct ||= options[:distinct]
- else
- distinct = nil
- end
+ scope = scope(:find)
+ includes = merge_includes(scope ? scope[:include] : [], options[:include])
- catch :invalid_query do
- relation = if merged_includes.any?
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, construct_join(options[:joins], scope))
- construct_finder_arel_with_included_associations(options, join_dependency)
+ if includes.any?
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(options[:joins], scope))
+ construct_calculation_arel_with_included_associations(options, join_dependency)
else
- relation = arel_table(options[:from]).
+ active_relation.
joins(construct_join(options[:joins], scope)).
- conditions(construct_conditions(options[:conditions], scope)).
+ from((scope && scope[:from]) || options[:from]).
+ where(construct_conditions(options[:conditions], scope)).
order(options[:order]).
limit(options[:limit]).
- offset(options[:offset])
- end
- if options[:group]
- return execute_grouped_calculation(operation, column_name, options, relation)
- else
- return execute_simple_calculation(operation, column_name, options.merge(:distinct => distinct), relation)
+ offset(options[:offset]).
+ group(options[:group]).
+ having(options[:having]).
+ select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins])))
end
end
- 0
- end
-
- def execute_simple_calculation(operation, column_name, options, relation) #:nodoc:
- column = if column_names.include?(column_name.to_s)
- Arel::Attribute.new(arel_table(options[:from] || table_name),
- options[:select] || column_name)
- else
- Arel::SqlLiteral.new(options[:select] ||
- (column_name == :all ? "*" : column_name.to_s))
- end
-
- relation = relation.select(operation == 'count' ? column.count(options[:distinct]) : column.send(operation))
-
- type_cast_calculated_value(connection.select_value(relation.to_sql), column_for(column_name), operation)
- end
- def execute_grouped_calculation(operation, column_name, options, relation) #:nodoc:
- group_attr = options[:group].to_s
- association = reflect_on_association(group_attr.to_sym)
- associated = association && association.macro == :belongs_to # only count belongs_to associations
- group_field = associated ? association.primary_key_name : group_attr
- group_alias = column_alias_for(group_field)
- group_column = column_for group_field
+ def construct_calculation_arel_with_included_associations(options, join_dependency)
+ scope = scope(:find)
- options[:group] = connection.adapter_name == 'FrontBase' ? group_alias : group_field
+ relation = active_relation
- aggregate_alias = column_alias_for(operation, column_name)
-
- options[:select] = (operation == 'count' && column_name == :all) ?
- "COUNT(*) AS count_all" :
- Arel::Attribute.new(arel_table, column_name).send(operation).as(aggregate_alias).to_sql
-
- options[:select] << ", #{group_field} AS #{group_alias}"
-
- relation = relation.select(options[:select]).group(construct_group(options[:group], options[:having], nil))
-
- calculated_data = connection.select_all(relation.to_sql)
-
- if association
- key_ids = calculated_data.collect { |row| row[group_alias] }
- key_records = association.klass.base_class.find(key_ids)
- key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
- end
-
- calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
- key = type_cast_calculated_value(row[group_alias], group_column)
- key = key_records[key] if associated
- value = row[aggregate_alias]
- all[key] = type_cast_calculated_value(value, column_for(column_name), operation)
- all
- end
- end
-
- protected
- def construct_count_options_from_args(*args)
- options = {}
- column_name = :all
-
- # We need to handle
- # count()
- # count(:column_name=:all)
- # count(options={})
- # count(column_name=:all, options={})
- # selects specified by scopes
- case args.size
- when 0
- column_name = scope(:find)[:select] if scope(:find)
- when 1
- if args[0].is_a?(Hash)
- column_name = scope(:find)[:select] if scope(:find)
- options = args[0]
- else
- column_name = args[0]
- end
- when 2
- column_name, options = args
- else
- raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
+ for association in join_dependency.join_associations
+ relation = association.join_relation(relation)
end
- [column_name || :all, options]
- end
-
- private
- def validate_calculation_options(operation, options = {})
- options.assert_valid_keys(CALCULATIONS_OPTIONS)
- end
+ relation = relation.joins(construct_join(options[:joins], scope)).
+ select(column_aliases(join_dependency)).
+ group(options[:group]).
+ having(options[:having]).
+ order(options[:order]).
+ where(construct_conditions(options[:conditions], scope)).
+ from((scope && scope[:from]) || options[:from])
- # Converts the given keys to the value that the database adapter returns as
- # a usable column name:
- #
- # column_alias_for("users.id") # => "users_id"
- # column_alias_for("sum(id)") # => "sum_id"
- # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
- # column_alias_for("count(*)") # => "count_all"
- # column_alias_for("count", "id") # => "count_id"
- def column_alias_for(*keys)
- table_name = keys.join(' ')
- table_name.downcase!
- table_name.gsub!(/\*/, 'all')
- table_name.gsub!(/\W+/, ' ')
- table_name.strip!
- table_name.gsub!(/ +/, '_')
+ relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
+ relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections)
- connection.table_alias_for(table_name)
+ relation
end
- def column_for(field)
- field_name = field.to_s.split('.').last
- columns.detect { |c| c.name.to_s == field_name }
- end
-
- def type_cast_calculated_value(value, column, operation = nil)
- case operation
- when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || '0', column)
- when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
- else type_cast_using_column(value, column)
- end
- end
-
- def type_cast_using_column(value, column)
- column ? column.type_cast(value) : value
- end
end
end
end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index b25893a1c3..e2a8f03c8f 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -1,5 +1,3 @@
-require 'observer'
-
module ActiveRecord
# Callbacks are hooks into the lifecycle of an Active Record object that allow you to trigger logic
# before or after an alteration of the object state. This can be used to make sure that associated and
@@ -210,7 +208,6 @@ module ActiveRecord
# instead of quietly returning +false+.
module Callbacks
extend ActiveSupport::Concern
- include ActiveSupport::Callbacks
CALLBACKS = [
:after_initialize, :after_find, :before_validation, :after_validation,
@@ -224,60 +221,14 @@ module ActiveRecord
alias_method_chain method, :callbacks
end
- define_callbacks :initialize, :find, :save, :create, :update, :destroy,
- :validation, :terminator => "result == false", :scope => [:kind, :name]
+ extend ActiveModel::Callbacks
+
+ define_model_callbacks :initialize, :find, :only => :after
+ define_model_callbacks :save, :create, :update, :destroy
+ define_model_callbacks :validation, :only => [:before, :after]
end
module ClassMethods
- def after_initialize(*args, &block)
- options = args.extract_options!
- options[:prepend] = true
- set_callback(:initialize, :after, *(args << options), &block)
- end
-
- def after_find(*args, &block)
- options = args.extract_options!
- options[:prepend] = true
- set_callback(:find, :after, *(args << options), &block)
- end
-
- [:save, :create, :update, :destroy].each do |callback|
- module_eval <<-CALLBACKS, __FILE__, __LINE__
- def before_#{callback}(*args, &block)
- set_callback(:#{callback}, :before, *args, &block)
- end
-
- def around_#{callback}(*args, &block)
- set_callback(:#{callback}, :around, *args, &block)
- end
-
- def after_#{callback}(*args, &block)
- options = args.extract_options!
- options[:prepend] = true
- options[:if] = Array(options[:if]) << "!halted && value != false"
- set_callback(:#{callback}, :after, *(args << options), &block)
- end
- CALLBACKS
- end
-
- def before_validation(*args, &block)
- options = args.extract_options!
- if options[:on]
- options[:if] = Array(options[:if])
- options[:if] << "@_on_validate == :#{options[:on]}"
- end
- set_callback(:validation, :before, *(args << options), &block)
- end
-
- def after_validation(*args, &block)
- options = args.extract_options!
- options[:if] = Array(options[:if])
- options[:if] << "!halted"
- options[:if] << "@_on_validate == :#{options[:on]}" if options[:on]
- options[:prepend] = true
- set_callback(:validation, :after, *(args << options), &block)
- end
-
def method_added(meth)
super
if CALLBACKS.include?(meth.to_sym)
@@ -323,7 +274,7 @@ module ActiveRecord
def deprecated_callback_method(symbol) #:nodoc:
if respond_to?(symbol)
- ActiveSupport::Deprecation.warn("Base##{symbol} has been deprecated, please use Base.#{symbol} :method instead")
+ ActiveSupport::Deprecation.warn("Overwriting #{symbol} in your models has been deprecated, please use Base##{symbol} :method_name instead")
send(symbol)
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index be89873632..027d736484 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -181,18 +181,6 @@ module ActiveRecord
# done if the transaction block raises an exception or returns false.
def rollback_db_transaction() end
- # Appends a locking clause to an SQL statement.
- # This method *modifies* the +sql+ parameter.
- # # SELECT * FROM suppliers FOR UPDATE
- # add_lock! 'SELECT * FROM suppliers', :lock => true
- # add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE'
- def add_lock!(sql, options)
- case lock = options[:lock]
- when true; sql << ' FOR UPDATE'
- when String; sql << " #{lock}"
- end
- end
-
def default_sequence_name(table, column)
nil
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 8fae26b790..d09aa3c4d2 100755
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,6 +1,7 @@
require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
+require 'active_support/core_ext/benchmark'
# TODO: Autoload these files
require 'active_record/connection_adapters/abstract/schema_definitions'
@@ -191,7 +192,6 @@ module ActiveRecord
end
def log_info(sql, name, ms)
- @runtime += ms
if @logger && @logger.debug?
name = '%s (%.1fms)' % [name || 'SQL', ms]
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
@@ -199,8 +199,12 @@ module ActiveRecord
end
protected
- def log(sql, name, &block)
- ActiveSupport::Notifications.instrument(:sql, :sql => sql, :name => name, &block)
+ def log(sql, name)
+ result = nil
+ ActiveSupport::Notifications.instrument(:sql, :sql => sql, :name => name) do
+ @runtime += Benchmark.ms { result = yield }
+ end
+ result
rescue Exception => e
# Log message and raise exception.
# Set last_verification to 0, so that connection gets verified
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index ad36ff22e3..fa28bc64df 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -470,6 +470,13 @@ module ActiveRecord
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
end
+ def add_column(table_name, column_name, type, options = {})
+ add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ add_column_options!(add_column_sql, options)
+ add_column_position!(add_column_sql, options)
+ execute(add_column_sql)
+ end
+
def change_column_default(table_name, column_name, default) #:nodoc:
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
@@ -498,6 +505,7 @@ module ActiveRecord
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(change_column_sql, options)
+ add_column_position!(change_column_sql, options)
execute(change_column_sql)
end
@@ -529,6 +537,13 @@ module ActiveRecord
end
end
+ def add_column_position!(sql, options)
+ if options[:first]
+ sql << " FIRST"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
+ end
+ end
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index c9c2892ba4..78b897add6 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -183,12 +183,6 @@ module ActiveRecord
catch_schema_changes { @connection.rollback }
end
- # SELECT ... FOR UPDATE is redundant since the table is locked.
- def add_lock!(sql, options) #:nodoc:
- sql
- end
-
-
# SCHEMA STATEMENTS ========================================
def tables(name = nil) #:nodoc:
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index 092f5f0023..e33d389f8c 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -1,6 +1,9 @@
en:
activerecord:
errors:
+ # model.errors.full_messages format.
+ format: "{{attribute}} {{message}}"
+
# The values :model, :attribute and :value are always available for interpolation
# The value :count is available when applicable. Can be used for pluralization.
messages:
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index c8cd79a2b0..f9e538c586 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -23,16 +23,6 @@ module ActiveRecord
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
- # Optimistic locking will also check for stale data when objects are destroyed. Example:
- #
- # p1 = Person.find(1)
- # p2 = Person.find(1)
- #
- # p1.first_name = "Michael"
- # p1.save
- #
- # p2.destroy # Raises a ActiveRecord::StaleObjectError
- #
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
@@ -49,7 +39,6 @@ module ActiveRecord
self.lock_optimistically = true
alias_method_chain :update, :lock
- alias_method_chain :destroy, :lock
alias_method_chain :attributes_from_column_definition, :lock
class << self
@@ -89,11 +78,11 @@ module ActiveRecord
attribute_names.uniq!
begin
- arel_table = self.class.arel_table(self.class.table_name)
+ relation = self.class.active_relation
- affected_rows = arel_table.where(
- arel_table[self.class.primary_key].eq(quoted_id).and(
- arel_table[self.class.locking_column].eq(quote_value(previous_value))
+ affected_rows = relation.where(
+ relation[self.class.primary_key].eq(quoted_id).and(
+ relation[self.class.locking_column].eq(quote_value(previous_value))
)
).update(arel_attributes_values(false, false, attribute_names))
@@ -111,29 +100,6 @@ module ActiveRecord
end
end
- def destroy_with_lock #:nodoc:
- return destroy_without_lock unless locking_enabled?
-
- unless new_record?
- lock_col = self.class.locking_column
- previous_value = send(lock_col).to_i
-
- arel_table = self.class.arel_table(self.class.table_name)
-
- affected_rows = arel_table.where(
- arel_table[self.class.primary_key].eq(quoted_id).and(
- arel_table[self.class.locking_column].eq(quote_value(previous_value))
- )
- ).delete
-
- unless affected_rows == 1
- raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object"
- end
- end
-
- freeze
- end
-
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb
index bbe2d1f205..f63b249241 100644
--- a/activerecord/lib/active_record/named_scope.rb
+++ b/activerecord/lib/active_record/named_scope.rb
@@ -6,18 +6,34 @@ module ActiveRecord
module NamedScope
extend ActiveSupport::Concern
- # All subclasses of ActiveRecord::Base have one named scope:
- # * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
- #
- # These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
- # intermediate values (scopes) around as first-class objects is convenient.
- #
- # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
- included do
- named_scope :scoped, lambda { |scope| scope }
- end
-
module ClassMethods
+ # Returns a relation if invoked without any arguments.
+ #
+ # posts = Post.scoped
+ # posts.size # Fires "select count(*) from posts" and returns the count
+ # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
+ #
+ # Returns an anonymous named scope if any options are supplied.
+ #
+ # shirts = Shirt.scoped(:conditions => {:color => 'red'})
+ # shirts = shirts.scoped(:include => :washing_instructions)
+ #
+ # Anonymous \scopes tend to be useful when procedurally generating complex queries, where passing
+ # intermediate values (scopes) around as first-class objects is convenient.
+ #
+ # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope.
+ def scoped(options = {}, &block)
+ if options.present?
+ Scope.new(self, options, &block)
+ else
+ unless scoped?(:find)
+ finder_needs_type_condition? ? active_relation.where(type_condition) : active_relation.spawn
+ else
+ construct_finder_arel
+ end
+ end
+ end
+
def scopes
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index f61a4a6c73..dbdeba6c24 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -212,6 +212,11 @@ module ActiveRecord
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
# exception is raised. If omitted, any number associations can be processed.
# Note that the :limit option is only applicable to one-to-many associations.
+ # [:update_only]
+ # Allows you to specify that an existing record may only be updated.
+ # A new record may only be created when there is no existing record.
+ # This option only works for one-to-one associations and is ignored for
+ # collection associations. This option is off by default.
#
# Examples:
# # creates avatar_attributes=
@@ -221,9 +226,9 @@ module ActiveRecord
# # creates avatar_attributes= and posts_attributes=
# accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
def accepts_nested_attributes_for(*attr_names)
- options = { :allow_destroy => false }
+ options = { :allow_destroy => false, :update_only => false }
options.update(attr_names.extract_options!)
- options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
@@ -235,7 +240,7 @@ module ActiveRecord
end
reflection.options[:autosave] = true
-
+ add_autosave_association_callbacks(reflection)
self.nested_attributes_options[association_name.to_sym] = options
if options[:reject_if] == :all_blank
@@ -243,15 +248,13 @@ module ActiveRecord
end
# def pirate_attributes=(attributes)
- # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
# end
class_eval %{
def #{association_name}_attributes=(attributes)
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
end
}, __FILE__, __LINE__
-
- add_autosave_association_callbacks(reflection)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
@@ -286,28 +289,29 @@ module ActiveRecord
# Assigns the given attributes to the association.
#
- # If the given attributes include an <tt>:id</tt> that matches the existing
- # record’s id, then the existing record will be modified. Otherwise a new
- # record will be built.
+ # If update_only is false and the given attributes include an <tt>:id</tt>
+ # that matches the existing record’s id, then the existing record will be
+ # modified. If update_only is true, a new record is only created when no
+ # object exists. Otherwise a new record will be built.
#
- # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
- # <tt>:_destroy</tt> key set to a truthy value, then the existing record
- # will be marked for destruction.
+ # If the given attributes include a matching <tt>:id</tt> attribute, or
+ # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
+ # then the existing record will be marked for destruction.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
+ check_existing_record = (options[:update_only] || !attributes['id'].blank?)
- if attributes['id'].blank?
- unless reject_new_record?(association_name, attributes)
- method = "build_#{association_name}"
- if respond_to?(method)
- send(method, attributes.except(*UNASSIGNABLE_KEYS))
- else
- raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
- end
+ if check_existing_record && (record = send(association_name)) &&
+ (options[:update_only] || record.id.to_s == attributes['id'].to_s)
+ assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy])
+ elsif !reject_new_record?(association_name, attributes)
+ method = "build_#{association_name}"
+ if respond_to?(method)
+ send(method, attributes.except(*UNASSIGNABLE_KEYS))
+ else
+ raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
end
- elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
end
diff --git a/activerecord/lib/active_record/notifications.rb b/activerecord/lib/active_record/notifications.rb
deleted file mode 100644
index 562a5b91f4..0000000000
--- a/activerecord/lib/active_record/notifications.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'active_support/notifications'
-
-ActiveSupport::Notifications.subscribe("sql") do |name, before, after, result, instrumenter_id, payload|
- ActiveRecord::Base.connection.log_info(payload[:sql], name, after - before)
-end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
new file mode 100644
index 0000000000..657ee738c0
--- /dev/null
+++ b/activerecord/lib/active_record/railtie.rb
@@ -0,0 +1,71 @@
+# For now, action_controller must always be present with
+# rails, so let's make sure that it gets required before
+# here. This is needed for correctly setting up the middleware.
+# In the future, this might become an optional require.
+require "active_record"
+require "action_controller/railtie"
+require "rails"
+
+module ActiveRecord
+ class Railtie < Rails::Railtie
+ plugin_name :active_record
+
+ rake_tasks do
+ load "active_record/railties/databases.rake"
+ end
+
+ initializer "active_record.set_configs" do |app|
+ app.config.active_record.each do |k,v|
+ ActiveRecord::Base.send "#{k}=", v
+ end
+ end
+
+ # This sets the database configuration from Configuration#database_configuration
+ # and then establishes the connection.
+ initializer "active_record.initialize_database" do |app|
+ ActiveRecord::Base.configurations = app.config.database_configuration
+ ActiveRecord::Base.establish_connection
+ end
+
+ initializer "active_record.initialize_timezone" do
+ ActiveRecord::Base.time_zone_aware_attributes = true
+ ActiveRecord::Base.default_timezone = :utc
+ end
+
+ # Expose database runtime to controller for logging.
+ initializer "active_record.log_runtime" do |app|
+ require "active_record/railties/controller_runtime"
+ ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime
+ end
+
+ # Setup database middleware after initializers have run
+ initializer "active_record.initialize_database_middleware" do |app|
+ middleware = app.config.middleware
+ if middleware.include?(ActiveRecord::SessionStore)
+ middleware.insert_before ActiveRecord::SessionStore, ActiveRecord::ConnectionAdapters::ConnectionManagement
+ middleware.insert_before ActiveRecord::SessionStore, ActiveRecord::QueryCache
+ else
+ middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement
+ middleware.use ActiveRecord::QueryCache
+ end
+ end
+
+ initializer "active_record.load_observers" do
+ ActiveRecord::Base.instantiate_observers
+ end
+
+ # TODO: ActiveRecord::Base.logger should delegate to its own config.logger
+ initializer "active_record.logger" do
+ ActiveRecord::Base.logger ||= ::Rails.logger
+ end
+
+ initializer "active_record.notifications" do
+ require 'active_support/notifications'
+
+ ActiveSupport::Notifications.subscribe("sql") do |name, before, after, instrumenter_id, payload|
+ ActiveRecord::Base.connection.log_info(payload[:sql], payload[:name], (after - before) * 1000)
+ end
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
new file mode 100644
index 0000000000..535e967ec3
--- /dev/null
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -0,0 +1,31 @@
+require 'active_support/core_ext/module/attr_internal'
+
+module ActiveRecord
+ module Railties
+ module ControllerRuntime
+ extend ActiveSupport::Concern
+
+ attr_internal :db_runtime
+
+ def cleanup_view_runtime
+ if ActiveRecord::Base.connected?
+ db_rt_before_render = ActiveRecord::Base.connection.reset_runtime
+ runtime = super
+ db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
+ self.db_runtime = db_rt_before_render + db_rt_after_render
+ runtime - db_rt_after_render
+ else
+ super
+ end
+ end
+
+ module ClassMethods
+ def log_process_action(controller)
+ super
+ db_runtime = controller.send :db_runtime
+ logger.info(" ActiveRecord runtime: %.1fms" % db_runtime.to_f) if db_runtime
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
new file mode 100644
index 0000000000..a35a6c156b
--- /dev/null
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -0,0 +1,469 @@
+namespace :db do
+ task :load_config => :rails_env do
+ require 'active_record'
+ ActiveRecord::Base.configurations = Rails::Configuration.new.database_configuration
+ end
+
+ namespace :create do
+ desc 'Create all the local databases defined in config/database.yml'
+ task :all => :load_config do
+ ActiveRecord::Base.configurations.each_value do |config|
+ # Skip entries that don't have a database key, such as the first entry here:
+ #
+ # defaults: &defaults
+ # adapter: mysql
+ # username: root
+ # password:
+ # host: localhost
+ #
+ # development:
+ # database: blog_development
+ # <<: *defaults
+ next unless config['database']
+ # Only connect to local databases
+ local_database?(config) { create_database(config) }
+ end
+ end
+ end
+
+ desc 'Create the database defined in config/database.yml for the current RAILS_ENV'
+ task :create => :load_config do
+ create_database(ActiveRecord::Base.configurations[RAILS_ENV])
+ end
+
+ def create_database(config)
+ begin
+ if config['adapter'] =~ /sqlite/
+ if File.exist?(config['database'])
+ $stderr.puts "#{config['database']} already exists"
+ else
+ begin
+ # Create the SQLite database
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Base.connection
+ rescue
+ $stderr.puts $!, *($!.backtrace)
+ $stderr.puts "Couldn't create database for #{config.inspect}"
+ end
+ end
+ return # Skip the else clause of begin/rescue
+ else
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Base.connection
+ end
+ rescue
+ case config['adapter']
+ when 'mysql'
+ @charset = ENV['CHARSET'] || 'utf8'
+ @collation = ENV['COLLATION'] || 'utf8_unicode_ci'
+ creation_options = {:charset => (config['charset'] || @charset), :collation => (config['collation'] || @collation)}
+ begin
+ ActiveRecord::Base.establish_connection(config.merge('database' => nil))
+ ActiveRecord::Base.connection.create_database(config['database'], creation_options)
+ ActiveRecord::Base.establish_connection(config)
+ rescue Mysql::Error => sqlerr
+ if sqlerr.errno == Mysql::Error::ER_ACCESS_DENIED_ERROR
+ print "#{sqlerr.error}. \nPlease provide the root password for your mysql installation\n>"
+ root_password = $stdin.gets.strip
+ grant_statement = "GRANT ALL PRIVILEGES ON #{config['database']}.* " \
+ "TO '#{config['username']}'@'localhost' " \
+ "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;"
+ ActiveRecord::Base.establish_connection(config.merge(
+ 'database' => nil, 'username' => 'root', 'password' => root_password))
+ ActiveRecord::Base.connection.create_database(config['database'], creation_options)
+ ActiveRecord::Base.connection.execute grant_statement
+ ActiveRecord::Base.establish_connection(config)
+ else
+ $stderr.puts sqlerr.error
+ $stderr.puts "Couldn't create database for #{config.inspect}, charset: #{config['charset'] || @charset}, collation: #{config['collation'] || @collation}"
+ $stderr.puts "(if you set the charset manually, make sure you have a matching collation)" if config['charset']
+ end
+ end
+ when 'postgresql'
+ @encoding = config[:encoding] || ENV['CHARSET'] || 'utf8'
+ begin
+ ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
+ ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => @encoding))
+ ActiveRecord::Base.establish_connection(config)
+ rescue
+ $stderr.puts $!, *($!.backtrace)
+ $stderr.puts "Couldn't create database for #{config.inspect}"
+ end
+ end
+ else
+ $stderr.puts "#{config['database']} already exists"
+ end
+ end
+
+ namespace :drop do
+ desc 'Drops all the local databases defined in config/database.yml'
+ task :all => :load_config do
+ ActiveRecord::Base.configurations.each_value do |config|
+ # Skip entries that don't have a database key
+ next unless config['database']
+ begin
+ # Only connect to local databases
+ local_database?(config) { drop_database(config) }
+ rescue Exception => e
+ puts "Couldn't drop #{config['database']} : #{e.inspect}"
+ end
+ end
+ end
+ end
+
+ desc 'Drops the database for the current RAILS_ENV'
+ task :drop => :load_config do
+ config = ActiveRecord::Base.configurations[RAILS_ENV || 'development']
+ begin
+ drop_database(config)
+ rescue Exception => e
+ puts "Couldn't drop #{config['database']} : #{e.inspect}"
+ end
+ end
+
+ def local_database?(config, &block)
+ if %w( 127.0.0.1 localhost ).include?(config['host']) || config['host'].blank?
+ yield
+ else
+ puts "This task only modifies local databases. #{config['database']} is on a remote host."
+ end
+ end
+
+
+ desc "Migrate the database through scripts in db/migrate and update db/schema.rb by invoking db:schema:dump. Target specific version with VERSION=x. Turn off output with VERBOSE=false."
+ task :migrate => :environment do
+ ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
+ ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+
+ namespace :migrate do
+ desc 'Rollbacks the database one migration and re migrate up. If you want to rollback more than one step, define STEP=x. Target specific version with VERSION=x.'
+ task :redo => :environment do
+ if ENV["VERSION"]
+ Rake::Task["db:migrate:down"].invoke
+ Rake::Task["db:migrate:up"].invoke
+ else
+ Rake::Task["db:rollback"].invoke
+ Rake::Task["db:migrate"].invoke
+ end
+ end
+
+ desc 'Resets your database using your migrations for the current environment'
+ task :reset => ["db:drop", "db:create", "db:migrate"]
+
+ desc 'Runs the "up" for a given migration VERSION.'
+ task :up => :environment do
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
+ raise "VERSION is required" unless version
+ ActiveRecord::Migrator.run(:up, "db/migrate/", version)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+
+ desc 'Runs the "down" for a given migration VERSION.'
+ task :down => :environment do
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
+ raise "VERSION is required" unless version
+ ActiveRecord::Migrator.run(:down, "db/migrate/", version)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+ end
+
+ desc 'Rolls the schema back to the previous version. Specify the number of steps with STEP=n'
+ task :rollback => :environment do
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
+ ActiveRecord::Migrator.rollback('db/migrate/', step)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+
+ desc 'Pushes the schema to the next version. Specify the number of steps with STEP=n'
+ task :forward => :environment do
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
+ ActiveRecord::Migrator.forward('db/migrate/', step)
+ Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
+ end
+
+ desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
+ task :reset => [ 'db:drop', 'db:setup' ]
+
+ desc "Retrieves the charset for the current environment's database"
+ task :charset => :environment do
+ config = ActiveRecord::Base.configurations[RAILS_ENV || 'development']
+ case config['adapter']
+ when 'mysql'
+ ActiveRecord::Base.establish_connection(config)
+ puts ActiveRecord::Base.connection.charset
+ when 'postgresql'
+ ActiveRecord::Base.establish_connection(config)
+ puts ActiveRecord::Base.connection.encoding
+ else
+ puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
+ end
+ end
+
+ desc "Retrieves the collation for the current environment's database"
+ task :collation => :environment do
+ config = ActiveRecord::Base.configurations[RAILS_ENV || 'development']
+ case config['adapter']
+ when 'mysql'
+ ActiveRecord::Base.establish_connection(config)
+ puts ActiveRecord::Base.connection.collation
+ else
+ puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
+ end
+ end
+
+ desc "Retrieves the current schema version number"
+ task :version => :environment do
+ puts "Current version: #{ActiveRecord::Migrator.current_version}"
+ end
+
+ desc "Raises an error if there are pending migrations"
+ task :abort_if_pending_migrations => :environment do
+ if defined? ActiveRecord
+ pending_migrations = ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations
+
+ if pending_migrations.any?
+ puts "You have #{pending_migrations.size} pending migrations:"
+ pending_migrations.each do |pending_migration|
+ puts ' %4d %s' % [pending_migration.version, pending_migration.name]
+ end
+ abort %{Run "rake db:migrate" to update your database then try again.}
+ end
+ end
+ end
+
+ desc 'Create the database, load the schema, and initialize with the seed data'
+ task :setup => [ 'db:create', 'db:schema:load', 'db:seed' ]
+
+ desc 'Load the seed data from db/seeds.rb'
+ task :seed => :environment do
+ seed_file = File.join(Rails.root, 'db', 'seeds.rb')
+ load(seed_file) if File.exist?(seed_file)
+ end
+
+ namespace :fixtures do
+ desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
+ task :load => :environment do
+ require 'active_record/fixtures'
+ ActiveRecord::Base.establish_connection(Rails.env)
+ base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
+ fixtures_dir = ENV['FIXTURES_DIR'] ? File.join(base_dir, ENV['FIXTURES_DIR']) : base_dir
+
+ (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/).map {|f| File.join(fixtures_dir, f) } : Dir.glob(File.join(fixtures_dir, '*.{yml,csv}'))).each do |fixture_file|
+ Fixtures.create_fixtures(File.dirname(fixture_file), File.basename(fixture_file, '.*'))
+ end
+ end
+
+ desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
+ task :identify => :environment do
+ require "active_record/fixtures"
+
+ label, id = ENV["LABEL"], ENV["ID"]
+ raise "LABEL or ID required" if label.blank? && id.blank?
+
+ puts %Q(The fixture ID for "#{label}" is #{Fixtures.identify(label)}.) if label
+
+ base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
+ Dir["#{base_dir}/**/*.yml"].each do |file|
+ if data = YAML::load(ERB.new(IO.read(file)).result)
+ data.keys.each do |key|
+ key_id = Fixtures.identify(key)
+
+ if key == label || key_id == id.to_i
+ puts "#{file}: #{key} (#{key_id})"
+ end
+ end
+ end
+ end
+ end
+ end
+
+ namespace :schema do
+ desc "Create a db/schema.rb file that can be portably used against any DB supported by AR"
+ task :dump => :environment do
+ require 'active_record/schema_dumper'
+ File.open(ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb", "w") do |file|
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ end
+ Rake::Task["db:schema:dump"].reenable
+ end
+
+ desc "Load a schema.rb file into the database"
+ task :load => :environment do
+ file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
+ if File.exists?(file)
+ load(file)
+ else
+ abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to prevent active_record from loading: config.frameworks -= [ :active_record ]}
+ end
+ end
+ end
+
+ namespace :structure do
+ desc "Dump the database structure to a SQL file"
+ task :dump => :environment do
+ abcs = ActiveRecord::Base.configurations
+ case abcs[RAILS_ENV]["adapter"]
+ when "mysql", "oci", "oracle"
+ ActiveRecord::Base.establish_connection(abcs[RAILS_ENV])
+ File.open("#{Rails.root}/db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump }
+ when "postgresql"
+ ENV['PGHOST'] = abcs[RAILS_ENV]["host"] if abcs[RAILS_ENV]["host"]
+ ENV['PGPORT'] = abcs[RAILS_ENV]["port"].to_s if abcs[RAILS_ENV]["port"]
+ ENV['PGPASSWORD'] = abcs[RAILS_ENV]["password"].to_s if abcs[RAILS_ENV]["password"]
+ search_path = abcs[RAILS_ENV]["schema_search_path"]
+ unless search_path.blank?
+ search_path = search_path.split(",").map{|search_path| "--schema=#{search_path.strip}" }.join(" ")
+ end
+ `pg_dump -i -U "#{abcs[RAILS_ENV]["username"]}" -s -x -O -f db/#{RAILS_ENV}_structure.sql #{search_path} #{abcs[RAILS_ENV]["database"]}`
+ raise "Error dumping database" if $?.exitstatus == 1
+ when "sqlite", "sqlite3"
+ dbfile = abcs[RAILS_ENV]["database"] || abcs[RAILS_ENV]["dbfile"]
+ `#{abcs[RAILS_ENV]["adapter"]} #{dbfile} .schema > db/#{RAILS_ENV}_structure.sql`
+ when "sqlserver"
+ `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /f db\\#{RAILS_ENV}_structure.sql /q /A /r`
+ `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /F db\ /q /A /r`
+ when "firebird"
+ set_firebird_env(abcs[RAILS_ENV])
+ db_string = firebird_db_string(abcs[RAILS_ENV])
+ sh "isql -a #{db_string} > #{Rails.root}/db/#{RAILS_ENV}_structure.sql"
+ else
+ raise "Task not supported by '#{abcs["test"]["adapter"]}'"
+ end
+
+ if ActiveRecord::Base.connection.supports_migrations?
+ File.open("#{Rails.root}/db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
+ end
+ end
+ end
+
+ namespace :test do
+ desc "Recreate the test database from the current schema.rb"
+ task :load => 'db:test:purge' do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
+ ActiveRecord::Schema.verbose = false
+ Rake::Task["db:schema:load"].invoke
+ end
+
+ desc "Recreate the test database from the current environment's database schema"
+ task :clone => %w(db:schema:dump db:test:load)
+
+ desc "Recreate the test databases from the development structure"
+ task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do
+ abcs = ActiveRecord::Base.configurations
+ case abcs["test"]["adapter"]
+ when "mysql"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0')
+ IO.readlines("#{Rails.root}/db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |table|
+ ActiveRecord::Base.connection.execute(table)
+ end
+ when "postgresql"
+ ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"]
+ ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"]
+ ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"]
+ `psql -U "#{abcs["test"]["username"]}" -f #{Rails.root}/db/#{RAILS_ENV}_structure.sql #{abcs["test"]["database"]}`
+ when "sqlite", "sqlite3"
+ dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"]
+ `#{abcs["test"]["adapter"]} #{dbfile} < #{Rails.root}/db/#{RAILS_ENV}_structure.sql`
+ when "sqlserver"
+ `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql`
+ when "oci", "oracle"
+ ActiveRecord::Base.establish_connection(:test)
+ IO.readlines("#{Rails.root}/db/#{RAILS_ENV}_structure.sql").join.split(";\n\n").each do |ddl|
+ ActiveRecord::Base.connection.execute(ddl)
+ end
+ when "firebird"
+ set_firebird_env(abcs["test"])
+ db_string = firebird_db_string(abcs["test"])
+ sh "isql -i #{Rails.root}/db/#{RAILS_ENV}_structure.sql #{db_string}"
+ else
+ raise "Task not supported by '#{abcs["test"]["adapter"]}'"
+ end
+ end
+
+ desc "Empty the test database"
+ task :purge => :environment do
+ abcs = ActiveRecord::Base.configurations
+ case abcs["test"]["adapter"]
+ when "mysql"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"], abcs["test"])
+ when "postgresql"
+ ActiveRecord::Base.clear_active_connections!
+ drop_database(abcs['test'])
+ create_database(abcs['test'])
+ when "sqlite","sqlite3"
+ dbfile = abcs["test"]["database"] || abcs["test"]["dbfile"]
+ File.delete(dbfile) if File.exist?(dbfile)
+ when "sqlserver"
+ dropfkscript = "#{abcs["test"]["host"]}.#{abcs["test"]["database"]}.DP1".gsub(/\\/,'-')
+ `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{dropfkscript}`
+ `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql`
+ when "oci", "oracle"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl|
+ ActiveRecord::Base.connection.execute(ddl)
+ end
+ when "firebird"
+ ActiveRecord::Base.establish_connection(:test)
+ ActiveRecord::Base.connection.recreate_database!
+ else
+ raise "Task not supported by '#{abcs["test"]["adapter"]}'"
+ end
+ end
+
+ desc 'Check for pending migrations and load the test schema'
+ task :prepare => 'db:abort_if_pending_migrations' do
+ if defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
+ Rake::Task[{ :sql => "db:test:clone_structure", :ruby => "db:test:load" }[ActiveRecord::Base.schema_format]].invoke
+ end
+ end
+ end
+
+ namespace :sessions do
+ desc "Creates a sessions migration for use with ActiveRecord::SessionStore"
+ task :create => :environment do
+ raise "Task unavailable to this database (no migration support)" unless ActiveRecord::Base.connection.supports_migrations?
+ require 'rails/generators'
+ require 'rails/generators/rails/session_migration/session_migration_generator'
+ Rails::Generators::SessionMigrationGenerator.start [ ENV["MIGRATION"] || "add_sessions_table" ]
+ end
+
+ desc "Clear the sessions table"
+ task :clear => :environment do
+ ActiveRecord::Base.connection.execute "DELETE FROM #{session_table_name}"
+ end
+ end
+end
+
+def drop_database(config)
+ case config['adapter']
+ when 'mysql'
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Base.connection.drop_database config['database']
+ when /^sqlite/
+ require 'pathname'
+ path = Pathname.new(config['database'])
+ file = path.absolute? ? path.to_s : File.join(Rails.root, path)
+
+ FileUtils.rm(file)
+ when 'postgresql'
+ ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
+ ActiveRecord::Base.connection.drop_database config['database']
+ end
+end
+
+def session_table_name
+ ActiveRecord::SessionStore::Session.table_name
+end
+
+def set_firebird_env(config)
+ ENV["ISC_USER"] = config["username"].to_s if config["username"]
+ ENV["ISC_PASSWORD"] = config["password"].to_s if config["password"]
+end
+
+def firebird_db_string(config)
+ FireRuby::Database.db_string_for(config.symbolize_keys)
+end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index db5d2b25ed..b751c9ad68 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -214,8 +214,10 @@ module ActiveRecord
end
def check_validity_of_inverse!
- if has_inverse? && inverse_of.nil?
- raise InverseOfAssociationNotFoundError.new(self)
+ unless options[:polymorphic]
+ if has_inverse? && inverse_of.nil?
+ raise InverseOfAssociationNotFoundError.new(self)
+ end
end
end
@@ -237,8 +239,16 @@ module ActiveRecord
def inverse_of
if has_inverse?
@inverse_of ||= klass.reflect_on_association(options[:inverse_of])
- else
- nil
+ end
+ end
+
+ def polymorphic_inverse_of(associated_class)
+ if has_inverse?
+ if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
+ inverse_relationship
+ else
+ raise InverseOfAssociationNotFoundError.new(self, associated_class)
+ end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 5f0eec754f..6b9925d4e7 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,127 +1,195 @@
module ActiveRecord
class Relation
- delegate :to_sql, :to => :relation
- delegate :length, :collect, :find, :map, :each, :to => :to_a
+ include QueryMethods, FinderMethods, CalculationMethods, SpawnMethods
+
+ delegate :length, :collect, :map, :each, :all?, :to => :to_a
+
attr_reader :relation, :klass
+ attr_writer :readonly, :table
+ attr_accessor :preload_associations, :eager_load_associations, :includes_associations
def initialize(klass, relation)
@klass, @relation = klass, relation
- @readonly = false
- @associations_to_preload = []
+ @preload_associations = []
@eager_load_associations = []
+ @includes_associations = []
+ @loaded, @readonly = false
end
- def preload(association)
- @associations_to_preload += association
- self
+ def new(*args, &block)
+ with_create_scope { @klass.new(*args, &block) }
end
- def eager_load(association)
- @eager_load_associations += association
- self
+ def create(*args, &block)
+ with_create_scope { @klass.create(*args, &block) }
end
- def readonly
- @readonly = true
- self
+ def create!(*args, &block)
+ with_create_scope { @klass.create!(*args, &block) }
+ end
+
+ def respond_to?(method, include_private = false)
+ return true if @relation.respond_to?(method, include_private) || Array.method_defined?(method)
+
+ if match = DynamicFinderMatch.match(method)
+ return true if @klass.send(:all_attributes_exists?, match.attribute_names)
+ elsif match = DynamicScopeMatch.match(method)
+ return true if @klass.send(:all_attributes_exists?, match.attribute_names)
+ else
+ super
+ end
end
def to_a
- records = if @eager_load_associations.any?
- catch :invalid_query do
- return @klass.send(:find_with_associations, {
+ return @records if loaded?
+
+ find_with_associations = @eager_load_associations.any? || references_eager_loaded_tables?
+
+ @records = if find_with_associations
+ begin
+ @klass.send(:find_with_associations, {
:select => @relation.send(:select_clauses).join(', '),
:joins => @relation.joins(relation),
:group => @relation.send(:group_clauses).join(', '),
- :order => @relation.send(:order_clauses).join(', '),
- :conditions => @relation.send(:where_clauses).join("\n\tAND "),
+ :order => order_clause,
+ :conditions => where_clause,
:limit => @relation.taken,
- :offset => @relation.skipped
+ :offset => @relation.skipped,
+ :from => (@relation.send(:from_clauses) if @relation.send(:sources).present?)
},
- ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations, nil))
+ ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations + @includes_associations, nil))
+ rescue ThrowResult
+ []
end
- []
else
@klass.find_by_sql(@relation.to_sql)
end
- @klass.send(:preload_associations, records, @associations_to_preload) unless @associations_to_preload.empty?
- records.each { |record| record.readonly! } if @readonly
+ preload = @preload_associations
+ preload += @includes_associations unless find_with_associations
+ preload.each {|associations| @klass.send(:preload_associations, @records, associations) }
+
+ @records.each { |record| record.readonly! } if @readonly
- records
+ @loaded = true
+ @records
end
- def first
- @relation = @relation.take(1)
- to_a.first
+ alias all to_a
+
+ def size
+ loaded? ? @records.length : count
end
- def select(selects)
- selects.blank? ? self : Relation.new(@klass, @relation.project(selects))
+ def empty?
+ loaded? ? @records.empty? : count.zero?
end
- def group(groups)
- groups.blank? ? self : Relation.new(@klass, @relation.group(groups))
+ def any?
+ if block_given?
+ to_a.any? { |*block_args| yield(*block_args) }
+ else
+ !empty?
+ end
end
- def order(orders)
- orders.blank? ? self : Relation.new(@klass, @relation.order(orders))
+ def many?
+ if block_given?
+ to_a.many? { |*block_args| yield(*block_args) }
+ else
+ @relation.send(:taken).present? ? to_a.many? : size > 1
+ end
end
- def limit(limits)
- limits.blank? ? self : Relation.new(@klass, @relation.take(limits))
+ def destroy_all
+ to_a.each {|object| object.destroy}
+ reset
end
- def offset(offsets)
- offsets.blank? ? self : Relation.new(@klass, @relation.skip(offsets))
+ def delete_all
+ @relation.delete.tap { reset }
end
- def on(join)
- join.blank? ? self : Relation.new(@klass, @relation.on(join))
+ def delete(id_or_array)
+ where(@klass.primary_key => id_or_array).delete_all
end
- def joins(join, join_type = nil)
- if join.blank?
- self
- else
- join = case join
- when String
- @relation.join(join)
- when Hash, Array, Symbol
- if @klass.send(:array_of_strings?, join)
- @relation.join(join.join(' '))
- else
- @relation.join(@klass.send(:build_association_joins, join))
- end
- else
- @relation.join(join, join_type)
- end
- Relation.new(@klass, join)
- end
+ def loaded?
+ @loaded
+ end
+
+ def reload
+ @loaded = false
+ reset
+ end
+
+ def reset
+ @first = @last = @create_scope = @to_sql = @order_clause = nil
+ @records = []
+ self
+ end
+
+ def table
+ @table ||= Arel::Table.new(@klass.table_name, :engine => @klass.active_relation_engine)
end
- def conditions(conditions)
- if conditions.blank?
- self
+ def primary_key
+ @primary_key ||= table[@klass.primary_key]
+ end
+
+ def to_sql
+ @to_sql ||= @relation.to_sql
+ end
+
+ protected
+
+ def method_missing(method, *args, &block)
+ if @relation.respond_to?(method)
+ @relation.send(method, *args, &block)
+ elsif Array.method_defined?(method)
+ to_a.send(method, *args, &block)
+ elsif match = DynamicFinderMatch.match(method)
+ attributes = match.attribute_names
+ super unless @klass.send(:all_attributes_exists?, attributes)
+
+ if match.finder?
+ find_by_attributes(match, attributes, *args)
+ elsif match.instantiator?
+ find_or_instantiator_by_attributes(match, attributes, *args, &block)
+ end
else
- conditions = @klass.send(:merge_conditions, conditions) if [String, Hash, Array].include?(conditions.class)
- Relation.new(@klass, @relation.where(conditions))
+ super
end
end
- def respond_to?(method)
- @relation.respond_to?(method) || Array.method_defined?(method) || super
+ def with_create_scope
+ @klass.send(:with_scope, :create => create_scope) { yield }
end
- private
- def method_missing(method, *args, &block)
- if @relation.respond_to?(method)
- @relation.send(method, *args, &block)
- elsif Array.method_defined?(method)
- to_a.send(method, *args, &block)
- else
- super
- end
+ def create_scope
+ @create_scope ||= wheres.inject({}) do |hash, where|
+ hash[where.operand1.name] = where.operand2.value if where.is_a?(Arel::Predicates::Equality)
+ hash
end
+ end
+
+ def where_clause(join_string = " AND ")
+ @relation.send(:where_clauses).join(join_string)
+ end
+
+ def order_clause
+ @order_clause ||= @relation.send(:order_clauses).join(', ')
+ end
+
+ def references_eager_loaded_tables?
+ joined_tables = (tables_in_string(@relation.joins(relation)) + [table.name, table.table_alias]).compact.uniq
+ (tables_in_string(to_sql) - joined_tables).any?
+ end
+
+ def tables_in_string(string)
+ return [] if string.blank?
+ string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.uniq
+ end
+
end
end
diff --git a/activerecord/lib/active_record/relation/calculation_methods.rb b/activerecord/lib/active_record/relation/calculation_methods.rb
new file mode 100644
index 0000000000..5246c7bc5d
--- /dev/null
+++ b/activerecord/lib/active_record/relation/calculation_methods.rb
@@ -0,0 +1,177 @@
+module ActiveRecord
+ module CalculationMethods
+
+ def count(*args)
+ calculate(:count, *construct_count_options_from_args(*args))
+ end
+
+ def average(column_name)
+ calculate(:average, column_name)
+ end
+
+ def minimum(column_name)
+ calculate(:minimum, column_name)
+ end
+
+ def maximum(column_name)
+ calculate(:maximum, column_name)
+ end
+
+ def sum(column_name)
+ calculate(:sum, column_name)
+ end
+
+ def calculate(operation, column_name, options = {})
+ operation = operation.to_s.downcase
+
+ if operation == "count"
+ joins = @relation.joins(relation)
+ if joins.present? && joins =~ /LEFT OUTER/i
+ distinct = true
+ column_name = @klass.primary_key if column_name == :all
+ end
+
+ distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i
+ distinct ||= options[:distinct]
+ else
+ distinct = nil
+ end
+
+ distinct = options[:distinct] || distinct
+ column_name = :all if column_name.blank? && operation == "count"
+
+ if @relation.send(:groupings).any?
+ return execute_grouped_calculation(operation, column_name)
+ else
+ return execute_simple_calculation(operation, column_name, distinct)
+ end
+ rescue ThrowResult
+ 0
+ end
+
+ private
+
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
+ column = if @klass.column_names.include?(column_name.to_s)
+ Arel::Attribute.new(@klass.active_relation, column_name)
+ else
+ Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s)
+ end
+
+ relation = select(operation == 'count' ? column.count(distinct) : column.send(operation))
+ type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
+ end
+
+ def execute_grouped_calculation(operation, column_name) #:nodoc:
+ group_attr = @relation.send(:groupings).first.value
+ association = @klass.reflect_on_association(group_attr.to_sym)
+ associated = association && association.macro == :belongs_to # only count belongs_to associations
+ group_field = associated ? association.primary_key_name : group_attr
+ group_alias = column_alias_for(group_field)
+ group_column = column_for(group_field)
+
+ group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field
+
+ aggregate_alias = column_alias_for(operation, column_name)
+
+ select_statement = if operation == 'count' && column_name == :all
+ "COUNT(*) AS count_all"
+ else
+ Arel::Attribute.new(@klass.active_relation, column_name).send(operation).as(aggregate_alias).to_sql
+ end
+
+ select_statement << ", #{group_field} AS #{group_alias}"
+
+ relation = select(select_statement).group(group)
+
+ calculated_data = @klass.connection.select_all(relation.to_sql)
+
+ if association
+ key_ids = calculated_data.collect { |row| row[group_alias] }
+ key_records = association.klass.base_class.find(key_ids)
+ key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
+ end
+
+ calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
+ key = type_cast_calculated_value(row[group_alias], group_column)
+ key = key_records[key] if associated
+ value = row[aggregate_alias]
+ all[key] = type_cast_calculated_value(value, column_for(column_name), operation)
+ all
+ end
+ end
+
+ def construct_count_options_from_args(*args)
+ options = {}
+ column_name = :all
+
+ # Handles count(), count(:column), count(:distinct => true), count(:column, :distinct => true)
+ # TODO : relation.projections only works when .select() was last in the chain. Fix it!
+ case args.size
+ when 0
+ select = get_projection_name_from_chained_relations
+ column_name = select if select !~ /(,|\*)/
+ when 1
+ if args[0].is_a?(Hash)
+ select = get_projection_name_from_chained_relations
+ column_name = select if select !~ /(,|\*)/
+ options = args[0]
+ else
+ column_name = args[0]
+ end
+ when 2
+ column_name, options = args
+ else
+ raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
+ end
+
+ [column_name || :all, options]
+ end
+
+ # Converts the given keys to the value that the database adapter returns as
+ # a usable column name:
+ #
+ # column_alias_for("users.id") # => "users_id"
+ # column_alias_for("sum(id)") # => "sum_id"
+ # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
+ # column_alias_for("count(*)") # => "count_all"
+ # column_alias_for("count", "id") # => "count_id"
+ def column_alias_for(*keys)
+ table_name = keys.join(' ')
+ table_name.downcase!
+ table_name.gsub!(/\*/, 'all')
+ table_name.gsub!(/\W+/, ' ')
+ table_name.strip!
+ table_name.gsub!(/ +/, '_')
+
+ @klass.connection.table_alias_for(table_name)
+ end
+
+ def column_for(field)
+ field_name = field.to_s.split('.').last
+ @klass.columns.detect { |c| c.name.to_s == field_name }
+ end
+
+ def type_cast_calculated_value(value, column, operation = nil)
+ case operation
+ when 'count' then value.to_i
+ when 'sum' then type_cast_using_column(value || '0', column)
+ when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
+ else type_cast_using_column(value, column)
+ end
+ end
+
+ def type_cast_using_column(value, column)
+ column ? column.type_cast(value) : value
+ end
+
+ def get_projection_name_from_chained_relations(relation = @relation)
+ if relation.respond_to?(:projections) && relation.projections.present?
+ relation.send(:select_clauses).join(', ')
+ elsif relation.respond_to?(:relation)
+ get_projection_name_from_chained_relations(relation.relation)
+ end
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
new file mode 100644
index 0000000000..c3e5f27838
--- /dev/null
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -0,0 +1,120 @@
+module ActiveRecord
+ module FinderMethods
+
+ def find(*ids, &block)
+ return to_a.find(&block) if block_given?
+
+ expects_array = ids.first.kind_of?(Array)
+ return ids.first if expects_array && ids.first.empty?
+
+ ids = ids.flatten.compact.uniq
+
+ case ids.size
+ when 0
+ raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
+ when 1
+ result = find_one(ids.first)
+ expects_array ? [ result ] : result
+ else
+ find_some(ids)
+ end
+ end
+
+ def exists?(id = nil)
+ relation = select(primary_key).limit(1)
+ relation = relation.where(primary_key.eq(id)) if id
+ relation.first ? true : false
+ end
+
+ def first
+ if loaded?
+ @records.first
+ else
+ @first ||= limit(1).to_a[0]
+ end
+ end
+
+ def last
+ if loaded?
+ @records.last
+ else
+ @last ||= reverse_order.limit(1).to_a[0]
+ end
+ end
+
+ protected
+
+ def find_by_attributes(match, attributes, *args)
+ conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h}
+ result = where(conditions).send(match.finder)
+
+ if match.bang? && result.blank?
+ raise RecordNotFound, "Couldn't find #{@klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
+ else
+ result
+ end
+ end
+
+ def find_or_instantiator_by_attributes(match, attributes, *args)
+ guard_protected_attributes = false
+
+ if args[0].is_a?(Hash)
+ guard_protected_attributes = true
+ attributes_for_create = args[0].with_indifferent_access
+ conditions = attributes_for_create.slice(*attributes).symbolize_keys
+ else
+ attributes_for_create = conditions = attributes.inject({}) {|h, a| h[a] = args[attributes.index(a)]; h}
+ end
+
+ record = where(conditions).first
+
+ unless record
+ record = @klass.new { |r| r.send(:attributes=, attributes_for_create, guard_protected_attributes) }
+ yield(record) if block_given?
+ record.save if match.instantiator == :create
+ end
+
+ record
+ end
+
+ def find_one(id)
+ record = where(primary_key.eq(id)).first
+
+ unless record
+ conditions = where_clause(', ')
+ conditions = " [WHERE #{conditions}]" if conditions.present?
+ raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}"
+ end
+
+ record
+ end
+
+ def find_some(ids)
+ result = where(primary_key.in(ids)).all
+
+ expected_size =
+ if @relation.taken && ids.size > @relation.taken
+ @relation.taken
+ else
+ ids.size
+ end
+
+ # 11 ids with limit 3, offset 9 should give 2 results.
+ if @relation.skipped && (ids.size - @relation.skipped < expected_size)
+ expected_size = ids.size - @relation.skipped
+ end
+
+ if result.size == expected_size
+ result
+ else
+ conditions = where_clause(', ')
+ conditions = " [WHERE #{conditions}]" if conditions.present?
+
+ error = "Couldn't find all #{@klass.name.pluralize} with IDs "
+ error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
+ raise RecordNotFound, error
+ end
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
new file mode 100644
index 0000000000..6b7d941350
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -0,0 +1,45 @@
+module ActiveRecord
+ class PredicateBuilder
+
+ def initialize(engine)
+ @engine = engine
+ end
+
+ def build_from_hash(attributes, default_table)
+ predicates = attributes.map do |column, value|
+ table = default_table
+
+ if value.is_a?(Hash)
+ table = Arel::Table.new(column, :engine => @engine)
+ build_from_hash(value, table)
+ else
+ column = column.to_s
+
+ if column.include?('.')
+ table_name, column = column.split('.', 2)
+ table = Arel::Table.new(table_name, :engine => @engine)
+ end
+
+ attribute = table[column] || Arel::Attribute.new(table, column.to_sym)
+
+ case value
+ when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope
+ attribute.in(value)
+ when Range
+ # TODO : Arel should handle ranges with excluded end.
+ if value.exclude_end?
+ [attribute.gteq(value.begin), attribute.lt(value.end)]
+ else
+ attribute.in(value)
+ end
+ else
+ attribute.eq(value)
+ end
+ end
+ end
+
+ predicates.flatten
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
new file mode 100644
index 0000000000..525a9cb365
--- /dev/null
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -0,0 +1,141 @@
+module ActiveRecord
+ module QueryMethods
+
+ def preload(*associations)
+ spawn.tap {|r| r.preload_associations += Array.wrap(associations) }
+ end
+
+ def includes(*associations)
+ spawn.tap {|r| r.includes_associations += Array.wrap(associations) }
+ end
+
+ def eager_load(*associations)
+ spawn.tap {|r| r.eager_load_associations += Array.wrap(associations) }
+ end
+
+ def readonly(status = true)
+ spawn.tap {|r| r.readonly = status }
+ end
+
+ def select(selects)
+ if selects.present?
+ relation = spawn(@relation.project(selects))
+ relation.readonly = @relation.joins(relation).present? ? false : @readonly
+ relation
+ else
+ spawn
+ end
+ end
+
+ def from(from)
+ from.present? ? spawn(@relation.from(from)) : spawn
+ end
+
+ def having(*args)
+ return spawn if args.blank?
+
+ if [String, Hash, Array].include?(args.first.class)
+ havings = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first)
+ else
+ havings = args.first
+ end
+
+ spawn(@relation.having(havings))
+ end
+
+ def group(groups)
+ groups.present? ? spawn(@relation.group(groups)) : spawn
+ end
+
+ def order(orders)
+ orders.present? ? spawn(@relation.order(orders)) : spawn
+ end
+
+ def lock(locks = true)
+ case locks
+ when String
+ spawn(@relation.lock(locks))
+ when TrueClass, NilClass
+ spawn(@relation.lock)
+ else
+ spawn
+ end
+ end
+
+ def reverse_order
+ relation = spawn
+ relation.instance_variable_set(:@orders, nil)
+
+ order_clause = @relation.send(:order_clauses).join(', ')
+ if order_clause.present?
+ relation.order(reverse_sql_order(order_clause))
+ else
+ relation.order("#{@klass.table_name}.#{@klass.primary_key} DESC")
+ end
+ end
+
+ def limit(limits)
+ limits.present? ? spawn(@relation.take(limits)) : spawn
+ end
+
+ def offset(offsets)
+ offsets.present? ? spawn(@relation.skip(offsets)) : spawn
+ end
+
+ def on(join)
+ spawn(@relation.on(join))
+ end
+
+ def joins(join, join_type = nil)
+ return spawn if join.blank?
+
+ join_relation = case join
+ when String
+ @relation.join(join)
+ when Hash, Array, Symbol
+ if @klass.send(:array_of_strings?, join)
+ @relation.join(join.join(' '))
+ else
+ @relation.join(@klass.send(:build_association_joins, join))
+ end
+ else
+ @relation.join(join, join_type)
+ end
+
+ spawn(join_relation).tap { |r| r.readonly = true }
+ end
+
+ def where(*args)
+ return spawn if args.blank?
+
+ builder = PredicateBuilder.new(Arel::Sql::Engine.new(@klass))
+
+ conditions = if [String, Array].include?(args.first.class)
+ merged = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first)
+ Arel::SqlLiteral.new(merged) if merged
+ elsif args.first.is_a?(Hash)
+ attributes = @klass.send(:expand_hash_conditions_for_aggregates, args.first)
+ builder.build_from_hash(attributes, table)
+ else
+ args.first
+ end
+
+ conditions.is_a?(String) ? spawn(@relation.where(conditions)) : spawn(@relation.where(*conditions))
+ end
+
+ private
+
+ def reverse_sql_order(order_query)
+ order_query.to_s.split(/,/).each { |s|
+ if s.match(/\s(asc|ASC)$/)
+ s.gsub!(/\s(asc|ASC)$/, ' DESC')
+ elsif s.match(/\s(desc|DESC)$/)
+ s.gsub!(/\s(desc|DESC)$/, ' ASC')
+ else
+ s.concat(' DESC')
+ end
+ }.join(',')
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
new file mode 100644
index 0000000000..a637e97155
--- /dev/null
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -0,0 +1,75 @@
+module ActiveRecord
+ module SpawnMethods
+ def spawn(relation = @relation)
+ relation = Relation.new(@klass, relation)
+ relation.readonly = @readonly
+ relation.preload_associations = @preload_associations
+ relation.eager_load_associations = @eager_load_associations
+ relation.includes_associations = @includes_associations
+ relation.table = table
+ relation
+ end
+
+ def merge(r)
+ raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass
+
+ merged_relation = spawn(table).eager_load(r.eager_load_associations).preload(r.preload_associations).includes(r.includes_associations)
+ merged_relation.readonly = r.readonly
+
+ [self.relation, r.relation].each do |arel|
+ merged_relation = merged_relation.
+ joins(arel.joins(arel)).
+ group(arel.groupings).
+ limit(arel.taken).
+ offset(arel.skipped).
+ select(arel.send(:select_clauses)).
+ from(arel.sources).
+ having(arel.havings).
+ lock(arel.locked)
+ end
+
+ relation_order = r.send(:order_clause)
+ merged_order = relation_order.present? ? relation_order : order_clause
+ merged_relation = merged_relation.order(merged_order)
+
+ merged_wheres = @relation.wheres
+
+ r.wheres.each do |w|
+ if w.is_a?(Arel::Predicates::Equality)
+ merged_wheres = merged_wheres.reject {|p| p.is_a?(Arel::Predicates::Equality) && p.operand1.name == w.operand1.name }
+ end
+
+ merged_wheres << w
+ end
+
+ merged_relation.where(*merged_wheres)
+ end
+
+ alias :& :merge
+
+ def except(*skips)
+ result = Relation.new(@klass, table)
+ result.table = table
+
+ [:eager_load, :preload, :includes].each do |load_method|
+ result = result.send(load_method, send(:"#{load_method}_associations"))
+ end
+
+ result.readonly = self.readonly unless skips.include?(:readonly)
+
+ result = result.joins(@relation.joins(@relation)) unless skips.include?(:joins)
+ result = result.group(@relation.groupings) unless skips.include?(:group)
+ result = result.limit(@relation.taken) unless skips.include?(:limit)
+ result = result.offset(@relation.skipped) unless skips.include?(:offset)
+ result = result.select(@relation.send(:select_clauses)) unless skips.include?(:select)
+ result = result.from(@relation.sources) unless skips.include?(:from)
+ result = result.order(order_clause) unless skips.include?(:order)
+ result = result.where(*@relation.wheres) unless skips.include?(:where)
+ result = result.having(*@relation.havings) unless skips.include?(:having)
+ result = result.lock(@relation.locked) unless skips.include?(:lock)
+
+ result
+ end
+
+ end
+end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index e8a2a72735..12c1f23763 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -17,8 +17,6 @@ module ActiveRecord
module Validations
extend ActiveSupport::Concern
-
- include ActiveSupport::DeprecatedCallbacks
include ActiveModel::Validations
included do
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 92f47d770f..66b78682ad 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -1,5 +1,12 @@
module ActiveRecord
module Validations
+ class AssociatedValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
+ record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
+ end
+ end
+
module ClassMethods
# Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
#
@@ -33,13 +40,8 @@ module ActiveRecord
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_associated(*attr_names)
- configuration = attr_names.extract_options!
-
- validates_each(attr_names, configuration) do |record, attr_name, value|
- unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all?
- record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
- end
- end
+ options = attr_names.extract_options!
+ validates_with AssociatedValidator, options.merge(:attributes => attr_names)
end
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 711086dc2c..7efd312357 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -1,5 +1,78 @@
module ActiveRecord
module Validations
+ class UniquenessValidator < ActiveModel::EachValidator
+ def initialize(options)
+ @klass = options.delete(:klass)
+ super(options.reverse_merge(:case_sensitive => true))
+ end
+
+ def validate_each(record, attribute, value)
+ finder_class = find_finder_class_for(record)
+ table = finder_class.active_relation
+
+ table_name = record.class.quoted_table_name
+ sql, params = mount_sql_and_params(finder_class, table_name, attribute, value)
+
+ relation = table.where(sql, *params)
+
+ Array(options[:scope]).each do |scope_item|
+ scope_value = record.send(scope_item)
+ relation = relation.where(scope_item => scope_value)
+ end
+
+ unless record.new_record?
+ # TODO : This should be in Arel
+ relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id))
+ end
+
+ if relation.exists?
+ record.errors.add(attribute, :taken, :default => options[:message], :value => value)
+ end
+ end
+
+ protected
+
+ # The check for an existing value should be run from a class that
+ # isn't abstract. This means working down from the current class
+ # (self), to the first non-abstract class. Since classes don't know
+ # their subclasses, we have to build the hierarchy between self and
+ # the record's class.
+ def find_finder_class_for(record) #:nodoc:
+ class_hierarchy = [record.class]
+
+ while class_hierarchy.first != @klass
+ class_hierarchy.insert(0, class_hierarchy.first.superclass)
+ end
+
+ class_hierarchy.detect { |klass| !klass.abstract_class? }
+ end
+
+ def mount_sql_and_params(klass, table_name, attribute, value) #:nodoc:
+ column = klass.columns_hash[attribute.to_s]
+
+ operator = if value.nil?
+ "IS ?"
+ elsif column.text?
+ value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
+ "#{klass.connection.case_sensitive_equality_operator} ?"
+ else
+ "= ?"
+ end
+
+ sql_attribute = "#{table_name}.#{klass.connection.quote_column_name(attribute)}"
+
+ if value.nil? || (options[:case_sensitive] || !column.text?)
+ sql = "#{sql_attribute} #{operator}"
+ params = [value]
+ else
+ sql = "LOWER(#{sql_attribute}) #{operator}"
+ params = [value.mb_chars.downcase]
+ end
+
+ [sql, params]
+ end
+ end
+
module ClassMethods
# Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
# can be named "davidhh".
@@ -69,6 +142,7 @@ module ActiveRecord
#
# This could even happen if you use transactions with the 'serializable'
# isolation level. There are several ways to get around this problem:
+ #
# - By locking the database table before validating, and unlocking it after
# saving. However, table locking is very expensive, and thus not
# recommended.
@@ -94,65 +168,10 @@ module ActiveRecord
# index constraint errors from other types of database errors, so you
# will have to parse the (database-specific) exception message to detect
# such a case.
+ #
def validates_uniqueness_of(*attr_names)
- configuration = { :case_sensitive => true }
- configuration.update(attr_names.extract_options!)
-
- validates_each(attr_names,configuration) do |record, attr_name, value|
- # The check for an existing value should be run from a class that
- # isn't abstract. This means working down from the current class
- # (self), to the first non-abstract class. Since classes don't know
- # their subclasses, we have to build the hierarchy between self and
- # the record's class.
- class_hierarchy = [record.class]
- while class_hierarchy.first != self
- class_hierarchy.insert(0, class_hierarchy.first.superclass)
- end
-
- # Now we can work our way down the tree to the first non-abstract
- # class (which has a database table to query from).
- finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
-
- column = finder_class.columns_hash[attr_name.to_s]
-
- if value.nil?
- comparison_operator = "IS ?"
- elsif column.text?
- comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
- value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
- else
- comparison_operator = "= ?"
- end
-
- sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
-
- if value.nil? || (configuration[:case_sensitive] || !column.text?)
- condition_sql = "#{sql_attribute} #{comparison_operator}"
- condition_params = [value]
- else
- condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
- condition_params = [value.mb_chars.downcase]
- end
-
- if scope = configuration[:scope]
- Array(scope).map do |scope_item|
- scope_value = record.send(scope_item)
- condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
- condition_params << scope_value
- end
- end
-
- unless record.new_record?
- condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
- condition_params << record.send(:id)
- end
-
- finder_class.with_exclusive_scope do
- if finder_class.exists?([condition_sql, *condition_params])
- record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
- end
- end
- end
+ options = attr_names.extract_options!
+ validates_with UniquenessValidator, options.merge(:attributes => attr_names, :klass => self)
end
end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index d5a4d9007b..ffa6d45948 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -61,14 +61,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_with_two_tables_in_from_without_getting_double_quoted
- posts = Post.find(:all,
- :select => "posts.*",
- :from => "authors, posts",
- :include => :comments,
- :conditions => "posts.author_id = authors.id",
- :order => "posts.id"
- )
-
+ posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a
assert_equal 2, posts.first.comments.size
end
@@ -469,7 +462,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers
posts = nil
- Post.with_scope(:find => {
+ Post.send(:with_scope, :find => {
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'"
}) do
@@ -477,7 +470,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal 2, posts.size
end
- Post.with_scope(:find => {
+ Post.send(:with_scope, :find => {
:include => [ :comments, :author ],
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')"
}) do
@@ -487,7 +480,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the_eagers
- Post.with_scope(:find => { :conditions => "1=1" }) do
+ Post.send(:with_scope, :find => { :conditions => "1=1" }) do
posts = authors(:david).posts.find(:all,
:include => :comments,
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
@@ -506,7 +499,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope
posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :order => 'posts.id DESC', :limit => 2)
- posts_with_scoped_order = Post.with_scope(:find => {:order => 'posts.id DESC'}) do
+ posts_with_scoped_order = Post.send(:with_scope, :find => {:order => 'posts.id DESC'}) do
Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :limit => 2)
end
assert_equal posts_with_explicit_order, posts_with_scoped_order
diff --git a/activerecord/test/cases/associations/habtm_join_table_test.rb b/activerecord/test/cases/associations/habtm_join_table_test.rb
index bf3e04c3eb..745f169ad7 100644
--- a/activerecord/test/cases/associations/habtm_join_table_test.rb
+++ b/activerecord/test/cases/associations/habtm_join_table_test.rb
@@ -36,21 +36,9 @@ class HabtmJoinTableTest < ActiveRecord::TestCase
uses_transaction :test_should_raise_exception_when_join_table_has_a_primary_key
def test_should_raise_exception_when_join_table_has_a_primary_key
if ActiveRecord::Base.connection.supports_primary_key?
- assert_raise ActiveRecord::ConfigurationError do
- jaime = MyReader.create(:name=>"Jaime")
- jaime.my_books << MyBook.create(:name=>'Great Expectations')
+ assert_raise ActiveRecord::HasAndBelongsToManyAssociationWithPrimaryKeyError do
+ MyReader.has_and_belongs_to_many :my_books
end
end
end
-
- uses_transaction :test_should_cache_result_of_primary_key_check
- def test_should_cache_result_of_primary_key_check
- if ActiveRecord::Base.connection.supports_primary_key?
- ActiveRecord::Base.connection.stubs(:primary_key).with('my_books_my_readers').returns(false).once
- weaz = MyReader.create(:name=>'Weaz')
-
- weaz.my_books << MyBook.create(:name=>'Great Expectations')
- weaz.my_books << MyBook.create(:name=>'Greater Expectations')
- end
- 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 86d14c9c81..ce7eedbb54 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1179,4 +1179,3 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal firm.name, client.firm_name
end
end
-
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 5f13b66d11..608d5a3608 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -137,6 +137,28 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert !posts(:welcome).reload.people(true).include?(people(:michael))
end
+ def test_replace_order_is_preserved
+ posts(:welcome).people.clear
+ posts(:welcome).people = [people(:david), people(:michael)]
+ assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order('id').map(&:person_id)
+
+ # Test the inverse order in case the first success was a coincidence
+ posts(:welcome).people.clear
+ posts(:welcome).people = [people(:michael), people(:david)]
+ assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order('id').map(&:person_id)
+ end
+
+ def test_replace_by_id_order_is_preserved
+ posts(:welcome).people.clear
+ posts(:welcome).person_ids = [people(:david).id, people(:michael).id]
+ assert_equal [people(:david).id, people(:michael).id], posts(:welcome).readers.order('id').map(&:person_id)
+
+ # Test the inverse order in case the first success was a coincidence
+ posts(:welcome).people.clear
+ posts(:welcome).person_ids = [people(:michael).id, people(:david).id]
+ assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order('id').map(&:person_id)
+ end
+
def test_associate_with_create
assert_queries(1) { posts(:thinking) }
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 289c89d1e2..d359ad48c5 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -327,10 +327,4 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert !account.new_record?
assert_equal 500, account.credit_limit
end
-
- def test_create!_respects_hash_condition
- account = companies(:first_firm).create_account_limit_500_with_hash_conditions!
- assert !account.new_record?
- assert_equal 500, account.credit_limit
- end
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 5f08c40005..18a1cd3cd0 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -9,84 +9,84 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations
def test_construct_finder_sql_creates_inner_joins
- sql = Author.send(:construct_finder_sql, :joins => :posts)
+ sql = Author.joins(:posts).to_sql
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
end
def test_construct_finder_sql_cascades_inner_joins
- sql = Author.send(:construct_finder_sql, :joins => {:posts => :comments})
+ sql = Author.joins(:posts => :comments).to_sql
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = posts.id/, sql
end
def test_construct_finder_sql_inner_joins_through_associations
- sql = Author.send(:construct_finder_sql, :joins => :categorized_posts)
+ sql = Author.joins(:categorized_posts).to_sql
assert_match /INNER JOIN .?categorizations.?.*INNER JOIN .?posts.?/, sql
end
def test_construct_finder_sql_applies_association_conditions
- sql = Author.send(:construct_finder_sql, :joins => :categories_like_general, :conditions => "TERMINATING_MARKER")
+ sql = Author.joins(:categories_like_general).where("TERMINATING_MARKER").to_sql
assert_match /INNER JOIN .?categories.? ON.*AND.*.?General.?(.|\n)*TERMINATING_MARKER/, sql
end
def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
- result = Author.find(:all, :joins => [:thinking_posts, :welcome_posts])
+ result = Author.joins(:thinking_posts, :welcome_posts).to_a
assert_equal authors(:david), result.first
end
def test_construct_finder_sql_unpacks_nested_joins
- sql = Author.send(:construct_finder_sql, :joins => {:posts => [[:comments]]})
+ sql = Author.joins(:posts => [[:comments]]).to_sql
assert_no_match /inner join.*inner join.*inner join/i, sql, "only two join clauses should be present"
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = .?posts.?.id/, sql
end
def test_construct_finder_sql_ignores_empty_joins_hash
- sql = Author.send(:construct_finder_sql, :joins => {})
+ sql = Author.joins({}).to_sql
assert_no_match /JOIN/i, sql
end
def test_construct_finder_sql_ignores_empty_joins_array
- sql = Author.send(:construct_finder_sql, :joins => [])
+ sql = Author.joins([]).to_sql
assert_no_match /JOIN/i, sql
end
def test_find_with_implicit_inner_joins_honors_readonly_without_select
- authors = Author.find(:all, :joins => :posts)
+ authors = Author.joins(:posts).to_a
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? {|a| a.readonly? }, "expected all authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_with_select
- authors = Author.find(:all, :select => 'authors.*', :joins => :posts)
+ authors = Author.joins(:posts).select('authors.*').to_a
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_false
- authors = Author.find(:all, :joins => :posts, :readonly => false)
+ authors = Author.joins(:posts).readonly(false).to_a
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_does_not_set_associations
- authors = Author.find(:all, :select => 'authors.*', :joins => :posts)
+ authors = Author.joins(:posts).select('authors.*')
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? {|a| !a.send(:instance_variable_names).include?("@posts")}, "expected no authors to have the @posts association loaded"
end
def test_count_honors_implicit_inner_joins
- real_count = Author.find(:all).sum{|a| a.posts.count }
+ real_count = Author.scoped.to_a.sum{|a| a.posts.count }
assert_equal real_count, Author.count(:joins => :posts), "plain inner join count should match the number of referenced posts records"
end
def test_calculate_honors_implicit_inner_joins
- real_count = Author.find(:all).sum{|a| a.posts.count }
+ real_count = Author.scoped.to_a.sum{|a| a.posts.count }
assert_equal real_count, Author.calculate(:count, 'authors.id', :joins => :posts), "plain inner join count should match the number of referenced posts records"
end
def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions
- real_count = Author.find(:all).select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length
+ real_count = Author.scoped.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length
authors_with_welcoming_post_titles = Author.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'")
assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'"
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 47f83db112..1d7604f52b 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -85,7 +85,7 @@ class InverseHasOneTests < ActiveRecord::TestCase
fixtures :men, :faces
def test_parent_instance_should_be_shared_with_child_on_find
- m = Man.find(:first)
+ m = men(:gordon)
f = m.face
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
m.name = 'Bongo'
@@ -96,7 +96,7 @@ class InverseHasOneTests < ActiveRecord::TestCase
def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find
- m = Man.find(:first, :include => :face)
+ m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face)
f = m.face
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
m.name = 'Bongo'
@@ -104,7 +104,7 @@ class InverseHasOneTests < ActiveRecord::TestCase
f.man.name = 'Mungo'
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
- m = Man.find(:first, :include => :face, :order => 'faces.id')
+ m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face, :order => 'faces.id')
f = m.face
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
m.name = 'Bongo'
@@ -114,7 +114,7 @@ class InverseHasOneTests < ActiveRecord::TestCase
end
def test_parent_instance_should_be_shared_with_newly_built_child
- m = Man.find(:first)
+ m = men(:gordon)
f = m.build_face(:description => 'haunted')
assert_not_nil f.man
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
@@ -125,7 +125,7 @@ class InverseHasOneTests < ActiveRecord::TestCase
end
def test_parent_instance_should_be_shared_with_newly_created_child
- m = Man.find(:first)
+ m = men(:gordon)
f = m.create_face(:description => 'haunted')
assert_not_nil f.man
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
@@ -135,6 +135,86 @@ class InverseHasOneTests < ActiveRecord::TestCase
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
+ def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method
+ m = Man.find(:first)
+ f = m.face.create!(:description => 'haunted')
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = 'Mungo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_built_child_when_we_dont_replace_existing
+ m = Man.find(:first)
+ f = m.build_face({:description => 'haunted'}, false)
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = 'Mungo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_created_child_when_we_dont_replace_existing
+ m = Man.find(:first)
+ f = m.create_face({:description => 'haunted'}, false)
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = 'Mungo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method_when_we_dont_replace_existing
+ m = Man.find(:first)
+ f = m.face.create!({:description => 'haunted'}, false)
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = 'Mungo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_replaced_via_accessor_child
+ m = Man.find(:first)
+ f = Face.new(:description => 'haunted')
+ m.face = f
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = 'Mungo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_replaced_via_method_child
+ m = Man.find(:first)
+ f = Face.new(:description => 'haunted')
+ m.face.replace(f)
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = 'Mungo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_replaced_via_method_child_when_we_dont_replace_existing
+ m = Man.find(:first)
+ f = Face.new(:description => 'haunted')
+ m.face.replace(f, false)
+ assert_not_nil f.man
+ assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
+ f.man.name = 'Mungo'
+ assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
+ end
+
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).dirty_face }
end
@@ -144,7 +224,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
fixtures :men, :interests
def test_parent_instance_should_be_shared_with_every_child_on_find
- m = Man.find(:first)
+ m = men(:gordon)
is = m.interests
is.each do |i|
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
@@ -156,7 +236,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
end
def test_parent_instance_should_be_shared_with_eager_loaded_children
- m = Man.find(:first, :include => :interests)
+ m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests)
is = m.interests
is.each do |i|
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
@@ -166,7 +246,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
end
- m = Man.find(:first, :include => :interests, :order => 'interests.id')
+ m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests, :order => 'interests.id')
is = m.interests
is.each do |i|
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
@@ -175,11 +255,10 @@ class InverseHasManyTests < ActiveRecord::TestCase
i.man.name = 'Mungo'
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
end
-
end
def test_parent_instance_should_be_shared_with_newly_built_child
- m = Man.find(:first)
+ m = men(:gordon)
i = m.interests.build(:topic => 'Industrial Revolution Re-enactment')
assert_not_nil i.man
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
@@ -189,8 +268,20 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
end
- def test_parent_instance_should_be_shared_with_newly_created_child
+ def test_parent_instance_should_be_shared_with_newly_block_style_built_child
m = Man.find(:first)
+ i = m.interests.build {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
+ assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated"
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = 'Mungo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_created_child
+ m = men(:gordon)
i = m.interests.create(:topic => 'Industrial Revolution Re-enactment')
assert_not_nil i.man
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
@@ -200,8 +291,31 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
- def test_parent_instance_should_be_shared_with_poked_in_child
+ def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child
m = Man.find(:first)
+ i = m.interests.create!(:topic => 'Industrial Revolution Re-enactment')
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = 'Mungo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_newly_block_style_created_child
+ m = Man.find(:first)
+ i = m.interests.create {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
+ assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated"
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = 'Mungo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_poked_in_child
+ m = men(:gordon)
i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
m.interests << i
assert_not_nil i.man
@@ -212,6 +326,30 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
+ def test_parent_instance_should_be_shared_with_replaced_via_accessor_children
+ m = Man.find(:first)
+ i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
+ m.interests = [i]
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = 'Mungo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
+ end
+
+ def test_parent_instance_should_be_shared_with_replaced_via_method_children
+ m = Man.find(:first)
+ i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
+ m.interests.replace([i])
+ assert_not_nil i.man
+ assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
+ m.name = 'Bongo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
+ i.man.name = 'Mungo'
+ assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
+ end
+
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).secret_interests }
end
@@ -221,7 +359,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase
fixtures :men, :faces, :interests
def test_child_instance_should_be_shared_with_parent_on_find
- f = Face.find(:first)
+ f = faces(:trusting)
m = f.man
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
f.description = 'gormless'
@@ -231,7 +369,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase
end
def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
- f = Face.find(:first, :include => :man)
+ f = Face.find(:first, :include => :man, :conditions => {:description => 'trusting'})
m = f.man
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
f.description = 'gormless'
@@ -239,8 +377,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase
m.face.description = 'pleasing'
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
-
- f = Face.find(:first, :include => :man, :order => 'men.id')
+ f = Face.find(:first, :include => :man, :order => 'men.id', :conditions => {:description => 'trusting'})
m = f.man
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
f.description = 'gormless'
@@ -250,7 +387,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase
end
def test_child_instance_should_be_shared_with_newly_built_parent
- f = Face.find(:first)
+ f = faces(:trusting)
m = f.build_man(:name => 'Charles')
assert_not_nil m.face
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
@@ -261,7 +398,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase
end
def test_child_instance_should_be_shared_with_newly_created_parent
- f = Face.find(:first)
+ f = faces(:trusting)
m = f.create_man(:name => 'Charles')
assert_not_nil m.face
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
@@ -272,7 +409,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase
end
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
- i = Interest.find(:first)
+ i = interests(:trainspotting)
m = i.man
assert_not_nil m.interests
iz = m.interests.detect {|iz| iz.id == i.id}
@@ -284,11 +421,128 @@ class InverseBelongsToTests < ActiveRecord::TestCase
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
end
+ def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
+ f = Face.find(:first)
+ m = Man.new(:name => 'Charles')
+ f.man = m
+ assert_not_nil m.face
+ assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
+ f.description = 'gormless'
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
+ m.face.description = 'pleasing'
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
+ end
+
+ def test_child_instance_should_be_shared_with_replaced_via_method_parent
+ f = faces(:trusting)
+ assert_not_nil f.man
+ m = Man.new(:name => 'Charles')
+ f.man.replace(m)
+ assert_not_nil m.face
+ assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
+ f.description = 'gormless'
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
+ m.face.description = 'pleasing'
+ assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
+ end
+
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_man }
end
end
+class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
+ fixtures :men, :faces, :interests
+
+ def test_child_instance_should_be_shared_with_parent_on_find
+ f = Face.find(:first, :conditions => {:description => 'confused'})
+ m = f.polymorphic_man
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
+ f.description = 'gormless'
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
+ m.polymorphic_face.description = 'pleasing'
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
+ end
+
+ def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
+ f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man)
+ m = f.polymorphic_man
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
+ f.description = 'gormless'
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
+ m.polymorphic_face.description = 'pleasing'
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
+
+ f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man, :order => 'men.id')
+ m = f.polymorphic_man
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
+ f.description = 'gormless'
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
+ m.polymorphic_face.description = 'pleasing'
+ assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
+ end
+
+ def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
+ face = faces(:confused)
+ old_man = face.polymorphic_man
+ new_man = Man.new
+
+ assert_not_nil face.polymorphic_man
+ face.polymorphic_man = new_man
+
+ assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
+ face.description = 'Bongo'
+ assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
+ new_man.polymorphic_face.description = 'Mungo'
+ assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
+ end
+
+ def test_child_instance_should_be_shared_with_replaced_via_method_parent
+ face = faces(:confused)
+ old_man = face.polymorphic_man
+ new_man = Man.new
+
+ assert_not_nil face.polymorphic_man
+ face.polymorphic_man.replace(new_man)
+
+ assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
+ face.description = 'Bongo'
+ assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
+ new_man.polymorphic_face.description = 'Mungo'
+ assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
+ end
+
+ def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
+ i = interests(:llama_wrangling)
+ m = i.polymorphic_man
+ assert_not_nil m.polymorphic_interests
+ iz = m.polymorphic_interests.detect {|iz| iz.id == i.id}
+ assert_not_nil iz
+ assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
+ i.topic = 'Eating cheese with a spoon'
+ assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
+ iz.topic = 'Cow tipping'
+ assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
+ end
+
+ def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error
+ # Ideally this would, if only for symmetry's sake with other association types
+ assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man }
+ end
+
+ def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error
+ # fails because no class has the correct inverse_of for horrible_polymorphic_man
+ assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man = Man.first }
+ end
+
+ def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error
+ # passes because Man does have the correct inverse_of
+ assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Man.first }
+ # fails because Interest does have the correct inverse_of
+ assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Interest.first }
+ end
+end
+
# NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin
# which would guess the inverse rather than look for an explicit configuration option.
class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index e429c1d157..9bc34bd750 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -64,6 +64,16 @@ class AssociationsTest < ActiveRecord::TestCase
assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
end
+
+ def test_force_reload_is_uncached
+ firm = Firm.create!("name" => "A New Firm, Inc")
+ client = Client.create!("name" => "TheClient.com", :firm => firm)
+ ActiveRecord::Base.cache do
+ firm.clients.each {}
+ assert_queries(0) { assert_not_nil firm.clients.each {} }
+ assert_queries(1) { assert_not_nil firm.clients(true).each {} }
+ end
+ end
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 9164701601..cf763d730a 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -31,11 +31,40 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
assert base.valid_keys_for_has_and_belongs_to_many_association.include?(:autosave)
end
+ def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
+ assert_no_difference_when_adding_callbacks_twice_for Pirate, :ship
+ end
+
+ def test_should_not_add_the_same_callbacks_multiple_times_for_belongs_to
+ assert_no_difference_when_adding_callbacks_twice_for Ship, :pirate
+ end
+
+ def test_should_not_add_the_same_callbacks_multiple_times_for_has_many
+ assert_no_difference_when_adding_callbacks_twice_for Pirate, :birds
+ end
+
+ def test_should_not_add_the_same_callbacks_multiple_times_for_has_and_belongs_to_many
+ assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots
+ end
+
private
def base
ActiveRecord::Base
end
+
+ def assert_no_difference_when_adding_callbacks_twice_for(model, association_name)
+ reflection = model.reflect_on_association(association_name)
+ assert_no_difference "callbacks_for_model(#{model.name}).length" do
+ model.send(:add_autosave_association_callbacks, reflection)
+ end
+ end
+
+ def callbacks_for_model(model)
+ model.instance_variables.grep(/_callbacks$/).map do |ivar|
+ model.instance_variable_get(ivar)
+ end.flatten
+ end
end
class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
@@ -757,14 +786,14 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_automatically_validate_the_associated_model
@pirate.ship.name = ''
assert @pirate.invalid?
- assert @pirate.errors[:ship_name].any?
+ assert @pirate.errors[:"ship.name"].any?
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@pirate.ship.name = nil
@pirate.catchphrase = nil
assert @pirate.invalid?
- assert @pirate.errors[:ship_name].any?
+ assert @pirate.errors[:"ship.name"].any?
assert @pirate.errors[:catchphrase].any?
end
@@ -857,7 +886,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_automatically_validate_the_associated_model
@ship.pirate.catchphrase = ''
assert @ship.invalid?
- assert @ship.errors[:pirate_catchphrase].any?
+ assert @ship.errors[:"pirate.catchphrase"].any?
end
def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
@@ -865,7 +894,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
@ship.pirate.catchphrase = nil
assert @ship.invalid?
assert @ship.errors[:name].any?
- assert @ship.errors[:pirate_catchphrase].any?
+ assert @ship.errors[:"pirate.catchphrase"].any?
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@@ -932,7 +961,7 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.send(@association_name).each { |child| child.name = '' }
assert !@pirate.valid?
- assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"]
+ assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
assert @pirate.errors[@association_name].empty?
end
@@ -940,8 +969,25 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.send(@association_name).build(:name => '')
assert !@pirate.valid?
- assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"]
+ assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
+ assert @pirate.errors[@association_name].empty?
+ end
+
+ def test_should_default_invalid_error_from_i18n
+ I18n.backend.store_translations(:en, :activerecord => { :errors => { :models =>
+ { @association_name.to_s.singularize.to_sym => { :blank => "cannot be blank" } }
+ }})
+
+ @pirate.send(@association_name).build(:name => '')
+
+ assert !@pirate.valid?
+ assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"]
+ assert_equal ["#{@association_name.to_s.titleize} name cannot be blank"], @pirate.errors.full_messages
assert @pirate.errors[@association_name].empty?
+ ensure
+ I18n.backend.store_translations(:en, :activerecord => { :errors => { :models =>
+ { @association_name.to_s.singularize.to_sym => nil }
+ }})
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@@ -949,7 +995,7 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.catchphrase = nil
assert !@pirate.valid?
- assert_equal ["can't be blank"], @pirate.errors["#{@association_name}_name"]
+ assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
assert @pirate.errors[:catchphrase].any?
end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 5c2911eca1..730d9d8df7 100755
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -201,7 +201,7 @@ class BasicsTest < ActiveRecord::TestCase
topic = Topic.new(:title => "New Topic")
assert topic.save!
- reply = Reply.new
+ reply = WrongReply.new
assert_raise(ActiveRecord::RecordInvalid) { reply.save! }
end
@@ -680,6 +680,16 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal -2, Topic.find(2).replies_count
end
+ def test_reset_counters
+ assert_equal 1, Topic.find(1).replies_count
+
+ Topic.increment_counter("replies_count", 1)
+ assert_equal 2, Topic.find(1).replies_count
+
+ Topic.reset_counters(1, :replies)
+ assert_equal 1, Topic.find(1).replies_count
+ end
+
def test_update_counter
category = categories(:general)
assert_nil category.categorizations_count
@@ -949,6 +959,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_update_attributes!
+ Reply.validates_presence_of(:title)
reply = Reply.find(2)
assert_equal "The Second Topic of the day", reply.title
assert_equal "Have a nice day", reply.content
@@ -964,6 +975,8 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "Have a nice day", reply.content
assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(:title => nil, :content => "Have a nice evening") }
+ ensure
+ Reply.reset_callbacks(:validate)
end
def test_mass_assignment_should_raise_exception_if_accessible_and_protected_attribute_writers_are_both_used
@@ -1815,7 +1828,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_scoped_find_conditions
- scoped_developers = Developer.with_scope(:find => { :conditions => 'salary > 90000' }) do
+ scoped_developers = Developer.send(:with_scope, :find => { :conditions => 'salary > 90000' }) do
Developer.find(:all, :conditions => 'id < 5')
end
assert !scoped_developers.include?(developers(:david)) # David's salary is less than 90,000
@@ -1823,7 +1836,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_scoped_find_limit_offset
- scoped_developers = Developer.with_scope(:find => { :limit => 3, :offset => 2 }) do
+ scoped_developers = Developer.send(:with_scope, :find => { :limit => 3, :offset => 2 }) do
Developer.find(:all, :order => 'id')
end
assert !scoped_developers.include?(developers(:david))
@@ -1837,17 +1850,17 @@ class BasicsTest < ActiveRecord::TestCase
def test_scoped_find_order
# Test order in scope
- scoped_developers = Developer.with_scope(:find => { :limit => 1, :order => 'salary DESC' }) do
+ scoped_developers = Developer.send(:with_scope, :find => { :limit => 1, :order => 'salary DESC' }) do
Developer.find(:all)
end
assert_equal 'Jamis', scoped_developers.first.name
assert scoped_developers.include?(developers(:jamis))
# Test scope without order and order in find
- scoped_developers = Developer.with_scope(:find => { :limit => 1 }) do
+ scoped_developers = Developer.send(:with_scope, :find => { :limit => 1 }) do
Developer.find(:all, :order => 'salary DESC')
end
# Test scope order + find order, find has priority
- scoped_developers = Developer.with_scope(:find => { :limit => 3, :order => 'id DESC' }) do
+ scoped_developers = Developer.send(:with_scope, :find => { :limit => 3, :order => 'id DESC' }) do
Developer.find(:all, :order => 'salary ASC')
end
assert scoped_developers.include?(developers(:poor_jamis))
@@ -1859,7 +1872,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_scoped_find_limit_offset_including_has_many_association
- topics = Topic.with_scope(:find => {:limit => 1, :offset => 1, :include => :replies}) do
+ topics = Topic.send(:with_scope, :find => {:limit => 1, :offset => 1, :include => :replies}) do
Topic.find(:all, :order => "topics.id")
end
assert_equal 1, topics.size
@@ -1867,7 +1880,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_scoped_find_order_including_has_many_association
- developers = Developer.with_scope(:find => { :order => 'developers.salary DESC', :include => :projects }) do
+ developers = Developer.send(:with_scope, :find => { :order => 'developers.salary DESC', :include => :projects }) do
Developer.find(:all)
end
assert developers.size >= 2
@@ -1877,7 +1890,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_scoped_find_with_group_and_having
- developers = Developer.with_scope(:find => { :group => 'developers.salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do
+ developers = Developer.send(:with_scope, :find => { :group => 'developers.salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do
Developer.find(:all)
end
assert_equal 3, developers.size
@@ -1892,8 +1905,14 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal Developer.find(:first, :order => 'id desc'), Developer.last
end
+ def test_all
+ developers = Developer.all
+ assert_kind_of Array, developers
+ assert_equal Developer.find(:all), developers
+ end
+
def test_all_with_conditions
- assert_equal Developer.find(:all, :order => 'id desc'), Developer.all.order('id desc').to_a
+ assert_equal Developer.find(:all, :order => 'id desc'), Developer.order('id desc').all
end
def test_find_ordered_last
@@ -1917,7 +1936,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_find_scoped_ordered_last
- last_developer = Developer.with_scope(:find => { :order => 'developers.salary ASC' }) do
+ last_developer = Developer.send(:with_scope, :find => { :order => 'developers.salary ASC' }) do
Developer.find(:last)
end
assert_equal last_developer, Developer.find(:all, :order => 'developers.salary ASC').last
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 5009a90846..e417d8a803 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -5,7 +5,7 @@ class EachTest < ActiveRecord::TestCase
fixtures :posts
def setup
- @posts = Post.all(:order => "id asc")
+ @posts = Post.order("id asc")
@total = Post.count
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 004f4d0ea6..bd2d471fc7 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -29,8 +29,8 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal
- assert_equal 0, NumericData.send(:type_cast_calculated_value, 0, nil, 'avg')
- assert_equal 53.0, NumericData.send(:type_cast_calculated_value, 53, nil, 'avg')
+ assert_equal 0, NumericData.scoped.send(:type_cast_calculated_value, 0, nil, 'avg')
+ assert_equal 53.0, NumericData.scoped.send(:type_cast_calculated_value, 53, nil, 'avg')
end
def test_should_get_maximum_of_field
@@ -42,7 +42,7 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_get_maximum_of_field_with_scoped_include
- Account.with_scope :find => { :include => :firm, :conditions => "companies.name != 'Summit'" } do
+ Account.send :with_scope, :find => { :include => :firm, :conditions => "companies.name != 'Summit'" } do
assert_equal 50, Account.maximum(:credit_limit)
end
end
@@ -248,17 +248,15 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_reject_invalid_options
assert_nothing_raised do
- [:count, :sum].each do |func|
- # empty options are valid
- Company.send(:validate_calculation_options, func)
- # these options are valid for all calculations
- [:select, :conditions, :joins, :order, :group, :having, :distinct].each do |opt|
- Company.send(:validate_calculation_options, func, opt => true)
- end
+ # empty options are valid
+ Company.send(:validate_calculation_options)
+ # these options are valid for all calculations
+ [:select, :conditions, :joins, :order, :group, :having, :distinct].each do |opt|
+ Company.send(:validate_calculation_options, opt => true)
end
# :include is only valid on :count
- Company.send(:validate_calculation_options, :count, :include => true)
+ Company.send(:validate_calculation_options, :include => true)
end
assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index f456d273fe..4961d12a44 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -301,7 +301,7 @@ class DirtyTest < ActiveRecord::TestCase
def test_save_should_not_save_serialized_attribute_with_partial_updates_if_not_present
with_partial_updates(Topic) do
Topic.create!(:author_name => 'Bill', :content => {:a => "a"})
- topic = Topic.first(:select => 'id, author_name')
+ topic = Topic.select('id, author_name').first
topic.update_attribute :author_name, 'John'
topic = Topic.first
assert_not_nil topic.content
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 3de07797d4..d2451f24c1 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -120,7 +120,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists_with_scoped_include
- Developer.with_scope(:find => { :include => :projects, :order => "projects.name" }) do
+ Developer.send(:with_scope, :find => { :include => :projects, :order => "projects.name" }) do
assert Developer.exists?
end
end
@@ -233,11 +233,11 @@ class FinderTest < ActiveRecord::TestCase
end
def test_first
- assert_equal topics(:second).title, Topic.first(:conditions => "title = 'The Second Topic of the day'").title
+ assert_equal topics(:second).title, Topic.where("title = 'The Second Topic of the day'").first.title
end
def test_first_failing
- assert_nil Topic.first(:conditions => "title = 'The Second Topic of the day!'")
+ assert_nil Topic.where("title = 'The Second Topic of the day!'").first
end
def test_unexisting_record_exception_handling
@@ -291,7 +291,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_hash_conditions_on_joined_table
- firms = Firm.all :joins => :account, :conditions => {:accounts => { :credit_limit => 50 }}
+ firms = Firm.joins(:account).where(:accounts => { :credit_limit => 50 })
assert_equal 1, firms.size
assert_equal companies(:first_firm), firms.first
end
@@ -571,21 +571,6 @@ class FinderTest < ActiveRecord::TestCase
assert_equal(2, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 1]))
end
- def test_dynamic_finders_should_go_through_the_find_class_method
- Topic.expects(:find).with(:first, :conditions => { :title => 'The First Topic!' })
- Topic.find_by_title("The First Topic!")
-
- Topic.expects(:find).with(:last, :conditions => { :title => 'The Last Topic!' })
- Topic.find_last_by_title("The Last Topic!")
-
- Topic.expects(:find).with(:all, :conditions => { :title => 'A Topic.' })
- Topic.find_all_by_title("A Topic.")
-
- Topic.expects(:find).with(:first, :conditions => { :title => 'Does not exist yet for sure!' }).times(2)
- Topic.find_or_initialize_by_title('Does not exist yet for sure!')
- Topic.find_or_create_by_title('Does not exist yet for sure!')
- end
-
def test_find_by_one_attribute
assert_equal topics(:first), Topic.find_by_title("The First Topic")
assert_nil Topic.find_by_title("The First Topic!")
@@ -596,21 +581,6 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(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' }
- assert !Topic.public_methods.any? { |m| m.to_s == 'find_by_title' }
- t = Topic.find_by_title("The First Topic")
- assert Topic.public_methods.any? { |m| m.to_s == 'find_by_title' }
- end
-
- def test_dynamic_finder_returns_same_results_after_caching
- # ensure this test can run independently of order
- class << Topic; self; end.send(:remove_method, :find_by_title) if Topic.public_method_defined?(:find_by_title)
- t = Topic.find_by_title("The First Topic")
- assert_equal t, Topic.find_by_title("The First Topic") # find_by_title has been cached
- end
-
def test_find_by_one_attribute_with_order_option
assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id')
assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC')
@@ -654,14 +624,6 @@ class FinderTest < ActiveRecord::TestCase
assert_equal customers(:david), found_customer
end
- def test_dynamic_finder_on_one_attribute_with_conditions_caches_method
- # ensure this test can run independently of order
- class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' }
- assert !Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' }
- a = Account.find_by_credit_limit(50, :conditions => ['firm_id = ?', 6])
- assert Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' }
- end
-
def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching
# ensure this test can run independently of order
class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.any? { |m| m.to_s == 'find_by_credit_limit' }
@@ -694,14 +656,6 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Topic.find_last_by_title("A title with no matches")
end
- def test_find_last_by_one_attribute_caches_dynamic_finder
- # ensure this test can run independently of order
- class << Topic; self; end.send(:remove_method, :find_last_by_title) if Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' }
- assert !Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' }
- t = Topic.find_last_by_title(Topic.last.title)
- assert Topic.public_methods.any? { |m| m.to_s == 'find_last_by_title' }
- end
-
def test_find_last_by_invalid_method_syntax
assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
@@ -926,13 +880,6 @@ class FinderTest < ActiveRecord::TestCase
assert !c.new_record?
end
- def test_dynamic_find_or_initialize_from_one_attribute_caches_method
- class << Company; self; end.send(:remove_method, :find_or_initialize_by_name) if Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' }
- assert !Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' }
- sig38 = Company.find_or_initialize_by_name("38signals")
- assert Company.public_methods.any? { |m| m.to_s == 'find_or_initialize_by_name' }
- end
-
def test_find_or_initialize_from_two_attributes
another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John")
assert_equal "Another topic", another.title
@@ -1075,8 +1022,8 @@ class FinderTest < ActiveRecord::TestCase
def test_finder_with_scoped_from
all_topics = Topic.find(:all)
- Topic.with_scope(:find => { :from => 'fake_topics' }) do
- assert_equal all_topics, Topic.all(:from => 'topics').to_a
+ Topic.send(:with_scope, :find => { :from => 'fake_topics' }) do
+ assert_equal all_topics, Topic.from('topics').to_a
end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 25613da912..479970b2fa 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -1,11 +1,9 @@
-root = File.expand_path('../../../..', __FILE__)
begin
- require "#{root}/vendor/gems/environment"
+ require File.expand_path('../../../../vendor/gems/environment', __FILE__)
rescue LoadError
- $:.unshift("#{root}/activesupport/lib")
end
-lib = File.expand_path("#{File.dirname(__FILE__)}/../../lib")
+lib = File.expand_path('../../../lib', __FILE__)
$:.unshift(lib) unless $:.include?('lib') || $:.include?(lib)
require 'config'
@@ -49,11 +47,6 @@ ActiveRecord::Base.connection.class.class_eval do
alias_method_chain :execute, :query_record
end
-# Make with_scope public for tests
-class << ActiveRecord::Base
- public :with_scope, :with_exclusive_scope
-end
-
unless ENV['FIXTURE_DEBUG']
module ActiveRecord::TestFixtures::ClassMethods
def try_to_load_dependency_with_silence(*args)
@@ -64,9 +57,10 @@ unless ENV['FIXTURE_DEBUG']
end
end
+require "cases/validations_repair_helper"
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
- include ActiveModel::ValidationsRepairHelper
+ include ActiveRecord::ValidationsRepairHelper
self.fixture_path = FIXTURES_ROOT
self.use_instantiated_fixtures = false
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index f946e8699e..dfaecf35cf 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -38,24 +38,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::StaleObjectError) { p2.save! }
end
- def test_lock_destroy
- p1 = Person.find(1)
- p2 = Person.find(1)
- assert_equal 0, p1.lock_version
- assert_equal 0, p2.lock_version
-
- p1.first_name = 'stu'
- p1.save!
- assert_equal 1, p1.lock_version
- assert_equal 0, p2.lock_version
-
- assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
-
- assert p1.destroy
- assert_equal true, p1.frozen?
- assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
- end
-
def test_lock_repeating
p1 = Person.find(1)
p2 = Person.find(1)
@@ -243,7 +225,7 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
def test_sane_find_with_scoped_lock
assert_nothing_raised do
Person.transaction do
- Person.with_scope(:find => { :lock => true }) do
+ Person.send(:with_scope, :find => { :lock => true }) do
Person.find 1
end
end
diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb
index eb4ce0e774..cfc6f8772c 100644
--- a/activerecord/test/cases/method_scoping_test.rb
+++ b/activerecord/test/cases/method_scoping_test.rb
@@ -10,19 +10,19 @@ class MethodScopingTest < ActiveRecord::TestCase
fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
def test_set_conditions
- Developer.with_scope(:find => { :conditions => 'just a test...' }) do
+ Developer.send(:with_scope, :find => { :conditions => 'just a test...' }) do
assert_equal 'just a test...', Developer.send(:current_scoped_methods)[:find][:conditions]
end
end
def test_scoped_find
- Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
assert_nothing_raised { Developer.find(1) }
end
end
def test_scoped_find_first
- Developer.with_scope(:find => { :conditions => "salary = 100000" }) do
+ Developer.send(:with_scope, :find => { :conditions => "salary = 100000" }) do
assert_equal Developer.find(10), Developer.find(:first, :order => 'name')
end
end
@@ -30,7 +30,7 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_scoped_find_last
highest_salary = Developer.find(:first, :order => "salary DESC")
- Developer.with_scope(:find => { :order => "salary" }) do
+ Developer.send(:with_scope, :find => { :order => "salary" }) do
assert_equal highest_salary, Developer.last
end
end
@@ -39,38 +39,38 @@ class MethodScopingTest < ActiveRecord::TestCase
lowest_salary = Developer.find(:first, :order => "salary ASC")
highest_salary = Developer.find(:first, :order => "salary DESC")
- Developer.with_scope(:find => { :order => "salary" }) do
+ Developer.send(:with_scope, :find => { :order => "salary" }) do
assert_equal highest_salary, Developer.last
assert_equal lowest_salary, Developer.first
end
end
def test_scoped_find_combines_conditions
- Developer.with_scope(:find => { :conditions => "salary = 9000" }) do
+ Developer.send(:with_scope, :find => { :conditions => "salary = 9000" }) do
assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'")
end
end
def test_scoped_find_sanitizes_conditions
- Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do
+ Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do
assert_equal developers(:poor_jamis), Developer.find(:first)
end
end
def test_scoped_find_combines_and_sanitizes_conditions
- Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do
+ Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do
assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis'])
end
end
def test_scoped_find_all
- Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
assert_equal [developers(:david)], Developer.find(:all)
end
end
def test_scoped_find_select
- Developer.with_scope(:find => { :select => "id, name" }) do
+ Developer.send(:with_scope, :find => { :select => "id, name" }) do
developer = Developer.find(:first, :conditions => "name = 'David'")
assert_equal "David", developer.name
assert !developer.has_attribute?(:salary)
@@ -78,7 +78,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_options_select_replaces_scope_select
- Developer.with_scope(:find => { :select => "id, name" }) do
+ Developer.send(:with_scope, :find => { :select => "id, name" }) do
developer = Developer.find(:first, :select => 'id, salary', :conditions => "name = 'David'")
assert_equal 80000, developer.salary
assert !developer.has_attribute?(:name)
@@ -86,11 +86,11 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_scoped_count
- Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
assert_equal 1, Developer.count
end
- Developer.with_scope(:find => { :conditions => 'salary = 100000' }) do
+ Developer.send(:with_scope, :find => { :conditions => 'salary = 100000' }) do
assert_equal 8, Developer.count
assert_equal 1, Developer.count(:conditions => "name LIKE 'fixture_1%'")
end
@@ -98,7 +98,7 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_scoped_find_include
# with the include, will retrieve only developers for the given project
- scoped_developers = Developer.with_scope(:find => { :include => :projects }) do
+ scoped_developers = Developer.send(:with_scope, :find => { :include => :projects }) do
Developer.find(:all, :conditions => 'projects.id = 2')
end
assert scoped_developers.include?(developers(:david))
@@ -107,7 +107,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_scoped_find_joins
- scoped_developers = Developer.with_scope(:find => { :joins => 'JOIN developers_projects ON id = developer_id' } ) do
+ scoped_developers = Developer.send(:with_scope, :find => { :joins => 'JOIN developers_projects ON id = developer_id' } ) do
Developer.find(:all, :conditions => 'developers_projects.project_id = 2')
end
assert scoped_developers.include?(developers(:david))
@@ -117,7 +117,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_scoped_find_using_new_style_joins
- scoped_developers = Developer.with_scope(:find => { :joins => :projects }) do
+ scoped_developers = Developer.send(:with_scope, :find => { :joins => :projects }) do
Developer.find(:all, :conditions => 'projects.id = 2')
end
assert scoped_developers.include?(developers(:david))
@@ -127,7 +127,7 @@ class MethodScopingTest < ActiveRecord::TestCase
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
+ scoped_authors = Author.send(: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))
@@ -137,7 +137,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_scoped_find_merges_new_style_joins
- scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
+ scoped_authors = Author.send(: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))
@@ -147,7 +147,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_scoped_find_merges_new_and_old_style_joins
- scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
+ scoped_authors = Author.send(: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))
@@ -157,7 +157,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_scoped_find_merges_string_array_style_and_string_style_joins
- scoped_authors = Author.with_scope(:find => { :joins => ["INNER JOIN posts ON posts.author_id = authors.id"]}) do
+ scoped_authors = Author.send(:with_scope, :find => { :joins => ["INNER JOIN posts ON posts.author_id = authors.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))
@@ -167,7 +167,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_scoped_find_merges_string_array_style_and_hash_style_joins
- scoped_authors = Author.with_scope(:find => { :joins => :posts}) do
+ scoped_authors = Author.send(:with_scope, :find => { :joins => :posts}) 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))
@@ -177,7 +177,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_scoped_find_merges_joins_and_eliminates_duplicate_string_joins
- scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON posts.author_id = authors.id'}) do
+ scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON posts.author_id = authors.id'}) do
Author.find(:all, :select => 'DISTINCT authors.*', :joins => ["INNER JOIN posts ON posts.author_id = authors.id", "INNER JOIN comments ON posts.id = comments.post_id"], :conditions => 'comments.id = 1')
end
assert scoped_authors.include?(authors(:david))
@@ -187,7 +187,7 @@ class MethodScopingTest < ActiveRecord::TestCase
end
def test_scoped_find_strips_spaces_from_string_joins_and_eliminates_duplicate_string_joins
- scoped_authors = Author.with_scope(:find => { :joins => ' INNER JOIN posts ON posts.author_id = authors.id '}) do
+ scoped_authors = Author.send(:with_scope, :find => { :joins => ' INNER JOIN posts ON posts.author_id = authors.id '}) do
Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN posts ON posts.author_id = authors.id'], :conditions => 'posts.id = 1')
end
assert scoped_authors.include?(authors(:david))
@@ -198,7 +198,7 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_scoped_count_include
# with the include, will retrieve only developers for the given project
- Developer.with_scope(:find => { :include => :projects }) do
+ Developer.send(:with_scope, :find => { :include => :projects }) do
assert_equal 1, Developer.count(:conditions => 'projects.id = 2')
end
end
@@ -206,7 +206,7 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_scoped_create
new_comment = nil
- VerySpecialComment.with_scope(:create => { :post_id => 1 }) do
+ VerySpecialComment.send(:with_scope, :create => { :post_id => 1 }) do
assert_equal({ :post_id => 1 }, VerySpecialComment.send(:current_scoped_methods)[:create])
new_comment = VerySpecialComment.create :body => "Wonderful world"
end
@@ -216,14 +216,14 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_immutable_scope
options = { :conditions => "name = 'David'" }
- Developer.with_scope(:find => options) do
+ Developer.send(:with_scope, :find => options) do
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
options[:conditions] = "name != 'David'"
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
end
scope = { :find => { :conditions => "name = 'David'" }}
- Developer.with_scope(scope) do
+ Developer.send(:with_scope, scope) do
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
scope[:find][:conditions] = "name != 'David'"
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
@@ -232,7 +232,7 @@ class MethodScopingTest < ActiveRecord::TestCase
def test_scoped_with_duck_typing
scoping = Struct.new(:method_scoping).new(:find => { :conditions => ["name = ?", 'David'] })
- Developer.with_scope(scoping) do
+ Developer.send(:with_scope, scoping) do
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
end
end
@@ -241,7 +241,7 @@ class MethodScopingTest < ActiveRecord::TestCase
scoped_methods = Developer.instance_eval('current_scoped_methods')
begin
- Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do
raise "an exception"
end
rescue
@@ -254,8 +254,8 @@ class NestedScopingTest < ActiveRecord::TestCase
fixtures :authors, :developers, :projects, :comments, :posts
def test_merge_options
- Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do
- Developer.with_scope(:find => { :limit => 10 }) do
+ Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do
+ Developer.send(:with_scope, :find => { :limit => 10 }) do
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
assert_equal({ :conditions => 'salary = 80000', :limit => 10 }, merged_option)
end
@@ -263,8 +263,8 @@ class NestedScopingTest < ActiveRecord::TestCase
end
def test_merge_inner_scope_has_priority
- Developer.with_scope(:find => { :limit => 5 }) do
- Developer.with_scope(:find => { :limit => 10 }) do
+ Developer.send(:with_scope, :find => { :limit => 5 }) do
+ Developer.send(:with_scope, :find => { :limit => 10 }) do
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
assert_equal({ :limit => 10 }, merged_option)
end
@@ -272,8 +272,8 @@ class NestedScopingTest < ActiveRecord::TestCase
end
def test_replace_options
- Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
- Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
+ Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'Jamis'" }) do
assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.instance_eval('current_scoped_methods'))
assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.send(:scoped_methods)[-1])
end
@@ -281,21 +281,21 @@ class NestedScopingTest < ActiveRecord::TestCase
end
def test_append_conditions
- Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
- Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
+ Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do
appended_condition = Developer.instance_eval('current_scoped_methods')[:find][:conditions]
assert_equal("(name = 'David') AND (salary = 80000)", appended_condition)
assert_equal(1, Developer.count)
end
- Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do
assert_equal(0, Developer.count)
end
end
end
def test_merge_and_append_options
- Developer.with_scope(:find => { :conditions => 'salary = 80000', :limit => 10 }) do
- Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ Developer.send(:with_scope, :find => { :conditions => 'salary = 80000', :limit => 10 }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
assert_equal({ :conditions => "(salary = 80000) AND (name = 'David')", :limit => 10 }, merged_option)
end
@@ -303,8 +303,8 @@ class NestedScopingTest < ActiveRecord::TestCase
end
def test_nested_scoped_find
- Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
- Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do
+ Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'David'" }) do
assert_nothing_raised { Developer.find(1) }
assert_equal('David', Developer.find(:first).name)
end
@@ -313,8 +313,8 @@ class NestedScopingTest < ActiveRecord::TestCase
end
def test_nested_scoped_find_include
- Developer.with_scope(:find => { :include => :projects }) do
- Developer.with_scope(:find => { :conditions => "projects.id = 2" }) do
+ Developer.send(:with_scope, :find => { :include => :projects }) do
+ Developer.send(:with_scope, :find => { :conditions => "projects.id = 2" }) do
assert_nothing_raised { Developer.find(1) }
assert_equal('David', Developer.find(:first).name)
end
@@ -323,24 +323,24 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_nested_scoped_find_merged_include
# :include's remain unique and don't "double up" when merging
- Developer.with_scope(:find => { :include => :projects, :conditions => "projects.id = 2" }) do
- Developer.with_scope(:find => { :include => :projects }) do
+ Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do
+ Developer.send(:with_scope, :find => { :include => :projects }) do
assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
assert_equal('David', Developer.find(:first).name)
end
end
# the nested scope doesn't remove the first :include
- Developer.with_scope(:find => { :include => :projects, :conditions => "projects.id = 2" }) do
- Developer.with_scope(:find => { :include => [] }) do
+ Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do
+ Developer.send(:with_scope, :find => { :include => [] }) do
assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
assert_equal('David', Developer.find(:first).name)
end
end
# mixing array and symbol include's will merge correctly
- Developer.with_scope(:find => { :include => [:projects], :conditions => "projects.id = 2" }) do
- Developer.with_scope(:find => { :include => :projects }) do
+ Developer.send(:with_scope, :find => { :include => [:projects], :conditions => "projects.id = 2" }) do
+ Developer.send(:with_scope, :find => { :include => :projects }) do
assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
assert_equal('David', Developer.find(:first).name)
end
@@ -348,21 +348,21 @@ class NestedScopingTest < ActiveRecord::TestCase
end
def test_nested_scoped_find_replace_include
- Developer.with_scope(:find => { :include => :projects }) do
- Developer.with_exclusive_scope(:find => { :include => [] }) do
+ Developer.send(:with_scope, :find => { :include => :projects }) do
+ Developer.send(:with_exclusive_scope, :find => { :include => [] }) do
assert_equal 0, Developer.instance_eval('current_scoped_methods')[:find][:include].length
end
end
end
def test_three_level_nested_exclusive_scoped_find
- Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do
assert_equal('Jamis', Developer.find(:first).name)
- Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do
+ Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'David'" }) do
assert_equal('David', Developer.find(:first).name)
- Developer.with_exclusive_scope(:find => { :conditions => "name = 'Maiha'" }) do
+ Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'Maiha'" }) do
assert_equal(nil, Developer.find(:first))
end
@@ -377,8 +377,8 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_merged_scoped_find
poor_jamis = developers(:poor_jamis)
- Developer.with_scope(:find => { :conditions => "salary < 100000" }) do
- Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do
+ Developer.send(:with_scope, :find => { :conditions => "salary < 100000" }) do
+ Developer.send(:with_scope, :find => { :offset => 1, :order => 'id asc' }) do
# Oracle adapter does not generated space after asc therefore trailing space removed from regex
assert_sql /ORDER BY id asc/ do
assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
@@ -388,16 +388,16 @@ class NestedScopingTest < ActiveRecord::TestCase
end
def test_merged_scoped_find_sanitizes_conditions
- Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do
- Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do
+ Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do
+ Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do
assert_raise(ActiveRecord::RecordNotFound) { developers(:poor_jamis) }
end
end
end
def test_nested_scoped_find_combines_and_sanitizes_conditions
- Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do
- Developer.with_exclusive_scope(:find => { :conditions => ['salary = ?', 9000] }) do
+ Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do
+ Developer.send(:with_exclusive_scope, :find => { :conditions => ['salary = ?', 9000] }) do
assert_equal developers(:poor_jamis), Developer.find(:first)
assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis'])
end
@@ -405,8 +405,8 @@ class NestedScopingTest < ActiveRecord::TestCase
end
def test_merged_scoped_find_combines_and_sanitizes_conditions
- Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do
- Developer.with_scope(:find => { :conditions => ['salary > ?', 9000] }) do
+ Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do
+ Developer.send(:with_scope, :find => { :conditions => ['salary > ?', 9000] }) do
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
end
end
@@ -414,8 +414,8 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_nested_scoped_create
comment = nil
- Comment.with_scope(:create => { :post_id => 1}) do
- Comment.with_scope(:create => { :post_id => 2}) do
+ Comment.send(:with_scope, :create => { :post_id => 1}) do
+ Comment.send(:with_scope, :create => { :post_id => 2}) do
assert_equal({ :post_id => 2 }, Comment.send(:current_scoped_methods)[:create])
comment = Comment.create :body => "Hey guys, nested scopes are broken. Please fix!"
end
@@ -425,8 +425,8 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_nested_exclusive_scope_for_create
comment = nil
- Comment.with_scope(:create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do
- Comment.with_exclusive_scope(:create => { :post_id => 1 }) do
+ Comment.send(:with_scope, :create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do
+ Comment.send(:with_exclusive_scope, :create => { :post_id => 1 }) do
assert_equal({ :post_id => 1 }, Comment.send(:current_scoped_methods)[:create])
comment = Comment.create :body => "Hey guys"
end
@@ -437,8 +437,8 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_merged_scoped_find_on_blank_conditions
[nil, " ", [], {}].each do |blank|
- Developer.with_scope(:find => {:conditions => blank}) do
- Developer.with_scope(:find => {:conditions => blank}) do
+ Developer.send(:with_scope, :find => {:conditions => blank}) do
+ Developer.send(:with_scope, :find => {:conditions => blank}) do
assert_nothing_raised { Developer.find(:first) }
end
end
@@ -447,8 +447,8 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_merged_scoped_find_on_blank_bind_conditions
[ [""], ["",{}] ].each do |blank|
- Developer.with_scope(:find => {:conditions => blank}) do
- Developer.with_scope(:find => {:conditions => blank}) do
+ Developer.send(:with_scope, :find => {:conditions => blank}) do
+ Developer.send(:with_scope, :find => {:conditions => blank}) do
assert_nothing_raised { Developer.find(:first) }
end
end
@@ -458,8 +458,8 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_immutable_nested_scope
options1 = { :conditions => "name = 'Jamis'" }
options2 = { :conditions => "name = 'David'" }
- Developer.with_scope(:find => options1) do
- Developer.with_exclusive_scope(:find => options2) do
+ Developer.send(:with_scope, :find => options1) do
+ Developer.send(:with_exclusive_scope, :find => options2) do
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
options1[:conditions] = options2[:conditions] = nil
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
@@ -470,8 +470,8 @@ class NestedScopingTest < ActiveRecord::TestCase
def test_immutable_merged_scope
options1 = { :conditions => "name = 'Jamis'" }
options2 = { :conditions => "salary > 10000" }
- Developer.with_scope(:find => options1) do
- Developer.with_scope(:find => options2) do
+ Developer.send(:with_scope, :find => options1) do
+ Developer.send(:with_scope, :find => options2) do
assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name }
options1[:conditions] = options2[:conditions] = nil
assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name }
@@ -480,10 +480,10 @@ class NestedScopingTest < ActiveRecord::TestCase
end
def test_ensure_that_method_scoping_is_correctly_restored
- Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
scoped_methods = Developer.instance_eval('current_scoped_methods')
begin
- Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do
+ Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do
raise "an exception"
end
rescue
@@ -493,8 +493,8 @@ class NestedScopingTest < ActiveRecord::TestCase
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
+ scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id' }) do
+ Author.send(: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
@@ -505,8 +505,8 @@ class NestedScopingTest < ActiveRecord::TestCase
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
+ scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do
+ Author.send(:with_scope, :find => { :joins => :comments }) do
Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1')
end
end
@@ -517,8 +517,8 @@ class NestedScopingTest < ActiveRecord::TestCase
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
+ scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do
+ Author.send(: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
@@ -552,7 +552,7 @@ class HasManyScopingTest< ActiveRecord::TestCase
end
def test_nested_scope
- Comment.with_scope(:find => { :conditions => '1=1' }) do
+ Comment.send(:with_scope, :find => { :conditions => '1=1' }) do
assert_equal 'a comment...', @welcome.comments.what_are_you
end
end
@@ -577,7 +577,7 @@ class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
end
def test_nested_scope
- Category.with_scope(:find => { :conditions => '1=1' }) do
+ Category.send(:with_scope, :find => { :conditions => '1=1' }) do
assert_equal 'a comment...', @welcome.comments.what_are_you
end
end
@@ -633,7 +633,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_nested_scope
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
- received = DeveloperOrderedBySalary.with_scope(:find => { :order => 'name DESC'}) do
+ received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do
DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary }
end
assert_equal expected, received
@@ -647,7 +647,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_nested_exclusive_scope
expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary }
- received = DeveloperOrderedBySalary.with_exclusive_scope(:find => { :limit => 100 }) do
+ received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do
DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary }
end
assert_equal expected, received
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 6d3f938799..0ef34e440a 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -527,6 +527,53 @@ if ActiveRecord::Base.connection.supports_migrations?
assert !Person.column_methods_hash.include?(:last_name)
end
+ if current_adapter?(:MysqlAdapter)
+ def testing_table_for_positioning
+ Person.connection.create_table :testings, :id => false do |t|
+ t.column :first, :integer
+ t.column :second, :integer
+ t.column :third, :integer
+ end
+
+ yield Person.connection
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+ protected :testing_table_for_positioning
+
+ def test_column_positioning
+ testing_table_for_positioning do |conn|
+ assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
+ end
+ end
+
+ def test_add_column_with_positioning
+ testing_table_for_positioning do |conn|
+ conn.add_column :testings, :new_col, :integer
+ assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
+ end
+ testing_table_for_positioning do |conn|
+ conn.add_column :testings, :new_col, :integer, :first => true
+ assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
+ end
+ testing_table_for_positioning do |conn|
+ conn.add_column :testings, :new_col, :integer, :after => :first
+ assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
+ end
+ end
+
+ def test_change_column_with_positioning
+ testing_table_for_positioning do |conn|
+ conn.change_column :testings, :second, :integer, :first => true
+ assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
+ end
+ testing_table_for_positioning do |conn|
+ conn.change_column :testings, :second, :integer, :after => :third
+ assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
+ end
+ end
+ end
+
def test_add_rename
Person.delete_all
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 7c3e0f2ca6..6155bfd50a 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/entrant'
+require 'models/bird'
# So we can test whether Course.connection survives a reload.
require_dependency 'models/course'
@@ -82,4 +83,9 @@ class MultipleDbTest < ActiveRecord::TestCase
assert_equal "Ruby Development", Course.find(1).name
assert_equal "Ruby Developer", Entrant.find(1).name
end
+
+ def test_arel_table_engines
+ assert_not_equal Entrant.active_relation_engine, Course.active_relation_engine
+ assert_equal Entrant.active_relation_engine, Bird.active_relation_engine
+ end
end
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
index 13427daf53..5d9232bc52 100644
--- a/activerecord/test/cases/named_scope_test.rb
+++ b/activerecord/test/cases/named_scope_test.rb
@@ -345,8 +345,8 @@ class NamedScopeTest < ActiveRecord::TestCase
def test_chaining_should_use_latest_conditions_when_searching
# Normal hash conditions
- assert_equal Topic.all(:conditions => {:approved => true}).to_a, Topic.rejected.approved.all.to_a
- assert_equal Topic.all(:conditions => {:approved => false}).to_a, Topic.approved.rejected.all.to_a
+ assert_equal Topic.where(:approved => true).to_a, Topic.rejected.approved.all.to_a
+ assert_equal Topic.where(:approved => false).to_a, Topic.approved.rejected.all.to_a
# Nested hash conditions with same keys
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.all.to_a
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 53fd168e1b..8891282915 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -245,6 +245,27 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_automatically_enable_autosave_on_the_association
assert Pirate.reflect_on_association(:ship).options[:autosave]
end
+
+ def test_should_accept_update_only_option
+ @pirate.update_attribute(:update_only_ship_attributes, { :id => @pirate.ship.id, :name => 'Mayflower' })
+ end
+
+ def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
+ @ship.delete
+ assert_difference('Ship.count', 1) do
+ @pirate.reload.update_attribute(:update_only_ship_attributes, { :name => 'Mayflower' })
+ end
+ end
+
+ def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
+ @ship.delete
+ @ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning')
+
+ assert_no_difference('Ship.count') do
+ @pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' })
+ end
+ assert_equal 'Mayflower', @ship.reload.name
+ end
end
class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
@@ -362,6 +383,27 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_automatically_enable_autosave_on_the_association
assert Ship.reflect_on_association(:pirate).options[:autosave]
end
+
+ def test_should_accept_update_only_option
+ @ship.update_attribute(:update_only_pirate_attributes, { :id => @pirate.ship.id, :catchphrase => 'Arr' })
+ end
+
+ def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
+ @pirate.delete
+ assert_difference('Pirate.count', 1) do
+ @ship.reload.update_attribute(:update_only_pirate_attributes, { :catchphrase => 'Arr' })
+ end
+ end
+
+ def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
+ @pirate.delete
+ @pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye')
+
+ assert_no_difference('Pirate.count') do
+ @ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr' })
+ end
+ assert_equal 'Arr', @pirate.reload.catchphrase
+ end
end
module NestedAttributesOnACollectionAssociationTests
@@ -371,6 +413,15 @@ module NestedAttributesOnACollectionAssociationTests
assert_respond_to @pirate, association_setter
end
+ def test_should_save_only_one_association_on_create
+ pirate = Pirate.create!({
+ :catchphrase => 'Arr',
+ association_getter => { 'foo' => { :name => 'Grace OMalley' } }
+ })
+
+ assert_equal 1, pirate.reload.send(@association_name).count
+ end
+
def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
@alternate_params[association_getter].stringify_keys!
@pirate.update_attributes @alternate_params
@@ -541,7 +592,7 @@ module NestedAttributesOnACollectionAssociationTests
assert_no_difference ['Man.count', 'Interest.count'] do
man = Man.create(:name => 'John',
:interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}])
- assert !man.errors[:interests_man].empty?
+ assert !man.errors[:"interests.man"].empty?
end
end
# restore :inverse_of
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index b921cbdc9c..98011f40a4 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -33,19 +33,20 @@ class ReadOnlyTest < ActiveRecord::TestCase
def test_find_with_readonly_option
Developer.find(:all).each { |d| assert !d.readonly? }
- Developer.find(:all, :readonly => false).each { |d| assert !d.readonly? }
- Developer.find(:all, :readonly => true).each { |d| assert d.readonly? }
+ Developer.readonly(false).each { |d| assert !d.readonly? }
+ Developer.readonly(true).each { |d| assert d.readonly? }
+ Developer.readonly.each { |d| assert d.readonly? }
end
def test_find_with_joins_option_implies_readonly
# Blank joins don't count.
- Developer.find(:all, :joins => ' ').each { |d| assert !d.readonly? }
- Developer.find(:all, :joins => ' ', :readonly => false).each { |d| assert !d.readonly? }
+ Developer.joins(' ').each { |d| assert !d.readonly? }
+ Developer.joins(' ').readonly(false).each { |d| assert !d.readonly? }
# Others do.
- Developer.find(:all, :joins => ', projects').each { |d| assert d.readonly? }
- Developer.find(:all, :joins => ', projects', :readonly => false).each { |d| assert !d.readonly? }
+ Developer.joins(', projects').each { |d| assert d.readonly? }
+ Developer.joins(', projects').readonly(false).each { |d| assert !d.readonly? }
end
@@ -54,7 +55,7 @@ class ReadOnlyTest < ActiveRecord::TestCase
assert !dev.projects.empty?
assert dev.projects.all?(&:readonly?)
assert dev.projects.find(:all).all?(&:readonly?)
- assert dev.projects.find(:all, :readonly => true).all?(&:readonly?)
+ assert dev.projects.readonly(true).all?(&:readonly?)
end
def test_has_many_find_readonly
@@ -62,7 +63,7 @@ class ReadOnlyTest < ActiveRecord::TestCase
assert !post.comments.empty?
assert !post.comments.any?(&:readonly?)
assert !post.comments.find(:all).any?(&:readonly?)
- assert post.comments.find(:all, :readonly => true).all?(&:readonly?)
+ assert post.comments.readonly(true).all?(&:readonly?)
end
def test_has_many_with_through_is_not_implicitly_marked_readonly
@@ -71,32 +72,32 @@ class ReadOnlyTest < ActiveRecord::TestCase
end
def test_readonly_scoping
- Post.with_scope(:find => { :conditions => '1=1' }) do
+ Post.send(:with_scope, :find => { :conditions => '1=1' }) do
assert !Post.find(1).readonly?
- assert Post.find(1, :readonly => true).readonly?
- assert !Post.find(1, :readonly => false).readonly?
+ assert Post.readonly(true).find(1).readonly?
+ assert !Post.readonly(false).find(1).readonly?
end
- Post.with_scope(:find => { :joins => ' ' }) do
+ Post.send(:with_scope, :find => { :joins => ' ' }) do
assert !Post.find(1).readonly?
- assert Post.find(1, :readonly => true).readonly?
- assert !Post.find(1, :readonly => false).readonly?
+ assert Post.readonly.find(1).readonly?
+ assert !Post.readonly(false).find(1).readonly?
end
# Oracle barfs on this because the join includes unqualified and
# conflicting column names
unless current_adapter?(:OracleAdapter)
- Post.with_scope(:find => { :joins => ', developers' }) do
+ Post.send(:with_scope, :find => { :joins => ', developers' }) do
assert Post.find(1).readonly?
- assert Post.find(1, :readonly => true).readonly?
- assert !Post.find(1, :readonly => false).readonly?
+ assert Post.readonly.find(1).readonly?
+ assert !Post.readonly(false).find(1).readonly?
end
end
- Post.with_scope(:find => { :readonly => true }) do
+ Post.send(:with_scope, :find => { :readonly => true }) do
assert Post.find(1).readonly?
- assert Post.find(1, :readonly => true).readonly?
- assert !Post.find(1, :readonly => false).readonly?
+ assert Post.readonly.find(1).readonly?
+ assert !Post.readonly(false).find(1).readonly?
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 1a2c8030fb..f895f8b8d2 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -1,4 +1,6 @@
require "cases/helper"
+require 'models/tag'
+require 'models/tagging'
require 'models/post'
require 'models/topic'
require 'models/comment'
@@ -8,52 +10,115 @@ require 'models/comment'
require 'models/entrant'
require 'models/developer'
require 'models/company'
+require 'models/bird'
class RelationTest < ActiveRecord::TestCase
- fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments
+ fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments,
+ :taggings
+
+ def test_scoped
+ topics = Topic.scoped
+ assert_kind_of ActiveRecord::Relation, topics
+ assert_equal 4, topics.size
+ end
+
+ def test_scoped_all
+ topics = Topic.scoped.all
+ assert_kind_of Array, topics
+ assert_no_queries { assert_equal 4, topics.size }
+ end
+
+ def test_loaded_all
+ topics = Topic.scoped
+
+ assert_queries(1) do
+ 2.times { assert_equal 4, topics.all.size }
+ end
+
+ assert topics.loaded?
+ end
+
+ def test_scoped_first
+ topics = Topic.scoped.order('id ASC')
+
+ assert_queries(1) do
+ 2.times { assert_equal "The First Topic", topics.first.title }
+ end
+
+ assert ! topics.loaded?
+ end
+
+ def test_loaded_first
+ topics = Topic.scoped.order('id ASC')
+
+ assert_queries(1) do
+ topics.all # force load
+ 2.times { assert_equal "The First Topic", topics.first.title }
+ end
+
+ assert topics.loaded?
+ end
+
+ def test_reload
+ topics = Topic.scoped
+
+ assert_queries(1) do
+ 2.times { topics.to_a }
+ end
+
+ assert topics.loaded?
+
+ topics.reload
+ assert ! topics.loaded?
+
+ assert_queries(1) { topics.to_a }
+ end
def test_finding_with_conditions
- assert_equal Author.find(:all, :conditions => "name = 'David'"), Author.all.conditions("name = 'David'").to_a
+ assert_equal ["David"], Author.where(:name => 'David').map(&:name)
+ assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name)
+ assert_equal ['Mary'], Author.where("name = ?", 'Mary').map(&:name)
end
def test_finding_with_order
- topics = Topic.all.order('id')
- assert_equal 4, topics.size
+ topics = Topic.order('id')
+ assert_equal 4, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
def test_finding_with_order_and_take
- entrants = Entrant.all.order("id ASC").limit(2).to_a
+ entrants = Entrant.order("id ASC").limit(2).to_a
- assert_equal(2, entrants.size)
- assert_equal(entrants(:first).name, entrants.first.name)
+ assert_equal 2, entrants.size
+ assert_equal entrants(:first).name, entrants.first.name
end
def test_finding_with_order_limit_and_offset
- entrants = Entrant.all.order("id ASC").limit(2).offset(1)
+ entrants = Entrant.order("id ASC").limit(2).offset(1)
- assert_equal(2, entrants.size)
- assert_equal(entrants(:second).name, entrants.first.name)
+ assert_equal 2, entrants.to_a.size
+ assert_equal entrants(:second).name, entrants.first.name
- entrants = Entrant.all.order("id ASC").limit(2).offset(2)
- assert_equal(1, entrants.size)
- assert_equal(entrants(:third).name, entrants.first.name)
+ entrants = Entrant.order("id ASC").limit(2).offset(2)
+ assert_equal 1, entrants.to_a.size
+ assert_equal entrants(:third).name, entrants.first.name
end
def test_finding_with_group
- developers = Developer.all.group("salary").select("salary").to_a
+ developers = Developer.group("salary").select("salary").to_a
assert_equal 4, developers.size
assert_equal 4, developers.map(&:salary).uniq.size
end
def test_finding_with_hash_conditions_on_joined_table
- firms = DependentFirm.all.joins(:account).conditions({:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }}).to_a
+ firms = DependentFirm.joins(:account).where({:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }}).to_a
assert_equal 1, firms.size
assert_equal companies(:rails_core), firms.first
end
def test_find_all_with_join
- developers_on_project_one = Developer.all.joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').conditions('project_id=1').to_a
+ developers_on_project_one = Developer.joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').
+ where('project_id=1').to_a
assert_equal 3, developers_on_project_one.length
developer_names = developers_on_project_one.map { |d| d.name }
@@ -62,11 +127,11 @@ class RelationTest < ActiveRecord::TestCase
end
def test_find_on_hash_conditions
- assert_equal Topic.find(:all, :conditions => {:approved => false}), Topic.all.conditions({ :approved => false }).to_a
+ assert_equal Topic.find(:all, :conditions => {:approved => false}), Topic.where({ :approved => false }).to_a
end
def test_joins_with_string_array
- person_with_reader_and_post = Post.all.joins([
+ person_with_reader_and_post = Post.joins([
"INNER JOIN categorizations ON categorizations.post_id = posts.id",
"INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'"
]
@@ -74,22 +139,38 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 1, person_with_reader_and_post.size
end
- def test_relation_responds_to_delegated_methods
- relation = Topic.all
+ def test_scoped_responds_to_delegated_methods
+ relation = Topic.scoped
["map", "uniq", "sort", "insert", "delete", "update"].each do |method|
- assert relation.respond_to?(method), "Topic.all should respond to #{method.inspect}"
+ assert relation.respond_to?(method), "Topic.scoped should respond to #{method.inspect}"
+ end
+ end
+
+ def test_respond_to_private_arel_methods
+ relation = Topic.scoped
+
+ assert ! relation.respond_to?(:matching_attributes)
+ assert relation.respond_to?(:matching_attributes, true)
+ end
+
+ def test_respond_to_dynamic_finders
+ relation = Topic.scoped
+
+ ["find_by_title", "find_by_title_and_author_name", "find_or_create_by_title", "find_or_initialize_by_title_and_author_name"].each do |method|
+ assert relation.respond_to?(method), "Topic.scoped should respond to #{method.inspect}"
end
end
def test_find_with_readonly_option
- Developer.all.each { |d| assert !d.readonly? }
- Developer.all.readonly.each { |d| assert d.readonly? }
- Developer.all(:readonly => true).each { |d| assert d.readonly? }
+ Developer.scoped.each { |d| assert !d.readonly? }
+ Developer.scoped.readonly.each { |d| assert d.readonly? }
end
def test_eager_association_loading_of_stis_with_multiple_references
- authors = Author.all(:include => { :posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } } }, :order => 'comments.body, very_special_comments_posts.body', :conditions => 'posts.id = 4').to_a
+ authors = Author.eager_load(:posts => { :special_comments => { :post => [ :special_comments, :very_special_comment ] } }).
+ order('comments.body, very_special_comments_posts.body').where('posts.id = 4').to_a
+
assert_equal [authors(:david)], authors
assert_no_queries do
authors.first.posts.first.special_comments.first.post.special_comments
@@ -97,54 +178,382 @@ class RelationTest < ActiveRecord::TestCase
end
end
- def test_find_with_included_associations
+ def test_find_with_preloaded_associations
assert_queries(2) do
- posts = Post.find(:all, :include => :comments)
- posts.first.comments.first
+ posts = Post.preload(:comments)
+ assert posts.first.comments.first
end
+
assert_queries(2) do
- posts = Post.all(:include => :comments).to_a
- posts.first.comments.first
+ posts = Post.preload(:comments).to_a
+ assert posts.first.comments.first
end
+
assert_queries(2) do
- posts = Post.find(:all, :include => :author)
- posts.first.author
+ posts = Post.preload(:author)
+ assert posts.first.author
end
+
assert_queries(2) do
- posts = Post.all(:include => :author).to_a
- posts.first.author
+ posts = Post.preload(:author).to_a
+ assert posts.first.author
+ end
+
+ assert_queries(3) do
+ posts = Post.preload(:author, :comments).to_a
+ assert posts.first.author
+ assert posts.first.comments.first
end
end
- def test_default_scope_with_conditions_string
- assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.all.to_a.map(&:id).sort
+ def test_find_with_included_associations
+ assert_queries(2) do
+ posts = Post.includes(:comments)
+ assert posts.first.comments.first
+ end
+
+ assert_queries(2) do
+ posts = Post.scoped.includes(:comments)
+ assert posts.first.comments.first
+ end
+
+ assert_queries(2) do
+ posts = Post.includes(:author)
+ assert posts.first.author
+ end
+
+ assert_queries(3) do
+ posts = Post.includes(:author, :comments).to_a
+ assert posts.first.author
+ assert posts.first.comments.first
+ end
+ end
+
+ def test_default_scope_with_conditions_string
+ assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.scoped.map(&:id).sort
assert_equal nil, DeveloperCalledDavid.create!.name
end
def test_default_scope_with_conditions_hash
- assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort
+ assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.scoped.map(&:id).sort
assert_equal 'Jamis', DeveloperCalledJamis.create!.name
end
- def test_loading_with_one_association
- posts = Post.all(:include => :comments)
+ def test_default_scoping_finder_methods
+ developers = DeveloperCalledDavid.order('id').map(&:id).sort
+ assert_equal Developer.find_all_by_name('David').map(&:id).sort, developers
+ end
+
+ def test_loading_with_one_association
+ posts = Post.preload(:comments)
post = posts.find { |p| p.id == 1 }
assert_equal 2, post.comments.size
assert post.comments.include?(comments(:greetings))
- post = Post.find(:first, :include => :comments, :conditions => "posts.title = 'Welcome to the weblog'")
+ post = Post.where("posts.title = 'Welcome to the weblog'").preload(:comments).first
assert_equal 2, post.comments.size
assert post.comments.include?(comments(:greetings))
- posts = Post.all(:include => :last_comment)
+ posts = Post.preload(:last_comment)
post = posts.find { |p| p.id == 1 }
assert_equal Post.find(1).last_comment, post.last_comment
end
def test_loading_with_one_association_with_non_preload
- posts = Post.all(:include => :last_comment, :order => 'comments.id DESC')
+ posts = Post.eager_load(:last_comment).order('comments.id DESC')
post = posts.find { |p| p.id == 1 }
assert_equal Post.find(1).last_comment, post.last_comment
end
-end
+ def test_dynamic_find_by_attributes
+ david = authors(:david)
+ author = Author.preload(:taggings).find_by_id(david.id)
+ expected_taggings = taggings(:welcome_general, :thinking_general)
+
+ assert_no_queries do
+ assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
+ end
+
+ authors = Author.scoped
+ assert_equal david, authors.find_by_id_and_name(david.id, david.name)
+ assert_equal david, authors.find_by_id_and_name!(david.id, david.name)
+ end
+
+ def test_dynamic_find_by_attributes_bang
+ author = Author.scoped.find_by_id!(authors(:david).id)
+ assert_equal "David", author.name
+
+ assert_raises(ActiveRecord::RecordNotFound) { Author.scoped.find_by_id_and_name!(20, 'invalid') }
+ end
+
+ def test_dynamic_find_all_by_attributes
+ authors = Author.scoped
+
+ davids = authors.find_all_by_name('David')
+ assert_kind_of Array, davids
+ assert_equal [authors(:david)], davids
+ end
+
+ def test_dynamic_find_or_initialize_by_attributes
+ authors = Author.scoped
+
+ lifo = authors.find_or_initialize_by_name('Lifo')
+ assert_equal "Lifo", lifo.name
+ assert lifo.new_record?
+
+ assert_equal authors(:david), authors.find_or_initialize_by_name(:name => 'David')
+ end
+
+ def test_dynamic_find_or_create_by_attributes
+ authors = Author.scoped
+
+ lifo = authors.find_or_create_by_name('Lifo')
+ assert_equal "Lifo", lifo.name
+ assert ! lifo.new_record?
+
+ assert_equal authors(:david), authors.find_or_create_by_name(:name => 'David')
+ end
+
+ def test_find_id
+ authors = Author.scoped
+
+ david = authors.find(authors(:david).id)
+ assert_equal 'David', david.name
+
+ assert_raises(ActiveRecord::RecordNotFound) { authors.where(:name => 'lifo').find('42') }
+ end
+
+ def test_find_ids
+ authors = Author.order('id ASC')
+
+ results = authors.find(authors(:david).id, authors(:mary).id)
+ assert_kind_of Array, results
+ assert_equal 2, results.size
+ assert_equal 'David', results[0].name
+ assert_equal 'Mary', results[1].name
+ assert_equal results, authors.find([authors(:david).id, authors(:mary).id])
+
+ assert_raises(ActiveRecord::RecordNotFound) { authors.where(:name => 'lifo').find(authors(:david).id, '42') }
+ assert_raises(ActiveRecord::RecordNotFound) { authors.find(['42', 43]) }
+ end
+
+ def test_exists
+ davids = Author.where(:name => 'David')
+ assert davids.exists?
+ assert davids.exists?(authors(:david).id)
+ assert ! davids.exists?(authors(:mary).id)
+ assert ! davids.exists?("42")
+ assert ! davids.exists?(42)
+
+ fake = Author.where(:name => 'fake author')
+ assert ! fake.exists?
+ assert ! fake.exists?(authors(:david).id)
+ end
+
+ def test_last
+ authors = Author.scoped
+ assert_equal authors(:mary), authors.last
+ end
+
+ def test_destroy_all
+ davids = Author.where(:name => 'David')
+
+ # Force load
+ assert_equal [authors(:david)], davids.to_a
+ assert davids.loaded?
+
+ assert_difference('Author.count', -1) { davids.destroy_all }
+
+ assert_equal [], davids.to_a
+ assert davids.loaded?
+ end
+
+ def test_delete_all
+ davids = Author.where(:name => 'David')
+
+ assert_difference('Author.count', -1) { davids.delete_all }
+ assert ! davids.loaded?
+ end
+
+ def test_delete_all_loaded
+ davids = Author.where(:name => 'David')
+
+ # Force load
+ assert_equal [authors(:david)], davids.to_a
+ assert davids.loaded?
+
+ assert_difference('Author.count', -1) { davids.delete_all }
+
+ assert_equal [], davids.to_a
+ assert davids.loaded?
+ end
+
+ def test_relation_merging
+ devs = Developer.where("salary >= 80000") & Developer.limit(2) & Developer.order('id ASC').where("id < 3")
+ assert_equal [developers(:david), developers(:jamis)], devs.to_a
+
+ dev_with_count = Developer.limit(1) & Developer.order('id DESC') & Developer.select('developers.*')
+ assert_equal [developers(:poor_jamis)], dev_with_count.to_a
+ end
+
+ def test_relation_merging_with_eager_load
+ relations = []
+ relations << (Post.order('comments.id DESC') & Post.eager_load(:last_comment) & Post.scoped)
+ relations << (Post.eager_load(:last_comment) & Post.order('comments.id DESC') & Post.scoped)
+
+ relations.each do |posts|
+ post = posts.find { |p| p.id == 1 }
+ assert_equal Post.find(1).last_comment, post.last_comment
+ end
+ end
+
+ def test_relation_merging_with_locks
+ devs = Developer.lock.where("salary >= 80000").order("id DESC") & Developer.limit(2)
+ assert devs.locked.present?
+ end
+
+ def test_relation_merging_with_preload
+ [Post.scoped & Post.preload(:author), Post.preload(:author) & Post.scoped].each do |posts|
+ assert_queries(2) { assert posts.first.author }
+ end
+ end
+
+ def test_invalid_merge
+ assert_raises(ArgumentError) { Post.scoped & Developer.scoped }
+ end
+
+ def test_count
+ posts = Post.scoped
+
+ assert_equal 7, posts.count
+ assert_equal 7, posts.count(:all)
+ assert_equal 7, posts.count(:id)
+
+ assert_equal 1, posts.where('comments_count > 1').count
+ assert_equal 5, posts.where(:comments_count => 0).count
+ end
+
+ def test_count_with_distinct
+ posts = Post.scoped
+
+ assert_equal 3, posts.count(:comments_count, :distinct => true)
+ assert_equal 7, posts.count(:comments_count, :distinct => false)
+
+ assert_equal 3, posts.select(:comments_count).count(:distinct => true)
+ assert_equal 7, posts.select(:comments_count).count(:distinct => false)
+ end
+
+ def test_count_explicit_columns
+ Post.update_all(:comments_count => nil)
+ posts = Post.scoped
+
+ assert_equal [0], posts.select('comments_count').where('id is not null').group('id').order('id').count.values.uniq
+ assert_equal 0, posts.where('id is not null').select('comments_count').count
+
+ assert_equal 7, posts.select('comments_count').count('id')
+ assert_equal 0, posts.select('comments_count').count
+ assert_equal 0, posts.count(:comments_count)
+ assert_equal 0, posts.count('comments_count')
+ end
+
+ def test_size
+ posts = Post.scoped
+
+ assert_queries(1) { assert_equal 7, posts.size }
+ assert ! posts.loaded?
+
+ best_posts = posts.where(:comments_count => 0)
+ best_posts.to_a # force load
+ assert_no_queries { assert_equal 5, best_posts.size }
+ end
+
+ def test_count_complex_chained_relations
+ posts = Post.select('comments_count').where('id is not null').group("author_id").where("comments_count > 0")
+
+ expected = { 1 => 2 }
+ assert_equal expected, posts.count
+ end
+
+ def test_any
+ posts = Post.scoped
+
+ assert_queries(3) do
+ assert posts.any? # Uses COUNT()
+ assert ! posts.where(:id => nil).any?
+
+ assert posts.any? {|p| p.id > 0 }
+ assert ! posts.any? {|p| p.id <= 0 }
+ end
+
+ assert posts.loaded?
+ end
+
+ def test_many
+ posts = Post.scoped
+
+ assert_queries(2) do
+ assert posts.many? # Uses COUNT()
+ assert posts.many? {|p| p.id > 0 }
+ assert ! posts.many? {|p| p.id < 2 }
+ end
+
+ assert posts.loaded?
+ end
+
+ def test_many_with_limits
+ posts = Post.scoped
+
+ assert posts.many?
+ assert ! posts.limit(1).many?
+ end
+
+ def test_build
+ posts = Post.scoped
+
+ post = posts.new
+ assert_kind_of Post, post
+ end
+
+ def test_scoped_build
+ posts = Post.where(:title => 'You told a lie')
+
+ post = posts.new
+ assert_kind_of Post, post
+ assert_equal 'You told a lie', post.title
+ end
+
+ def test_create
+ birds = Bird.scoped
+
+ sparrow = birds.create
+ assert_kind_of Bird, sparrow
+ assert sparrow.new_record?
+
+ hen = birds.where(:name => 'hen').create
+ assert ! hen.new_record?
+ assert_equal 'hen', hen.name
+ end
+
+ def test_create_bang
+ birds = Bird.scoped
+
+ assert_raises(ActiveRecord::RecordInvalid) { birds.create! }
+
+ hen = birds.where(:name => 'hen').create!
+ assert_kind_of Bird, hen
+ assert ! hen.new_record?
+ assert_equal 'hen', hen.name
+ end
+
+ def test_except
+ relation = Post.where(:author_id => 1).order('id ASC').limit(1)
+ assert_equal [posts(:welcome)], relation.all
+
+ author_posts = relation.except(:order, :limit)
+ assert_equal Post.where(:author_id => 1).all, author_posts.all
+
+ all_posts = relation.except(:where, :order, :limit)
+ assert_equal Post.all, all_posts.all
+ end
+
+end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index 278a7a6a06..5ed997356b 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -10,7 +10,7 @@ require 'models/interest'
class AssociationValidationTest < ActiveRecord::TestCase
fixtures :topics, :owners
- repair_validations(Topic)
+ repair_validations(Topic, Reply)
def test_validates_size_of_association
repair_validations(Owner) do
@@ -40,7 +40,8 @@ class AssociationValidationTest < ActiveRecord::TestCase
end
def test_validates_associated_many
- Topic.validates_associated( :replies )
+ Topic.validates_associated(:replies)
+ Reply.validates_presence_of(:content)
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")]
assert !t.valid?
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index 532de67d99..f017f24048 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -3,8 +3,9 @@ require 'models/topic'
require 'models/reply'
class I18nValidationTest < ActiveRecord::TestCase
+ repair_validations(Topic, Reply)
def setup
- Topic.reset_callbacks(:validate)
+ Reply.validates_presence_of(:title)
@topic = Topic.new
@old_load_path, @old_backend = I18n.load_path, I18n.backend
I18n.load_path.clear
@@ -13,7 +14,6 @@ class I18nValidationTest < ActiveRecord::TestCase
end
def teardown
- Topic.reset_callbacks(:validate)
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 17ba4e2f8a..8f84841fe6 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -5,7 +5,6 @@ require 'models/reply'
require 'models/warehouse_thing'
require 'models/guid'
require 'models/event'
-require 'models/developer'
# The following methods in Topic are used in test_conditional_validation_*
class Topic
@@ -213,7 +212,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validates_uniqueness_inside_with_scope
Topic.validates_uniqueness_of(:title)
- Topic.with_scope(:find => { :conditions => { :author_name => "David" } }) do
+ Topic.send(:with_scope, :find => { :conditions => { :author_name => "David" } }) do
t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary")
assert t1.save
t2 = Topic.new("title" => "I'm unique!", "author_name" => "David")
@@ -276,14 +275,4 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert w6.errors[:city].any?, "Should have errors for city"
assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city"
end
-
- def test_validates_uniqueness_of_with_custom_message_using_quotes
- repair_validations(Developer) do
- Developer.validates_uniqueness_of :name, :message=> "This string contains 'single' and \"double\" quotes"
- d = Developer.new
- d.name = "David"
- assert !d.valid?
- assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name]
- end
- end
end
diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb
new file mode 100644
index 0000000000..11912ca1cc
--- /dev/null
+++ b/activerecord/test/cases/validations_repair_helper.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module ValidationsRepairHelper
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def repair_validations(*model_classes)
+ teardown do
+ model_classes.each do |k|
+ k.reset_callbacks(:validate)
+ end
+ end
+ end
+ end
+
+ def repair_validations(*model_classes)
+ yield
+ ensure
+ model_classes.each do |k|
+ k.reset_callbacks(:validate)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 130231c622..3a1d5ae212 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -42,7 +42,7 @@ class ValidationsTest < ActiveRecord::TestCase
repair_validations(Topic)
def test_error_on_create
- r = Reply.new
+ r = WrongReply.new
r.title = "Wrong Create"
assert !r.valid?
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
@@ -50,7 +50,7 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_error_on_update
- r = Reply.new
+ r = WrongReply.new
r.title = "Bad"
r.content = "Good"
assert r.save, "First save should be successful"
@@ -63,11 +63,11 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_invalid_record_exception
- assert_raise(ActiveRecord::RecordInvalid) { Reply.create! }
- assert_raise(ActiveRecord::RecordInvalid) { Reply.new.save! }
+ assert_raise(ActiveRecord::RecordInvalid) { WrongReply.create! }
+ assert_raise(ActiveRecord::RecordInvalid) { WrongReply.new.save! }
begin
- r = Reply.new
+ r = WrongReply.new
r.save!
flunk
rescue ActiveRecord::RecordInvalid => invalid
@@ -77,13 +77,13 @@ class ValidationsTest < ActiveRecord::TestCase
def test_exception_on_create_bang_many
assert_raise(ActiveRecord::RecordInvalid) do
- Reply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
+ WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
end
end
def test_exception_on_create_bang_with_block
assert_raise(ActiveRecord::RecordInvalid) do
- Reply.create!({ "title" => "OK" }) do |r|
+ WrongReply.create!({ "title" => "OK" }) do |r|
r.content = nil
end
end
@@ -91,21 +91,21 @@ class ValidationsTest < ActiveRecord::TestCase
def test_exception_on_create_bang_many_with_block
assert_raise(ActiveRecord::RecordInvalid) do
- Reply.create!([{ "title" => "OK" }, { "title" => "Wrong Create" }]) do |r|
+ WrongReply.create!([{ "title" => "OK" }, { "title" => "Wrong Create" }]) do |r|
r.content = nil
end
end
end
def test_scoped_create_without_attributes
- Reply.with_scope(:create => {}) do
- assert_raise(ActiveRecord::RecordInvalid) { Reply.create! }
+ WrongReply.send(:with_scope, :create => {}) do
+ assert_raise(ActiveRecord::RecordInvalid) { WrongReply.create! }
end
end
def test_create_with_exceptions_using_scope_for_protected_attributes
assert_nothing_raised do
- ProtectedPerson.with_scope( :create => { :first_name => "Mary" } ) do
+ ProtectedPerson.send(:with_scope, :create => { :first_name => "Mary" } ) do
person = ProtectedPerson.create! :addon => "Addon"
assert_equal person.first_name, "Mary", "scope should ignore attr_protected"
end
@@ -114,7 +114,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_create_with_exceptions_using_scope_and_empty_attributes
assert_nothing_raised do
- ProtectedPerson.with_scope( :create => { :first_name => "Mary" } ) do
+ ProtectedPerson.send(:with_scope, :create => { :first_name => "Mary" } ) do
person = ProtectedPerson.create!
assert_equal person.first_name, "Mary", "should be ok when no attributes are passed to create!"
end
@@ -122,15 +122,15 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_create_without_validation
- reply = Reply.new
+ reply = WrongReply.new
assert !reply.save
assert reply.save(false)
end
def test_create_without_validation_bang
- count = Reply.count
- assert_nothing_raised { Reply.new.save_without_validation! }
- assert count+1, Reply.count
+ count = WrongReply.count
+ assert_nothing_raised { WrongReply.new.save_without_validation! }
+ assert count+1, WrongReply.count
end
def test_validates_acceptance_of_with_non_existant_table
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
new file mode 100644
index 0000000000..f221def6b6
--- /dev/null
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -0,0 +1,11 @@
+require "cases/helper"
+require 'models/topic'
+
+class YamlSerializationTest < ActiveRecord::TestCase
+ def test_to_yaml_with_time_with_zone_should_not_raise_exception
+ Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
+ ActiveRecord::Base.time_zone_aware_attributes = true
+ topic = Topic.new(:written_on => DateTime.now)
+ assert_nothing_raised { topic.to_yaml }
+ end
+end
diff --git a/activerecord/test/fixtures/edges.yml b/activerecord/test/fixtures/edges.yml
index c16c70dd2f..b804f7b6a6 100644
--- a/activerecord/test/fixtures/edges.yml
+++ b/activerecord/test/fixtures/edges.yml
@@ -1,6 +1,5 @@
<% (1..4).each do |id| %>
edge_<%= id %>:
- id: <%= id %>
source_id: <%= id %>
sink_id: <%= id + 1 %>
-<% end %> \ No newline at end of file
+<% end %>
diff --git a/activerecord/test/fixtures/faces.yml b/activerecord/test/fixtures/faces.yml
index 1dd2907cf7..c8e4a34484 100644
--- a/activerecord/test/fixtures/faces.yml
+++ b/activerecord/test/fixtures/faces.yml
@@ -5,3 +5,7 @@ trusting:
weather_beaten:
description: weather beaten
man: steve
+
+confused:
+ description: confused
+ polymorphic_man: gordon (Man)
diff --git a/activerecord/test/fixtures/interests.yml b/activerecord/test/fixtures/interests.yml
index ec71890ab6..9200a19d5a 100644
--- a/activerecord/test/fixtures/interests.yml
+++ b/activerecord/test/fixtures/interests.yml
@@ -23,7 +23,11 @@ woodsmanship:
zine: going_out
man: steve
-survial:
+survival:
topic: Survival
zine: going_out
man: steve
+
+llama_wrangling:
+ topic: Llama Wrangling
+ polymorphic_man: gordon (Man)
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index 1540dbf741..edb75d333f 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -1,5 +1,7 @@
class Face < ActiveRecord::Base
belongs_to :man, :inverse_of => :face
- # This is a "broken" inverse_of for the purposes of testing
+ belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
+ # These is a "broken" inverse_of for the purposes of testing
belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face
+ belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face
end
diff --git a/activerecord/test/models/interest.rb b/activerecord/test/models/interest.rb
index d8291d00cc..d5d9226204 100644
--- a/activerecord/test/models/interest.rb
+++ b/activerecord/test/models/interest.rb
@@ -1,4 +1,5 @@
class Interest < ActiveRecord::Base
belongs_to :man, :inverse_of => :interests
+ belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_interests
belongs_to :zine, :inverse_of => :interests
end
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index f40bc9d0fc..4bff92dc98 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -1,6 +1,8 @@
class Man < ActiveRecord::Base
has_one :face, :inverse_of => :man
+ has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
has_many :interests, :inverse_of => :man
+ has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
# These are "broken" inverse_of associations for the purposes of testing
has_one :dirty_face, :class_name => 'Face', :inverse_of => :dirty_man
has_many :secret_interests, :class_name => 'Interest', :inverse_of => :secret_man
diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb
index f2c05dd48f..88c1634717 100644
--- a/activerecord/test/models/pirate.rb
+++ b/activerecord/test/models/pirate.rb
@@ -19,6 +19,7 @@ class Pirate < ActiveRecord::Base
# These both have :autosave enabled because accepts_nested_attributes_for is used on them.
has_one :ship
+ has_one :update_only_ship, :class_name => 'Ship'
has_one :non_validated_ship, :class_name => 'Ship'
has_many :birds
has_many :birds_with_method_callbacks, :class_name => "Bird",
@@ -35,6 +36,7 @@ class Pirate < ActiveRecord::Base
accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ accepts_nested_attributes_for :update_only_ship, :update_only => true
accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks,
:birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true
accepts_nested_attributes_for :birds_with_reject_all_blank, :reject_if => :all_blank
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index ba5a1d1d01..f1ba45b528 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -7,11 +7,13 @@ class Reply < Topic
belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count"
has_many :replies, :class_name => "SillyReply", :dependent => :destroy, :foreign_key => "parent_id"
+ attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read, :parent_title
+end
+
+class WrongReply < Reply
validate :errors_on_empty_content
validate :title_is_wrong_create, :on => :create
- attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read, :parent_title
-
validate :check_empty_title
validate :check_content_mismatch, :on => :create
validate :check_wrong_update, :on => :update
diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb
index 06759d64b8..a96e38ab41 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -2,9 +2,11 @@ class Ship < ActiveRecord::Base
self.record_timestamps = false
belongs_to :pirate
+ belongs_to :update_only_pirate, :class_name => 'Pirate'
has_many :parts, :class_name => 'ShipPart', :autosave => true
accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ accepts_nested_attributes_for :update_only_pirate, :update_only => true
validates_presence_of :name
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 15e5e12d03..1ec36e7832 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -160,7 +160,7 @@ ActiveRecord::Schema.define do
t.integer :access_level, :default => 1
end
- create_table :edges, :force => true do |t|
+ create_table :edges, :force => true, :id => false do |t|
t.column :source_id, :integer, :null => false
t.column :sink_id, :integer, :null => false
end
@@ -520,11 +520,15 @@ ActiveRecord::Schema.define do
create_table :faces, :force => true do |t|
t.string :description
t.integer :man_id
+ t.integer :polymorphic_man_id
+ t.string :polymorphic_man_type
end
create_table :interests, :force => true do |t|
t.string :topic
t.integer :man_id
+ t.integer :polymorphic_man_id
+ t.string :polymorphic_man_type
t.integer :zine_id
end