diff options
-rw-r--r-- | activerecord/CHANGELOG | 5 | ||||
-rwxr-xr-x | activerecord/lib/active_record/base.rb | 243 |
2 files changed, 157 insertions, 91 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index b3a5ee0249..152e42485d 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,3 +1,8 @@ +*SVN* + +* Document Active Record exceptions. #10444 [Michael Klishin] + + *2.0.1* (December 7th, 2007) * Removed query cache rescue as it could cause code to be run twice (closes #10408) [DHH] diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 643a5fe02a..68f21bc89b 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -3,37 +3,98 @@ require 'yaml' require 'set' module ActiveRecord #:nodoc: - class ActiveRecordError < StandardError #:nodoc: + # Generic ActiveRecord exception class. + class ActiveRecordError < StandardError end + + # Raised when the single-table inheritance mechanism failes to locate the subclass + # (for example due to improper usage of column that +inheritance_column+ points to). class SubclassNotFound < ActiveRecordError #:nodoc: end - class AssociationTypeMismatch < ActiveRecordError #:nodoc: + + # Raised when object assigned to association is of incorrect type. + # + # Example: + # + # class Ticket < ActiveRecord::Base + # has_many :patches + # end + # + # class Patch < ActiveRecord::Base + # belongs_to :ticket + # end + # + # and somewhere in the code: + # + # @ticket.patches << Comment.new(:content => "Please attach tests to your patch.") + # @ticket.save + class AssociationTypeMismatch < ActiveRecordError end - class SerializationTypeMismatch < ActiveRecordError #:nodoc: + + # Raised when unserialized object's type mismatches one specified for serializable field. + class SerializationTypeMismatch < ActiveRecordError end - class AdapterNotSpecified < ActiveRecordError # :nodoc: + + # Raised when adapter not specified on connection (or configuration file config/database.yml misses adapter field). + class AdapterNotSpecified < ActiveRecordError end - class AdapterNotFound < ActiveRecordError # :nodoc: + + # Raised when ActiveRecord cannot find database adapter specified in config/database.yml or programmatically. + class AdapterNotFound < ActiveRecordError end - class ConnectionNotEstablished < ActiveRecordError #:nodoc: + + # Raised when connection to the database could not been established (for example when connection= is given a nil object). + class ConnectionNotEstablished < ActiveRecordError end - class RecordNotFound < ActiveRecordError #:nodoc: + + # Raised when ActiveRecord cannot find record by given id or set of ids. + class RecordNotFound < ActiveRecordError end - class RecordNotSaved < ActiveRecordError #:nodoc: + + # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be + # saved because record is invalid. + class RecordNotSaved < ActiveRecordError end - class StatementInvalid < ActiveRecordError #:nodoc: + + # Raised when SQL statement cannot be executed by the database (for example, it's often the case for MySQL when Ruby driver used is too old). + class StatementInvalid < ActiveRecordError end - class PreparedStatementInvalid < ActiveRecordError #:nodoc: + + # Raised when number of bind variables in statement given to :condition key (for example, when using +find+ method) + # does not match number of expected variables. + # + # Example: + # + # Location.find :all, :conditions => ["lat = ? AND lng = ?", 53.7362] + # + # in example above two placeholders are given but only one variable to fill them. + class PreparedStatementInvalid < ActiveRecordError end - class StaleObjectError < ActiveRecordError #:nodoc: + + # Raised on attempt to save stale record. Record is stale when it's being saved in another query after + # instantiation, for example, when two users edit the same wiki page and one starts editing and saves + # the page before the other. + # + # Read more about optimistic locking in +ActiveRecord::Locking+ module RDoc. + class StaleObjectError < ActiveRecordError end - class ConfigurationError < ActiveRecordError #:nodoc: + + # Raised when association is being configured improperly or + # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations. + class ConfigurationError < ActiveRecordError end - class ReadOnlyRecord < ActiveRecordError #:nodoc: + + # Raised on attempt to update record that is instantiated as read only. + class ReadOnlyRecord < ActiveRecordError end - class Rollback < ActiveRecordError #:nodoc: + + # Used by ActiveRecord 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. + class Rollback < ActiveRecordError end - class DangerousAttributeError < ActiveRecordError #:nodoc: + + # Raised when attribute has a name reserved by ActiveRecord (when attribute has name of one of ActiveRecord instance methods). + class DangerousAttributeError < ActiveRecordError end # Raised when you've tried to access a column which wasn't @@ -109,7 +170,7 @@ module ActiveRecord #:nodoc: # # The <tt>authenticate_unsafely</tt> method inserts the parameters directly into the query and is thus susceptible to SQL-injection # attacks if the <tt>user_name</tt> and +password+ parameters come directly from an HTTP request. The <tt>authenticate_safely</tt> and - # <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+ before inserting them in the query, + # <tt>authenticate_safely_simply</tt> both will sanitize the <tt>user_name</tt> and +password+ before inserting them in the query, # which will ensure that an attacker can't escape the query and fake the login (or worse). # # When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth @@ -157,7 +218,7 @@ module ActiveRecord #:nodoc: # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. # Query methods allow you to test whether an attribute value is present. - # + # # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: # @@ -199,7 +260,7 @@ module ActiveRecord #:nodoc: # # # No 'Summer' tag exists # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer") - # + # # # Now the 'Summer' tag does exist # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer") # @@ -362,7 +423,7 @@ module ActiveRecord #:nodoc: # Specifies the format to use when dumping the database schema with Rails' # Rakefile. If :sql, the schema is dumped as (potentially database- - # specific) SQL statements. If :ruby, the schema is dumped as an + # specific) SQL statements. If :ruby, the schema is dumped as an # ActiveRecord::Schema file which can be loaded into any database that # supports migrations. Use :ruby if you want to have different database # adapters for, e.g., your development and test environments. @@ -395,7 +456,7 @@ module ActiveRecord #:nodoc: # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not # include the joined columns. # * <tt>:from</tt>: By default, this is the table name of the class, but can be changed to an alternate table name (or even the name - # of a database view). + # of a database view). # * <tt>:readonly</tt>: Mark the returned records read-only so they cannot be saved or updated. # * <tt>:lock</tt>: An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". # :lock => true gives connection's default exclusive lock, usually "FOR UPDATE". @@ -444,20 +505,20 @@ module ActiveRecord #:nodoc: else find_from_ids(args, options) end end - + # - # Executes a custom sql query against your database and returns all the results. The results will - # be returned as an array with columns requested encapsulated as attributes of the model you call - # this method from. If you call +Product.find_by_sql+ then the results will be returned in a Product + # Executes a custom sql query against your database and returns all the results. The results will + # be returned as an array with columns requested encapsulated as attributes of the model you call + # this method from. If you call +Product.find_by_sql+ then the results will be returned in a Product # object with the attributes you specified in the SQL query. # - # If you call a complicated SQL query which spans multiple tables the columns specified by the - # SELECT will be attributes of the model, whether or not they are columns of the corresponding + # If you call a complicated SQL query which spans multiple tables the columns specified by the + # SELECT will be attributes of the model, whether or not they are columns of the corresponding # table. # - # The +sql+ parameter is a full sql query as a string. It will be called as is, there will be - # no database agnostic conversions performed. This should be a last resort because using, for example, - # MySQL specific terms will lock you to using that particular database engine or require you to + # The +sql+ parameter is a full sql query as a string. It will be called as is, there will be + # no database agnostic conversions performed. This should be a last resort because using, for example, + # MySQL specific terms will lock you to using that particular database engine or require you to # change your call if you switch engines # # ==== Examples @@ -472,15 +533,15 @@ module ActiveRecord #:nodoc: connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } end - # Checks whether a record exists in the database that matches conditions given. These conditions - # can either be a single integer representing a primary key id to be found, or a condition to be + # Checks whether a record exists in the database that matches conditions given. These conditions + # can either be a single integer representing a primary key id to be found, or a condition to be # matched like using ActiveRecord#find. # - # The +id_or_conditions+ parameter can be an Integer or a String if you want to search the primary key - # column of the table for a matching id, or if you're looking to match against a condition you can use + # The +id_or_conditions+ parameter can be an Integer or a String if you want to search the primary key + # column of the table for a matching id, or if you're looking to match against a condition you can use # an Array or a Hash. # - # Possible gotcha: You can't pass in a condition as a string e.g. "name = 'Jamie'", this would be + # Possible gotcha: You can't pass in a condition as a string e.g. "name = 'Jamie'", this would be # sanitized and then queried against the primary key column as "id = 'name = \'Jamie" # # ==== Examples @@ -494,7 +555,7 @@ module ActiveRecord #:nodoc: false end - # Creates an object (or multiple objects) and saves it to the database, if validations pass. + # Creates an object (or multiple objects) and saves it to the database, if validations pass. # The resulting object is returned whether the object was saved successfully to the database or not. # # The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the @@ -527,9 +588,9 @@ module ActiveRecord #:nodoc: # # # Updating one record: # Person.update(15, {:user_name => 'Samuel', :group => 'expert'}) - # + # # # Updating multiple records: - # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} } + # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy"} } # Person.update(people.keys, people.values) def update(id, attributes) if id.is_a?(Array) @@ -545,18 +606,18 @@ module ActiveRecord #:nodoc: # Delete an object (or multiple objects) where the +id+ given matches the primary_key. A SQL +DELETE+ command # is executed on the database which means that no callbacks are fired off running this. This is an efficient method # of deleting records that don't need cleaning up after or other actions to be taken. - # + # # Objects are _not_ instantiated with this method. # # ==== Options - # + # # +id+ Can be either an Integer or an Array of Integers # # ==== Examples # # # Delete a single object # Todo.delete(1) - # + # # # Delete multiple objects # todos = [1,2,3] # Todo.delete(todos) @@ -567,19 +628,19 @@ module ActiveRecord #:nodoc: # Destroy an object (or multiple objects) that has the given id, the object is instantiated first, # therefore all callbacks and filters are fired off before the object is deleted. This method is # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run. - # - # This essentially finds the object (or multiple objects) with the given id, creates a new object + # + # This essentially finds the object (or multiple objects) with the given id, creates a new object # from the attributes, and then calls destroy on it. # # ==== Options - # + # # +id+ Can be either an Integer or an Array of Integers # # ==== Examples # # # Destroy a single object # Todo.destroy(1) - # + # # # Destroy multiple objects # todos = [1,2,3] # Todo.destroy(todos) @@ -593,7 +654,7 @@ module ActiveRecord #:nodoc: # ==== Options # # +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 ]. + # +conditions+ An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. # See conditions in the intro for more info. # +options+ Additional options are :limit and/or :order, see the examples for usage. # @@ -601,12 +662,12 @@ module ActiveRecord #:nodoc: # # # Update all billing objects with the 3 different attributes given # Billing.update_all( "category = 'authorized', approved = 1, author = 'David'" ) - # + # # # Update records that match our conditions # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'" ) # # # Update records that match our conditions but limit it to 5 ordered by date - # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'", + # Billing.update_all( "author = 'David'", "title LIKE '%Rails%'", # :order => 'created_at', :limit => 5 ) def update_all(updates, conditions = nil, options = {}) sql = "UPDATE #{table_name} SET #{sanitize_sql_for_assignment(updates)} " @@ -634,11 +695,11 @@ module ActiveRecord #:nodoc: end # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. - # The use of this method should be restricted to complicated SQL queries that can't be executed + # The use of this method should be restricted to complicated SQL queries that can't be executed # using the ActiveRecord::Calculations class methods. Look into those before using this. # # ==== Options - # + # # +sql+: An SQL statement which should return a count query from the database, see the example below # # ==== Examples @@ -656,15 +717,15 @@ module ActiveRecord #:nodoc: # given by the corresponding value: # # ==== Options - # + # # +id+ The id of the object you wish to update a counter on - # +counters+ An Array of Hashes containing the names of the fields + # +counters+ An Array of Hashes containing the names of the fields # to update as keys and the amount to update the field by as # values - # + # # ==== Examples - # - # # For the Post with id of 5, decrement the comment_count by 1, and + # + # # For the Post with id of 5, decrement the comment_count by 1, and # # increment the action_count by 1 # Post.update_counters 5, :comment_count => -1, :action_count => 1 # # Executes the following SQL: @@ -682,8 +743,8 @@ module ActiveRecord #:nodoc: # Increment a number field by one, usually representing a count. # - # This is used for caching aggregate values, so that they don't need to be computed every time. - # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is + # This is used for caching aggregate values, so that they don't need to be computed every time. + # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is # shown it would have to run an SQL query to find how many posts and comments there are. # # ==== Options @@ -743,14 +804,14 @@ module ActiveRecord #:nodoc: read_inheritable_attribute("attr_protected") end - # Similar to the attr_protected macro, this protects attributes of your model from mass-assignment, + # Similar to the attr_protected macro, this protects attributes of your model from mass-assignment, # such as <tt>new(attributes)</tt> and <tt>attributes=(attributes)</tt> - # however, it does it in the opposite way. This locks all attributes and only allows access to the - # attributes specified. Assignment to attributes not in this list will be ignored and need to be set - # using the direct writer methods instead. This is meant to protect sensitive attributes from being - # overwritten by URL/form hackers. If you'd rather start from an all-open default and restrict + # however, it does it in the opposite way. This locks all attributes and only allows access to the + # attributes specified. Assignment to attributes not in this list will be ignored and need to be set + # using the direct writer methods instead. This is meant to protect sensitive attributes from being + # overwritten by URL/form hackers. If you'd rather start from an all-open default and restrict # attributes as needed, have a look at attr_protected. - # + # # ==== Options # # <tt>*attributes</tt> A comma separated list of symbols that represent columns _not_ to be protected @@ -787,9 +848,9 @@ module ActiveRecord #:nodoc: read_inheritable_attribute("attr_readonly") end - # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, - # then specify the name of that attribute using this method and it will be handled automatically. - # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that + # If you have an attribute that needs to be saved to the database as an object, and retrieved as the same object, + # then specify the name of that attribute using this method and it will be handled automatically. + # The serialization is done through YAML. If +class_name+ is specified, the serialized object must be of that # class on retrieval or +SerializationTypeMismatch+ will be raised. # # ==== Options @@ -988,7 +1049,7 @@ module ActiveRecord #:nodoc: columns.size > 0 rescue ActiveRecord::StatementInvalid false - end + end end end @@ -1121,7 +1182,7 @@ module ActiveRecord #:nodoc: # Overwrite the default class equality method to provide support for association proxies. def ===(object) object.is_a?(self) - end + end # Returns the base AR subclass that this class descends from. If A # extends AR::Base, A.base_class will return A. If B descends from A @@ -1147,7 +1208,7 @@ module ActiveRecord #:nodoc: def find_every(options) records = scoped?(:find, :include) || options[:include] ? - find_with_associations(options) : + find_with_associations(options) : find_by_sql(construct_finder_sql(options)) records.each { |record| record.readonly! } if options[:readonly] @@ -1171,7 +1232,7 @@ module ActiveRecord #:nodoc: find_some(ids, options) end end - + def find_one(id, options) conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] options.update :conditions => "#{quoted_table_name}.#{connection.quote_column_name(primary_key)} = #{quote_value(id,columns_hash[primary_key])}#{conditions}" @@ -1185,7 +1246,7 @@ module ActiveRecord #:nodoc: raise RecordNotFound, "Couldn't find #{name} with ID=#{id}#{conditions}" end end - + def find_some(ids, options) conditions = " AND (#{sanitize_sql(options[:conditions])})" if options[:conditions] ids_list = ids.map { |id| quote_value(id,columns_hash[primary_key]) }.join(',') @@ -1388,7 +1449,7 @@ module ActiveRecord #:nodoc: # It's even possible to use all the additional parameters to find. For example, the full interface for find_all_by_amount # is actually find_all_by_amount(amount, options). # - # This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount) + # This also enables you to initialize a record if it is not found, such as find_or_initialize_by_amount(amount) # or find_or_create_by_user_and_password(user, password). # # Each dynamic finder or initializer/creator is also defined in the class after it is first invoked, so that future @@ -1401,7 +1462,7 @@ module ActiveRecord #:nodoc: super unless all_attributes_exists?(attribute_names) self.class_eval %{ - def self.#{method_id}(*args) + def self.#{method_id}(*args) options = args.last.is_a?(Hash) ? args.pop : {} attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) finder_options = { :conditions => attributes } @@ -1424,24 +1485,24 @@ module ActiveRecord #:nodoc: super unless all_attributes_exists?(attribute_names) self.class_eval %{ - def self.#{method_id}(*args) + def self.#{method_id}(*args) if args[0].is_a?(Hash) attributes = args[0].with_indifferent_access find_attributes = attributes.slice(*[:#{attribute_names.join(',:')}]) else find_attributes = attributes = construct_attributes_from_arguments([:#{attribute_names.join(',:')}], args) end - + options = { :conditions => find_attributes } set_readonly_option!(options) record = find_initial(options) if record.nil? - record = self.new { |r| r.send(:attributes=, attributes, false) } + record = self.new { |r| r.send(:attributes=, attributes, false) } #{'record.save' if instantiator == :create} record else - record + record end end }, __FILE__, __LINE__ @@ -1471,7 +1532,7 @@ module ActiveRecord #:nodoc: def all_attributes_exists?(attribute_names) attribute_names.all? { |name| column_methods_hash.include?(name.to_sym) } - end + end def attribute_condition(argument) case argument @@ -1642,18 +1703,18 @@ module ActiveRecord #:nodoc: scoped_methods = (Thread.current[:scoped_methods] ||= {}) scoped_methods[self] ||= [] end - + def single_threaded_scoped_methods #:nodoc: @scoped_methods ||= [] end - + # pick up the correct scoped_methods version from @@allow_concurrency if @@allow_concurrency alias_method :scoped_methods, :thread_safe_scoped_methods else alias_method :scoped_methods, :single_threaded_scoped_methods end - + def current_scoped_methods #:nodoc: scoped_methods.last end @@ -1826,8 +1887,8 @@ module ActiveRecord #:nodoc: def encode_quoted_value(value) #:nodoc: quoted_value = connection.quote(value) - quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) " - quoted_value + quoted_value = "'#{quoted_value[1..-2].gsub(/\'/, "\\\\'")}'" if quoted_value.include?("\\\'") # (for ruby mode) " + quoted_value end end @@ -1853,7 +1914,7 @@ module ActiveRecord #:nodoc: def id attr_name = self.class.primary_key column = column_for_attribute(attr_name) - + self.class.send(:define_read_method, :id, attr_name, column) # now that the method exists, call it self.send attr_name.to_sym @@ -1889,8 +1950,8 @@ module ActiveRecord #:nodoc: def save create_or_update end - - # Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a + + # Attempts to save the record, but instead of just returning false if it couldn't happen, it raises a # RecordNotSaved exception def save! create_or_update || raise(RecordNotSaved) @@ -1951,7 +2012,7 @@ module ActiveRecord #:nodoc: self.attributes = attributes save end - + # Updates an object just like Base.update_attributes but calls save! instead of save so an exception is raised if the record is invalid. def update_attributes!(attributes) self.attributes = attributes @@ -2022,7 +2083,7 @@ module ActiveRecord #:nodoc: # matching the attribute names (which again matches the column names). Sensitive attributes can be protected # from this form of mass-assignment by using the +attr_protected+ macro. Or you can alternatively # specify which attributes *can* be accessed with the +attr_accessible+ macro. Then all the - # attributes not included in that won't be allowed to be mass-assigned. + # attributes not included in that won't be allowed to be mass-assigned. def attributes=(new_attributes, guard_protected_attributes = true) return if new_attributes.nil? attributes = new_attributes.dup @@ -2030,7 +2091,7 @@ module ActiveRecord #:nodoc: multi_parameter_attributes = [] 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) end @@ -2042,7 +2103,7 @@ module ActiveRecord #:nodoc: # Returns a hash of all the attributes with their names as keys and clones of their objects as values. def attributes(options = nil) attributes = clone_attributes :read_attribute - + if options.nil? attributes else @@ -2103,8 +2164,8 @@ module ActiveRecord #:nodoc: # Returns true if the +comparison_object+ is the same object, or is of the same type and has the same id. def ==(comparison_object) comparison_object.equal?(self) || - (comparison_object.instance_of?(self.class) && - comparison_object.id == id && + (comparison_object.instance_of?(self.class) && + comparison_object.id == id && !comparison_object.new_record?) end |