diff options
Diffstat (limited to 'activerecord/lib')
18 files changed, 350 insertions, 224 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e2dc883b1b..301b3a3b58 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -22,7 +22,7 @@ module ActiveRecord through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } - super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '}?") + super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -51,6 +51,12 @@ module ActiveRecord end end + class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc: + def initialize(reflection) + super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.") + end + end + class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: def initialize(reflection) super("Can not eagerly load the polymorphic association #{reflection.name.inspect}") @@ -65,7 +71,7 @@ module ActiveRecord # See ActiveRecord::Associations::ClassMethods for documentation. module Associations # :nodoc: - # These classes will be loaded when associatoins are created. + # These classes will be loaded when associations are created. # So there is no need to eager load them. autoload :AssociationCollection, 'active_record/associations/association_collection' autoload :AssociationProxy, 'active_record/associations/association_proxy' @@ -786,11 +792,7 @@ module ActiveRecord # 'ORDER BY p.first_name' def has_many(association_id, options = {}, &extension) reflection = create_has_many_reflection(association_id, options, &extension) - configure_dependency_for_has_many(reflection) - - add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false - add_multiple_associated_save_callbacks(reflection.name) add_association_callbacks(reflection.name, reflection.options) if options[:through] @@ -872,10 +874,10 @@ module ActiveRecord # [:source] # Specifies the source association name used by <tt>has_one :through</tt> queries. Only use it if the name cannot be # inferred from the association. <tt>has_one :favorite, :through => :favorites</tt> will look for a - # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. + # <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given. # [:source_type] # Specifies type of the source association used by <tt>has_one :through</tt> queries where the source - # association is a polymorphic +belongs_to+. + # association is a polymorphic +belongs_to+. # [:readonly] # If true, the associated object is readonly through the association. # [:validate] @@ -898,22 +900,9 @@ module ActiveRecord association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation) else reflection = create_has_one_reflection(association_id, options) - - method_name = "has_one_after_save_for_#{reflection.name}".to_sym - define_method(method_name) do - association = association_instance_get(reflection.name) - if association && (new_record? || association.new_record? || association[reflection.primary_key_name] != id) - association[reflection.primary_key_name] = id - association.save(true) - end - end - after_save method_name - - add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true association_accessor_methods(reflection, HasOneAssociation) association_constructor_method(:build, reflection, HasOneAssociation) association_constructor_method(:create, reflection, HasOneAssociation) - configure_dependency_for_has_one(reflection) end end @@ -1006,47 +995,15 @@ module ActiveRecord if reflection.options[:polymorphic] association_accessor_methods(reflection, BelongsToPolymorphicAssociation) - - method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym - define_method(method_name) do - association = association_instance_get(reflection.name) - if association && association.target - if association.new_record? - association.save(true) - end - - if association.updated? - self[reflection.primary_key_name] = association.id - self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s - end - end - end - before_save method_name else association_accessor_methods(reflection, BelongsToAssociation) association_constructor_method(:build, reflection, BelongsToAssociation) association_constructor_method(:create, reflection, BelongsToAssociation) - - method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym - define_method(method_name) do - if association = association_instance_get(reflection.name) - if association.new_record? - association.save(true) - end - - if association.updated? - self[reflection.primary_key_name] = association.id - end - end - end - before_save method_name end # Create the callbacks to update counter cache if options[:counter_cache] - cache_column = options[:counter_cache] == true ? - "#{self.to_s.demodulize.underscore.pluralize}_count" : - options[:counter_cache] + cache_column = reflection.counter_cache_column method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym define_method(method_name) do @@ -1067,8 +1024,6 @@ module ActiveRecord ) end - add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true - configure_dependency_for_belongs_to(reflection) end @@ -1234,9 +1189,6 @@ module ActiveRecord # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' def has_and_belongs_to_many(association_id, options = {}, &extension) reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - - add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false - add_multiple_associated_save_callbacks(reflection.name) collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) # Don't use a before_destroy callback since users' before_destroy @@ -1358,70 +1310,6 @@ module ActiveRecord end end - def add_single_associated_validation_callbacks(association_name) - method_name = "validate_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - if association = association_instance_get(association_name) - errors.add association_name unless association.target.nil? || association.valid? - end - end - - validate method_name - end - - def add_multiple_associated_validation_callbacks(association_name) - method_name = "validate_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - association = association_instance_get(association_name) - - if association - if new_record? - association - elsif association.loaded? - association.select { |record| record.new_record? } - else - association.target.select { |record| record.new_record? } - end.each do |record| - errors.add association_name unless record.valid? - end - end - end - - validate method_name - end - - def add_multiple_associated_save_callbacks(association_name) - method_name = "before_save_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - @new_record_before_save = new_record? - true - end - before_save method_name - - method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - association = association_instance_get(association_name) - - records_to_save = if @new_record_before_save - association - elsif association && association.loaded? - association.select { |record| record.new_record? } - elsif association && !association.loaded? - association.target.select { |record| record.new_record? } - else - [] - end - records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank? - - # reconstruct the SQL queries now that we know the owner's id - association.send(:construct_sql) if association.respond_to?(:construct_sql) - end - - # Doesn't use after_save as that would save associations added in after_create/after_update twice - after_create method_name - after_update method_name - end - def association_constructor_method(constructor, reflection, association_proxy_class) define_method("#{constructor}_#{reflection.name}") do |*params| attributees = params.first unless params.empty? @@ -1642,6 +1530,10 @@ module ActiveRecord options[:extend] = create_extension_modules(association_id, extension, options[:extend]) reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self) + + if reflection.association_foreign_key == reflection.primary_key_name + raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(reflection) + end reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name)) diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index a5cc3bf091..af9ce3dfb2 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -28,12 +28,12 @@ module ActiveRecord load_target.size end - def insert_record(record, force=true) + def insert_record(record, force = true, validate = true) if record.new_record? if force record.save! else - return false unless record.save + return false unless record.save(validate) end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 3348079e9d..a2cbabfe0c 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -56,9 +56,9 @@ module ActiveRecord "#{@reflection.name}_count" end - def insert_record(record) + def insert_record(record, force = false, validate = true) set_belongs_to_association_for(record) - record.save + force ? record.save! : record.save(validate) end # Deletes the records according to the <tt>:dependent</tt> option. diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 2eeeb28d1f..1c091e7d5a 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -23,8 +23,8 @@ module ActiveRecord end # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and - # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero - # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length. + # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero, + # and you need to fetch that collection afterwards, it'll take one fewer SELECT query if you use #length. def size return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter? return @target.size if loaded? @@ -47,12 +47,12 @@ module ActiveRecord options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? end - def insert_record(record, force=true) + def insert_record(record, force = true, validate = true) if record.new_record? if force record.save! else - return false unless record.save + return false unless record.save(validate) end end through_reflection = @reflection.through_reflection @@ -150,7 +150,7 @@ module ActiveRecord end else reflection_primary_key = @reflection.source_reflection.primary_key_name - source_primary_key = @reflection.klass.primary_key + source_primary_key = @reflection.through_reflection.klass.primary_key if @reflection.source_reflection.options[:as] polymorphic_join = "AND %s.%s = %s" % [ @reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type", diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 960323004d..b92cbbdeab 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -29,8 +29,17 @@ module ActiveRecord unless @target.nil? || @target == obj if dependent? && !dont_save - @target.destroy unless @target.new_record? - @owner.clear_association_cache + case @reflection.options[:dependent] + when :delete + @target.delete unless @target.new_record? + @owner.clear_association_cache + when :destroy + @target.destroy unless @target.new_record? + @owner.clear_association_cache + when :nullify + @target[@reflection.primary_key_name] = nil + @target.save unless @owner.new_record? || @target.new_record? + end else @target[@reflection.primary_key_name] = nil @target.save unless @owner.new_record? || @target.new_record? diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 680b41518c..1c3d0567c1 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -125,79 +125,63 @@ module ActiveRecord # post.author.name = '' # post.save(false) # => true module AutosaveAssociation + ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many } + def self.included(base) base.class_eval do + base.extend(ClassMethods) alias_method_chain :reload, :autosave_associations - alias_method_chain :save, :autosave_associations - alias_method_chain :save!, :autosave_associations - alias_method_chain :valid?, :autosave_associations - %w{ has_one belongs_to has_many has_and_belongs_to_many }.each do |type| + ASSOCIATION_TYPES.each do |type| base.send("valid_keys_for_#{type}_association") << :autosave end end end - # Saves the parent, <tt>self</tt>, and any loaded autosave associations. - # In addition, it destroys all children that were marked for destruction - # with mark_for_destruction. - # - # This all happens inside a transaction, _if_ the Transactions module is included into - # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. - def save_with_autosave_associations(perform_validation = true) - returning(save_without_autosave_associations(perform_validation)) do |valid| - if valid - self.class.reflect_on_all_autosave_associations.each do |reflection| - if (association = association_instance_get(reflection.name)) && association.loaded? - if association.is_a?(Array) - association.proxy_target.each do |child| - child.marked_for_destruction? ? child.destroy : child.save(perform_validation) - end - else - association.marked_for_destruction? ? association.destroy : association.save(perform_validation) - end - end + module ClassMethods + private + + # def belongs_to(name, options = {}) + # super + # add_autosave_association_callbacks(reflect_on_association(name)) + # end + ASSOCIATION_TYPES.each do |type| + module_eval %{ + def #{type}(name, options = {}) + super + add_autosave_association_callbacks(reflect_on_association(name)) end - end + } end - end - # Attempts to save the record just like save_with_autosave_associations but - # will raise a RecordInvalid exception instead of returning false if the - # record is not valid. - def save_with_autosave_associations! - if valid_with_autosave_associations? - save_with_autosave_associations(false) || raise(RecordNotSaved) - else - raise RecordInvalid.new(self) - end - end + # Adds a validate and save callback for the association as specified by + # the +reflection+. + def add_autosave_association_callbacks(reflection) + save_method = "autosave_associated_records_for_#{reflection.name}" + validation_method = "validate_associated_records_for_#{reflection.name}" + validate validation_method - # Returns whether or not the parent, <tt>self</tt>, and any loaded autosave associations are valid. - def valid_with_autosave_associations? - if valid_without_autosave_associations? - self.class.reflect_on_all_autosave_associations.all? do |reflection| - if (association = association_instance_get(reflection.name)) && association.loaded? - if association.is_a?(Array) - association.proxy_target.all? { |child| autosave_association_valid?(reflection, child) } - else - autosave_association_valid?(reflection, association) - end - else - true # association not loaded yet, so it should be valid + case reflection.macro + when :has_many, :has_and_belongs_to_many + before_save :before_save_collection_association + + define_method(save_method) { save_collection_association(reflection) } + # Doesn't use after_save as that would save associations added in after_create/after_update twice + after_create save_method + after_update save_method + + define_method(validation_method) { validate_collection_association(reflection) } + else + case reflection.macro + when :has_one + define_method(save_method) { save_has_one_association(reflection) } + after_save save_method + when :belongs_to + define_method(save_method) { save_belongs_to_association(reflection) } + before_save save_method end + define_method(validation_method) { validate_single_association(reflection) } end - else - false # self was not valid - end - end - - # Returns whether or not the association is valid and applies any errors to the parent, <tt>self</tt>, if it wasn't. - def autosave_association_valid?(reflection, association) - returning(association.valid?) do |valid| - association.errors.each do |attribute, message| - errors.add "#{reflection.name}_#{attribute}", message - end unless valid end end @@ -221,5 +205,145 @@ module ActiveRecord def marked_for_destruction? @marked_for_destruction end + + private + + # Returns the record for an association collection that should be validated + # or saved. If +autosave+ is +false+ only new records will be returned, + # unless the parent is/was a new record itself. + def associated_records_to_validate_or_save(association, new_record, autosave) + if new_record + association + elsif association.loaded? + autosave ? association : association.select { |record| record.new_record? } + else + autosave ? association.target : association.target.select { |record| record.new_record? } + end + end + + # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is + # turned on for the association specified by +reflection+. + def validate_single_association(reflection) + if reflection.options[:validate] == true || reflection.options[:autosave] == true + if (association = association_instance_get(reflection.name)) && !association.target.nil? + association_valid?(reflection, association) + end + end + end + + # Validate the associated records if <tt>:validate</tt> or + # <tt>:autosave</tt> is turned on for the association specified by + # +reflection+. + def validate_collection_association(reflection) + if reflection.options[:validate] != false && association = association_instance_get(reflection.name) + if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) + records.each { |record| association_valid?(reflection, record) } + end + end + end + + # Returns whether or not the association is valid and applies any errors to + # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> + # enabled records if they're marked_for_destruction?. + def association_valid?(reflection, association) + unless valid = association.valid? + if reflection.options[:autosave] + unless association.marked_for_destruction? + association.errors.each do |attribute, message| + attribute = "#{reflection.name}_#{attribute}" + errors.add(attribute, message) unless errors.on(attribute) + end + end + else + errors.add(reflection.name) + end + end + valid + end + + # Is used as a before_save callback to check while saving a collection + # association whether or not the parent was a new record before saving. + def before_save_collection_association + @new_record_before_save = new_record? + true + end + + # Saves any new associated records, or all loaded autosave associations if + # <tt>:autosave</tt> is enabled on the association. + # + # In addition, it destroys all children that were marked for destruction + # with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_collection_association(reflection) + if association = association_instance_get(reflection.name) + autosave = reflection.options[:autosave] + + if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) + records.each do |record| + if autosave && record.marked_for_destruction? + record.destroy + elsif @new_record_before_save || record.new_record? + if autosave + association.send(:insert_record, record, false, false) + else + association.send(:insert_record, record) + end + elsif autosave + record.save(false) + end + end + end + + # reconstruct the SQL queries now that we know the owner's id + association.send(:construct_sql) if association.respond_to?(:construct_sql) + end + end + + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled + # on the association. + # + # In addition, it will destroy the association if it was marked for + # destruction with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_has_one_association(reflection) + if association = association_instance_get(reflection.name) + if reflection.options[:autosave] && association.marked_for_destruction? + association.destroy + elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || reflection.options[:autosave] + association[reflection.primary_key_name] = id + association.save(false) + end + end + end + + # Saves the associated record if it's new or <tt>:autosave</tt> is enabled + # on the association. + # + # In addition, it will destroy the association if it was marked for + # destruction with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_belongs_to_association(reflection) + if association = association_instance_get(reflection.name) + if reflection.options[:autosave] && association.marked_for_destruction? + association.destroy + else + association.save(false) if association.new_record? || reflection.options[:autosave] + + if association.updated? + self[reflection.primary_key_name] = association.id + # TODO: Removing this code doesn't seem to matter… + if reflection.options[:polymorphic] + self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s + end + end + end + end + end end end
\ No newline at end of file diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 55ab1facf2..75f84ef2ed 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -737,12 +737,12 @@ module ActiveRecord #:nodoc: # ==== Parameters # # * +id+ - This should be the id or an array of ids to be updated. - # * +attributes+ - This should be a Hash of attributes to be set on the object, or an array of Hashes. + # * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes. # # ==== Examples # # # Updating one record: - # Person.update(15, { :user_name => 'Samuel', :group => 'expert' }) + # Person.update(15, :user_name => 'Samuel', :group => 'expert') # # # Updating multiple records: # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } @@ -1537,7 +1537,7 @@ module ActiveRecord #:nodoc: end def reverse_sql_order(order_query) - reversed_query = order_query.split(/,/).each { |s| + reversed_query = order_query.to_s.split(/,/).each { |s| if s.match(/\s(asc|ASC)$/) s.gsub!(/\s(asc|ASC)$/, ' DESC') elsif s.match(/\s(desc|DESC)$/) @@ -1690,7 +1690,7 @@ module ActiveRecord #:nodoc: def construct_finder_sql(options) scope = scope(:find) sql = "SELECT #{options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))} " - sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} " + sql << "FROM #{options[:from] || (scope && scope[:from]) || quoted_table_name} " add_joins!(sql, options[:joins], scope) add_conditions!(sql, options[:conditions], scope) @@ -1754,12 +1754,12 @@ module ActiveRecord #:nodoc: def add_group!(sql, group, having, scope = :auto) if group sql << " GROUP BY #{group}" - sql << " HAVING #{having}" if having + sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having else scope = scope(:find) if :auto == scope if scope && (scoped_group = scope[:group]) sql << " GROUP BY #{scoped_group}" - sql << " HAVING #{scoped_having}" if (scoped_having = scope[:having]) + sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having] end end end @@ -2174,7 +2174,7 @@ module ActiveRecord #:nodoc: # Test whether the given method and optional key are scoped. def scoped?(method, key = nil) #:nodoc: if current_scoped_methods && (scope = current_scoped_methods[method]) - !key || scope.has_key?(key) + !key || !scope[key].nil? end end diff --git a/activerecord/lib/active_record/calculations.rb b/activerecord/lib/active_record/calculations.rb index b239c03284..f077818d3b 100644 --- a/activerecord/lib/active_record/calculations.rb +++ b/activerecord/lib/active_record/calculations.rb @@ -141,22 +141,30 @@ module ActiveRecord def construct_count_options_from_args(*args) options = {} column_name = :all - + # We need to handle # count() # count(:column_name=:all) # count(options={}) # count(column_name=:all, options={}) + # selects specified by scopes case args.size + when 0 + column_name = scope(:find)[:select] if scope(:find) when 1 - args[0].is_a?(Hash) ? options = args[0] : column_name = args[0] + if args[0].is_a?(Hash) + column_name = scope(:find)[:select] if scope(:find) + options = args[0] + else + column_name = args[0] + end when 2 column_name, options = args else raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}" - end if args.size > 0 - - [column_name, options] + end + + [column_name || :all, options] end def construct_calculation_sql(operation, column_name, options) #:nodoc: @@ -214,13 +222,15 @@ module ActiveRecord end if options[:group] && options[:having] + having = sanitize_sql_for_conditions(options[:having]) + # FrontBase requires identifiers in the HAVING clause and chokes on function calls if connection.adapter_name == 'FrontBase' - options[:having].downcase! - options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias) + having.downcase! + having.gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias) end - sql << " HAVING #{options[:having]} " + sql << " HAVING #{having} " end sql << " ORDER BY #{options[:order]} " if options[:order] diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 901b17124c..aac84cc5f4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -351,5 +351,21 @@ module ActiveRecord retrieve_connection_pool klass.superclass end end + + class ConnectionManagement + def initialize(app) + @app = app + end + + def call(env) + @app.call(env) + ensure + # Don't return connection (and peform implicit rollback) if + # this request is a part of integration test + unless env.key?("rack.test") + ActiveRecord::Base.clear_active_connections! + end + end + end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index cc9c46505f..75420f69aa 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -18,7 +18,7 @@ module ActiveRecord db.busy_timeout(config[:timeout]) unless config[:timeout].nil? - ConnectionAdapters::SQLite3Adapter.new(db, logger) + ConnectionAdapters::SQLite3Adapter.new(db, logger, config) end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 5390f49f04..6077ddcdb6 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -17,9 +17,9 @@ module ActiveRecord # "Downgrade" deprecated sqlite API if SQLite.const_defined?(:Version) - ConnectionAdapters::SQLite2Adapter.new(db, logger) + ConnectionAdapters::SQLite2Adapter.new(db, logger, config) else - ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger) + ConnectionAdapters::DeprecatedSQLiteAdapter.new(db, logger, config) end end end @@ -72,6 +72,11 @@ module ActiveRecord # # * <tt>:database</tt> - Path to the database file. class SQLiteAdapter < AbstractAdapter + def initialize(connection, logger, config) + super(connection, logger) + @config = config + end + def adapter_name #:nodoc: 'SQLite' end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index ff9899d032..7fa7e267d8 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -23,6 +23,16 @@ module ActiveRecord # p2.first_name = "should fail" # p2.save # Raises a ActiveRecord::StaleObjectError # + # Optimistic locking will also check for stale data when objects are destroyed. Example: + # + # p1 = Person.find(1) + # p2 = Person.find(1) + # + # p1.first_name = "Michael" + # p1.save + # + # p2.destroy # Raises a ActiveRecord::StaleObjectError + # # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, # or otherwise apply the business logic needed to resolve the conflict. # @@ -39,6 +49,7 @@ module ActiveRecord base.lock_optimistically = true base.alias_method_chain :update, :lock + base.alias_method_chain :destroy, :lock base.alias_method_chain :attributes_from_column_definition, :lock class << base @@ -98,6 +109,28 @@ module ActiveRecord end end + def destroy_with_lock #:nodoc: + return destroy_without_lock unless locking_enabled? + + unless new_record? + lock_col = self.class.locking_column + previous_value = send(lock_col).to_i + + affected_rows = connection.delete( + "DELETE FROM #{self.class.quoted_table_name} " + + "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " + + "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}", + "#{self.class.name} Destroy" + ) + + unless affected_rows == 1 + raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object" + end + end + + freeze + end + module ClassMethods DEFAULT_LOCKING_COLUMN = 'lock_version' diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 989b2a1ec5..43411dfb55 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -1,11 +1,12 @@ module ActiveRecord module NamedScope - # All subclasses of ActiveRecord::Base have two named \scopes: - # * <tt>all</tt> - which is similar to a <tt>find(:all)</tt> query, and + # All subclasses of ActiveRecord::Base have one named scope: # * <tt>scoped</tt> - which allows for the creation of anonymous \scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt> # # These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing # intermediate values (scopes) around as first-class objects is convenient. + # + # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope. def self.included(base) base.class_eval do extend ClassMethods @@ -88,7 +89,12 @@ module ActiveRecord when Hash options when Proc - options.call(*args) + case parent_scope + when Scope + with_scope(:find => parent_scope.proxy_options) { options.call(*args) } + else + options.call(*args) + end end, &block) end (class << self; self end).instance_eval do @@ -100,7 +106,7 @@ module ActiveRecord end class Scope - attr_reader :proxy_scope, :proxy_options + attr_reader :proxy_scope, :proxy_options, :current_scoped_methods_when_defined NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?).to_set [].methods.each do |m| unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s) @@ -113,6 +119,9 @@ module ActiveRecord def initialize(proxy_scope, options, &block) [options[:extend]].flatten.each { |extension| extend extension } if options[:extend] extend Module.new(&block) if block_given? + unless Scope === proxy_scope + @current_scoped_methods_when_defined = proxy_scope.send(:current_scoped_methods) + end @proxy_scope, @proxy_options = proxy_scope, options.except(:extend) end @@ -168,7 +177,13 @@ module ActiveRecord else with_scope :find => proxy_options, :create => proxy_options[:conditions].is_a?(Hash) ? proxy_options[:conditions] : {} do method = :new if method == :build - proxy_scope.send(method, *args, &block) + if current_scoped_methods_when_defined + with_scope current_scoped_methods_when_defined do + proxy_scope.send(method, *args, &block) + end + else + proxy_scope.send(method, *args, &block) + end end end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index e69bfb1355..2d4c1d5507 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -197,7 +197,7 @@ module ActiveRecord def counter_cache_column if options[:counter_cache] == true - "#{active_record.name.underscore.pluralize}_count" + "#{active_record.name.demodulize.underscore.pluralize}_count" elsif options[:counter_cache] options[:counter_cache] end diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb index de199d30bf..3cc4640f42 100644 --- a/activerecord/lib/active_record/session_store.rb +++ b/activerecord/lib/active_record/session_store.rb @@ -287,8 +287,7 @@ module ActiveRecord def get_session(env, sid) Base.silence do sid ||= generate_sid - session = @@session_class.find_by_session_id(sid) - session ||= @@session_class.new(:session_id => sid, :data => {}) + session = find_session(sid) env[SESSION_RECORD_KEY] = session [sid, session.data] end @@ -296,7 +295,7 @@ module ActiveRecord def set_session(env, sid, session_data) Base.silence do - record = env[SESSION_RECORD_KEY] + record = env[SESSION_RECORD_KEY] ||= find_session(sid) record.data = session_data return false unless record.save @@ -310,5 +309,10 @@ module ActiveRecord return true end + + def find_session(id) + @@session_class.find_by_session_id(id) || + @@session_class.new(:session_id => id, :data => {}) + end end end diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb index 211dd78874..8c6abaaccb 100644 --- a/activerecord/lib/active_record/test_case.rb +++ b/activerecord/lib/active_record/test_case.rb @@ -49,5 +49,18 @@ module ActiveRecord ActiveRecord::Base.clear_all_connections! ActiveRecord::Base.establish_connection(@connection) end + + def with_kcode(kcode) + if RUBY_VERSION < '1.9' + orig_kcode, $KCODE = $KCODE, kcode + begin + yield + ensure + $KCODE = orig_kcode + end + else + yield + end + end end end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 8862bd05d6..e2b596446f 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -720,20 +720,20 @@ module ActiveRecord # class (which has a database table to query from). finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? } - is_text_column = finder_class.columns_hash[attr_name.to_s].text? + column = finder_class.columns_hash[attr_name.to_s] if value.nil? comparison_operator = "IS ?" - elsif is_text_column + elsif column.text? comparison_operator = "#{connection.case_sensitive_equality_operator} ?" - value = value.to_s + value = column.limit ? value.to_s[0, column.limit] : value.to_s else comparison_operator = "= ?" end sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}" - if value.nil? || (configuration[:case_sensitive] || !is_text_column) + if value.nil? || (configuration[:case_sensitive] || !column.text?) condition_sql = "#{sql_attribute} #{comparison_operator}" condition_params = [value] else @@ -1040,6 +1040,11 @@ module ActiveRecord errors.empty? end + # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise. + def invalid? + !valid? + end + # Returns the Errors object that holds all information about attribute error messages. def errors @errors ||= Errors.new(self) diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 6ac4bdc905..809e91897e 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -2,7 +2,7 @@ module ActiveRecord module VERSION #:nodoc: MAJOR = 2 MINOR = 3 - TINY = 0 + TINY = 1 STRING = [MAJOR, MINOR, TINY].join('.') end |