aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb24
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb155
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb43
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb34
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb51
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb24
-rw-r--r--activerecord/lib/active_record/callbacks.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb8
-rw-r--r--activerecord/lib/active_record/fixtures.rb3
-rw-r--r--activerecord/lib/active_record/locale/en.yml6
-rw-r--r--activerecord/lib/active_record/querying.rb1
-rw-r--r--activerecord/lib/active_record/railties/databases.rake2
-rw-r--r--activerecord/lib/active_record/relation.rb53
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb4
-rw-r--r--activerecord/lib/active_record/relation/merger.rb22
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb71
-rw-r--r--activerecord/lib/active_record/timestamp.rb3
-rw-r--r--activerecord/lib/active_record/validations/presence.rb6
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb3
27 files changed, 404 insertions, 178 deletions
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index ce5bf15f10..c1cd3a4ae3 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -30,17 +30,21 @@ module ActiveRecord
# option references an association's column), it will fallback to the table
# join strategy.
class Preloader #:nodoc:
- autoload :Association, 'active_record/associations/preloader/association'
- autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
- autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
- autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
+ extend ActiveSupport::Autoload
- autoload :HasMany, 'active_record/associations/preloader/has_many'
- autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
- autoload :HasOne, 'active_record/associations/preloader/has_one'
- autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
- autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
+ eager_autoload do
+ autoload :Association, 'active_record/associations/preloader/association'
+ autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
+ autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
+ autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
+
+ autoload :HasMany, 'active_record/associations/preloader/has_many'
+ autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
+ autoload :HasOne, 'active_record/associations/preloader/has_one'
+ autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
+ autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
+ autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
+ end
attr_reader :records, :associations, :preload_scope, :model
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index af13b75a9d..6c5e2ac05d 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -57,9 +57,8 @@ module ActiveRecord
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
- # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
- # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
- # attribute will be set to +nil+.
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum and
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
def assign_multiparameter_attributes(pairs)
execute_callstack_for_multiparameter_attributes(
extract_callstack_for_multiparameter_attributes(pairs)
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 0aff2562b8..1dd9a3d45d 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -2,7 +2,7 @@ require 'active_support/core_ext/enumerable'
module ActiveRecord
# = Active Record Attribute Methods
- module AttributeMethods #:nodoc:
+ module AttributeMethods
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
@@ -20,7 +20,7 @@ module ActiveRecord
module ClassMethods
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
- def define_attribute_methods
+ def define_attribute_methods # :nodoc:
# Use a mutex; we don't want two thread simaltaneously trying to define
# attribute methods.
@attribute_methods_mutex.synchronize do
@@ -31,15 +31,29 @@ module ActiveRecord
end
end
- def attribute_methods_generated?
+ def attribute_methods_generated? # :nodoc:
@attribute_methods_generated ||= false
end
- def undefine_attribute_methods
+ def undefine_attribute_methods # :nodoc:
super if attribute_methods_generated?
@attribute_methods_generated = false
end
+ # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
+ # \Active \Record method is defined in the model, otherwise +false+.
+ #
+ # class Person < ActiveRecord::Base
+ # def save
+ # 'already defined by Active Record'
+ # end
+ # end
+ #
+ # Person.instance_method_already_implemented?(:save)
+ # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
+ #
+ # Person.instance_method_already_implemented?(:name)
+ # # => false
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
@@ -56,11 +70,11 @@ module ActiveRecord
# A method name is 'dangerous' if it is already defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
- def dangerous_attribute_method?(name)
+ def dangerous_attribute_method?(name) # :nodoc:
method_defined_within?(name, Base)
end
- def method_defined_within?(name, klass, sup = klass.superclass)
+ def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
if klass.method_defined?(name) || klass.private_method_defined?(name)
if sup.method_defined?(name) || sup.private_method_defined?(name)
klass.instance_method(name).owner != sup.instance_method(name).owner
@@ -72,13 +86,27 @@ module ActiveRecord
end
end
+ # Returns +true+ if +attribute+ is an attribute method and table exists,
+ # +false+ otherwise.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # Person.attribute_method?('name') # => true
+ # Person.attribute_method?(:age=) # => true
+ # Person.attribute_method?(:nothing) # => false
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
end
- # Returns an array of column names as strings if it's not
- # an abstract class and table exists.
- # Otherwise it returns an empty array.
+ # Returns an array of column names as strings if it's not an abstract class and
+ # table exists. Otherwise it returns an empty array.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # Person.attribute_names
+ # # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
column_names
@@ -90,7 +118,7 @@ module ActiveRecord
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
- def method_missing(method, *args, &block)
+ def method_missing(method, *args, &block) # :nodoc:
unless self.class.attribute_methods_generated?
self.class.define_attribute_methods
@@ -104,7 +132,7 @@ module ActiveRecord
end
end
- def attribute_missing(match, *args, &block)
+ def attribute_missing(match, *args, &block) # :nodoc:
if self.class.columns_hash[match.attr_name]
ActiveSupport::Deprecation.warn(
"The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
@@ -118,22 +146,60 @@ module ActiveRecord
super
end
+ # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
+ # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
+ # which will all return +true+. It also define the attribute methods if they have
+ # not been generated.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.respond_to(:name) # => true
+ # person.respond_to(:name=) # => true
+ # person.respond_to(:name?) # => true
+ # person.respond_to('age') # => true
+ # person.respond_to('age=') # => true
+ # person.respond_to('age?') # => true
+ # person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
self.class.define_attribute_methods unless self.class.attribute_methods_generated?
super
end
- # Returns true if the given attribute is in the attributes hash
+ # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.has_attribute?(:name) # => true
+ # person.has_attribute?('age') # => true
+ # person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
@attributes.has_key?(attr_name.to_s)
end
# Returns an array of names for the attributes available on this object.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.attribute_names
+ # # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attributes.keys
end
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.create(name: 'Francesco', age: 22)
+ # person.attributes
+ # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
def attributes
attribute_names.each_with_object({}) { |name, attrs|
attrs[name] = read_attribute(name)
@@ -146,13 +212,13 @@ module ActiveRecord
# <tt>:db</tt> format. Other attributes return the value of
# <tt>#inspect</tt> without modification.
#
- # person = Person.create!(:name => "David Heinemeier Hansson " * 3)
+ # person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
# person.attribute_for_inspect(:name)
- # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\""
#
# person.attribute_for_inspect(:created_at)
- # # => '"2009-01-12 04:48:57"'
+ # # => "\"2012-10-22 00:15:07\""
def attribute_for_inspect(attr_name)
value = read_attribute(attr_name)
@@ -165,14 +231,38 @@ module ActiveRecord
end
end
- # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
- # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
+ # Returns +true+ if the specified +attribute+ has been set by the user or by a
+ # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
+ # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
+ # Note that it always returns +true+ with boolean attributes.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # person = Task.new(title: '', is_done: false)
+ # person.attribute_present?(:title) # => false
+ # person.attribute_present?(:is_done) # => true
+ # person.name = 'Francesco'
+ # person.is_done = true
+ # person.attribute_present?(:title) # => true
+ # person.attribute_present?(:is_done) # => true
def attribute_present?(attribute)
value = read_attribute(attribute)
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
end
- # Returns the column object for the named attribute.
+ # Returns the column object for the named attribute. Returns +nil+ if the
+ # named attribute not exists.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
+ # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
+ #
+ # person.column_for_attribute(:nothing)
+ # # => nil
def column_for_attribute(name)
# FIXME: should this return a null object for columns that don't exist?
self.class.columns_hash[name.to_s]
@@ -180,42 +270,57 @@ module ActiveRecord
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
- # (Alias for the protected read_attribute method).
+ # (Alias for <tt>read_attribute</tt>).
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new(name: 'Francesco', age: '22')
+ # person[:name] # => "Francesco"
+ # person[:age] # => 22
def [](attr_name)
read_attribute(attr_name)
end
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected write_attribute method).
+ # (Alias for the protected <tt>write_attribute</tt> method).
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person[:age] = '22'
+ # person[:age] # => 22
+ # person[:age] # => Fixnum
def []=(attr_name, value)
write_attribute(attr_name, value)
end
protected
- def clone_attributes(reader_method = :read_attribute, attributes = {})
+ def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
attribute_names.each do |name|
attributes[name] = clone_attribute_value(reader_method, name)
end
attributes
end
- def clone_attribute_value(reader_method, attribute_name)
+ def clone_attribute_value(reader_method, attribute_name) # :nodoc:
value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value
rescue TypeError, NoMethodError
value
end
- def arel_attributes_with_values_for_create(attribute_names)
+ def arel_attributes_with_values_for_create(attribute_names) # :nodoc:
arel_attributes_with_values(attributes_for_create(attribute_names))
end
- def arel_attributes_with_values_for_update(attribute_names)
+ def arel_attributes_with_values_for_update(attribute_names) # :nodoc:
arel_attributes_with_values(attributes_for_update(attribute_names))
end
- def attribute_method?(attr_name)
+ def attribute_method?(attr_name) # :nodoc:
defined?(@attributes) && @attributes.include?(attr_name)
end
diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
index d4f529acbf..faee99ccd1 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -1,5 +1,28 @@
module ActiveRecord
module AttributeMethods
+ # = Active Record Attribute Methods Before Type Cast
+ #
+ # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to
+ # read the value of the attributes before typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
+ # task.id # => 1
+ # task.completed_on # => Sun, 21 Oct 2012
+ #
+ # task.attributes_before_type_cast
+ # # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
+ # task.read_attribute_before_type_cast('id') # => 1
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
+ #
+ # In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
+ # it declares a method for all attributes with the <tt>*_before_type_cast</tt>
+ # suffix.
+ #
+ # task.id_before_type_cast # => "1"
+ # task.completed_on_before_type_cast # => "2012-10-21"
module BeforeTypeCast
extend ActiveSupport::Concern
@@ -7,11 +30,31 @@ module ActiveRecord
attribute_method_suffix "_before_type_cast"
end
+ # Returns the value of the attribute identified by +attr_name+ before
+ # typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
+ # task.read_attribute('id') # => 1
+ # task.read_attribute_before_type_cast('id') # => '1'
+ # task.read_attribute('completed_on') # => Sun, 21 Oct 2012
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
def read_attribute_before_type_cast(attr_name)
@attributes[attr_name]
end
# Returns a hash of attributes before typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
+ # task.attributes
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
+ # task.attributes_before_type_cast
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
@attributes
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 7a5bb9e863..59f209cec8 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,9 +1,10 @@
require 'active_support/core_ext/module/attribute_accessors'
+require 'active_support/deprecation'
module ActiveRecord
ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :partial_updates, instance_accessor: false
- self.partial_updates = true
+ mattr_accessor :partial_writes, instance_accessor: false
+ self.partial_writes = true
end
module AttributeMethods
@@ -17,7 +18,18 @@ module ActiveRecord
raise "You cannot include Dirty after Timestamp"
end
- config_attribute :partial_updates
+ config_attribute :partial_writes
+
+ def self.partial_updates=(v); self.partial_writes = v; end
+ def self.partial_updates?; partial_writes?; end
+ def self.partial_updates; partial_writes; end
+
+ ActiveSupport::Deprecation.deprecate_methods(
+ singleton_class,
+ :partial_updates= => :partial_writes=,
+ :partial_updates? => :partial_writes?,
+ :partial_updates => :partial_writes
+ )
end
# Attempts to +save+ the record and clears changed attributes if successful.
@@ -64,26 +76,16 @@ module ActiveRecord
end
def update(*)
- partial_updates? ? super(keys_for_partial_update) : super
+ partial_writes? ? super(keys_for_partial_write) : super
end
def create(*)
- if partial_updates?
- keys = keys_for_partial_update
-
- # This is an extremely bloody annoying necessity to work around mysql being crap.
- # See test_mysql_text_not_null_defaults
- keys.concat self.class.columns.select(&:explicit_default?).map(&:name)
-
- super keys
- else
- super
- end
+ partial_writes? ? super(keys_for_partial_write) : super
end
# Serialized attributes should always be written in case they've been
# changed in place.
- def keys_for_partial_update
+ def keys_for_partial_write
changed | (attributes.keys & self.class.serialized_attributes.keys)
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 6213b5dcd5..46fd6ebfb3 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -35,21 +35,36 @@ module ActiveRecord
protected
- # We want to generate the methods via module_eval rather than define_method,
- # because define_method is slower on dispatch and uses more memory (because it
- # creates a closure).
+ # We want to generate the methods via module_eval rather than
+ # define_method, because define_method is slower on dispatch and
+ # uses more memory (because it creates a closure).
#
- # But sometimes the database might return columns with characters that are not
- # allowed in normal method names (like 'my_column(omg)'. So to work around this
- # we first define with the __temp__ identifier, and then use alias method to
- # rename it to what we want.
- def define_method_attribute(attr_name)
+ # But sometimes the database might return columns with
+ # characters that are not allowed in normal method names (like
+ # 'my_column(omg)'. So to work around this we first define with
+ # the __temp__ identifier, and then use alias method to rename
+ # it to what we want.
+ #
+ # We are also defining a constant to hold the frozen string of
+ # the attribute name. Using a constant means that we do not have
+ # to allocate an object on each call to the attribute method.
+ # Making it frozen means that it doesn't get duped when used to
+ # key the @attributes_cache in read_attribute.
+ def define_method_attribute(name)
+ safe_name = name.unpack('h*').first
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__
- read_attribute(:'#{attr_name}') { |n| missing_attribute(n, caller) }
+ module AttrNames
+ unless defined? ATTR_#{safe_name}
+ ATTR_#{safe_name} = #{name.inspect}.freeze
+ end
+ end
+
+ def __temp__#{safe_name}
+ read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
end
- alias_method '#{attr_name}', :__temp__
- undef_method :__temp__
+
+ alias_method #{name.inspect}, :__temp__#{safe_name}
+ undef_method :__temp__#{safe_name}
STR
end
@@ -70,17 +85,13 @@ module ActiveRecord
# it has been typecast (for example, "2004-12-12" in a data column is cast
# to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name)
- return unless attr_name
- name_sym = attr_name.to_sym
-
# If it's cached, just return it
# We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829.
- @attributes_cache[name_sym] || @attributes_cache.fetch(name_sym) {
- name = attr_name.to_s
-
+ name = attr_name.to_s
+ @attributes_cache[name] || @attributes_cache.fetch(name) {
column = @columns_hash.fetch(name) {
return @attributes.fetch(name) {
- if name_sym == :id && self.class.primary_key != name
+ if name == 'id' && self.class.primary_key != name
read_attribute(self.class.primary_key)
end
}
@@ -91,7 +102,7 @@ module ActiveRecord
}
if self.class.cache_attribute?(name)
- @attributes_cache[name_sym] = column.type_cast(value)
+ @attributes_cache[name] = column.type_cast(value)
else
column.type_cast value
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index b9a69cdb0a..f36a5806a9 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -50,7 +50,7 @@ module ActiveRecord
if (rounded_value != rounded_time) || (!rounded_value && original_time)
write_attribute("#{attr_name}", original_time)
#{attr_name}_will_change!
- @attributes_cache[:"#{attr_name}"] = zoned_time
+ @attributes_cache["#{attr_name}"] = zoned_time
end
end
EOV
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 6eb9e25fd9..cd33494cc3 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -9,15 +9,19 @@ module ActiveRecord
module ClassMethods
protected
- def define_method_attribute=(attr_name)
- if attr_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP
- generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
- else
- generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value|
- write_attribute(attr_name, new_value)
- end
+
+ # See define_method_attribute in read.rb for an explanation of
+ # this code.
+ def define_method_attribute=(name)
+ safe_name = name.unpack('h*').first
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ write_attribute(AttrNames::ATTR_#{safe_name}, value)
end
- end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
+ end
end
# Updates the attribute identified by <tt>attr_name</tt> with the
@@ -26,13 +30,13 @@ module ActiveRecord
def write_attribute(attr_name, value)
attr_name = attr_name.to_s
attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
- @attributes_cache.delete(attr_name.to_sym)
+ @attributes_cache.delete(attr_name)
column = column_for_attribute(attr_name)
# If we're dealing with a binary column, write the data to the cache
# so we don't attempt to typecast multiple times.
if column && column.binary?
- @attributes_cache[attr_name.to_sym] = value
+ @attributes_cache[attr_name] = value
end
if column || @attributes.has_key?(attr_name)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 188a06e448..725bfffef2 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -203,10 +203,9 @@ 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 +dependent: destroy+ option.
#
- # Let's take at the code below:
+ # Let's look at the code below:
#
# class Topic < ActiveRecord::Base
# has_many :children, dependent: destroy
@@ -219,9 +218,8 @@ module ActiveRecord
# end
# end
#
- # In this case the problem is that when the +before_destroy+ is executed, the children are not available
- # because the dependent destroy gets executed first. To solve this issue it is possible to use the
- # +prepend+ option on the +before_destroy+ callback.
+ # In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available
+ # because the +destroy+ callback gets executed first. You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
#
# class Topic < ActiveRecord::Base
# has_many :children, dependent: destroy
@@ -234,8 +232,7 @@ module ActiveRecord
# end
# end
#
- # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and
- # the data is still available.
+ # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available.
#
# == Transactions
#
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 8c83c4f5db..b0e7bd7e82 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -4,17 +4,19 @@ module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
class Column < ConnectionAdapters::Column # :nodoc:
- attr_reader :collation
+ attr_reader :collation, :strict
- def initialize(name, default, sql_type = nil, null = true, collation = nil)
- super(name, default, sql_type, null)
+ def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false)
+ @strict = strict
@collation = collation
+
+ super(name, default, sql_type, null)
end
def extract_default(default)
if sql_type =~ /blob/i || type == :text
if default.blank?
- return null ? nil : ''
+ null || strict ? nil : ''
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
@@ -30,10 +32,6 @@ module ActiveRecord
super
end
- def explicit_default?
- !null && (sql_type =~ /blob/i || type == :text)
- end
-
# Must return the relevant concrete adapter
def adapter
raise NotImplementedError
@@ -571,6 +569,10 @@ module ActiveRecord
where_sql
end
+ def strict_mode?
+ @config.fetch(:strict, true)
+ end
+
protected
# MySQL is too stupid to create a temporary table for use subquery, so we have
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 2028abf6f0..816b5e17c1 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -53,10 +53,6 @@ module ActiveRecord
!default.nil?
end
- def explicit_default?
- false
- end
-
# Returns the Ruby class that corresponds to the abstract data type.
def klass
case type
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 328d080687..879eec7fcf 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -53,7 +53,7 @@ module ActiveRecord
end
def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation)
+ Column.new(field, default, type, null, collation, strict_mode?)
end
def error_number(exception)
@@ -259,9 +259,7 @@ module ActiveRecord
# Make MySQL reject illegal values rather than truncating or
# blanking them. See
# http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables
- if @config.fetch(:strict, true)
- variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'"
- end
+ variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'" if strict_mode?
encoding = @config[:encoding]
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 0b936bbf39..76667616a1 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -150,7 +150,7 @@ module ActiveRecord
end
def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation)
+ Column.new(field, default, type, null, collation, strict_mode?)
end
def error_number(exception) # :nodoc:
@@ -546,9 +546,7 @@ module ActiveRecord
# Make MySQL reject illegal values rather than truncating or
# blanking them. See
# http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables
- if @config.fetch(:strict, true)
- execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging)
- end
+ execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging) if strict_mode?
end
def select(sql, name = nil, binds = [])
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 37d43d891d..9d3fa18e3a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -90,7 +90,7 @@ module ActiveRecord
else super(value, column)
end
when IPAddr
- return super(value, column) unless ['inet','cidr'].includes? column.sql_type
+ return super(value, column) unless ['inet','cidr'].include? column.sql_type
PostgreSQLColumn.cidr_to_string(value)
else
super(value, column)
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 2264595751..7cad8f94cf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -280,16 +280,13 @@ module ActiveRecord
end_sql
if result.nil? or result.empty?
- # If that fails, try parsing the primary key's default value.
- # Support the 7.x and 8.0 nextval('foo'::text) as well as
- # the 8.1+ nextval('foo'::regclass).
result = query(<<-end_sql, 'SCHEMA')[0]
SELECT attr.attname,
CASE
- WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN
- substr(split_part(def.adsrc, '''', 2),
- strpos(split_part(def.adsrc, '''', 2), '.')+1)
- ELSE split_part(def.adsrc, '''', 2)
+ WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
+ substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
+ strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
+ ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2)
END
FROM pg_class t
JOIN pg_attribute attr ON (t.oid = attrelid)
@@ -297,7 +294,7 @@ module ActiveRecord
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
- AND def.adsrc ~* 'nextval'
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval'
end_sql
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index bd375ad15a..e18464fa35 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -78,11 +78,8 @@ module ActiveRecord
when /\A\(?(-?\d+(\.\d*)?\)?)\z/
$1
# Character types
- when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
+ when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
$1
- # Character types (8.1 formatting)
- when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m
- $1.gsub(/\\(\d\d\d)/) { $1.oct.chr }
# Binary data types
when /\A'(.*)'::bytea\z/m
$1
@@ -763,7 +760,8 @@ module ActiveRecord
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name) #:nodoc:
exec_query(<<-end_sql, 'SCHEMA').rows
- SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull, a.atttypid, a.atttypmod
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod),
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 899e89a6f5..7f1d62af39 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -657,7 +657,8 @@ module ActiveRecord
#--
# Deprecate 'Fixtures' in favor of 'FixtureSet'.
#++
- Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', '::ActiveRecord::FixtureSet')
+ # :nodoc:
+ Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet')
class Fixture #:nodoc:
include Enumerable
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index 896132d566..b1fbd38622 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -4,11 +4,15 @@ en:
#created_at: "Created at"
#updated_at: "Updated at"
+ # Default error messages
+ errors:
+ messages:
+ taken: "has already been taken"
+
# Active Record models configuration
activerecord:
errors:
messages:
- taken: "has already been taken"
record_invalid: "Validation failed: %{errors}"
restrict_dependent_destroy:
one: "Cannot delete record because a dependent %{record} exists"
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 13e09eda53..45f6a78428 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -3,6 +3,7 @@ module ActiveRecord
module Querying
delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all
delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all
+ 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
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 69a9526fcc..0a9caa25b2 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -307,7 +307,7 @@ db_namespace = namespace :db do
# desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config(:env => (ENV['RAILS_ENV'] || 'test'))
+ current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
case current_config['adapter']
when /mysql/, /postgresql/, /sqlite/
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index ed80422336..2e2286e4fd 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -91,8 +91,10 @@ module ActiveRecord
end
def initialize_copy(other)
- @values = @values.dup
- @values[:bind] = @values[:bind].dup if @values[:bind]
+ # This method is a hot spot, so for now, use Hash[] to dup the hash.
+ # https://bugs.ruby-lang.org/issues/7166
+ @values = Hash[@values]
+ @values[:bind] = @values[:bind].dup if @values.key? :bind
reset
end
@@ -127,46 +129,53 @@ module ActiveRecord
scoping { @klass.create!(*args, &block) }
end
- # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
- #
- # Expects arguments in the same format as +Base.create+.
+ def first_or_create(attributes = nil, &block) # :nodoc:
+ first || create(attributes, &block)
+ end
+
+ def first_or_create!(attributes = nil, &block) # :nodoc:
+ first || create!(attributes, &block)
+ end
+
+ def first_or_initialize(attributes = nil, &block) # :nodoc:
+ first || new(attributes, &block)
+ end
+
+ # Finds the first record with the given attributes, or creates a record with the attributes
+ # if one is not found.
#
# ==== Examples
# # Find the first user named Penélope or create a new one.
- # User.where(:first_name => 'Penélope').first_or_create
+ # User.find_or_create_by(first_name: 'Penélope')
# # => <User id: 1, first_name: 'Penélope', last_name: nil>
#
# # Find the first user named Penélope or create a new one.
# # We already have one so the existing record will be returned.
- # User.where(:first_name => 'Penélope').first_or_create
+ # User.find_or_create_by(first_name: 'Penélope')
# # => <User id: 1, first_name: 'Penélope', last_name: nil>
#
# # Find the first user named Scarlett or create a new one with a particular last name.
- # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')
+ # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
# # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
#
# # Find the first user named Scarlett or create a new one with a different last name.
# # We already have one so the existing record will be returned.
- # User.where(:first_name => 'Scarlett').first_or_create do |user|
+ # User.find_or_create_by(first_name: 'Scarlett') do |user|
# user.last_name = "O'Hara"
# end
# # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
- def first_or_create(attributes = nil, &block)
- first || create(attributes, &block)
+ def find_or_create_by(attributes, &block)
+ find_by(attributes) || create(attributes, &block)
end
- # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
- #
- # Expects arguments in the same format as <tt>Base.create!</tt>.
- def first_or_create!(attributes = nil, &block)
- first || create!(attributes, &block)
+ # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
+ def find_or_create_by!(attributes, &block)
+ find_by(attributes) || create!(attributes, &block)
end
- # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
- #
- # Expects arguments in the same format as <tt>Base.new</tt>.
- def first_or_initialize(attributes = nil, &block)
- first || new(attributes, &block)
+ # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
+ def find_or_initialize_by(attributes, &block)
+ find_by(attributes) || new(attributes, &block)
end
# Runs EXPLAIN on the query or queries triggered by this relation and
@@ -540,7 +549,7 @@ module ActiveRecord
end
def values
- @values.dup
+ Hash[@values]
end
def inspect
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 7c43d844d0..a7d2f4bd24 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -343,13 +343,13 @@ module ActiveRecord
def column_for(field)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.columns.detect { |c| c.name.to_s == field_name }
+ @klass.columns_hash[field_name]
end
def type_cast_calculated_value(value, column, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || '0', column)
+ when 'sum' then type_cast_using_column(value || 0, column)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
else type_cast_using_column(value, column)
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index e5b50673da..59226d316e 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -22,7 +22,17 @@ module ActiveRecord
# the values.
def other
other = Relation.new(relation.klass, relation.table)
- hash.each { |k, v| other.send("#{k}!", v) }
+ hash.each { |k, v|
+ if k == :joins
+ if Hash === v
+ other.joins!(v)
+ else
+ other.joins!(*v)
+ end
+ else
+ other.send("#{k}!", v)
+ end
+ }
other
end
end
@@ -39,16 +49,18 @@ module ActiveRecord
@values = other.values
end
+ NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS +
+ Relation::MULTI_VALUE_METHODS -
+ [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
+
def normal_values
- Relation::SINGLE_VALUE_METHODS +
- Relation::MULTI_VALUE_METHODS -
- [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from]
+ NORMAL_VALUES
end
def merge
normal_values.each do |name|
value = values[name]
- relation.send("#{name}!", value) unless value.blank?
+ relation.send("#{name}!", *value) unless value.blank?
end
merge_multi_values
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 3c59bd8a68..14bcb337e9 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -202,6 +202,15 @@ module ActiveRecord
#
# User.order('name DESC, email')
# => SELECT "users".* FROM "users" ORDER BY name DESC, email
+ #
+ # User.order(:name)
+ # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
+ #
+ # User.order(email: :desc)
+ # => SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
+ #
+ # User.order(:name, email: :desc)
+ # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
def order(*args)
args.blank? ? self : spawn.order!(*args)
end
@@ -210,6 +219,8 @@ module ActiveRecord
def order!(*args)
args.flatten!
+ validate_order_args args
+
references = args.reject { |arg| Arel::Node === arg }
references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
references!(references) if references.any?
@@ -235,6 +246,8 @@ module ActiveRecord
def reorder!(*args)
args.flatten!
+ validate_order_args args
+
self.reordering_value = true
self.order_values = args
self
@@ -245,13 +258,11 @@ module ActiveRecord
# User.joins(:posts)
# => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
def joins(*args)
- args.compact.blank? ? self : spawn.joins!(*args)
+ args.compact.blank? ? self : spawn.joins!(*args.flatten)
end
# Like #joins, but modifies relation in place.
def joins!(*args)
- args.flatten!
-
self.joins_values += args
self
end
@@ -658,9 +669,7 @@ module ActiveRecord
arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty?
- order = order_values
- order = reverse_sql_order(order) if reverse_order_value
- arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty?
+ build_order(arel)
build_select(arel, select_values.uniq)
@@ -729,22 +738,22 @@ module ActiveRecord
buckets = joins.group_by do |join|
case join
when String
- 'string_join'
+ :string_join
when Hash, Symbol, Array
- 'association_join'
+ :association_join
when ActiveRecord::Associations::JoinDependency::JoinAssociation
- 'stashed_join'
+ :stashed_join
when Arel::Nodes::Join
- 'join_node'
+ :join_node
else
raise 'unknown class: %s' % join.class.name
end
end
- association_joins = buckets['association_join'] || []
- stashed_association_joins = buckets['stashed_join'] || []
- join_nodes = (buckets['join_node'] || []).uniq
- string_joins = (buckets['string_join'] || []).map { |x|
+ association_joins = buckets[:association_join] || []
+ stashed_association_joins = buckets[:stashed_join] || []
+ join_nodes = (buckets[:join_node] || []).uniq
+ string_joins = (buckets[:string_join] || []).map { |x|
x.strip
}.uniq
@@ -786,11 +795,17 @@ module ActiveRecord
case o
when Arel::Nodes::Ordering
o.reverse
- when String, Symbol
+ when String
o.to_s.split(',').collect do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
+ when Symbol
+ { o => :desc }
+ when Hash
+ o.each_with_object({}) do |(field, dir), memo|
+ memo[field] = (dir == :asc ? :desc : :asc )
+ end
else
o
end
@@ -801,5 +816,31 @@ module ActiveRecord
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
end
+ def build_order(arel)
+ orders = order_values
+ orders = reverse_sql_order(orders) if reverse_order_value
+
+ orders = orders.uniq.reject(&:blank?).map do |order|
+ case order
+ when Symbol
+ table[order].asc
+ when Hash
+ order.map { |field, dir| table[field].send(dir) }
+ else
+ order
+ end
+ end.flatten
+
+ arel.order(*orders) unless orders.empty?
+ end
+
+ def validate_order_args(args)
+ args.select { |a| Hash === a }.each do |h|
+ unless (h.values - [:asc, :desc]).empty?
+ raise ArgumentError, 'Direction should be :asc or :desc'
+ end
+ end
+ end
+
end
end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index bf95ccb298..ec4588f601 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -42,6 +42,7 @@ module ActiveRecord
def initialize_dup(other) # :nodoc:
clear_timestamp_attributes
+ super
end
private
@@ -74,7 +75,7 @@ module ActiveRecord
end
def should_record_timestamps?
- self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
+ self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
end
def timestamp_attributes_for_create_in_model
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 81a3521d24..6b14c39686 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -5,8 +5,10 @@ module ActiveRecord
super
attributes.each do |attribute|
next unless record.class.reflect_on_association(attribute)
- value = record.send(attribute)
- if Array(value).all? { |r| r.marked_for_destruction? }
+ associated_records = Array(record.send(attribute))
+
+ # Superclass validates presence. Ensure present records aren't about to be destroyed.
+ if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? }
record.errors.add(attribute, :blank, options)
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 5dece1cb36..5fa6a0b892 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -26,11 +26,12 @@ module ActiveRecord
relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted?
Array(options[:scope]).each do |scope_item|
- scope_value = record.read_attribute(scope_item)
reflection = record.class.reflect_on_association(scope_item)
if reflection
scope_value = record.send(reflection.foreign_key)
scope_item = reflection.foreign_key
+ else
+ scope_value = record.read_attribute(scope_item)
end
relation = relation.and(table[scope_item].eq(scope_value))
end