diff options
Diffstat (limited to 'activerecord/lib/active_record')
30 files changed, 903 insertions, 464 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 174ec95de2..61fa34ac39 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -34,7 +34,7 @@ module ActiveRecord class_to_reflection = {} # Not all records have the same class, so group then preload # group on the reflection itself so that if various subclass share the same association then we do not split them - # unncessarily + # unnecessarily records.group_by {|record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, records| raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection send("preload_#{reflection.macro}_association", records, reflection, preload_options) @@ -51,9 +51,7 @@ module ActiveRecord def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| - association_proxy = parent_record.send(reflection_name) - association_proxy.loaded - association_proxy.target = associated_record + parent_record.send("set_#{reflection_name}_target", associated_record) end end @@ -112,8 +110,8 @@ module ActiveRecord def preload_has_one_association(records, reflection, preload_options={}) id_to_record_map, ids = construct_id_map(records) options = reflection.options + records.each {|record| record.send("set_#{reflection.name}_target", nil)} if options[:through] - records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded} through_records = preload_through_records(records, reflection, options[:through]) through_reflection = reflections[options[:through]] through_primary_key = through_reflection.primary_key_name @@ -126,8 +124,6 @@ module ActiveRecord end end else - records.each {|record| record.send("set_#{reflection.name}_target", nil)} - set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name) end end @@ -252,7 +248,7 @@ module ActiveRecord table_name = reflection.klass.quoted_table_name if interface = reflection.options[:as] - conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'" + conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'" else foreign_key = reflection.primary_key_name conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)" diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 7ad7802cbc..f915daafba 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -73,6 +73,7 @@ module ActiveRecord end end + # See ActiveRecord::Associations::ClassMethods for documentation. module Associations # :nodoc: def self.included(base) base.extend(ClassMethods) @@ -150,6 +151,7 @@ module ActiveRecord # #others.destroy_all | X | X | X # #others.find(*args) | X | X | X # #others.find_first | X | | + # #others.exist? | X | X | X # #others.uniq | X | X | X # #others.reset | X | X | X # @@ -582,12 +584,13 @@ module ActiveRecord # has_many :clients # end # - # class Company < ActiveRecord::Base; end + # class Client < ActiveRecord::Base; end # end # end # - # When Firm#clients is called, it will in turn call <tt>MyApplication::Business::Company.find(firm.id)</tt>. If you want to associate - # with a class in another module scope, this can be done by specifying the complete class name. Example: + # When <tt>Firm#clients</tt> is called, it will in turn call <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>. + # If you want to associate with a class in another module scope, this can be done by specifying the complete class name. + # Example: # # module MyApplication # module Business @@ -611,31 +614,53 @@ module ActiveRecord # All of the association macros can be specialized through options. This makes cases more complex than the simple and guessable ones # possible. module ClassMethods - # Adds the following methods for retrieval and query of collections of associated objects: - # +collection+ is replaced with the symbol passed as the first argument, so - # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. - # * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects. + # Specifies a one-to-many association. The following methods for retrieval and query of + # collections of associated objects will be added: + # + # [collection(force_reload = false)] + # Returns an array of all the associated objects. # An empty array is returned if none are found. - # * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. - # * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by setting their foreign keys to +NULL+. + # [collection<<(object, ...)] + # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. + # [collection.delete(object, ...)] + # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # This will also destroy the objects if they're declared as +belongs_to+ and dependent on this model. - # * <tt>collection=objects</tt> - Replaces the collections content by deleting and adding objects as appropriate. - # * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids - # * <tt>collection_singular_ids=ids</tt> - Replace the collection with the objects identified by the primary keys in +ids+ - # * <tt>collection.clear</tt> - Removes every object from the collection. This destroys the associated objects if they - # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the database if <tt>:dependent => :delete_all</tt>, - # otherwise sets their foreign keys to +NULL+. - # * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects. - # * <tt>collection.size</tt> - Returns the number of associated objects. - # * <tt>collection.find</tt> - Finds an associated object according to the same rules as Base.find. - # * <tt>collection.build(attributes = {}, ...)</tt> - Returns one or more new objects of the collection type that have been instantiated - # with +attributes+ and linked to this object through a foreign key, but have not yet been saved. *Note:* This only works if an - # associated object already exists, not if it's +nil+! - # * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated - # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation). - # *Note:* This only works if an associated object already exists, not if it's +nil+! + # [collection=objects] + # Replaces the collections content by deleting and adding objects as appropriate. + # [collection_singular_ids] + # Returns an array of the associated objects' ids + # [collection_singular_ids=ids] + # Replace the collection with the objects identified by the primary keys in +ids+ + # [collection.clear] + # Removes every object from the collection. This destroys the associated objects if they + # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the + # database if <tt>:dependent => :delete_all</tt>, otherwise sets their foreign keys to +NULL+. + # [collection.empty?] + # Returns +true+ if there are no associated objects. + # [collection.size] + # Returns the number of associated objects. + # [collection.find(...)] + # Finds an associated object according to the same rules as ActiveRecord::Base.find. + # [collection.exist?(...)] + # Checks whether an associated object with the given conditions exists. + # Uses the same rules as ActiveRecord::Base.exists?. + # [collection.build(attributes = {}, ...)] + # Returns one or more new objects of the collection type that have been instantiated + # with +attributes+ and linked to this object through a foreign key, but have not yet + # been saved. <b>Note:</b> This only works if an associated object already exists, not if + # it's +nil+! + # [collection.create(attributes = {})] + # Returns a new object of the collection type that has been instantiated + # with +attributes+, linked to this object through a foreign key, and that has already + # been saved (if it passed the validation). <b>Note:</b> This only works if an associated + # object already exists, not if it's +nil+! + # + # (*Note*: +collection+ is replaced with the symbol passed as the first argument, so + # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.) # - # Example: A Firm class declares <tt>has_many :clients</tt>, which will add: + # === Example + # + # A Firm class declares <tt>has_many :clients</tt>, which will add: # * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>) # * <tt>Firm#clients<<</tt> # * <tt>Firm#clients.delete</tt> @@ -646,54 +671,77 @@ module ActiveRecord # * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>) # * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>) # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>) + # * <tt>Firm#clients.exist?(:name => 'ACME')</tt> (similar to <tt>Client.exist?(:name => 'ACME', :firm_id => firm.id)</tt>) # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # The declaration can also include an options hash to specialize the behavior of the association. # - # Options are: - # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred + # === Supported options + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_many :products</tt> will by default be linked to the Product class, but # if the real class name is SpecialProduct, you'll have to specify it with this option. - # * <tt>:conditions</tt> - Specify the conditions that the associated objects must meet in order to be included as a +WHERE+ + # [:conditions] + # Specify the conditions that the associated objects must meet in order to be included as a +WHERE+ # SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from the association are scoped if a hash # is used. <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt> # or <tt>@blog.posts.build</tt>. - # * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, + # [:order] + # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, # such as <tt>last_name, first_name DESC</tt>. - # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name + # [: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_many+ association will use "person_id" # as the default <tt>:foreign_key</tt>. - # * <tt>:primary_key</tt> - Specify the method that returns the primary key used for the association. By default this is +id+. - # * <tt>:dependent</tt> - If set to <tt>:destroy</tt> all the associated objects are destroyed + # [:primary_key] + # Specify the method that returns the primary key used for the association. By default this is +id+. + # [:dependent] + # If set to <tt>:destroy</tt> all the associated objects are destroyed # alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated # objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated # objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. *Warning:* This option is ignored when also using # the <tt>:through</tt> option. - # * <tt>:finder_sql</tt> - Specify a complete SQL statement to fetch the association. This is a good way to go for complex + # [:finder_sql] + # Specify a complete SQL statement to fetch the association. This is a good way to go for complex # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added. - # * <tt>:counter_sql</tt> - Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is + # [:counter_sql] + # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is # specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>. - # * <tt>:extend</tt> - Specify a named module for extending the proxy. See "Association extensions". - # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded. - # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. - # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. - # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join + # [:extend] + # Specify a named module for extending the proxy. See "Association extensions". + # [:include] + # Specify second-order associations that should be eager loaded when the collection is loaded. + # [:group] + # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. + # [:limit] + # An integer determining the limit on the number of rows that should be returned. + # [:offset] + # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. + # [:select] + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. - # * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>). - # * <tt>:through</tt> - Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> + # [:as] + # Specifies a polymorphic interface (See <tt>belongs_to</tt>). + # [:through] + # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt> # or <tt>has_many</tt> association on the join model. - # * <tt>:source</tt> - Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be + # [:source] + # Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be # inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or # <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given. - # * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_many :through</tt> queries where the source + # [:source_type] + # Specifies type of the source association used by <tt>has_many :through</tt> queries where the source # association is a polymorphic +belongs_to+. - # * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>. - # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association. - # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. true by default. - # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). - # + # [:uniq] + # If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>. + # [:readonly] + # 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. + # [:accessible] + # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). + # Option examples: # has_many :comments, :order => "posted_on" # has_many :comments, :include => :author @@ -724,58 +772,91 @@ module ActiveRecord end end - # Adds the following methods for retrieval and query of a single associated object: - # +association+ is replaced with the symbol passed as the first argument, so - # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. - # * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found. - # * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, sets it as the foreign key, + # Specifies a one-to-one association with another class. This method should only be used + # if the other class contains the foreign key. If the current class contains the foreign key, + # then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use has_one and when to use belongs_to. + # + # The following methods for retrieval and query of a single associated object will be added: + # + # [association(force_reload = false)] + # Returns the associated object. +nil+ is returned if none is found. + # [association=(associate)] + # Assigns the associate object, extracts the primary key, sets it as the foreign key, # and saves the associate object. - # * <tt>association.nil?</tt> - Returns +true+ if there is no associated object. - # * <tt>build_association(attributes = {})</tt> - 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. Note: This ONLY works if - # an association already exists. It will NOT work if the association is +nil+. - # * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated - # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation). + # [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. <b>Note:</b> This ONLY works if an association already exists. + # It will NOT work if the association is +nil+. + # [create_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated + # with +attributes+, linked to this object through a foreign key, and that + # has already been saved (if it passed the validation). # - # Example: An Account class declares <tt>has_one :beneficiary</tt>, which will add: + # (+association+ is replaced with the symbol passed as the first argument, so + # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.) + # + # === Example + # + # 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>) # + # === Options + # # The declaration can also include an options hash to specialize the behavior of the association. # # Options are: - # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_one :manager</tt> will by default be linked to the Manager class, but # if the real class name is Person, you'll have to specify it with this option. - # * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+ + # [:conditions] + # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ # SQL fragment, such as <tt>rank = 5</tt>. - # * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, + # [:order] + # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, # such as <tt>last_name, first_name DESC</tt>. - # * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to + # [:dependent] + # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to <tt>:nullify</tt>, the associated # object's foreign key is set to +NULL+. Also, association is assigned. - # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name + # [: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_one+ association will use "person_id" # as the default <tt>:foreign_key</tt>. - # * <tt>:primary_key</tt> - Specify the method that returns the primary key used for the association. By default this is +id+. - # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded. - # * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>). - # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join + # [:primary_key] + # Specify the method that returns the primary key used for the association. By default this is +id+. + # [:include] + # Specify second-order associations that should be eager loaded when this object is loaded. + # [:as] + # Specifies a polymorphic interface (See <tt>belongs_to</tt>). + # [:select] + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. - # * <tt>:through</tt>: Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> + # [:through] + # Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt> # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a # <tt>has_one</tt> or <tt>belongs_to</tt> association on the join model. - # * <tt>:source</tt> - Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be + # [: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>:source_type</tt> - Specifies type of the source association used by <tt>has_one :through</tt> queries where the source + # [: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+. - # * <tt>:readonly</tt> - If true, the associated object is readonly through the association. - # * <tt>:validate</tt> - If false, don't validate the associated object when saving the parent object. +false+ by default. - # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). + # [: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. + # [:accessible] + # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card @@ -815,18 +896,34 @@ module ActiveRecord end end - # Adds the following methods for retrieval and query for a single associated object for which this object holds an id: - # +association+ is replaced with the symbol passed as the first argument, so - # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. - # * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found. - # * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, and sets it as the foreign key. - # * <tt>association.nil?</tt> - Returns +true+ if there is no associated object. - # * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated + # Specifies a one-to-one association with another class. This method should only be used + # if this class contains the foreign key. If the other class contains the foreign key, + # then you should use +has_one+ instead. See also ActiveRecord::Associations::ClassMethods's overview + # on when to use +has_one+ and when to use +belongs_to+. + # + # Methods will be added for retrieval and query for a single associated object, for which + # this object holds an id: + # + # [association(force_reload = false)] + # 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. - # * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated - # with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation). + # [create_association(attributes = {})] + # Returns a new object of the associated type that has been instantiated + # with +attributes+, linked to this object through a foreign key, and that + # has already been saved (if it passed the validation). + # + # (+association+ is replaced with the symbol passed as the first argument, so + # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.) # - # Example: A Post class declares <tt>belongs_to :author</tt>, which will add: + # === Example + # + # A Post class declares <tt>belongs_to :author</tt>, which will add: # * <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>) @@ -835,23 +932,30 @@ module ActiveRecord # * <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. # - # Options are: - # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred + # === Options + # + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but # if the real class name is Person, you'll have to specify it with this option. - # * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+ + # [: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>. - # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join + # [:select] + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. - # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name + # [:foreign_key] + # Specify the foreign key used for the association. By default this is guessed to be the name # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use # "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt> # will use a foreign key of "favorite_person_id". - # * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to + # [:dependent] + # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when # <tt>belongs_to</tt> is used in conjunction with a <tt>has_many</tt> relationship on another class because of the potential to leave # orphaned records behind. - # * <tt>:counter_cache</tt> - Caches the number of belonging objects on the associate class through the use of +increment_counter+ + # [:counter_cache] + # Caches the number of belonging objects on the associate class through the use of +increment_counter+ # and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's # destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class) # is used on the associate class (such as a Post class). You can also specify a custom counter cache column by providing @@ -859,13 +963,18 @@ module ActiveRecord # When creating a counter cache column, the database statement or migration must specify a default value of <tt>0</tt>, failing to do # this results in a counter with +NULL+ value, which will never increment. # Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+. - # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded. - # * <tt>:polymorphic</tt> - Specify this association is a polymorphic association by passing +true+. + # [:include] + # Specify second-order associations that should be eager loaded when this object is loaded. + # [:polymorphic] + # Specify this association is a polymorphic association by passing +true+. # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute # to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>). - # * <tt>:readonly</tt> - If true, the associated object is readonly through the association. - # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default. - # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). + # [:readonly] + # 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. + # [:accessible] + # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -951,8 +1060,9 @@ module ActiveRecord configure_dependency_for_belongs_to(reflection) end - # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as - # an option, it is guessed using the lexical order of the class names. So a join between Developer and Project + # Specifies a many-to-many relationship with another class. This associates two classes via an + # intermediate join table. Unless the join table is explicitly specified as an option, it is + # guessed using the lexical order of the class names. So a join between Developer and Project # will give the default join table name of "developers_projects" because "D" outranks "P". Note that this precedence # is calculated using the <tt><</tt> operator for String. This means that if the strings are of different lengths, # and the strings are equal when compared up to the shortest length, then the longer string is considered of higher @@ -967,28 +1077,48 @@ module ActiveRecord # associations with attributes to a real join model (see introduction). # # Adds the following methods for retrieval and query: - # +collection+ is replaced with the symbol passed as the first argument, so - # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. - # * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects. + # + # [collection(force_reload = false)] + # Returns an array of all the associated objects. # An empty array is returned if none are found. - # * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by creating associations in the join table + # [collection<<(object, ...)] + # Adds one or more objects to the collection by creating associations in the join table # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). - # * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by removing their associations from the join table. + # [collection.delete(object, ...)] + # Removes one or more objects from the collection by removing their associations from the join table. # This does not destroy the objects. - # * <tt>collection=objects</tt> - Replaces the collection's content by deleting and adding objects as appropriate. - # * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids. - # * <tt>collection_singular_ids=ids</tt> - Replace the collection by the objects identified by the primary keys in +ids+. - # * <tt>collection.clear</tt> - Removes every object from the collection. This does not destroy the objects. - # * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects. - # * <tt>collection.size</tt> - Returns the number of associated objects. - # * <tt>collection.find(id)</tt> - Finds an associated object responding to the +id+ and that + # [collection=objects] + # Replaces the collection's content by deleting and adding objects as appropriate. + # [collection_singular_ids] + # Returns an array of the associated objects' ids. + # [collection_singular_ids=ids] + # Replace the collection by the objects identified by the primary keys in +ids+. + # [collection.clear] + # Removes every object from the collection. This does not destroy the objects. + # [collection.empty?] + # Returns +true+ if there are no associated objects. + # [collection.size] + # Returns the number of associated objects. + # [collection.find(id)] + # Finds an associated object responding to the +id+ and that # meets the condition that it has to be associated with this object. - # * <tt>collection.build(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated + # Uses the same rules as ActiveRecord::Base.find. + # [collection.exist?(...)] + # Checks whether an associated object with the given conditions exists. + # Uses the same rules as ActiveRecord::Base.exists?. + # [collection.build(attributes = {})] + # Returns a new object of the collection type that has been instantiated # with +attributes+ and linked to this object through the join table, but has not yet been saved. - # * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated + # [collection.create(attributes = {})] + # Returns a new object of the collection type that has been instantiated # with +attributes+, linked to this object through the join table, and that has already been saved (if it passed the validation). # - # Example: A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add: + # (+collection+ is replaced with the symbol passed as the first argument, so + # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.) + # + # === Example + # + # A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add: # * <tt>Developer#projects</tt> # * <tt>Developer#projects<<</tt> # * <tt>Developer#projects.delete</tt> @@ -999,45 +1129,66 @@ module ActiveRecord # * <tt>Developer#projects.empty?</tt> # * <tt>Developer#projects.size</tt> # * <tt>Developer#projects.find(id)</tt> + # * <tt>Developer#clients.exist?(...)</tt> # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("project_id" => id)</tt>) # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("project_id" => id); c.save; c</tt>) # The declaration may include an options hash to specialize the behavior of the association. # - # Options are: - # * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred + # === Options + # + # [:class_name] + # Specify the class name of the association. Use it only if that name can't be inferred # from the association name. So <tt>has_and_belongs_to_many :projects</tt> will by default be linked to the # Project class, but if the real class name is SuperProject, you'll have to specify it with this option. - # * <tt>:join_table</tt> - Specify the name of the join table if the default based on lexical order isn't what you want. - # WARNING: If you're overwriting the table name of either class, the +table_name+ method MUST be declared underneath any - # +has_and_belongs_to_many+ declaration in order to work. - # * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name + # [:join_table] + # Specify the name of the join table if the default based on lexical order isn't what you want. + # <b>WARNING:</b> If you're overwriting the table name of either class, the +table_name+ method + # MUST be declared underneath any +has_and_belongs_to_many+ declaration in order to work. + # [: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>. - # * <tt>:association_foreign_key</tt> - Specify the association foreign key used for the association. By default this is + # [: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>. - # * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+ + # [: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. # <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt> # or <tt>@blog.posts.build</tt>. - # * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, + # [:order] + # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment, # such as <tt>last_name, first_name DESC</tt> - # * <tt>:uniq</tt> - If true, duplicate associated objects will be ignored by accessors and query methods. - # * <tt>:finder_sql</tt> - Overwrite the default generated SQL statement used to fetch the association with a manual statement - # * <tt>:delete_sql</tt> - Overwrite the default generated SQL statement used to remove links between the associated + # [:uniq] + # If true, duplicate associated objects will be ignored by accessors and query methods. + # [:finder_sql] + # Overwrite the default generated SQL statement used to fetch the association with a manual statement + # [:delete_sql] + # Overwrite the default generated SQL statement used to remove links between the associated # classes with a manual statement. - # * <tt>:insert_sql</tt> - Overwrite the default generated SQL statement used to add links between the associated classes + # [:insert_sql] + # Overwrite the default generated SQL statement used to add links between the associated classes # with a manual statement. - # * <tt>:extend</tt> - Anonymous module for extending the proxy, see "Association extensions". - # * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded. - # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. - # * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned. - # * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. - # * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join + # [:extend] + # Anonymous module for extending the proxy, see "Association extensions". + # [:include] + # Specify second-order associations that should be eager loaded when the collection is loaded. + # [:group] + # An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause. + # [:limit] + # An integer determining the limit on the number of rows that should be returned. + # [:offset] + # An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. + # [:select] + # By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. - # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association. - # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +true+ by default. - # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). + # [:readonly] + # 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. + # [:accessible<] + # Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>). # # Option examples: # has_and_belongs_to_many :projects @@ -1528,19 +1679,19 @@ module ActiveRecord else all << cond end end - conditions.join(' ').scan(/([\.\w]+).?\./).flatten + conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten end def order_tables(options) order = [options[:order], scope(:find, :order) ].join(", ") return [] unless order && order.is_a?(String) - order.scan(/([\.\w]+).?\./).flatten + order.scan(/([\.a-zA-Z_]+).?\./).flatten end def selects_tables(options) select = options[:select] return [] unless select && select.is_a?(String) - select.scan(/"?([\.\w]+)"?.?\./).flatten + select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten end # Checks if the conditions reference a table other than the current model table @@ -1742,6 +1893,7 @@ module ActiveRecord collection.target.push(association) when :has_one return if record.id.to_s != join.parent.record_id(row).to_s + return if record.instance_variable_defined?("@#{join.reflection.name}") association = join.instantiate(row) unless row[join.aliased_primary_key].nil? record.send("set_#{join.reflection.name}_target", association) when :belongs_to @@ -1823,7 +1975,7 @@ module ActiveRecord @aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join") end - if reflection.macro == :has_many && reflection.options[:through] + if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through] @aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join") end end @@ -1847,7 +1999,7 @@ module ActiveRecord ] when :has_many, :has_one case - when reflection.macro == :has_many && reflection.options[:through] + when reflection.options[:through] through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : '' jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil @@ -1883,7 +2035,7 @@ module ActiveRecord jt_sti_extra = " AND %s.%s = %s" % [ connection.quote_table_name(aliased_join_table_name), connection.quote_column_name(through_reflection.active_record.inheritance_column), - through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)] + through_reflection.klass.quote_value(through_reflection.klass.sti_name)] end when :belongs_to first_key = primary_key @@ -1948,10 +2100,8 @@ module ActiveRecord else "" end || '' - join << %(AND %s.%s = %s ) % [ - connection.quote_table_name(aliased_table_name), - connection.quote_column_name(klass.inheritance_column), - klass.quote_value(klass.name.demodulize)] unless klass.descends_from_active_record? + join << %(AND %s) % [ + klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record? [through_reflection, reflection].each do |ref| join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions] diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index a28be9eed1..9061037b39 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -344,7 +344,7 @@ module ActiveRecord callback(:before_add, record) yield(record) if block_given? @target ||= [] unless loaded? - @target << record + @target << record unless @reflection.options[:uniq] && @target.include?(record) callback(:after_add, record) record end diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 77fc827e11..99b8748a48 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -131,10 +131,6 @@ module ActiveRecord records.map { |record| record.quoted_id }.join(',') end - def interpolate_sql_options!(options, *keys) - keys.each { |key| options[key] &&= interpolate_sql(options[key]) } - end - def interpolate_sql(sql, record = nil) @owner.send(:interpolate_sql, sql, record) end @@ -217,7 +213,7 @@ module ActiveRecord # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems. def flatten_deeper(array) - array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten + array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten end def owner_quoted_id diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 7c28cbdd07..7c28cbdd07 100755..100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index d8146daa54..d8146daa54 100755..100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index d516d54151..e7e433b6b6 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -70,10 +70,8 @@ module ActiveRecord end def construct_sql - interpolate_sql_options!(@reflection.options, :finder_sql) - if @reflection.options[:finder_sql] - @finder_sql = @reflection.options[:finder_sql] + @finder_sql = interpolate_sql(@reflection.options[:finder_sql]) else @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} " @finder_sql << " AND (#{conditions})" if conditions diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index e6fa15c173..ce62127505 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -35,8 +35,11 @@ module ActiveRecord else @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include]) end - - @target = [] and loaded if count == 0 + + # If there's nothing in the database and @target has no new records + # we are certain the current target is an empty array. This is a + # documented side-effect of the method that may avoid an extra SELECT. + @target ||= [] and loaded if count == 0 if @reflection.options[:limit] count = [ @reflection.options[:limit], count ].min diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index e1bfff5923..24b02efc35 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -237,7 +237,7 @@ module ActiveRecord end def build_sti_condition - "#{@reflection.through_reflection.quoted_table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.sti_name)}" + @reflection.through_reflection.klass.send(:type_condition) end alias_method :sql_conditions, :conditions diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index fdc0fa52c9..18733255d2 100755..100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -21,8 +21,8 @@ module ActiveRecord def replace(obj, dont_save = false) load_target - unless @target.nil? - if dependent? && !dont_save && @target != obj + unless @target.nil? || @target == obj + if dependent? && !dont_save @target.destroy unless @target.new_record? @owner.clear_association_cache else diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 962c2b36d9..5c30f80555 100755..100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -6,7 +6,7 @@ module ActiveRecord #:nodoc: class ActiveRecordError < StandardError end - # Raised when the single-table inheritance mechanism failes to locate the subclass + # Raised when the single-table inheritance mechanism fails to locate the subclass # (for example due to improper usage of column that +inheritance_column+ points to). class SubclassNotFound < ActiveRecordError #:nodoc: end @@ -83,8 +83,33 @@ module ActiveRecord #:nodoc: class ReadOnlyRecord < ActiveRecordError end - # Used by Active Record transaction mechanism to distinguish rollback from other exceptional situations. - # You can use it to roll your transaction back explicitly in the block passed to +transaction+ method. + # ActiveRecord::Transactions::ClassMethods.transaction uses this exception + # to distinguish a deliberate rollback from other exceptional situations. + # Normally, raising an exception will cause the +transaction+ method to rollback + # the database transaction *and* pass on the exception. But if you raise an + # ActiveRecord::Rollback exception, then the database transaction will be rolled back, + # without passing on the exception. + # + # For example, you could do this in your controller to rollback a transaction: + # + # class BooksController < ActionController::Base + # def create + # Book.transaction do + # book = Book.new(params[:book]) + # book.save! + # if today_is_friday? + # # The system must fail on Friday so that our support department + # # won't be out of job. We silently rollback this transaction + # # without telling the user. + # raise ActiveRecord::Rollback, "Call tech support!" + # end + # end + # # ActiveRecord::Rollback is the only exception that won't be passed on + # # by ActiveRecord::Base.transaction, so this line will still be reached + # # even on Friday. + # redirect_to root_url + # end + # end class Rollback < ActiveRecordError end @@ -97,7 +122,11 @@ module ActiveRecord #:nodoc: class MissingAttributeError < NoMethodError end - # Raised when an error occured while doing a mass assignment to an attribute through the + # Raised when unknown attributes are supplied via mass assignment. + class UnknownAttributeError < NoMethodError + end + + # Raised when an error occurred while doing a mass assignment to an attribute through the # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the # offending attribute. class AttributeAssignmentError < ActiveRecordError @@ -271,7 +300,7 @@ module ActiveRecord #:nodoc: # # Now 'Bob' exist and is an 'admin' # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true } # - # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Protected attributes won't be setted unless they are given in a block. For example: + # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without saving it first. Protected attributes won't be set unless they are given in a block. For example: # # # No 'Winter' tag exists # winter = Tag.find_or_initialize_by_name("Winter") @@ -439,6 +468,10 @@ module ActiveRecord #:nodoc: cattr_accessor :schema_format , :instance_writer => false @@schema_format = :ruby + # Specify whether or not to use timestamps for migration numbers + cattr_accessor :timestamped_migrations , :instance_writer => false + @@timestamped_migrations = true + # Determine whether to store the full constant name including namespace when using STI superclass_delegating_accessor :store_full_sti_class self.store_full_sti_class = false @@ -724,8 +757,7 @@ module ActiveRecord #:nodoc: # ==== Attributes # # * +updates+ - A String of column and value pairs that will be set on any records that match conditions. - # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. - # See conditions in the intro for more info. + # * +conditions+ - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro for more info. # * +options+ - Additional options are <tt>:limit</tt> and/or <tt>:order</tt>, see the examples for usage. # # ==== Examples @@ -828,7 +860,7 @@ module ActiveRecord #:nodoc: def update_counters(id, counters) updates = counters.inject([]) { |list, (counter_name, increment)| sign = increment < 0 ? "-" : "+" - list << "#{connection.quote_column_name(counter_name)} = #{connection.quote_column_name(counter_name)} #{sign} #{increment.abs}" + list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}" }.join(", ") update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}") end @@ -1188,11 +1220,46 @@ module ActiveRecord #:nodoc: subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information } end + def self_and_descendents_from_active_record#nodoc: + klass = self + classes = [klass] + while klass != klass.base_class + classes << klass = klass.superclass + end + classes + rescue + # OPTIMIZE this rescue is to fix this test: ./test/cases/reflection_test.rb:56:in `test_human_name_for_column' + # Appearantly the method base_class causes some trouble. + # It now works for sure. + [self] + end + # Transforms attribute key names into a more humane format, such as "First name" instead of "first_name". Example: # Person.human_attribute_name("first_name") # => "First name" - # Deprecated in favor of just calling "first_name".humanize - def human_attribute_name(attribute_key_name) #:nodoc: - attribute_key_name.humanize + # This used to be depricated in favor of humanize, but is now preferred, because it automatically uses the I18n + # module now. + # Specify +options+ with additional translating options. + def human_attribute_name(attribute_key_name, options = {}) + defaults = self_and_descendents_from_active_record.map do |klass| + :"#{klass.name.underscore}.#{attribute_key_name}" + end + defaults << options[:default] if options[:default] + defaults.flatten! + defaults << attribute_key_name.humanize + options[:count] ||= 1 + I18n.translate(defaults.shift, options.merge(:default => defaults, :scope => [:activerecord, :attributes])) + end + + # Transform the modelname into a more humane format, using I18n. + # Defaults to the basic humanize method. + # Default scope of the translation is activerecord.models + # Specify +options+ with additional translating options. + def human_name(options = {}) + defaults = self_and_descendents_from_active_record.map do |klass| + :"#{klass.name.underscore}" + end + defaults << self.name.humanize + I18n.translate(defaults.shift, {:scope => [:activerecord, :models], :count => 1, :default => defaults}.merge(options)) end # True if this isn't a concrete subclass needing a STI type condition. @@ -1287,8 +1354,8 @@ module ActiveRecord #:nodoc: end def respond_to?(method_id, include_private = false) - if match = matches_dynamic_finder?(method_id) || matches_dynamic_finder_with_initialize_or_create?(method_id) - return true if all_attributes_exists?(extract_attribute_names_from_match(match)) + if match = DynamicFinderMatch.match(method_id) + return true if all_attributes_exists?(match.attribute_names) end super end @@ -1577,10 +1644,11 @@ module ActiveRecord #:nodoc: sql << "WHERE #{merged_conditions} " unless merged_conditions.blank? end - def type_condition + def type_condition(table_alias=nil) + quoted_table_alias = self.connection.quote_table_name(table_alias || table_name) quoted_inheritance_column = connection.quote_column_name(inheritance_column) - type_condition = subclasses.inject("#{quoted_table_name}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass| - condition << "OR #{quoted_table_name}.#{quoted_inheritance_column} = '#{subclass.sti_name}' " + type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass| + condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' " end " (#{type_condition}) " @@ -1606,88 +1674,67 @@ module ActiveRecord #:nodoc: # Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future # attempts to use it do not run through method_missing. def method_missing(method_id, *arguments) - if match = matches_dynamic_finder?(method_id) - finder = determine_finder(match) - - attribute_names = extract_attribute_names_from_match(match) + if match = DynamicFinderMatch.match(method_id) + attribute_names = match.attribute_names super unless all_attributes_exists?(attribute_names) - - self.class_eval %{ - def self.#{method_id}(*args) - options = args.extract_options! - attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) - finder_options = { :conditions => attributes } - validate_find_options(options) - set_readonly_option!(options) - - if options[:conditions] - with_scope(:find => finder_options) do - ActiveSupport::Deprecation.silence { send(:#{finder}, options) } + if match.finder? + finder = match.finder + bang = match.bang? + self.class_eval %{ + def self.#{method_id}(*args) + options = args.extract_options! + attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) + finder_options = { :conditions => attributes } + validate_find_options(options) + set_readonly_option!(options) + + #{'result = ' if bang}if options[:conditions] + with_scope(:find => finder_options) do + ActiveSupport::Deprecation.silence { send(:#{finder}, options) } + end + else + ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) } end - else - ActiveSupport::Deprecation.silence { send(:#{finder}, options.merge(finder_options)) } - end - end - }, __FILE__, __LINE__ - send(method_id, *arguments) - elsif match = matches_dynamic_finder_with_initialize_or_create?(method_id) - instantiator = determine_instantiator(match) - attribute_names = extract_attribute_names_from_match(match) - super unless all_attributes_exists?(attribute_names) - - self.class_eval %{ - def self.#{method_id}(*args) - guard_protected_attributes = false - - if args[0].is_a?(Hash) - guard_protected_attributes = true - attributes = args[0].with_indifferent_access - find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}]) - else - find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) + #{'result || raise(RecordNotFound)' if bang} end + }, __FILE__, __LINE__ + send(method_id, *arguments) + elsif match.instantiator? + instantiator = match.instantiator + self.class_eval %{ + def self.#{method_id}(*args) + guard_protected_attributes = false + + if args[0].is_a?(Hash) + guard_protected_attributes = true + attributes = args[0].with_indifferent_access + find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}]) + else + find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) + end - options = { :conditions => find_attributes } - set_readonly_option!(options) + options = { :conditions => find_attributes } + set_readonly_option!(options) - record = find_initial(options) + record = find_initial(options) - if record.nil? - record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) } - #{'yield(record) if block_given?'} - #{'record.save' if instantiator == :create} - record - else - record + if record.nil? + record = self.new { |r| r.send(:attributes=, attributes, guard_protected_attributes) } + #{'yield(record) if block_given?'} + #{'record.save' if instantiator == :create} + record + else + record + end end - end - }, __FILE__, __LINE__ - send(method_id, *arguments) + }, __FILE__, __LINE__ + send(method_id, *arguments) + end else super end end - def matches_dynamic_finder?(method_id) - /^find_(all_by|by)_([_a-zA-Z]\w*)$/.match(method_id.to_s) - end - - def matches_dynamic_finder_with_initialize_or_create?(method_id) - /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/.match(method_id.to_s) - end - - def determine_finder(match) - match.captures.first == 'all_by' ? :find_every : :find_initial - end - - def determine_instantiator(match) - match.captures.first == 'initialize' ? :new : :create - end - - def extract_attribute_names_from_match(match) - match.captures.last.split('_and_') - end - def construct_attributes_from_arguments(attribute_names, arguments) attributes = {} attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] } @@ -1717,7 +1764,7 @@ module ActiveRecord #:nodoc: def attribute_condition(argument) case argument when nil then "IS ?" - when Array, ActiveRecord::Associations::AssociationCollection then "IN (?)" + when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)" when Range then "BETWEEN ? AND ?" else "= ?" end @@ -2372,7 +2419,11 @@ module ActiveRecord #:nodoc: attributes = remove_attributes_protected_from_mass_assignment(attributes) if guard_protected_attributes attributes.each do |k, v| - k.include?("(") ? multi_parameter_attributes << [ k, v ] : send(k + "=", v) + if k.include?("(") + multi_parameter_attributes << [ k, v ] + else + respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}") + end end assign_multiparameter_attributes(multi_parameter_attributes) @@ -2535,11 +2586,14 @@ module ActiveRecord #:nodoc: end def convert_number_column_value(value) - case value - when FalseClass; 0 - when TrueClass; 1 - when ''; nil - else value + if value == false + 0 + elsif value == true + 1 + elsif value.is_a?(String) && value.blank? + nil + else + value end end @@ -2558,7 +2612,7 @@ module ActiveRecord #:nodoc: removed_attributes = attributes.keys - safe_attributes.keys if removed_attributes.any? - logger.debug "WARNING: Can't mass-assign these protected attributes: #{removed_attributes.join(', ')}" + log_protected_attribute_removal(removed_attributes) end safe_attributes @@ -2573,6 +2627,10 @@ module ActiveRecord #:nodoc: end end + def log_protected_attribute_removal(*attributes) + logger.debug "WARNING: Can't mass-assign these protected attributes: #{attributes.join(', ')}" + end + # The primary key and inheritance column can never be set by mass-assignment for security reasons. def attributes_protected_by_default default = [ self.class.primary_key, self.class.inheritance_column ] @@ -2586,8 +2644,15 @@ module ActiveRecord #:nodoc: quoted = {} connection = self.class.connection attribute_names.each do |name| - if column = column_for_attribute(name) - quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && column.primary + if (column = column_for_attribute(name)) && (include_primary_key || !column.primary) + value = read_attribute(name) + + # We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML. + if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time)) + value = value.to_yaml + end + + quoted[name] = connection.quote(value, column) end end include_readonly_attributes ? quoted : remove_readonly_attributes(quoted) diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index 2ca1a0aaa3..246f87b7a9 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -211,7 +211,7 @@ module ActiveRecord sql << " ORDER BY #{options[:order]} " if options[:order] add_limit!(sql, options, scope) - sql << ')' if use_workaround + sql << ') AS #{aggregate_alias}_subquery' if use_workaround sql end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 1e385fb128..eec531c514 100755..100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -50,7 +50,7 @@ module ActiveRecord # # == Inheritable callback queues # - # Besides the overwriteable callback methods, it's also possible to register callbacks through the use of the callback macros. + # Besides the overwritable callback methods, it's also possible to register callbacks through the use of the callback macros. # Their main advantage is that the macros add behavior into a callback queue that is kept intact down through an inheritance # hierarchy. Example: # @@ -169,6 +169,18 @@ module ActiveRecord # If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns # +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks # defined as methods on the model, which are called last. + # + # == Transactions + # + # The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs + # within a transaction. That includes <tt>after_*</tt> hooks. If everything + # goes fine a COMMIT is executed once the chain has been completed. + # + # If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You + # can also trigger a ROLLBACK raising an exception in any of the callbacks, + # including <tt>after_*</tt> hooks. Note, however, that in that case the client + # needs to be aware of it because an ordinary +save+ will raise such exception + # instead of quietly returning +false+. module Callbacks CALLBACKS = %w( after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 5358491cde..aaf9e2e73f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -149,6 +149,10 @@ module ActiveRecord "INSERT INTO #{quote_table_name(table_name)} VALUES(DEFAULT)" end + def case_sensitive_equality_operator + "=" + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 31d6c7942c..75032efe57 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -138,7 +138,11 @@ module ActiveRecord # convert something to a boolean def value_to_boolean(value) - TRUE_VALUES.include?(value) + if value.is_a?(String) && value.blank? + nil + else + TRUE_VALUES.include?(value) + end end # convert something to a BigDecimal @@ -443,9 +447,10 @@ module ActiveRecord # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and # <tt>:updated_at</tt> to the table. - def timestamps - column(:created_at, :datetime) - column(:updated_at, :datetime) + def timestamps(*args) + options = args.extract_options! + column(:created_at, :datetime, options) + column(:updated_at, :datetime, options) end def references(*args) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 7d8530ebef..bececf82a0 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -383,13 +383,9 @@ module ActiveRecord def add_column_options!(sql, options) #:nodoc: sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options) - # must explcitly check for :null to allow change_column to work on migrations - if options.has_key? :null - if options[:null] == false - sql << " NOT NULL" - else - sql << " NULL" - end + # must explicitly check for :null to allow change_column to work on migrations + if options[:null] == false + sql << " NOT NULL" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index f48b107a2a..6924bb7e6f 100755..100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -51,6 +51,13 @@ module ActiveRecord true end + # Does this adapter support DDL rollbacks in transactions? That is, would + # CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL, + # SQL Server, and others support this. MySQL and others do not. + def supports_ddl_transactions? + false + end + # Should primary key values be selected from their corresponding # sequence before the insert statement? If true, next_sequence_value # is called before each insert to set the record's primary key. @@ -118,6 +125,19 @@ module ActiveRecord @connection end + def open_transactions + @open_transactions ||= 0 + end + + def increment_open_transactions + @open_transactions ||= 0 + @open_transactions += 1 + end + + def decrement_open_transactions + @open_transactions -= 1 + end + def log_info(sql, name, runtime) if @logger && @logger.debug? name = "#{name.nil? ? "SQL" : name} (#{sprintf("%f", runtime)})" diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 35b9ed4746..204ebaa2e2 100755..100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -511,6 +511,10 @@ module ActiveRecord keys.length == 1 ? [keys.first, nil] : nil end + def case_sensitive_equality_operator + "= BINARY" + end + private def connect @connection.reconnect = true if @connection.respond_to?(:reconnect=) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6d16d72dea..55c7da5b4f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -23,8 +23,8 @@ module ActiveRecord config = config.symbolize_keys host = config[:host] port = config[:port] || 5432 - username = config[:username].to_s - password = config[:password].to_s + username = config[:username].to_s if config[:username] + password = config[:password].to_s if config[:password] if config.has_key?(:database) database = config[:database] @@ -182,8 +182,8 @@ module ActiveRecord def self.extract_value_from_default(default) case default # Numeric types - when /\A-?\d+(\.\d*)?\z/ - default + when /\A\(?(-?\d+(\.\d*)?\)?)\z/ + $1 # Character types when /\A'(.*)'::(?:character varying|bpchar|text)\z/m $1 @@ -335,6 +335,10 @@ module ActiveRecord postgresql_version >= 80200 end + def supports_ddl_transactions? + true + end + # Returns the configured supported identifier length supported by PostgreSQL, # or report the default of 63 on PostgreSQL 7.x. def table_alias_length @@ -534,13 +538,13 @@ module ActiveRecord option_string = options.symbolize_keys.sum do |key, value| case key when :owner - " OWNER = '#{value}'" + " OWNER = \"#{value}\"" when :template - " TEMPLATE = #{value}" + " TEMPLATE = \"#{value}\"" when :encoding " ENCODING = '#{value}'" when :tablespace - " TABLESPACE = #{value}" + " TABLESPACE = \"#{value}\"" when :connection_limit " CONNECTION LIMIT = #{value}" else @@ -761,7 +765,8 @@ module ActiveRecord begin execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - rescue ActiveRecord::StatementInvalid + rescue ActiveRecord::StatementInvalid => e + raise e if postgresql_version > 80000 # This is PostgreSQL 7.x, so we have to use a more arcane way of doing it. begin begin_db_transaction @@ -867,7 +872,7 @@ module ActiveRecord end private - # The internal PostgreSQL identifer of the money data type. + # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # Connects to a PostgreSQL server and sets up the adapter depending on the diff --git a/activerecord/lib/active_record/dirty.rb b/activerecord/lib/active_record/dirty.rb index a7d767486c..63bf8c8f5b 100644 --- a/activerecord/lib/active_record/dirty.rb +++ b/activerecord/lib/active_record/dirty.rb @@ -62,7 +62,7 @@ module ActiveRecord changed_attributes.keys end - # Map of changed attrs => [original value, new value] + # Map of changed attrs => [original value, new value]. # person.changes # => {} # person.name = 'bob' # person.changes # => { 'name' => ['bill', 'bob'] } @@ -93,27 +93,27 @@ module ActiveRecord end private - # Map of change attr => original value. + # Map of change <tt>attr => original value</tt>. def changed_attributes @changed_attributes ||= {} end - # Handle *_changed? for method_missing. + # Handle <tt>*_changed?</tt> for +method_missing+. def attribute_changed?(attr) changed_attributes.include?(attr) end - # Handle *_change for method_missing. + # Handle <tt>*_change</tt> for +method_missing+. def attribute_change(attr) [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) end - # Handle *_was for method_missing. + # Handle <tt>*_was</tt> for +method_missing+. def attribute_was(attr) attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end - # Handle *_will_change! for method_missing. + # Handle <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) changed_attributes[attr] = clone_attribute_value(:read_attribute, attr) end @@ -134,7 +134,9 @@ module ActiveRecord def update_with_dirty if partial_updates? - update_without_dirty(changed) + # Serialized attributes should always be written in case they've been + # changed in place. + update_without_dirty(changed | self.class.serialized_attributes.keys) else update_without_dirty end diff --git a/activerecord/lib/active_record/dynamic_finder_match.rb b/activerecord/lib/active_record/dynamic_finder_match.rb new file mode 100644 index 0000000000..b105b919f5 --- /dev/null +++ b/activerecord/lib/active_record/dynamic_finder_match.rb @@ -0,0 +1,40 @@ +module ActiveRecord + class DynamicFinderMatch + def self.match(method) + df_match = self.new(method) + df_match.finder ? df_match : nil + end + + def initialize(method) + @finder = :find_initial + case method.to_s + when /^find_(all_by|by)_([_a-zA-Z]\w*)$/ + @finder = :find_every if $1 == 'all_by' + names = $2 + when /^find_by_([_a-zA-Z]\w*)\!$/ + @bang = true + names = $1 + when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/ + @instantiator = $1 == 'initialize' ? :new : :create + names = $2 + else + @finder = nil + end + @attribute_names = names && names.split('_and_') + end + + attr_reader :finder, :attribute_names, :instantiator + + def finder? + !@finder.nil? && @instantiator.nil? + end + + def instantiator? + @finder == :find_initial && !@instantiator.nil? + end + + def bang? + @bang + end + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 17fb9355c4..622cfc3c3f 100755..100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -515,7 +515,7 @@ class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash) all_loaded_fixtures.update(fixtures_map) - connection.transaction(Thread.current['open_transactions'].to_i == 0) do + connection.transaction(connection.open_transactions.zero?) do fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } fixtures.each { |fixture| fixture.insert_fixtures } @@ -930,7 +930,7 @@ module Test #:nodoc: load_fixtures @@already_loaded_fixtures[self.class] = @loaded_fixtures end - ActiveRecord::Base.send :increment_open_transactions + ActiveRecord::Base.connection.increment_open_transactions ActiveRecord::Base.connection.begin_db_transaction # Load fixtures for every test. else @@ -951,9 +951,9 @@ module Test #:nodoc: end # Rollback changes if a transaction is active. - if use_transactional_fixtures? && Thread.current['open_transactions'] != 0 + if use_transactional_fixtures? && ActiveRecord::Base.connection.open_transactions != 0 ActiveRecord::Base.connection.rollback_db_transaction - Thread.current['open_transactions'] = 0 + ActiveRecord::Base.connection.decrement_open_transactions end ActiveRecord::Base.verify_active_connections! end diff --git a/activerecord/lib/active_record/locale/en-US.yml b/activerecord/lib/active_record/locale/en-US.yml new file mode 100644 index 0000000000..8148f31a81 --- /dev/null +++ b/activerecord/lib/active_record/locale/en-US.yml @@ -0,0 +1,33 @@ +en-US: + activerecord: + errors: + # The values :model, :attribute and :value are always available for interpolation + # The value :count is available when applicable. Can be used for pluralization. + messages: + inclusion: "is not included in the list" + exclusion: "is reserved" + invalid: "is invalid" + confirmation: "doesn't match confirmation" + accepted: "must be accepted" + empty: "can't be empty" + blank: "can't be blank" + too_long: "is too long (maximum is {{count}} characters)" + too_short: "is too short (minimum is {{count}} characters)" + wrong_length: "is the wrong length (should be {{count}} characters)" + taken: "has already been taken" + not_a_number: "is not a number" + greater_than: "must be greater than {{count}}" + greater_than_or_equal_to: "must be greater than or equal to {{count}}" + equal_to: "must be equal to {{count}}" + less_than: "must be less than {{count}}" + less_than_or_equal_to: "must be less than or equal to {{count}}" + odd: "must be odd" + even: "must be even" + # Append your own errors here or at the model/attributes scope. + + models: + # Overrides default messages + + attributes: + # Overrides model and default messages. + diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index e095b3c766..1d843fff28 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -238,6 +238,22 @@ module ActiveRecord # lower than the current schema version: when migrating up, those # never-applied "interleaved" migrations will be automatically applied, and # when migrating down, never-applied "interleaved" migrations will be skipped. + # + # == Timestamped Migrations + # + # By default, Rails generates migrations that look like: + # + # 20080717013526_your_migration_name.rb + # + # The prefix is a generation timestamp (in UTC). + # + # If you'd prefer to use numeric prefixes, you can turn timestamped migrations + # off by setting: + # + # config.active_record.timestamped_migrations = false + # + # In environment.rb. + # class Migration @@verbose = true cattr_accessor :verbose @@ -333,6 +349,27 @@ module ActiveRecord end end + # MigrationProxy is used to defer loading of the actual migration classes + # until they are needed + class MigrationProxy + + attr_accessor :name, :version, :filename + + delegate :migrate, :announce, :write, :to=>:migration + + private + + def migration + @migration ||= load_migration + end + + def load_migration + load(filename) + name.constantize + end + + end + class Migrator#:nodoc: class << self def migrate(migrations_path, target_version = nil) @@ -369,11 +406,17 @@ module ActiveRecord Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix end + def get_all_versions + Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort + end + def current_version - version = Base.connection.select_values( - "SELECT version FROM #{schema_migrations_table_name}" - ).map(&:to_i).max rescue nil - version || 0 + sm_table = schema_migrations_table_name + if Base.connection.table_exists?(sm_table) + get_all_versions.max || 0 + else + 0 + end end def proper_table_name(name) @@ -389,7 +432,7 @@ module ActiveRecord end def current_version - self.class.current_version + migrated.last || 0 end def current_migration @@ -421,17 +464,25 @@ module ActiveRecord runnable.pop if down? && !target.nil? runnable.each do |migration| - Base.logger.info "Migrating to #{migration} (#{migration.version})" + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" # On our way up, we skip migrating the ones we've already migrated - # On our way down, we skip reverting the ones we've never migrated next if up? && migrated.include?(migration.version.to_i) + # On our way down, we skip reverting the ones we've never migrated if down? && !migrated.include?(migration.version.to_i) migration.announce 'never migrated, skipping'; migration.write - else - migration.migrate(@direction) - record_version_state_after_migrating(migration.version) + next + end + + begin + ddl_transaction do + migration.migrate(@direction) + record_version_state_after_migrating(migration.version) + end + rescue => e + canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : "" + raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace end end end @@ -454,11 +505,10 @@ module ActiveRecord raise DuplicateMigrationNameError.new(name.camelize) end - load(file) - - klasses << returning(name.camelize.constantize) do |klass| - class << klass; attr_accessor :version end - klass.version = version + klasses << returning(MigrationProxy.new) do |migration| + migration.name = name.camelize + migration.version = version + migration.filename = file end end @@ -473,17 +523,19 @@ module ActiveRecord end def migrated - sm_table = self.class.schema_migrations_table_name - Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort + @migrated_versions ||= self.class.get_all_versions end private def record_version_state_after_migrating(version) sm_table = self.class.schema_migrations_table_name + @migrated_versions ||= [] if down? + @migrated_versions.delete(version.to_i) Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'") else + @migrated_versions.push(version.to_i).sort! Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')") end end @@ -495,5 +547,14 @@ module ActiveRecord def down? @direction == :down end + + # Wrap the migration in a transaction only if supported by the adapter. + def ddl_transaction(&block) + if Base.connection.supports_ddl_transactions? + Base.transaction { block.call } + else + block.call + end + end end end diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 080e3d0f5e..c99c4beca9 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -103,7 +103,7 @@ module ActiveRecord attr_reader :proxy_scope, :proxy_options [].methods.each do |m| - unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/ + unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty?|any?|respond_to?)/ delegate m, :to => :proxy_found end end @@ -140,6 +140,18 @@ module ActiveRecord @found ? @found.empty? : count.zero? end + def respond_to?(method, include_private = false) + super || @proxy_scope.respond_to?(method, include_private) + end + + def any? + if block_given? + proxy_found.any? { |*block_args| yield(*block_args) } + else + !empty? + end + end + protected def proxy_found @found || load_found diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index 25e0e61c69..b35e407cc1 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -20,7 +20,7 @@ module ActiveRecord # ActiveRecord::Base.observers = Cacher, GarbageCollector # # Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is - # called during startup, and before each development request. + # called during startup, and before each development request. def observers=(*observers) @observers = observers.flatten end @@ -130,11 +130,11 @@ module ActiveRecord # Observers register themselves in the model class they observe, since it is the class that # notifies them of events when they occur. As a side-effect, when an observer is loaded its # corresponding model class is loaded. - # + # # Up to (and including) Rails 2.0.2 observers were instantiated between plugins and - # application initializers. Now observers are loaded after application initializers, + # application initializers. Now observers are loaded after application initializers, # so observed models can make use of extensions. - # + # # If by any chance you are using observed models in the initialization you can still # load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are # singletons and that call instantiates and registers them. @@ -189,7 +189,9 @@ module ActiveRecord def add_observer!(klass) klass.add_observer(self) - klass.class_eval 'def after_find() end' unless klass.method_defined?(:after_find) + if respond_to?(:after_find) && !klass.method_defined?(:after_find) + klass.class_eval 'def after_find() end' + end end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 3f74c03714..935b1939d8 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -109,7 +109,7 @@ module ActiveRecord # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, # and +other_aggregation+ has an options hash assigned to it. def ==(other_aggregation) - name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record + other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record end def sanitized_conditions #:nodoc: diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index ca5591ae35..ffaa41282f 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -37,7 +37,7 @@ module ActiveRecord $queries_executed = [] yield ensure - assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed." + assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}" end def assert_no_queries(&block) diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 3b6835762c..81462a2917 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -66,32 +66,24 @@ module ActiveRecord # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction # depends on or you can raise exceptions in the callbacks to rollback. # - # == Exception handling + # == Exception handling and rolling back # # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you - # should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will - # trigger a ROLLBACK when raised, but not be re-raised by the transaction block. + # should be ready to catch those in your application code. + # + # One exception is the ActiveRecord::Rollback exception, which will trigger a ROLLBACK when raised, + # but not be re-raised by the transaction block. module ClassMethods + # See ActiveRecord::Transactions::ClassMethods for detailed documentation. def transaction(&block) - increment_open_transactions + connection.increment_open_transactions begin - connection.transaction(Thread.current['start_db_transaction'], &block) + connection.transaction(connection.open_transactions == 1, &block) ensure - decrement_open_transactions + connection.decrement_open_transactions end end - - private - def increment_open_transactions #:nodoc: - open = Thread.current['open_transactions'] ||= 0 - Thread.current['start_db_transaction'] = open.zero? - Thread.current['open_transactions'] = open + 1 - end - - def decrement_open_transactions #:nodoc: - Thread.current['open_transactions'] -= 1 - end end def transaction(&block) @@ -99,11 +91,11 @@ module ActiveRecord end def destroy_with_transactions #:nodoc: - transaction { destroy_without_transactions } + with_transaction_returning_status(:destroy_without_transactions) end def save_with_transactions(perform_validation = true) #:nodoc: - rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } } + rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) } end def save_with_transactions! #:nodoc: @@ -126,5 +118,17 @@ module ActiveRecord end raise end + + # Executes +method+ within a transaction and captures its return value as a + # status flag. If the status is true the transaction is committed, otherwise + # a ROLLBACK is issued. In any case the status flag is returned. + def with_transaction_returning_status(method, *args) + status = nil + transaction do + status = send(method, *args) + raise ActiveRecord::Rollback unless status + end + status + end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 1035308aa5..8fe4336bbc 100755..100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -1,5 +1,5 @@ module ActiveRecord - # Raised by save! and create! when the record is invalid. Use the + # Raised by <tt>save!</tt> and <tt>create!</tt> when the record is invalid. Use the # +record+ method to retrieve the record which did not validate. # begin # complex_operation_that_calls_save!_internally @@ -18,69 +18,95 @@ module ActiveRecord # determine whether the object is in a valid state to be saved. See usage example in Validations. class Errors include Enumerable + + class << self + def default_error_messages + ActiveSupport::Deprecation.warn("ActiveRecord::Errors.default_error_messages has been deprecated. Please use I18n.translate('activerecord.errors.messages').") + I18n.translate 'activerecord.errors.messages' + end + end def initialize(base) # :nodoc: @base, @errors = base, {} end - @@default_error_messages = { - :inclusion => "is not included in the list", - :exclusion => "is reserved", - :invalid => "is invalid", - :confirmation => "doesn't match confirmation", - :accepted => "must be accepted", - :empty => "can't be empty", - :blank => "can't be blank", - :too_long => "is too long (maximum is %d characters)", - :too_short => "is too short (minimum is %d characters)", - :wrong_length => "is the wrong length (should be %d characters)", - :taken => "has already been taken", - :not_a_number => "is not a number", - :greater_than => "must be greater than %d", - :greater_than_or_equal_to => "must be greater than or equal to %d", - :equal_to => "must be equal to %d", - :less_than => "must be less than %d", - :less_than_or_equal_to => "must be less than or equal to %d", - :odd => "must be odd", - :even => "must be even" - } - - # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. - cattr_accessor :default_error_messages - - # Adds an error to the base object instead of any particular attribute. This is used # to report errors that don't tie to any specific attribute, but rather to the object # as a whole. These error messages don't get prepended with any field name when iterating - # with each_full, so they should be complete sentences. + # with +each_full+, so they should be complete sentences. def add_to_base(msg) add(:base, msg) end - # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt> + # Adds an error message (+messsage+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt> # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. - # If no +msg+ is supplied, "invalid" is assumed. - def add(attribute, msg = @@default_error_messages[:invalid]) - @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? - @errors[attribute.to_s] << msg + # If no +messsage+ is supplied, :invalid is assumed. + # If +message+ is a Symbol, it will be translated, using the appropriate scope (see translate_error). + def add(attribute, message = nil, options = {}) + message ||= :invalid + message = generate_message(attribute, message, options) if message.is_a?(Symbol) + @errors[attribute.to_s] ||= [] + @errors[attribute.to_s] << message end # Will add an error message to each of the attributes in +attributes+ that is empty. - def add_on_empty(attributes, msg = @@default_error_messages[:empty]) + def add_on_empty(attributes, custom_message = nil) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - is_empty = value.respond_to?("empty?") ? value.empty? : false - add(attr, msg) unless !value.nil? && !is_empty + is_empty = value.respond_to?("empty?") ? value.empty? : false + add(attr, :empty, :default => custom_message) unless !value.nil? && !is_empty end end # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). - def add_on_blank(attributes, msg = @@default_error_messages[:blank]) + def add_on_blank(attributes, custom_message = nil) for attr in [attributes].flatten value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] - add(attr, msg) if value.blank? + add(attr, :blank, :default => custom_message) if value.blank? + end + end + + # Translates an error message in it's default scope (<tt>activerecord.errrors.messages</tt>). + # Error messages are first looked up in <tt>models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>, if it's not there, + # it's looked up in <tt>models.MODEL.MESSAGE</tt> and if that is not there it returns the translation of the + # default message (e.g. <tt>activerecord.errors.messages.MESSAGE</tt>). The translated model name, + # translated attribute name and the value are available for interpolation. + # + # When using inheritence in your models, it will check all the inherited models too, but only if the model itself + # hasn't been found. Say you have <tt>class Admin < User; end</tt> and you wanted the translation for the <tt>:blank</tt> + # error +message+ for the <tt>title</tt> +attribute+, it looks for these translations: + # + # <ol> + # <li><tt>activerecord.errors.models.admin.attributes.title.blank</tt></li> + # <li><tt>activerecord.errors.models.admin.blank</tt></li> + # <li><tt>activerecord.errors.models.user.attributes.title.blank</tt></li> + # <li><tt>activerecord.errors.models.user.blank</tt></li> + # <li><tt>activerecord.errors.messages.blank</tt></li> + # <li>any default you provided through the +options+ hash (in the activerecord.errors scope)</li> + # </ol> + def generate_message(attribute, message = :invalid, options = {}) + + defaults = @base.class.self_and_descendents_from_active_record.map do |klass| + [ :"models.#{klass.name.underscore}.attributes.#{attribute}.#{message}", + :"models.#{klass.name.underscore}.#{message}" ] end + + defaults << options.delete(:default) + defaults = defaults.compact.flatten << :"messages.#{message}" + + model_name = @base.class.name + key = defaults.shift + value = @base.respond_to?(attribute) ? @base.send(attribute) : nil + + options = { :default => defaults, + :model => @base.class.human_name, + :attribute => @base.class.human_attribute_name(attribute.to_s), + :value => value, + :scope => [:activerecord, :errors] + }.merge(options) + + I18n.translate(key, options) end # Returns true if the specified +attribute+ has errors associated with it. @@ -97,7 +123,7 @@ module ActiveRecord !@errors[attribute.to_s].nil? end - # Returns nil, if no errors are associated with the specified +attribute+. + # Returns +nil+, if no errors are associated with the specified +attribute+. # Returns the error message, if one error is associated with the specified +attribute+. # Returns an array of error messages, if more than one error is associated with the specified +attribute+. # @@ -118,7 +144,7 @@ module ActiveRecord alias :[] :on - # Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute). + # Returns errors assigned to the base object through +add_to_base+ according to the normal rules of <tt>on(attribute)</tt>. def on_base on(:base) end @@ -131,15 +157,15 @@ module ActiveRecord # end # # company = Company.create(:address => '123 First St.') - # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # => - # name - is too short (minimum is 5 characters) - # name - can't be blank - # address - can't be blank + # company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } + # # => name - is too short (minimum is 5 characters) + # # name - can't be blank + # # address - can't be blank def each @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } end - # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned + # Yields each full error message added. So <tt>Person.errors.add("first_name", "can't be empty")</tt> will be returned # through iteration as "First name can't be empty". # # class Company < ActiveRecord::Base @@ -148,10 +174,10 @@ module ActiveRecord # end # # company = Company.create(:address => '123 First St.') - # company.errors.each_full{|msg| puts msg } # => - # Name is too short (minimum is 5 characters) - # Name can't be blank - # Address can't be blank + # company.errors.each_full{|msg| puts msg } + # # => Name is too short (minimum is 5 characters) + # # Name can't be blank + # # Address can't be blank def each_full full_messages.each { |msg| yield msg } end @@ -166,22 +192,24 @@ module ActiveRecord # company = Company.create(:address => '123 First St.') # company.errors.full_messages # => # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] - def full_messages + def full_messages(options = {}) full_messages = [] @errors.each_key do |attr| - @errors[attr].each do |msg| - next if msg.nil? + @errors[attr].each do |message| + next unless message if attr == "base" - full_messages << msg + full_messages << message else - full_messages << @base.class.human_attribute_name(attr) + " " + msg + #key = :"activerecord.att.#{@base.class.name.underscore.to_sym}.#{attr}" + attr_name = @base.class.human_attribute_name(attr) + full_messages << attr_name + ' ' + message end end end full_messages - end + end # Returns true if no errors have been added. def empty? @@ -209,13 +237,13 @@ module ActiveRecord # end # # company = Company.create(:address => '123 First St.') - # company.errors.to_xml # => - # <?xml version="1.0" encoding="UTF-8"?> - # <errors> - # <error>Name is too short (minimum is 5 characters)</error> - # <error>Name can't be blank</error> - # <error>Address can't be blank</error> - # </errors> + # company.errors.to_xml + # # => <?xml version="1.0" encoding="UTF-8"?> + # # <errors> + # # <error>Name is too short (minimum is 5 characters)</error> + # # <error>Name can't be blank</error> + # # <error>Address can't be blank</error> + # # </errors> def to_xml(options={}) options[:root] ||= "errors" options[:indent] ||= 2 @@ -226,6 +254,7 @@ module ActiveRecord full_messages.each { |msg| e.error(msg) } end end + end @@ -261,7 +290,7 @@ module ActiveRecord # person.errors.on "phone_number" # => "has invalid format" # person.errors.each_full { |msg| puts msg } # # => "Last name can't be empty\n" + - # "Phone number has invalid format" + # # "Phone number has invalid format" # # person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" } # person.save # => true (and person is now saved in the database) @@ -300,7 +329,7 @@ module ActiveRecord :odd => 'odd?', :even => 'even?' }.freeze # Adds a validation method or block to the class. This is useful when - # overriding the +validate+ instance method becomes too unwieldly and + # overriding the +validate+ instance method becomes too unwieldy and # you're looking for more descriptive declaration of your validations. # # This can be done with a symbol pointing to a method: @@ -388,13 +417,15 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_confirmation_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") + record.errors.add(attr_name, :confirmation, :default => configuration[:message]) + end end end @@ -422,7 +453,7 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_acceptance_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" } + configuration = { :on => :save, :allow_nil => true, :accept => "1" } configuration.update(attr_names.extract_options!) db_cols = begin @@ -434,7 +465,9 @@ module ActiveRecord attr_accessor(*names) validates_each(attr_names,configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] + unless value == configuration[:accept] + record.errors.add(attr_name, :accepted, :default => configuration[:message]) + end end end @@ -461,7 +494,7 @@ module ActiveRecord # method, proc or string should return or evaluate to a true or false value. # def validates_presence_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) # can't use validates_each here, because it cannot cope with nonexistent attributes, @@ -509,10 +542,7 @@ module ActiveRecord def validates_length_of(*attrs) # Merge given options with defaults. options = { - :too_long => ActiveRecord::Errors.default_error_messages[:too_long], - :too_short => ActiveRecord::Errors.default_error_messages[:too_short], - :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length], - :tokenizer => lambda {|value| value.split(//)} + :tokenizer => lambda {|value| value.split(//)} }.merge(DEFAULT_VALIDATION_OPTIONS) options.update(attrs.extract_options!.symbolize_keys) @@ -535,15 +565,12 @@ module ActiveRecord when :within, :in raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) - too_short = options[:too_short] % option_value.begin - too_long = options[:too_long] % option_value.end - 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) + record.errors.add(attr, :too_short, :default => options[:too_short], :count => option_value.begin) elsif value.size > option_value.end - record.errors.add(attr, too_long) + record.errors.add(attr, :too_long, :default => options[:too_long], :count => option_value.end) end end when :is, :minimum, :maximum @@ -553,11 +580,13 @@ module ActiveRecord validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } - message = (options[:message] || options[message_options[option]]) % option_value - validates_each(attrs, options) do |record, attr, value| value = options[:tokenizer].call(value) if value.kind_of?(String) - record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] + 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 end end @@ -599,7 +628,7 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_uniqueness_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true } + configuration = { :case_sensitive => true } configuration.update(attr_names.extract_options!) validates_each(attr_names,configuration) do |record, attr_name, value| @@ -617,13 +646,24 @@ module ActiveRecord # class (which has a database table to query from). finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } - if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?) - condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" + is_text_column = finder_class.columns_hash[attr_name.to_s].text? + + if value.nil? + comparison_operator = "IS ?" + elsif is_text_column + comparison_operator = "#{connection.case_sensitive_equality_operator} ?" + value = 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) + condition_sql = "#{sql_attribute} #{comparison_operator}" condition_params = [value] else - # sqlite has case sensitive SELECT query, while MySQL/Postgresql don't. - # Hence, this is needed only for sqlite. - condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}" + condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}" condition_params = [value.downcase] end @@ -640,26 +680,10 @@ module ActiveRecord condition_params << record.send(:id) end - results = finder_class.with_exclusive_scope do - connection.select_all( - construct_finder_sql( - :select => "#{connection.quote_column_name(attr_name)}", - :from => "#{finder_class.quoted_table_name}", - :conditions => [condition_sql, *condition_params] - ) - ) - end - - unless results.length.zero? - found = true - - # As MySQL/Postgres don't have case sensitive SELECT queries, we try to find duplicate - # column in ruby when case sensitive option - if configuration[:case_sensitive] && finder_class.columns_hash[attr_name.to_s].text? - found = results.any? { |a| a[attr_name.to_s] == value } + finder_class.with_exclusive_scope do + if finder_class.exists?([condition_sql, *condition_params]) + record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value) end - - record.errors.add(attr_name, configuration[:message]) if found end end end @@ -689,13 +713,15 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_format_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } + configuration = { :on => :save, :with => nil } configuration.update(attr_names.extract_options!) raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) unless value.to_s =~ configuration[:with] + unless value.to_s =~ configuration[:with] + record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) + end end end @@ -719,7 +745,7 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_inclusion_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) enum = configuration[:in] || configuration[:within] @@ -727,7 +753,9 @@ module ActiveRecord raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value) + unless enum.include?(value) + record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value) + end end end @@ -751,7 +779,7 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_exclusion_of(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) enum = configuration[:in] || configuration[:within] @@ -759,7 +787,9 @@ module ActiveRecord raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value) + if enum.include?(value) + record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value) + end end end @@ -795,12 +825,13 @@ module ActiveRecord # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a true or false value. def validates_associated(*attr_names) - configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save } + configuration = { :on => :save } configuration.update(attr_names.extract_options!) validates_each(attr_names, configuration) do |record, attr_name, value| - record.errors.add(attr_name, configuration[:message]) unless - (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } + unless (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } + record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) + end end end @@ -848,15 +879,15 @@ module ActiveRecord if configuration[:only_integer] unless raw_value.to_s =~ /\A[+-]?\d+\Z/ - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) next end raw_value = raw_value.to_i else - begin + begin raw_value = Kernel.Float(raw_value) rescue ArgumentError, TypeError - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) + record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message]) next end end @@ -864,11 +895,11 @@ module ActiveRecord numericality_options.each do |option| case option when :odd, :even - record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] + record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message]) + end else - message = configuration[:message] || ActiveRecord::Errors.default_error_messages[option] - message = message % configuration[option] if configuration[option] - record.errors.add(attr_name, message) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] + record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option]) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] end end end |