diff options
Diffstat (limited to 'activerecord')
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 |