diff options
Diffstat (limited to 'activerecord/lib')
45 files changed, 568 insertions, 980 deletions
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 5a51aaaced..1675127ab0 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -25,7 +25,7 @@ require 'active_support' require 'active_support/rails' require 'active_model' require 'arel' -require 'active_record_deprecated_finders' +require 'active_record/deprecated_finders' require 'active_record/version' @@ -53,17 +53,6 @@ module ActiveRecord autoload :ReadonlyAttributes autoload :Reflection autoload :Sanitization - - # ActiveRecord::SessionStore depends on the abstract store in Action Pack. - # Eager loading this class would break client code that eager loads Active - # Record standalone. - # - # Note that the Rails application generator creates an initializer specific - # for setting the session store. Thus, albeit in theory this autoload would - # not be thread-safe, in practice it is because if the application uses this - # session store its autoload happens at boot time. - autoload :SessionStore - autoload :Schema autoload :SchemaDumper autoload :SchemaMigration @@ -160,6 +149,15 @@ module ActiveRecord autoload :TestCase autoload :TestFixtures, 'active_record/fixtures' + + def self.eager_load! + super + ActiveRecord::Locking.eager_load! + ActiveRecord::Scoping.eager_load! + ActiveRecord::Associations.eager_load! + ActiveRecord::AttributeMethods.eager_load! + ActiveRecord::ConnectionAdapters.eager_load! + end end ActiveSupport.on_load(:active_record) do diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index f5ee4f3ebe..d6b8552e6e 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1094,12 +1094,14 @@ module ActiveRecord # [: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. If set to - # <tt>:restrict</tt> an error will be added to the object, preventing its deletion, if any associated - # objects are present. + # Controls what happens to the associated objects when + # their owner is destroyed: + # + # * <tt>:destroy</tt> causes all the associated objects to also be destroyed + # * <tt>:delete_all</tt> causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute) + # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects # # If using with the <tt>:through</tt> option, the association on the join model must be # a +belongs_to+, and the records which get deleted are the join records, rather than @@ -1135,7 +1137,9 @@ module ActiveRecord # [:autosave] # If true, always save the associated objects or destroy them if marked for destruction, # when saving the parent object. If false, never save or destroy the associated objects. - # By default, only save associated objects that are new records. + # By default, only save associated objects that are new records. This option is implemented as a + # before_save callback. Because callbacks are run in the order they are defined, associated objects + # may need to be explicitly saved in any user-defined before_save callbacks. # # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:inverse_of] @@ -1203,11 +1207,14 @@ module ActiveRecord # 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. # [: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+. - # If set to <tt>:restrict</tt>, an error will be added to the object, preventing its deletion, if an - # associated object is present. + # Controls what happens to the associated object when + # its owner is destroyed: + # + # * <tt>:destroy</tt> causes the associated object to also be destroyed + # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute) + # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. + # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record + # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object # [: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 diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 4db7038d2e..9f47e7e631 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -176,7 +176,7 @@ module ActiveRecord def creation_attributes attributes = {} - if reflection.macro.in?([:has_one, :has_many]) && !options[:through] + if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index ddfc6f6c05..75f72c1a46 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -2,6 +2,11 @@ module ActiveRecord # = Active Record Belongs To Associations module Associations class BelongsToAssociation < SingularAssociation #:nodoc: + + def handle_dependency + target.send(options[:dependent]) if load_target + end + def replace(record) raise_on_type_mismatch(record) if record diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index c3f32b5ed9..1df876bf62 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -39,6 +39,7 @@ module ActiveRecord::Associations::Builder def build validate_options define_accessors + configure_dependency if options[:dependent] @reflection = model.create_reflection(macro, name, scope, options, model) super # provides an extension point @reflection @@ -52,70 +53,54 @@ module ActiveRecord::Associations::Builder Association.valid_options end - private - - def validate_options - options.assert_valid_keys(valid_options) - end + def validate_options + options.assert_valid_keys(valid_options) + end - def define_accessors - define_readers - define_writers - end + def define_accessors + define_readers + define_writers + end - def define_readers - name = self.name - mixin.redefine_method(name) do |*params| - association(name).reader(*params) + def define_readers + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}(*args) + association(:#{name}).reader(*args) end - end + CODE + end - def define_writers - name = self.name - mixin.redefine_method("#{name}=") do |value| - association(name).writer(value) + def define_writers + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name}=(value) + association(:#{name}).writer(value) end - end - - def check_valid_dependent!(dependent, valid_options) - unless valid_options.include?(dependent) - valid_options_message = valid_options.map(&:inspect).to_sentence( - words_connector: ', ', two_words_connector: ' or ', last_word_connector: ' or ') + CODE + end - raise ArgumentError, "The :dependent option expects either " \ - "#{valid_options_message} (#{dependent.inspect})" - end + def configure_dependency + unless valid_dependent_options.include? options[:dependent] + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}" end - def dependent_restrict_raises? - ActiveRecord::Base.dependent_restrict_raises == true + if options[:dependent] == :restrict + ActiveSupport::Deprecation.warn( + "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \ + "provides the same functionality." + ) end - def dependent_restrict_deprecation_warning - if dependent_restrict_raises? - msg = "In the next release, `:dependent => :restrict` will not raise a `DeleteRestrictionError`. "\ - "Instead, it will add an error on the model. To fix this warning, make sure your code " \ - "isn't relying on a `DeleteRestrictionError` and then add " \ - "`config.active_record.dependent_restrict_raises = false` to your application config." - ActiveSupport::Deprecation.warn msg + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{macro}_dependent_for_#{name} + association(:#{name}).handle_dependency end - end + CODE - def define_restrict_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - has_one_macro = association(name).reflection.macro == :has_one - if has_one_macro ? !send(name).nil? : send(name).exists? - if dependent_restrict_raises? - raise ActiveRecord::DeleteRestrictionError.new(name) - else - key = has_one_macro ? "one" : "many" - errors.add(:base, :"restrict_dependent_destroy.#{key}", - :record => self.class.human_attribute_name(name).downcase) - return false - end - end - end - end + model.before_destroy "#{macro}_dependent_for_#{name}" + end + + def valid_dependent_options + raise NotImplementedError + end end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index f205a456f7..2f2600b7fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,4 +1,3 @@ - module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: def macro @@ -17,72 +16,51 @@ module ActiveRecord::Associations::Builder reflection = super add_counter_cache_callbacks(reflection) if options[:counter_cache] add_touch_callbacks(reflection) if options[:touch] - configure_dependency reflection end - private - - def add_counter_cache_callbacks(reflection) - cache_column = reflection.counter_cache_column - name = self.name + def add_counter_cache_callbacks(reflection) + cache_column = reflection.counter_cache_column - method_name = "belongs_to_counter_cache_after_create_for_#{name}" - mixin.redefine_method(method_name) do - record = send(name) - record.class.increment_counter(cache_column, record.id) unless record.nil? + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def belongs_to_counter_cache_after_create_for_#{name} + record = #{name} + record.class.increment_counter(:#{cache_column}, record.id) unless record.nil? end - model.after_create(method_name) - method_name = "belongs_to_counter_cache_before_destroy_for_#{name}" - mixin.redefine_method(method_name) do + def belongs_to_counter_cache_before_destroy_for_#{name} unless marked_for_destruction? - record = send(name) - record.class.decrement_counter(cache_column, record.id) unless record.nil? + record = #{name} + record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil? end end - model.before_destroy(method_name) + CODE - model.send(:module_eval, - "#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__ - ) - end + model.after_create "belongs_to_counter_cache_after_create_for_#{name}" + model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}" - def add_touch_callbacks(reflection) - name = self.name - method_name = "belongs_to_touch_after_save_or_destroy_for_#{name}" - touch = options[:touch] + klass = reflection.class_name.safe_constantize + klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) + end - mixin.redefine_method(method_name) do - record = send(name) + def add_touch_callbacks(reflection) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def belongs_to_touch_after_save_or_destroy_for_#{name} + record = #{name} unless record.nil? - if touch == true - record.touch - else - record.touch(touch) - end + record.touch #{options[:touch].inspect if options[:touch] != true} end end + CODE - model.after_save(method_name) - model.after_touch(method_name) - model.after_destroy(method_name) - end - - def configure_dependency - if dependent = options[:dependent] - check_valid_dependent! dependent, [:destroy, :delete] + model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}" + model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}" + model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}" + end - method_name = "belongs_to_dependent_#{dependent}_for_#{name}" - model.send(:class_eval, <<-eoruby, __FILE__, __LINE__ + 1) - def #{method_name} - association = #{name} - association.#{dependent} if association - end - eoruby - model.after_destroy method_name - end - end + def valid_dependent_options + [:destroy, :delete] + end end end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index 3fb0a57450..1b382f7285 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,4 +1,3 @@ - module ActiveRecord::Associations::Builder class CollectionAssociation < Association #:nodoc: CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] @@ -34,53 +33,53 @@ module ActiveRecord::Associations::Builder end end - private - - def wrap_block_extension - if block_extension - @extension_module = mod = Module.new(&block_extension) - silence_warnings do - model.parent.const_set(extension_module_name, mod) - end + def wrap_block_extension + if block_extension + @extension_module = mod = Module.new(&block_extension) + silence_warnings do + model.parent.const_set(extension_module_name, mod) + end - prev_scope = @scope + prev_scope = @scope - if prev_scope - @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) } - else - @scope = proc { extending(mod) } - end + if prev_scope + @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) } + else + @scope = proc { extending(mod) } end end + end - def extension_module_name - @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" - end + def extension_module_name + @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension" + end - def define_callback(callback_name) - full_callback_name = "#{callback_name}_for_#{name}" + def define_callback(callback_name) + full_callback_name = "#{callback_name}_for_#{name}" - # TODO : why do i need method_defined? I think its because of the inheritance chain - model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) - model.send("#{full_callback_name}=", Array(options[callback_name.to_sym])) - end + # TODO : why do i need method_defined? I think its because of the inheritance chain + model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name) + model.send("#{full_callback_name}=", Array(options[callback_name.to_sym])) + end - def define_readers - super + def define_readers + super - name = self.name - mixin.redefine_method("#{name.to_s.singularize}_ids") do - association(name).ids_reader + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids + association(:#{name}).ids_reader end - end + CODE + end - def define_writers - super + def define_writers + super - name = self.name - mixin.redefine_method("#{name.to_s.singularize}_ids=") do |ids| - association(name).ids_writer(ids) + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name.to_s.singularize}_ids=(ids) + association(:#{name}).ids_writer(ids) end - end + CODE + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 8df28ad876..bdac02b5bf 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -24,18 +24,16 @@ module ActiveRecord::Associations::Builder end end - private - - def define_destroy_hook - name = self.name - model.send(:include, Module.new { - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy_associations - association(#{name.to_sym.inspect}).delete_all - super - end - RUBY - }) - end + def define_destroy_hook + name = self.name + model.send(:include, Module.new { + class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def destroy_associations + association(:#{name}).delete_all + super + end + RUBY + }) + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 9e60dbc30b..ab8225460a 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,4 +1,3 @@ - module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: def macro @@ -9,52 +8,8 @@ module ActiveRecord::Associations::Builder super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of] end - def build - reflection = super - configure_dependency - reflection + def valid_dependent_options + [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] end - - private - - def configure_dependency - if dependent = options[:dependent] - check_valid_dependent! dependent, [:destroy, :delete_all, :nullify, :restrict] - dependent_restrict_deprecation_warning if dependent == :restrict - - send("define_#{dependent}_dependency_method") - model.before_destroy dependency_method_name - end - end - - def define_destroy_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - send(name).each do |o| - # No point in executing the counter update since we're going to destroy the parent anyway - o.mark_for_destruction - end - - send(name).delete_all - end - end - - def define_delete_all_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - association(name).delete_all - end - end - - def define_nullify_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - send(name).delete_all - end - end - - def dependency_method_name - "has_many_dependent_for_#{name}" - end end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 9c84f1913a..0da564f402 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,4 +1,3 @@ - module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: def macro @@ -15,35 +14,12 @@ module ActiveRecord::Associations::Builder !options[:through] end - def build - reflection = super - configure_dependency unless options[:through] - reflection + def configure_dependency + super unless options[:through] end - private - - def configure_dependency - if dependent = options[:dependent] - check_valid_dependent! dependent, [:destroy, :delete, :nullify, :restrict] - dependent_restrict_deprecation_warning if dependent == :restrict - - send("define_#{dependent}_dependency_method") - model.before_destroy dependency_method_name - end - end - - def define_destroy_dependency_method - name = self.name - mixin.redefine_method(dependency_method_name) do - association(name).delete - end - end - alias :define_delete_dependency_method :define_destroy_dependency_method - alias :define_nullify_dependency_method :define_destroy_dependency_method - - def dependency_method_name - "has_one_dependent_#{options[:dependent]}_for_#{name}" - end + def valid_dependent_options + [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception] + end end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 90a4b7c2ef..6a5830e57f 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -13,22 +13,20 @@ module ActiveRecord::Associations::Builder define_constructors if constructable? end - private - - def define_constructors - name = self.name - - mixin.redefine_method("build_#{name}") do |*params, &block| - association(name).build(*params, &block) + def define_constructors + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def build_#{name}(*args, &block) + association(:#{name}).build(*args, &block) end - mixin.redefine_method("create_#{name}") do |*params, &block| - association(name).create(*params, &block) + def create_#{name}(*args, &block) + association(:#{name}).create(*args, &block) end - mixin.redefine_method("create_#{name}!") do |*params, &block| - association(name).create!(*params, &block) + def create_#{name}!(*args, &block) + association(:#{name}).create!(*args, &block) end - end + CODE + end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index f8c1103ea9..b15df4f308 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -270,12 +270,20 @@ module ActiveRecord load_target.size end - # Returns true if the collection is empty. Equivalent to - # <tt>collection.size.zero?</tt>. If the collection has not been already + # Returns true if the collection is empty. + # + # If the collection has been loaded or the <tt>:counter_sql</tt> option + # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the + # collection has not been loaded, it is equivalent to + # <tt>collection.exists?</tt>. If the collection has not already been # loaded and you are going to fetch the records anyway it is better to # check <tt>collection.length.zero?</tt>. def empty? - size.zero? + if loaded? || options[:counter_sql] + size.zero? + else + !scope.exists? + end end # Returns true if the collections is not empty. @@ -566,7 +574,7 @@ module ActiveRecord args.shift if args.first.is_a?(Hash) && args.first.empty? collection = fetch_first_or_last_using_find?(args) ? scope : load_target - collection.send(type, *args) + collection.send(type, *args).tap {|it| set_inverse_instance it } end 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 7a363c896e..74864d271f 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -7,6 +7,28 @@ module ActiveRecord # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + def handle_dependency + case options[:dependent] + when :restrict, :restrict_with_exception + raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty? + + when :restrict_with_error + unless empty? + record = klass.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + false + end + + else + if options[:dependent] == :destroy + # No point in executing the counter update since we're going to destroy the parent anyway + load_target.each(&:mark_for_destruction) + end + + delete_all + end + end + def insert_record(record, validate = true, raise = false) set_owner_attributes(record) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index c5aed6e26a..dd7da59a86 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -3,23 +3,43 @@ module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: - def replace(record, save = true) - raise_on_type_mismatch(record) if record - load_target - reflection.klass.transaction do - if target && target != record - remove_target!(options[:dependent]) unless target.destroyed? + def handle_dependency + case options[:dependent] + when :restrict, :restrict_with_exception + raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target + + when :restrict_with_error + if load_target + record = klass.human_attribute_name(reflection.name).downcase + owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record) + false end - if record - set_owner_attributes(record) - set_inverse_instance(record) + else + delete + end + end + + def replace(record, save = true) + raise_on_type_mismatch(record) if record + load_target - if owner.persisted? && save && !record.save - nullify_owner_attributes(record) - set_owner_attributes(target) if target - raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + # If target and record are nil, or target is equal to record, + # we don't need to have transaction. + if (target || record) && target != record + reflection.klass.transaction do + remove_target!(options[:dependent]) if target && !target.destroyed? + + if record + set_owner_attributes(record) + set_inverse_instance(record) + + if owner.persisted? && save && !record.save + nullify_owner_attributes(record) + set_owner_attributes(target) if target + raise RecordNotSaved, "Failed to save the new associated #{reflection.name}." + end end end end @@ -51,16 +71,19 @@ module ActiveRecord end def remove_target!(method) - if method.in?([:delete, :destroy]) - target.send(method) - else - nullify_owner_attributes(target) + case method + when :delete + target.delete + when :destroy + target.destroy + else + nullify_owner_attributes(target) - if target.persisted? && owner.persisted? && !target.save - set_owner_attributes(target) - raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + - "The record failed to save when after its foreign key was set to nil." - end + if target.persisted? && owner.persisted? && !target.save + set_owner_attributes(target) + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + + "The record failed to save when after its foreign key was set to nil." + end end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 6992840040..d9989274c8 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -84,11 +84,11 @@ module ActiveRecord def assign_attributes(new_attributes, options = {}) return if new_attributes.blank? - attributes = new_attributes.stringify_keys - multi_parameter_attributes = [] + attributes = new_attributes.stringify_keys + multi_parameter_attributes = [] nested_parameter_attributes = [] - previous_options = @mass_assignment_options - @mass_assignment_options = options + previous_options = @mass_assignment_options + @mass_assignment_options = options unless options[:without_protection] attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role) @@ -97,23 +97,15 @@ module ActiveRecord attributes.each do |k, v| if k.include?("(") multi_parameter_attributes << [ k, v ] - elsif respond_to?("#{k}=") - if v.is_a?(Hash) - nested_parameter_attributes << [ k, v ] - else - send("#{k}=", v) - end + elsif v.is_a?(Hash) + nested_parameter_attributes << [ k, v ] else - raise(UnknownAttributeError, "unknown attribute: #{k}") + _assign_attribute(k, v) end end - # assign any deferred nested attributes after the base attributes have been set - nested_parameter_attributes.each do |k,v| - send("#{k}=", v) - end - - assign_multiparameter_attributes(multi_parameter_attributes) + assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty? + assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? ensure @mass_assignment_options = previous_options end @@ -130,6 +122,21 @@ module ActiveRecord private + def _assign_attribute(k, v) + public_send("#{k}=", v) + rescue NoMethodError + if respond_to?("#{k}=") + raise + else + raise UnknownAttributeError, "unknown attribute: #{k}" + end + end + + # Assign any deferred nested attributes after the base attributes have been set. + def assign_nested_parameter_attributes(pairs) + pairs.each { |k, v| _assign_attribute(k, v) } + end + # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate @@ -143,19 +150,11 @@ module ActiveRecord ) end - def instantiate_time_object(name, values) - if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name)) - Time.zone.local(*values) - else - Time.time_with_datetime_fallback(self.class.default_timezone, *values) - end - end - def execute_callstack_for_multiparameter_attributes(callstack) errors = [] callstack.each do |name, values_with_empty_parameters| begin - send(name + "=", read_value_from_parameter(name, values_with_empty_parameters)) + send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value) rescue => ex errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name) end @@ -166,74 +165,12 @@ module ActiveRecord end end - def read_value_from_parameter(name, values_hash_from_param) - klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass - if values_hash_from_param.values.all?{|v|v.nil?} - nil - elsif klass == Time - read_time_parameter_value(name, values_hash_from_param) - elsif klass == Date - read_date_parameter_value(name, values_hash_from_param) - else - read_other_parameter_value(klass, name, values_hash_from_param) - end - end - - def read_time_parameter_value(name, values_hash_from_param) - # If column is a :time (and not :date or :timestamp) there is no need to validate if - # there are year/month/day fields - if column_for_attribute(name).type == :time - # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil - {1 => 1970, 2 => 1, 3 => 1}.each do |key,value| - values_hash_from_param[key] ||= value - end - else - # else column is a timestamp, so if Date bits were not provided, error - if missing_parameter = [1,2,3].detect{ |position| !values_hash_from_param.has_key?(position) } - raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter}i)") - end - - # If Date bits were provided but blank, then return nil - return nil if (1..3).any? { |position| values_hash_from_param[position].blank? } - end - - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6) - set_values = (1..max_position).collect{ |position| values_hash_from_param[position] } - # If Time bits are not there, then default to 0 - (3..5).each { |i| set_values[i] = set_values[i].blank? ? 0 : set_values[i] } - instantiate_time_object(name, set_values) - end - - def read_date_parameter_value(name, values_hash_from_param) - return nil if (1..3).any? {|position| values_hash_from_param[position].blank?} - set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]] - begin - Date.new(*set_values) - rescue ArgumentError # if Date.new raises an exception on an invalid date - instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates - end - end - - def read_other_parameter_value(klass, name, values_hash_from_param) - max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param) - values = (1..max_position).collect do |position| - raise "Missing Parameter" if !values_hash_from_param.has_key?(position) - values_hash_from_param[position] - end - klass.new(*values) - end - - def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100) - [values_hash_from_param.keys.max,upper_cap].min - end - def extract_callstack_for_multiparameter_attributes(pairs) attributes = { } - pairs.each do |pair| - multiparameter_name, value = pair + pairs.each do |(multiparameter_name, value)| attribute_name = multiparameter_name.split("(").first - attributes[attribute_name] = {} unless attributes.include?(attribute_name) + attributes[attribute_name] ||= {} parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value) attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value @@ -250,5 +187,100 @@ module ActiveRecord multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i end + class MultiparameterAttribute #:nodoc: + attr_reader :object, :name, :values, :column + + def initialize(object, name, values) + @object = object + @name = name + @values = values + end + + def read_value + return if values.values.compact.empty? + + @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name) + klass = column.klass + + if klass == Time + read_time + elsif klass == Date + read_date + else + read_other(klass) + end + end + + private + + def instantiate_time_object(set_values) + if object.class.send(:create_time_zone_conversion_attribute?, name, column) + Time.zone.local(*set_values) + else + Time.time_with_datetime_fallback(object.class.default_timezone, *set_values) + end + end + + def read_time + # If column is a :time (and not :date or :timestamp) there is no need to validate if + # there are year/month/day fields + if column.type == :time + # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil + { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| + values[key] ||= value + end + else + # else column is a timestamp, so if Date bits were not provided, error + validate_missing_parameters!([1,2,3]) + + # If Date bits were provided but blank, then return nil + return if blank_date_parameter? + end + + max_position = extract_max_param(6) + set_values = values.values_at(*(1..max_position)) + # If Time bits are not there, then default to 0 + (3..5).each { |i| set_values[i] = set_values[i].presence || 0 } + instantiate_time_object(set_values) + end + + def read_date + return if blank_date_parameter? + set_values = values.values_at(1,2,3) + begin + Date.new(*set_values) + rescue ArgumentError # if Date.new raises an exception on an invalid date + instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates + end + end + + def read_other(klass) + max_position = extract_max_param + positions = (1..max_position) + validate_missing_parameters!(positions) + + set_values = values.values_at(*positions) + klass.new(*set_values) + end + + # Checks whether some blank date parameter exists. Note that this is different + # than the validate_missing_parameters! method, since it just checks for blank + # positions instead of missing ones, and does not raise in case one blank position + # exists. The caller is responsible to handle the case of this returning true. + def blank_date_parameter? + (1..3).any? { |position| values[position].blank? } + end + + # If some position is not provided, it errors out a missing parameter exception. + def validate_missing_parameters!(positions) + if missing_parameter = positions.detect { |position| !values.key?(position) } + raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})") + end + end + + def extract_max_param(upper_cap = 100) + [values.keys.max, upper_cap].min + end + end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index a7af086e43..5fd139156c 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -8,7 +8,6 @@ module ActiveRecord extend ActiveSupport::Concern ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] - ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT included do config_attribute :attribute_types_cached_by_default @@ -64,6 +63,8 @@ module ActiveRecord end end + ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT + # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 49ab3ab808..bdda5bc009 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -6,10 +6,46 @@ module ActiveRecord included do # Returns a hash of all the attributes that have been specified for serialization as # keys and their class restriction as values. - class_attribute :serialized_attributes, instance_writer: false + class_attribute :serialized_attributes, instance_accessor: false self.serialized_attributes = {} end + module ClassMethods + # 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. + # + # ==== Parameters + # + # * +attr_name+ - The field name that should be serialized. + # * +class_name+ - Optional, class name that the object type should be equal to. + # + # ==== Example + # # Serialize a preferences attribute + # class User < ActiveRecord::Base + # serialize :preferences + # end + def serialize(attr_name, class_name = Object) + include Behavior + + coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } + class_name + else + Coders::YAMLColumn.new(class_name) + end + + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy + # has its own hash of own serialized attributes + self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) + end + end + + def serialized_attributes + ActiveSupport::Deprecation.warn("Instance level serialized_attributes method is deprecated, please use class level method.") + defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes + end + class Type # :nodoc: def initialize(column) @column = column @@ -44,71 +80,50 @@ module ActiveRecord end end - module ClassMethods - # 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. - # - # ==== Parameters - # - # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. - # - # ==== Example - # # Serialize a preferences attribute - # class User < ActiveRecord::Base - # serialize :preferences - # end - def serialize(attr_name, class_name = Object) - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name - else - Coders::YAMLColumn.new(class_name) - end + # This is only added to the model when serialize is called, which + # ensures we do not make things slower when serialization is not used. + module Behavior #:nodoc: + extend ActiveSupport::Concern - # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy - # has its own hash of own serialized attributes - self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) - end - - def initialize_attributes(attributes, options = {}) #:nodoc: - serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized - super(attributes, options) + module ClassMethods + def initialize_attributes(attributes, options = {}) + serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized + super(attributes, options) - serialized_attributes.each do |key, coder| - if attributes.key?(key) - attributes[key] = Attribute.new(coder, attributes[key], serialized) + serialized_attributes.each do |key, coder| + if attributes.key?(key) + attributes[key] = Attribute.new(coder, attributes[key], serialized) + end end + + attributes end - attributes - end + private - private + def attribute_cast_code(attr_name) + if serialized_attributes.include?(attr_name) + "v.unserialized_value" + else + super + end + end + end - def attribute_cast_code(attr_name) - if serialized_attributes.include?(attr_name) - "v.unserialized_value" + def type_cast_attribute_for_write(column, value) + if column && coder = self.class.serialized_attributes[column.name] + Attribute.new(coder, value, :unserialized) else super end end - end - - def type_cast_attribute_for_write(column, value) - if column && coder = self.class.serialized_attributes[column.name] - Attribute.new(coder, value, :unserialized) - else - super - end - end - def read_attribute_before_type_cast(attr_name) - if serialized_attributes.include?(attr_name) - super.unserialized_value - else - super + def read_attribute_before_type_cast(attr_name) + if self.class.serialized_attributes.include?(attr_name) + super.unserialized_value + else + super + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index fa5b2ef336..d1e9d2de0e 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -59,11 +59,14 @@ module ActiveRecord unless time.acts_like?(:time) time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time end - time = time.in_time_zone rescue nil if time - changed = read_attribute(:#{attr_name}) != time - write_attribute(:#{attr_name}, original_time) - #{attr_name}_will_change! if changed - @attributes_cache["#{attr_name}"] = time + zoned_time = time && time.in_time_zone rescue nil + rounded_time = round_usec(zoned_time) + rounded_value = round_usec(read_attribute("#{attr_name}")) + if (rounded_value != rounded_time) || (!rounded_value && original_time) + write_attribute("#{attr_name}", original_time) + #{attr_name}_will_change! + @attributes_cache["#{attr_name}"] = zoned_time + end end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) @@ -79,6 +82,12 @@ module ActiveRecord [:datetime, :timestamp].include?(column.type) end end + + private + def round_usec(value) + return unless value + value.change(:usec => 0) + end end end end 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 b0b51f540c..4e1f0e1d62 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,6 +1,12 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements + def initialize + super + @_current_transaction_records = [] + @transaction_joinable = nil + end + # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) @@ -167,31 +173,23 @@ module ActiveRecord def transaction(options = {}) options.assert_valid_keys :requires_new, :joinable - last_transaction_joinable = defined?(@transaction_joinable) ? @transaction_joinable : nil - if options.has_key?(:joinable) - @transaction_joinable = options[:joinable] - else - @transaction_joinable = true - end - requires_new = options[:requires_new] || !last_transaction_joinable - - transaction_open = false - @_current_transaction_records ||= [] + last_transaction_joinable = @transaction_joinable + @transaction_joinable = options.fetch(:joinable, true) + requires_new = options[:requires_new] || !last_transaction_joinable + transaction_open = false begin - if block_given? - if requires_new || open_transactions == 0 - if open_transactions == 0 - begin_db_transaction - elsif requires_new - create_savepoint - end - increment_open_transactions - transaction_open = true - @_current_transaction_records.push([]) + if requires_new || open_transactions == 0 + if open_transactions == 0 + begin_db_transaction + elsif requires_new + create_savepoint end - yield + increment_open_transactions + transaction_open = true + @_current_transaction_records.push([]) end + yield rescue Exception => database_transaction_rollback if transaction_open && !outside_transaction? transaction_open = false @@ -225,7 +223,7 @@ module ActiveRecord @_current_transaction_records.last.concat(save_point_records) end end - rescue Exception => database_transaction_rollback + rescue Exception if open_transactions == 0 rollback_db_transaction rollback_transaction_records(true) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index b9045cf1e7..1445bb3b2f 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -208,7 +208,7 @@ module ActiveRecord # '0.123456' -> 123456 # '1.123456' -> 123456 def microseconds(time) - ((time[:sec_fraction].to_f % 1) * 1_000_000).to_i + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 end def new_date(year, mon, mday) @@ -233,7 +233,7 @@ module ActiveRecord # Doesn't handle time zones. def fast_string_to_time(string) if string =~ Format::ISO_DATETIME - microsec = ($7.to_f * 1_000_000).to_i + microsec = ($7.to_r * 1_000_000).to_i new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 3b0353358a..6bf7af081f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -298,13 +298,6 @@ module ActiveRecord @connection.insert_id end - class Result < ActiveRecord::Result - def initialize(columns, rows, column_types) - super(columns, rows) - @column_types = column_types - end - end - module Fields class Type def type; end @@ -437,7 +430,7 @@ module ActiveRecord } end } - result_set = Result.new(types.keys, result.to_a, types) + result_set = ActiveRecord::Result.new(types.keys, result.to_a, types) result.free else result_set = ActiveRecord::Result.new([], []) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 8e9ce80697..40cd65cce9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -803,13 +803,6 @@ module ActiveRecord Arel::Nodes::BindParam.new "$#{index + 1}" end - class Result < ActiveRecord::Result - def initialize(columns, rows, column_types) - super(columns, rows) - @column_types = column_types - end - end - def exec_query(sql, name = 'SQL', binds = []) log(sql, name, binds) do result = binds.empty? ? exec_no_cache(sql, binds) : @@ -825,7 +818,7 @@ module ActiveRecord } end - ret = Result.new(result.fields, result.values, types) + ret = ActiveRecord::Result.new(result.fields, result.values, types) result.clear return ret end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 57aa47ab61..4fe0013f0f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -380,9 +380,9 @@ module ActiveRecord case field["dflt_value"] when /^null$/i field["dflt_value"] = nil - when /^'(.*)'$/ + when /^'(.*)'$/m field["dflt_value"] = $1.gsub("''", "'") - when /^"(.*)"$/ + when /^"(.*)"$/m field["dflt_value"] = $1.gsub('""', '"') end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 1145d2138c..aad21b8e37 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/hash/indifferent_access' -require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/object/duplicable' require 'thread' module ActiveRecord @@ -82,15 +82,6 @@ module ActiveRecord # The connection handler config_attribute :connection_handler - ## - # :singleton-method: - # Specifies whether or not has_many or has_one association option - # :dependent => :restrict raises an exception. If set to true, the - # ActiveRecord::DeleteRestrictionError exception will be raised - # along with a DEPRECATION WARNING. If set to false, an error would - # be added to the model instead. - config_attribute :dependent_restrict_raises - %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name| config_attribute name, global: true end @@ -182,7 +173,10 @@ module ActiveRecord # # Instantiates a single new object bypassing mass-assignment security # User.new({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true) def initialize(attributes = nil, options = {}) - @attributes = self.class.initialize_attributes(self.class.column_defaults.deep_dup) + defaults = self.class.column_defaults.dup + defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } + + @attributes = self.class.initialize_attributes(defaults) @columns_hash = self.class.column_types.dup init_internals @@ -194,7 +188,7 @@ module ActiveRecord assign_attributes(attributes, options) if attributes yield self if block_given? - run_callbacks :initialize if _initialize_callbacks.any? + run_callbacks :initialize unless _initialize_callbacks.empty? end # Initialize an empty model object from +coder+. +coder+ must contain @@ -399,6 +393,7 @@ module ActiveRecord @marked_for_destruction = false @new_record = true @mass_assignment_options = nil + @_start_transaction_state = {} end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index b27a19f89a..c877079b25 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -25,7 +25,7 @@ module ActiveRecord foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass belongs_to = child_class.reflect_on_all_associations(:belongs_to) - reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key } + reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 843587c32e..3bac31c6aa 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,8 +1,8 @@ module ActiveRecord module DynamicMatchers #:nodoc: # This code in this file seems to have a lot of indirection, but the indirection - # is there to provide extension points for the active_record_deprecated_finders - # gem. When we stop supporting active_record_deprecated_finders (from Rails 5), + # is there to provide extension points for the activerecord-deprecated_finders + # gem. When we stop supporting activerecord-deprecated_finders (from Rails 5), # then we can remove the indirection. def respond_to?(name, include_private = false) @@ -74,17 +74,17 @@ module ActiveRecord end module Finder - # Extended in active_record_deprecated_finders + # Extended in activerecord-deprecated_finders def body result end - # Extended in active_record_deprecated_finders + # Extended in activerecord-deprecated_finders def result "#{finder}(#{attributes_hash})" end - # Extended in active_record_deprecated_finders + # Extended in activerecord-deprecated_finders def signature attribute_names.join(', ') end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 7d759c1048..04fff99a6e 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -55,7 +55,7 @@ module ActiveRecord end sup = active_record_super - if sup.in?([Base, Model]) || sup.abstract_class? + if sup == Base || sup == Model || sup.abstract_class? self else sup.base_class diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index e0344f3c56..e96ed00f9c 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -168,16 +168,16 @@ module ActiveRecord super end - # If the locking column has no default value set, - # start the lock version at zero. Note we can't use - # <tt>locking_enabled?</tt> at this point as - # <tt>@attributes</tt> may not have been initialized yet. - def initialize_attributes(attributes, options = {}) #:nodoc: - if attributes.key?(locking_column) && lock_optimistically - attributes[locking_column] ||= 0 - end + def column_defaults + @column_defaults ||= begin + defaults = super + + if defaults.key?(locking_column) && lock_optimistically + defaults[locking_column] ||= 0 + end - attributes + defaults + end end end end diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb index a326dabcd3..57553c29eb 100644 --- a/activerecord/lib/active_record/model.rb +++ b/activerecord/lib/active_record/model.rb @@ -141,7 +141,7 @@ module ActiveRecord "ActiveSupport::Concern + include, which will ensure that your class methods are " \ "inherited." ) - @base.extend *mods + @base.extend(*mods) end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index def48c03bf..99de16cd33 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -225,7 +225,7 @@ module ActiveRecord def decorate_columns(columns_hash) # :nodoc: return if columns_hash.empty? - serialized_attributes.keys.each do |key| + serialized_attributes.each_key do |key| columns_hash[key] = AttributeMethods::Serialization::Type.new(columns_hash[key]) end @@ -259,13 +259,12 @@ module ActiveRecord # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute # is available. def column_methods_hash #:nodoc: - @dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr| + @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods| attr_name = attr.to_s methods[attr.to_sym] = attr_name methods["#{attr}=".to_sym] = attr_name methods["#{attr}?".to_sym] = attr_name methods["#{attr}_before_type_cast".to_sym] = attr_name - methods end end @@ -312,13 +311,19 @@ module ActiveRecord @relation = nil end + # This is a hook for use by modules that need to do extra stuff to + # attributes when they are initialized. (e.g. attribute + # serialization) + def initialize_attributes(attributes, options = {}) #:nodoc: + attributes + end + private # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore - table_name = table_name.pluralize if pluralize_table_names - table_name + pluralize_table_names ? table_name.pluralize : table_name end # Computes and returns a table name according to default conventions. diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index be013a068c..f91e41b535 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -245,7 +245,8 @@ module ActiveRecord # any value for _destroy. # [:limit] # Allows you to specify the maximum number of the associated records that - # can be processed with the nested attributes. If the size of the + # can be processed with the nested attributes. Limit also can be specified as a + # Proc or a Symbol pointing to a method that should return number. If the size of the # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords # exception is raised. If omitted, any number associations can be processed. # Note that the :limit option is only applicable to one-to-many associations. @@ -388,8 +389,17 @@ module ActiveRecord raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end - if options[:limit] && attributes_collection.size > options[:limit] - raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead." + limit = case options[:limit] + when Symbol + send(options[:limit]) + when Proc + options[:limit].call + else + options[:limit] + end + + if limit && attributes_collection.size > limit + raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." end if attributes_collection.is_a? Hash diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 593fed5a85..6b4b9bd103 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -218,7 +218,7 @@ module ActiveRecord raise ActiveRecordError, "can not update on a new record object" unless persisted? attributes.each_key do |key| - raise ActiveRecordError, "#{key.to_s} is marked as readonly" if self.class.readonly_attributes.include?(key.to_s) + raise ActiveRecordError, "#{key} is marked as readonly" if self.class.readonly_attributes.include?(key.to_s) end attributes.each do |k,v| @@ -391,9 +391,5 @@ module ActiveRecord @new_record = false id end - - def verify_readonly_attribute(name) - raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) - end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 672d9a4246..ecf8547e67 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -29,8 +29,11 @@ module ActiveRecord 'ActiveRecord::RecordNotSaved' => :unprocessable_entity ) + config.active_record.use_schema_cache_dump = true + config.eager_load_namespaces << ActiveRecord + rake_tasks do require "active_record/base" load "active_record/railties/databases.rake" diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6bb0c39b79..ae24542521 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,4 +1,3 @@ -require 'active_support/core_ext/object/inclusion' require 'active_record' db_namespace = namespace :db do @@ -242,7 +241,7 @@ db_namespace = namespace :db do end end - task :load_if_ruby => 'db:create' do + task :load_if_ruby => [:environment, 'db:create'] do db_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby end @@ -327,7 +326,7 @@ db_namespace = namespace :db do end end - task :load_if_sql => 'db:create' do + task :load_if_sql => [:environment, 'db:create'] do db_namespace["structure:load"].invoke if ActiveRecord::Base.schema_format == :sql end end @@ -409,21 +408,6 @@ db_namespace = namespace :db do end end end - - namespace :sessions do - # desc "Creates a sessions migration for use with ActiveRecord::SessionStore" - task :create => [:environment, :load_config] do - raise 'Task unavailable to this database (no migration support)' unless ActiveRecord::Base.connection.supports_migrations? - Rails.application.load_generators - require 'rails/generators/rails/session_migration/session_migration_generator' - Rails::Generators::SessionMigrationGenerator.start [ ENV['MIGRATION'] || 'add_sessions_table' ] - end - - # desc "Clear the sessions table" - task :clear => [:environment, :load_config] do - ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::SessionStore::Session.table_name}" - end - end end namespace :railties do diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 1d8c566e40..b3c20c4aff 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -4,7 +4,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_attr_readonly, instance_writer: false + class_attribute :_attr_readonly, instance_accessor: false self._attr_readonly = [] end @@ -20,5 +20,10 @@ module ActiveRecord self._attr_readonly end end + + def _attr_readonly + ActiveSupport::Deprecation.warn("Instance level _attr_readonly method is deprecated, please use class level method.") + defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 9ed3256ae9..2d0457636e 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -18,6 +18,7 @@ module ActiveRecord attr_reader :table, :klass, :loaded attr_accessor :default_scoped + alias :model :klass alias :loaded? :loaded alias :default_scoped? :default_scoped @@ -258,6 +259,8 @@ module ActiveRecord # # Update all books that match conditions, but limit it to 5 ordered by date # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') def update_all(updates) + raise ArgumentError, "Empty list of attributes to change" if updates.blank? + stmt = Arel::UpdateManager.new(arel.engine) stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) @@ -466,7 +469,7 @@ module ActiveRecord # Returns sql statement for the relation. # # Users.where(name: 'Oscar').to_sql - # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' + # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= klass.connection.to_sql(arel, bind_values.dup) end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 71aaedee1e..e5b50673da 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -97,15 +97,13 @@ module ActiveRecord merged_wheres = relation.where_values + values[:where] unless relation.where_values.empty? - # Remove duplicates, last one wins. - seen = Hash.new { |h,table| h[table] = {} } + # Remove equalities with duplicated left-hand. Last one wins. + seen = {} merged_wheres = merged_wheres.reverse.reject { |w| nuke = false if w.respond_to?(:operator) && w.operator == :== - name = w.left.name - table = w.left.relation.name - nuke = seen[table][name] - seen[table][name] = true + nuke = seen[w.left] + seen[w.left] = true end nuke }.reverse diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 8e6254f918..f6bacf4822 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -136,8 +136,8 @@ module ActiveRecord # Second: Modifies the SELECT statement for the query so that only certain # fields are retrieved: # - # >> Model.select(:field) - # => [#<Model field:value>] + # Model.select(:field) + # # => [#<Model field:value>] # # Although in the above example it looks as though this method returns an # array, it actually returns a relation object and can have other query @@ -145,25 +145,26 @@ module ActiveRecord # # The argument to the method can also be an array of fields. # - # >> Model.select([:field, :other_field, :and_one_more]) - # => [#<Model field: "value", other_field: "value", and_one_more: "value">] + # Model.select(:field, :other_field, :and_one_more) + # # => [#<Model field: "value", other_field: "value", and_one_more: "value">] # # Accessing attributes of an object that do not have fields retrieved by a select # will throw <tt>ActiveModel::MissingAttributeError</tt>: # - # >> Model.select(:field).first.other_field - # => ActiveModel::MissingAttributeError: missing attribute: other_field - def select(value = Proc.new) + # Model.select(:field).first.other_field + # # => ActiveModel::MissingAttributeError: missing attribute: other_field + def select(*fields) if block_given? - to_a.select { |*block_args| value.call(*block_args) } + to_a.select { |*block_args| yield(*block_args) } else - spawn.select!(value) + raise ArgumentError, 'Call this with at least one field' if fields.empty? + spawn.select!(*fields) end end # Like #select, but modifies relation in place. - def select!(value) - self.select_values += Array.wrap(value) + def select!(*fields) + self.select_values += fields.flatten self end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index fd276ccf5d..2414a4bbd7 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -10,11 +10,11 @@ module ActiveRecord attr_reader :columns, :rows, :column_types - def initialize(columns, rows) + def initialize(columns, rows, column_types = {}) @columns = columns @rows = rows @hash_rows = nil - @column_types = {} + @column_types = column_types end def each diff --git a/activerecord/lib/active_record/session_store.rb b/activerecord/lib/active_record/session_store.rb deleted file mode 100644 index 58e1dab508..0000000000 --- a/activerecord/lib/active_record/session_store.rb +++ /dev/null @@ -1,365 +0,0 @@ -require 'action_dispatch/middleware/session/abstract_store' - -module ActiveRecord - # = Active Record Session Store - # - # A session store backed by an Active Record class. A default class is - # provided, but any object duck-typing to an Active Record Session class - # with text +session_id+ and +data+ attributes is sufficient. - # - # The default assumes a +sessions+ tables with columns: - # +id+ (numeric primary key), - # +session_id+ (string, usually varchar; maximum length is 255), and - # +data+ (text or longtext; careful if your session data exceeds 65KB). - # - # The +session_id+ column should always be indexed for speedy lookups. - # Session data is marshaled to the +data+ column in Base64 format. - # If the data you write is larger than the column's size limit, - # ActionController::SessionOverflowError will be raised. - # - # You may configure the table name, primary key, and data column. - # For example, at the end of <tt>config/application.rb</tt>: - # - # ActiveRecord::SessionStore::Session.table_name = 'legacy_session_table' - # ActiveRecord::SessionStore::Session.primary_key = 'session_id' - # ActiveRecord::SessionStore::Session.data_column_name = 'legacy_session_data' - # - # Note that setting the primary key to the +session_id+ frees you from - # having a separate +id+ column if you don't want it. However, you must - # set <tt>session.model.id = session.session_id</tt> by hand! A before filter - # on ApplicationController is a good place. - # - # Since the default class is a simple Active Record, you get timestamps - # for free if you add +created_at+ and +updated_at+ datetime columns to - # the +sessions+ table, making periodic session expiration a snap. - # - # You may provide your own session class implementation, whether a - # feature-packed Active Record or a bare-metal high-performance SQL - # store, by setting - # - # ActiveRecord::SessionStore.session_class = MySessionClass - # - # You must implement these methods: - # - # self.find_by_session_id(session_id) - # initialize(hash_of_session_id_and_data, options_hash = {}) - # attr_reader :session_id - # attr_accessor :data - # save - # destroy - # - # The example SqlBypass class is a generic SQL session store. You may - # use it as a basis for high-performance database-specific stores. - class SessionStore < ActionDispatch::Session::AbstractStore - module ClassMethods # :nodoc: - def marshal(data) - ::Base64.encode64(Marshal.dump(data)) if data - end - - def unmarshal(data) - Marshal.load(::Base64.decode64(data)) if data - end - - def drop_table! - connection.schema_cache.clear_table_cache!(table_name) - connection.drop_table table_name - end - - def create_table! - connection.schema_cache.clear_table_cache!(table_name) - connection.create_table(table_name) do |t| - t.string session_id_column, :limit => 255 - t.text data_column_name - end - connection.add_index table_name, session_id_column, :unique => true - end - end - - # The default Active Record class. - class Session < ActiveRecord::Base - extend ClassMethods - - ## - # :singleton-method: - # Customizable data column name. Defaults to 'data'. - cattr_accessor :data_column_name - self.data_column_name = 'data' - - attr_accessible :session_id, :data, :marshaled_data - - before_save :marshal_data! - before_save :raise_on_session_data_overflow! - - class << self - def data_column_size_limit - @data_column_size_limit ||= columns_hash[data_column_name].limit - end - - # Hook to set up sessid compatibility. - def find_by_session_id(session_id) - setup_sessid_compatibility! - find_by_session_id(session_id) - end - - private - def session_id_column - 'session_id' - end - - # Compatibility with tables using sessid instead of session_id. - def setup_sessid_compatibility! - # Reset column info since it may be stale. - reset_column_information - if columns_hash['sessid'] - def self.find_by_session_id(*args) - find_by_sessid(*args) - end - - define_method(:session_id) { sessid } - define_method(:session_id=) { |session_id| self.sessid = session_id } - else - class << self; remove_possible_method :find_by_session_id; end - - def self.find_by_session_id(session_id) - where(session_id: session_id).first - end - end - end - end - - def initialize(attributes = nil, options = {}) - @data = nil - super - end - - # Lazy-unmarshal session state. - def data - @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {} - end - - attr_writer :data - - # Has the session been loaded yet? - def loaded? - @data - end - - private - def marshal_data! - return false unless loaded? - write_attribute(@@data_column_name, self.class.marshal(data)) - end - - # Ensures that the data about to be stored in the database is not - # larger than the data storage column. Raises - # ActionController::SessionOverflowError. - def raise_on_session_data_overflow! - return false unless loaded? - limit = self.class.data_column_size_limit - if limit and read_attribute(@@data_column_name).size > limit - raise ActionController::SessionOverflowError - end - end - end - - # A barebones session store which duck-types with the default session - # store but bypasses Active Record and issues SQL directly. This is - # an example session model class meant as a basis for your own classes. - # - # The database connection, table name, and session id and data columns - # are configurable class attributes. Marshaling and unmarshaling - # are implemented as class methods that you may override. By default, - # marshaling data is - # - # ::Base64.encode64(Marshal.dump(data)) - # - # and unmarshaling data is - # - # Marshal.load(::Base64.decode64(data)) - # - # This marshaling behavior is intended to store the widest range of - # binary session data in a +text+ column. For higher performance, - # store in a +blob+ column instead and forgo the Base64 encoding. - class SqlBypass - extend ClassMethods - - ## - # :singleton-method: - # The table name defaults to 'sessions'. - cattr_accessor :table_name - @@table_name = 'sessions' - - ## - # :singleton-method: - # The session id field defaults to 'session_id'. - cattr_accessor :session_id_column - @@session_id_column = 'session_id' - - ## - # :singleton-method: - # The data field defaults to 'data'. - cattr_accessor :data_column - @@data_column = 'data' - - class << self - alias :data_column_name :data_column - - # Use the ActiveRecord::Base.connection by default. - attr_writer :connection - - # Use the ActiveRecord::Base.connection_pool by default. - attr_writer :connection_pool - - def connection - @connection ||= ActiveRecord::Base.connection - end - - def connection_pool - @connection_pool ||= ActiveRecord::Base.connection_pool - end - - # Look up a session by id and unmarshal its data if found. - def find_by_session_id(session_id) - if record = connection.select_one("SELECT #{connection.quote_column_name(data_column)} AS data FROM #{@@table_name} WHERE #{connection.quote_column_name(@@session_id_column)}=#{connection.quote(session_id.to_s)}") - new(:session_id => session_id, :marshaled_data => record['data']) - end - end - end - - delegate :connection, :connection=, :connection_pool, :connection_pool=, :to => self - - attr_reader :session_id, :new_record - alias :new_record? :new_record - - attr_writer :data - - # Look for normal and marshaled data, self.find_by_session_id's way of - # telling us to postpone unmarshaling until the data is requested. - # We need to handle a normal data attribute in case of a new record. - def initialize(attributes) - @session_id = attributes[:session_id] - @data = attributes[:data] - @marshaled_data = attributes[:marshaled_data] - @new_record = @marshaled_data.nil? - end - - # Returns true if the record is persisted, i.e. it's not a new record - def persisted? - !@new_record - end - - # Lazy-unmarshal session state. - def data - unless @data - if @marshaled_data - @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil - else - @data = {} - end - end - @data - end - - def loaded? - @data - end - - def save - return false unless loaded? - marshaled_data = self.class.marshal(data) - connect = connection - - if @new_record - @new_record = false - connect.update <<-end_sql, 'Create session' - INSERT INTO #{table_name} ( - #{connect.quote_column_name(session_id_column)}, - #{connect.quote_column_name(data_column)} ) - VALUES ( - #{connect.quote(session_id)}, - #{connect.quote(marshaled_data)} ) - end_sql - else - connect.update <<-end_sql, 'Update session' - UPDATE #{table_name} - SET #{connect.quote_column_name(data_column)}=#{connect.quote(marshaled_data)} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id)} - end_sql - end - end - - def destroy - return if @new_record - - connect = connection - connect.delete <<-end_sql, 'Destroy session' - DELETE FROM #{table_name} - WHERE #{connect.quote_column_name(session_id_column)}=#{connect.quote(session_id.to_s)} - end_sql - end - end - - # The class used for session storage. Defaults to - # ActiveRecord::SessionStore::Session - cattr_accessor :session_class - self.session_class = Session - - SESSION_RECORD_KEY = 'rack.session.record' - ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY - - private - def get_session(env, sid) - Base.silence do - unless sid and session = @@session_class.find_by_session_id(sid) - # If the sid was nil or if there is no pre-existing session under the sid, - # force the generation of a new sid and associate a new session associated with the new sid - sid = generate_sid - session = @@session_class.new(:session_id => sid, :data => {}) - end - env[SESSION_RECORD_KEY] = session - [sid, session.data] - end - end - - def set_session(env, sid, session_data, options) - Base.silence do - record = get_session_model(env, sid) - record.data = session_data - return false unless record.save - - session_data = record.data - if session_data && session_data.respond_to?(:each_value) - session_data.each_value do |obj| - obj.clear_association_cache if obj.respond_to?(:clear_association_cache) - end - end - end - - sid - end - - def destroy_session(env, session_id, options) - if sid = current_session_id(env) - Base.silence do - get_session_model(env, sid).destroy - env[SESSION_RECORD_KEY] = nil - end - end - - generate_sid unless options[:drop] - end - - def get_session_model(env, sid) - if env[ENV_SESSION_OPTIONS_KEY][:id].nil? - env[SESSION_RECORD_KEY] = find_session(sid) - else - env[SESSION_RECORD_KEY] ||= find_session(sid) - end - 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/store.rb b/activerecord/lib/active_record/store.rb index 5151f349b7..b4013ecc1e 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -41,7 +41,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :stored_attributes, instance_writer: false + class_attribute :stored_attributes, instance_accessor: false self.stored_attributes = {} end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index fb3dfc2730..b41cc68b6a 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -115,7 +115,7 @@ module ActiveRecord end def local_database?(configuration) - configuration['host'].in?(LOCAL_HOSTS) || configuration['host'].blank? + configuration['host'].blank? || LOCAL_HOSTS.include?(configuration['host']) end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 9cb9b4627b..8acbc08e09 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -293,12 +293,10 @@ module ActiveRecord begin status = yield rescue ActiveRecord::Rollback - if defined?(@_start_transaction_state) - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - end + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 status = nil end - + raise ActiveRecord::Rollback unless status end status @@ -308,7 +306,6 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: - @_start_transaction_state ||= {} @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) @_start_transaction_state[:new_record] = @new_record @_start_transaction_state[:destroyed] = @destroyed @@ -317,18 +314,16 @@ module ActiveRecord # Clear the new record state and id of a record. def clear_transaction_record_state #:nodoc: - if defined?(@_start_transaction_state) - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 - end + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. def restore_transaction_record_state(force = false) #:nodoc: - if defined?(@_start_transaction_state) + unless @_start_transaction_state.empty? @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - if @_start_transaction_state[:level] < 1 - restore_state = remove_instance_variable(:@_start_transaction_state) + if @_start_transaction_state[:level] < 1 || force + restore_state = @_start_transaction_state was_frozen = @attributes.frozen? @attributes = @attributes.dup if was_frozen @new_record = restore_state[:new_record] @@ -340,13 +335,14 @@ module ActiveRecord @attributes_cache.delete(self.class.primary_key) end @attributes.freeze if was_frozen + @_start_transaction_state.clear end end end # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. def transaction_record_state(state) #:nodoc: - @_start_transaction_state[state] if defined?(@_start_transaction_state) + @_start_transaction_state[state] end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. diff --git a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb deleted file mode 100644 index a976571dee..0000000000 --- a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'rails/generators/active_record' - -module ActiveRecord - module Generators - class SessionMigrationGenerator < Base - argument :name, :type => :string, :default => "add_sessions_table" - - def create_migration_file - migration_template "migration.rb", "db/migrate/#{file_name}.rb" - end - - protected - - def session_table_name - current_table_name = ActiveRecord::SessionStore::Session.table_name - if current_table_name.in?(["sessions", "session"]) - current_table_name = (ActiveRecord::Base.pluralize_table_names ? 'session'.pluralize : 'session') - end - current_table_name - end - - end - end -end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb deleted file mode 100644 index 9ea3248513..0000000000 --- a/activerecord/lib/rails/generators/active_record/session_migration/templates/migration.rb +++ /dev/null @@ -1,12 +0,0 @@ -class <%= migration_class_name %> < ActiveRecord::Migration - def change - create_table :<%= session_table_name %> do |t| - t.string :session_id, :null => false - t.text :data - t.timestamps - end - - add_index :<%= session_table_name %>, :session_id - add_index :<%= session_table_name %>, :updated_at - end -end |