diff options
Diffstat (limited to 'activerecord/lib/active_record')
79 files changed, 880 insertions, 494 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1ca648d48d..a830b0e0e4 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -61,12 +61,18 @@ module ActiveRecord end end - class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: + class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") end end + class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: + end + + class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: + end + class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") @@ -79,12 +85,18 @@ module ActiveRecord end end - class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: + class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: def initialize(owner, reflection) super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") end end + class HasManyThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: + end + + class HasOneThroughNestedAssociationsAreReadonly < ThroughNestedAssociationsAreReadonly #:nodoc: + end + class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: def initialize(reflection) super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") @@ -955,20 +967,16 @@ module ActiveRecord # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+ # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, - # Active Record doesn't know anything about these inverse relationships and so no object - # loading optimization is possible. For example: + # Active Record can guess the inverse of the association based on the name + # of the class. The result is the following: # # d = Dungeon.first # t = d.traps.first - # d.level == t.dungeon.level # => true - # d.level = 10 - # d.level == t.dungeon.level # => false + # d.object_id == t.dungeon.object_id # => true # # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to - # the same object data from the database, but are actually different in-memory copies - # of that data. Specifying the <tt>:inverse_of</tt> option on associations lets you tell - # Active Record about inverse relationships and it will optimise object loading. For - # example, if we changed our model definitions to: + # the same in-memory instance since the association matches the name of the class. + # The result would be the same if we added +:inverse_of+ to our model definitions: # # class Dungeon < ActiveRecord::Base # has_many :traps, inverse_of: :dungeon @@ -983,15 +991,14 @@ module ActiveRecord # belongs_to :dungeon, inverse_of: :evil_wizard # end # - # Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same - # in-memory instance and our final <tt>d.level == t.dungeon.level</tt> will return +true+. - # # There are limitations to <tt>:inverse_of</tt> support: # # * does not work with <tt>:through</tt> associations. # * does not work with <tt>:polymorphic</tt> associations. # * for +belongs_to+ associations +has_many+ inverse associations are ignored. # + # For more information, see the documentation for the +:inverse_of+ option. + # # == Deleting from associations # # === Dependent associations diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 930f678ae8..c7b396f3d4 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -211,9 +211,12 @@ module ActiveRecord # the kind of the class of the associated objects. Meant to be used as # a sanity check when you are about to assign an associated record. def raise_on_type_mismatch!(record) - unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize) - message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" - raise ActiveRecord::AssociationTypeMismatch, message + unless record.is_a?(reflection.klass) + fresh_class = reflection.class_name.safe_constantize + unless fresh_class && record.is_a?(fresh_class) + message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" + raise ActiveRecord::AssociationTypeMismatch, message + end end end @@ -248,6 +251,14 @@ module ActiveRecord initialize_attributes(record) end end + + # Returns true if statement cache should be skipped on the association reader. + def skip_statement_cache? + reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.scope_attributes? || + reflection.source_reflection.active_record.default_scopes.any? + end end end end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 2416167834..a140dc239c 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -149,6 +149,7 @@ module ActiveRecord scope.where_clause += item.where_clause scope.order_values |= item.order_values + scope.unscope!(*item.unscope_values) end reflection = reflection.next diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 265a65c4c1..260a0c6a2d 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -107,7 +107,7 @@ module ActiveRecord end def stale_state - result = owner._read_attribute(reflection.foreign_key) + result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) } result && result.to_s 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 97eb007f62..6e4a53f7fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -33,16 +33,24 @@ module ActiveRecord::Associations::Builder if (@_after_create_counter_called ||= false) @_after_create_counter_called = false - elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable? - model = reflection.klass + elsif attribute_changed?(foreign_key) && !new_record? + if reflection.polymorphic? + model = attribute(reflection.foreign_type).try(:constantize) + model_was = attribute_was(reflection.foreign_type).try(:constantize) + else + model = reflection.klass + model_was = reflection.klass + end + foreign_key_was = attribute_was foreign_key foreign_key = attribute foreign_key if foreign_key && model.respond_to?(:increment_counter) model.increment_counter(cache_column, foreign_key) end - if foreign_key_was && model.respond_to?(:decrement_counter) - model.decrement_counter(cache_column, foreign_key_was) + + if foreign_key_was && model_was.respond_to?(:decrement_counter) + model_was.decrement_counter(cache_column, foreign_key_was) 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 ffd9c9d6fc..b18d99d54e 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 @@ -46,7 +46,7 @@ module ActiveRecord::Associations::Builder join_model = Class.new(ActiveRecord::Base) { class << self; - attr_accessor :class_resolver + attr_accessor :left_model attr_accessor :name attr_accessor :table_name_resolver attr_accessor :left_reflection @@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder end def self.compute_type(class_name) - class_resolver.compute_type class_name + left_model.compute_type class_name end def self.add_left_association(name, options) @@ -72,11 +72,15 @@ module ActiveRecord::Associations::Builder self.right_reflection = _reflect_on_association(rhs_name) end + def self.retrieve_connection + left_model.retrieve_connection + end + } join_model.name = "HABTM_#{association_name.to_s.camelize}" join_model.table_name_resolver = habtm - join_model.class_resolver = lhs_model + join_model.left_model = lhs_model join_model.add_left_association :left_side, anonymous_class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 6caadb4ce8..256df3ca11 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -1,3 +1,5 @@ +require "active_support/deprecation" + module ActiveRecord module Associations # = Active Record Association Collection @@ -28,6 +30,12 @@ module ActiveRecord # Implements the reader method, e.g. foo.items for Foo.has_many :items def reader(force_reload = false) if force_reload + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing an argument to force an association to reload is now + deprecated and will be removed in Rails 5.1. Please call `reload` + on the result collection proxy instead. + MSG + klass.uncached { reload } elsif stale_target? reload @@ -54,8 +62,10 @@ module ActiveRecord record.send(reflection.association_primary_key) end else - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) + @association_ids ||= ( + column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + scope.pluck(column) + ) end end @@ -432,12 +442,7 @@ module ActiveRecord private def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.to_a - end + return scope.to_a if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 685c3a5f17..b5a8c81fe4 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -470,15 +470,16 @@ module ActiveRecord @association.destroy_all end - # Deletes the +records+ supplied and removes them from the collection. For - # +has_many+ associations, the deletion is done according to the strategy - # specified by the <tt>:dependent</tt> option. Returns an array with the + # Deletes the +records+ supplied from the collection according to the strategy + # specified by the +:dependent+ option. If no +:dependent+ option is given, + # then it will follow the default strategy. Returns an array with the # deleted records. # - # If no <tt>:dependent</tt> option is given, then it will follow the default - # strategy. The default strategy is <tt>:nullify</tt>. This sets the foreign - # keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, the default - # strategy is +delete_all+. + # For +has_many :through+ associations, the default deletion strategy is + # +:delete_all+. + # + # For +has_many+ associations, the default deletion strategy is +:nullify+. + # This sets the foreign keys to +NULL+. # # class Person < ActiveRecord::Base # has_many :pets # dependent: :nullify option by default @@ -780,7 +781,7 @@ module ActiveRecord # person.pets.any? # => false # # person.pets << Pet.new(name: 'Snoop') - # person.pets.count # => 0 + # person.pets.count # => 1 # person.pets.any? # => true # # You can also pass a +block+ to define criteria. The behavior @@ -970,7 +971,7 @@ module ActiveRecord alias_method :append, :<< def prepend(*args) - raise NoMethodError, "prepend on association is not defined. Please use << or append" + raise NoMethodError, "prepend on association is not defined. Please use <<, push or append" end # Equivalent to +delete_all+. The difference is that returns +self+, instead diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index ca27c9fdde..19cdd7f470 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -16,7 +16,14 @@ module ActiveRecord when :restrict_with_error unless empty? record = klass.human_attribute_name(reflection.name).downcase - owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record) + message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil + if message + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1. + Please use `:'restrict_dependent_destroy.has_many'` instead. + MESSAGE + end + owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record) throw(:abort) end @@ -80,8 +87,15 @@ module ActiveRecord [association_scope.limit_value, count].compact.min end + + # Returns whether a counter cache should be used for this association. + # + # The counter_cache option must be given on either the owner or inverse + # association, and the column must be present on the owner. def has_cached_counter?(reflection = reflection()) - owner.attribute_present?(cached_counter_attribute_name(reflection)) + if reflection.options[:counter_cache] || (inverse = inverse_which_updates_counter_cache(reflection)) && inverse.options[:counter_cache] + owner.attribute_present?(cached_counter_attribute_name(reflection)) + end end def cached_counter_attribute_name(reflection = reflection()) 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 cd79266952..1aa6a2ca74 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -133,7 +133,7 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.each { |record| record.run_callbacks :destroy } + scope.each(&:_run_destroy_callbacks) arel = scope.arel diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 41a75b820e..5a92bc5e8a 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -12,7 +12,14 @@ module ActiveRecord 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) + message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil + if message + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1. + Please use `:'restrict_dependent_destroy.has_one'` instead. + MESSAGE + end + owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record) throw(:abort) end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 97f4bd3811..3992a240b9 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -10,13 +10,13 @@ module ActiveRecord # end # # class Book < ActiveRecord::Base - # # columns: title, sales + # # columns: title, sales, author_id # end # # When you load an author with all associated books Active Record will make # multiple queries like this: # - # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a + # Author.includes(:books).where(name: ['bell hooks', 'Homer']).to_a # # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer') # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5) @@ -116,7 +116,7 @@ module ActiveRecord when String preloaders_for_one(association.to_sym, records, scope) else - raise ArgumentError, "#{association.inspect} was not recognised for preload" + raise ArgumentError, "#{association.inspect} was not recognized for preload" end end @@ -160,7 +160,7 @@ module ActiveRecord h end - class AlreadyLoaded + class AlreadyLoaded # :nodoc: attr_reader :owners, :reflection def initialize(klass, owners, reflection, preload_scope) @@ -175,7 +175,7 @@ module ActiveRecord end end - class NullPreloader + class NullPreloader # :nodoc: def self.new(klass, owners, reflection, preload_scope); self; end def self.run(preloader); end def self.preloaded_records; []; end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 58d0f7d65d..03cb8cb8c3 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -1,9 +1,17 @@ +require "active_support/deprecation" + module ActiveRecord module Associations class SingularAssociation < Association #:nodoc: # Implements the reader method, e.g. foo.bar for Foo.has_one :bar def reader(force_reload = false) - if force_reload + if force_reload && klass + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing an argument to force an association to reload is now + deprecated and will be removed in Rails 5.1. Please call `reload` + on the parent object instead. + MSG + klass.uncached { reload } elsif !loaded? || stale_target? reload @@ -39,12 +47,7 @@ module ActiveRecord end def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.limit(1).to_a - end + return scope.limit(1).to_a if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index af1bce523c..d0ec3e8015 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -15,12 +15,6 @@ module ActiveRecord scope = super reflection.chain.drop(1).each do |reflection| relation = reflection.klass.all - - reflection_scope = reflection.scope - if reflection_scope && reflection_scope.arity.zero? - relation = relation.merge(reflection_scope) - end - scope.merge!( relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) @@ -82,13 +76,21 @@ module ActiveRecord def ensure_mutable unless source_reflection.belongs_to? - raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + if reflection.has_one? + raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + else + raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) + end end end def ensure_not_nested if reflection.nested? - raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + if reflection.has_one? + raise HasOneThroughNestedAssociationsAreReadonly.new(owner, reflection) + else + raise HasManyThroughNestedAssociationsAreReadonly.new(owner, reflection) + end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 9d58a19304..0862306749 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -42,7 +42,7 @@ module ActiveRecord def [](name) @method_cache.compute_if_absent(name) do - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ @@ -230,7 +230,15 @@ module ActiveRecord # person.respond_to(:nothing) # => false def respond_to?(name, include_private = false) return false unless super - name = name.to_s + + case name + when :to_partial_path + name = "to_partial_path".freeze + when :to_model + name = "to_model".freeze + else + name = name.to_s + end # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. @@ -377,27 +385,27 @@ module ActiveRecord # # For example: # - # class PostsController < ActionController::Base - # after_action :print_accessed_fields, only: :index + # class PostsController < ActionController::Base + # after_action :print_accessed_fields, only: :index # - # def index - # @posts = Post.all - # end + # def index + # @posts = Post.all + # end # - # private + # private # - # def print_accessed_fields - # p @posts.first.accessed_fields + # def print_accessed_fields + # p @posts.first.accessed_fields + # end # end - # end # # Which allows you to quickly change your code to: # - # class PostsController < ActionController::Base - # def index - # @posts = Post.select(:id, :title, :author_id, :updated_at) + # class PostsController < ActionController::Base + # def index + # @posts = Post.select(:id, :title, :author_id, :updated_at) + # end # end - # end def accessed_fields @attributes.accessed end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 7ba907f786..0171ef3bdf 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -108,6 +108,7 @@ module ActiveRecord end def save_changed_attribute(attr, old_value) + clear_changed_attributes_cache if attribute_changed_by_setter?(attr) clear_attribute_changes(attr) unless _field_changed?(attr, old_value) else @@ -176,7 +177,11 @@ module ActiveRecord @cached_changed_attributes = changed_attributes yield ensure - remove_instance_variable(:@cached_changed_attributes) + clear_changed_attributes_cache + end + + def clear_changed_attributes_cache + remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 0d989c2eca..2363cf7608 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -37,7 +37,7 @@ module ActiveRecord protected def define_method_attribute(name) - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index d0d8a968c5..60eecab0d0 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -11,6 +11,18 @@ module ActiveRecord # serialized object must be of that class on assignment and retrieval. # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. # + # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of + # +Array+, will always be persisted as null. + # + # Keep in mind that database adapters handle certain serialization tasks + # for you. For instance: +json+ and +jsonb+ types in PostgreSQL will be + # converted between JSON object/array syntax and Ruby +Hash+ or +Array+ + # objects transparently. There is no need to use +serialize+ in this + # case. + # + # For more complex cases, such as conversion to or from your application + # domain objects, consider using the ActiveRecord::Attributes API. + # # ==== Parameters # # * +attr_name+ - The field name that should be serialized. diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index ab017c7b54..07d5e7d38e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -24,7 +24,7 @@ module ActiveRecord protected def define_method_attribute=(name) - safe_name = name.unpack('h*').first + safe_name = name.unpack('h*'.freeze).first ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index c89099589e..8b2c4c7170 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -82,6 +82,14 @@ module ActiveRecord # # StoreListing.new.my_string # => "new default" # + # class Product < ActiveRecord::Base + # attribute :my_default_proc, :datetime, default: -> { Time.now } + # end + # + # Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600 + # sleep 1 + # Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600 + # # Attributes do not need to be backed by a database column. # # class MyModel < ActiveRecord::Base @@ -204,7 +212,8 @@ module ActiveRecord # # +default+ The default value to use when no value is provided. If this option # is not passed, the previous default value (if any) will be used. - # Otherwise, the default will be +nil+. + # Otherwise, the default will be +nil+. A proc can also be passed, and + # will be called once each time a new value is needed. # # +user_provided_default+ Whether the default value should be cast using # +cast+ or +deserialize+. diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 0792d19c3e..dbb0e2fab2 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -222,6 +222,7 @@ module ActiveRecord true end validate validation_method + after_validation :_ensure_no_duplicate_errors end end end @@ -324,7 +325,7 @@ module ActiveRecord # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. def association_valid?(reflection, record) - return true if record.destroyed? || record.marked_for_destruction? + return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) unless valid = record.valid?(validation_context) @@ -456,5 +457,11 @@ module ActiveRecord end end end + + def _ensure_no_duplicate_errors + errors.messages.each_key do |attribute| + errors[attribute].uniq! + end + end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c918e88590..55a7e053bc 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -280,6 +280,7 @@ module ActiveRecord #:nodoc: extend Explain extend Enum extend Delegation::DelegateCache + extend CollectionCacheKey include Core include Persistence diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 3027ce928e..ccdbebbc77 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -206,7 +206,8 @@ module ActiveRecord # == Ordering callbacks # # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+ - # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option. + # callback (+log_children+ in this case) should be executed before the children get destroyed by the + # <tt>dependent: destroy</tt> option. # # Let's look at the code below: # @@ -289,24 +290,28 @@ module ActiveRecord end def destroy #:nodoc: - run_callbacks(:destroy) { super } + _run_destroy_callbacks { super } + rescue RecordNotDestroyed => e + @_association_destroy_exception = e + false end def touch(*) #:nodoc: - run_callbacks(:touch) { super } + _run_touch_callbacks { super } end private + def create_or_update(*) #:nodoc: - run_callbacks(:save) { super } + _run_save_callbacks { super } end def _create_record #:nodoc: - run_callbacks(:create) { super } + _run_create_callbacks { super } end def _update_record(*) #:nodoc: - run_callbacks(:update) { super } + _run_update_callbacks { super } end end end diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb new file mode 100644 index 0000000000..3c4ca3d116 --- /dev/null +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -0,0 +1,31 @@ +module ActiveRecord + module CollectionCacheKey + + def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: + query_signature = Digest::MD5.hexdigest(collection.to_sql) + key = "#{collection.model_name.cache_key}/query-#{query_signature}" + + if collection.loaded? + size = collection.size + timestamp = collection.max_by(×tamp_column).public_send(timestamp_column) + else + column_type = type_for_attribute(timestamp_column.to_s) + column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}" + + query = collection + .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp") + .unscope(:order) + result = connection.select_one(query) + + size = result["size"] + timestamp = column_type.deserialize(result["timestamp"]) + end + + if timestamp + "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}" + else + "#{key}-#{size}" + end + end + end +end 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 6535121075..282af220fb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -508,7 +508,7 @@ module ActiveRecord synchronize do remove_connection_from_thread_cache conn - conn.run_callbacks :checkin do + conn._run_checkin_callbacks do conn.expire end @@ -764,7 +764,7 @@ module ActiveRecord end def checkout_and_verify(c) - c.run_callbacks :checkout do + c._run_checkout_callbacks do c.verify! end c 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 38dd9578fe..1e13b24867 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -289,8 +289,12 @@ module ActiveRecord columns = schema_cache.columns_hash(table_name) binds = fixture.map do |name, value| - type = lookup_cast_type_from_column(columns[name]) - Relation::QueryAttribute.new(name, value, type) + if column = columns[name] + type = lookup_cast_type_from_column(column) + Relation::QueryAttribute.new(name, value, type) + else + raise Fixture::FixtureError, %(table "#{table_name}" has no column named "#{name}".) + end end key_list = fixture.keys.map { |name| quote_column_name(name) } value_list = prepare_binds_for_database(binds).map do |value| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 158b773e11..d17e272ed1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -592,10 +592,11 @@ module ActiveRecord # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # t.change_default(:status, from: nil, to: "draft") # # See SchemaStatements#change_column_default - def change_default(column_name, default) - @base.change_column_default(name, column_name, default) + def change_default(column_name, default_or_changes) + @base.change_column_default(name, column_name, default_or_changes) end # Removes the column(s) from the table definition. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index ed19819d63..a30945d0ee 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -460,7 +460,12 @@ module ActiveRecord # # change_column_default(:users, :email, nil) # - def change_column_default(table_name, column_name, default) + # Passing a hash containing +:from+ and +:to+ will make this change + # reversible in migration: + # + # change_column_default(:posts, :state, from: nil, to: "draft") + # + def change_column_default(table_name, column_name, default_or_changes) raise NotImplementedError, "change_column_default is not implemented" end @@ -587,7 +592,7 @@ module ActiveRecord # # Removes the +index_accounts_on_column+ in the +accounts+ table. # - # remove_index :accounts, :column + # remove_index :accounts, :branch_id # # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. # @@ -662,7 +667,7 @@ module ActiveRecord # [<tt>:foreign_key</tt>] # Add an appropriate foreign key. Defaults to false. # [<tt>:polymorphic</tt>] - # Wether an additional +_type+ column should be added. Defaults to false. + # Whether an additional +_type+ column should be added. Defaults to false. # # ====== Create a user_id integer column # @@ -756,9 +761,9 @@ module ActiveRecord # [<tt>:name</tt>] # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. # [<tt>:on_delete</tt>] - # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ # [<tt>:on_update</tt>] - # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+ def add_foreign_key(from_table, to_table, options = {}) return unless supports_foreign_keys? @@ -777,7 +782,10 @@ module ActiveRecord execute schema_creation.accept(at) end - # Removes the given foreign key from the table. + # Removes the given foreign key from the table. Any option parameters provided + # will be used to re-add the foreign key in case of a migration rollback. + # It is recommended that you provide any options used when creating the foreign + # key so that the migration can be reverted properly. # # Removes the foreign key on +accounts.branch_id+. # @@ -791,6 +799,7 @@ module ActiveRecord # # remove_foreign_key :accounts, name: :special_fk_name # + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? @@ -828,7 +837,10 @@ module ActiveRecord end def foreign_key_column_for(table_name) # :nodoc: - "#{table_name.to_s.singularize}_id" + prefix = Base.table_name_prefix + suffix = Base.table_name_suffix + name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s + "#{name.singularize}_id" end def dump_schema_information #:nodoc: @@ -1064,6 +1076,14 @@ module ActiveRecord raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" end end + + def extract_new_default_value(default_or_changes) + if default_or_changes.is_a?(Hash) && default_or_changes.has_key?(:from) && default_or_changes.has_key?(:to) + default_or_changes[:to] + else + default_or_changes + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 6d3a21a3dc..56227ddd80 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -107,6 +107,18 @@ module ActiveRecord @prepared_statements = false end + class Version + include Comparable + + def initialize(version_string) + @version = version_string.split('.').map(&:to_i) + end + + def <=>(version_string) + @version <=> version_string.split('.').map(&:to_i) + end + end + class BindCollector < Arel::Collectors::Bind def compile(bvs, conn) casted_binds = conn.prepare_binds_for_database(bvs) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 00e3d2965b..af156c9c78 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -307,7 +307,7 @@ module ActiveRecord # # http://bugs.mysql.com/bug.php?id=39170 def supports_transaction_isolation? - version[0] >= 5 + version >= '5.0.0' end def supports_indexes_in_create? @@ -319,11 +319,11 @@ module ActiveRecord end def supports_views? - version[0] >= 5 + version >= '5.0.0' end def supports_datetime_with_precision? - (version[0] == 5 && version[1] >= 6) || version[0] >= 6 + version >= '5.6.4' end def native_database_types @@ -386,6 +386,14 @@ module ActiveRecord 0 end + def quoted_date(value) + if supports_datetime_with_precision? + super + else + super.sub(/\.\d{6}\z/, '') + end + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -629,7 +637,8 @@ module ActiveRecord end end - def change_column_default(table_name, column_name, default) #:nodoc: + def change_column_default(table_name, column_name, default_or_changes) #:nodoc: + default = extract_new_default_value(default_or_changes) column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default end @@ -937,7 +946,7 @@ module ActiveRecord end def version - @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) + @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) end def mariadb? @@ -945,7 +954,7 @@ module ActiveRecord end def supports_rename_index? - mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + mariadb? ? false : version >= '5.7.6' end def configure_connection diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index e97e82f056..b7db57c9fe 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -254,7 +254,7 @@ module ActiveRecord end def full_version - @full_version ||= @connection.info[:version] + @full_version ||= @connection.server_info[:version] end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 92349e2f9b..68752cdd80 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -12,6 +12,7 @@ require 'active_record/connection_adapters/postgresql/oid/json' require 'active_record/connection_adapters/postgresql/oid/jsonb' require 'active_record/connection_adapters/postgresql/oid/money' require 'active_record/connection_adapters/postgresql/oid/point' +require 'active_record/connection_adapters/postgresql/oid/rails_5_1_point' require 'active_record/connection_adapters/postgresql/oid/range' require 'active_record/connection_adapters/postgresql/oid/specialized_string' require 'active_record/connection_adapters/postgresql/oid/uuid' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 3de794f797..25961a9869 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -45,6 +45,11 @@ module ActiveRecord delimiter == other.delimiter end + def type_cast_for_schema(value) + return super unless value.is_a?(::Array) + "[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]" + end + private def type_cast_array(value, method) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index afc9383f91..87391b5dc7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -9,7 +9,7 @@ module ActiveRecord def changed_in_place?(raw_old_value, new_value) # Postgres does not preserve insignificant whitespaces when - # roundtripping jsonb columns. This causes some false positives for + # round-tripping jsonb columns. This causes some false positives for # the comparison here. Therefore, we need to parse and re-dump the # raw value here to ensure the insignificant whitespaces are # consistent with our encoder's output. diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb new file mode 100644 index 0000000000..7427a25ad5 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb @@ -0,0 +1,50 @@ +module ActiveRecord + Point = Struct.new(:x, :y) + + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Rails51Point < Type::Value # :nodoc: + include Type::Helpers::Mutable + + def type + :point + end + + def cast(value) + case value + when ::String + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + x, y = value.split(",") + build_point(x, y) + when ::Array + build_point(*value) + else + value + end + end + + def serialize(value) + if value.is_a?(ActiveRecord::Point) + "(#{number_for_point(value.x)},#{number_for_point(value.y)})" + else + super + end + end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end + + def build_point(x, y) + ActiveRecord::Point.new(Float(x), Float(y)) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 022dbdfa27..6399bddbee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -103,6 +103,30 @@ module ActiveRecord args.each { |name| column(name, :point, options) } end + def line(*args, **options) + args.each { |name| column(name, :line, options) } + end + + def lseg(*args, **options) + args.each { |name| column(name, :lseg, options) } + end + + def box(*args, **options) + args.each { |name| column(name, :box, options) } + end + + def path(*args, **options) + args.each { |name| column(name, :path, options) } + end + + def polygon(*args, **options) + args.each { |name| column(name, :polygon, options) } + end + + def circle(*args, **options) + args.each { |name| column(name, :circle, options) } + end + def serial(*args, **options) args.each { |name| column(name, :serial, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 595c635fc0..d114cad16b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -410,11 +410,12 @@ module ActiveRecord end # Changes the default value of a table column. - def change_column_default(table_name, column_name, default) # :nodoc: + def change_column_default(table_name, column_name, default_or_changes) # :nodoc: clear_cache! column = column_for(table_name, column_name) return unless column + default = extract_new_default_value(default_or_changes) alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" if default.nil? # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index 58715978f7..b2c49989a4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -12,7 +12,7 @@ module ActiveRecord end def sql_type - super.gsub(/\[\]$/, "") + super.gsub(/\[\]$/, "".freeze) end def ==(other) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 93fa3984e6..2c43c46a3d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -838,6 +838,8 @@ module ActiveRecord ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:rails_5_1_point, OID::Rails51Point, adapter: :postgresql) ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 87129c42cf..358039723f 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -65,18 +65,6 @@ module ActiveRecord boolean: { name: "boolean" } } - class Version - include Comparable - - def initialize(version_string) - @version = version_string.split('.').map(&:to_i) - end - - def <=>(version_string) - @version <=> version_string.split('.').map(&:to_i) - end - end - class StatementPool < ConnectionAdapters::StatementPool private @@ -422,7 +410,9 @@ module ActiveRecord end end - def change_column_default(table_name, column_name, default) #:nodoc: + def change_column_default(table_name, column_name, default_or_changes) #:nodoc: + default = extract_new_default_value(default_or_changes) + alter_table(table_name) do |definition| definition[column_name].default = default end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 8a014e682e..ffce2173ec 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -162,11 +162,13 @@ module ActiveRecord } record = statement.execute([id], self, connection).first unless record - raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" + raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", + name, primary_key, id) end record rescue RangeError - raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" + raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'", + name, primary_key) end def find_by(*args) # :nodoc: @@ -199,7 +201,7 @@ module ActiveRecord end def find_by!(*args) # :nodoc: - find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") + find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}", name) end def initialize_generated_modules # :nodoc: @@ -303,7 +305,7 @@ module ActiveRecord assign_attributes(attributes) if attributes yield self if block_given? - run_callbacks :initialize + _run_initialize_callbacks end # Initialize an empty model object from +coder+. +coder+ should be @@ -330,8 +332,8 @@ module ActiveRecord self.class.define_attribute_methods - run_callbacks :find - run_callbacks :initialize + _run_find_callbacks + _run_initialize_callbacks self end @@ -367,7 +369,7 @@ module ActiveRecord @attributes = @attributes.dup @attributes.reset(self.class.primary_key) - run_callbacks(:initialize) + _run_initialize_callbacks @new_record = true @destroyed = false diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 2b99899e42..91a13cb0cd 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -75,6 +75,24 @@ module ActiveRecord # # Conversation.where("status <> ?", Conversation.statuses[:archived]) # + # You can use the +:_prefix+ or +:_suffix+ options when you need to define + # multiple enums with same values. If the passed value is +true+, the methods + # are prefixed/suffixed with the name of the enum. It is also possible to + # supply a custom value: + # + # class Conversation < ActiveRecord::Base + # enum status: [:active, :archived], _suffix: true + # enum comments_status: [:active, :inactive], _prefix: :comments + # end + # + # With the above example, the bang and predicate methods along with the + # associated scopes are now prefixed and/or suffixed accordingly: + # + # conversation.active_status! + # conversation.archived_status? # => false + # + # conversation.comments_inactive! + # conversation.comments_active? # => false module Enum def self.extended(base) # :nodoc: @@ -121,6 +139,8 @@ module ActiveRecord def enum(definitions) klass = self + enum_prefix = definitions.delete(:_prefix) + enum_suffix = definitions.delete(:_suffix) definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new @@ -138,19 +158,31 @@ module ActiveRecord _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index pairs.each do |value, i| + if enum_prefix == true + prefix = "#{name}_" + elsif enum_prefix + prefix = "#{enum_prefix}_" + end + if enum_suffix == true + suffix = "_#{name}" + elsif enum_suffix + suffix = "_#{enum_suffix}" + end + + value_method_name = "#{prefix}#{value}#{suffix}" enum_values[value] = i # def active?() status == 0 end - klass.send(:detect_enum_conflict!, name, "#{value}?") - define_method("#{value}?") { self[name] == value.to_s } + klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") + define_method("#{value_method_name}?") { self[name] == value.to_s } # def active!() update! status: :active end - klass.send(:detect_enum_conflict!, name, "#{value}!") - define_method("#{value}!") { update! name => value } + klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") + define_method("#{value_method_name}!") { update! name => value } # scope :active, -> { where status: 0 } - klass.send(:detect_enum_conflict!, name, value, true) - klass.scope value, -> { klass.where name => value } + klass.send(:detect_enum_conflict!, name, value_method_name, true) + klass.scope value_method_name, -> { klass.where name => value } end end defined_enums[name.to_s] = enum_values diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 0f1759abaa..d589620f8a 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -47,6 +47,15 @@ module ActiveRecord # Raised when Active Record cannot find record by given id or set of ids. class RecordNotFound < ActiveRecordError + attr_reader :model, :primary_key, :id + + def initialize(message = nil, model = nil, primary_key = nil, id = nil) + @primary_key = primary_key + @model = model + @id = id + + super(message) + end end # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 6a49936644..9adabd7819 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -19,7 +19,7 @@ module ActiveRecord # On the other hand, we want to monitor the performance of our real database # queries, not the performance of the access to the query cache. IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE) - EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i + EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i def ignore_payload?(payload) payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 1ec8f818cd..f1dc56df63 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -110,7 +110,7 @@ module ActiveRecord # <% 1.upto(1000) do |i| %> # fix_<%= i %>: # id: <%= i %> - # name: guy_<%= 1 %> + # name: guy_<%= i %> # <% end %> # # This will create 1000 very simple fixtures. @@ -615,7 +615,6 @@ module ActiveRecord # a list of rows to insert to that table. def table_rows now = config.default_timezone == :utc ? Time.now.utc : Time.now - now = now.to_s(:db) # allow a standard key to be used for doing defaults in YAML fixtures.delete('DEFAULTS') @@ -646,7 +645,9 @@ module ActiveRecord # Resolve enums model_class.defined_enums.each do |name, values| - row[name] = values.fetch(row[name], row[name]) + if row.include?(name) + row[name] = values.fetch(row[name], row[name]) + end end # If STI is used, find the correct subclass for association reflection @@ -669,7 +670,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - fk_type = association.active_record.type_for_attribute(fk_name).type + fk_type = reflection_class.type_for_attribute(fk_name).type row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) end when :has_many @@ -826,12 +827,12 @@ module ActiveRecord module TestFixtures extend ActiveSupport::Concern - def before_setup + def before_setup # :nodoc: setup_fixtures super end - def after_teardown + def after_teardown # :nodoc: super teardown_fixtures end diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml index 8a3c27e6da..0b35027b2b 100644 --- a/activerecord/lib/active_record/locale/en.yml +++ b/activerecord/lib/active_record/locale/en.yml @@ -16,8 +16,8 @@ en: messages: record_invalid: "Validation failed: %{errors}" restrict_dependent_destroy: - one: "Cannot delete record because a dependent %{record} exists" - many: "Cannot delete record because dependent %{record} exist" + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" # Append your own errors here or at the model/attributes scope. # You can define own errors for models or model attributes. diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index af816a278e..4d597a0ab1 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -47,18 +47,21 @@ module ActiveRecord binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect end - if odd? - name = color(name, CYAN, true) - sql = color(sql, nil, true) - else - name = color(name, MAGENTA, true) - end + name = color(name, nil, true) + sql = color(sql, sql_color(sql), true) debug " #{name} #{sql}#{binds}" end - def odd? - @odd = !@odd + def sql_color(sql) + case sql + when /\s*\Ainsert/i then GREEN + when /\s*\Aselect/i then BLUE + when /\s*\Aupdate/i then YELLOW + when /\s*\Adelete/i then RED + when /transaction\s*\Z/i then CYAN + else MAGENTA + end end def logger diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 5af788bc10..b5b91451c7 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -138,10 +138,13 @@ module ActiveRecord # <tt>:name</tt>, <tt>:unique</tt> (e.g. # <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt> # (e.g. <tt>{ order: { name: :desc } }</tt>). - # * <tt>remove_index(table_name, column: column_name)</tt>: Removes the index - # specified by +column_name+. + # * <tt>remove_index(table_name, column: column_names)</tt>: Removes the index + # specified by +column_names+. # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index # specified by +index_name+. + # * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column + # +reference_name_id+ by default a integer. See + # ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details. # # == Irreversible transformations # @@ -638,7 +641,8 @@ module ActiveRecord unless @connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - if [:rename_table, :add_foreign_key].include?(method) + if [:rename_table, :add_foreign_key].include?(method) || + (method == :remove_foreign_key && !arguments.second.is_a?(Hash)) arguments[1] = proper_table_name(arguments.second, table_name_options) end end @@ -713,7 +717,9 @@ module ActiveRecord end end - def table_name_options(config = ActiveRecord::Base) + # Builds a hash for use in ActiveRecord::Migration#proper_table_name using + # the Active Record object's table_name prefix and suffix + def table_name_options(config = ActiveRecord::Base) #:nodoc: { table_name_prefix: config.table_name_prefix, table_name_suffix: config.table_name_suffix diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 36256415df..dcc2362397 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -72,7 +72,7 @@ module ActiveRecord [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, - :change_column_default, :add_reference, :remove_reference, :transaction, + :add_reference, :remove_reference, :transaction, :drop_join_table, :drop_table, :execute_block, :enable_extension, :change_column, :execute, :remove_columns, :change_column_null, :add_foreign_key, :remove_foreign_key @@ -151,19 +151,31 @@ module ActiveRecord end def invert_remove_index(args) - table, options = *args - - unless options && options.is_a?(Hash) && options[:column] - raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + table, options_or_column = *args + if (options = options_or_column).is_a?(Hash) + unless options[:column] + raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option." + end + options = options.dup + [:add_index, [table, options.delete(:column), options]] + elsif (column = options_or_column).present? + [:add_index, [table, column]] end - - options = options.dup - [:add_index, [table, options.delete(:column), options]] end alias :invert_add_belongs_to :invert_add_reference alias :invert_remove_belongs_to :invert_remove_reference + def invert_change_column_default(args) + table, column, options = *args + + unless options && options.is_a?(Hash) && options.has_key?(:from) && options.has_key?(:to) + raise ActiveRecord::IrreversibleMigration, "change_column_default is only reversible if given a :from and :to option." + end + + [:change_column_default, [table, column, from: options[:to], to: options[:from]]] + end + def invert_change_column_null(args) args[2] = !args[2] [:change_column_null, args] @@ -184,6 +196,16 @@ module ActiveRecord [:remove_foreign_key, [from_table, options]] end + def invert_remove_foreign_key(args) + from_table, to_table, remove_options = args + raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash) + + reversed_args = [from_table, to_table] + reversed_args << remove_options if remove_options + + [:add_foreign_key, reversed_args] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 3674f672cb..5a6f42ba09 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -310,6 +310,7 @@ module ActiveRecord def load_schema! @columns_hash = connection.schema_cache.columns_hash(table_name) @columns_hash.each do |name, column| + warn_if_deprecated_type(column) define_attribute( name, connection.lookup_cast_type_from_column(column), @@ -356,6 +357,28 @@ module ActiveRecord base.table_name end end + + def warn_if_deprecated_type(column) + return if attributes_to_define_after_schema_loads.key?(column.name) + if column.respond_to?(:oid) && column.sql_type.start_with?("point") + if column.array? + array_arguments = ", array: true" + else + array_arguments = "" + end + ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) + The behavior of the `:point` type will be changing in Rails 5.1 to + return a `Point` object, rather than an `Array`. If you'd like to + keep the old behavior, you can add this line to #{self.name}: + + attribute :#{column.name}, :legacy_point#{array_arguments} + + If you'd like the new behavior today, you can add this line: + + attribute :#{column.name}, :rails_5_1_point#{array_arguments} + WARNING + end + end end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index c942d0e265..c5a1488588 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -386,6 +386,9 @@ module ActiveRecord # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) options = self.nested_attributes_options[association_name] + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end attributes = attributes.with_indifferent_access existing_record = send(association_name) @@ -442,6 +445,9 @@ module ActiveRecord # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) options = self.nested_attributes_options[association_name] + if attributes_collection.respond_to?(:permitted?) + attributes_collection = attributes_collection.to_h + end unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" @@ -468,6 +474,9 @@ module ActiveRecord end attributes_collection.each do |attributes| + if attributes.respond_to?(:permitted?) + attributes = attributes.to_h + end attributes = attributes.with_indifferent_access if attributes['id'].blank? @@ -552,7 +561,9 @@ module ActiveRecord end def raise_nested_attributes_record_not_found!(association_name, record_id) - raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + model = self.class._reflect_on_association(association_name).klass.name + raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}", + model, 'id', record_id) end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 437ba31711..09c36d7b4d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -193,7 +193,7 @@ module ActiveRecord # and #destroy! raises ActiveRecord::RecordNotDestroyed. # See ActiveRecord::Callbacks for further details. def destroy! - destroy || raise(RecordNotDestroyed.new("Failed to destroy the record", self)) + destroy || _raise_record_not_destroyed end # Returns an instance of the specified +klass+ with the attributes of the @@ -211,8 +211,7 @@ module ActiveRecord def becomes(klass) became = klass.new became.instance_variable_set("@attributes", @attributes) - changed_attributes = @changed_attributes if defined?(@changed_attributes) - became.instance_variable_set("@changed_attributes", changed_attributes || {}) + became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.instance_variable_set("@errors", errors) @@ -549,5 +548,12 @@ module ActiveRecord def verify_readonly_attribute(name) raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) end + + def _raise_record_not_destroyed + @_association_destroy_exception ||= nil + raise @_association_destroy_exception || RecordNotDestroyed.new("Failed to destroy the record", self) + ensure + @_association_destroy_exception = nil + end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 4e597590e9..87a1988f2f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -6,7 +6,7 @@ module ActiveRecord delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all - delegate :find_each, :find_in_batches, to: :all + delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 5af64b717a..6dd54f9262 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -16,11 +16,11 @@ module ActiveRecord config.app_generators.orm :active_record, :migration => true, :timestamps => true - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::QueryCache" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::QueryCache - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::ConnectionAdapters::ConnectionManagement config.action_dispatch.rescue_responses.merge!( 'ActiveRecord::RecordNotFound' => :not_found, @@ -78,8 +78,8 @@ module ActiveRecord initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::Migration::CheckPending" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::Migration::CheckPending end end @@ -156,8 +156,8 @@ end_warning ActiveSupport.on_load(:active_record) do ActionDispatch::Reloader.send(hook) do if ActiveRecord::Base.connected? - ActiveRecord::Base.clear_reloadable_connections! ActiveRecord::Base.clear_cache! + ActiveRecord::Base.clear_reloadable_connections! end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d168786e71..6a72d528b4 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -12,7 +12,7 @@ db_namespace = namespace :db do end end - desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV it defaults to creating the development and test databases.' + desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV, it defaults to creating the development and test databases.' task :create => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.create_current end @@ -23,7 +23,7 @@ db_namespace = namespace :db do end end - desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to dropping the development and test databases.' + desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases.' task :drop => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.drop_current end @@ -42,15 +42,18 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.migrate - db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration + db_namespace['_dump'].invoke end + # IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false task :_dump do - case ActiveRecord::Base.schema_format - when :ruby then db_namespace["schema:dump"].invoke - when :sql then db_namespace["structure:dump"].invoke - else - raise "unknown schema format #{ActiveRecord::Base.schema_format}" + if ActiveRecord::Base.dump_schema_after_migration + case ActiveRecord::Base.schema_format + when :ruby then db_namespace["schema:dump"].invoke + when :sql then db_namespace["structure:dump"].invoke + else + raise "unknown schema format #{ActiveRecord::Base.schema_format}" + end end # Allow this task to be called as many times as required. An example is the # migrate:redo task, which calls other two internally that depend on this one. @@ -134,10 +137,7 @@ db_namespace = namespace :db do end # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.' - task :reset => [:environment, :load_config] do - db_namespace["drop"].invoke - db_namespace["setup"].invoke - end + task :reset => [ 'db:drop', 'db:setup' ] # desc "Retrieves the charset for the current environment's database" task :charset => [:environment, :load_config] do @@ -159,7 +159,7 @@ db_namespace = namespace :db do end # desc "Raises an error if there are pending migrations" - task :abort_if_pending_migrations => :environment do + task :abort_if_pending_migrations => [:environment, :load_config] do pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations if pending_migrations.any? @@ -171,17 +171,17 @@ db_namespace = namespace :db do end end - desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)' + desc 'Creates the database, loads the schema, and initializes with the seed data (use db:reset to also drop the database first)' task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed] - desc 'Load the seed data from db/seeds.rb' + desc 'Loads the seed data from db/seeds.rb' task :seed do db_namespace['abort_if_pending_migrations'].invoke ActiveRecord::Tasks::DatabaseTasks.load_seed end namespace :fixtures do - desc "Load fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." + desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task :load => [:environment, :load_config] do require 'active_record/fixtures' @@ -229,7 +229,7 @@ db_namespace = namespace :db do end namespace :schema do - desc 'Create a db/schema.rb file that is portable against any DB supported by AR' + desc 'Creates a db/schema.rb file that is portable against any DB supported by AR' task :dump => [:environment, :load_config] do require 'active_record/schema_dumper' filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') @@ -239,7 +239,7 @@ db_namespace = namespace :db do db_namespace['schema:dump'].reenable end - desc 'Load a schema.rb file into the database' + desc 'Loads a schema.rb file into the database' task :load => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end @@ -249,7 +249,7 @@ db_namespace = namespace :db do end namespace :cache do - desc 'Create a db/schema_cache.dump file.' + desc 'Creates a db/schema_cache.dump file.' task :dump => [:environment, :load_config] do con = ActiveRecord::Base.connection filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") @@ -259,7 +259,7 @@ db_namespace = namespace :db do open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) } end - desc 'Clear a db/schema_cache.dump file.' + desc 'Clears a db/schema_cache.dump file.' task :clear => [:environment, :load_config] do filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") FileUtils.rm(filename) if File.exist?(filename) @@ -269,7 +269,7 @@ db_namespace = namespace :db do end namespace :structure do - desc 'Dump the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' + desc 'Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql' task :dump => [:environment, :load_config] do filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") current_config = ActiveRecord::Tasks::DatabaseTasks.current_config @@ -285,7 +285,7 @@ db_namespace = namespace :db do db_namespace['structure:dump'].reenable end - desc "Recreate the databases from the structure.sql file" + desc "Recreates the databases from the structure.sql file" task :load => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 7d37313058..e47b7b1ed9 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -15,6 +15,7 @@ module ActiveRecord VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS + include Enumerable include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded, :predicate_builder @@ -275,38 +276,52 @@ module ActiveRecord # Returns true if there are no records. def none? - if block_given? - to_a.none? { |*block_args| yield(*block_args) } - else - empty? - end + return super if block_given? + empty? end # Returns true if there are any records. def any? - if block_given? - to_a.any? { |*block_args| yield(*block_args) } - else - !empty? - end + return super if block_given? + !empty? end # Returns true if there is exactly one record. def one? - if block_given? - to_a.one? { |*block_args| yield(*block_args) } - else - limit_value ? to_a.one? : size == 1 - end + return super if block_given? + limit_value ? to_a.one? : size == 1 end # Returns true if there is more than one record. def many? - if block_given? - to_a.many? { |*block_args| yield(*block_args) } - else - limit_value ? to_a.many? : size > 1 - end + return super if block_given? + limit_value ? to_a.many? : size > 1 + end + + # Returns a cache key that can be used to identify the records fetched by + # this query. The cache key is built with a fingerprint of the sql query, + # the number of records matched by the query and a timestamp of the last + # updated record. When a new record comes to match the query, or any of + # the existing records is updated or deleted, the cache key changes. + # + # Product.where("name like ?", "%Cosmic Encounter%").cache_key + # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000" + # + # If the collection is loaded, the method will iterate through the records + # to generate the timestamp, otherwise it will trigger one SQL query like: + # + # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%') + # + # You can also pass a custom timestamp column to fetch the timestamp of the + # last updated record. + # + # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at) + # + # You can customize the strategy to generate the key on a per model basis + # overriding ActiveRecord::Base#collection_cache_key. + def cache_key(timestamp_column = :updated_at) + @cache_keys ||= {} + @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column) end # Scope all queries to the current scope. @@ -652,6 +667,13 @@ module ActiveRecord "#<#{self.class.name} [#{entries.join(', ')}]>" end + protected + + def load_records(records) + @records = records + @loaded = true + end + private def exec_queries diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index e07580a563..beb8fa511c 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,3 +1,5 @@ +require "active_record/relation/batches/batch_enumerator" + module ActiveRecord module Batches # Looping through a collection of records from the database @@ -122,24 +124,102 @@ module ActiveRecord end end + in_batches(of: batch_size, begin_at: begin_at, end_at: end_at, load: true) do |batch| + yield batch.to_a + end + end + + # Yields ActiveRecord::Relation objects to work with a batch of records. + # + # Person.where("age > 21").in_batches do |relation| + # relation.delete_all + # sleep(10) # Throttle the delete queries + # end + # + # If you do not provide a block to #in_batches, it will return a + # BatchEnumerator which is enumerable. + # + # Person.in_batches.with_index do |relation, batch_index| + # puts "Processing relation ##{batch_index}" + # relation.each { |relation| relation.delete_all } + # end + # + # Examples of calling methods on the returned BatchEnumerator object: + # + # Person.in_batches.delete_all + # Person.in_batches.update_all(awesome: true) + # Person.in_batches.each_record(&:party_all_night!) + # + # ==== Options + # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # + # This is especially useful if you want to work with the + # ActiveRecord::Relation object instead of the array of records, or if + # you want multiple workers dealing with the same processing queue. You can + # make worker 1 handle all the records between id 0 and 10,000 and worker 2 + # handle from 10,000 and beyond (by setting the +:begin_at+ and +:end_at+ + # option on each worker). + # + # # Let's process the next 2000 records + # Person.in_batches(of: 2000, begin_at: 2000).update_all(awesome: true) + # + # An example of calling where query method on the relation: + # + # Person.in_batches.each do |relation| + # relation.update_all('age = age + 1') + # relation.where('age > 21').update_all(should_party: true) + # relation.where('age <= 21').delete_all + # end + # + # NOTE: If you are going to iterate through each record, you should call + # #each_record on the yielded BatchEnumerator: + # + # Person.in_batches.each_record(&:party_all_night!) + # + # NOTE: It's not possible to set the order. That is automatically set to + # ascending on the primary key ("id ASC") to make the batch ordering + # consistent. Therefore the primary key must be orderable, e.g an integer + # or a string. + # + # NOTE: You can't set the limit either, that's used to control the batch + # sizes. + def in_batches(of: 1000, begin_at: nil, end_at: nil, load: false) + relation = self + unless block_given? + return BatchEnumerator.new(of: of, begin_at: begin_at, end_at: end_at, relation: self) + end + if logger && (arel.orders.present? || arel.taken.present?) logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end - relation = relation.reorder(batch_order).limit(batch_size) + relation = relation.reorder(batch_order).limit(of) relation = apply_limits(relation, begin_at, end_at) - records = relation.to_a + batch_relation = relation + + loop do + if load + records = batch_relation.to_a + ids = records.map(&:id) + yielded_relation = self.where(primary_key => ids) + yielded_relation.load_records(records) + else + ids = batch_relation.pluck(primary_key) + yielded_relation = self.where(primary_key => ids) + end - while records.any? - records_size = records.size - primary_key_offset = records.last.id - raise "Primary key not included in the custom select clause" unless primary_key_offset + break if ids.empty? - yield records + primary_key_offset = ids.last + raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset - break if records_size < batch_size + yield yielded_relation - records = relation.where(table[primary_key].gt(primary_key_offset)).to_a + break if ids.length < of + batch_relation = relation.where(table[primary_key].gt(primary_key_offset)) end end diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb new file mode 100644 index 0000000000..153aae9584 --- /dev/null +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -0,0 +1,67 @@ +module ActiveRecord + module Batches + class BatchEnumerator + include Enumerable + + def initialize(of: 1000, begin_at: nil, end_at: nil, relation:) #:nodoc: + @of = of + @relation = relation + @begin_at = begin_at + @end_at = end_at + end + + # Looping through a collection of records from the database (using the + # +all+ method, for example) is very inefficient since it will try to + # instantiate all the objects at once. + # + # In that case, batch processing methods allow you to work with the + # records in batches, thereby greatly reducing memory consumption. + # + # Person.in_batches.each_record do |person| + # person.do_awesome_stuff + # end + # + # Person.where("age > 21").in_batches(of: 10).each_record do |person| + # person.party_all_night! + # end + # + # If you do not provide a block to #each_record, it will return an Enumerator + # for chaining with other methods: + # + # Person.in_batches.each_record.with_index do |person, index| + # person.award_trophy(index + 1) + # end + def each_record + return to_enum(:each_record) unless block_given? + + @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: true).each do |relation| + relation.to_a.each { |record| yield record } + end + end + + # Delegates #delete_all, #update_all, #destroy_all methods to each batch. + # + # People.in_batches.delete_all + # People.in_batches.destroy_all('age < 10') + # People.in_batches.update_all('age = age + 1') + [:delete_all, :update_all, :destroy_all].each do |method| + define_method(method) do |*args, &block| + @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false).each do |relation| + relation.send(method, *args, &block) + end + end + end + + # Yields an ActiveRecord::Relation object for each batch of records. + # + # Person.in_batches.each do |relation| + # relation.update_all(awesome: true) + # end + def each + enum = @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false) + return enum.each { |relation| yield relation } if block_given? + enum + end + end + end +end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 402b317d9c..0f6015fa93 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -71,6 +71,7 @@ module ActiveRecord # # Person.sum(:age) # => 4562 def sum(*args) + return super if block_given? calculate(:sum, *args) end @@ -138,7 +139,7 @@ module ActiveRecord # # SELECT people.id, people.name FROM people # # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']] # - # Person.pluck('DISTINCT role') + # Person.distinct.pluck(:role) # # SELECT DISTINCT role FROM people # # => ['admin', 'member', 'guest'] # @@ -161,6 +162,10 @@ module ActiveRecord end end + if loaded? && (column_names - @klass.column_names).empty? + return @records.pluck(*column_names) + end + if has_include?(column_names.first) construct_relation_for_association_calculations.pluck(*column_names) else @@ -190,7 +195,8 @@ module ActiveRecord def perform_calculation(operation, column_name) operation = operation.to_s.downcase - # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count) + # If #count is used with #distinct (i.e. `relation.distinct.count`) it is + # considered distinct. distinct = self.distinct_value if operation == "count" diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 86f2c30168..d75ec72b1a 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -18,7 +18,7 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } - const_set klass.name.gsub('::', '_'), delegate + const_set klass.name.gsub('::'.freeze, '_'.freeze), delegate cache[klass] = delegate end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 6020aa238f..009b2bad57 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -62,11 +62,8 @@ module ActiveRecord # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2) # # returns an Array of the required fields. def find(*args) - if block_given? - to_a.find(*args) { |*block_args| yield(*block_args) } - else - find_with_ids(*args) - end + return super if block_given? + find_with_ids(*args) end # Finds the first record matching the specified conditions. There @@ -88,7 +85,8 @@ module ActiveRecord def find_by!(arg, *args) where(arg, *args).take! rescue RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value" + raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", + @klass.name) end # Gives a record (or N records if a parameter is supplied) without any implied diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index dd8f0aa298..0b38666ce9 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -87,8 +87,8 @@ module ActiveRecord return if other.preload_values.empty? && other.includes_values.empty? if other.klass == relation.klass - relation.preload! other.preload_values unless other.preload_values.empty? - relation.includes! other.includes_values unless other.includes_values.empty? + relation.preload!(*other.preload_values) unless other.preload_values.empty? + relation.includes!(other.includes_values) unless other.includes_values.empty? else reflection = relation.klass.reflect_on_all_associations.find do |r| r.class_name == other.klass.name diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 43e9afe853..d26db7d4cf 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -52,7 +52,7 @@ module ActiveRecord key else key = key.to_s - key.split('.').first if key.include?('.') + key.split('.'.freeze).first if key.include?('.'.freeze) end end.compact end @@ -123,10 +123,10 @@ module ActiveRecord end def convert_dot_notation_to_hash(attributes) - dot_notation = attributes.keys.select { |s| s.include?(".") } + dot_notation = attributes.keys.select { |s| s.include?(".".freeze) } dot_notation.each do |key| - table_name, column_name = key.split(".") + table_name, column_name = key.split(".".freeze) value = attributes.delete(key) attributes[table_name] ||= {} diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index fd78db2e95..706c99c245 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -242,12 +242,9 @@ module ActiveRecord # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(*fields) - if block_given? - to_a.select { |*block_args| yield(*block_args) } - else - raise ArgumentError, 'Call this with at least one field' if fields.empty? - spawn._select!(*fields) - end + return super if block_given? + raise ArgumentError, 'Call this with at least one field' if fields.empty? + spawn._select!(*fields) end def _select!(*fields) # :nodoc: @@ -1001,15 +998,13 @@ module ActiveRecord end def arel_columns(columns) - if from_clause.value - columns - else - columns.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) - arel_table[field] - else - field - end + columns.map do |field| + if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_clause.value + arel_table[field] + elsif Symbol === field + connection.quote_table_name(field.to_s) + else + field end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index c2567311bd..ba75ffa5a1 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -119,9 +119,9 @@ module ActiveRecord end def replace_named_bind_variables(statement, bind_vars) #:nodoc: - statement.gsub(/(:?):([a-zA-Z]\w*)/) do + statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match| if $1 == ':' # skip postgresql casts - $& # return the whole match + match # return the whole match elsif bind_vars.include?(match = $2.to_sym) replace_bind_variable(bind_vars[match]) else diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index b5038104ac..cb47bf23f7 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -1,6 +1,5 @@ require 'active_record/scoping/default' require 'active_record/scoping/named' -require 'active_record/base' module ActiveRecord class SchemaMigration < ActiveRecord::Base diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 3590b8846e..a1adf8e3ee 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -100,6 +100,7 @@ module ActiveRecord end def build_default_scope(base_rel = relation) # :nodoc: + return if abstract_class? if !Base.is_a?(method(:default_scope).owner) # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 48c12dcf9f..23dc6465af 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -18,5 +18,3 @@ module ActiveRecord #:nodoc: end end end - -require 'active_record/serializers/xml_serializer' diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb deleted file mode 100644 index 89b7e0be82..0000000000 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ /dev/null @@ -1,193 +0,0 @@ -require 'active_support/core_ext/hash/conversions' - -module ActiveRecord #:nodoc: - module Serialization - include ActiveModel::Serializers::Xml - - # Builds an XML document to represent the model. Some configuration is - # available through +options+. However more complicated cases should - # override ActiveRecord::Base#to_xml. - # - # By default the generated XML document will include the processing - # instruction and all the object's attributes. For example: - # - # <?xml version="1.0" encoding="UTF-8"?> - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <id type="integer">1</id> - # <approved type="boolean">false</approved> - # <replies-count type="integer">0</replies-count> - # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>, - # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> . - # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the - # +attributes+ method. The default is to dasherize all column names, but you - # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt> - # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>. - # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+. - # - # For instance: - # - # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ]) - # - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <approved type="boolean">false</approved> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # To include first level associations use <tt>:include</tt>: - # - # firm.to_xml include: [ :account, :clients ] - # - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # Additionally, the record being serialized will be passed to a Proc's second - # parameter. This allows for ad hoc additions to the resultant document that - # incorporate the context of the record being serialized. And by leveraging the - # closure created by a Proc, to_xml can be used to add elements that normally fall - # outside of the scope of the model -- for example, generating and appending URLs - # associated with models. - # - # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <name-reverse>slangis73</name-reverse> - # </firm> - # - # To include deeper levels of associations pass a hash like this: - # - # firm.to_xml include: {account: {}, clients: {include: :address}} - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # <address> - # ... - # </address> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # <address> - # ... - # </address> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # To include any methods on the model being called use <tt>:methods</tt>: - # - # firm.to_xml methods: [ :calculated_earnings, :real_earnings ] - # - # <firm> - # # ... normal attributes as shown above ... - # <calculated-earnings>100000000000000000</calculated-earnings> - # <real-earnings>5</real-earnings> - # </firm> - # - # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a - # modified version of the options hash that was given to +to_xml+: - # - # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <abc>def</abc> - # </firm> - # - # Alternatively, you can yield the builder object as part of the +to_xml+ call: - # - # firm.to_xml do |xml| - # xml.creator do - # xml.first_name "David" - # xml.last_name "Heinemeier Hansson" - # end - # end - # - # <firm> - # # ... normal attributes as shown above ... - # <creator> - # <first_name>David</first_name> - # <last_name>Heinemeier Hansson</last_name> - # </creator> - # </firm> - # - # As noted above, you may override +to_xml+ in your ActiveRecord::Base - # subclasses to have complete control about what's generated. The general - # form of doing this is: - # - # class IHaveMyOwnXML < ActiveRecord::Base - # def to_xml(options = {}) - # require 'builder' - # options[:indent] ||= 2 - # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - # xml.instruct! unless options[:skip_instruct] - # xml.level_one do - # xml.tag!(:second_level, 'content') - # end - # end - # end - def to_xml(options = {}, &block) - XmlSerializer.new(self, options).serialize(&block) - end - end - - class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: - def compute_type - klass = @serializable.class - cast_type = klass.type_for_attribute(name) - - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type - - { :text => :string, - :time => :datetime }[type] || type - end - protected :compute_type - end - end -end diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb index b0b86865fd..b3644bf569 100644 --- a/activerecord/lib/active_record/suppressor.rb +++ b/activerecord/lib/active_record/suppressor.rb @@ -37,8 +37,7 @@ module ActiveRecord end end - # Ignore saving events if we're in suppression mode. - def save!(*args) # :nodoc: + def create_or_update(*args) # :nodoc: SuppressorRegistry.suppressed[self.class.name] ? true : super end end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 3dd6321a97..41f1c55c3c 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -41,7 +41,7 @@ module ActiveRecord association = klass._reflect_on_association(table_name) if association && !association.polymorphic? association_klass = association.klass - arel_table = association_klass.arel_table + arel_table = association_klass.arel_table.alias(table_name) else type_caster = TypeCaster::Connection.new(klass, table_name) association_klass = nil diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index d7da95c8a9..55f839444b 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -1,5 +1,3 @@ -require 'shellwords' - module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: @@ -55,19 +53,22 @@ module ActiveRecord when String ActiveRecord::Base.dump_schemas end + + args = ['-i', '-s', '-x', '-O', '-f', filename] unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + args << search_path.split(',').map do |part| + "--schema=#{part.strip}" + end.join(' ') end - - command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" - raise 'Error dumping database' unless Kernel.system(command) - + args << configuration['database'] + run_cmd('pg_dump', args, 'dumping') File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end def structure_load(filename) set_psql_env - Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}") + args = [ '-q', '-f', filename, configuration['database'] ] + run_cmd('psql', args, 'loading' ) end private @@ -93,6 +94,17 @@ module ActiveRecord ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] end + + def run_cmd(cmd, args, action) + fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) + end + + def run_cmd_error(cmd, args, action) + msg = "failed to execute:\n" + msg << "#{cmd} #{args.join(' ')}\n\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 311dacb449..887d7a5903 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -196,17 +196,16 @@ module ActiveRecord # automatically released. The following example demonstrates the problem: # # Model.connection.transaction do # BEGIN - # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1 + # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1 # Model.connection.create_table(...) # active_record_1 now automatically released - # end # RELEASE savepoint active_record_1 + # end # RELEASE SAVEPOINT active_record_1 # # ^^^^ BOOM! database error! # end # # Note that "TRUNCATE" is also a MySQL DDL statement! module ClassMethods - # See ActiveRecord::Transactions::ClassMethods for detailed documentation. + # See the ConnectionAdapters::DatabaseStatements#transaction API docs. def transaction(options = {}, &block) - # See the ConnectionAdapters::DatabaseStatements#transaction API docs. connection.transaction(options, &block) end @@ -319,8 +318,8 @@ module ActiveRecord end def before_committed! # :nodoc: - run_callbacks :before_commit_without_transaction_enrollment - run_callbacks :before_commit + _run_before_commit_without_transaction_enrollment_callbacks + _run_before_commit_callbacks end # Call the +after_commit+ callbacks. @@ -329,8 +328,8 @@ module ActiveRecord # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: if should_run_callbacks && destroyed? || persisted? - run_callbacks :commit_without_transaction_enrollment - run_callbacks :commit + _run_commit_without_transaction_enrollment_callbacks + _run_commit_callbacks end ensure force_clear_transaction_record_state @@ -340,8 +339,8 @@ module ActiveRecord # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: if should_run_callbacks - run_callbacks :rollback - run_callbacks :rollback_without_transaction_enrollment + _run_rollback_callbacks + _run_rollback_without_transaction_enrollment_callbacks end ensure restore_transaction_record_state(force_restore_state) @@ -380,6 +379,10 @@ module ActiveRecord raise ActiveRecord::Rollback unless status end status + ensure + if @transaction_state && @transaction_state.committed? + clear_transaction_record_state + end end protected diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index 867b5f75c7..f200a92d10 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -8,7 +8,7 @@ module ActiveRecord end def type_cast_for_schema(value) - value.to_s + value.to_s.inspect end private diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index e227212827..34d96b19fe 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -76,4 +76,5 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" require "active_record/validations/presence" +require "active_record/validations/absence" require "active_record/validations/length" diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb new file mode 100644 index 0000000000..2e19e6dc5c --- /dev/null +++ b/activerecord/lib/active_record/validations/absence.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module Validations + class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc: + def validate_each(record, attribute, association_or_value) + return unless should_validate?(record) + if record.class._reflect_on_association(attribute) + association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) + end + super + end + end + + module ClassMethods + # Validates that the specified attributes are not present (as defined by + # Object#present?). If the attribute is an association, the associated object + # is considered absent if it was marked for destruction. + # + # See ActiveModel::Validations::HelperMethods.validates_absence_of for more information. + def validates_absence_of(*attr_names) + validates_with AbsenceValidator, _merge_attributes(attr_names) + end + end + end +end diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index 5991fbad8e..69e048eef1 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -22,7 +22,10 @@ module ActiveRecord end module ClassMethods - # See <tt>ActiveModel::Validation::LengthValidator</tt> for more information. + # Validates that the specified attributes match the length restrictions supplied. + # If the attribute is an association, records that are marked for destruction are not counted. + # + # See ActiveModel::Validations::HelperMethods.validates_length_of for more information. def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index a9b791397b..23a3985d35 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -1,18 +1,12 @@ module ActiveRecord module Validations class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: - def validate(record) + def validate_each(record, attribute, association_or_value) return unless should_validate?(record) - super - attributes.each do |attribute| - next unless record.class._reflect_on_association(attribute) - associated_records = Array.wrap(record.send(attribute)) - - # Superclass validates presence. Ensure present records aren't about to be destroyed. - if associated_records.present? && associated_records.all?(&:marked_for_destruction?) - record.errors.add(attribute, :blank, options) - end + if record.class._reflect_on_association(attribute) + association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) end + super end end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5106f4e127..32d17a1392 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -17,7 +17,9 @@ module ActiveRecord value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) - relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted? + if record.persisted? && finder_class.primary_key.to_s != attribute.to_s + relation = relation.where.not(finder_class.primary_key => record.id) + end relation = scope_relation(record, table, relation) relation = relation.merge(options[:conditions]) if options[:conditions] |