diff options
author | Pablo Ifran <pabloifran@gmail.com> | 2012-10-22 09:42:42 -0200 |
---|---|---|
committer | Pablo Ifran <pabloifran@gmail.com> | 2012-10-22 09:42:42 -0200 |
commit | e041a50f2917f82950f9e5666f966d8992afd45d (patch) | |
tree | 9f4d2e3aa88f28dba9d7a1d24d46977e0642a1eb /activerecord | |
parent | 3e6b2f5d38e0f31db3fb0fcd3bbab92666a0e3e2 (diff) | |
parent | ae27acb342c575ce19d5ad78cb13ba23f826fab1 (diff) | |
download | rails-e041a50f2917f82950f9e5666f966d8992afd45d.tar.gz rails-e041a50f2917f82950f9e5666f966d8992afd45d.tar.bz2 rails-e041a50f2917f82950f9e5666f966d8992afd45d.zip |
Merge branch 'master' of https://github.com/lifo/docrails
Conflicts:
activerecord/lib/active_record/callbacks.rb
Diffstat (limited to 'activerecord')
52 files changed, 729 insertions, 260 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 9abfe2e6fd..ffd19a5334 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,67 @@ ## Rails 4.0.0 (unreleased) ## +* Add `find_or_create_by`, `find_or_create_by!` and + `find_or_initialize_by` methods to `Relation`. + + These are similar to the `first_or_create` family of methods, but + the behaviour when a record is created is slightly different: + + User.where(first_name: 'Penélope').first_or_create + + will execute: + + User.where(first_name: 'Penélope').create + + Causing all the `create` callbacks to execute within the context of + the scope. This could affect queries that occur within callbacks. + + User.find_or_create_by(first_name: 'Penélope') + + will execute: + + User.create(first_name: 'Penélope') + + Which obviously does not affect the scoping of queries within + callbacks. + + The `find_or_create_by` version also reads better, frankly. + + If you need to add extra attributes during create, you can do one of: + + User.create_with(active: true).find_or_create_by(first_name: 'Jon') + User.find_or_create_by(first_name: 'Jon') { |u| u.active = true } + + The `first_or_create` family of methods have been nodoc'ed in favour + of this API. They may be deprecated in the future but their + implementation is very small and it's probably not worth putting users + through lots of annoying deprecation warnings. + + *Jon Leighton* + +* Fix bug with presence validation of associations. Would incorrectly add duplicated errors + when the association was blank. Bug introduced in 1fab518c6a75dac5773654646eb724a59741bc13. + + *Scott Willson* + +* Fix bug where sum(expression) returns string '0' for no matching records + Fixes #7439 + + *Tim Macfarlane* + +* PostgreSQL adapter correctly fetches default values when using multiple schemas and domains in a db. Fixes #7914 + + *Arturo Pie* + +* Learn ActiveRecord::QueryMethods#order work with hash arguments + + When symbol or hash passed we convert it to Arel::Nodes::Ordering. + If we pass invalid direction(like name: :DeSc) ActiveRecord::QueryMethods#order will raise an exception + + User.order(:name, email: :desc) + # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC + + *Tima Maslyuchenko* + * Rename `ActiveRecord::Fixtures` class to `ActiveRecord::FixtureSet`. Instances of this class normally hold a collection of fixtures (records) loaded either from a single YAML file, or from a file and a folder @@ -50,6 +112,10 @@ app processes (so long as the code in those processes doesn't contain any references to the removed column). + The `partial_updates` configuration option is now renamed to + `partial_writes` to reflect the fact that it now impacts both inserts + and updates. + *Jon Leighton* * Allow before and after validations to take an array of lifecycle events @@ -269,6 +335,11 @@ *Ari Pollak* +* Fix AR#dup to nullify the validation errors in the dup'ed object. Previously the original + and the dup'ed object shared the same errors. + + * Christian Seiler* + * Raise `ArgumentError` if list of attributes to change is empty in `update_all`. *Roman Shatsov* @@ -578,9 +649,9 @@ * `find_or_initialize_by_...` can be rewritten using `where(...).first_or_initialize` * `find_or_create_by_...` can be rewritten using - `where(...).first_or_create` + `find_or_create_by(...)` or where(...).first_or_create` * `find_or_create_by_...!` can be rewritten using - `where(...).first_or_create!` + `find_or_create_by!(...) or `where(...).first_or_create!` The implementation of the deprecated dynamic finders has been moved to the `activerecord-deprecated_finders` gem. See below for details. 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 diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb index 297cd094c2..c8aa37f275 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -4,8 +4,8 @@ require 'rails/generators/active_model' require 'active_record' module ActiveRecord - module Generators - class Base < Rails::Generators::NamedBase #:nodoc: + module Generators # :nodoc: + class Base < Rails::Generators::NamedBase # :nodoc: include Rails::Generators::Migration # Set the current directory as base for the inherited generators. @@ -14,7 +14,7 @@ module ActiveRecord end # Implement the required interface for Rails::Generators::Migration. - def self.next_migration_number(dirname) #:nodoc: + def self.next_migration_number(dirname) next_migration_number = current_migration_number(dirname) + 1 ActiveRecord::Migration.next_migration_number(next_migration_number) end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index a3c274d9b9..5f1dbe36d6 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -1,8 +1,8 @@ require 'rails/generators/active_record' module ActiveRecord - module Generators - class MigrationGenerator < Base + module Generators # :nodoc: + class MigrationGenerator < Base # :nodoc: argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" def create_migration_file @@ -42,7 +42,7 @@ module ActiveRecord attribute.name.singularize.foreign_key end.to_sym end - + private def validate_file_name! diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 8e6ef20285..5f36181694 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -1,8 +1,8 @@ require 'rails/generators/active_record' module ActiveRecord - module Generators - class ModelGenerator < Base + module Generators # :nodoc: + class ModelGenerator < Base # :nodoc: argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]" check_class_collision diff --git a/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb b/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb index c1c0e3f25b..e7445d03a2 100644 --- a/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb +++ b/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb @@ -1,8 +1,8 @@ require 'rails/generators/active_record' module ActiveRecord - module Generators - class ObserverGenerator < Base + module Generators # :nodoc: + class ObserverGenerator < Base # :nodoc: check_class_collision :suffix => "Observer" def create_observer_file diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index f8a605b67c..685f0ea74f 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'ipaddr' module ActiveRecord module ConnectionAdapters @@ -20,6 +21,18 @@ module ActiveRecord assert_equal 'f', @conn.type_cast(false, c) end + def test_type_cast_cidr + ip = IPAddr.new('255.0.0.0/8') + c = Column.new(nil, ip, 'cidr') + assert_equal ip, @conn.type_cast(ip, c) + end + + def test_type_cast_inet + ip = IPAddr.new('255.1.0.0/8') + c = Column.new(nil, ip, 'inet') + assert_equal ip, @conn.type_cast(ip, c) + end + def test_quote_float_nan nan = 0.0/0 c = Column.new(nil, 1, 'float') diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 9208f53997..cd31900d4e 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -72,7 +72,7 @@ class SchemaTest < ActiveRecord::TestCase end def test_schema_names - assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names + assert_equal ["public", "schema_1", "test_schema", "test_schema2"], @connection.schema_names end def test_create_schema @@ -97,7 +97,7 @@ class SchemaTest < ActiveRecord::TestCase def test_drop_schema begin - @connection.create_schema "test_schema3" + @connection.create_schema "test_schema3" ensure @connection.drop_schema "test_schema3" end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 7eef4ace81..74288a98d1 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -59,7 +59,7 @@ class CopyTableTest < ActiveRecord::TestCase def test_copy_table_with_unconventional_primary_key test_copy_table('owners', 'owners_unconventional') do |from, to, options| - original_pk = @connection.primary_key('owners') + original_pk = @connection.primary_key('owners') copied_pk = @connection.primary_key('owners_unconventional') assert_equal original_pk, copied_pk end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index f0582a3090..b2a5d9d6f7 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -19,7 +19,6 @@ require 'models/book' require 'models/subscription' require 'models/essay' require 'models/category' -require 'models/owner' require 'models/categorization' require 'models/member' require 'models/membership' diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index d08b157011..c2b58fd7d1 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -542,10 +542,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase val = t.send attr_name unless attr_name == "type" if attribute_gets_cached assert cached_columns.include?(attr_name) - assert_equal val, cache[attr_name.to_sym] + assert_equal val, cache[attr_name] else assert uncached_columns.include?(attr_name) - assert !cache.include?(attr_name.to_sym) + assert !cache.include?(attr_name) end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index fd4f09ab36..16ce150396 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -16,7 +16,6 @@ require 'models/ship_part' require 'models/tag' require 'models/tagging' require 'models/treasure' -require 'models/company' require 'models/eye' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase @@ -145,7 +144,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas firm = Firm.first firm.account = Account.first - assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! } + assert_queries(Firm.partial_writes? ? 0 : 1) { firm.save! } firm = Firm.first.dup firm.account = Account.first diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 6cb6c469d2..abbf2a765e 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -6,6 +6,8 @@ require 'models/edge' require 'models/organization' require 'models/possession' require 'models/topic' +require 'models/minivan' +require 'models/speedometer' Company.has_many :accounts @@ -239,21 +241,12 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_association_with_non_numeric_foreign_key - ActiveRecord::Base.connection.expects(:select_all).returns([{"count_all" => 1, "firm_id" => "ABC"}]) + Speedometer.create! id: 'ABC' + Minivan.create! id: 'OMG', speedometer_id: 'ABC' - firm = mock() - firm.expects(:id).returns("ABC") - firm.expects(:class).returns(Firm) - Company.expects(:find).with(["ABC"]).returns([firm]) - - column = mock() - column.expects(:name).at_least_once.returns(:firm_id) - column.expects(:type_cast).with("ABC").returns("ABC") - Account.expects(:columns).at_least_once.returns([column]) - - c = Account.group(:firm).count(:all) + c = Minivan.group(:speedometer).count(:all) first_key = c.keys.first - assert_equal Firm, first_key.class + assert_equal Speedometer, first_key.class assert_equal 1, c[first_key] end @@ -378,6 +371,10 @@ class CalculationsTest < ActiveRecord::TestCase end end + def test_sum_expression_returns_zero_when_no_records_to_sum + assert_equal 0, Account.where('1 = 2').sum("2 * credit_limit") + end + def test_count_with_from_option assert_equal Company.count(:all), Company.from('companies').count(:all) assert_equal Account.where("credit_limit = 50").count(:all), diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index deaf5252db..0df872ff10 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -51,11 +51,60 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) # We don't want that to happen, so we disable transactional fixtures here. self.use_transactional_fixtures = false - # MySQL 5 and higher is quirky with not null text/blob columns. - # With MySQL Text/blob columns cannot have defaults. If the column is not - # null MySQL will report that the column has a null default - # but it behaves as though the column had a default of '' - def test_mysql_text_not_null_defaults + def using_strict(strict) + connection = ActiveRecord::Model.remove_connection + ActiveRecord::Model.establish_connection connection.merge(strict: strict) + yield + ensure + ActiveRecord::Model.remove_connection + ActiveRecord::Model.establish_connection connection + end + + # MySQL cannot have defaults on text/blob columns. It reports the + # default value as null. + # + # Despite this, in non-strict mode, MySQL will use an empty string + # as the default value of the field, if no other value is + # specified. + # + # Therefore, in non-strict mode, we want column.default to report + # an empty string as its default, to be consistent with that. + # + # In strict mode, column.default should be nil. + def test_mysql_text_not_null_defaults_non_strict + using_strict(false) do + with_text_blob_not_null_table do |klass| + assert_equal '', klass.columns_hash['non_null_blob'].default + assert_equal '', klass.columns_hash['non_null_text'].default + + assert_nil klass.columns_hash['null_blob'].default + assert_nil klass.columns_hash['null_text'].default + + instance = klass.create! + + assert_equal '', instance.non_null_text + assert_equal '', instance.non_null_blob + + assert_nil instance.null_text + assert_nil instance.null_blob + end + end + end + + def test_mysql_text_not_null_defaults_strict + using_strict(true) do + with_text_blob_not_null_table do |klass| + assert_nil klass.columns_hash['non_null_blob'].default + assert_nil klass.columns_hash['non_null_text'].default + assert_nil klass.columns_hash['null_blob'].default + assert_nil klass.columns_hash['null_text'].default + + assert_raises(ActiveRecord::StatementInvalid) { klass.create } + end + end + end + + def with_text_blob_not_null_table klass = Class.new(ActiveRecord::Base) klass.table_name = 'test_mysql_text_not_null_defaults' klass.connection.create_table klass.table_name do |t| @@ -64,19 +113,8 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) t.column :null_text, :text, :null => true t.column :null_blob, :blob, :null => true end - assert_equal '', klass.columns_hash['non_null_blob'].default - assert_equal '', klass.columns_hash['non_null_text'].default - - assert_nil klass.columns_hash['null_blob'].default - assert_nil klass.columns_hash['null_text'].default - assert_nothing_raised do - instance = klass.create! - assert_equal '', instance.non_null_text - assert_equal '', instance.non_null_blob - assert_nil instance.null_text - assert_nil instance.null_blob - end + yield klass ensure klass.connection.drop_table(klass.table_name) rescue nil end @@ -109,3 +147,43 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) end end end + +if current_adapter?(:PostgreSQLAdapter) + class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase + def setup + @connection = ActiveRecord::Base.connection + + @old_search_path = @connection.schema_search_path + @connection.schema_search_path = "schema_1, pg_catalog" + @connection.create_table "defaults" do |t| + t.text "text_col", :default => "some value" + t.string "string_col", :default => "some value" + end + Default.reset_column_information + end + + def test_text_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parse" + end + + def test_string_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parse" + end + + def test_bpchar_defaults_in_new_schema_when_overriding_domain + @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" + Default.reset_column_information + assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parse" + end + + def test_text_defaults_after_updating_column_default + @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text" + assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db" + end + + def teardown + @connection.schema_search_path = @old_search_path + Default.reset_column_information + end + end +end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 7334514f9a..40f1dbccde 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -311,12 +311,12 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.new(:catchphrase => 'foo') old_updated_on = 1.hour.ago.beginning_of_day - with_partial_updates Pirate, false do + with_partial_writes Pirate, false do assert_queries(2) { 2.times { pirate.save! } } Pirate.where(id: pirate.id).update_all(:updated_on => old_updated_on) end - with_partial_updates Pirate, true do + with_partial_writes Pirate, true do assert_queries(0) { 2.times { pirate.save! } } assert_equal old_updated_on, pirate.reload.updated_on @@ -329,12 +329,12 @@ class DirtyTest < ActiveRecord::TestCase person = Person.new(:first_name => 'foo') old_lock_version = 1 - with_partial_updates Person, false do + with_partial_writes Person, false do assert_queries(2) { 2.times { person.save! } } Person.where(id: person.id).update_all(:first_name => 'baz') end - with_partial_updates Person, true do + with_partial_writes Person, true do assert_queries(0) { 2.times { person.save! } } assert_equal old_lock_version, person.reload.lock_version @@ -408,8 +408,8 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.catchphrase_changed? end - def test_save_should_store_serialized_attributes_even_with_partial_updates - with_partial_updates(Topic) do + def test_save_should_store_serialized_attributes_even_with_partial_writes + with_partial_writes(Topic) do topic = Topic.create!(:content => {:a => "a"}) topic.content[:b] = "b" #assert topic.changed? # Known bug, will fail @@ -421,7 +421,7 @@ class DirtyTest < ActiveRecord::TestCase end def test_save_always_should_update_timestamps_when_serialized_attributes_are_present - with_partial_updates(Topic) do + with_partial_writes(Topic) do topic = Topic.create!(:content => {:a => "a"}) topic.save! @@ -434,8 +434,8 @@ class DirtyTest < ActiveRecord::TestCase end end - def test_save_should_not_save_serialized_attribute_with_partial_updates_if_not_present - with_partial_updates(Topic) do + def test_save_should_not_save_serialized_attribute_with_partial_writes_if_not_present + with_partial_writes(Topic) do Topic.create!(:author_name => 'Bill', :content => {:a => "a"}) topic = Topic.select('id, author_name').first topic.update_columns author_name: 'John' @@ -552,7 +552,7 @@ class DirtyTest < ActiveRecord::TestCase end test "partial insert" do - with_partial_updates Person do + with_partial_writes Person do jon = nil assert_sql(/first_name/i) do jon = Person.create! first_name: 'Jon' @@ -568,20 +568,34 @@ class DirtyTest < ActiveRecord::TestCase end test "partial insert with empty values" do - with_partial_updates Aircraft do + with_partial_writes Aircraft do a = Aircraft.create! a.reload assert_not_nil a.id end end + test "partial_updates config attribute is deprecated" do + klass = Class.new(ActiveRecord::Base) + + assert klass.partial_writes? + assert_deprecated { assert klass.partial_updates? } + assert_deprecated { assert klass.partial_updates } + + assert_deprecated { klass.partial_updates = false } + + assert !klass.partial_writes? + assert_deprecated { assert !klass.partial_updates? } + assert_deprecated { assert !klass.partial_updates } + end + private - def with_partial_updates(klass, on = true) - old = klass.partial_updates? - klass.partial_updates = on + def with_partial_writes(klass, on = true) + old = klass.partial_writes? + klass.partial_writes = on yield ensure - klass.partial_updates = old + klass.partial_writes = old end def check_pirate_after_save_failure(pirate) diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 71b2b16608..4e2adff344 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -107,5 +107,19 @@ module ActiveRecord assert Topic.after_initialize_called end + def test_dup_validity_is_independent + Topic.validates_presence_of :title + topic = Topic.new("title" => "Litterature") + topic.valid? + + duped = topic.dup + duped.title = nil + assert duped.invalid? + + topic.title = nil + duped.title = 'Mathematics' + assert topic.invalid? + assert duped.valid? + end end end diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index ec4c554abb..17c1634444 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -293,7 +293,7 @@ module ActiveRecord connection.create_table :testings do |t| t.column :foo, :string, limit: 100 t.column :bar, :decimal, precision: 8, scale: 2 - t.column :taggable_id, :integer, null: false + t.column :taggable_id, :integer, null: false t.column :taggable_type, :string, default: 'Photo' end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 3c0d2b18d9..c155f29973 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -344,11 +344,7 @@ class MigrationTest < ActiveRecord::TestCase columns = Person.connection.columns(:binary_testings) data_column = columns.detect { |c| c.name == "data" } - if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) - assert_equal '', data_column.default - else - assert_nil data_column.default - end + assert_nil data_column.default Person.connection.drop_table :binary_testings rescue nil end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 2d778e9e90..51a285a2b4 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -48,7 +48,7 @@ class QueryCacheTest < ActiveRecord::TestCase } assert_raises(RuntimeError) { mw.call({}) } - assert_equal connection_id, ActiveRecord::Base.connection_id + assert_equal connection_id, ActiveRecord::Base.connection_id end def test_middleware_delegates diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 588da68ec1..a9d46f4fba 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -3,7 +3,6 @@ require 'models/topic' require 'models/customer' require 'models/company' require 'models/company_in_module' -require 'models/subscriber' require 'models/ship' require 'models/pirate' require 'models/price_estimate' diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index b91423351e..5f96145b47 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -5,7 +5,6 @@ require 'models/post' require 'models/topic' require 'models/comment' require 'models/author' -require 'models/comment' require 'models/entrant' require 'models/developer' require 'models/reply' @@ -158,6 +157,22 @@ class RelationTest < ActiveRecord::TestCase assert_equal 4, topics.to_a.size assert_equal topics(:first).title, topics.first.title end + + def test_finding_with_assoc_order + topics = Topic.order(:id => :desc) + assert_equal 4, topics.to_a.size + assert_equal topics(:fourth).title, topics.first.title + end + + def test_finding_with_reverted_assoc_order + topics = Topic.order(:id => :asc).reverse_order + assert_equal 4, topics.to_a.size + assert_equal topics(:fourth).title, topics.first.title + end + + def test_raising_exception_on_invalid_hash_params + assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) } + end def test_finding_last_with_arel_order topics = Topic.order(Topic.arel_table[:id].asc) @@ -1043,6 +1058,39 @@ class RelationTest < ActiveRecord::TestCase assert_equal 'parrot', parrot.name end + def test_find_or_create_by + assert_nil Bird.find_by(name: 'bob') + + bird = Bird.find_or_create_by(name: 'bob') + assert bird.persisted? + + assert_equal bird, Bird.find_or_create_by(name: 'bob') + end + + def test_find_or_create_by_with_create_with + assert_nil Bird.find_by(name: 'bob') + + bird = Bird.create_with(color: 'green').find_or_create_by(name: 'bob') + assert bird.persisted? + assert_equal 'green', bird.color + + assert_equal bird, Bird.create_with(color: 'blue').find_or_create_by(name: 'bob') + end + + def test_find_or_create_by! + assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: 'green') } + end + + def test_find_or_initialize_by + assert_nil Bird.find_by(name: 'bob') + + bird = Bird.find_or_initialize_by(name: 'bob') + assert bird.new_record? + bird.save! + + assert_equal bird, Bird.find_or_initialize_by(name: 'bob') + end + def test_explicit_create_scope hens = Bird.where(:name => 'hen') assert_equal 'hen', hens.new.name diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 80f46c6b08..5f13124e5b 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -2,13 +2,9 @@ require "cases/helper" class SchemaDumperTest < ActiveRecord::TestCase - def initialize(*) - super - ActiveRecord::SchemaMigration.create_table - end - def setup super + ActiveRecord::SchemaMigration.create_table @stream = StringIO.new end diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index a8e513d81f..174d96aa4e 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -13,7 +13,7 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase I18n.load_path.clear I18n.backend = I18n::Backend::Simple.new yield - ensure + ensure I18n.load_path.replace @old_load_path I18n.backend = @old_backend end @@ -54,4 +54,9 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase end end + test "translation for 'taken' can be overridden" do + I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}} + assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title') + end + end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index cd9175f454..1de8934406 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -18,6 +18,13 @@ class PresenceValidationTest < ActiveRecord::TestCase assert b.valid? end + def test_validates_presence_of_has_one + Boy.validates_presence_of(:face) + b = Boy.new + assert b.invalid?, "should not be valid if has_one association missing" + assert_equal 1, b.errors[:face].size, "validates_presence_of should only add one error" + end + def test_validates_presence_of_has_one_marked_for_destruction Boy.validates_presence_of(:face) b = Boy.new diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 2cd9f30b59..d0e7338f15 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -12,6 +12,8 @@ ActiveRecord::Schema.define do execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()' + execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + %w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name| execute "SELECT setval('#{seq_name}', 100)" end @@ -37,7 +39,12 @@ ActiveRecord::Schema.define do ); _SQL - execute <<_SQL + execute "CREATE SCHEMA schema_1" + execute "CREATE DOMAIN schema_1.text AS text" + execute "CREATE DOMAIN schema_1.varchar AS varchar" + execute "CREATE DOMAIN schema_1.bpchar AS bpchar" + + execute <<_SQL CREATE TABLE geometrics ( id serial primary key, a_point point, diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index 92736e0ca9..bea894a583 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -13,7 +13,7 @@ module ARTest def self.connect puts "Using #{connection_name}" - ActiveRecord::Model.logger = ActiveSupport::Logger.new("debug.log") + ActiveRecord::Model.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024) ActiveRecord::Model.configurations = connection_config ActiveRecord::Model.establish_connection 'arunit' ARUnit2Model.establish_connection 'arunit2' |