diff options
Diffstat (limited to 'activerecord/lib')
31 files changed, 1313 insertions, 402 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index e1265b7e1e..2f8c5c712f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -46,7 +46,9 @@ module ActiveRecord autoload :AssociationPreload, 'active_record/association_preload' autoload :Associations, 'active_record/associations' autoload :AttributeMethods, 'active_record/attribute_methods' + autoload :AutosaveAssociation, 'active_record/autosave_association' autoload :Base, 'active_record/base' + autoload :Batches, 'active_record/batches' autoload :Calculations, 'active_record/calculations' autoload :Callbacks, 'active_record/callbacks' autoload :Dirty, 'active_record/dirty' @@ -55,6 +57,7 @@ module ActiveRecord autoload :Migration, 'active_record/migration' autoload :Migrator, 'active_record/migration' autoload :NamedScope, 'active_record/named_scope' + autoload :NestedAttributes, 'active_record/nested_attributes' autoload :Observing, 'active_record/observer' autoload :QueryCache, 'active_record/query_cache' autoload :Reflection, 'active_record/reflection' diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 8b51a38f48..6d25b36aea 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -22,7 +22,7 @@ module ActiveRecord through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } - super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '}?") + super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -51,6 +51,12 @@ module ActiveRecord 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.") + end + end + class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: def initialize(reflection) super("Can not eagerly load the polymorphic association #{reflection.name.inspect}") @@ -65,7 +71,7 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods for documentation. module Associations # :nodoc: - # These classes will be loaded when associatoins are created. + # These classes will be loaded when associations are created. # So there is no need to eager load them. autoload :AssociationCollection, 'active_record/associations/association_collection' autoload :AssociationProxy, 'active_record/associations/association_proxy' @@ -88,6 +94,18 @@ module ActiveRecord end unless self.new_record? end + private + # Gets the specified association instance if it responds to :loaded?, nil otherwise. + def association_instance_get(name) + association = instance_variable_get("@#{name}") + association if association.respond_to?(:loaded?) + end + + # Set the specified association instance. + def association_instance_set(name, association) + instance_variable_set("@#{name}", association) + end + # Associations are a set of macro-like class methods for tying objects together through foreign keys. They express relationships like # "Project has one Project Manager" or "Project belongs to a Portfolio". Each macro adds a number of methods to the class which are # specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own <tt>attr*</tt> @@ -121,41 +139,40 @@ module ActiveRecord # | | belongs_to | # generated methods | belongs_to | :polymorphic | has_one # ----------------------------------+------------+--------------+--------- - # #other | X | X | X - # #other=(other) | X | X | X - # #build_other(attributes={}) | X | | X - # #create_other(attributes={}) | X | | X - # #other.create!(attributes={}) | | | X - # #other.nil? | X | X | + # other | X | X | X + # other=(other) | X | X | X + # build_other(attributes={}) | X | | X + # create_other(attributes={}) | X | | X + # other.create!(attributes={}) | | | X # # ===Collection associations (one-to-many / many-to-many) # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- - # #others | X | X | X - # #others=(other,other,...) | X | X | X - # #other_ids | X | X | X - # #other_ids=(id,id,...) | X | X | X - # #others<< | X | X | X - # #others.push | X | X | X - # #others.concat | X | X | X - # #others.build(attributes={}) | X | X | X - # #others.create(attributes={}) | X | X | X - # #others.create!(attributes={}) | X | X | X - # #others.size | X | X | X - # #others.length | X | X | X - # #others.count | X | X | X - # #others.sum(args*,&block) | X | X | X - # #others.empty? | X | X | X - # #others.clear | X | X | X - # #others.delete(other,other,...) | X | X | X - # #others.delete_all | X | X | - # #others.destroy_all | X | X | X - # #others.find(*args) | X | X | X - # #others.find_first | X | | - # #others.exists? | X | X | X - # #others.uniq | X | X | X - # #others.reset | X | X | X + # others | X | X | X + # others=(other,other,...) | X | X | X + # other_ids | X | X | X + # other_ids=(id,id,...) | X | X | X + # others<< | X | X | X + # others.push | X | X | X + # others.concat | X | X | X + # others.build(attributes={}) | X | X | X + # others.create(attributes={}) | X | X | X + # others.create!(attributes={}) | X | X | X + # others.size | X | X | X + # others.length | X | X | X + # others.count | X | X | X + # others.sum(args*,&block) | X | X | X + # others.empty? | X | X | X + # others.clear | X | X | X + # others.delete(other,other,...) | X | X | X + # others.delete_all | X | X | + # others.destroy_all | X | X | X + # others.find(*args) | X | X | X + # others.find_first | X | | + # others.exists? | X | X | X + # others.uniq | X | X | X + # others.reset | X | X | X # # == Cardinality and associations # @@ -256,6 +273,10 @@ module ActiveRecord # You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be # aware of, mostly involving the saving of associated objects. # + # Unless you enable the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>, + # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association, + # in which case the members are always saved. + # # === One-to-one associations # # * Assigning an object to a +has_one+ association automatically saves that object and the object being replaced (if there is one), in @@ -752,6 +773,9 @@ module ActiveRecord # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. true by default. + # [:autosave] + # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default. + # # Option examples: # has_many :comments, :order => "posted_on" # has_many :comments, :include => :author @@ -768,11 +792,7 @@ module ActiveRecord # 'ORDER BY p.first_name' def has_many(association_id, options = {}, &extension) reflection = create_has_many_reflection(association_id, options, &extension) - configure_dependency_for_has_many(reflection) - - add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false - add_multiple_associated_save_callbacks(reflection.name) add_association_callbacks(reflection.name, reflection.options) if options[:through] @@ -794,8 +814,6 @@ module ActiveRecord # [association=(associate)] # Assigns the associate object, extracts the primary key, sets it as the foreign key, # and saves the associate object. - # [association.nil?] - # Returns +true+ if there is no associated object. # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not @@ -814,7 +832,6 @@ module ActiveRecord # An Account class declares <tt>has_one :beneficiary</tt>, which will add: # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.find(:first, :conditions => "account_id = #{id}")</tt>) # * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>) - # * <tt>Account#beneficiary.nil?</tt> # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>) # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) # @@ -857,14 +874,16 @@ module ActiveRecord # [:source] # Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be # inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a - # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. + # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. # [:source_type] # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source - # association is a polymorphic +belongs_to+. + # association is a polymorphic +belongs_to+. # [:readonly] # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated object when saving the parent object. +false+ by default. + # [:autosave] + # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default. # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card @@ -881,25 +900,9 @@ module ActiveRecord association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation) else reflection = create_has_one_reflection(association_id, options) - - ivar = "@#{reflection.name}" - - method_name = "has_one_after_save_for_#{reflection.name}".to_sym - define_method(method_name) do - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) - - if !association.nil? && (new_record? || association.new_record? || association[reflection.primary_key_name] != id) - association[reflection.primary_key_name] = id - association.save(true) - end - end - after_save method_name - - add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true association_accessor_methods(reflection, HasOneAssociation) association_constructor_method(:build, reflection, HasOneAssociation) association_constructor_method(:create, reflection, HasOneAssociation) - configure_dependency_for_has_one(reflection) end end @@ -916,8 +919,6 @@ module ActiveRecord # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, and sets it as the foreign key. - # [association.nil?] - # Returns +true+ if there is no associated object. # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. @@ -935,7 +936,6 @@ module ActiveRecord # * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>) # * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>) # * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>) - # * <tt>Post#author.nil?</tt> # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) # The declaration can also include an options hash to specialize the behavior of the association. @@ -979,6 +979,8 @@ module ActiveRecord # If true, the associated object is readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +false+ by default. + # [:autosave] + # If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default. # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -991,54 +993,17 @@ module ActiveRecord def belongs_to(association_id, options = {}) reflection = create_belongs_to_reflection(association_id, options) - ivar = "@#{reflection.name}" - if reflection.options[:polymorphic] association_accessor_methods(reflection, BelongsToPolymorphicAssociation) - - method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym - define_method(method_name) do - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) - - if association && association.target - if association.new_record? - association.save(true) - end - - if association.updated? - self[reflection.primary_key_name] = association.id - self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s - end - end - end - before_save method_name else association_accessor_methods(reflection, BelongsToAssociation) association_constructor_method(:build, reflection, BelongsToAssociation) association_constructor_method(:create, reflection, BelongsToAssociation) - - method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym - define_method(method_name) do - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) - - if !association.nil? - if association.new_record? - association.save(true) - end - - if association.updated? - self[reflection.primary_key_name] = association.id - end - end - end - before_save method_name end # Create the callbacks to update counter cache if options[:counter_cache] - cache_column = options[:counter_cache] == true ? - "#{self.to_s.demodulize.underscore.pluralize}_count" : - options[:counter_cache] + cache_column = reflection.counter_cache_column method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym define_method(method_name) do @@ -1059,8 +1024,6 @@ module ActiveRecord ) end - add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true - configure_dependency_for_belongs_to(reflection) end @@ -1075,6 +1038,22 @@ module ActiveRecord # but it in fact generates a join table name of "paper_boxes_papers". Be aware of this caveat, and use the # custom <tt>:join_table</tt> option if you need to. # + # The join table should not have a primary key or a model associated with it. You must manually generate the + # join table with a migration such as this: + # + # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration + # def self.up + # create_table :developers_projects, :id => false do |t| + # t.integer :developer_id + # t.integer :project_id + # end + # end + # + # def self.down + # drop_table :developers_projects + # end + # end + # # Deprecated: Any additional fields added to the join table will be placed as attributes when pulling records out through # +has_and_belongs_to_many+ associations. Records returned from join tables with additional attributes will be marked as # readonly (because we can't save changes to the additional attributes). It's strongly recommended that you upgrade any @@ -1151,11 +1130,12 @@ module ActiveRecord # [:foreign_key] # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_and_belongs_to_many+ association - # will use "person_id" as the default <tt>:foreign_key</tt>. + # to Project will use "person_id" as the default <tt>:foreign_key</tt>. # [:association_foreign_key] - # Specify the association foreign key used for the association. By default this is - # guessed to be the name of the associated class in lower-case and "_id" suffixed. So if the associated class is Project, - # the +has_and_belongs_to_many+ association will use "project_id" as the default <tt>:association_foreign_key</tt>. + # Specify the foreign key used for the association on the receiving side of the association. + # By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed. + # So if a Person class makes a +has_and_belongs_to_many+ association to Project, + # the association will use "project_id" as the default <tt>:association_foreign_key</tt>. # [:conditions] # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ # SQL fragment, such as <tt>authorized = 1</tt>. Record creations from the association are scoped if a hash is used. @@ -1196,6 +1176,8 @@ module ActiveRecord # If true, all the associated objects are readonly through the association. # [:validate] # If false, don't validate the associated objects when saving the parent object. +true+ by default. + # [:autosave] + # If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default. # # Option examples: # has_and_belongs_to_many :projects @@ -1207,9 +1189,6 @@ module ActiveRecord # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' def has_and_belongs_to_many(association_id, options = {}, &extension) reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - - add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false - add_multiple_associated_save_callbacks(reflection.name) collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) # Don't use a before_destroy callback since users' before_destroy @@ -1243,33 +1222,30 @@ module ActiveRecord end def association_accessor_methods(reflection, association_proxy_class) - ivar = "@#{reflection.name}" - define_method(reflection.name) do |*params| force_reload = params.first unless params.empty? - - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) + association = association_instance_get(reflection.name) if association.nil? || force_reload association = association_proxy_class.new(self, reflection) retval = association.reload if retval.nil? and association_proxy_class == BelongsToAssociation - instance_variable_set(ivar, nil) + association_instance_set(reflection.name, nil) return nil end - instance_variable_set(ivar, association) + association_instance_set(reflection.name, association) end association.target.nil? ? nil : association end define_method("loaded_#{reflection.name}?") do - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) + association = association_instance_get(reflection.name) association && association.loaded? end define_method("#{reflection.name}=") do |new_value| - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) + association = association_instance_get(reflection.name) if association.nil? || association.target != new_value association = association_proxy_class.new(self, reflection) @@ -1280,7 +1256,7 @@ module ActiveRecord self.send(reflection.name, new_value) else association.replace(new_value) - instance_variable_set(ivar, new_value.nil? ? nil : association) + association_instance_set(reflection.name, new_value.nil? ? nil : association) end end @@ -1288,20 +1264,18 @@ module ActiveRecord return if target.nil? and association_proxy_class == BelongsToAssociation association = association_proxy_class.new(self, reflection) association.target = target - instance_variable_set(ivar, association) + association_instance_set(reflection.name, association) end end def collection_reader_method(reflection, association_proxy_class) define_method(reflection.name) do |*params| - ivar = "@#{reflection.name}" - force_reload = params.first unless params.empty? - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) + association = association_instance_get(reflection.name) - unless association.respond_to?(:loaded?) + unless association association = association_proxy_class.new(self, reflection) - instance_variable_set(ivar, association) + association_instance_set(reflection.name, association) end association.reload if force_reload @@ -1336,86 +1310,15 @@ module ActiveRecord end end - def add_single_associated_validation_callbacks(association_name) - method_name = "validate_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - association = instance_variable_get("@#{association_name}") - if !association.nil? - errors.add association_name unless association.target.nil? || association.valid? - end - end - - validate method_name - end - - def add_multiple_associated_validation_callbacks(association_name) - method_name = "validate_associated_records_for_#{association_name}".to_sym - ivar = "@#{association_name}" - - define_method(method_name) do - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) - - if association.respond_to?(:loaded?) - if new_record? - association - elsif association.loaded? - association.select { |record| record.new_record? } - else - association.target.select { |record| record.new_record? } - end.each do |record| - errors.add association_name unless record.valid? - end - end - end - - validate method_name - end - - def add_multiple_associated_save_callbacks(association_name) - ivar = "@#{association_name}" - - method_name = "before_save_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - @new_record_before_save = new_record? - true - end - before_save method_name - - method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) - - records_to_save = if @new_record_before_save - association - elsif association.respond_to?(:loaded?) && association.loaded? - association.select { |record| record.new_record? } - elsif association.respond_to?(:loaded?) && !association.loaded? - association.target.select { |record| record.new_record? } - else - [] - end - records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank? - - # reconstruct the SQL queries now that we know the owner's id - association.send(:construct_sql) if association.respond_to?(:construct_sql) - end - - # Doesn't use after_save as that would save associations added in after_create/after_update twice - after_create method_name - after_update method_name - end - def association_constructor_method(constructor, reflection, association_proxy_class) define_method("#{constructor}_#{reflection.name}") do |*params| - ivar = "@#{reflection.name}" - attributees = params.first unless params.empty? replace_existing = params[1].nil? ? true : params[1] - association = instance_variable_get(ivar) if instance_variable_defined?(ivar) + association = association_instance_get(reflection.name) - if association.nil? + unless association association = association_proxy_class.new(self, reflection) - instance_variable_set(ivar, association) + association_instance_set(reflection.name, association) end if association_proxy_class == HasOneAssociation @@ -1627,6 +1530,10 @@ module ActiveRecord options[:extend] = create_extension_modules(association_id, extension, options[:extend]) reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) + + if reflection.association_foreign_key == reflection.primary_key_name + raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) + end reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name)) @@ -1949,9 +1856,10 @@ module ActiveRecord def construct(parent, associations, joins, row) case associations when Symbol, String - while (join = joins.shift).reflection.name.to_s != associations.to_s - raise ConfigurationError, "Not Enough Associations" if joins.empty? - end + join = joins.detect{|j| j.reflection.name.to_s == associations.to_s && j.parent_table_name == parent.class.table_name } + raise(ConfigurationError, "No such association") if join.nil? + + joins.delete(join) construct_association(parent, join, row) when Array associations.each do |association| @@ -1959,7 +1867,11 @@ module ActiveRecord end when Hash associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name| - association = construct_association(parent, joins.shift, row) + join = joins.detect{|j| j.reflection.name.to_s == name.to_s && j.parent_table_name == parent.class.table_name } + raise(ConfigurationError, "No such association") if join.nil? + + association = construct_association(parent, join, row) + joins.delete(join) construct(association, associations[name], joins, row) if association end else diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 0fefec1216..3aef1b21e9 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -60,7 +60,7 @@ module ActiveRecord @reflection.klass.find(*args) end end - + # Fetches the first one using SQL if possible. def first(*args) if fetch_first_or_last_using_find?(args) @@ -143,6 +143,8 @@ module ActiveRecord end # Remove all records from this association + # + # See delete for more info. def delete_all load_target delete(@target) @@ -186,7 +188,6 @@ module ActiveRecord end end - # Removes +records+ from this association calling +before_remove+ and # +after_remove+ callbacks. # @@ -195,22 +196,25 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - records = flatten_deeper(records) - records.each { |record| raise_on_type_mismatch(record) } - - transaction do - records.each { |record| callback(:before_remove, record) } - - old_records = records.reject {|r| r.new_record? } + remove_records(records) do |records, old_records| delete_records(old_records) if old_records.any? - - records.each do |record| - @target.delete(record) - callback(:after_remove, record) - end + records.each { |record| @target.delete(record) } end end + # Destroy +records+ and remove them from this association calling + # +before_remove+ and +after_remove+ callbacks. + # + # Note that this method will _always_ remove records from the database + # ignoring the +:dependent+ option. + def destroy(*records) + remove_records(records) do |records, old_records| + old_records.each { |record| record.destroy } + end + + load_target + end + # Removes all records from this association. Returns +self+ so method calls may be chained. def clear return self if length.zero? # forces load_target if it hasn't happened already @@ -223,15 +227,16 @@ module ActiveRecord self end - - def destroy_all - transaction do - each { |record| record.destroy } - end + # Destory all the records from this association. + # + # See destroy for more info. + def destroy_all + load_target + destroy(@target) reset_target! end - + def create(attrs = {}) if attrs.is_a?(Array) attrs.collect { |attr| create(attr) } @@ -431,6 +436,18 @@ module ActiveRecord record end + def remove_records(*records) + records = flatten_deeper(records) + records.each { |record| raise_on_type_mismatch(record) } + + transaction do + records.each { |record| callback(:before_remove, record) } + old_records = records.reject { |r| r.new_record? } + yield(records, old_records) + records.each { |record| callback(:after_remove, record) } + end + end + def callback(method, record) callbacks_for(method).each do |callback| ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record) 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 a5cc3bf091..af9ce3dfb2 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 @@ -28,12 +28,12 @@ module ActiveRecord load_target.size end - def insert_record(record, force=true) + def insert_record(record, force = true, validate = true) if record.new_record? if force record.save! else - return false unless record.save + return false unless record.save(validate) end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 3348079e9d..a2cbabfe0c 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -56,9 +56,9 @@ module ActiveRecord "#{@reflection.name}_count" end - def insert_record(record) + def insert_record(record, force = false, validate = true) set_belongs_to_association_for(record) - record.save + force ? record.save! : record.save(validate) end # Deletes the records according to the <tt>:dependent</tt> option. diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 2eeeb28d1f..1c091e7d5a 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -23,8 +23,8 @@ module ActiveRecord end # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and - # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero - # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length. + # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero, + # and you need to fetch that collection afterwards, it'll take one fewer SELECT query if you use #length. def size return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter? return @target.size if loaded? @@ -47,12 +47,12 @@ module ActiveRecord options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? end - def insert_record(record, force=true) + def insert_record(record, force = true, validate = true) if record.new_record? if force record.save! else - return false unless record.save + return false unless record.save(validate) end end through_reflection = @reflection.through_reflection @@ -150,7 +150,7 @@ module ActiveRecord end else reflection_primary_key = @reflection.source_reflection.primary_key_name - source_primary_key = @reflection.klass.primary_key + source_primary_key = @reflection.through_reflection.klass.primary_key if @reflection.source_reflection.options[:as] polymorphic_join = "AND %s.%s = %s" % [ @reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type", diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 960323004d..b92cbbdeab 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -29,8 +29,17 @@ module ActiveRecord unless @target.nil? || @target == obj if dependent? && !dont_save - @target.destroy unless @target.new_record? - @owner.clear_association_cache + case @reflection.options[:dependent] + when :delete + @target.delete unless @target.new_record? + @owner.clear_association_cache + when :destroy + @target.destroy unless @target.new_record? + @owner.clear_association_cache + when :nullify + @target[@reflection.primary_key_name] = nil + @target.save unless @owner.new_record? || @target.new_record? + end else @target[@reflection.primary_key_name] = nil @target.save unless @owner.new_record? || @target.new_record? diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 177d156834..3ffc48941c 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -324,6 +324,7 @@ module ActiveRecord if Numeric === value || value !~ /[^0-9]/ !value.to_i.zero? else + return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value) !value.blank? end elsif column.number? diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb new file mode 100644 index 0000000000..741aa2acbe --- /dev/null +++ b/activerecord/lib/active_record/autosave_association.rb @@ -0,0 +1,349 @@ +module ActiveRecord + # AutosaveAssociation is a module that takes care of automatically saving + # your associations when the parent is saved. In addition to saving, it + # also destroys any associations that were marked for destruction. + # (See mark_for_destruction and marked_for_destruction?) + # + # Saving of the parent, its associations, and the destruction of marked + # associations, all happen inside 1 transaction. This should never leave the + # database in an inconsistent state after, for instance, mass assigning + # attributes and saving them. + # + # If validations for any of the associations fail, their error messages will + # be applied to the parent. + # + # Note that it also means that associations marked for destruction won't + # be destroyed directly. They will however still be marked for destruction. + # + # === One-to-one Example + # + # Consider a Post model with one Author: + # + # class Post + # has_one :author, :autosave => true + # end + # + # Saving changes to the parent and its associated model can now be performed + # automatically _and_ atomically: + # + # post = Post.find(1) + # post.title # => "The current global position of migrating ducks" + # post.author.name # => "alloy" + # + # post.title = "On the migration of ducks" + # post.author.name = "Eloy Duran" + # + # post.save + # post.reload + # post.title # => "On the migration of ducks" + # post.author.name # => "Eloy Duran" + # + # Destroying an associated model, as part of the parent's save action, is as + # simple as marking it for destruction: + # + # post.author.mark_for_destruction + # post.author.marked_for_destruction? # => true + # + # Note that the model is _not_ yet removed from the database: + # id = post.author.id + # Author.find_by_id(id).nil? # => false + # + # post.save + # post.reload.author # => nil + # + # Now it _is_ removed from the database: + # Author.find_by_id(id).nil? # => true + # + # === One-to-many Example + # + # Consider a Post model with many Comments: + # + # class Post + # has_many :comments, :autosave => true + # end + # + # Saving changes to the parent and its associated model can now be performed + # automatically _and_ atomically: + # + # post = Post.find(1) + # post.title # => "The current global position of migrating ducks" + # post.comments.first.body # => "Wow, awesome info thanks!" + # post.comments.last.body # => "Actually, your article should be named differently." + # + # post.title = "On the migration of ducks" + # post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks." + # + # post.save + # post.reload + # post.title # => "On the migration of ducks" + # post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks." + # + # Destroying one of the associated models members, as part of the parent's + # save action, is as simple as marking it for destruction: + # + # post.comments.last.mark_for_destruction + # post.comments.last.marked_for_destruction? # => true + # post.comments.length # => 2 + # + # Note that the model is _not_ yet removed from the database: + # id = post.comments.last.id + # Comment.find_by_id(id).nil? # => false + # + # post.save + # post.reload.comments.length # => 1 + # + # Now it _is_ removed from the database: + # Comment.find_by_id(id).nil? # => true + # + # === Validation + # + # Validation is performed on the parent as usual, but also on all autosave + # enabled associations. If any of the associations fail validation, its + # error messages will be applied on the parents errors object and validation + # of the parent will fail. + # + # Consider a Post model with Author which validates the presence of its name + # attribute: + # + # class Post + # has_one :author, :autosave => true + # end + # + # class Author + # validates_presence_of :name + # end + # + # post = Post.find(1) + # post.author.name = '' + # post.save # => false + # post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>> + # + # No validations will be performed on the associated models when validations + # are skipped for the parent: + # + # post = Post.find(1) + # post.author.name = '' + # post.save(false) # => true + module AutosaveAssociation + ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many } + + def self.included(base) + base.class_eval do + base.extend(ClassMethods) + alias_method_chain :reload, :autosave_associations + + ASSOCIATION_TYPES.each do |type| + base.send("valid_keys_for_#{type}_association") << :autosave + end + end + end + + module ClassMethods + private + + # def belongs_to(name, options = {}) + # super + # add_autosave_association_callbacks(reflect_on_association(name)) + # end + ASSOCIATION_TYPES.each do |type| + module_eval %{ + def #{type}(name, options = {}) + super + add_autosave_association_callbacks(reflect_on_association(name)) + end + } + end + + # Adds a validate and save callback for the association as specified by + # the +reflection+. + def add_autosave_association_callbacks(reflection) + save_method = "autosave_associated_records_for_#{reflection.name}" + validation_method = "validate_associated_records_for_#{reflection.name}" + validate validation_method + + case reflection.macro + when :has_many, :has_and_belongs_to_many + 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(validation_method) { validate_collection_association(reflection) } + 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 + end + define_method(validation_method) { validate_single_association(reflection) } + end + end + end + + # Reloads the attributes of the object as usual and removes a mark for destruction. + def reload_with_autosave_associations(options = nil) + @marked_for_destruction = false + reload_without_autosave_associations(options) + end + + # Marks this record to be destroyed as part of the parents save transaction. + # This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called. + # + # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model. + def mark_for_destruction + @marked_for_destruction = true + end + + # Returns whether or not this record will be destroyed as part of the parents save transaction. + # + # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model. + def marked_for_destruction? + @marked_for_destruction + end + + private + + # Returns the record for an association collection that should be validated + # or saved. If +autosave+ is +false+ only new records will be returned, + # unless the parent is/was a new record itself. + def associated_records_to_validate_or_save(association, new_record, autosave) + if new_record + association + elsif association.loaded? + autosave ? association : association.select { |record| record.new_record? } + else + autosave ? association.target : association.target.select { |record| record.new_record? } + end + end + + # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is + # turned on for the association specified by +reflection+. + def validate_single_association(reflection) + if reflection.options[:validate] == true || reflection.options[:autosave] == true + if (association = association_instance_get(reflection.name)) && !association.target.nil? + association_valid?(reflection, association) + end + end + end + + # Validate the associated records if <tt>:validate</tt> or + # <tt>:autosave</tt> is turned on for the association specified by + # +reflection+. + def validate_collection_association(reflection) + if reflection.options[:validate] != false && association = association_instance_get(reflection.name) + if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) + records.each { |record| association_valid?(reflection, record) } + end + end + end + + # Returns whether or not the association is valid and applies any errors to + # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> + # enabled records if they're marked_for_destruction?. + def association_valid?(reflection, association) + unless valid = association.valid? + if reflection.options[:autosave] + unless association.marked_for_destruction? + association.errors.each do |attribute, message| + attribute = "#{reflection.name}_#{attribute}" + errors.add(attribute, message) unless errors.on(attribute) + end + end + else + errors.add(reflection.name) + end + end + valid + end + + # Is used as a before_save callback to check while saving a collection + # association whether or not the parent was a new record before saving. + def before_save_collection_association + @new_record_before_save = new_record? + true + end + + # Saves any new associated records, or all loaded autosave associations if + # <tt>:autosave</tt> is enabled on the association. + # + # In addition, it destroys all children that were marked for destruction + # with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_collection_association(reflection) + if association = association_instance_get(reflection.name) + autosave = reflection.options[:autosave] + + if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) + records.each do |record| + if autosave && record.marked_for_destruction? + association.destroy(record) + elsif @new_record_before_save || record.new_record? + if autosave + association.send(:insert_record, record, false, false) + else + association.send(:insert_record, record) + end + elsif autosave + record.save(false) + end + end + end + + # reconstruct the SQL queries now that we know the owner's id + association.send(:construct_sql) if association.respond_to?(:construct_sql) + end + end + + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled + # on the association. + # + # In addition, it will destroy the association if it was marked for + # destruction with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_has_one_association(reflection) + if (association = association_instance_get(reflection.name)) && !association.target.nil? + if reflection.options[:autosave] && association.marked_for_destruction? + association.destroy + elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || reflection.options[:autosave] + association[reflection.primary_key_name] = id + association.save(false) + end + end + end + + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled + # on the association. + # + # In addition, it will destroy the association if it was marked for + # destruction with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_belongs_to_association(reflection) + if association = association_instance_get(reflection.name) + if reflection.options[:autosave] && association.marked_for_destruction? + association.destroy + else + association.save(false) if association.new_record? || reflection.options[:autosave] + + if association.updated? + self[reflection.primary_key_name] = association.id + # TODO: Removing this code doesn't seem to matter… + if reflection.options[:polymorphic] + self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s + end + end + end + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 0efccb66ee..9943a7014a 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -416,7 +416,7 @@ module ActiveRecord #:nodoc: end @@subclasses = {} - + ## # :singleton-method: # Contains the database configuration - as is typically stored in config/database.yml - @@ -661,9 +661,8 @@ module ActiveRecord #:nodoc: connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } end - # Returns true if a record exists in the table that matches the +id+ or - # conditions given, or false otherwise. The argument can take four forms: + # conditions given, or false otherwise. The argument can take five forms: # # * Integer - Finds the record with this primary key. # * String - Finds the record with a primary key corresponding to this @@ -672,6 +671,7 @@ module ActiveRecord #:nodoc: # (such as <tt>['color = ?', 'red']</tt>). # * Hash - Finds the record that matches these +find+-style conditions # (such as <tt>{:color => 'red'}</tt>). + # * No args - Returns false if the table is empty, true otherwise. # # For more information about specifying conditions as a Hash or Array, # see the Conditions section in the introduction to ActiveRecord::Base. @@ -685,7 +685,8 @@ module ActiveRecord #:nodoc: # Person.exists?('5') # Person.exists?(:name => "David") # Person.exists?(['name LIKE ?', "%#{query}%"]) - def exists?(id_or_conditions) + # Person.exists? + def exists?(id_or_conditions = {}) connection.select_all( construct_finder_sql( :select => "#{quoted_table_name}.#{primary_key}", @@ -735,12 +736,12 @@ module ActiveRecord #:nodoc: # ==== Parameters # # * +id+ - This should be the id or an array of ids to be updated. - # * +attributes+ - This should be a Hash of attributes to be set on the object, or an array of Hashes. + # * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes. # # ==== Examples # # # Updating one record: - # Person.update(15, { :user_name => 'Samuel', :group => 'expert' }) + # Person.update(15, :user_name => 'Samuel', :group => 'expert') # # # Updating multiple records: # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } @@ -809,25 +810,28 @@ module ActiveRecord #:nodoc: # Updates all records with details given if they match a set of conditions supplied, limits and order can # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the - # database. It does not instantiate the involved models and it does not trigger Active Record callbacks. + # database. It does not instantiate the involved models and it does not trigger Active Record callbacks + # or validations. # # ==== Parameters # - # * +updates+ - A string of column and value pairs that will be set on any records that match conditions. This creates the SET clause of the generated SQL. - # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info. + # * +updates+ - A string, array, or hash representing the SET part of an SQL statement. + # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. See conditions in the intro. # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage. # # ==== Examples # - # # Update all billing objects with the 3 different attributes given - # Billing.update_all( "category = 'authorized', approved = 1, author = 'David'" ) + # # Update all customers with the given attributes + # Customer.update_all :wants_email => true # - # # Update records that match our conditions - # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'" ) + # # Update all books with 'Rails' in their title + # Book.update_all "author = 'David'", "title LIKE '%Rails%'" # - # # Update records that match our conditions but limit it to 5 ordered by date - # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'", - # :order => 'created_at', :limit => 5 ) + # # Update all avatars migrated more than a week ago + # Avatar.update_all ['migrated_at = ?, Time.now.utc], ['migrated_at > ?', 1.week.ago] + # + # # Update all books that match our conditions, but limit it to 5 ordered by date + # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5 def update_all(updates, conditions = nil, options = {}) sql = "UPDATE #{quoted_table_name} SET #{sanitize_sql_for_assignment(updates)} " @@ -884,7 +888,8 @@ module ActiveRecord #:nodoc: # Deletes the records matching +conditions+ without instantiating the records first, and hence not # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations - # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. + # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns + # the number of rows affected. # # ==== Parameters # @@ -1000,7 +1005,6 @@ module ActiveRecord #:nodoc: update_counters(id, counter_name => -1) end - # Attributes named in this macro are protected from mass-assignment, # such as <tt>new(attributes)</tt>, # <tt>update_attributes(attributes)</tt>, or @@ -1101,7 +1105,6 @@ module ActiveRecord #:nodoc: read_inheritable_attribute(:attr_serialized) or write_inheritable_attribute(:attr_serialized, {}) end - # Guesses the table name (in forced lower-case) based on the name of the class in the inheritance hierarchy descending # directly from ActiveRecord::Base. So if the hierarchy looks like: Reply < Message < ActiveRecord::Base, then Message is used # to guess the table name even when called on Reply. The rules used to do the guess are handled by the Inflector class @@ -1344,7 +1347,7 @@ module ActiveRecord #:nodoc: subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } end - def self_and_descendents_from_active_record#nodoc: + def self_and_descendants_from_active_record#nodoc: klass = self classes = [klass] while klass != klass.base_class @@ -1364,7 +1367,7 @@ module ActiveRecord #:nodoc: # module now. # Specify +options+ with additional translating options. def human_attribute_name(attribute_key_name, options = {}) - defaults = self_and_descendents_from_active_record.map do |klass| + defaults = self_and_descendants_from_active_record.map do |klass| :"#{klass.name.underscore}.#{attribute_key_name}" end defaults << options[:default] if options[:default] @@ -1379,7 +1382,7 @@ module ActiveRecord #:nodoc: # Default scope of the translation is activerecord.models # Specify +options+ with additional translating options. def human_name(options = {}) - defaults = self_and_descendents_from_active_record.map do |klass| + defaults = self_and_descendants_from_active_record.map do |klass| :"#{klass.name.underscore}" end defaults << self.name.humanize @@ -1414,7 +1417,6 @@ module ActiveRecord #:nodoc: end end - def quote_value(value, column = nil) #:nodoc: connection.quote(value,column) end @@ -1483,7 +1485,7 @@ module ActiveRecord #:nodoc: elsif match = DynamicScopeMatch.match(method_id) return true if all_attributes_exists?(match.attribute_names) end - + super end @@ -1534,7 +1536,7 @@ module ActiveRecord #:nodoc: end def reverse_sql_order(order_query) - reversed_query = order_query.split(/,/).each { |s| + reversed_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)$/) @@ -1687,7 +1689,7 @@ module ActiveRecord #:nodoc: def construct_finder_sql(options) scope = scope(:find) sql = "SELECT #{options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))} " - sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} " + sql << "FROM #{options[:from] || (scope && scope[:from]) || quoted_table_name} " add_joins!(sql, options[:joins], scope) add_conditions!(sql, options[:conditions], scope) @@ -1742,7 +1744,9 @@ module ActiveRecord #:nodoc: scoped_order = scope[:order] if scope if order sql << " ORDER BY #{order}" - sql << ", #{scoped_order}" if scoped_order + if scoped_order && scoped_order != order + sql << ", #{scoped_order}" + end else sql << " ORDER BY #{scoped_order}" if scoped_order end @@ -1751,12 +1755,12 @@ module ActiveRecord #:nodoc: def add_group!(sql, group, having, scope = :auto) if group sql << " GROUP BY #{group}" - sql << " HAVING #{having}" if having + sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having else scope = scope(:find) if :auto == scope if scope && (scoped_group = scope[:group]) sql << " GROUP BY #{scoped_group}" - sql << " HAVING #{scoped_having}" if (scoped_having = scope[:having]) + sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having] end end end @@ -1990,12 +1994,16 @@ module ActiveRecord #:nodoc: attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) } end - def attribute_condition(argument) + def attribute_condition(quoted_column_name, argument) case argument - when nil then "IS ?" - when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)" - when Range then "BETWEEN ? AND ?" - else "= ?" + when nil then "#{quoted_column_name} IS ?" + when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "#{quoted_column_name} IN (?)" + when Range then if argument.exclude_end? + "#{quoted_column_name} >= ? AND #{quoted_column_name} < ?" + else + "#{quoted_column_name} BETWEEN ? AND ?" + end + else "#{quoted_column_name} = ?" end end @@ -2007,7 +2015,6 @@ module ActiveRecord #:nodoc: end end - # Defines an "attribute" method (like +inheritance_column+ or # +table_name+). A new (class) method will be created with the # given name. If a value is specified, the new method will @@ -2104,7 +2111,7 @@ module ActiveRecord #:nodoc: end # Merge scopings - if action == :merge && current_scoped_methods + if [:merge, :reverse_merge].include?(action) && current_scoped_methods method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)| case hash[method] when Hash @@ -2126,7 +2133,11 @@ module ActiveRecord #:nodoc: end end else - hash[method] = hash[method].merge(params) + if action == :reverse_merge + hash[method] = hash[method].merge(params) + else + hash[method] = params.merge(hash[method]) + end end else hash[method] = params @@ -2136,7 +2147,6 @@ module ActiveRecord #:nodoc: end self.scoped_methods << method_scoping - begin yield ensure @@ -2167,7 +2177,7 @@ module ActiveRecord #:nodoc: # Test whether the given method and optional key are scoped. def scoped?(method, key = nil) #:nodoc: if current_scoped_methods && (scope = current_scoped_methods[method]) - !key || scope.has_key?(key) + !key || !scope[key].nil? end end @@ -2305,7 +2315,7 @@ module ActiveRecord #:nodoc: table_name = connection.quote_table_name(table_name) end - "#{table_name}.#{connection.quote_column_name(attr)} #{attribute_condition(value)}" + attribute_condition("#{table_name}.#{connection.quote_column_name(attr)}", value) else sanitize_sql_hash_for_conditions(value, connection.quote_table_name(attr.to_s)) end @@ -2742,7 +2752,6 @@ module ActiveRecord #:nodoc: assign_multiparameter_attributes(multi_parameter_attributes) end - # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. def attributes self.attribute_names.inject({}) do |attrs, name| @@ -3136,7 +3145,12 @@ module ActiveRecord #:nodoc: include Dirty include Callbacks, Observing, Timestamp include Associations, AssociationPreload, NamedScope - include Aggregations, Transactions, Reflection, Calculations, Serialization + + # AutosaveAssociation needs to be included before Transactions, because we want + # #save_with_autosave_associations to be wrapped inside a transaction. + include AutosaveAssociation, NestedAttributes + + include Aggregations, Transactions, Reflection, Batches, Calculations, Serialization end end diff --git a/activerecord/lib/active_record/batches.rb b/activerecord/lib/active_record/batches.rb new file mode 100644 index 0000000000..5a6cecd4ad --- /dev/null +++ b/activerecord/lib/active_record/batches.rb @@ -0,0 +1,81 @@ +module ActiveRecord + module Batches # :nodoc: + def self.included(base) + base.extend(ClassMethods) + end + + # When processing large numbers of records, it's often a good idea to do + # so in batches to prevent memory ballooning. + module ClassMethods + # Yields each record that was found by the find +options+. The find is + # performed by find_in_batches with a batch size of 1000 (or as + # specified by the <tt>:batch_size</tt> option). + # + # Example: + # + # Person.find_each(:conditions => "age > 21") do |person| + # person.party_all_night! + # end + # + # Note: This method is only intended to use for batch processing of + # large amounts of records that wouldn't fit in memory all at once. If + # you just need to loop over less than 1000 records, it's probably + # better just to use the regular find methods. + def find_each(options = {}) + find_in_batches(options) do |records| + records.each { |record| yield record } + end + + self + end + + # Yields each batch of records that was found by the find +options+ as + # an array. The size of each batch is set by the <tt>:batch_size</tt> + # option; the default is 1000. + # + # You can control the starting point for the batch processing by + # supplying the <tt>:start</tt> option. This is especially useful if you + # want multiple workers dealing with the same processing queue. You can + # make worker 1 handle all the records between id 0 and 10,000 and + # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt> + # option on that worker). + # + # It's not possible to set the order. That is automatically set to + # ascending on the primary key ("id ASC") to make the batch ordering + # work. This also mean that this method only works with integer-based + # primary keys. You can't set the limit either, that's used to control + # the the batch sizes. + # + # Example: + # + # Person.find_in_batches(:conditions => "age > 21") do |group| + # sleep(50) # Make sure it doesn't get too crowded in there! + # group.each { |person| person.party_all_night! } + # end + def find_in_batches(options = {}) + raise "You can't specify an order, it's forced to be #{batch_order}" if options[:order] + raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit] + + start = options.delete(:start).to_i + batch_size = options.delete(:batch_size) || 1000 + + with_scope(:find => options.merge(:order => batch_order, :limit => batch_size)) do + records = find(:all, :conditions => [ "#{table_name}.#{primary_key} >= ?", start ]) + + while records.any? + yield records + + break if records.size < batch_size + records = find(:all, :conditions => [ "#{table_name}.#{primary_key} > ?", records.last.id ]) + end + end + end + + + private + def batch_order + "#{table_name}.#{primary_key} ASC" + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index b239c03284..f077818d3b 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -141,22 +141,30 @@ module ActiveRecord 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 - args[0].is_a?(Hash) ? options = args[0] : column_name = args[0] + 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}" - end if args.size > 0 - - [column_name, options] + end + + [column_name || :all, options] end def construct_calculation_sql(operation, column_name, options) #:nodoc: @@ -214,13 +222,15 @@ module ActiveRecord end if options[:group] && options[:having] + having = sanitize_sql_for_conditions(options[:having]) + # FrontBase requires identifiers in the HAVING clause and chokes on function calls if connection.adapter_name == 'FrontBase' - options[:having].downcase! - options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias) + having.downcase! + having.gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias) end - sql << " HAVING #{options[:having]} " + sql << " HAVING #{having} " end sql << " ORDER BY #{options[:order]} " if options[:order] diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 88958f4583..e375037b5b 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -104,6 +104,37 @@ module ActiveRecord # The callback objects have methods named after the callback called with the record as the only parameter, such as: # # class BankAccount < ActiveRecord::Base + # before_save EncryptionWrapper.new + # after_save EncryptionWrapper.new + # after_initialize EncryptionWrapper.new + # end + # + # class EncryptionWrapper + # def before_save(record) + # record.credit_card_number = encrypt(record.credit_card_number) + # end + # + # def after_save(record) + # record.credit_card_number = decrypt(record.credit_card_number) + # end + # + # alias_method :after_find, :after_save + # + # private + # def encrypt(value) + # # Secrecy is committed + # end + # + # def decrypt(value) + # # Secrecy is unveiled + # end + # end + # + # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has + # a method by the name of the callback messaged. You can make these callbacks more flexible by passing in other + # initialization data such as the name of the attribute to work with: + # + # class BankAccount < ActiveRecord::Base # before_save EncryptionWrapper.new("credit_card_number") # after_save EncryptionWrapper.new("credit_card_number") # after_initialize EncryptionWrapper.new("credit_card_number") @@ -115,11 +146,11 @@ module ActiveRecord # end # # def before_save(record) - # record.credit_card_number = encrypt(record.credit_card_number) + # record.send("#{@attribute}=", encrypt(record.send("#{@attribute}"))) # end # # def after_save(record) - # record.credit_card_number = decrypt(record.credit_card_number) + # record.send("#{@attribute}=", decrypt(record.send("#{@attribute}"))) # end # # alias_method :after_find, :after_save @@ -134,9 +165,6 @@ module ActiveRecord # end # end # - # So you specify the object you want messaged on a given callback. When that callback is triggered, the object has - # a method by the name of the callback messaged. - # # The callback macros usually accept a symbol for the method they're supposed to run, but you can also pass a "method string", # which will then be evaluated within the binding of the callback. Example: # diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 901b17124c..aac84cc5f4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -351,5 +351,21 @@ module ActiveRecord retrieve_connection_pool klass.superclass end end + + class ConnectionManagement + def initialize(app) + @app = app + end + + def call(env) + @app.call(env) + ensure + # Don't return connection (and peform implicit rollback) if + # this request is a part of integration test + unless env.key?("rack.test") + ActiveRecord::Base.clear_active_connections! + end + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 273f823e7f..24c734cddb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -8,6 +8,7 @@ module ActiveRecord # An abstract definition of a column in a table. class Column TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set + FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set module Format ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/ diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 913bb521ca..ec204d0f03 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -392,9 +392,28 @@ module ActiveRecord quote_string(s) end + # Checks the following cases: + # + # - table_name + # - "table.name" + # - schema_name.table_name + # - schema_name."table.name" + # - "schema.name".table_name + # - "schema.name"."table.name" + def quote_table_name(name) + schema, name_part = extract_pg_identifier_from_name(name.to_s) + + unless name_part + quote_column_name(schema) + else + table_name, name_part = extract_pg_identifier_from_name(name_part) + "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" + end + end + # Quotes column names for use in SQL queries. def quote_column_name(name) #:nodoc: - %("#{name}") + PGconn.quote_ident(name.to_s) end # Quote date/time values for use in SQL input. Includes microseconds @@ -1045,6 +1064,16 @@ module ActiveRecord ORDER BY a.attnum end_sql end + + def extract_pg_identifier_from_name(name) + match_data = name[0,1] == '"' ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/) + + if match_data + rest = name[match_data[0].length..-1] + rest = rest[1..-1] if rest[0,1] == "." + [match_data[1], (rest.length > 0 ? rest : nil)] + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index cc9c46505f..75420f69aa 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -18,7 +18,7 @@ module ActiveRecord db.busy_timeout(config[:timeout]) unless config[:timeout].nil? - ConnectionAdapters::SQLite3Adapter.new(db, logger) + ConnectionAdapters::SQLite3Adapter.new(db, logger, config) end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 5390f49f04..afd6472db8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -17,9 +17,9 @@ module ActiveRecord # "Downgrade" deprecated sqlite API if SQLite.const_defined?(:Version) - ConnectionAdapters::SQLite2Adapter.new(db, logger) + ConnectionAdapters::SQLite2Adapter.new(db, logger, config) else - ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger) + ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config) end end end @@ -72,10 +72,31 @@ module ActiveRecord # # * <tt>:database</tt> - Path to the database file. class SQLiteAdapter < AbstractAdapter + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map(&:to_i) + end + + def <=>(version_string) + @version <=> version_string.split('.').map(&:to_i) + end + end + + def initialize(connection, logger, config) + super(connection, logger) + @config = config + end + def adapter_name #:nodoc: 'SQLite' end + def supports_ddl_transactions? + sqlite_version >= '2.0.0' + end + def supports_migrations? #:nodoc: true end @@ -83,6 +104,10 @@ module ActiveRecord def requires_reloading? true end + + def supports_add_column? + sqlite_version >= '3.1.6' + end def disconnect! super @@ -164,7 +189,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 @@ -213,14 +237,20 @@ module ActiveRecord execute "ALTER TABLE #{name} RENAME TO #{new_name}" end + # See: http://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def valid_alter_table_options( type, options) + type.to_sym != :primary_key + end + def add_column(table_name, column_name, type, options = {}) #:nodoc: - if @connection.respond_to?(:transaction_active?) && @connection.transaction_active? - raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction' + if supports_add_column? && valid_alter_table_options( type, options ) + super(table_name, column_name, type, options) + else + alter_table(table_name) do |definition| + definition.column(column_name, type, options) + end end - - super(table_name, column_name, type, options) - # See last paragraph on http://www.sqlite.org/lang_altertable.html - execute "VACUUM" end def remove_column(table_name, *column_names) #:nodoc: @@ -380,7 +410,7 @@ module ActiveRecord end def sqlite_version - @sqlite_version ||= select_value('select sqlite_version(*)') + @sqlite_version ||= SQLiteAdapter::Version.new(select_value('select sqlite_version(*)')) end def default_primary_key_type @@ -393,23 +423,9 @@ module ActiveRecord end class SQLite2Adapter < SQLiteAdapter # :nodoc: - def supports_count_distinct? #:nodoc: - false - end - def rename_table(name, new_name) move_table(name, new_name) end - - def add_column(table_name, column_name, type, options = {}) #:nodoc: - if @connection.respond_to?(:transaction_active?) && @connection.transaction_active? - raise StatementInvalid, 'Cannot add columns to a SQLite database while inside a transaction' - end - - alter_table(table_name) do |definition| - definition.column(column_name, type, options) - end - end end class DeprecatedSQLiteAdapter < SQLite2Adapter # :nodoc: diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 0131d9fac5..c6501113bf 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -21,13 +21,17 @@ else end end -# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors: +# Fixtures are a way of organizing data that you want to test against; in short, sample data. +# +# = Fixture formats +# +# Fixtures come in 3 flavors: # # 1. YAML fixtures # 2. CSV fixtures # 3. Single-file fixtures # -# = YAML fixtures +# == YAML fixtures # # This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures # in a non-verbose, human-readable format. It ships with Ruby 1.8.1+. @@ -65,9 +69,9 @@ end # parent_id: 1 # title: Child # -# = CSV fixtures +# == CSV fixtures # -# Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored +# Fixtures can also be kept in the Comma Separated Value (CSV) format. Akin to YAML fixtures, CSV fixtures are stored # in a single file, but instead end with the <tt>.csv</tt> file extension # (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>). # @@ -90,7 +94,7 @@ end # Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you # have existing data somewhere already. # -# = Single-file fixtures +# == Single-file fixtures # # This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats. # Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory @@ -113,65 +117,53 @@ end # name => Ruby on Rails # url => http://www.rubyonrails.org # -# = Using Fixtures +# = Using fixtures in testcases # # Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the # fixtures, but first let's take a look at a sample unit test: # -# require 'web_site' +# require 'test_helper' # # class WebSiteTest < ActiveSupport::TestCase -# def test_web_site_count +# test "web_site_count" do # assert_equal 2, WebSite.count # end # end # -# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail. Here's the -# easiest way to add fixtures to the database: -# -# ... -# class WebSiteTest < ActiveSupport::TestCase -# fixtures :web_sites # add more by separating the symbols with commas -# ... -# -# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger -# the testing environment to automatically load the appropriate fixtures into the database before each test. +# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, so this test will succeed. +# The testing environment will automatically load the all fixtures into the database before each test. # To ensure consistent data, the environment deletes the fixtures before running the load. # -# In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable -# of the test case. It is named after the symbol... so, in our example, there would be a hash available called -# <tt>@web_sites</tt>. This is where the "fixture name" comes into play. -# -# On top of that, each record is automatically "found" (using <tt>Model.find(id)</tt>) and placed in the instance variable of its name. -# So for the YAML fixtures, we'd get <tt>@rubyonrails</tt> and <tt>@google</tt>, which could be interrogated using regular Active Record semantics: +# In addition to being available in the database, the fixture's data may also be accessed by +# using a special dynamic method, which has the same name as the model, and accepts the +# name of the fixture to instantiate: # -# # test if the object created from the fixture data has the same attributes as the data itself -# def test_find -# assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name +# test "find" do +# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name # end # -# As seen above, the data hash created from the YAML fixtures would have <tt>@web_sites["rubyonrails"]["url"]</tt> return -# "http://www.rubyonrails.org" and <tt>@web_sites["google"]["name"]</tt> would return "Google". The same fixtures, but loaded -# from a CSV fixture file, would be accessible via <tt>@web_sites["web_site_1"]["name"] == "Ruby on Rails"</tt> and have the individual -# fixtures available as instance variables <tt>@web_site_1</tt> and <tt>@web_site_2</tt>. +# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the following tests: # -# If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options. +# test "find_alt_method_1" do +# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name'] +# end # -# - to completely disable instantiated fixtures: -# self.use_instantiated_fixtures = false +# test "find_alt_method_2" do +# assert_equal "Ruby on Rails", @rubyonrails.news +# end # -# - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance: -# self.use_instantiated_fixtures = :no_instances +# In order to use these methods to access fixtured data within your testcases, you must specify one of the +# following in your <tt>ActiveSupport::TestCase</tt>-derived class: # -# Even if auto-instantiated fixtures are disabled, you can still access them -# by name via special dynamic methods. Each method has the same name as the -# model, and accepts the name of the fixture to instantiate: +# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) +# self.use_instantiated_fixtures = true # -# fixtures :web_sites +# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only) +# self.use_instantiated_fixtures = :no_instances # -# def test_find -# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name -# end +# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully +# traversed in the database to create the fixture hash and/or instance variables. This is expensive for +# large sets of fixtured data. # # = Dynamic fixtures with ERb # @@ -194,21 +186,17 @@ end # = Transactional fixtures # # TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case. -# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused. # # class FooTest < ActiveSupport::TestCase # self.use_transactional_fixtures = true -# self.use_instantiated_fixtures = false -# -# fixtures :foos # -# def test_godzilla +# test "godzilla" do # assert !Foo.find(:all).empty? # Foo.destroy_all # assert Foo.find(:all).empty? # end # -# def test_godzilla_aftermath +# test "godzilla aftermath" do # assert !Foo.find(:all).empty? # end # end @@ -220,24 +208,25 @@ end # access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+) # # When *not* to use transactional fixtures: -# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit, -# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify -# the results of your transaction until Active Record supports nested transactions or savepoints (in progress). -# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. -# Use InnoDB, MaxDB, or NDB instead. +# +# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit, +# particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify +# the results of your transaction until Active Record supports nested transactions or savepoints (in progress). +# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. +# Use InnoDB, MaxDB, or NDB instead. # # = Advanced YAML Fixtures # # YAML fixtures that don't specify an ID get some extra features: # -# * Stable, autogenerated ID's +# * Stable, autogenerated IDs # * Label references for associations (belongs_to, has_one, has_many) # * HABTM associations as inline lists # * Autofilled timestamp columns # * Fixture label interpolation # * Support for YAML defaults # -# == Stable, autogenerated ID's +# == Stable, autogenerated IDs # # Here, have a monkey fixture: # @@ -292,7 +281,7 @@ end # # Add a few more monkeys and pirates and break this into multiple files, # and it gets pretty hard to keep track of what's going on. Let's -# use labels instead of ID's: +# use labels instead of IDs: # # ### in pirates.yml # diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index ff9899d032..7fa7e267d8 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -23,6 +23,16 @@ 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. # @@ -39,6 +49,7 @@ module ActiveRecord base.lock_optimistically = true base.alias_method_chain :update, :lock + base.alias_method_chain :destroy, :lock base.alias_method_chain :attributes_from_column_definition, :lock class << base @@ -98,6 +109,28 @@ 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 + + affected_rows = connection.delete( + "DELETE FROM #{self.class.quoted_table_name} " + + "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " + + "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}", + "#{self.class.name} Destroy" + ) + + 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/migration.rb b/activerecord/lib/active_record/migration.rb index 15350cf1e1..657acd6dc0 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -338,6 +338,10 @@ module ActiveRecord self.verbose = save end + def connection + ActiveRecord::Base.connection + end + def method_missing(method, *arguments, &block) arg_list = arguments.map(&:inspect) * ', ' @@ -345,7 +349,7 @@ module ActiveRecord unless arguments.empty? || method == :execute arguments[0] = Migrator.proper_table_name(arguments.first) end - ActiveRecord::Base.connection.send(method, *arguments, &block) + connection.send(method, *arguments, &block) end end end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 989b2a1ec5..1f3ef300f2 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -1,11 +1,12 @@ module ActiveRecord module NamedScope - # All subclasses of ActiveRecord::Base have two named \scopes: - # * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and + # 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. def self.included(base) base.class_eval do extend ClassMethods @@ -88,7 +89,12 @@ module ActiveRecord when Hash options when Proc - options.call(*args) + case parent_scope + when Scope + with_scope(:find => parent_scope.proxy_options) { options.call(*args) } + else + options.call(*args) + end end, &block) end (class << self; self end).instance_eval do @@ -98,9 +104,9 @@ module ActiveRecord end end end - + class Scope - attr_reader :proxy_scope, :proxy_options + attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set [].methods.each do |m| unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s) @@ -111,8 +117,12 @@ module ActiveRecord delegate :scopes, :with_scope, :to => :proxy_scope def initialize(proxy_scope, options, &block) + options ||= {} [options[:extend]].flatten.each { |extension| extend extension } if options[:extend] extend Module.new(&block) if block_given? + unless Scope === proxy_scope + @current_scoped_methods_when_defined = proxy_scope.send(:current_scoped_methods) + end @proxy_scope, @proxy_options = proxy_scope, options.except(:extend) end @@ -166,9 +176,15 @@ module ActiveRecord if scopes.include?(method) scopes[method].call(self, *args) else - with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do + with_scope({:find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {}}, :reverse_merge) do method = :new if method == :build - proxy_scope.send(method, *args, &block) + if current_scoped_methods_when_defined + with_scope current_scoped_methods_when_defined do + proxy_scope.send(method, *args, &block) + end + else + proxy_scope.send(method, *args, &block) + end end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb new file mode 100644 index 0000000000..e3122d195a --- /dev/null +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -0,0 +1,329 @@ +module ActiveRecord + module NestedAttributes #:nodoc: + def self.included(base) + base.extend(ClassMethods) + base.class_inheritable_accessor :reject_new_nested_attributes_procs, :instance_writer => false + base.reject_new_nested_attributes_procs = {} + end + + # == Nested Attributes + # + # Nested attributes allow you to save attributes on associated records + # through the parent. By default nested attribute updating is turned off, + # you can enable it using the accepts_nested_attributes_for class method. + # When you enable nested attributes an attribute writer is defined on + # the model. + # + # The attribute writer is named after the association, which means that + # in the following example, two new methods are added to your model: + # <tt>author_attributes=(attributes)</tt> and + # <tt>pages_attributes=(attributes)</tt>. + # + # class Book < ActiveRecord::Base + # has_one :author + # has_many :pages + # + # accepts_nested_attributes_for :author, :pages + # end + # + # Note that the <tt>:autosave</tt> option is automatically enabled on every + # association that accepts_nested_attributes_for is used for. + # + # === One-to-one + # + # Consider a Member model that has one Avatar: + # + # class Member < ActiveRecord::Base + # has_one :avatar + # accepts_nested_attributes_for :avatar + # end + # + # Enabling nested attributes on a one-to-one association allows you to + # create the member and avatar in one go: + # + # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } } + # member = Member.create(params) + # member.avatar.id # => 2 + # member.avatar.icon # => 'smiling' + # + # It also allows you to update the avatar through the member: + # + # params = { :member' => { :avatar_attributes => { :id => '2', :icon => 'sad' } } } + # member.update_attributes params['member'] + # member.avatar.icon # => 'sad' + # + # By default you will only be able to set and update attributes on the + # associated model. If you want to destroy the associated model through the + # attributes hash, you have to enable it first using the + # <tt>:allow_destroy</tt> option. + # + # class Member < ActiveRecord::Base + # has_one :avatar + # accepts_nested_attributes_for :avatar, :allow_destroy => true + # end + # + # Now, when you add the <tt>_delete</tt> key to the attributes hash, with a + # value that evaluates to +true+, you will destroy the associated model: + # + # member.avatar_attributes = { :id => '2', :_delete => '1' } + # member.avatar.marked_for_destruction? # => true + # member.save + # member.avatar #=> nil + # + # Note that the model will _not_ be destroyed until the parent is saved. + # + # === One-to-many + # + # Consider a member that has a number of posts: + # + # class Member < ActiveRecord::Base + # has_many :posts + # accepts_nested_attributes_for :posts + # end + # + # You can now set or update attributes on an associated post model through + # the attribute hash. + # + # For each hash that does _not_ have an <tt>id</tt> key a new record will + # be instantiated, unless the hash also contains a <tt>_delete</tt> key + # that evaluates to +true+. + # + # params = { :member => { + # :name => 'joe', :posts_attributes => [ + # { :title => 'Kari, the awesome Ruby documentation browser!' }, + # { :title => 'The egalitarian assumption of the modern citizen' }, + # { :title => '', :_delete => '1' } # this will be ignored + # ] + # }} + # + # member = Member.create(params['member']) + # member.posts.length # => 2 + # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' + # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' + # + # You may also set a :reject_if proc to silently ignore any new record + # hashes if they fail to pass your criteria. For example, the previous + # example could be rewritten as: + # + # class Member < ActiveRecord::Base + # has_many :posts + # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? } + # end + # + # params = { :member => { + # :name => 'joe', :posts_attributes => [ + # { :title => 'Kari, the awesome Ruby documentation browser!' }, + # { :title => 'The egalitarian assumption of the modern citizen' }, + # { :title => '' } # this will be ignored because of the :reject_if proc + # ] + # }} + # + # member = Member.create(params['member']) + # member.posts.length # => 2 + # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' + # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' + # + # If the hash contains an <tt>id</tt> key that matches an already + # associated record, the matching record will be modified: + # + # member.attributes = { + # :name => 'Joe', + # :posts_attributes => [ + # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' }, + # { :id => 2, :title => '[UPDATED] other post' } + # ] + # } + # + # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' + # member.posts.second.title # => '[UPDATED] other post' + # + # By default the associated records are protected from being destroyed. If + # you want to destroy any of the associated records through the attributes + # hash, you have to enable it first using the <tt>:allow_destroy</tt> + # option. This will allow you to also use the <tt>_delete</tt> key to + # destroy existing records: + # + # class Member < ActiveRecord::Base + # has_many :posts + # accepts_nested_attributes_for :posts, :allow_destroy => true + # end + # + # params = { :member => { + # :posts_attributes => [{ :id => '2', :_delete => '1' }] + # }} + # + # member.attributes = params['member'] + # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true + # member.posts.length #=> 2 + # member.save + # member.posts.length # => 1 + # + # === Saving + # + # All changes to models, including the destruction of those marked for + # destruction, are saved and destroyed automatically and atomically when + # the parent model is saved. This happens inside the transaction initiated + # by the parents save method. See ActiveRecord::AutosaveAssociation. + module ClassMethods + # Defines an attributes writer for the specified association(s). If you + # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you + # will need to add the attribute writer to the allowed list. + # + # Supported options: + # [:allow_destroy] + # If true, destroys any members from the attributes hash with a + # <tt>_delete</tt> key and a value that evaluates to +true+ + # (eg. 1, '1', true, or 'true'). This option is off by default. + # [:reject_if] + # Allows you to specify a Proc that checks whether a record should be + # built for a certain attribute hash. The hash is passed to the Proc + # and the Proc should return either +true+ or +false+. When no Proc + # is specified a record will be built for all attribute hashes that + # do not have a <tt>_delete</tt> that evaluates to true. + # + # Examples: + # # creates avatar_attributes= + # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? } + # # 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.update(attr_names.extract_options!) + options.assert_valid_keys(:allow_destroy, :reject_if) + + attr_names.each do |association_name| + if reflection = reflect_on_association(association_name) + type = case reflection.macro + when :has_one, :belongs_to + :one_to_one + when :has_many, :has_and_belongs_to_many + :collection + end + + reflection.options[:autosave] = true + self.reject_new_nested_attributes_procs[association_name.to_sym] = options[:reject_if] + + # def pirate_attributes=(attributes) + # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false) + # end + class_eval %{ + def #{association_name}_attributes=(attributes) + assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]}) + end + }, __FILE__, __LINE__ + else + raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?" + end + end + end + end + + # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's + # used in conjunction with fields_for to build a form element for the + # destruction of this association. + # + # See ActionView::Helpers::FormHelper::fields_for for more info. + def _delete + marked_for_destruction? + end + + private + + # Attribute hash keys that should not be assigned as normal attributes. + # These hash keys are nested attributes implementation details. + UNASSIGNABLE_KEYS = %w{ id _delete } + + # 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 the given attributes include a matching <tt>:id</tt> attribute _and_ a + # <tt>:_delete</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, allow_destroy) + attributes = attributes.stringify_keys + + if attributes['id'].blank? + unless reject_new_record?(association_name, attributes) + send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS)) + 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, allow_destroy) + end + end + + # Assigns the given attributes to the collection association. + # + # Hashes with an <tt>:id</tt> value matching an existing associated record + # will update that record. Hashes without an <tt>:id</tt> value will build + # a new record for the association. Hashes with a matching <tt>:id</tt> + # value and a <tt>:_delete</tt> key set to a truthy value will mark the + # matched record for destruction. + # + # For example: + # + # assign_nested_attributes_for_collection_association(:people, { + # '1' => { :id => '1', :name => 'Peter' }, + # '2' => { :name => 'John' }, + # '3' => { :id => '2', :_delete => true } + # }) + # + # Will update the name of the Person with ID 1, build a new associated + # person with the name `John', and mark the associatied Person with ID 2 + # for destruction. + # + # Also accepts an Array of attribute hashes: + # + # assign_nested_attributes_for_collection_association(:people, [ + # { :id => '1', :name => 'Peter' }, + # { :name => 'John' }, + # { :id => '2', :_delete => true } + # ]) + def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy) + unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) + raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" + end + + if attributes_collection.is_a? Hash + attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes } + end + + attributes_collection.each do |attributes| + attributes = attributes.stringify_keys + + if attributes['id'].blank? + unless reject_new_record?(association_name, attributes) + send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS)) + end + elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s } + assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy) + end + end + end + + # Updates a record with the +attributes+ or marks it for destruction if + # +allow_destroy+ is +true+ and has_delete_flag? returns +true+. + def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) + if has_delete_flag?(attributes) && allow_destroy + record.mark_for_destruction + else + record.attributes = attributes.except(*UNASSIGNABLE_KEYS) + end + end + + # Determines if a hash contains a truthy _delete key. + def has_delete_flag?(hash) + ConnectionAdapters::Column.value_to_boolean hash['_delete'] + end + + # Determines if a new record should be build by checking for + # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this + # association and evaluates to +true+. + def reject_new_record?(association_name, attributes) + has_delete_flag?(attributes) || + self.class.reject_new_nested_attributes_procs[association_name].try(:call, attributes) + end + end +end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 1937abdc83..2d4c1d5507 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -65,6 +65,11 @@ module ActiveRecord def reflect_on_association(association) reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil end + + # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. + def reflect_on_all_autosave_associations + reflections.values.select { |reflection| reflection.options[:autosave] } + end end @@ -192,7 +197,7 @@ module ActiveRecord def counter_cache_column if options[:counter_cache] == true - "#{active_record.name.underscore.pluralize}_count" + "#{active_record.name.demodulize.underscore.pluralize}_count" elsif options[:counter_cache] options[:counter_cache] end diff --git a/activerecord/lib/active_record/serializers/json_serializer.rb b/activerecord/lib/active_record/serializers/json_serializer.rb index 419b45d475..1fd65ed006 100644 --- a/activerecord/lib/active_record/serializers/json_serializer.rb +++ b/activerecord/lib/active_record/serializers/json_serializer.rb @@ -8,6 +8,25 @@ module ActiveRecord #:nodoc: # Returns a JSON string representing the model. Some configuration is # available through +options+. # + # The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the + # top-level behavior of to_json. In a new Rails application, it is set to + # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>, + # to_json will emit a single root node named after the object's type. For example: + # + # konata = User.find(1) + # ActiveRecord::Base.include_root_in_json = true + # konata.to_json + # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} } + # + # ActiveRecord::Base.include_root_in_json = false + # konata.to_json + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} + # + # The remainder of the examples in this section assume include_root_in_json is set to + # <tt>false</tt>. + # # Without any +options+, the returned JSON string will include all # the model's attributes. For example: # diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 4749823b94..fa75874603 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -231,16 +231,22 @@ module ActiveRecord #:nodoc: def add_associations(association, records, opts) if records.is_a?(Enumerable) tag = reformat_name(association.to_s) + type = options[:skip_types] ? {} : {:type => "array"} + if records.empty? - builder.tag!(tag, :type => :array) + builder.tag!(tag, type) else - builder.tag!(tag, :type => :array) do + builder.tag!(tag, type) do association_name = association.to_s.singularize records.each do |record| - record.to_xml opts.merge( - :root => association_name, - :type => (record.class.to_s.underscore == association_name ? nil : record.class.name) - ) + if options[:skip_types] + record_type = {} + else + record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name + record_type = {:type => record_class} + end + + record.to_xml opts.merge(:root => association_name).merge(record_type) end end end diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index 74d91f129e..21471da419 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -99,7 +99,7 @@ module ActiveRecord define_method(:session_id=) { |session_id| self.sessid = session_id } else def self.find_by_session_id(session_id) - find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id] + find :first, :conditions => {:session_id=>session_id} end end end @@ -184,7 +184,7 @@ module ActiveRecord # Look up a session by id and unmarshal its data if found. def find_by_session_id(session_id) - if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}") + if record = connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{connection.quote(session_id)}") new(:session_id => session_id, :marshaled_data => record['data']) end end @@ -287,8 +287,7 @@ module ActiveRecord def get_session(env, sid) Base.silence do sid ||= generate_sid - session = @@session_class.find_by_session_id(sid) - session ||= @@session_class.new(:session_id => sid, :data => {}) + session = find_session(sid) env[SESSION_RECORD_KEY] = session [sid, session.data] end @@ -296,7 +295,7 @@ module ActiveRecord def set_session(env, sid, session_data) Base.silence do - record = env[SESSION_RECORD_KEY] + record = env[SESSION_RECORD_KEY] ||= find_session(sid) record.data = session_data return false unless record.save @@ -310,5 +309,10 @@ module ActiveRecord return true end + + def find_session(id) + @@session_class.find_by_session_id(id) || + @@session_class.new(:session_id => id, :data => {}) + end end end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 149b93203e..8c6abaaccb 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -27,6 +27,7 @@ module ActiveRecord $queries_executed = [] yield ensure + %w{ BEGIN COMMIT }.each { |x| $queries_executed.delete(x) } assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}" end @@ -48,5 +49,18 @@ module ActiveRecord ActiveRecord::Base.clear_all_connections! ActiveRecord::Base.establish_connection(@connection) end + + def with_kcode(kcode) + if RUBY_VERSION < '1.9' + orig_kcode, $KCODE = $KCODE, kcode + begin + yield + ensure + $KCODE = orig_kcode + end + else + yield + end + end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 0b6e52c79b..b059eb7f6f 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -175,6 +175,8 @@ module ActiveRecord # end # RELEASE savepoint active_record_1 # # ^^^^ BOOM! database error! # end + # + # Note that "TRUNCATE" is also a MySQL DDL statement! module ClassMethods # See ActiveRecord::Transactions::ClassMethods for detailed documentation. def transaction(options = {}, &block) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 6d750accb0..d2d12b80c9 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -89,7 +89,7 @@ module ActiveRecord message, options[:default] = options[:default], message if options[:default].is_a?(Symbol) - defaults = @base.class.self_and_descendents_from_active_record.map do |klass| + defaults = @base.class.self_and_descendants_from_active_record.map do |klass| [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", :"models.#{klass.name.underscore}.#{message}" ] end @@ -575,6 +575,8 @@ module ActiveRecord # Get range option and value. option = range_options.first option_value = options[range_options.first] + key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option] + custom_message = options[:message] || options[key] case option when :within, :in @@ -583,9 +585,9 @@ module ActiveRecord validates_each(attrs, options) do |record, attr, value| value = options[:tokenizer].call(value) if value.kind_of?(String) if value.nil? or value.size < option_value.begin - record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin) + record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => option_value.begin) elsif value.size > option_value.end - record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end) + record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => option_value.end) end end when :is, :minimum, :maximum @@ -593,13 +595,10 @@ module ActiveRecord # Declare different validations per option. validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } - message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } validates_each(attrs, options) do |record, attr, value| value = options[:tokenizer].call(value) if value.kind_of?(String) unless !value.nil? and value.size.method(validity_checks[option])[option_value] - key = message_options[option] - custom_message = options[:message] || options[key] record.errors.add(attr, key, :default => custom_message, :count => option_value) end end @@ -721,20 +720,20 @@ module ActiveRecord # class (which has a database table to query from). finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } - is_text_column = finder_class.columns_hash[attr_name.to_s].text? + column = finder_class.columns_hash[attr_name.to_s] if value.nil? comparison_operator = "IS ?" - elsif is_text_column + elsif column.text? comparison_operator = "#{connection.case_sensitive_equality_operator} ?" - value = value.to_s + value = column.limit ? value.to_s[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] || !is_text_column) + if value.nil? || (configuration[:case_sensitive] || !column.text?) condition_sql = "#{sql_attribute} #{comparison_operator}" condition_params = [value] else @@ -745,7 +744,7 @@ module ActiveRecord if scope = configuration[:scope] Array(scope).map do |scope_item| scope_value = record.send(scope_item) - condition_sql << " AND #{record.class.quoted_table_name}.#{scope_item} #{attribute_condition(scope_value)}" + condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value) condition_params << scope_value end end @@ -803,7 +802,7 @@ module ActiveRecord # Validates whether the value of the specified attribute is available in a particular enumerable object. # # class Person < ActiveRecord::Base - # validates_inclusion_of :gender, :in => %w( m f ), :message => "woah! what are you then!??!!" + # validates_inclusion_of :gender, :in => %w( m f ) # validates_inclusion_of :age, :in => 0..99 # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension {{value}} is not included in the list" # end @@ -1041,6 +1040,11 @@ module ActiveRecord errors.empty? end + # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise. + def invalid? + !valid? + end + # Returns the Errors object that holds all information about attribute error messages. def errors @errors ||= Errors.new(self) diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 6ac4bdc905..852807b4c5 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -2,7 +2,7 @@ module ActiveRecord module VERSION #:nodoc: MAJOR = 2 MINOR = 3 - TINY = 0 + TINY = 2 STRING = [MAJOR, MINOR, TINY].join('.') end |