diff options
Diffstat (limited to 'activerecord')
76 files changed, 1099 insertions, 665 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 67bec5f38e..ffd19a5334 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,106 @@ ## 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 + with the same name. This change make the class name singular and makes + the class easier to distinguish from the modules like + `ActiveRecord::TestFixtures`, which operates on multiple fixture sets, + or `DelegatingFixtures`, `::Fixtures`, etc., + and from the class `ActiveRecord::Fixture`, which corresponds to a single + fixture. + + *Alexey Muranov* + +* The postgres adapter now supports tables with capital letters. + Fix #5920 + + *Yves Senn* + +* `CollectionAssociation#count` returns `0` without querying if the + parent record is not persisted. + + Before: + + person.pets.count + # SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" IS NULL + # => 0 + + After: + + person.pets.count + # fires without sql query + # => 0 + + *Francesco Rodriguez* + +* Fix `reset_counters` crashing on `has_many :through` associations. + Fix #7822. + + *lulalala* + * Support for partial inserts. When inserting new records, only the fields which have been changed @@ -11,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 @@ -230,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* @@ -539,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. @@ -608,7 +718,7 @@ *Michael Pearson* * Added default order to `first` to assure consistent results among - diferent database engines. Introduced `take` as a replacement to + different database engines. Introduced `take` as a replacement to the old behavior of `first`. *Marcelo Silveira* diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 258d602afa..69b95f814c 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -190,10 +190,10 @@ module ActiveRecord # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> - # <tt>Project#milestones.delete(milestone), Project#milestones.find(milestone_id), Project#milestones.all(options),</tt> - # <tt>Project#milestones.build, Project#milestones.create</tt> + # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(mileston), Project#milestones.find(milestone_id),</tt> + # <tt>Project#milestones.all(options), Project#milestones.build, Project#milestones.create</tt> # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> - # <tt>Project#categories.delete(category1)</tt> + # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt> # # === A word of warning # @@ -236,6 +236,7 @@ module ActiveRecord # others.clear | X | X | X # others.delete(other,other,...) | X | X | X # others.delete_all | X | X | X + # others.destroy(other,other,...) | X | X | X # others.destroy_all | X | X | X # others.find(*args) | X | X | X # others.exists? | X | X | X @@ -1031,6 +1032,12 @@ module ActiveRecord # If the <tt>:through</tt> option is used, then the join records are deleted (rather than # nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or # <tt>:dependent => :nullify</tt> to override this. + # [collection.destroy(object, ...)] + # Removes one or more objects from the collection by running <tt>destroy</tt> on + # each record, regardless of any dependent option, ensuring callbacks are run. + # + # If the <tt>:through</tt> option is used, then the join records are destroyed + # instead, not the objects themselves. # [collection=objects] # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt> # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is @@ -1074,6 +1081,7 @@ module ActiveRecord # * <tt>Firm#clients</tt> (similar to <tt>Clients.all :conditions => ["firm_id = ?", id]</tt>) # * <tt>Firm#clients<<</tt> # * <tt>Firm#clients.delete</tt> + # * <tt>Firm#clients.destroy</tt> # * <tt>Firm#clients=</tt> # * <tt>Firm#client_ids</tt> # * <tt>Firm#client_ids=</tt> @@ -1425,6 +1433,9 @@ module ActiveRecord # [collection.delete(object, ...)] # Removes one or more objects from the collection by removing their associations from the join table. # This does not destroy the objects. + # [collection.destroy(object, ...)] + # Removes one or more objects from the collection by running destroy on each association in the join table, overriding any dependent option. + # This does not destroy the objects. # [collection=objects] # Replaces the collection's content by deleting and adding objects as appropriate. # [collection_singular_ids] @@ -1461,6 +1472,7 @@ module ActiveRecord # * <tt>Developer#projects</tt> # * <tt>Developer#projects<<</tt> # * <tt>Developer#projects.delete</tt> + # * <tt>Developer#projects.destroy</tt> # * <tt>Developer#projects=</tt> # * <tt>Developer#project_ids</tt> # * <tt>Developer#project_ids=</tt> diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index fe3e5b00f7..7f39d3083e 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -174,6 +174,8 @@ module ActiveRecord # association, it will be used for the query. Otherwise, construct options and pass them with # scope to the target class's +count+. def count(column_name = nil, count_options = {}) + return 0 if owner.new_record? + column_name, count_options = nil, column_name if column_name.is_a?(Hash) if options[:counter_sql] || options[:finder_sql] @@ -412,7 +414,7 @@ module ActiveRecord persisted.map! do |record| if mem_record = memory.delete(record) - (record.attribute_names - mem_record.changes.keys).each do |name| + ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name| mem_record[name] = record[name] end 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/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/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 8a073bf878..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 @@ -314,7 +311,7 @@ module ActiveRecord INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] WHERE cons.contype = 'p' - AND dep.refobjid = '#{table}'::regclass + AND dep.refobjid = '#{quote_table_name(table)}'::regclass end_sql row && row.first diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5e35f472c7..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 @@ -241,7 +238,7 @@ module ActiveRecord # <encoding></tt> call on the connection. # * <tt>:min_messages</tt> - An optional client min messages that is used in a # <tt>SET client_min_messages TO <min_messages></tt> call on the connection. - # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT<tt> statements + # * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT</tt> statements # defaults to true. # # Any further options are used as connection parameters to libpq. See @@ -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/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index c877079b25..57838ff984 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -22,6 +22,10 @@ module ActiveRecord counters.each do |association| has_many_association = reflect_on_association(association.to_sym) + if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection + has_many_association = has_many_association.through_reflection + end + foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass belongs_to = child_class.reflect_on_all_associations(:belongs_to) diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index 0f6ab3e396..11b53275e1 100644 --- a/activerecord/lib/active_record/fixtures/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -2,7 +2,7 @@ require 'erb' require 'yaml' module ActiveRecord - class Fixtures + class FixtureSet class File # :nodoc: include Enumerable diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 60fc653735..413bd147de 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,9 +2,11 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' -require 'active_record/fixtures/file' +require 'active_record/fixture_set/file' require 'active_record/errors' +require 'active_support/deprecation' # temporary + module ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: end @@ -350,8 +352,8 @@ module ActiveRecord # to the rescue: # # george_reginald: - # monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %> - # pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %> + # monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %> + # pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %> # # == Support for YAML defaults # @@ -370,10 +372,11 @@ module ActiveRecord # *DEFAULTS # # Any fixture labeled "DEFAULTS" is safely ignored. - class Fixtures + class FixtureSet #-- - # NOTE: an instance of Fixtures can be called fixture_set, it is normally stored in a single YAML file and possibly in a folder with the same name. + # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name. #++ + MAX_ID = 2 ** 30 - 1 @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } @@ -451,7 +454,7 @@ module ActiveRecord fixtures_map = {} fixture_sets = files_to_read.map do |fs_name| - fixtures_map[fs_name] = new( # ActiveRecord::Fixtures.new + fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new connection, fs_name, class_names[fs_name] || default_fixture_model_name(fs_name), @@ -565,7 +568,7 @@ module ActiveRecord # generate a primary key if necessary if has_primary_key_column? && !row.include?(primary_key_name) - row[primary_key_name] = ActiveRecord::Fixtures.identify(label) + row[primary_key_name] = ActiveRecord::FixtureSet.identify(label) end # If STI is used, find the correct subclass for association reflection @@ -588,7 +591,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - row[fk_name] = ActiveRecord::Fixtures.identify(value) + row[fk_name] = ActiveRecord::FixtureSet.identify(value) end when :has_and_belongs_to_many if (targets = row.delete(association.name.to_s)) @@ -596,7 +599,7 @@ module ActiveRecord table_name = association.join_table rows[table_name].concat targets.map { |target| { association.foreign_key => row[primary_key_name], - association.association_foreign_key => ActiveRecord::Fixtures.identify(target) } + association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) } } end end @@ -637,7 +640,7 @@ module ActiveRecord } + [yaml_file_path] yaml_files.each do |file| - Fixtures::File.open(file) do |fh| + FixtureSet::File.open(file) do |fh| fh.each do |fixture_name, row| fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class) end @@ -651,6 +654,11 @@ module ActiveRecord end + #-- + # Deprecate 'Fixtures' in favor of 'FixtureSet'. + #++ + Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet') + class Fixture #:nodoc: include Enumerable @@ -712,7 +720,7 @@ module ActiveRecord self.pre_loaded_fixtures = false self.fixture_class_names = Hash.new do |h, fixture_set_name| - h[fixture_set_name] = ActiveRecord::Fixtures.default_fixture_model_name(fixture_set_name) + h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name) end end @@ -847,7 +855,7 @@ module ActiveRecord end # Load fixtures for every test. else - ActiveRecord::Fixtures.reset_cache + ActiveRecord::FixtureSet.reset_cache @@already_loaded_fixtures[self.class] = nil @loaded_fixtures = load_fixtures end @@ -860,7 +868,7 @@ module ActiveRecord return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank? unless run_in_transaction? - ActiveRecord::Fixtures.reset_cache + ActiveRecord::FixtureSet.reset_cache end # Rollback changes if a transaction is active. @@ -879,7 +887,7 @@ module ActiveRecord private def load_fixtures - fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) + fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) Hash[fixtures.map { |f| [f.name, f] }] end @@ -888,16 +896,16 @@ module ActiveRecord def instantiate_fixtures if pre_loaded_fixtures - raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::Fixtures.all_loaded_fixtures.empty? + raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? unless @@required_fixture_classes - self.class.require_fixture_classes ActiveRecord::Fixtures.all_loaded_fixtures.keys + self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys @@required_fixture_classes = true end - ActiveRecord::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) + ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? @loaded_fixtures.each_value do |fixture_set| - ActiveRecord::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?) + ActiveRecord::FixtureSet.instantiate_fixtures(self, fixture_set, load_instances?) end end end 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/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index a25f2c7bca..ca79950049 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -1,11 +1,13 @@ module ActiveRecord class LogSubscriber < ActiveSupport::LogSubscriber + IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"] + def self.runtime=(value) - Thread.current["active_record_sql_runtime"] = value + Thread.current[:active_record_sql_runtime] = value end def self.runtime - Thread.current["active_record_sql_runtime"] ||= 0 + Thread.current[:active_record_sql_runtime] ||= 0 end def self.reset_runtime @@ -24,9 +26,9 @@ module ActiveRecord payload = event.payload - return if 'SCHEMA' == payload[:name] + return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) - name = '%s (%.1fms)' % [payload[:name], event.duration] + name = "#{payload[:name]} (#{event.duration.round(1)}ms)" sql = payload[:sql].squeeze(' ') binds = nil diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index c1d57855a9..d5ee98382d 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -50,7 +50,7 @@ module ActiveRecord # # class AddSsl < ActiveRecord::Migration # def up - # add_column :accounts, :ssl_enabled, :boolean, :default => true + # add_column :accounts, :ssl_enabled, :boolean, default: true # end # # def down @@ -62,7 +62,7 @@ module ActiveRecord # if you're backing out of the migration. It shows how all migrations have # two methods +up+ and +down+ that describes the transformations # required to implement or remove the migration. These methods can consist - # of both the migration specific methods like add_column and remove_column, + # of both the migration specific methods like +add_column+ and +remove_column+, # but may also contain regular Ruby code for generating data needed for the # transformations. # @@ -78,9 +78,9 @@ module ActiveRecord # t.integer :position # end # - # SystemSetting.create :name => "notice", - # :label => "Use notice?", - # :value => 1 + # SystemSetting.create name: 'notice', + # label: 'Use notice?', + # value: 1 # end # # def down @@ -88,19 +88,22 @@ module ActiveRecord # end # end # - # This migration first adds the system_settings table, then creates the very + # This migration first adds the +system_settings+ table, then creates the very # first row in it using the Active Record model that relies on the table. It - # also uses the more advanced create_table syntax where you can specify a + # also uses the more advanced +create_table+ syntax where you can specify a # complete table schema in one block call. # # == Available transformations # - # * <tt>create_table(name, options)</tt> Creates a table called +name+ and + # * <tt>create_table(name, options)</tt>: Creates a table called +name+ and # makes the table object available to a block that can then add columns to it, - # following the same format as add_column. See example above. The options hash + # following the same format as +add_column+. See example above. The options hash # is for fragments like "DEFAULT CHARSET=UTF-8" that are appended to the create # table definition. # * <tt>drop_table(name)</tt>: Drops the table called +name+. + # * <tt>change_table(name, options)</tt>: Allows to make column alterations to + # the table called +name+. It makes the table object availabe to a block that + # can then add/remove columns, indexes or foreign keys to it. # * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+ # to +new_name+. # * <tt>add_column(table_name, column_name, type, options)</tt>: Adds a new column @@ -109,9 +112,9 @@ module ActiveRecord # <tt>:string</tt>, <tt>:text</tt>, <tt>:integer</tt>, <tt>:float</tt>, # <tt>:decimal</tt>, <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. A default value can be - # specified by passing an +options+ hash like <tt>{ :default => 11 }</tt>. + # specified by passing an +options+ hash like <tt>{ default: 11 }</tt>. # Other options include <tt>:limit</tt> and <tt>:null</tt> (e.g. - # <tt>{ :limit => 50, :null => false }</tt>) -- see + # <tt>{ limit: 50, null: false }</tt>) -- see # ActiveRecord::ConnectionAdapters::TableDefinition#column for details. # * <tt>rename_column(table_name, column_name, new_column_name)</tt>: Renames # a column but keeps the type and content. @@ -122,11 +125,11 @@ module ActiveRecord # * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index # with the name of the column. Other options include # <tt>:name</tt>, <tt>:unique</tt> (e.g. - # <tt>{ :name => "users_name_index", :unique => true }</tt>) and <tt>:order</tt> - # (e.g. { :order => {:name => :desc} }</tt>). - # * <tt>remove_index(table_name, :column => column_name)</tt>: Removes the index + # <tt>{ name: 'users_name_index', unique: true }</tt>) and <tt>:order</tt> + # (e.g. <tt>{ order: { name: :desc } }</tt>). + # * <tt>remove_index(table_name, column: column_name)</tt>: Removes the index # specified by +column_name+. - # * <tt>remove_index(table_name, :name => index_name)</tt>: Removes the index + # * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index # specified by +index_name+. # # == Irreversible transformations diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index f81eb5f5d1..611d3d97c3 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -197,7 +197,7 @@ module ActiveRecord end end - # Updates a single attribute of an object, without calling save. + # Updates a single attribute of an object, without having to explicitly call save on that object. # # * Validation is skipped. # * Callbacks are skipped. @@ -209,7 +209,7 @@ module ActiveRecord update_columns(name => value) end - # Updates the attributes from the passed-in hash, without calling save. + # Updates the attributes from the passed-in hash, without having to explicitly call save on that object. # # * Validation is skipped. # * Callbacks are skipped. 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/railtie.rb b/activerecord/lib/active_record/railtie.rb index b11483de8c..d7e35fb771 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -129,7 +129,8 @@ module ActiveRecord end initializer "active_record.add_watchable_files" do |app| - config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"] + path = app.paths["db"].first + config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"] end config.after_initialize do |app| diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d134978128..0a9caa25b2 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -196,7 +196,7 @@ db_namespace = namespace :db do fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| - ActiveRecord::Fixtures.create_fixtures(fixtures_dir, fixture_file) + ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file) end end @@ -207,13 +207,13 @@ db_namespace = namespace :db do label, id = ENV['LABEL'], ENV['ID'] raise 'LABEL or ID required' if label.blank? && id.blank? - puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::Fixtures.identify(label)}.) if label + puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures') Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) data.keys.each do |key| - key_id = ActiveRecord::Fixtures.identify(key) + key_id = ActiveRecord::FixtureSet.identify(key) if key == label || key_id == id.to_i puts "#{file}: #{key} (#{key_id})" @@ -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/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 28aab6d92b..8af0c6a8ef 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -40,7 +40,7 @@ module ActiveRecord # # It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also mean that this method only works with integer-based + # work. This also means that this method only works with integer-based # primary keys. You can't set the limit either, that's used to control # the batch sizes. # 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/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 42b4cff4b8..f3e47a958e 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -141,23 +141,6 @@ module ActiveRecord end end - def expand_range_bind_variables(bind_vars) #:nodoc: - expanded = [] - - bind_vars.each do |var| - next if var.is_a?(Hash) - - if var.is_a?(Range) - expanded << var.first - expanded << var.last - else - expanded << var - end - end - - expanded - end - def quote_bound_value(value, c = connection) #:nodoc: if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? 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/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index aff971a955..5164acf77f 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -136,9 +136,9 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #the following functions were added to DRY test cases private - # custom fixture loader, uses Fixtures#create_fixtures and appends base_path to the current file's path + # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path def create_test_fixtures(*fixture_names) - ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) end # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 9fd07f014e..1017b0758d 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -136,9 +136,9 @@ class MysqlReservedWordTest < ActiveRecord::TestCase #the following functions were added to DRY test cases private - # custom fixture loader, uses Fixtures#create_fixtures and appends base_path to the current file's path + # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path def create_test_fixtures(*fixture_names) - ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) end # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 92e31a3e44..f1362dd15f 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -14,6 +14,10 @@ module ActiveRecord assert_equal 'id', @connection.primary_key('ex') end + def test_primary_key_works_tables_containing_capital_letters + assert_equal 'id', @connection.primary_key('CamelCase') + end + def test_non_standard_primary_key @connection.exec_query('drop table if exists ex') @connection.exec_query('create table ex(data character varying(255) primary key)') 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_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index f3520d43e0..42f5b69d4e 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -799,6 +799,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, developer.projects.count end + def test_counting_should_not_fire_sql_if_parent_is_unsaved + assert_no_queries do + assert_equal 0, Developer.new.projects.count + end + end + unless current_adapter?(:PostgreSQLAdapter) def test_count_with_finder_sql assert_equal 3, projects(:active_record).developers_with_finder_sql.count diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 4b56037a08..50c23c863f 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -262,6 +262,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal firm.limited_clients.length, firm.limited_clients.count end + def test_counting_should_not_fire_sql_if_parent_is_unsaved + assert_no_queries do + assert_equal 0, Person.new.readers.count + end + end + def test_finding assert_equal 2, Firm.all.merge!(:order => "id").first.clients.length 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 d4ceae6f80..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' @@ -767,6 +766,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 1, authors(:mary).categories.general.count end + def test_counting_should_not_fire_sql_if_parent_is_unsaved + assert_no_queries do + assert_equal 0, Person.new.posts.count + end + end + def test_has_many_through_belongs_to_should_update_when_the_through_foreign_key_changes post = posts(:eager_other) diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 86893ec4b3..9b00c21b52 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -231,6 +231,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal "2", categories(:sti_test).authors_with_select.first.post_id.to_s end + def test_create_through_has_many_with_piggyback + category = categories(:sti_test) + ernie = category.authors_with_select.create(:name => 'Ernie') + assert_nothing_raised do + assert_equal ernie, category.authors_with_select.detect {|a| a.name == 'Ernie'} + end + end + def test_include_has_many_through posts = Post.all.merge!(:order => 'posts.id').to_a posts_with_authors = Post.all.merge!(:includes => :authors, :order => 'posts.id').to_a 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/base_test.rb b/activerecord/test/cases/base_test.rb index fbfdd0f07a..0f859bf452 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -617,343 +617,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 'value2', weird.read_attribute('a$b') end - def test_multiparameter_attributes_on_date - attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } - topic = Topic.find(1) - topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same - assert_date_from_db Date.new(2004, 6, 24), topic.last_read.to_date - end - - def test_multiparameter_attributes_on_date_with_empty_year - attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "24" } - topic = Topic.find(1) - topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same - assert_nil topic.last_read - end - - def test_multiparameter_attributes_on_date_with_empty_month - attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "24" } - topic = Topic.find(1) - topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same - assert_nil topic.last_read - end - - def test_multiparameter_attributes_on_date_with_empty_day - attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" } - topic = Topic.find(1) - topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same - assert_nil topic.last_read - end - - def test_multiparameter_attributes_on_date_with_empty_day_and_year - attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "" } - topic = Topic.find(1) - topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same - assert_nil topic.last_read - end - - def test_multiparameter_attributes_on_date_with_empty_day_and_month - attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "" } - topic = Topic.find(1) - topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same - assert_nil topic.last_read - end - - def test_multiparameter_attributes_on_date_with_empty_year_and_month - attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "24" } - topic = Topic.find(1) - topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same - assert_nil topic.last_read - end - - def test_multiparameter_attributes_on_date_with_all_empty - attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "" } - topic = Topic.find(1) - topic.attributes = attributes - assert_nil topic.last_read - end - - def test_multiparameter_attributes_on_time - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on - end - - def test_multiparameter_attributes_on_time_with_no_date - ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do - attributes = { - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - end - assert_equal("written_on", ex.errors[0].attribute) - end - - def test_multiparameter_attributes_on_time_with_invalid_time_params - ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "2004", "written_on(5i)" => "36", "written_on(6i)" => "64", - } - topic = Topic.find(1) - topic.attributes = attributes - end - assert_equal("written_on", ex.errors[0].attribute) - end - - def test_multiparameter_attributes_on_time_with_old_date - attributes = { - "written_on(1i)" => "1850", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - # testing against to_s(:db) representation because either a Time or a DateTime might be returned, depending on platform - assert_equal "1850-06-24 16:24:00", topic.written_on.to_s(:db) - end - - def test_multiparameter_attributes_on_time_will_raise_on_big_time_if_missing_date_parts - ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do - attributes = { - "written_on(4i)" => "16", "written_on(5i)" => "24" - } - topic = Topic.find(1) - topic.attributes = attributes - end - assert_equal("written_on", ex.errors[0].attribute) - end - - def test_multiparameter_attributes_on_time_with_raise_on_small_time_if_missing_date_parts - ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do - attributes = { - "written_on(4i)" => "16", "written_on(5i)" => "12", "written_on(6i)" => "02" - } - topic = Topic.find(1) - topic.attributes = attributes - end - assert_equal("written_on", ex.errors[0].attribute) - end - - def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", - "written_on(5i)" => "12", "written_on(6i)" => "02" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on - end - - def test_multiparameter_attributes_on_time_will_ignore_hour_if_blank - attributes = { - "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", - "written_on(4i)" => "", "written_on(5i)" => "12", "written_on(6i)" => "02" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_nil topic.written_on - end - - def test_multiparameter_attributes_on_time_will_ignore_date_if_empty - attributes = { - "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", - "written_on(4i)" => "16", "written_on(5i)" => "24" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_nil topic.written_on - end - def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty - attributes = { - "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", - "written_on(4i)" => "16", "written_on(5i)" => "12", "written_on(6i)" => "02" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_nil topic.written_on - end - - def test_multiparameter_attributes_on_time_with_utc - ActiveRecord::Base.default_timezone = :utc - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on - end - - def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time - assert_equal Time.zone, topic.written_on.time_zone - end - - def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false - Time.zone = ActiveSupport::TimeZone[-28800] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) - end - - def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] - Topic.skip_time_zone_conversion_for_attributes = [:written_on] - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on - assert_equal false, topic.written_on.respond_to?(:time_zone) - ensure - Topic.skip_time_zone_conversion_for_attributes = [] - end - - # Oracle, and Sybase do not have a TIME datatype. - unless current_adapter?(:OracleAdapter, :SybaseAdapter) - def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion - ActiveRecord::Base.time_zone_aware_attributes = true - ActiveRecord::Base.default_timezone = :utc - Time.zone = ActiveSupport::TimeZone[-28800] - attributes = { - "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", - "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time - assert topic.bonus_time.utc? - end - end - - def test_multiparameter_attributes_on_time_with_empty_seconds - attributes = { - "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", - "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" - } - topic = Topic.find(1) - topic.attributes = attributes - assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on - end - - def test_multiparameter_attributes_setting_time_attribute - return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter - - topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) - assert_equal 1, topic.bonus_time.hour - assert_equal 5, topic.bonus_time.min - end - - def test_multiparameter_attributes_setting_date_attribute - topic = Topic.new( "written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11" ) - assert_equal 1952, topic.written_on.year - assert_equal 3, topic.written_on.month - assert_equal 11, topic.written_on.day - end - - def test_multiparameter_attributes_setting_date_and_time_attribute - topic = Topic.new( - "written_on(1i)" => "1952", - "written_on(2i)" => "3", - "written_on(3i)" => "11", - "written_on(4i)" => "13", - "written_on(5i)" => "55") - assert_equal 1952, topic.written_on.year - assert_equal 3, topic.written_on.month - assert_equal 11, topic.written_on.day - assert_equal 13, topic.written_on.hour - assert_equal 55, topic.written_on.min - end - - def test_multiparameter_attributes_setting_time_but_not_date_on_date_field - assert_raise( ActiveRecord::MultiparameterAssignmentErrors ) do - Topic.new( "written_on(4i)" => "13", "written_on(5i)" => "55" ) - end - end - - def test_multiparameter_assignment_of_aggregation - customer = Customer.new - address = Address.new("The Street", "The City", "The Country") - attributes = { "address(1)" => address.street, "address(2)" => address.city, "address(3)" => address.country } - customer.attributes = attributes - assert_equal address, customer.address - end - - def test_multiparameter_assignment_of_aggregation_out_of_order - customer = Customer.new - address = Address.new("The Street", "The City", "The Country") - attributes = { "address(3)" => address.country, "address(2)" => address.city, "address(1)" => address.street } - customer.attributes = attributes - assert_equal address, customer.address - end - - def test_multiparameter_assignment_of_aggregation_with_missing_values - ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do - customer = Customer.new - address = Address.new("The Street", "The City", "The Country") - attributes = { "address(2)" => address.city, "address(3)" => address.country } - customer.attributes = attributes - end - assert_equal("address", ex.errors[0].attribute) - end - - def test_multiparameter_assignment_of_aggregation_with_blank_values - customer = Customer.new - address = Address.new("The Street", "The City", "The Country") - attributes = { "address(1)" => "", "address(2)" => address.city, "address(3)" => address.country } - customer.attributes = attributes - assert_equal Address.new(nil, "The City", "The Country"), customer.address - end - - def test_multiparameter_assignment_of_aggregation_with_large_index - ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do - customer = Customer.new - address = Address.new("The Street", "The City", "The Country") - attributes = { "address(1)" => "The Street", "address(2)" => address.city, "address(3000)" => address.country } - customer.attributes = attributes - end - - assert_equal("address", ex.errors[0].attribute) - end - def test_attributes_on_dummy_time # Oracle, and Sybase do not have a TIME datatype. return true if current_adapter?(:OracleAdapter, :SybaseAdapter) 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/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index ee443741ca..fc46a249c8 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -10,9 +10,12 @@ require 'models/dog' require 'models/dog_lover' require 'models/person' require 'models/friendship' +require 'models/subscriber' +require 'models/subscription' +require 'models/book' class CounterCacheTest < ActiveRecord::TestCase - fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers, :people, :friendships + fixtures :topics, :categories, :categorizations, :cars, :dogs, :dog_lovers, :people, :friendships, :subscribers, :subscriptions, :books class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' @@ -118,4 +121,14 @@ class CounterCacheTest < ActiveRecord::TestCase Person.reset_counters(michael.id, :followers) end end + + test "reset counter of has_many :through association" do + subscriber = subscribers('second') + Subscriber.reset_counters(subscriber.id, 'books') + Subscriber.increment_counter('books_count', subscriber.id) + + assert_difference 'subscriber.reload.books_count', -1 do + Subscriber.reset_counters(subscriber.id, 'books') + end + end end 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/fixtures/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb index e623fbe4d1..a029fedbd3 100644 --- a/activerecord/test/cases/fixtures/file_test.rb +++ b/activerecord/test/cases/fixture_set/file_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' require 'tempfile' module ActiveRecord - class Fixtures + class FixtureSet class FileTest < ActiveRecord::TestCase def test_open fh = File.open(::File.join(FIXTURES_ROOT, "accounts.yml")) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index c28f8de682..b0b29f5f42 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -40,7 +40,7 @@ class FixturesTest < ActiveRecord::TestCase FIXTURES.each do |name| fixtures = nil assert_nothing_raised { fixtures = create_fixtures(name).first } - assert_kind_of(ActiveRecord::Fixtures, fixtures) + assert_kind_of(ActiveRecord::FixtureSet, fixtures) fixtures.each { |_name, fixture| fixture.each { |key, value| assert_match(MATCH_ATTRIBUTE_NAME, key) @@ -57,7 +57,7 @@ class FixturesTest < ActiveRecord::TestCase dir = File.dirname badyaml.path name = File.basename badyaml.path, '.yml' assert_raises(ActiveRecord::Fixture::FormatError) do - ActiveRecord::Fixtures.create_fixtures(dir, name) + ActiveRecord::FixtureSet.create_fixtures(dir, name) end ensure badyaml.close @@ -65,7 +65,7 @@ class FixturesTest < ActiveRecord::TestCase end def test_create_fixtures - fixtures = ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, "parrots") + fixtures = ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, "parrots") assert Parrot.find_by_name('Curious George'), 'George is not in the database' assert fixtures.detect { |f| f.name == 'parrots' }, "no fixtures named 'parrots' in #{fixtures.map(&:name).inspect}" end @@ -74,11 +74,11 @@ class FixturesTest < ActiveRecord::TestCase fixtures_array = nil assert_nothing_raised { fixtures_array = create_fixtures(*FIXTURES) } assert_kind_of(Array, fixtures_array) - fixtures_array.each { |fixtures| assert_kind_of(ActiveRecord::Fixtures, fixtures) } + fixtures_array.each { |fixtures| assert_kind_of(ActiveRecord::FixtureSet, fixtures) } end def test_create_symbol_fixtures - fixtures = ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, :collections, :collections => Course) { Course.connection } + fixtures = ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => Course) { Course.connection } assert Course.find_by_name('Collection'), 'course is not in the database' assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}" @@ -102,7 +102,7 @@ class FixturesTest < ActiveRecord::TestCase if ActiveRecord::Base.connection.supports_migrations? def test_inserts_with_pre_and_suffix # Reset cache to make finds on the new table work - ActiveRecord::Fixtures.reset_cache + ActiveRecord::FixtureSet.reset_cache ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t| t.column :title, :string @@ -190,11 +190,11 @@ class FixturesTest < ActiveRecord::TestCase end def test_empty_yaml_fixture - assert_not_nil ActiveRecord::Fixtures.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts") + assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts") end def test_empty_yaml_fixture_with_a_comment_in_it - assert_not_nil ActiveRecord::Fixtures.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies") + assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies") end def test_nonexistent_fixture_file @@ -204,19 +204,19 @@ class FixturesTest < ActiveRecord::TestCase assert Dir[nonexistent_fixture_path+"*"].empty? assert_raise(Errno::ENOENT) do - ActiveRecord::Fixtures.new( Account.connection, "companies", 'Company', nonexistent_fixture_path) + ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', nonexistent_fixture_path) end end def test_dirty_dirty_yaml_file assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::Fixtures.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses") + ActiveRecord::FixtureSet.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses") end end def test_omap_fixtures assert_nothing_raised do - fixtures = ActiveRecord::Fixtures.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered") + fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered") fixtures.each.with_index do |(name, fixture), i| assert_equal "fixture_no_#{i}", name @@ -254,7 +254,7 @@ if Account.connection.respond_to?(:reset_pk_sequence!) def setup @instances = [Account.new(:credit_limit => 50), Company.new(:name => 'RoR Consulting')] - ActiveRecord::Fixtures.reset_cache # make sure tables get reinitialized + ActiveRecord::FixtureSet.reset_cache # make sure tables get reinitialized end def test_resets_to_min_pk_with_specified_pk_and_sequence @@ -582,13 +582,13 @@ class FasterFixturesTest < ActiveRecord::TestCase def load_extra_fixture(name) fixture = create_fixtures(name).first - assert fixture.is_a?(ActiveRecord::Fixtures) + assert fixture.is_a?(ActiveRecord::FixtureSet) @loaded_fixtures[fixture.table_name] = fixture end def test_cache - assert ActiveRecord::Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'categories') - assert ActiveRecord::Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'authors') + assert ActiveRecord::FixtureSet.fixture_is_cached?(ActiveRecord::Base.connection, 'categories') + assert ActiveRecord::FixtureSet.fixture_is_cached?(ActiveRecord::Base.connection, 'authors') assert_no_queries do create_fixtures('categories') @@ -596,7 +596,7 @@ class FasterFixturesTest < ActiveRecord::TestCase end load_extra_fixture('posts') - assert ActiveRecord::Fixtures.fixture_is_cached?(ActiveRecord::Base.connection, 'posts') + assert ActiveRecord::FixtureSet.fixture_is_cached?(ActiveRecord::Base.connection, 'posts') self.class.setup_fixture_accessors :posts assert_equal 'Welcome to the weblog', posts(:welcome).title end @@ -606,17 +606,17 @@ class FoxyFixturesTest < ActiveRecord::TestCase fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users" def test_identifies_strings - assert_equal(ActiveRecord::Fixtures.identify("foo"), ActiveRecord::Fixtures.identify("foo")) - assert_not_equal(ActiveRecord::Fixtures.identify("foo"), ActiveRecord::Fixtures.identify("FOO")) + assert_equal(ActiveRecord::FixtureSet.identify("foo"), ActiveRecord::FixtureSet.identify("foo")) + assert_not_equal(ActiveRecord::FixtureSet.identify("foo"), ActiveRecord::FixtureSet.identify("FOO")) end def test_identifies_symbols - assert_equal(ActiveRecord::Fixtures.identify(:foo), ActiveRecord::Fixtures.identify(:foo)) + assert_equal(ActiveRecord::FixtureSet.identify(:foo), ActiveRecord::FixtureSet.identify(:foo)) end def test_identifies_consistently - assert_equal 207281424, ActiveRecord::Fixtures.identify(:ruby) - assert_equal 1066363776, ActiveRecord::Fixtures.identify(:sapphire_2) + assert_equal 207281424, ActiveRecord::FixtureSet.identify(:ruby) + assert_equal 1066363776, ActiveRecord::FixtureSet.identify(:sapphire_2) end TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on) @@ -757,7 +757,7 @@ class FixtureLoadingTest < ActiveRecord::TestCase end class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase - ActiveRecord::Fixtures.reset_cache + ActiveRecord::FixtureSet.reset_cache set_fixture_class :randomly_named_a9 => ClassNameThatDoesNotFollowCONVENTIONS, @@ -782,7 +782,7 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase end def test_table_name_is_defined_in_the_model - assert_equal 'randomly_named_table', ActiveRecord::Fixtures::all_loaded_fixtures["admin/randomly_named_a9"].table_name + assert_equal 'randomly_named_table', ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name assert_equal 'randomly_named_table', Admin::ClassNameThatDoesNotFollowCONVENTIONS.table_name end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index f39111ba77..cff6689c15 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -80,7 +80,7 @@ class ActiveSupport::TestCase self.use_transactional_fixtures = true def create_fixtures(*fixture_set_names, &block) - ActiveRecord::Fixtures.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) + ActiveRecord::FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) 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/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb new file mode 100644 index 0000000000..1209f5460f --- /dev/null +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -0,0 +1,350 @@ +require "cases/helper" +require 'models/topic' +require 'models/customer' + +class MultiParameterAttributeTest < ActiveRecord::TestCase + fixtures :topics + + def setup + ActiveRecord::Base.time_zone_aware_attributes = false + ActiveRecord::Base.default_timezone = :local + Time.zone = nil + end + + def test_multiparameter_attributes_on_date + attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" } + topic = Topic.find(1) + topic.attributes = attributes + # note that extra #to_date call allows test to pass for Oracle, which + # treats dates/times the same + assert_date_from_db Date.new(2004, 6, 24), topic.last_read.to_date + end + + def test_multiparameter_attributes_on_date_with_empty_year + attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "24" } + topic = Topic.find(1) + topic.attributes = attributes + # note that extra #to_date call allows test to pass for Oracle, which + # treats dates/times the same + assert_nil topic.last_read + end + + def test_multiparameter_attributes_on_date_with_empty_month + attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "24" } + topic = Topic.find(1) + topic.attributes = attributes + # note that extra #to_date call allows test to pass for Oracle, which + # treats dates/times the same + assert_nil topic.last_read + end + + def test_multiparameter_attributes_on_date_with_empty_day + attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" } + topic = Topic.find(1) + topic.attributes = attributes + # note that extra #to_date call allows test to pass for Oracle, which + # treats dates/times the same + assert_nil topic.last_read + end + + def test_multiparameter_attributes_on_date_with_empty_day_and_year + attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "" } + topic = Topic.find(1) + topic.attributes = attributes + # note that extra #to_date call allows test to pass for Oracle, which + # treats dates/times the same + assert_nil topic.last_read + end + + def test_multiparameter_attributes_on_date_with_empty_day_and_month + attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "" } + topic = Topic.find(1) + topic.attributes = attributes + # note that extra #to_date call allows test to pass for Oracle, which + # treats dates/times the same + assert_nil topic.last_read + end + + def test_multiparameter_attributes_on_date_with_empty_year_and_month + attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "24" } + topic = Topic.find(1) + topic.attributes = attributes + # note that extra #to_date call allows test to pass for Oracle, which + # treats dates/times the same + assert_nil topic.last_read + end + + def test_multiparameter_attributes_on_date_with_all_empty + attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "" } + topic = Topic.find(1) + topic.attributes = attributes + assert_nil topic.last_read + end + + def test_multiparameter_attributes_on_time + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + end + + def test_multiparameter_attributes_on_time_with_no_date + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + attributes = { + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + end + assert_equal("written_on", ex.errors[0].attribute) + end + + def test_multiparameter_attributes_on_time_with_invalid_time_params + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "2004", "written_on(5i)" => "36", "written_on(6i)" => "64", + } + topic = Topic.find(1) + topic.attributes = attributes + end + assert_equal("written_on", ex.errors[0].attribute) + end + + def test_multiparameter_attributes_on_time_with_old_date + attributes = { + "written_on(1i)" => "1850", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + # testing against to_s(:db) representation because either a Time or a DateTime might be returned, depending on platform + assert_equal "1850-06-24 16:24:00", topic.written_on.to_s(:db) + end + + def test_multiparameter_attributes_on_time_will_raise_on_big_time_if_missing_date_parts + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + attributes = { + "written_on(4i)" => "16", "written_on(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + end + assert_equal("written_on", ex.errors[0].attribute) + end + + def test_multiparameter_attributes_on_time_with_raise_on_small_time_if_missing_date_parts + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + attributes = { + "written_on(4i)" => "16", "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + end + assert_equal("written_on", ex.errors[0].attribute) + end + + def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12", + "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on + end + + def test_multiparameter_attributes_on_time_will_ignore_hour_if_blank + attributes = { + "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", + "written_on(4i)" => "", "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_nil topic.written_on + end + + def test_multiparameter_attributes_on_time_will_ignore_date_if_empty + attributes = { + "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", + "written_on(4i)" => "16", "written_on(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_nil topic.written_on + end + def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty + attributes = { + "written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "", + "written_on(4i)" => "16", "written_on(5i)" => "12", "written_on(6i)" => "02" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_nil topic.written_on + end + + def test_multiparameter_attributes_on_time_with_utc + ActiveRecord::Base.default_timezone = :utc + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + end + + def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes + ActiveRecord::Base.time_zone_aware_attributes = true + ActiveRecord::Base.default_timezone = :utc + Time.zone = ActiveSupport::TimeZone[-28800] + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time + assert_equal Time.zone, topic.written_on.time_zone + end + + def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false + Time.zone = ActiveSupport::TimeZone[-28800] + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal false, topic.written_on.respond_to?(:time_zone) + end + + def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes + ActiveRecord::Base.time_zone_aware_attributes = true + ActiveRecord::Base.default_timezone = :utc + Time.zone = ActiveSupport::TimeZone[-28800] + Topic.skip_time_zone_conversion_for_attributes = [:written_on] + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on + assert_equal false, topic.written_on.respond_to?(:time_zone) + ensure + Topic.skip_time_zone_conversion_for_attributes = [] + end + + # Oracle, and Sybase do not have a TIME datatype. + unless current_adapter?(:OracleAdapter, :SybaseAdapter) + def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion + ActiveRecord::Base.time_zone_aware_attributes = true + ActiveRecord::Base.default_timezone = :utc + Time.zone = ActiveSupport::TimeZone[-28800] + attributes = { + "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1", + "bonus_time(4i)" => "16", "bonus_time(5i)" => "24" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time + assert topic.bonus_time.utc? + end + end + + def test_multiparameter_attributes_on_time_with_empty_seconds + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on + end + + def test_multiparameter_attributes_setting_time_attribute + return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter + + topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) + assert_equal 1, topic.bonus_time.hour + assert_equal 5, topic.bonus_time.min + end + + def test_multiparameter_attributes_setting_date_attribute + topic = Topic.new( "written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11" ) + assert_equal 1952, topic.written_on.year + assert_equal 3, topic.written_on.month + assert_equal 11, topic.written_on.day + end + + def test_multiparameter_attributes_setting_date_and_time_attribute + topic = Topic.new( + "written_on(1i)" => "1952", + "written_on(2i)" => "3", + "written_on(3i)" => "11", + "written_on(4i)" => "13", + "written_on(5i)" => "55") + assert_equal 1952, topic.written_on.year + assert_equal 3, topic.written_on.month + assert_equal 11, topic.written_on.day + assert_equal 13, topic.written_on.hour + assert_equal 55, topic.written_on.min + end + + def test_multiparameter_attributes_setting_time_but_not_date_on_date_field + assert_raise( ActiveRecord::MultiparameterAssignmentErrors ) do + Topic.new( "written_on(4i)" => "13", "written_on(5i)" => "55" ) + end + end + + def test_multiparameter_assignment_of_aggregation + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(1)" => address.street, "address(2)" => address.city, "address(3)" => address.country } + customer.attributes = attributes + assert_equal address, customer.address + end + + def test_multiparameter_assignment_of_aggregation_out_of_order + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(3)" => address.country, "address(2)" => address.city, "address(1)" => address.street } + customer.attributes = attributes + assert_equal address, customer.address + end + + def test_multiparameter_assignment_of_aggregation_with_missing_values + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(2)" => address.city, "address(3)" => address.country } + customer.attributes = attributes + end + assert_equal("address", ex.errors[0].attribute) + end + + def test_multiparameter_assignment_of_aggregation_with_blank_values + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(1)" => "", "address(2)" => address.city, "address(3)" => address.country } + customer.attributes = attributes + assert_equal Address.new(nil, "The City", "The Country"), customer.address + end + + def test_multiparameter_assignment_of_aggregation_with_large_index + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + customer = Customer.new + address = Address.new("The Street", "The City", "The Country") + attributes = { "address(1)" => "The Street", "address(2)" => address.city, "address(3000)" => address.country } + customer.attributes = attributes + end + + assert_equal("address", ex.errors[0].attribute) + end +end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 9120083eca..fe9eddbdec 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -156,7 +156,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_reject_if_with_blank_nested_attributes_id - # When using a select list to choose an existing 'ship' id, with :include_blank => true + # When using a select list to choose an existing 'ship' id, with include_blank: true Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| attributes[:id].blank? } pirate = Pirate.new(:catchphrase => "Stop wastin' me time") 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/store_test.rb b/activerecord/test/cases/store_test.rb index dc47d40f41..562ca8d9ff 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -136,7 +136,7 @@ class StoreTest < ActiveRecord::TestCase end test "all stored attributes are returned" do - assert_equal [:color, :homepage, :favorite_food, :phone_number], Admin::User.stored_attributes[:settings] + assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings] end test "stores_attributes are class level settings" do 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/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 15b97c02c8..efa0c9b934 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -43,7 +43,7 @@ class I18nValidationTest < ActiveRecord::TestCase [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }] # TODO Add :on case, but below doesn't work, because then the validation isn't run for some reason # even when using .save instead .valid? - # [ "given on condition", {:on => :save}, {}] + # [ "given on condition", {on: :save}, {}] ] # validates_uniqueness_of w/ mocha 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/fixtures/mateys.yml b/activerecord/test/fixtures/mateys.yml index d3690955fc..3f0405aaf8 100644 --- a/activerecord/test/fixtures/mateys.yml +++ b/activerecord/test/fixtures/mateys.yml @@ -1,4 +1,4 @@ blackbeard_to_redbeard: - pirate_id: <%= ActiveRecord::Fixtures.identify(:blackbeard) %> - target_id: <%= ActiveRecord::Fixtures.identify(:redbeard) %> + pirate_id: <%= ActiveRecord::FixtureSet.identify(:blackbeard) %> + target_id: <%= ActiveRecord::FixtureSet.identify(:redbeard) %> weight: 10 diff --git a/activerecord/test/fixtures/parrots_pirates.yml b/activerecord/test/fixtures/parrots_pirates.yml index 66472243c7..e1a301b91a 100644 --- a/activerecord/test/fixtures/parrots_pirates.yml +++ b/activerecord/test/fixtures/parrots_pirates.yml @@ -1,7 +1,7 @@ george_blackbeard: - parrot_id: <%= ActiveRecord::Fixtures.identify(:george) %> - pirate_id: <%= ActiveRecord::Fixtures.identify(:blackbeard) %> + parrot_id: <%= ActiveRecord::FixtureSet.identify(:george) %> + pirate_id: <%= ActiveRecord::FixtureSet.identify(:blackbeard) %> louis_blackbeard: - parrot_id: <%= ActiveRecord::Fixtures.identify(:louis) %> - pirate_id: <%= ActiveRecord::Fixtures.identify(:blackbeard) %> + parrot_id: <%= ActiveRecord::FixtureSet.identify(:louis) %> + pirate_id: <%= ActiveRecord::FixtureSet.identify(:blackbeard) %> diff --git a/activerecord/test/fixtures/peoples_treasures.yml b/activerecord/test/fixtures/peoples_treasures.yml index a72b190d0c..46abe50e6c 100644 --- a/activerecord/test/fixtures/peoples_treasures.yml +++ b/activerecord/test/fixtures/peoples_treasures.yml @@ -1,3 +1,3 @@ michael_diamond: - rich_person_id: <%= ActiveRecord::Fixtures.identify(:michael) %> - treasure_id: <%= ActiveRecord::Fixtures.identify(:diamond) %> + rich_person_id: <%= ActiveRecord::FixtureSet.identify(:michael) %> + treasure_id: <%= ActiveRecord::FixtureSet.identify(:diamond) %> diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index 35170faa76..467f3ccd39 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -1,7 +1,7 @@ class Admin::User < ActiveRecord::Base belongs_to :account store :settings, :accessors => [ :color, :homepage ] - store_accessor :settings, :favorite_food, :phone_number + store_accessor :settings, :favorite_food store :preferences, :accessors => [ :remember_login ] store :json_data, :accessors => [ :height, :weight ], :coder => JSON store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => JSON diff --git a/activerecord/test/models/subscription.rb b/activerecord/test/models/subscription.rb index 4bdb36ea46..bcac4738a3 100644 --- a/activerecord/test/models/subscription.rb +++ b/activerecord/test/models/subscription.rb @@ -1,4 +1,4 @@ class Subscription < ActiveRecord::Base - belongs_to :subscriber + belongs_to :subscriber, :counter_cache => :books_count belongs_to :book end 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/schema/schema.rb b/activerecord/test/schema/schema.rb index 798ea20efc..2e4ec96933 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -621,6 +621,7 @@ ActiveRecord::Schema.define do create_table :subscribers, :force => true, :id => false do |t| t.string :nick, :null => false t.string :name + t.column :books_count, :integer, :null => false, :default => 0 end add_index :subscribers, :nick, :unique => true 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' |