diff options
author | Jeremy Kemper <jeremy@bitsweat.net> | 2009-12-29 15:46:12 -0800 |
---|---|---|
committer | Jeremy Kemper <jeremy@bitsweat.net> | 2009-12-29 15:46:12 -0800 |
commit | b27a3e8da39484d8a02d3b9c1e4dc3cb60ddcce7 (patch) | |
tree | 372a4af6df43eb8bed19c11e55b2f70907d60507 /activerecord | |
parent | ada895e8cac855a2f248aafdb92457365f062d07 (diff) | |
parent | b354496bda901cb0af499d6f3dff17a96a834a67 (diff) | |
download | rails-b27a3e8da39484d8a02d3b9c1e4dc3cb60ddcce7.tar.gz rails-b27a3e8da39484d8a02d3b9c1e4dc3cb60ddcce7.tar.bz2 rails-b27a3e8da39484d8a02d3b9c1e4dc3cb60ddcce7.zip |
Merge branch 'master' of git://github.com/mikel/rails into mail
Conflicts:
actionmailer/lib/action_mailer.rb
Diffstat (limited to 'activerecord')
26 files changed, 805 insertions, 387 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index d8bfb1916d..a2c5528860 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,65 @@ *Edge* +* 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..196b87c0ac 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -21,89 +21,108 @@ # 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 :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 :ActiveRecordError, 'active_record/base' + autoload :ConnectionNotEstablished, 'active_record/base' + + autoload :Aggregations + autoload :AssociationPreload + autoload :Associations + autoload :AttributeMethods + autoload :Attributes + autoload :AutosaveAssociation + autoload :Relation + 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 0fcd288fc5..2735bc5141 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1325,7 +1325,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 @@ -1370,7 +1370,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 @@ -1382,9 +1382,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 @@ -1717,9 +1717,9 @@ module ActiveRecord select(column_aliases(join_dependency)). group(construct_group(options[:group], options[:having], scope)). order(construct_order(options[:order], scope)). - conditions(construct_conditions(options[:conditions], scope)) + where(construct_conditions(options[:conditions], scope)) - 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 @@ -1757,7 +1757,7 @@ module ActiveRecord end relation = relation.joins(construct_join(options[:joins], scope)). - conditions(construct_conditions(options[:conditions], scope)). + where(construct_conditions(options[:conditions], scope)). group(construct_group(options[:group], options[:having], scope)). order(construct_order(options[:order], scope)). limit(construct_limit(options[:limit], scope)). diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 25e329c0c1..b85d76e8d3 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, :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! @@ -383,7 +398,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? 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 b01faa5212..9569b0c6f9 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 @@ -71,7 +71,7 @@ module ActiveRecord 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.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..be74ddfcf0 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -70,7 +70,7 @@ module ActiveRecord @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.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/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 8f37fcd515..c0d8904bc8 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -221,9 +221,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 diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 056f29f029..2008dea5e9 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -13,6 +13,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. @@ -650,6 +651,8 @@ module ActiveRecord #:nodoc: end end + delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :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) @@ -668,20 +671,10 @@ module ActiveRecord #:nodoc: options = args.extract_options! if options.empty? && !scoped?(:find) - relation = arel_table + arel_table.to_a 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 + construct_finder_arel_with_includes(options).to_a end - relation end # Executes a custom SQL query against your database and returns all the results. The results will @@ -882,7 +875,7 @@ module ActiveRecord #:nodoc: relation = arel_table 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]) @@ -945,7 +938,7 @@ module ActiveRecord #:nodoc: # 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 + arel_table.where(Arel::SqlLiteral.new(construct_conditions(conditions, scope(:find)))).delete else arel_table.delete end @@ -967,6 +960,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 @@ -1491,13 +1507,8 @@ 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 + Relation.new(self, Arel::Table.new(table || table_name)) end private @@ -1666,9 +1677,11 @@ module ActiveRecord #:nodoc: def construct_finder_arel(options = {}, scope = scope(:find)) # TODO add lock to Arel + validate_find_options(options) + relation = arel_table(options[:from]). 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)). order(construct_order(options[:order], scope)). @@ -1680,6 +1693,21 @@ module ActiveRecord #:nodoc: relation end + def construct_finder_arel_with_includes(options = {}) + 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 = relation.eager_load(include_associations) + else + relation = relation.preload(include_associations) + end + end + + relation + end + def construct_finder_sql(options, scope = scope(:find)) construct_finder_arel(options, scope).to_sql end @@ -1814,9 +1842,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 +1859,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_with_includes(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 @@ -2543,7 +2478,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.arel_table.where(self.class.arel_table[self.class.primary_key].eq(id)).delete end @destroyed = true @@ -2830,7 +2765,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.arel_table.where(self.class.arel_table[self.class.primary_key].eq(id)).update(attributes_with_values) end # Creates a record with values matching those of the instance attributes diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 40242333e5..fcba23dc0d 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -148,7 +148,7 @@ module ActiveRecord else relation = arel_table(options[:from]). joins(construct_join(options[:joins], scope)). - conditions(construct_conditions(options[:conditions], scope)). + where(construct_conditions(options[:conditions], scope)). order(options[:order]). limit(options[:limit]). offset(options[:offset]) 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/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index bbe2d1f205..a6336e762a 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? ? arel_table.where(type_condition) : arel_table + else + construct_finder_arel_with_includes + end + end + end + def scopes read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {}) 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/rails.rb b/activerecord/lib/active_record/rails.rb new file mode 100644 index 0000000000..ddbc555113 --- /dev/null +++ b/activerecord/lib/active_record/rails.rb @@ -0,0 +1,59 @@ +# 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 "action_controller/rails" + +module ActiveRecord + class Plugin < Rails::Plugin + plugin_name :active_record + + 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 + + # 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, result, instrumenter_id, payload| + ActiveRecord::Base.connection.log_info(payload[:sql], name, after - before) + end + end + + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 5f0eec754f..c927270eee 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -4,37 +4,94 @@ module ActiveRecord delegate :length, :collect, :find, :map, :each, :to => :to_a attr_reader :relation, :klass - def initialize(klass, relation) + def initialize(klass, relation, readonly = false, preload = [], eager_load = []) @klass, @relation = klass, relation - @readonly = false - @associations_to_preload = [] - @eager_load_associations = [] + @readonly = readonly + @associations_to_preload = preload + @eager_load_associations = eager_load + @loaded = false end - def preload(association) - @associations_to_preload += association - self + def preload(*associations) + create_new_relation(@relation, @readonly, @associations_to_preload + Array.wrap(associations)) end - def eager_load(association) - @eager_load_associations += association - self + def eager_load(*associations) + create_new_relation(@relation, @readonly, @associations_to_preload, @eager_load_associations + Array.wrap(associations)) end def readonly - @readonly = true - self + create_new_relation(@relation, true) + end + + def select(selects) + create_new_relation(@relation.project(selects)) + end + + def group(groups) + create_new_relation(@relation.group(groups)) + end + + def order(orders) + create_new_relation(@relation.order(orders)) + end + + def limit(limits) + create_new_relation(@relation.take(limits)) + end + + def offset(offsets) + create_new_relation(@relation.skip(offsets)) + end + + def on(join) + create_new_relation(@relation.on(join)) + end + + def joins(join, join_type = nil) + return self 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 + + create_new_relation(join_relation) + end + + def where(*args) + if [String, Hash, Array].include?(args.first.class) + conditions = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first) + else + conditions = args.first + end + + create_new_relation(@relation.where(conditions)) + end + + def respond_to?(method) + @relation.respond_to?(method) || Array.method_defined?(method) || super end def to_a - records = if @eager_load_associations.any? + return @records if loaded? + + @records = if @eager_load_associations.any? catch :invalid_query do return @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 "), + :conditions => where_clause, :limit => @relation.taken, :offset => @relation.skipped }, @@ -45,83 +102,151 @@ module ActiveRecord @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 + @associations_to_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 - end + alias all to_a - def select(selects) - selects.blank? ? self : Relation.new(@klass, @relation.project(selects)) - end + def find(*ids, &block) + return to_a.find(&block) if block_given? - def group(groups) - groups.blank? ? self : Relation.new(@klass, @relation.group(groups)) + 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 order(orders) - orders.blank? ? self : Relation.new(@klass, @relation.order(orders)) + def first + if loaded? + @records.first + else + @first ||= limit(1).to_a[0] + end end - def limit(limits) - limits.blank? ? self : Relation.new(@klass, @relation.take(limits)) + def loaded? + @loaded end - def offset(offsets) - offsets.blank? ? self : Relation.new(@klass, @relation.skip(offsets)) + def reload + @loaded = false + @records = @first = nil + self end - def on(join) - join.blank? ? self : Relation.new(@klass, @relation.on(join)) + 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 + super + end end - def joins(join, join_type = nil) - if join.blank? - self + 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 - 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) + result end end - def conditions(conditions) - if conditions.blank? - self + 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 - conditions = @klass.send(:merge_conditions, conditions) if [String, Hash, Array].include?(conditions.class) - Relation.new(@klass, @relation.where(conditions)) + 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 respond_to?(method) - @relation.respond_to?(method) || Array.method_defined?(method) || super + def find_one(id) + record = where(@klass.primary_key => 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 - 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) + def find_some(ids) + result = where(@klass.primary_key => ids).all + + expected_size = + if @relation.taken && ids.size > @relation.taken + @relation.taken else - super + 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 + + def create_new_relation(relation, readonly = @readonly, preload = @associations_to_preload, eager_load = @eager_load_associations) + Relation.new(@klass, relation, readonly, preload, eager_load) + end + + def where_clause(join_string = "\n\tAND ") + @relation.send(:where_clauses).join(join_string) + 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 fe68d03de2..608d5a3608 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -140,23 +140,23 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase 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.all(:order => 'id').map(&:person_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).people = [people(:michael), people(:david)] - assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.all(:order => 'id').map(&:person_id) + 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.all(:order => 'id').map(&:person_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.all(:order => 'id').map(&:person_id) + assert_equal [people(:michael).id, people(:david).id], posts(:welcome).readers.order('id').map(&:person_id) end def test_associate_with_create 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/base_test.rb b/activerecord/test/cases/base_test.rb index 5c2911eca1..b51c9f0cb3 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -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 @@ -1892,8 +1902,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 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/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..c531a2dec1 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -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 diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 25613da912..307320b964 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' 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/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/relations_test.rb b/activerecord/test/cases/relations_test.rb index 1a2c8030fb..9c5a38a399 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' @@ -10,50 +12,112 @@ require 'models/developer' require 'models/company' 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 + + 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 + + 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') + topics = Topic.order('id') assert_equal 4, topics.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.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.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 +126,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,8 +138,8 @@ 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}" @@ -83,13 +147,14 @@ class RelationTest < ActiveRecord::TestCase 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 @@ -99,52 +164,138 @@ class RelationTest < ActiveRecord::TestCase def test_find_with_included_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_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!('invalid', 'wt') } + 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('invalid') } + 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, 'invalid') } + assert_raises(ActiveRecord::RecordNotFound) { authors.find(['invalid', 'oops']) } + end + +end 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 |