diff options
Diffstat (limited to 'activerecord')
128 files changed, 1802 insertions, 1295 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 56aba1c483..554bec17d6 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,77 @@ +* Correct query for PostgreSQL 8.2 compatibility. + + *Ben Murphy*, *Matthew Draper* + +* `bin/rake db:migrate` uses + `ActiveRecord::Tasks::DatabaseTasks.migrations_paths` instead of + `Migrator.migrations_paths`. + + *Tobias Bielohlawek* + +* Support dropping indexes concurrently in PostgreSQL. + + See http://www.postgresql.org/docs/9.4/static/sql-dropindex.html for more + details. + + *Grey Baker* + +* Deprecate passing conditions to `ActiveRecord::Relation#delete_all` + and `ActiveRecord::Relation#destroy_all`. + + *Wojciech Wnętrzak* + +* PostgreSQL, `create_schema`, `drop_schema` and `rename_table` now quote + schema names. + + Fixes #21418. + + Example: + + create_schema("my.schema") + # CREATE SCHEMA "my.schema"; + + *Yves Senn* + +* PostgreSQL, add `:if_exists` option to `#drop_schema`. This makes it + possible to drop a schema that might exist without raising an exception if + it doesn't. + + *Yves Senn* + +* Only try to nullify has_one target association if the record is persisted. + + Fixes #21223. + + *Agis Anastasopoulos* + +* Uniqueness validator raises descriptive error when running on a persisted + record without primary key. + + Fixes #21304. + + *Yves Senn* + +* Add a native JSON data type support in MySQL. + + Example: + + create_table :json_data_type do |t| + t.json :settings + end + + *Ryuta Kamizono* + +* Descriptive error message when fixtures contain a missing column. + + Fixes #21201. + + *Yves Senn* + +* `ActiveRecord::Tasks::PostgreSQLDatabaseTasks` fail if shellout to + postgresql commands (like `pg_dump`) is not successful. + + *Bryan Paxton*, *Nate Berkopec* + * Add `ActiveRecord::Relation#in_batches` to work with records and relations in batches. @@ -13,11 +87,11 @@ sleep 10 # Throttles the delete queries end - Closes #20933. + Fixes #20933. *Sina Siadat* -* Added methods for PostgreSQL geometric data types to use in migrations +* Added methods for PostgreSQL geometric data types to use in migrations. Example: @@ -345,6 +419,10 @@ *Ryuta Kamizono* +* Remove `ActiveRecord::Serialization::XmlSerializer` from core. + + *Zachary Scott* + * Make `unscope` aware of "less than" and "greater than" conditions. *TAKAHASHI Kazuaki* @@ -1039,7 +1117,7 @@ * `eager_load` preserves readonly flag for associations. - Closes #15853. + Fixes #15853. *Takashi Kokubun* @@ -1095,7 +1173,7 @@ * Fix bug with `ActiveRecord::Type::Numeric` that caused negative values to be marked as having changed when set to the same negative value. - Closes #18161. + Fixes #18161. *Daniel Fox* @@ -1110,7 +1188,7 @@ before loading the schema. This is left for the user to do. `db:test:prepare` will still purge the database. - Closes #17945. + Fixes #17945. *Yves Senn* diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index a830b0e0e4..65eb6fba27 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -5,65 +5,105 @@ require 'active_record/errors' module ActiveRecord class AssociationNotFoundError < ConfigurationError #:nodoc: - def initialize(record, association_name) - super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?") + def initialize(record = nil, association_name = nil) + if record && association_name + super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?") + else + super("Association was not found.") + end end end class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc: - def initialize(reflection, associated_class = nil) - super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") + def initialize(reflection = nil, associated_class = nil) + if reflection + super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})") + else + super("Could not find the inverse association.") + end end end class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection) - super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}") + def initialize(owner_class_name = nil, reflection = nil) + if owner_class_name && reflection + super("Could not find the association #{reflection.options[:through].inspect} in model #{owner_class_name}") + else + super("Could not find the association.") + end end end class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection, source_reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.") + def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil) + if owner_class_name && reflection && source_reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}' without 'source_type'. Try adding 'source_type: \"#{reflection.name.to_s.classify}\"' to 'has_many :through' definition.") + else + super("Cannot have a has_many :through association.") + end end end class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + def initialize(owner_class_name = nil, reflection = nil) + if owner_class_name && reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + else + super("Cannot have a has_many :through association.") + end end end class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection, source_reflection) - super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.") + def initialize(owner_class_name = nil, reflection = nil, source_reflection = nil) + if owner_class_name && reflection && source_reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.") + else + super("Cannot have a has_many :through association.") + end end end class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection, through_reflection) - super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.") + def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil) + if owner_class_name && reflection && through_reflection + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.") + else + super("Cannot have a has_one :through association.") + end end end class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: - def initialize(owner_class_name, reflection) - super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + def initialize(owner_class_name = nil, reflection = nil) + if owner_class_name && reflection + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + else + super("Cannot have a has_one :through association.") + end end end class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc: - def initialize(reflection) - through_reflection = reflection.through_reflection - source_reflection_names = reflection.source_reflection_names - source_associations = reflection.through_reflection.klass._reflections.keys - super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") + def initialize(reflection = nil) + if reflection + through_reflection = reflection.through_reflection + source_reflection_names = reflection.source_reflection_names + source_associations = reflection.through_reflection.klass._reflections.keys + super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") + else + super("Could not find the source association(s).") + end end end class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because the source reflection class '#{reflection.source_reflection.class_name}' is associated to '#{reflection.through_reflection.class_name}' via :#{reflection.source_reflection.macro}.") + else + super("Cannot modify association.") + end end end @@ -74,20 +114,32 @@ module ActiveRecord end class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.") + else + super("Cannot associate new records.") + end end end class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.") + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.") + else + super("Cannot dissociate new records.") + end end end class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc: - def initialize(owner, reflection) - super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") + def initialize(owner = nil, reflection = nil) + if owner && reflection + super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.") + else + super("Through nested associations are read-only.") + end end end @@ -98,14 +150,22 @@ module ActiveRecord end class EagerLoadPolymorphicError < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") + def initialize(reflection = nil) + if reflection + super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}") + else + super("Eager load polymorphic error.") + end end end class ReadOnlyAssociation < ActiveRecordError #:nodoc: - def initialize(reflection) - super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.") + def initialize(reflection = nil) + if reflection + super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.") + else + super("Read-only reflection error.") + end end end @@ -113,8 +173,12 @@ module ActiveRecord # (has_many, has_one) when there is at least 1 child associated instance. # ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project class DeleteRestrictionError < ActiveRecordError #:nodoc: - def initialize(name) - super("Cannot delete record because of dependent #{name}") + def initialize(name = nil) + if name + super("Cannot delete record because of dependent #{name}") + else + super("Delete restriction error.") + end end end @@ -619,10 +683,10 @@ module ActiveRecord # @tag = @post.tags.build name: "ruby" # @tag.save # - # The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the + # The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the # <tt>:inverse_of</tt> is set: # - # class Taggable < ActiveRecord::Base + # class Tagging < ActiveRecord::Base # belongs_to :post # belongs_to :tag, inverse_of: :taggings # end @@ -643,7 +707,7 @@ module ActiveRecord # You can turn off the automatic detection of inverse associations by setting # the <tt>:inverse_of</tt> option to <tt>false</tt> like so: # - # class Taggable < ActiveRecord::Base + # class Tagging < ActiveRecord::Base # belongs_to :tag, inverse_of: false # end # diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 7c729676a7..c7b396f3d4 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -251,6 +251,14 @@ module ActiveRecord initialize_attributes(record) end end + + # Returns true if statement cache should be skipped on the association reader. + def skip_statement_cache? + reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.scope_attributes? || + reflection.source_reflection.active_record.default_scopes.any? + end end end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index ffd9c9d6fc..b18d99d54e 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -46,7 +46,7 @@ module ActiveRecord::Associations::Builder join_model = Class.new(ActiveRecord::Base) { class << self; - attr_accessor :class_resolver + attr_accessor :left_model attr_accessor :name attr_accessor :table_name_resolver attr_accessor :left_reflection @@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder end def self.compute_type(class_name) - class_resolver.compute_type class_name + left_model.compute_type class_name end def self.add_left_association(name, options) @@ -72,11 +72,15 @@ module ActiveRecord::Associations::Builder self.right_reflection = _reflect_on_association(rhs_name) end + def self.retrieve_connection + left_model.retrieve_connection + end + } join_model.name = "HABTM_#{association_name.to_s.camelize}" join_model.table_name_resolver = habtm - join_model.class_resolver = lhs_model + join_model.left_model = lhs_model join_model.add_left_association :left_side, anonymous_class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 0fc2b83b71..256df3ca11 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -442,12 +442,7 @@ module ActiveRecord private def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.to_a - end + return scope.to_a if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index b5a8c81fe4..79b6b80d84 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -227,6 +227,31 @@ module ActiveRecord @association.last(*args) end + # Gives a record (or N records if a parameter is supplied) from the collection + # using the same rules as <tt>ActiveRecord::Base.take</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.take(2) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.take # => nil + # another_person_without.pets.take(2) # => [] def take(n = nil) @association.take(n) end @@ -856,7 +881,7 @@ module ActiveRecord !!@association.include?(record) end - def arel + def arel #:nodoc: scope.arel end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 9f6c832c1b..f8211ef9fb 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -50,7 +50,7 @@ module ActiveRecord end def empty? - if has_cached_counter? + if reflection.has_cached_counter? size.zero? else super @@ -73,8 +73,8 @@ module ActiveRecord # If the collection is empty the target is set to an empty array and # the loaded flag is set to true as well. def count_records - count = if has_cached_counter? - owner._read_attribute cached_counter_attribute_name + count = if reflection.has_cached_counter? + owner._read_attribute reflection.counter_cache_column else scope.count end @@ -87,70 +87,26 @@ module ActiveRecord [association_scope.limit_value, count].compact.min end - def has_cached_counter?(reflection = reflection()) - owner.attribute_present?(cached_counter_attribute_name(reflection)) - end - - def cached_counter_attribute_name(reflection = reflection()) - if reflection.options[:counter_cache] - reflection.options[:counter_cache].to_s - else - "#{reflection.name}_count" - end - end - def update_counter(difference, reflection = reflection()) update_counter_in_database(difference, reflection) update_counter_in_memory(difference, reflection) end def update_counter_in_database(difference, reflection = reflection()) - if has_cached_counter?(reflection) - counter = cached_counter_attribute_name(reflection) - owner.class.update_counters(owner.id, counter => difference) + if reflection.has_cached_counter? + owner.class.update_counters(owner.id, reflection.counter_cache_column => difference) end end def update_counter_in_memory(difference, reflection = reflection()) - if counter_must_be_updated_by_has_many?(reflection) - counter = cached_counter_attribute_name(reflection) + if reflection.counter_must_be_updated_by_has_many? + counter = reflection.counter_cache_column + owner[counter] ||= 0 owner[counter] += difference owner.send(:clear_attribute_changes, counter) # eww end end - # This shit is nasty. We need to avoid the following situation: - # - # * An associated record is deleted via record.destroy - # * Hence the callbacks run, and they find a belongs_to on the record with a - # :counter_cache options which points back at our owner. So they update the - # counter cache. - # * In which case, we must make sure to *not* update the counter cache, or else - # it will be decremented twice. - # - # Hence this method. - def inverse_which_updates_counter_cache(reflection = reflection()) - counter_name = cached_counter_attribute_name(reflection) - inverse_which_updates_counter_named(counter_name, reflection) - end - alias inverse_updates_counter_cache? inverse_which_updates_counter_cache - - def inverse_which_updates_counter_named(counter_name, reflection) - reflection.klass._reflections.values.find { |inverse_reflection| - inverse_reflection.belongs_to? && - inverse_reflection.counter_cache_column == counter_name - } - end - - def inverse_updates_counter_in_memory?(reflection) - inverse = inverse_which_updates_counter_cache(reflection) - inverse && inverse == reflection.inverse_of - end - - def counter_must_be_updated_by_has_many?(reflection) - !inverse_updates_counter_in_memory?(reflection) && has_cached_counter?(reflection) - end - def delete_count(method, scope) if method == :delete_all scope.delete_all @@ -168,7 +124,7 @@ module ActiveRecord def delete_records(records, method) if method == :destroy records.each(&:destroy!) - update_counter(-records.length) unless inverse_updates_counter_cache? + update_counter(-records.length) unless reflection.inverse_updates_counter_cache? else scope = self.scope.where(reflection.klass.primary_key => records) update_counter(-delete_count(method, scope)) diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 1aa6a2ca74..deb0f8c9f5 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -110,7 +110,7 @@ module ActiveRecord def update_through_counter?(method) case method when :destroy - !inverse_updates_counter_cache?(through_reflection) + !through_reflection.inverse_updates_counter_cache? when :nullify false else diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 5a92bc5e8a..1829453d73 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -65,7 +65,7 @@ module ActiveRecord when :destroy target.destroy when :nullify - target.update_columns(reflection.foreign_key => nil) + target.update_columns(reflection.foreign_key => nil) if target.persisted? end end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 6ecc741195..3992a240b9 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -116,7 +116,7 @@ module ActiveRecord when String preloaders_for_one(association.to_sym, records, scope) else - raise ArgumentError, "#{association.inspect} was not recognised for preload" + raise ArgumentError, "#{association.inspect} was not recognized for preload" end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 30c5d72482..03cb8cb8c3 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -47,12 +47,7 @@ module ActiveRecord end def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.limit(1).to_a - end + return scope.limit(1).to_a if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 7fb899c242..0862306749 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -385,27 +385,27 @@ module ActiveRecord # # For example: # - # class PostsController < ActionController::Base - # after_action :print_accessed_fields, only: :index + # class PostsController < ActionController::Base + # after_action :print_accessed_fields, only: :index # - # def index - # @posts = Post.all - # end + # def index + # @posts = Post.all + # end # - # private + # private # - # def print_accessed_fields - # p @posts.first.accessed_fields + # def print_accessed_fields + # p @posts.first.accessed_fields + # end # end - # end # # Which allows you to quickly change your code to: # - # class PostsController < ActionController::Base - # def index - # @posts = Post.select(:id, :title, :author_id, :updated_at) + # class PostsController < ActionController::Base + # def index + # @posts = Post.select(:id, :title, :author_id, :updated_at) + # end # end - # end def accessed_fields @attributes.accessed end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 2363cf7608..5197e21fa4 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -56,14 +56,12 @@ module ActiveRecord end end - ID = 'id'.freeze - # Returns the value of the attribute identified by <tt>attr_name</tt> after # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) name = attr_name.to_s - name = self.class.primary_key if name == ID + name = self.class.primary_key if name == 'id'.freeze _read_attribute(name, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index e03bf5945d..60eecab0d0 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -11,7 +11,7 @@ module ActiveRecord # serialized object must be of that class on assignment and retrieval. # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. # - # Empty objects as +{}+, in the case of +Hash+, or +[]+, in the case of + # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of # +Array+, will always be persisted as null. # # Keep in mind that database adapters handle certain serialization tasks diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index e85777c335..0c730d313f 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -1,3 +1,5 @@ +require 'active_record/attribute' + module ActiveRecord class AttributeSet # :nodoc: class Builder # :nodoc: diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index dbb0e2fab2..d0de42d27c 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -234,7 +234,7 @@ module ActiveRecord super end - # Marks this record to be destroyed as part of the parents save transaction. + # Marks this record to be destroyed as part of the parent's save transaction. # This does _not_ actually destroy the record instantly, rather child record will be destroyed # when <tt>parent.save</tt> is called. # @@ -243,7 +243,7 @@ module ActiveRecord @marked_for_destruction = true end - # Returns whether or not this record will be destroyed as part of the parents save transaction. + # Returns whether or not this record will be destroyed as part of the parent's save transaction. # # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model. def marked_for_destruction? diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 55a7e053bc..4b66d8cd36 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1,5 +1,4 @@ require 'yaml' -require 'set' require 'active_support/benchmarkable' require 'active_support/dependencies' require 'active_support/descendants_tracker' diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index c7c769b283..ccdbebbc77 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -206,7 +206,8 @@ module ActiveRecord # == Ordering callbacks # # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+ - # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option. + # callback (+log_children+ in this case) should be executed before the children get destroyed by the + # <tt>dependent: destroy</tt> option. # # Let's look at the code below: # diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 38dd9578fe..107806cd93 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -137,7 +137,7 @@ module ActiveRecord # # In order to get around this problem, #transaction will emulate the effect # of nested transactions, by using savepoints: - # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8' # supports savepoints. # @@ -190,7 +190,7 @@ module ActiveRecord # semantics of these different levels: # # * http://www.postgresql.org/docs/current/static/transaction-iso.html - # * https://dev.mysql.com/doc/refman/5.6/en/set-transaction.html + # * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html # # An <tt>ActiveRecord::TransactionIsolationError</tt> will be raised if: # @@ -289,8 +289,12 @@ module ActiveRecord columns = schema_cache.columns_hash(table_name) binds = fixture.map do |name, value| - type = lookup_cast_type_from_column(columns[name]) - Relation::QueryAttribute.new(name, value, type) + if column = columns[name] + type = lookup_cast_type_from_column(column) + Relation::QueryAttribute.new(name, value, type) + else + raise Fixture::FixtureError, %(table "#{table_name}" has no column named "#{name}".) + end end key_list = fixture.keys.map { |name| quote_column_name(name) } value_list = prepare_binds_for_database(binds).map do |value| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index d17e272ed1..2c76dd1518 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -214,6 +214,7 @@ module ActiveRecord @name = name end + # Returns an array of ColumnDefinition objects for the columns of the table. def columns; @columns_hash.values; end # Returns a ColumnDefinition for the column with name +name+. @@ -235,8 +236,8 @@ module ActiveRecord # # Available options are (none of these exists by default): # * <tt>:limit</tt> - - # Requests a maximum column length. This is number of characters for <tt>:string</tt> and - # <tt>:text</tt> columns and number of bytes for <tt>:binary</tt> and <tt>:integer</tt> columns. + # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column + # and number of bytes for <tt>:text</tt>, <tt>:binary</tt> and <tt>:integer</tt> columns. # * <tt>:default</tt> - # The column's default value. Use nil for NULL. # * <tt>:null</tt> - @@ -369,6 +370,8 @@ module ActiveRecord self end + # remove the column +name+ from the table. + # remove_column(:account_id) def remove_column(name) @columns_hash.delete name.to_s end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index a30945d0ee..16aefc94ab 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -23,6 +23,11 @@ module ActiveRecord table_name[0...table_alias_length].tr('.', '_') end + # Returns an array of table names defined in the database. + def tables(name = nil) + raise NotImplementedError, "#tables is not implemented" + end + # Checks to see if the table +table_name+ exists on the database. # # table_exists?(:developers) @@ -153,7 +158,7 @@ module ActiveRecord # generates: # # CREATE TABLE suppliers ( - # id int(11) DEFAULT NULL auto_increment PRIMARY KEY + # id int(11) auto_increment PRIMARY KEY # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # # ====== Rename the primary key column @@ -165,7 +170,7 @@ module ActiveRecord # generates: # # CREATE TABLE objects ( - # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY, + # guid int(11) auto_increment PRIMARY KEY, # name varchar(80) # ) # @@ -607,10 +612,7 @@ module ActiveRecord # remove_index :accounts, name: :by_branch_party # def remove_index(table_name, options = {}) - remove_index!(table_name, index_name_for_remove(table_name, options)) - end - - def remove_index!(table_name, index_name) #:nodoc: + index_name = index_name_for_remove(table_name, options) execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" end @@ -857,7 +859,7 @@ module ActiveRecord ActiveRecord::SchemaMigration.create_table end - def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths) + def assume_migrated_upto_version(version, migrations_paths) migrations_paths = Array(migrations_paths) version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 56227ddd80..ed14c781c6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -266,6 +266,11 @@ module ActiveRecord false end + # Does this adapter support json data type? + def supports_json? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end 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 af156c9c78..597f0d0597 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -10,6 +10,10 @@ module ActiveRecord options[:auto_increment] = true if type == :bigint super end + + def json(*args, **options) + args.each { |name| column(name, :json, options) } + end end class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition @@ -242,17 +246,19 @@ module ActiveRecord QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) auto_increment PRIMARY KEY", - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "int", :limit => 4 }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "tinyint", :limit => 1 } + primary_key: "int(11) auto_increment PRIMARY KEY", + string: { name: "varchar", limit: 255 }, + text: { name: "text" }, + integer: { name: "int", limit: 4 }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "datetime" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "blob" }, + boolean: { name: "tinyint", limit: 1 }, + bigint: { name: "bigint" }, + json: { name: "json" }, } INDEX_TYPES = [:fulltext, :spatial] @@ -721,8 +727,10 @@ module ActiveRecord # SHOW VARIABLES LIKE 'name' def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') + variables = select_all("select @@#{name} as 'Value'", 'SCHEMA') variables.first['Value'] unless variables.empty? + rescue ActiveRecord::StatementInvalid + nil end # Returns a table's primary key and belonging sequence. @@ -790,6 +798,7 @@ module ActiveRecord m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) + m.register_type %r(^json)i, MysqlJson.new register_integer_type m, %r(^bigint)i, limit: 8 register_integer_type m, %r(^int)i, limit: 4 @@ -971,14 +980,14 @@ module ActiveRecord defaults = [':default', :default].to_set # Make MySQL reject illegal values rather than truncating or blanking them, see - # http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html#sqlmode_strict_all_tables + # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict]) variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see - # http://dev.mysql.com/doc/refman/5.6/en/set-statement.html#id944430 + # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) if @config[:encoding] encoding = "NAMES #{@config[:encoding]}" @@ -1018,7 +1027,7 @@ module ActiveRecord when 0..0xfff; "varbinary(#{limit})" when nil; "blob" when 0x1000..0xffffffff; "blob(#{limit})" - else raise(ActiveRecordError, "No binary type has character length #{limit}") + else raise(ActiveRecordError, "No binary type has byte length #{limit}") end end @@ -1039,7 +1048,15 @@ module ActiveRecord when nil, 0x100..0xffff; 'text' when 0x10000..0xffffff; 'mediumtext' when 0x1000000..0xffffffff; 'longtext' - else raise(ActiveRecordError, "No text type has character length #{limit}") + else raise(ActiveRecordError, "No text type has byte length #{limit}") + end + end + + class MysqlJson < Type::Internal::AbstractJson # :nodoc: + def changed_in_place?(raw_old_value, new_value) + # Normalization is required because MySQL JSON data format includes + # the space between the elements. + super(serialize(deserialize(raw_old_value)), new_value) end end @@ -1063,6 +1080,8 @@ module ActiveRecord end end + ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql) + ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index b7db57c9fe..734b384e80 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,6 +1,6 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.18' +gem 'mysql2', '>= 0.3.18', '< 0.5' require 'mysql2' module ActiveRecord @@ -41,6 +41,10 @@ module ActiveRecord true end + def supports_json? + version >= '5.7.8' + end + # HELPER METHODS =========================================== def each_hash(result) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 2ae462d773..e4fcff8814 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -58,9 +58,9 @@ module ActiveRecord # * <tt>:password</tt> - Defaults to nothing. # * <tt>:database</tt> - The name of the database. No default, must be provided. # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.6/en/set-statement.html). + # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/auto-reconnect.html). + # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html) + # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/set-statement.html). # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. @@ -223,7 +223,7 @@ module ActiveRecord return @client_encoding if @client_encoding result = exec_query( - "SHOW VARIABLES WHERE Variable_name = 'character_set_client'", + "select @@character_set_client", 'SCHEMA') @client_encoding = ENCODINGS[result.rows.last.last] end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index 8e1256baad..dbc879ffd4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,32 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Value # :nodoc: - include Type::Helpers::Mutable - - def type - :json - end - - def deserialize(value) - if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) rescue nil - else - value - end - end - - def serialize(value) - if value.is_a?(::Array) || value.is_a?(::Hash) - ::ActiveSupport::JSON.encode(value) - else - value - end - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end + class Json < Type::Internal::AbstractJson end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 191c828e60..6155e53632 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -36,7 +36,7 @@ module ActiveRecord WHERE t.typname IN (%s) OR t.typtype IN (%s) - OR t.typinput::varchar = 'array_in' + OR t.typinput = 'array_in(cstring,oid,integer)'::regprocedure OR t.typelem != 0 SQL end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index f175730551..d5879ea7df 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -31,6 +31,11 @@ module ActiveRecord Utils.extract_schema_qualified_name(name.to_s).quoted end + # Quotes schema names for use in SQL queries. + def quote_schema_name(name) + PGconn.quote_ident(name) + end + def quote_table_name_for_assignment(table, attr) quote_column_name(attr) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index d114cad16b..69aa02ccf4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -68,7 +68,7 @@ module ActiveRecord execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end - # Returns the list of all tables in the schema search path or a specified schema. + # Returns the list of all tables in the schema search path. def tables(name = nil) select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA') end @@ -101,15 +101,19 @@ module ActiveRecord # Verifies existence of an index with a given name. def index_name_exists?(table_name, index_name, default) + table = Utils.extract_schema_qualified_name(table_name.to_s) + index = Utils.extract_schema_qualified_name(index_name.to_s) + select_value(<<-SQL, 'SCHEMA').to_i > 0 SELECT COUNT(*) FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid INNER JOIN pg_class i ON d.indexrelid = i.oid + LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' - AND i.relname = '#{index_name}' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + AND i.relname = '#{index.identifier}' + AND t.relname = '#{table.identifier}' + AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'} SQL end @@ -210,12 +214,12 @@ module ActiveRecord # Creates a schema for the given schema name. def create_schema schema_name - execute "CREATE SCHEMA #{schema_name}" + execute "CREATE SCHEMA #{quote_schema_name(schema_name)}" end # Drops the schema for the given schema name. - def drop_schema schema_name - execute "DROP SCHEMA #{schema_name} CASCADE" + def drop_schema(schema_name, options = {}) + execute "DROP SCHEMA#{' IF EXISTS' if options[:if_exists]} #{quote_schema_name(schema_name)} CASCADE" end # Sets the schema search path to a string of comma-separated schema names. @@ -376,7 +380,7 @@ module ActiveRecord new_seq = "#{new_name}_#{pk}_seq" idx = "#{table_name}_pkey" new_idx = "#{new_name}_pkey" - execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" end @@ -447,8 +451,15 @@ module ActiveRecord execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}" end - def remove_index!(table_name, index_name) #:nodoc: - execute "DROP INDEX #{quote_table_name(index_name)}" + def remove_index(table_name, options = {}) #:nodoc: + index_name = index_name_for_remove(table_name, options) + algorithm = + if Hash === options && options.key?(:algorithm) + index_algorithms.fetch(options[:algorithm]) do + raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") + end + end + execute "DROP INDEX #{algorithm} #{quote_table_name(index_name)}" end # Renames an index of a table. Raises error if length of new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 2c43c46a3d..27291bd2ea 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -201,6 +201,10 @@ module ActiveRecord true end + def supports_json? + postgresql_version >= 90200 + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 358039723f..24fc67938d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -375,7 +375,8 @@ module ActiveRecord pks[0]['name'] end - def remove_index!(table_name, index_name) #:nodoc: + def remove_index(table_name, options = {}) #:nodoc: + index_name = index_name_for_remove(table_name, options) exec_query "DROP INDEX #{quote_column_name(index_name)}" end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 91a13cb0cd..4902fcb1a2 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -18,10 +18,9 @@ module ActiveRecord # conversation.archived? # => true # conversation.status # => "archived" # - # # conversation.update! status: 1 + # # conversation.status = 1 # conversation.status = "archived" # - # # conversation.update! status: nil # conversation.status = nil # conversation.status.nil? # => true # conversation.status # => nil diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index d589620f8a..6721fe144f 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -63,7 +63,7 @@ module ActiveRecord class RecordNotSaved < ActiveRecordError attr_reader :record - def initialize(message, record = nil) + def initialize(message = nil, record = nil) @record = record super(message) end @@ -80,7 +80,7 @@ module ActiveRecord class RecordNotDestroyed < ActiveRecordError attr_reader :record - def initialize(message, record = nil) + def initialize(message = nil, record = nil) @record = record super(message) end @@ -92,9 +92,9 @@ module ActiveRecord class StatementInvalid < ActiveRecordError attr_reader :original_exception - def initialize(message, original_exception = nil) - super(message) + def initialize(message = nil, original_exception = nil) @original_exception = original_exception + super(message) end end @@ -134,10 +134,14 @@ module ActiveRecord class StaleObjectError < ActiveRecordError attr_reader :record, :attempted_action - def initialize(record, attempted_action) - super("Attempted to #{attempted_action} a stale object: #{record.class.name}") - @record = record - @attempted_action = attempted_action + def initialize(record = nil, attempted_action = nil) + if record && attempted_action + @record = record + @attempted_action = attempted_action + super("Attempted to #{attempted_action} a stale object: #{record.class.name}.") + else + super("Stale object error.") + end end end @@ -196,7 +200,7 @@ module ActiveRecord class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute - def initialize(message, exception, attribute) + def initialize(message = nil, exception = nil, attribute = nil) super(message) @exception = exception @attribute = attribute @@ -209,7 +213,7 @@ module ActiveRecord class MultiparameterAssignmentErrors < ActiveRecordError attr_reader :errors - def initialize(errors) + def initialize(errors = nil) @errors = errors end end @@ -218,11 +222,16 @@ module ActiveRecord class UnknownPrimaryKey < ActiveRecordError attr_reader :model - def initialize(model) - super("Unknown primary key for table #{model.table_name} in model #{model}.") - @model = model + def initialize(model = nil, description = nil) + if model + message = "Unknown primary key for table #{model.table_name} in model #{model}." + message += "\n#{description}" if description + @model = model + super(message) + else + super("Unknown primary key.") + end end - end # Raised when a relation cannot be mutated because it's already loaded. diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index e613d157aa..c26842014d 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -82,7 +82,7 @@ module ActiveRecord # Returns the class descending directly from ActiveRecord::Base, or # an abstract class, if any, in the inheritance hierarchy. # - # If A extends AR::Base, A.base_class will return A. If B descends from A + # If A extends ActiveRecord::Base, A.base_class will return A. If B descends from A # through some arbitrarily deep hierarchy, B.base_class will return A. # # If B < A and C < B and if A is an abstract_class then both B.base_class diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index 3d95c54ef3..8ecdf76b72 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -51,7 +51,7 @@ module ActiveRecord # end # # Database-specific information on row locking: - # MySQL: http://dev.mysql.com/doc/refman/5.6/en/innodb-locking-reads.html + # MySQL: http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html # PostgreSQL: http://www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE module Pessimistic # Obtain a row lock on this record. Reloads the record to obtain the requested diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 4d597a0ab1..b63caa4473 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -47,20 +47,40 @@ module ActiveRecord binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect end - name = color(name, nil, true) + name = colorize_payload_name(name, payload[:name]) sql = color(sql, sql_color(sql), true) debug " #{name} #{sql}#{binds}" end + private + + def colorize_payload_name(name, payload_name) + if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists + color(name, MAGENTA, true) + else + color(name, CYAN, true) + end + end + def sql_color(sql) case sql - when /\s*\Ainsert/i then GREEN - when /\s*\Aselect/i then BLUE - when /\s*\Aupdate/i then YELLOW - when /\s*\Adelete/i then RED - when /transaction\s*\Z/i then CYAN - else MAGENTA + when /\A\s*rollback/mi + RED + when /\s*.*?select .*for update/mi, /\A\s*lock/mi + WHITE + when /\A\s*select/i + BLUE + when /\A\s*insert/i + GREEN + when /\A\s*update/i + YELLOW + when /\A\s*delete/i + RED + when /transaction\s*\Z/i + CYAN + else + MAGENTA end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index b7b508d853..5bdb7213cd 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -9,40 +9,128 @@ module ActiveRecord end end - # Exception that can be raised to stop migrations from going backwards. + # Exception that can be raised to stop migrations from being rolled back. + # For example the following migration is not reversible. + # Rolling back this migration will raise an ActiveRecord::IrreversibleMigration error. + # + # class IrreversibleMigrationExample < ActiveRecord::Migration + # def change + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # execute <<-SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # end + # + # There are two ways to mitigate this problem. + # + # 1. Define <tt>#up</tt> and <tt>#down</tt> methods instead of <tt>#change</tt>: + # + # class ReversibleMigrationExample < ActiveRecord::Migration + # def up + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # execute <<-SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # + # def down + # execute <<-SQL + # ALTER TABLE distributors + # DROP CONSTRAINT zipchk + # SQL + # + # drop_table :distributors + # end + # end + # + # 2. Use the #reversible method in <tt>#change</tt> method: + # + # class ReversibleMigrationExample < ActiveRecord::Migration + # def change + # create_table :distributors do |t| + # t.string :zipcode + # end + # + # reversible do |dir| + # dir.up do + # execute <<-SQL + # ALTER TABLE distributors + # ADD CONSTRAINT zipchk + # CHECK (char_length(zipcode) = 5) NO INHERIT; + # SQL + # end + # + # dir.down do + # execute <<-SQL + # ALTER TABLE distributors + # DROP CONSTRAINT zipchk + # SQL + # end + # end + # end + # end class IrreversibleMigration < MigrationError end class DuplicateMigrationVersionError < MigrationError#:nodoc: - def initialize(version) - super("Multiple migrations have the version number #{version}") + def initialize(version = nil) + if version + super("Multiple migrations have the version number #{version}.") + else + super("Duplicate migration version error.") + end end end class DuplicateMigrationNameError < MigrationError#:nodoc: - def initialize(name) - super("Multiple migrations have the name #{name}") + def initialize(name = nil) + if name + super("Multiple migrations have the name #{name}.") + else + super("Duplicate migration name.") + end end end class UnknownMigrationVersionError < MigrationError #:nodoc: - def initialize(version) - super("No migration with version number #{version}") + def initialize(version = nil) + if version + super("No migration with version number #{version}.") + else + super("Unknown migration version.") + end end end class IllegalMigrationNameError < MigrationError#:nodoc: - def initialize(name) - super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)") + def initialize(name = nil) + if name + super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed).") + else + super("Illegal name for migration.") + end end end class PendingMigrationError < MigrationError#:nodoc: - def initialize - if defined?(Rails.env) - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}") + def initialize(message = nil) + if !message && defined?(Rails.env) + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}.") + elsif !message + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate.") else - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate") + super end end end @@ -168,7 +256,7 @@ module ActiveRecord # # rails generate migration add_fieldname_to_tablename fieldname:string # - # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: + # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration # def change # add_column :tablenames, :fieldname, :string @@ -376,6 +464,7 @@ module ActiveRecord attr_accessor :delegate # :nodoc: attr_accessor :disable_ddl_transaction # :nodoc: + # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) end @@ -408,7 +497,10 @@ module ActiveRecord new.migrate direction end - # Disable DDL transactions for this migration. + # Disable the transaction wrapping this migration. + # You can still create your own transactions even after calling #disable_ddl_transaction! + # + # For more details read the {"Transactional Migrations" section above}[rdoc-ref:Migration]. def disable_ddl_transaction! @disable_ddl_transaction = true end @@ -455,7 +547,7 @@ module ActiveRecord # Or equivalently, if +TenderloveMigration+ is defined as in the # documentation for Migration: # - # require_relative '2012121212_tenderlove_migration' + # require_relative '20121212123456_tenderlove_migration' # # class FixupTLMigration < ActiveRecord::Migration # def change @@ -471,13 +563,13 @@ module ActiveRecord def revert(*migration_classes) run(*migration_classes.reverse, revert: true) unless migration_classes.empty? if block_given? - if @connection.respond_to? :revert - @connection.revert { yield } + if connection.respond_to? :revert + connection.revert { yield } else - recorder = CommandRecorder.new(@connection) + recorder = CommandRecorder.new(connection) @connection = recorder suppress_messages do - @connection.revert { yield } + connection.revert { yield } end @connection = recorder.delegate recorder.commands.each do |cmd, args, block| @@ -488,7 +580,7 @@ module ActiveRecord end def reverting? - @connection.respond_to?(:reverting) && @connection.reverting + connection.respond_to?(:reverting) && connection.reverting end class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc: @@ -545,7 +637,7 @@ module ActiveRecord revert { run(*migration_classes, direction: dir, revert: true) } else migration_classes.each do |migration_class| - migration_class.new.exec_migration(@connection, dir) + migration_class.new.exec_migration(connection, dir) end end end @@ -637,7 +729,7 @@ module ActiveRecord arg_list = arguments.map(&:inspect) * ', ' say_with_time "#{method}(#{arg_list})" do - unless @connection.respond_to? :revert + unless connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) if [:rename_table, :add_foreign_key].include?(method) || @@ -810,7 +902,7 @@ module ActiveRecord new(:up, migrations, target_version).migrate end - def down(migrations_paths, target_version = nil, &block) + def down(migrations_paths, target_version = nil) migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? @@ -849,10 +941,6 @@ module ActiveRecord migrations(migrations_paths).any? end - def last_version - last_migration.version - end - def last_migration #:nodoc: migrations(migrations_paths).last || NullMigration.new end @@ -863,10 +951,6 @@ module ActiveRecord Array(@migrations_paths) end - def migrations_path - migrations_paths.first - end - def migrations(paths) paths = Array(paths) diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index dcc2362397..4c4afb4dbd 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -5,15 +5,34 @@ module ActiveRecord # knows how to invert the following commands: # # * add_column + # * add_foreign_key # * add_index + # * add_reference # * add_timestamps - # * create_table + # * change_column_default (must supply a :from and :to option) + # * change_column_null # * create_join_table + # * create_table + # * disable_extension + # * drop_join_table + # * drop_table (must supply a block) + # * enable_extension + # * remove_column (must supply a type) + # * remove_foreign_key (must supply a second table) + # * remove_index + # * remove_reference # * remove_timestamps # * rename_column # * rename_index # * rename_table class CommandRecorder + ReversibleAndIrreversibleMethods = [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, + :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, + :change_column_default, :add_reference, :remove_reference, :transaction, + :drop_join_table, :drop_table, :execute_block, :enable_extension, :disable_extension, + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + ] include JoinTable attr_accessor :commands, :delegate, :reverting @@ -41,7 +60,7 @@ module ActiveRecord @reverting = !@reverting end - # record +command+. +command+ should be a method name and arguments. + # Record +command+. +command+ should be a method name and arguments. # For example: # # recorder.record(:method_name, [:arg1, :arg2]) @@ -62,7 +81,12 @@ module ActiveRecord # invert the +command+. def inverse_of(command, args, &block) method = :"invert_#{command}" - raise IrreversibleMigration unless respond_to?(method, true) + raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true) + This migration uses #{command}, which is not automatically reversible. + To make the migration reversible you can either: + 1. Define #up and #down methods in place of the #change method. + 2. Use the #reversible method to define reversible behavior. + MSG send(method, args, &block) end @@ -70,14 +94,7 @@ module ActiveRecord super || delegate.respond_to?(*args) end - [:create_table, :create_join_table, :rename_table, :add_column, :remove_column, - :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, - :add_reference, :remove_reference, :transaction, - :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null, - :add_foreign_key, :remove_foreign_key - # irreversible methods need to be here too - ].each do |method| + ReversibleAndIrreversibleMethods.each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) record(:"#{method}", args, &block) # record(:create_table, args, &block) diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 5a6f42ba09..2b0c755ef4 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -240,7 +240,7 @@ module ActiveRecord end # Returns a hash where the keys are column names and the values are - # default values when instantiating the AR object for this table. + # default values when instantiating the Active Record object for this table. def column_defaults load_schema _default_attributes.to_hash diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index da6b8447d3..6dd54f9262 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -16,11 +16,11 @@ module ActiveRecord config.app_generators.orm :active_record, :migration => true, :timestamps => true - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::QueryCache" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::QueryCache - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::ConnectionAdapters::ConnectionManagement config.action_dispatch.rescue_responses.merge!( 'ActiveRecord::RecordNotFound' => :not_found, @@ -78,8 +78,8 @@ module ActiveRecord initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::Migration::CheckPending" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::Migration::CheckPending end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6a72d528b4..d543a84ff1 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -79,7 +79,7 @@ db_namespace = namespace :db do task :up => [:environment, :load_config] do version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil raise 'VERSION is required' unless version - ActiveRecord::Migrator.run(:up, ActiveRecord::Migrator.migrations_paths, version) + ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace['_dump'].invoke end @@ -87,7 +87,7 @@ db_namespace = namespace :db do task :down => [:environment, :load_config] do version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil raise 'VERSION is required - To go down one migration, run db:rollback' unless version - ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version) + ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace['_dump'].invoke end @@ -99,7 +99,7 @@ db_namespace = namespace :db do db_list = ActiveRecord::SchemaMigration.normalized_versions file_list = - ActiveRecord::Migrator.migrations_paths.flat_map do |path| + ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path| # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do version = ActiveRecord::SchemaMigration.normalize_migration_number($1) @@ -125,14 +125,14 @@ db_namespace = namespace :db do desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).' task :rollback => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step) + ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) db_namespace['_dump'].invoke end # desc 'Pushes the schema to the next version (specify steps w/ STEP=n).' task :forward => [:environment, :load_config] do step = ENV['STEP'] ? ENV['STEP'].to_i : 1 - ActiveRecord::Migrator.forward(ActiveRecord::Migrator.migrations_paths, step) + ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step) db_namespace['_dump'].invoke end @@ -160,7 +160,7 @@ db_namespace = namespace :db do # desc "Raises an error if there are pending migrations" task :abort_if_pending_migrations => [:environment, :load_config] do - pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations + pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations if pending_migrations.any? puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}" @@ -229,7 +229,7 @@ db_namespace = namespace :db do end namespace :schema do - desc 'Creates a db/schema.rb file that is portable against any DB supported by AR' + desc 'Creates a db/schema.rb file that is portable against any DB supported by Active Record' task :dump => [:environment, :load_config] do require 'active_record/schema_dumper' filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb') @@ -384,7 +384,7 @@ namespace :railties do puts "Copied migration #{migration.basename} from #{name}" end - ActiveRecord::Migration.copy(ActiveRecord::Migrator.migrations_paths.first, railties, + ActiveRecord::Migration.copy(ActiveRecord::Tasks::DatabaseTasks.migrations_paths.first, railties, :on_skip => on_skip, :on_copy => on_copy) end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 5360db6a19..2b648978b3 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -32,6 +32,7 @@ module ActiveRecord end def self.add_reflection(ar, name, reflection) + ar.clear_reflections_cache ar._reflections = ar._reflections.merge(name.to_s => reflection) end @@ -67,18 +68,22 @@ module ActiveRecord # # @api public def reflections - ref = {} - _reflections.each do |name, reflection| - parent_reflection = reflection.parent_reflection + @__reflections ||= begin + ref = {} - if parent_reflection - parent_name = parent_reflection.name - ref[parent_name.to_s] = parent_reflection - else - ref[name] = reflection + _reflections.each do |name, reflection| + parent_reflection = reflection.parent_reflection + + if parent_reflection + parent_name = parent_reflection.name + ref[parent_name.to_s] = parent_reflection + else + ref[name] = reflection + end end + + ref end - ref end # Returns an array of AssociationReflection objects for all the @@ -94,7 +99,8 @@ module ActiveRecord # @api public def reflect_on_all_associations(macro = nil) association_reflections = reflections.values - macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections + association_reflections.select! { |reflection| reflection.macro == macro } if macro + association_reflections end # Returns the AssociationReflection object for the +association+ (use the symbol). @@ -118,6 +124,10 @@ module ActiveRecord def reflect_on_all_autosave_associations reflections.values.select { |reflection| reflection.options[:autosave] } end + + def clear_reflections_cache #:nodoc: + @__reflections = nil + end end # Holds all the methods that are shared between MacroReflection, AssociationReflection @@ -159,6 +169,54 @@ module ActiveRecord scope_chain.flatten end + def counter_cache_column + if belongs_to? + if options[:counter_cache] == true + "#{active_record.name.demodulize.underscore.pluralize}_count" + elsif options[:counter_cache] + options[:counter_cache].to_s + end + else + options[:counter_cache] ? options[:counter_cache].to_s : "#{name}_count" + end + end + + # This shit is nasty. We need to avoid the following situation: + # + # * An associated record is deleted via record.destroy + # * Hence the callbacks run, and they find a belongs_to on the record with a + # :counter_cache options which points back at our owner. So they update the + # counter cache. + # * In which case, we must make sure to *not* update the counter cache, or else + # it will be decremented twice. + # + # Hence this method. + def inverse_which_updates_counter_cache + return @inverse_which_updates_counter_cache if defined?(@inverse_which_updates_counter_cache) + @inverse_which_updates_counter_cache = klass.reflect_on_all_associations(:belongs_to).find do |inverse| + inverse.counter_cache_column == counter_cache_column + end + end + alias inverse_updates_counter_cache? inverse_which_updates_counter_cache + + def inverse_updates_counter_in_memory? + inverse_of && inverse_which_updates_counter_cache == inverse_of + end + + # Returns whether a counter cache should be used for this association. + # + # The counter_cache option must be given on either the owner or inverse + # association, and the column must be present on the owner. + def has_cached_counter? + options[:counter_cache] || + inverse_which_updates_counter_cache && inverse_which_updates_counter_cache.options[:counter_cache] && + !!active_record.columns_hash[counter_cache_column] + end + + def counter_must_be_updated_by_has_many? + !inverse_updates_counter_in_memory? && has_cached_counter? + end + def alias_candidate(name) "#{plural_name}_#{name}" end @@ -321,14 +379,6 @@ module ActiveRecord @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end - def counter_cache_column - if options[:counter_cache] == true - "#{active_record.name.demodulize.underscore.pluralize}_count" - elsif options[:counter_cache] - options[:counter_cache].to_s - end - end - def check_validity! check_validity_of_inverse! end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index e47b7b1ed9..bf08cdbbf3 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -418,7 +418,7 @@ module ActiveRecord end end - # Destroys the records matching +conditions+ by instantiating each + # Destroys the records by instantiating each # record and calling its +destroy+ method. Each object's callbacks are # executed (including <tt>:dependent</tt> association options). Returns the # collection of objects that were destroyed; each will be frozen, to @@ -431,20 +431,15 @@ module ActiveRecord # rows quickly, without concern for their associations or callbacks, use # +delete_all+ instead. # - # ==== Parameters - # - # * +conditions+ - A string, array, or hash that specifies which records - # to destroy. If omitted, all records are destroyed. See the - # Conditions section in the introduction to ActiveRecord::Base for - # more information. - # # ==== Examples # - # Person.destroy_all("last_login < '2004-04-04'") - # Person.destroy_all(status: "inactive") # Person.where(age: 0..18).destroy_all def destroy_all(conditions = nil) if conditions + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1. + To achieve the same use where(conditions).destroy_all + MESSAGE where(conditions).destroy_all else to_a.each(&:destroy).tap { reset } @@ -478,15 +473,13 @@ module ActiveRecord end end - # Deletes the records matching +conditions+ without instantiating the records + # Deletes the records without instantiating the records # first, and hence not calling the +destroy+ method nor invoking callbacks. This # is a single SQL DELETE statement that goes straight to the database, much more # efficient than +destroy_all+. Be careful with relations though, in particular # <tt>:dependent</tt> rules defined on associations are not honored. Returns the # number of rows affected. # - # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')") - # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else']) # Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all # # Both calls delete the affected posts all at once with a single DELETE statement. @@ -512,6 +505,10 @@ module ActiveRecord end if conditions + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Passing conditions to delete_all is deprecated and will be removed in Rails 5.1. + To achieve the same use where(conditions).delete_all + MESSAGE where(conditions).delete_all else stmt = Arel::DeleteManager.new diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 0b38666ce9..cb971eb255 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -148,11 +148,15 @@ module ActiveRecord end end + CLAUSE_METHOD_NAMES = CLAUSE_METHODS.map do |name| + ["#{name}_clause", "#{name}_clause="] + end + def merge_clauses - CLAUSE_METHODS.each do |name| - clause = relation.send("#{name}_clause") - other_clause = other.send("#{name}_clause") - relation.send("#{name}_clause=", clause.merge(other_clause)) + CLAUSE_METHOD_NAMES.each do |(reader, writer)| + clause = relation.send(reader) + other_clause = other.send(reader) + relation.send(writer, clause.merge(other_clause)) end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index d26db7d4cf..e232516b0c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -24,12 +24,12 @@ module ActiveRecord end def build_from_hash(attributes) - attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + attributes = convert_dot_notation_to_hash(attributes) expand_from_hash(attributes) end def create_binds(attributes) - attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + attributes = convert_dot_notation_to_hash(attributes) create_binds_for_hash(attributes) end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb index 159889d3b8..e81be63cd3 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -10,10 +10,10 @@ module ActiveRecord table = value.associated_table if value.base_class - queries[table.association_foreign_type] = value.base_class.name + queries[table.association_foreign_type.to_s] = value.base_class.name end - queries[table.association_foreign_key] = value.ids + queries[table.association_foreign_key.to_s] = value.ids predicate_builder.build_from_hash(queries) end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 706c99c245..e25b889851 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -548,7 +548,7 @@ module ActiveRecord # If the condition is any blank-ish object, then #where is a no-op and returns # the current relation. def where(opts = :chain, *rest) - if opts == :chain + if :chain == opts WhereChain.new(spawn) elsif opts.blank? self diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 0430922be3..23eaab4699 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -15,6 +15,7 @@ module ActiveRecord when Hash attributes = predicate_builder.resolve_column_aliases(opts) attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) + attributes.stringify_keys! attributes, binds = predicate_builder.create_binds(attributes) diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 0a5546a760..c1a42dc629 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -28,24 +28,6 @@ module ActiveRecord # ActiveRecord::Schema is only supported by database adapters that also # support migrations, the two features being very similar. class Schema < Migration - - # Returns the migrations paths. - # - # ActiveRecord::Schema.new.migrations_paths - # # => ["db/migrate"] # Rails migration path by default. - def migrations_paths - ActiveRecord::Migrator.migrations_paths - end - - def define(info, &block) # :nodoc: - instance_eval(&block) - - unless info[:version].blank? - initialize_schema_migrations_table - connection.assume_migrated_upto_version(info[:version], migrations_paths) - end - end - # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the # database definition DSL to build up your schema (+create_table+, @@ -60,5 +42,23 @@ module ActiveRecord def self.define(info={}, &block) new.define(info, &block) end + + def define(info, &block) # :nodoc: + instance_eval(&block) + + if info[:version].present? + initialize_schema_migrations_table + connection.assume_migrated_upto_version(info[:version], migrations_paths) + end + end + + private + # Returns the migrations paths. + # + # ActiveRecord::Schema.new.migrations_paths + # # => ["db/migrate"] # Rails migration path by default. + def migrations_paths # :nodoc: + ActiveRecord::Migrator.migrations_paths + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index cb47bf23f7..b384529e75 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -2,7 +2,11 @@ require 'active_record/scoping/default' require 'active_record/scoping/named' module ActiveRecord - class SchemaMigration < ActiveRecord::Base + # This class is used to create a table that keeps track of which migrations + # have been applied to a given database. When a migration is run, its schema + # number is inserted in to the `SchemaMigration.table_name` so it doesn't need + # to be executed the next time. + class SchemaMigration < ActiveRecord::Base # :nodoc: class << self def primary_key nil diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 3590b8846e..fac566e12b 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -6,8 +6,10 @@ module ActiveRecord included do # Stores the default scope for the class. class_attribute :default_scopes, instance_writer: false, instance_predicate: false + class_attribute :default_scope_override, instance_predicate: false self.default_scopes = [] + self.default_scope_override = nil end module ClassMethods @@ -99,11 +101,18 @@ module ActiveRecord self.default_scopes += [scope] end - def build_default_scope(base_rel = relation) # :nodoc: - if !Base.is_a?(method(:default_scope).owner) + def build_default_scope(base_rel = nil) # :nodoc: + return if abstract_class? + + if self.default_scope_override.nil? + self.default_scope_override = !Base.is_a?(method(:default_scope).owner) + end + + if self.default_scope_override # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } elsif default_scopes.any? + base_rel ||= relation evaluate_default_scope do default_scopes.inject(base_rel) do |default_scope, scope| default_scope.merge(base_rel.scoping { scope.call }) diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 48c12dcf9f..23dc6465af 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -18,5 +18,3 @@ module ActiveRecord #:nodoc: end end end - -require 'active_record/serializers/xml_serializer' diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb deleted file mode 100644 index 89b7e0be82..0000000000 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ /dev/null @@ -1,193 +0,0 @@ -require 'active_support/core_ext/hash/conversions' - -module ActiveRecord #:nodoc: - module Serialization - include ActiveModel::Serializers::Xml - - # Builds an XML document to represent the model. Some configuration is - # available through +options+. However more complicated cases should - # override ActiveRecord::Base#to_xml. - # - # By default the generated XML document will include the processing - # instruction and all the object's attributes. For example: - # - # <?xml version="1.0" encoding="UTF-8"?> - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <id type="integer">1</id> - # <approved type="boolean">false</approved> - # <replies-count type="integer">0</replies-count> - # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>, - # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> . - # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the - # +attributes+ method. The default is to dasherize all column names, but you - # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt> - # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>. - # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+. - # - # For instance: - # - # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ]) - # - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <approved type="boolean">false</approved> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # To include first level associations use <tt>:include</tt>: - # - # firm.to_xml include: [ :account, :clients ] - # - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # Additionally, the record being serialized will be passed to a Proc's second - # parameter. This allows for ad hoc additions to the resultant document that - # incorporate the context of the record being serialized. And by leveraging the - # closure created by a Proc, to_xml can be used to add elements that normally fall - # outside of the scope of the model -- for example, generating and appending URLs - # associated with models. - # - # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <name-reverse>slangis73</name-reverse> - # </firm> - # - # To include deeper levels of associations pass a hash like this: - # - # firm.to_xml include: {account: {}, clients: {include: :address}} - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # <address> - # ... - # </address> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # <address> - # ... - # </address> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # To include any methods on the model being called use <tt>:methods</tt>: - # - # firm.to_xml methods: [ :calculated_earnings, :real_earnings ] - # - # <firm> - # # ... normal attributes as shown above ... - # <calculated-earnings>100000000000000000</calculated-earnings> - # <real-earnings>5</real-earnings> - # </firm> - # - # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a - # modified version of the options hash that was given to +to_xml+: - # - # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <abc>def</abc> - # </firm> - # - # Alternatively, you can yield the builder object as part of the +to_xml+ call: - # - # firm.to_xml do |xml| - # xml.creator do - # xml.first_name "David" - # xml.last_name "Heinemeier Hansson" - # end - # end - # - # <firm> - # # ... normal attributes as shown above ... - # <creator> - # <first_name>David</first_name> - # <last_name>Heinemeier Hansson</last_name> - # </creator> - # </firm> - # - # As noted above, you may override +to_xml+ in your ActiveRecord::Base - # subclasses to have complete control about what's generated. The general - # form of doing this is: - # - # class IHaveMyOwnXML < ActiveRecord::Base - # def to_xml(options = {}) - # require 'builder' - # options[:indent] ||= 2 - # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - # xml.instruct! unless options[:skip_instruct] - # xml.level_one do - # xml.tag!(:second_level, 'content') - # end - # end - # end - def to_xml(options = {}, &block) - XmlSerializer.new(self, options).serialize(&block) - end - end - - class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: - def compute_type - klass = @serializable.class - cast_type = klass.type_for_attribute(name) - - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type - - { :text => :string, - :time => :datetime }[type] || type - end - protected :compute_type - end - end -end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 41f1c55c3c..f9bb1cf5e0 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -10,13 +10,15 @@ module ActiveRecord end def resolve_column_aliases(hash) - hash = hash.dup - hash.keys.grep(Symbol) do |key| - if klass.attribute_alias? key - hash[klass.attribute_alias(key)] = hash.delete key + # This method is a hot spot, so for now, use Hash[] to dup the hash. + # https://bugs.ruby-lang.org/issues/7166 + new_hash = Hash[hash] + hash.each do |key, _| + if (key.is_a?(Symbol)) && klass.attribute_alias?(key) + new_hash[klass.attribute_alias(key)] = new_hash.delete(key) end end - hash + new_hash end def arel_attribute(column_name) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 683741768b..f1141a8613 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -134,7 +134,7 @@ module ActiveRecord version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil scope = ENV['SCOPE'] verbose_was, Migration.verbose = Migration.verbose, verbose - Migrator.migrate(Migrator.migrations_paths, version) do |migration| + Migrator.migrate(migrations_paths, version) do |migration| scope.blank? || scope == migration.scope end ensure @@ -221,12 +221,6 @@ module ActiveRecord end end - def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env) - if File.exist?(file || schema_file(format)) - load_schema_current(format, file, environment) - end - end - def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) each_current_configuration(environment) { |configuration| load_schema configuration, format, file diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 673386f0d9..ba5dd0a0d9 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -23,7 +23,7 @@ module ActiveRecord end rescue error_class => error if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR - $stdout.print error.error + $stdout.print error.message establish_connection root_configuration_without_database connection.create_database configuration['database'], creation_options if configuration['username'] != 'root' diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index d7da95c8a9..55f839444b 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -1,5 +1,3 @@ -require 'shellwords' - module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: @@ -55,19 +53,22 @@ module ActiveRecord when String ActiveRecord::Base.dump_schemas end + + args = ['-i', '-s', '-x', '-O', '-f', filename] unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + args << search_path.split(',').map do |part| + "--schema=#{part.strip}" + end.join(' ') end - - command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" - raise 'Error dumping database' unless Kernel.system(command) - + args << configuration['database'] + run_cmd('pg_dump', args, 'dumping') File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end def structure_load(filename) set_psql_env - Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}") + args = [ '-q', '-f', filename, configuration['database'] ] + run_cmd('psql', args, 'loading' ) end private @@ -93,6 +94,17 @@ module ActiveRecord ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] end + + def run_cmd(cmd, args, action) + fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) + end + + def run_cmd_error(cmd, args, action) + msg = "failed to execute:\n" + msg << "#{cmd} #{args.join(' ')}\n\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 20e4235788..e759475cfb 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -15,14 +15,21 @@ module ActiveRecord # # == Time Zone aware attributes # - # By default, ActiveRecord::Base keeps all the datetime columns time zone aware by executing following code. + # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns + # time-zone aware. By default, these values are stored in the database as UTC + # and converted back to the current Time.zone when pulled from the database. # - # config.active_record.time_zone_aware_attributes = true + # This feature can be turned off completely by setting: # - # This feature can easily be turned off by assigning value <tt>false</tt> . + # config.active_record.time_zone_aware_attributes = false # - # If your attributes are time zone aware and you desire to skip time zone conversion to the current Time.zone - # when reading certain attributes then you can do following: + # You can also specify that only <tt>datetime</tt> columns should be time-zone + # aware (while <tt>time</tt> should not) by setting: + # + # ActiveRecord::Base.time_zone_aware_types = [:datetime] + # + # Finally, you can indicate specific attributes of a model for which time zone + # conversion should not applied, for instance by setting: # # class Topic < ActiveRecord::Base # self.skip_time_zone_conversion_for_attributes = [:written_on] diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 887d7a5903..4a569fc242 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -167,7 +167,7 @@ module ActiveRecord # writing, the only database that we're aware of that supports true nested # transactions, is MS-SQL. Because of this, Active Record emulates nested # transactions by using savepoints on MySQL and PostgreSQL. See - # http://dev.mysql.com/doc/refman/5.6/en/savepoint.html + # http://dev.mysql.com/doc/refman/5.7/en/savepoint.html # for more information about savepoints. # # === Callbacks diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 2c0cda69d0..53f3b53bec 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -20,6 +20,8 @@ require 'active_record/type/adapter_specific_registry' require 'active_record/type/type_map' require 'active_record/type/hash_lookup_type_map' +require 'active_record/type/internal/abstract_json' + module ActiveRecord module Type @registry = AdapterSpecificRegistry.new diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index f200a92d10..f5b145230d 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -14,7 +14,7 @@ module ActiveRecord private def cast_value(value) - case value + casted_value = case value when ::Float convert_float_to_big_decimal(value) when ::Numeric, ::String @@ -26,6 +26,8 @@ module ActiveRecord cast_value(value.to_s) end end + + scale ? casted_value.round(scale) : casted_value end def convert_float_to_big_decimal(value) diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb new file mode 100644 index 0000000000..963a8245d0 --- /dev/null +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module Type + module Internal # :nodoc: + class AbstractJson < Type::Value # :nodoc: + include Type::Helpers::Mutable + + def type + :json + end + + def deserialize(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) rescue nil + else + value + end + end + + def serialize(value) + if value.is_a?(::Array) || value.is_a?(::Hash) + ::ActiveSupport::JSON.encode(value) + else + value + end + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end +end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 34d96b19fe..4113ca4561 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -12,10 +12,16 @@ module ActiveRecord class RecordInvalid < ActiveRecordError attr_reader :record - def initialize(record) - @record = record - errors = @record.errors.full_messages.join(", ") - super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid")) + def initialize(record = nil) + if record + @record = record + errors = @record.errors.full_messages.join(", ") + message = I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", errors: errors, default: :"errors.messages.record_invalid") + else + message = "Record invalid" + end + + super(message) end end @@ -54,7 +60,7 @@ module ActiveRecord # Validations with no <tt>:on</tt> option will run no matter the context. Validations with # some <tt>:on</tt> option will only run in the specified context. def valid?(context = nil) - context ||= (new_record? ? :create : :update) + context ||= default_validation_context output = super(context) errors.empty? && output end @@ -63,6 +69,10 @@ module ActiveRecord protected + def default_validation_context + new_record? ? :create : :update + end + def raise_validation_error raise(RecordInvalid.new(self)) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 32d17a1392..5706bbd903 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -18,7 +18,11 @@ module ActiveRecord relation = build_relation(finder_class, table, attribute, value) if record.persisted? && finder_class.primary_key.to_s != attribute.to_s - relation = relation.where.not(finder_class.primary_key => record.id) + if finder_class.primary_key + relation = relation.where.not(finder_class.primary_key => record.id) + else + raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") + end end relation = scope_relation(record, table, relation) relation = relation.merge(options[:conditions]) if options[:conditions] diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index 426b088e2f..2024aa36ab 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -24,6 +24,6 @@ class MysqlQuotingTest < ActiveRecord::MysqlTestCase @conn.stubs(:full_version).returns('5.6.3') @conn.remove_instance_variable(:@version) if @conn.instance_variable_defined?(:@version) t = Time.now.change(usec: 1) - refute_match(/\.000001\z/, @conn.quoted_date(t)) + assert_no_match(/\.000001\z/, @conn.quoted_date(t)) end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb new file mode 100644 index 0000000000..c8c933af5e --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -0,0 +1,172 @@ +require 'cases/helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_json? +class Mysql2JSONTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + class JsonDataType < ActiveRecord::Base + self.table_name = 'json_data_type' + + store_accessor :settings, :resolution + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.create_table('json_data_type') do |t| + t.json 'payload' + t.json 'settings' + end + end + end + + def teardown + @connection.drop_table :json_data_type, if_exists: true + JsonDataType.reset_column_information + end + + def test_column + column = JsonDataType.columns_hash["payload"] + assert_equal :json, column.type + assert_equal 'json', column.sql_type + + type = JsonDataType.type_for_attribute("payload") + assert_not type.binary? + end + + def test_change_table_supports_json + @connection.change_table('json_data_type') do |t| + t.json 'users' + end + JsonDataType.reset_column_information + column = JsonDataType.columns_hash['users'] + assert_equal :json, column.type + end + + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t\.json\s+"settings"/, output) + end + + def test_cast_value_on_write + x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} + assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) + assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) + x.save + assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) + end + + def test_type_cast_json + type = JsonDataType.type_for_attribute("payload") + + data = "{\"a_key\":\"a_value\"}" + hash = type.deserialize(data) + assert_equal({'a_key' => 'a_value'}, hash) + assert_equal({'a_key' => 'a_value'}, type.deserialize(data)) + + assert_equal({}, type.deserialize("{}")) + assert_equal({'key'=>nil}, type.deserialize('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + x.payload = { '"a\'' => 'b' } + assert x.save! + end + + def test_select + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + assert_equal({'k' => 'v'}, x.payload) + end + + def test_select_multikey + @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| + x = JsonDataType.first + assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload) + end + + def test_null_json + @connection.execute %q|insert into json_data_type (payload) VALUES(null)| + x = JsonDataType.first + assert_equal(nil, x.payload) + end + + def test_select_array_json_value + @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| + x = JsonDataType.first + assert_equal(['v0', {'k1' => 'v1'}], x.payload) + end + + def test_rewrite_array_json_value + @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| + x = JsonDataType.first + x.payload = ['v1', {'k2' => 'v2'}, 'v3'] + assert x.save! + end + + def test_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = JsonDataType.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = JsonDataType.first + assert_equal "640×1136", x.resolution + end + + def test_duplication_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? + + json.payload = { 'one' => 'two' } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload['three'] = 'four' + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload) + assert_not json.changed? + end + + def test_assigning_invalid_json + json = JsonDataType.new + + json.payload = 'foo' + + assert_nil json.payload + end +end +end diff --git a/activerecord/test/cases/adapters/mysql2/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb index 92221e61bf..2de7e1b526 100644 --- a/activerecord/test/cases/adapters/mysql2/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql2/quoting_test.rb @@ -16,6 +16,6 @@ class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase @connection.stubs(:full_version).returns('5.6.3') @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) t = Time.now.change(usec: 1) - refute_match(/\.000001\z/, @connection.quoted_date(t)) + assert_no_match(/\.000001\z/, @connection.quoted_date(t)) end end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index dc7ba314c6..24def31e36 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -25,7 +25,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase def test_add_index # add_index calls index_name_exists? which can't work since execute is stubbed - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.stubs(:index_name_exists?).returns(false) + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| false } expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active') assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'") @@ -49,6 +49,22 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name") WHERE state = 'active') assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'", :using => :gist) + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists? + end + + def test_remove_index + # remove_index calls index_name_exists? which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| true } + + expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name") + assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently) + + assert_raise ArgumentError do + add_index(:people, :last_name, algorithm: :copy) + end + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists? end private diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index fa6584eae5..a0afd922b2 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -31,7 +31,7 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase set_session_auth @connection.execute "RESET search_path" USERS.each do |u| - @connection.execute "DROP SCHEMA #{u} CASCADE" + @connection.drop_schema u @connection.execute "DROP USER #{u}" end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 35d5581aa7..bee612d8d3 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -2,7 +2,17 @@ require "cases/helper" require 'models/default' require 'support/schema_dumping_helper' +module PGSchemaHelper + def with_schema_search_path(schema_search_path) + @connection.schema_search_path = schema_search_path + yield if block_given? + ensure + @connection.schema_search_path = "'$user', public" + end +end + class SchemaTest < ActiveRecord::PostgreSQLTestCase + include PGSchemaHelper self.use_transactional_tests = false SCHEMA_NAME = 'test_schema' @@ -84,8 +94,8 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase end teardown do - @connection.execute "DROP SCHEMA #{SCHEMA2_NAME} CASCADE" - @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" + @connection.drop_schema SCHEMA2_NAME, if_exists: true + @connection.drop_schema SCHEMA_NAME, if_exists: true end def test_schema_names @@ -121,10 +131,17 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase assert !@connection.schema_names.include?("test_schema3") end + def test_drop_schema_if_exists + @connection.create_schema "some_schema" + assert_includes @connection.schema_names, "some_schema" + @connection.drop_schema "some_schema", if_exists: true + assert_not_includes @connection.schema_names, "some_schema" + end + def test_habtm_table_name_with_schema + ActiveRecord::Base.connection.drop_schema "music", if_exists: true + ActiveRecord::Base.connection.create_schema "music" ActiveRecord::Base.connection.execute <<-SQL - DROP SCHEMA IF EXISTS music CASCADE; - CREATE SCHEMA music; CREATE TABLE music.albums (id serial primary key); CREATE TABLE music.songs (id serial primary key); CREATE TABLE music.albums_songs (album_id integer, song_id integer); @@ -134,12 +151,16 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase Album.create assert_equal song, Song.includes(:albums).references(:albums).first ensure - ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;" + ActiveRecord::Base.connection.drop_schema "music", if_exists: true end - def test_raise_drop_schema_with_nonexisting_schema + def test_drop_schema_with_nonexisting_schema assert_raises(ActiveRecord::StatementInvalid) do - @connection.drop_schema "test_schema3" + @connection.drop_schema "idontexist" + end + + assert_nothing_raised do + @connection.drop_schema "idontexist", if_exists: true end end @@ -300,11 +321,11 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_with_uppercase_index_name @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" - assert_nothing_raised { @connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"} + assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index"} @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" with_schema_search_path SCHEMA_NAME do - assert_nothing_raised { @connection.remove_index! "things", "things_Index"} + assert_nothing_raised { @connection.remove_index "things", name: "things_Index"} end end @@ -404,13 +425,6 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase end end - def with_schema_search_path(schema_search_path) - @connection.schema_search_path = schema_search_path - yield if block_given? - ensure - @connection.schema_search_path = "'$user', public" - end - def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name) with_schema_search_path(this_schema_name) do indexes = @connection.indexes(TABLE_NAME).sort_by(&:name) @@ -462,14 +476,14 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase ensure @connection.drop_table "wagons", if_exists: true @connection.drop_table "my_schema.trains", if_exists: true - @connection.execute "DROP SCHEMA IF EXISTS my_schema" + @connection.drop_schema "my_schema", if_exists: true end end class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection - @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + @connection.drop_schema "schema_1", if_exists: true @connection.execute "CREATE SCHEMA schema_1" @connection.execute "CREATE DOMAIN schema_1.text AS text" @connection.execute "CREATE DOMAIN schema_1.varchar AS varchar" @@ -487,7 +501,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa teardown do @connection.schema_search_path = @old_search_path - @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + @connection.drop_schema "schema_1", if_exists: true Default.reset_column_information end @@ -519,3 +533,40 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa assert_equal "foo'::bar", Default.new.string_col end end + +class SchemaWithDotsTest < ActiveRecord::PostgreSQLTestCase + include PGSchemaHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_schema "my.schema" + end + + teardown do + @connection.drop_schema "my.schema", if_exists: true + end + + test "rename_table" do + with_schema_search_path('"my.schema"') do + @connection.create_table :posts + @connection.rename_table :posts, :articles + assert_equal ["articles"], @connection.tables + end + end + + test "Active Record basics" do + with_schema_search_path('"my.schema"') do + @connection.create_table :articles do |t| + t.string :title + end + article_class = Class.new(ActiveRecord::Base) do + self.table_name = '"my.schema".articles' + end + + article_class.create!(title: "zOMG, welcome to my blorgh!") + welcome_article = article_class.last + assert_equal "zOMG, welcome to my blorgh!", welcome_article.title + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb deleted file mode 100644 index 2dd6ec5fe6..0000000000 --- a/activerecord/test/cases/adapters/postgresql/view_test.rb +++ /dev/null @@ -1,64 +0,0 @@ -require "cases/helper" -require "cases/view_test" - -class UpdateableViewTest < ActiveRecord::PostgreSQLTestCase - fixtures :books - - class PrintedBook < ActiveRecord::Base - self.primary_key = "id" - end - - setup do - @connection = ActiveRecord::Base.connection - @connection.execute <<-SQL - CREATE VIEW printed_books - AS SELECT id, name, status, format FROM books WHERE format = 'paperback' - SQL - end - - teardown do - @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books" - end - - def test_update_record - book = PrintedBook.first - book.name = "AWDwR" - book.save! - book.reload - assert_equal "AWDwR", book.name - end - - def test_insert_record - PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback" - - new_book = PrintedBook.last - assert_equal "Rails in Action", new_book.name - end - - def test_update_record_to_fail_view_conditions - book = PrintedBook.first - book.format = "ebook" - book.save! - - assert_raises ActiveRecord::RecordNotFound do - book.reload - end - end -end - -if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && - ActiveRecord::Base.connection.supports_materialized_views? -class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase - include ViewBehavior - - private - def create_view(name, query) - @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}" - end - - def drop_view(name) - @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name - - end -end -end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 7996e7ad50..77d99bc116 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -425,13 +425,16 @@ module ActiveRecord configurations['arunit']['database']) statement = ::SQLite3::Statement.new(db, 'CREATE TABLE statement_test (number integer not null)') - statement.stubs(:step).raises(::SQLite3::BusyException, 'busy') - statement.stubs(:columns).once.returns([]) - statement.expects(:close).once - ::SQLite3::Statement.stubs(:new).returns(statement) - - assert_raises ActiveRecord::StatementInvalid do - @conn.exec_query 'select * from statement_test' + statement.stub(:step, ->{ raise ::SQLite3::BusyException.new('busy') }) do + assert_called(statement, :columns, returns: []) do + assert_called(statement, :close) do + ::SQLite3::Statement.stub(:new, statement) do + assert_raises ActiveRecord::StatementInvalid do + @conn.exec_query 'select * from statement_test' + end + end + end + end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 73cde05504..02b67f901f 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -1,6 +1,5 @@ require 'cases/helper' require 'models/developer' -require 'models/computer' require 'models/project' require 'models/company' require 'models/topic' @@ -21,6 +20,9 @@ require 'models/column' require 'models/record' require 'models/admin' require 'models/admin/user' +require 'models/ship' +require 'models/treasure' +require 'models/parrot' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -91,7 +93,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -108,7 +110,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -347,6 +349,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Company.all.merge!(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size end + def test_belongs_to_without_counter_cache_option + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless') + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = Treasure.new(name: 'Gold', ship: ship) + treasure.save + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = ship.treasures.first + treasure.destroy + end + end + def test_belongs_to_counter debate = Topic.create("title" => "debate") assert_equal 0, debate.read_attribute("replies_count"), "No replies yet" diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index ffbf60e390..ddfb856a05 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -108,53 +108,57 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.all.merge!(:includes=>:comments).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, returns: 5) do + posts = Post.all.merge!(:includes=>:comments).to_a + assert_equal 11, posts.size + end end def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.all.merge!(:includes=>:comments).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, returns: nil) do + posts = Post.all.merge!(:includes=>:comments).to_a + assert_equal 11, posts.size + end end def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.all.merge!(:includes=>:categories).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, times: 2, returns: 5) do + posts = Post.all.merge!(:includes=>:categories).to_a + assert_equal 11, posts.size + end end def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.all.merge!(:includes=>:categories).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, times: 2, returns: nil) do + posts = Post.all.merge!(:includes=>:categories).to_a + assert_equal 11, posts.size + end end def test_load_associated_records_in_one_query_when_adapter_has_no_limit - Comment.connection.expects(:in_clause_length).at_least_once.returns(nil) - - post = posts(:welcome) - assert_queries(2) do - Post.includes(:comments).where(:id => post.id).to_a + assert_called(Comment.connection, :in_clause_length, returns: nil) do + post = posts(:welcome) + assert_queries(2) do + Post.includes(:comments).where(:id => post.id).to_a + end end end def test_load_associated_records_in_several_queries_when_many_ids_passed - Comment.connection.expects(:in_clause_length).at_least_once.returns(1) - - post1, post2 = posts(:welcome), posts(:thinking) - assert_queries(3) do - Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a + assert_called(Comment.connection, :in_clause_length, returns: 1) do + post1, post2 = posts(:welcome), posts(:thinking) + assert_queries(3) do + Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a + end end end def test_load_associated_records_in_one_query_when_a_few_ids_passed - Comment.connection.expects(:in_clause_length).at_least_once.returns(3) - - post = posts(:welcome) - assert_queries(2) do - Post.includes(:comments).where(:id => post.id).to_a + assert_called(Comment.connection, :in_clause_length, returns: 3) do + post = posts(:welcome) + assert_queries(2) do + Post.includes(:comments).where(:id => post.id).to_a + end end end @@ -1325,6 +1329,14 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_match message, error.message end + test "preload with invalid argument" do + exception = assert_raises(ArgumentError) do + Author.preload(10).to_a + end + assert_equal('10 was not recognized for preload', exception.message) + end + + test "preloading readonly association" do # has-one firm = Firm.where(id: "1").preload(:readonly_account).first! 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 abbe37ab38..d160c30375 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 @@ -3,6 +3,7 @@ require 'models/developer' require 'models/computer' require 'models/project' require 'models/company' +require 'models/course' require 'models/customer' require 'models/order' require 'models/categorization' @@ -14,6 +15,7 @@ require 'models/tagging' require 'models/parrot' require 'models/person' require 'models/pirate' +require 'models/professor' require 'models/treasure' require 'models/price_estimate' require 'models/club' @@ -794,9 +796,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_association_proxy_transaction_method_starts_transaction_in_association_class - Post.expects(:transaction) - Category.first.posts.transaction do - # nothing + assert_called(Post, :transaction) do + Category.first.posts.transaction do + # nothing + end end end @@ -923,4 +926,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_deprecated { developer.projects(true) } end + + def test_alternate_database + professor = Professor.create(name: "Plum") + course = Course.create(name: "Forensics") + assert_equal 0, professor.courses.count + assert_nothing_raised do + professor.courses << course + end + assert_equal 1, professor.courses.count + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 2cbee3cd21..7de5bc01ae 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -31,6 +31,8 @@ require 'models/student' require 'models/pirate' require 'models/ship' require 'models/ship_part' +require 'models/treasure' +require 'models/parrot' require 'models/tyre' require 'models/subscriber' require 'models/subscription' @@ -932,6 +934,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, new_firm.clients_of_firm.size end + def test_has_many_without_counter_cache_option + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless', treasures_count: 10) + + assert_not Ship.reflect_on_association(:treasures).has_cached_counter? + + # Count should come from sql count() of treasures rather than treasures_count attribute + assert_equal ship.treasures.size, 0 + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.create(name: 'Gold') + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.destroy_all + end + end + def test_deleting_updates_counter_cache topic = Topic.order("id ASC").first assert_equal topic.replies.to_a.size, topic.replies_count 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 ce2557339e..cf730e4fe7 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -744,8 +744,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_has_many_through_with_conditions_should_not_preload Tagging.create!(:taggable_type => 'Post', :taggable_id => posts(:welcome).id, :tag => tags(:misc)) - ActiveRecord::Associations::Preloader.expects(:new).never - posts(:welcome).misc_tag_ids + assert_not_called(ActiveRecord::Associations::Preloader, :new) do + posts(:welcome).misc_tag_ids + end end def test_get_ids_for_loaded_associations @@ -765,9 +766,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_association_proxy_transaction_method_starts_transaction_in_association_class - Tag.expects(:transaction) - Post.first.tags.transaction do - # nothing + assert_called(Tag, :transaction) do + Post.first.tags.transaction do + # nothing + end end end @@ -1171,4 +1173,32 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_deprecated { post.people(true) } end + + def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes + member = Member.create! + club = Club.create! + TenantMembership.create!( + member: member, + club: club + ) + + TenantMembership.current_member = member + + tenant_clubs = member.tenant_clubs + assert_equal [club], tenant_clubs + + TenantMembership.current_member = nil + + other_member = Member.create! + other_club = Club.create! + TenantMembership.create!( + member: other_member, + club: other_club + ) + + tenant_clubs = other_member.tenant_clubs + assert_equal [other_club], tenant_clubs + ensure + TenantMembership.current_member = nil + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 5a8afaf4d2..d46e7ad235 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -107,6 +107,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nil Account.find(old_account_id).firm_id end + def test_nullification_on_destroyed_association + developer = Developer.create!(name: "Someone") + ship = Ship.create!(name: "Planet Caravan", developer: developer) + ship.destroy + assert !ship.persisted? + assert !developer.persisted? + end + def test_natural_assignment_to_nil_after_destroy firm = companies(:rails_core) old_account_id = firm.account.id diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 2c8feb9152..b2b46812b9 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -16,6 +16,10 @@ require 'models/owner' require 'models/post' require 'models/comment' require 'models/categorization' +require 'models/customer' +require 'models/carrier' +require 'models/shop_account' +require 'models/customer_carrier' class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, @@ -347,16 +351,33 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end end - def test_association_force_reload_with_only_true_is_deprecated - member = Member.find(1) + def test_has_one_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes + customer = Customer.create! + carrier = Carrier.create! + customer_carrier = CustomerCarrier.create!( + customer: customer, + carrier: carrier, + ) + account = ShopAccount.create!(customer_carrier: customer_carrier) - assert_deprecated { member.club(true) } - end + CustomerCarrier.current_customer = customer - def test_has_one_through_associations_are_mutable_unless_through_belongs_to - member_detail = MemberDetail.new(member: @member) - assert_raise(ActiveRecord::HasOneThroughCantAssociateThroughHasOneOrManyReflection) do - member_detail.membership = Membership.new - end + account_carrier = account.carrier + assert_equal carrier, account_carrier + + CustomerCarrier.current_customer = nil + + other_carrier = Carrier.create! + other_customer = Customer.create! + other_customer_carrier = CustomerCarrier.create!( + customer: other_customer, + carrier: other_carrier, + ) + other_account = ShopAccount.create!(customer_carrier: other_customer_carrier) + + account_carrier = other_account.carrier + assert_equal other_carrier, account_carrier + ensure + CustomerCarrier.current_customer = nil end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 08f790e21b..01a058918a 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -1,7 +1,6 @@ require "cases/helper" require 'models/computer' require 'models/developer' -require 'models/computer' require 'models/project' require 'models/company' require 'models/categorization' @@ -13,7 +12,6 @@ require 'models/tag' require 'models/tagging' require 'models/person' require 'models/reader' -require 'models/parrot' require 'models/ship_part' require 'models/ship' require 'models/liquid' diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index ea2b94cbf4..e2608e3670 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,7 +1,6 @@ require "cases/helper" require 'models/minimalistic' require 'models/developer' -require 'models/computer' require 'models/auto_id' require 'models/boolean' require 'models/computer' @@ -67,8 +66,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_caching_nil_primary_key klass = Class.new(Minimalistic) - klass.expects(:reset_primary_key).returns(nil).once - 2.times { klass.primary_key } + assert_called(klass, :reset_primary_key, returns: nil) do + 2.times { klass.primary_key } + end end def test_attribute_keys_on_new_instance diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index aa419c7a67..0ec368f51d 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -1,5 +1,4 @@ require 'cases/helper' -require 'minitest/mock' module ActiveRecord class AttributeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index bd74821d87..264b275181 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -152,7 +152,7 @@ module ActiveRecord Type::String.new(limit: 50)) int_array = ConnectionAdapters::PostgreSQL::OID::Array.new( Type::Integer.new) - refute_equal string_array, int_array + assert_not_equal string_array, int_array assert_equal string_array, klass.type_for_attribute("my_array") assert_equal int_array, klass.type_for_attribute("my_int_array") end @@ -167,7 +167,7 @@ module ActiveRecord Type::String.new(limit: 50)) int_range = ConnectionAdapters::PostgreSQL::OID::Range.new( Type::Integer.new) - refute_equal string_range, int_range + assert_not_equal string_range, int_range assert_equal string_range, klass.type_for_attribute("my_range") assert_equal int_range, klass.type_for_attribute("my_int_range") end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 382adbbdc7..0edb8a8901 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -946,6 +946,34 @@ class BasicsTest < ActiveRecord::TestCase assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance end + def test_numeric_fields_with_scale + m = NumericData.new( + :bank_balance => 1586.43122334, + :big_bank_balance => BigDecimal("234000567.952344"), + :world_population => 6000000000, + :my_house_population => 3 + ) + assert m.save + + m1 = NumericData.find(m.id) + assert_not_nil m1 + + # As with migration_test.rb, we should make world_population >= 2**62 + # to cover 64-bit platforms and test it is a Bignum, but the main thing + # is that it's an Integer. + assert_kind_of Integer, m1.world_population + assert_equal 6000000000, m1.world_population + + assert_kind_of Fixnum, m1.my_house_population + assert_equal 3, m1.my_house_population + + assert_kind_of BigDecimal, m1.bank_balance + assert_equal BigDecimal("1586.43"), m1.bank_balance + + assert_kind_of BigDecimal, m1.big_bank_balance + assert_equal BigDecimal("234000567.95"), m1.big_bank_balance + end + def test_auto_id auto = AutoId.new auto.save diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 48dc8135ab..9cb70ee239 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -69,13 +69,15 @@ class EachTest < ActiveRecord::TestCase end def test_warn_if_limit_scope_is_set - ActiveRecord::Base.logger.expects(:warn) - Post.limit(1).find_each { |post| post } + assert_called(ActiveRecord::Base.logger, :warn) do + Post.limit(1).find_each { |post| post } + end end def test_warn_if_order_scope_is_set - ActiveRecord::Base.logger.expects(:warn) - Post.order("title").find_each { |post| post } + assert_called(ActiveRecord::Base.logger, :warn) do + Post.order("title").find_each { |post| post } + end end def test_logger_not_required @@ -137,14 +139,15 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified not_a_post = "not a post" - not_a_post.stubs(:id).raises(StandardError, "not_a_post had #id called on it") - - assert_nothing_raised do - Post.find_in_batches(:batch_size => 1) do |batch| - assert_kind_of Array, batch - assert_kind_of Post, batch.first + def not_a_post.id; end + not_a_post.stub(:id, ->{ raise StandardError.new("not_a_post had #id called on it") }) do + assert_nothing_raised do + Post.find_in_batches(:batch_size => 1) do |batch| + assert_kind_of Array, batch + assert_kind_of Post, batch.first - batch.map! { not_a_post } + batch.map! { not_a_post } + end end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index ba23049a92..7c930de97b 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -40,43 +40,43 @@ class EnumTest < ActiveRecord::TestCase published, written = Book.statuses[:published], Book.statuses[:written] assert_equal @book, Book.where(status: published).first - refute_equal @book, Book.where(status: written).first + assert_not_equal @book, Book.where(status: written).first assert_equal @book, Book.where(status: [published]).first - refute_equal @book, Book.where(status: [written]).first - refute_equal @book, Book.where("status <> ?", published).first + assert_not_equal @book, Book.where(status: [written]).first + assert_not_equal @book, Book.where("status <> ?", published).first assert_equal @book, Book.where("status <> ?", written).first end test "find via where with symbols" do assert_equal @book, Book.where(status: :published).first - refute_equal @book, Book.where(status: :written).first + assert_not_equal @book, Book.where(status: :written).first assert_equal @book, Book.where(status: [:published]).first - refute_equal @book, Book.where(status: [:written]).first - refute_equal @book, Book.where.not(status: :published).first + assert_not_equal @book, Book.where(status: [:written]).first + assert_not_equal @book, Book.where.not(status: :published).first assert_equal @book, Book.where.not(status: :written).first end test "find via where with strings" do assert_equal @book, Book.where(status: "published").first - refute_equal @book, Book.where(status: "written").first + assert_not_equal @book, Book.where(status: "written").first assert_equal @book, Book.where(status: ["published"]).first - refute_equal @book, Book.where(status: ["written"]).first - refute_equal @book, Book.where.not(status: "published").first + assert_not_equal @book, Book.where(status: ["written"]).first + assert_not_equal @book, Book.where.not(status: "published").first assert_equal @book, Book.where.not(status: "written").first end test "build from scope" do assert Book.written.build.written? - refute Book.written.build.proposed? + assert_not Book.written.build.proposed? end test "build from where" do assert Book.where(status: Book.statuses[:written]).build.written? - refute Book.where(status: Book.statuses[:written]).build.proposed? + assert_not Book.where(status: Book.statuses[:written]).build.proposed? assert Book.where(status: :written).build.written? - refute Book.where(status: :written).build.proposed? + assert_not Book.where(status: :written).build.proposed? assert Book.where(status: "written").build.written? - refute Book.where(status: "written").build.proposed? + assert_not Book.where(status: "written").build.proposed? end test "update by declaration" do diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb new file mode 100644 index 0000000000..0711a372f2 --- /dev/null +++ b/activerecord/test/cases/errors_test.rb @@ -0,0 +1,16 @@ +require_relative "../cases/helper" + +class ErrorsTest < ActiveRecord::TestCase + def test_can_be_instantiated_with_no_args + base = ActiveRecord::ActiveRecordError + error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base } + + error_klasses.each do |error_klass| + begin + error_klass.new.inspect + rescue ArgumentError + raise "Instance of #{error_klass} can't be initialized with no arguments" + end + end + end +end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 03a187ae92..64759160dc 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -11,7 +11,6 @@ require 'models/company' require 'models/computer' require 'models/course' require 'models/developer' -require 'models/computer' require 'models/joke' require 'models/matey' require 'models/parrot' @@ -217,6 +216,13 @@ class FixturesTest < ActiveRecord::TestCase end end + def test_yaml_file_with_invalid_column + e = assert_raise(ActiveRecord::Fixture::FixtureError) do + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots") + end + assert_equal(%(table "parrots" has no column named "arrr".), e.message) + end + def test_omap_fixtures assert_nothing_raised do fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered") @@ -252,18 +258,19 @@ class FixturesTest < ActiveRecord::TestCase def test_fixtures_are_set_up_with_database_env_variable db_url_tmp = ENV['DATABASE_URL'] ENV['DATABASE_URL'] = "sqlite3::memory:" - ActiveRecord::Base.stubs(:configurations).returns({}) - test_case = Class.new(ActiveRecord::TestCase) do - fixtures :accounts + ActiveRecord::Base.stub(:configurations, {}) do + test_case = Class.new(ActiveRecord::TestCase) do + fixtures :accounts - def test_fixtures - assert accounts(:signals37) + def test_fixtures + assert accounts(:signals37) + end end - end - result = test_case.new(:test_fixtures).run + result = test_case.new(:test_fixtures).run - assert result.passed?, "Expected #{result.name} to pass:\n#{result}" + assert result.passed?, "Expected #{result.name} to pass:\n#{result}" + end ensure ENV['DATABASE_URL'] = db_url_tmp end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index d9d0f929db..b61a5126e0 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -3,6 +3,7 @@ require File.expand_path('../../../../load_paths', __FILE__) require 'config' require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' require 'stringio' require 'active_record' @@ -141,6 +142,7 @@ require "cases/validations_repair_helper" class ActiveSupport::TestCase include ActiveRecord::TestFixtures include ActiveRecord::ValidationsRepairHelper + include ActiveSupport::Testing::MethodCallAssertions self.fixture_path = FIXTURES_ROOT self.use_instantiated_fixtures = false diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 99230aa3d5..84b0ff8fcb 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -1,5 +1,8 @@ require "cases/helper" +class Horse < ActiveRecord::Base +end + module ActiveRecord class InvertibleMigrationTest < ActiveRecord::TestCase class SilentMigration < ActiveRecord::Migration @@ -76,6 +79,32 @@ module ActiveRecord end end + class ChangeColumnDefault1 < SilentMigration + def change + create_table("horses") do |t| + t.column :name, :string, default: "Sekitoba" + end + end + end + + class ChangeColumnDefault2 < SilentMigration + def change + change_column_default :horses, :name, from: "Sekitoba", to: "Diomed" + end + end + + class DisableExtension1 < SilentMigration + def change + enable_extension "hstore" + end + end + + class DisableExtension2 < SilentMigration + def change + disable_extension "hstore" + end + end + class LegacyMigration < ActiveRecord::Migration def self.up create_table("horses") do |t| @@ -223,6 +252,42 @@ module ActiveRecord assert !revert.connection.table_exists?("horses") end + def test_migrate_revert_change_column_default + migration1 = ChangeColumnDefault1.new + migration1.migrate(:up) + assert_equal "Sekitoba", Horse.new.name + + migration2 = ChangeColumnDefault2.new + migration2.migrate(:up) + Horse.reset_column_information + assert_equal "Diomed", Horse.new.name + + migration2.migrate(:down) + Horse.reset_column_information + assert_equal "Sekitoba", Horse.new.name + end + + if current_adapter?(:PostgreSQLAdapter) + def test_migrate_enable_and_disable_extension + migration1 = InvertibleMigration.new + migration2 = DisableExtension1.new + migration3 = DisableExtension2.new + + migration1.migrate(:up) + migration2.migrate(:up) + assert_equal true, Horse.connection.extension_enabled?('hstore') + + migration3.migrate(:up) + assert_equal false, Horse.connection.extension_enabled?('hstore') + + migration3.migrate(:down) + assert_equal true, Horse.connection.extension_enabled?('hstore') + + migration2.migrate(:down) + assert_equal false, Horse.connection.extension_enabled?('hstore') + end + end + def test_revert_order block = Proc.new{|t| t.string :name } recorder = ActiveRecord::Migration::CommandRecorder.new(ActiveRecord::Base.connection) diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 4192d12ff4..3846ba8e7f 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -7,6 +7,20 @@ require "active_support/log_subscriber/test_helper" class LogSubscriberTest < ActiveRecord::TestCase include ActiveSupport::LogSubscriber::TestHelper include ActiveSupport::Logger::Severity + REGEXP_CLEAR = Regexp.escape(ActiveRecord::LogSubscriber::CLEAR) + REGEXP_BOLD = Regexp.escape(ActiveRecord::LogSubscriber::BOLD) + REGEXP_MAGENTA = Regexp.escape(ActiveRecord::LogSubscriber::MAGENTA) + REGEXP_CYAN = Regexp.escape(ActiveRecord::LogSubscriber::CYAN) + SQL_COLORINGS = { + SELECT: Regexp.escape(ActiveRecord::LogSubscriber::BLUE), + INSERT: Regexp.escape(ActiveRecord::LogSubscriber::GREEN), + UPDATE: Regexp.escape(ActiveRecord::LogSubscriber::YELLOW), + DELETE: Regexp.escape(ActiveRecord::LogSubscriber::RED), + LOCK: Regexp.escape(ActiveRecord::LogSubscriber::WHITE), + ROLLBACK: Regexp.escape(ActiveRecord::LogSubscriber::RED), + TRANSACTION: REGEXP_CYAN, + OTHER: REGEXP_MAGENTA + } class TestDebugLogSubscriber < ActiveRecord::LogSubscriber attr_reader :debugs @@ -71,6 +85,90 @@ class LogSubscriberTest < ActiveRecord::TestCase assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last) end + def test_basic_query_logging_coloration + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, color_regex| + logger.sql(event.new(0, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_basic_payload_name_logging_coloration_generic_sql + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, _| + logger.sql(event.new(0, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "SQL"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_basic_payload_name_logging_coloration_named_sql + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, _| + logger.sql(event.new(0, {sql: verb.to_s, name: "Model Load"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "Model Exists"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "ANY SPECIFIC NAME"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_nested_select + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| + logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_multi_line_nested_select + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| + sql = <<-EOS + #{verb} + WHERE ID IN ( + SELECT ID FROM THINGS + ) + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_lock + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + sql = <<-EOS + SELECT * FROM + (SELECT * FROM mytable FOR UPDATE) ss + WHERE col1 = 5; + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + + sql = <<-EOS + LOCK TABLE films IN SHARE MODE; + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + end + def test_exists_query_logging Developer.exists? 1 wait diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 2ffe7a1b0d..2f9c50141f 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -1,5 +1,4 @@ require "cases/migration/helper" -require "minitest/mock" module ActiveRecord class Migration diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index 7afac83bd2..4f5589f32a 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -1,5 +1,4 @@ require 'cases/helper' -require "minitest/mock" module ActiveRecord class Migration diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index b2f209fe97..10f1c7216f 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -75,15 +75,13 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.up(migrations_path) assert_equal 3, ActiveRecord::Migrator.current_version - assert_equal 3, ActiveRecord::Migrator.last_version assert_equal false, ActiveRecord::Migrator.needs_migration? ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") assert_equal 0, ActiveRecord::Migrator.current_version - assert_equal 3, ActiveRecord::Migrator.last_version assert_equal true, ActiveRecord::Migrator.needs_migration? - ActiveRecord::SchemaMigration.create!(:version => ActiveRecord::Migrator.last_version) + ActiveRecord::SchemaMigration.create!(version: 3) assert_equal true, ActiveRecord::Migrator.needs_migration? ensure ActiveRecord::Migrator.migrations_paths = old_path @@ -115,7 +113,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_migration_version - ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) + assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) } end def test_create_table_with_force_true_does_not_drop_nonexisting_table @@ -132,13 +130,9 @@ class MigrationTest < ActiveRecord::TestCase Person.connection.drop_table :testings2, if_exists: true end - def connection - ActiveRecord::Base.connection - end - def test_migration_instance_has_connection migration = Class.new(ActiveRecord::Migration).new - assert_equal connection, migration.connection + assert_equal ActiveRecord::Base.connection, migration.connection end def test_method_missing_delegates_to_connection diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 42e7507631..7f14082a9a 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -17,6 +17,7 @@ require 'models/minivan' require 'models/owner' require 'models/person' require 'models/pet' +require 'models/ship' require 'models/toy' require 'rexml/document' @@ -125,7 +126,7 @@ class PersistenceTest < ActiveRecord::TestCase assert ! topics_by_mary.empty? assert_difference('Topic.count', -topics_by_mary.size) do - destroyed = Topic.destroy_all(conditions).sort_by(&:id) + destroyed = Topic.where(conditions).destroy_all.sort_by(&:id) assert_equal topics_by_mary, destroyed assert destroyed.all?(&:frozen?), "destroyed topics should be frozen" end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 2f0b5df286..d84653e4c9 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -262,61 +262,66 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase end def test_find - Task.connection.expects(:clear_query_cache).times(1) + assert_called(Task.connection, :clear_query_cache) do + assert !Task.connection.query_cache_enabled + Task.cache do + assert Task.connection.query_cache_enabled + Task.find(1) - assert !Task.connection.query_cache_enabled - Task.cache do - assert Task.connection.query_cache_enabled - Task.find(1) + Task.uncached do + assert !Task.connection.query_cache_enabled + Task.find(1) + end - Task.uncached do - assert !Task.connection.query_cache_enabled - Task.find(1) + assert Task.connection.query_cache_enabled end - - assert Task.connection.query_cache_enabled + assert !Task.connection.query_cache_enabled end - assert !Task.connection.query_cache_enabled end def test_update - Task.connection.expects(:clear_query_cache).times(2) - Task.cache do - task = Task.find(1) - task.starting = Time.now.utc - task.save! + assert_called(Task.connection, :clear_query_cache, times: 2) do + Task.cache do + task = Task.find(1) + task.starting = Time.now.utc + task.save! + end end end def test_destroy - Task.connection.expects(:clear_query_cache).times(2) - Task.cache do - Task.find(1).destroy + assert_called(Task.connection, :clear_query_cache, times: 2) do + Task.cache do + Task.find(1).destroy + end end end def test_insert - ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - Task.cache do - Task.create! + assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do + Task.cache do + Task.create! + end end end def test_cache_is_expired_by_habtm_update - ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - ActiveRecord::Base.cache do - c = Category.first - p = Post.first - p.categories << c + assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do + ActiveRecord::Base.cache do + c = Category.first + p = Post.first + p.categories << c + end end end def test_cache_is_expired_by_habtm_delete - ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - ActiveRecord::Base.cache do - p = Post.find(1) - assert p.categories.any? - p.categories.delete_all + assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do + ActiveRecord::Base.cache do + p = Post.find(1) + assert p.categories.any? + p.categories.delete_all + end end end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 1c919f0b57..5f6eb41240 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -7,6 +7,7 @@ require 'models/computer' require 'models/project' require 'models/reader' require 'models/person' +require 'models/ship' class ReadOnlyTest < ActiveRecord::TestCase fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 7b47c80331..9c04a41e69 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -393,12 +393,14 @@ class ReflectionTest < ActiveRecord::TestCase product = Struct.new(:table_name, :pluralize_table_names).new('products', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) - reflection.stubs(:klass).returns(category) - assert_equal 'categories_products', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'categories_products', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) - reflection.stubs(:klass).returns(product) - assert_equal 'categories_products', reflection.join_table + reflection.stub(:klass, product) do + assert_equal 'categories_products', reflection.join_table + end end def test_join_table_with_common_prefix @@ -406,12 +408,14 @@ class ReflectionTest < ActiveRecord::TestCase product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) - reflection.stubs(:klass).returns(category) - assert_equal 'catalog_categories_products', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'catalog_categories_products', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) - reflection.stubs(:klass).returns(product) - assert_equal 'catalog_categories_products', reflection.join_table + reflection.stub(:klass, product) do + assert_equal 'catalog_categories_products', reflection.join_table + end end def test_join_table_with_different_prefix @@ -419,12 +423,14 @@ class ReflectionTest < ActiveRecord::TestCase page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page) - reflection.stubs(:klass).returns(category) - assert_equal 'catalog_categories_content_pages', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'catalog_categories_content_pages', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category) - reflection.stubs(:klass).returns(page) - assert_equal 'catalog_categories_content_pages', reflection.join_table + reflection.stub(:klass, page) do + assert_equal 'catalog_categories_content_pages', reflection.join_table + end end def test_join_table_can_be_overridden @@ -432,12 +438,14 @@ class ReflectionTest < ActiveRecord::TestCase product = Struct.new(:table_name, :pluralize_table_names).new('products', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product) - reflection.stubs(:klass).returns(category) - assert_equal 'product_categories', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'product_categories', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category) - reflection.stubs(:klass).returns(product) - assert_equal 'product_categories', reflection.join_table + reflection.stub(:klass, product) do + assert_equal 'product_categories', reflection.join_table + end end def test_includes_accepts_symbols diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 5f48c2b40f..8256762f96 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -931,6 +931,12 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end + def test_destroy_all_with_conditions_is_deprecated + assert_deprecated do + assert_difference('Author.count', -1) { Author.destroy_all(name: 'David') } + end + end + def test_delete_all davids = Author.where(:name => 'David') @@ -938,6 +944,12 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.loaded? end + def test_delete_all_with_conditions_is_deprecated + assert_deprecated do + assert_difference('Author.count', -1) { Author.delete_all(name: 'David') } + end + end + def test_delete_all_loaded davids = Author.where(:name => 'David') diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 0dbc60940e..86316ab476 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/comment' require 'models/developer' require 'models/computer' +require 'models/vehicle' class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments @@ -453,4 +454,9 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, scope.where_clause.ast.children.length assert_equal Developer.where(name: "David"), scope end + + def test_with_abstract_class_where_clause_should_not_be_duplicated + scope = Bus.all + assert_equal scope.where_clause.ast.children.length, 1 + end end diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index e4cc533517..7a8eaeccb7 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -188,8 +188,9 @@ class NamedScopingTest < ActiveRecord::TestCase def test_any_should_call_proxy_found_if_using_a_block topics = Topic.base assert_queries(1) do - topics.expects(:empty?).never - topics.any? { true } + assert_not_called(topics, :empty?) do + topics.any? { true } + end end end @@ -217,8 +218,9 @@ class NamedScopingTest < ActiveRecord::TestCase def test_many_should_call_proxy_found_if_using_a_block topics = Topic.base assert_queries(1) do - topics.expects(:size).never - topics.many? { true } + assert_not_called(topics, :size) do + topics.many? { true } + end end end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 35b13ea247..14b80f4df4 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -8,7 +8,7 @@ require 'models/post' class SerializationTest < ActiveRecord::TestCase fixtures :books - FORMATS = [ :xml, :json ] + FORMATS = [ :json ] def setup @contact_attributes = { diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 38164b2228..c8f4179313 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -277,12 +277,14 @@ module ActiveRecord def test_migrate_receives_correct_env_vars verbose, version = ENV['VERBOSE'], ENV['VERSION'] + ActiveRecord::Tasks::DatabaseTasks.migrations_paths = 'custom/path' ENV['VERBOSE'] = 'false' ENV['VERSION'] = '4' - ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4) + ActiveRecord::Migrator.expects(:migrate).with('custom/path', 4) ActiveRecord::Tasks::DatabaseTasks.migrate ensure + ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil ENV['VERBOSE'], ENV['VERSION'] = verbose, version end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 084302cde5..184ff7fc63 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -204,7 +204,7 @@ module ActiveRecord end def test_structure_dump - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-i', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -212,7 +212,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-i', '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -220,7 +220,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path_and_dump_schemas_all @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-i', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) with_dump_schemas(:all) do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -228,7 +228,7 @@ module ActiveRecord end def test_structure_dump_with_dump_schemas_string - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-i', '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', "my-app-db").returns(true) with_dump_schemas('foo,bar') do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -261,14 +261,14 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("psql -X -q -f #{filename} my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" - Kernel.expects(:system).with("psql -X -q -f awesome\\ file.sql my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 49ada22529..b3c42c8e42 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -11,7 +11,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_laster_raise_if_non_persisted invoice = Invoice.new Invoice.transaction do - refute invoice.persisted? + assert_not invoice.persisted? assert_raises(ActiveRecord::ActiveRecordError) do invoice.touch_later end @@ -21,7 +21,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_later_dont_set_dirty_attributes invoice = Invoice.create! invoice.touch_later - refute invoice.changed? + assert_not invoice.changed? end def test_touch_later_update_the_attributes diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb index fe49d0e79a..01ee36b892 100644 --- a/activerecord/test/cases/type/decimal_test.rb +++ b/activerecord/test/cases/type/decimal_test.rb @@ -25,6 +25,11 @@ module ActiveRecord assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3)) end + def test_type_cast_decimal_from_rational_with_precision_and_scale + type = Decimal.new(precision: 4, scale: 2) + assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3)) + end + def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36 type = Decimal.new assert_equal BigDecimal("0.333333333333333333E0"), type.cast(Rational(1, 3)) diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 268d7914b5..981239c4d6 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -53,8 +53,9 @@ class I18nValidationTest < ActiveRecord::TestCase test "validates_uniqueness_of on generated message #{name}" do Topic.validates_uniqueness_of :title, validation_options @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, generate_message_options.merge(:value => 'unique!')) - @topic.valid? + assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(:value => 'unique!')]) do + @topic.valid? + end end end @@ -63,8 +64,9 @@ class I18nValidationTest < ActiveRecord::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_associated on generated message #{name}" do Topic.validates_associated :replies, validation_options - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies)) - replied_topic.save + assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies)]) do + replied_topic.save + end end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index ceca2c8366..7502a55391 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -4,6 +4,7 @@ require 'models/reply' require 'models/warehouse_thing' require 'models/guid' require 'models/event' +require 'models/dashboard' class Wizard < ActiveRecord::Base self.abstract_class = true @@ -446,4 +447,26 @@ class UniquenessValidationTest < ActiveRecord::TestCase key2.key_number = 10 assert_not key2.valid? end + + def test_validate_uniqueness_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = "dashboards" + + validates_uniqueness_of :dashboard_id + + def self.name; "Dashboard" end + end + + abc = klass.create!(dashboard_id: "abc") + assert klass.new(dashboard_id: "xyz").valid? + assert_not klass.new(dashboard_id: "abc").valid? + + abc.dashboard_id = "def" + + e = assert_raises ActiveRecord::UnknownPrimaryKey do + abc.save! + end + assert_match(/\AUnknown primary key for table dashboards in model/, e.message) + assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message) + end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index f4f316f393..a429d06aad 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -52,6 +52,13 @@ class ValidationsTest < ActiveRecord::TestCase assert r.valid?(:special_case) end + def test_invalid_using_multiple_contexts + r = WrongReply.new(:title => 'Wrong Create') + assert r.invalid?([:special_case, :create]) + assert_equal "Invalid", r.errors[:author_name].join + assert_equal "is Wrong Create", r.errors[:title].join + end + def test_validate r = WrongReply.new diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index f9dca1e196..1eb1430065 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -110,4 +110,70 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase assert_nil Paperback.primary_key end end + +# sqlite dose not support CREATE, INSERT, and DELETE for VIEW +if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) +class UpdateableViewTest < ActiveRecord::TestCase + self.use_transactional_tests = false + fixtures :books + + class PrintedBook < ActiveRecord::Base + self.primary_key = "id" + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute <<-SQL + CREATE VIEW printed_books + AS SELECT id, name, status, format FROM books WHERE format = 'paperback' + SQL + end + + teardown do + @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books" + end + + def test_update_record + book = PrintedBook.first + book.name = "AWDwR" + book.save! + book.reload + assert_equal "AWDwR", book.name + end + + def test_insert_record + PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback" + + new_book = PrintedBook.last + assert_equal "Rails in Action", new_book.name + end + + def test_update_record_to_fail_view_conditions + book = PrintedBook.first + book.format = "ebook" + book.save! + + assert_raises ActiveRecord::RecordNotFound do + book.reload + end + end +end +end # end fo `if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)` +end # end fo `if ActiveRecord::Base.connection.supports_views?` + +if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && + ActiveRecord::Base.connection.supports_materialized_views? +class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase + include ViewBehavior + + private + def create_view(name, query) + @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}" + end + + def drop_view(name) + @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name + + end +end end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb deleted file mode 100644 index b30b50f597..0000000000 --- a/activerecord/test/cases/xml_serialization_test.rb +++ /dev/null @@ -1,447 +0,0 @@ -require "cases/helper" -require "rexml/document" -require 'models/contact' -require 'models/post' -require 'models/author' -require 'models/comment' -require 'models/company_in_module' -require 'models/toy' -require 'models/topic' -require 'models/reply' -require 'models/company' - -class XmlSerializationTest < ActiveRecord::TestCase - def test_should_serialize_default_root - @xml = Contact.new.to_xml - assert_match %r{^<contact>}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_default_root_with_namespace - @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml\.rubyonrails\.org/contact">}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_custom_root - @xml = Contact.new.to_xml :root => 'xml_contact' - assert_match %r{^<xml-contact>}, @xml - assert_match %r{</xml-contact>$}, @xml - end - - def test_should_allow_undasherized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false - assert_match %r{^<xml_contact>}, @xml - assert_match %r{</xml_contact>$}, @xml - assert_match %r{<created_at}, @xml - end - - def test_should_allow_camelized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true - assert_match %r{^<XmlContact>}, @xml - assert_match %r{</XmlContact>$}, @xml - assert_match %r{<CreatedAt}, @xml - end - - def test_should_allow_skipped_types - @xml = Contact.new(:age => 25).to_xml :skip_types => true - assert %r{<age>25</age>}.match(@xml) - end - - def test_should_include_yielded_additions - @xml = Contact.new.to_xml do |xml| - xml.creator "David" - end - assert_match %r{<creator>David</creator>}, @xml - end - - def test_to_xml_with_block - value = "Rockin' the block" - xml = Contact.new.to_xml(:skip_instruct => true) do |_xml| - _xml.tag! "arbitrary-element", value - end - assert_equal "<contact>", xml.first(9) - assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>)) - end - - def test_should_skip_instruct_for_included_records - @contact = Contact.new - @contact.alternative = Contact.new(:name => 'Copa Cabana') - @xml = @contact.to_xml(:include => [ :alternative ]) - assert_equal @xml.index('<?xml '), 0 - assert_nil @xml.index('<?xml ', 1) - end -end - -class DefaultXmlSerializationTest < ActiveRecord::TestCase - def setup - @contact = Contact.new( - :name => 'aaron stack', - :age => 25, - :avatar => 'binarydata', - :created_at => Time.utc(2006, 8, 1), - :awesome => false, - :preferences => { :gem => 'ruby' } - ) - end - - def test_should_serialize_string - assert_match %r{<name>aaron stack</name>}, @contact.to_xml - end - - def test_should_serialize_integer - assert_match %r{<age type="integer">25</age>}, @contact.to_xml - end - - def test_should_serialize_binary - xml = @contact.to_xml - assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml - assert_match %r{<avatar(.*)(type="binary")}, xml - assert_match %r{<avatar(.*)(encoding="base64")}, xml - end - - def test_should_serialize_datetime - assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml - end - - def test_should_serialize_boolean - assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml - end - - def test_should_serialize_hash - assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml - end - - def test_uses_serializable_hash_with_only_option - def @contact.serializable_hash(options=nil) - super(only: %w(name)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{awesome}, xml - end - - def test_uses_serializable_hash_with_except_option - def @contact.serializable_hash(options=nil) - super(except: %w(age)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml - assert_no_match %r{age}, xml - end - - def test_does_not_include_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end - - def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - def @contact.serializable_hash(options={}) - super({ except: %w(age) }.merge!(options)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end -end - -class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase - def test_should_serialize_datetime_with_timezone - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end - - def test_should_serialize_datetime_with_timezone_reloaded - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end -end - -class NilXmlSerializationTest < ActiveRecord::TestCase - def setup - @xml = Contact.new.to_xml(:root => 'xml_contact') - end - - def test_should_serialize_string - assert_match %r{<name nil="true"/>}, @xml - end - - def test_should_serialize_integer - assert %r{<age (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="integer"}, attributes - end - - def test_should_serialize_binary - assert %r{<avatar (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="binary"}, attributes - assert_match %r{encoding="base64"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_datetime - assert %r{<created-at (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="dateTime"}, attributes - end - - def test_should_serialize_boolean - assert %r{<awesome (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="boolean"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_yaml - assert_match %r{<preferences nil=\"true\"/>}, @xml - end -end - -class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase - fixtures :topics, :companies, :accounts, :authors, :posts, :projects - - def test_to_xml - xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) - bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema - written_on_in_current_timezone = topics(:first).written_on.xmlschema - - assert_equal "topic", xml.root.name - assert_equal "The First Topic" , xml.elements["//title"].text - assert_equal "David" , xml.elements["//author-name"].text - assert_match "Have a nice day", xml.elements["//content"].text - - assert_equal "1", xml.elements["//id"].text - assert_equal "integer" , xml.elements["//id"].attributes['type'] - - assert_equal "1", xml.elements["//replies-count"].text - assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] - - assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] - - assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text - - assert_equal nil, xml.elements["//parent-id"].text - assert_equal "integer", xml.elements["//parent-id"].attributes['type'] - assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_equal "2004-04-15", xml.elements["//last-read"].text - assert_equal "date" , xml.elements["//last-read"].attributes['type'] - - # Oracle and DB2 don't have true boolean or time-only fields - unless current_adapter?(:OracleAdapter, :DB2Adapter) - assert_equal "false", xml.elements["//approved"].text - assert_equal "boolean" , xml.elements["//approved"].attributes['type'] - - assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] - end - end - - def test_except_option - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count]) - assert_equal "<topic>", xml.first(7) - assert !xml.include?(%(<title>The First Topic</title>)) - assert xml.include?(%(<author-name>David</author-name>)) - - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count]) - assert !xml.include?(%(<title>The First Topic</title>)) - assert !xml.include?(%(<author-name>David</author-name>)) - end - - # to_xml used to mess with the hash the user provided which - # caused the builder to be reused. This meant the document kept - # getting appended to. - - def test_modules - projects = MyApplication::Business::Project.all - xml = projects.to_xml - root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize - assert_match "<#{root} type=\"array\">", xml - assert_match "</#{root}>", xml - end - - def test_passing_hash_shouldnt_reuse_builder - options = {:include=>:posts} - david = authors(:david) - first_xml_size = david.to_xml(options).size - second_xml_size = david.to_xml(options).size - assert_equal first_xml_size, second_xml_size - end - - def test_include_uses_association_name - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0 - assert_match %r{<hello-posts type="array">}, xml - assert_match %r{<hello-post type="Post">}, xml - assert_match %r{<hello-post type="StiPost">}, xml - end - - def test_included_associations_should_skip_types - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true - assert_match %r{<hello-posts>}, xml - assert_match %r{<hello-post>}, xml - assert_match %r{<hello-post>}, xml - end - - def test_including_has_many_association - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count) - assert_equal "<topic>", xml.first(7) - assert xml.include?(%(<replies type="array"><reply>)) - assert xml.include?(%(<title>The Second Topic of the day</title>)) - end - - def test_including_belongs_to_association - xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert !xml.include?("<firm>") - - xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?("<firm>") - end - - def test_including_multiple_associations - xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ]) - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<account>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_including_association_with_options - xml = companies(:first_firm).to_xml( - :indent => 0, :skip_instruct => true, - :include => { :clients => { :only => :name } } - ) - - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<client><name>Summit</name></client>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_methods_are_called_on_object - xml = authors(:david).to_xml :methods => :label, :indent => 0 - assert_match %r{<label>.*</label>}, xml - end - - def test_should_not_call_methods_on_associations_that_dont_respond - xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2 - assert !authors(:david).hello_posts.first.respond_to?(:label) - assert_match %r{^ <label>.*</label>}, xml - assert_no_match %r{^ <label>}, xml - end - - def test_procs_are_called_on_object - proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<nationality>Danish</nationality>}, xml - end - - def test_dual_arity_procs_are_called_on_object - proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<name-reverse>divaD</name-reverse>}, xml - end - - def test_top_level_procs_arent_applied_to_associations - author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2) - - assert_match %r{^ <nationality>Danish</nationality>}, xml - assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml - end - - def test_procs_on_included_associations_are_called - posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') } - xml = authors(:david).to_xml( - :indent => 2, - :include => { - :posts => { :procs => [ posts_proc ] } - } - ) - - assert_no_match %r{^ <copyright>DHH</copyright>}, xml - assert_match %r{^ {6}<copyright>DHH</copyright>}, xml - end - - def test_should_include_empty_has_many_as_empty_array - authors(:david).posts.delete_all - xml = authors(:david).to_xml :include=>:posts, :indent => 2 - - assert_equal [], Hash.from_xml(xml)['author']['posts'] - assert_match %r{^ <posts type="array"/>}, xml - end - - def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value - xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2 - - assert Hash.from_xml(xml) - assert_match %r{^ <posts-with-comments type="array">}, xml - assert_match %r{^ <posts-with-comment type="Post">}, xml - assert_match %r{^ <posts-with-comment type="StiPost">}, xml - - types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] } - assert types.include?('SpecialPost') - assert types.include?('Post') - assert types.include?('StiPost') - end - - def test_should_produce_xml_for_methods_returning_array - xml = authors(:david).to_xml(:methods => :social) - array = Hash.from_xml(xml)['author']['social'] - assert_equal 2, array.size - assert array.include? 'twitter' - assert array.include? 'github' - end - - def test_should_support_aliased_attributes - xml = Author.select("name as firstname").to_xml - Author.all.each do |author| - assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml - end - end - - def test_array_to_xml_including_has_many_association - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies) - assert xml.include?(%(<replies type="array"><reply>)) - end - - def test_array_to_xml_including_methods - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ]) - assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml - assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml - end - - def test_array_to_xml_including_has_one_association - xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account) - assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true)) - end - - def test_array_to_xml_including_belongs_to_association - xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - end -end diff --git a/activerecord/test/fixtures/naked/yml/parrots.yml b/activerecord/test/fixtures/naked/yml/parrots.yml new file mode 100644 index 0000000000..3e10331105 --- /dev/null +++ b/activerecord/test/fixtures/naked/yml/parrots.yml @@ -0,0 +1,2 @@ +george: + arrr: "Curious George" diff --git a/activerecord/test/models/carrier.rb b/activerecord/test/models/carrier.rb new file mode 100644 index 0000000000..230be118c3 --- /dev/null +++ b/activerecord/test/models/carrier.rb @@ -0,0 +1,2 @@ +class Carrier < ActiveRecord::Base +end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 6588531de6..4cd67c970a 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -1,6 +1,6 @@ class Categorization < ActiveRecord::Base belongs_to :post - belongs_to :category + belongs_to :category, counter_cache: true belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name belongs_to :author diff --git a/activerecord/test/models/customer_carrier.rb b/activerecord/test/models/customer_carrier.rb new file mode 100644 index 0000000000..37186903ff --- /dev/null +++ b/activerecord/test/models/customer_carrier.rb @@ -0,0 +1,14 @@ +class CustomerCarrier < ActiveRecord::Base + cattr_accessor :current_customer + + belongs_to :customer + belongs_to :carrier + + default_scope -> { + if current_customer + where(customer: current_customer) + else + all + end + } +end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index d2a5a7fc49..8ac7a9df6a 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -50,6 +50,7 @@ class Developer < ActiveRecord::Base has_many :firms, :through => :contracts, :source => :firm has_many :comments, ->(developer) { where(body: "I'm #{developer.name}") } has_many :ratings, through: :comments + has_one :ship, dependent: :nullify scope :jamises, -> { where(:name => 'Jamis') } diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index 91e46f83e5..af76fea52c 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,7 +1,7 @@ class Face < ActiveRecord::Base belongs_to :man, :inverse_of => :face belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face - # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly` + # Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly` belongs_to :poly_man_without_inverse, :polymorphic => true # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index dc0566d8a7..7693c6e515 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -26,6 +26,9 @@ class Member < ActiveRecord::Base has_many :current_memberships, -> { where :favourite => true } has_many :clubs, :through => :current_memberships + has_many :tenant_memberships + has_many :tenant_clubs, through: :tenant_memberships, class_name: 'Club', source: :club + has_one :club_through_many, :through => :current_memberships, :source => :club belongs_to :admittable, polymorphic: true diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index df7167ee93..e181ba1f11 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -18,3 +18,18 @@ class SelectedMembership < Membership select("'1' as foo") end end + +class TenantMembership < Membership + cattr_accessor :current_member + + belongs_to :member + belongs_to :club + + default_scope -> { + if current_member + where(member: current_member) + else + all + end + } +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 10f13b67da..81a18188d4 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -98,11 +98,11 @@ class Post < ActiveRecord::Base end end - has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all - has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy + has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all, counter_cache: :taggings_with_delete_all_count + has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy, counter_cache: :taggings_with_destroy_count - has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy - has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify + has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy, counter_cache: :tags_with_destroy_count + has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify, counter_cache: :tags_with_nullify_count has_many :misc_tags, -> { where :tags => { :name => 'Misc' } }, :through => :taggings, :source => :tag has_many :funky_tags, :through => :taggings, :source => :tag diff --git a/activerecord/test/models/professor.rb b/activerecord/test/models/professor.rb new file mode 100644 index 0000000000..7654eda0ef --- /dev/null +++ b/activerecord/test/models/professor.rb @@ -0,0 +1,5 @@ +require_dependency 'models/arunit2_model' + +class Professor < ARUnit2Model + has_and_belongs_to_many :courses +end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 95172e4d3e..e333b964ab 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -3,6 +3,7 @@ class Ship < ActiveRecord::Base belongs_to :pirate belongs_to :update_only_pirate, :class_name => 'Pirate' + belongs_to :developer, dependent: :destroy has_many :parts, :class_name => 'ShipPart' has_many :treasures diff --git a/activerecord/test/models/shop_account.rb b/activerecord/test/models/shop_account.rb new file mode 100644 index 0000000000..1580e8b20c --- /dev/null +++ b/activerecord/test/models/shop_account.rb @@ -0,0 +1,6 @@ +class ShopAccount < ActiveRecord::Base + belongs_to :customer + belongs_to :customer_carrier + + has_one :carrier, through: :customer_carrier +end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index d17270021a..176bc79dc7 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -86,7 +86,7 @@ class Topic < ActiveRecord::Base end def destroy_children - self.class.delete_all "parent_id = #{id}" + self.class.where("parent_id = #{id}").delete_all end def set_email_address diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb index ffc65466d5..63ff0c23ec 100644 --- a/activerecord/test/models/treasure.rb +++ b/activerecord/test/models/treasure.rb @@ -1,6 +1,7 @@ class Treasure < ActiveRecord::Base has_and_belongs_to_many :parrots belongs_to :looter, :polymorphic => true + # No counter_cache option given belongs_to :ship has_many :price_estimates, :as => :estimate_of diff --git a/activerecord/test/models/vehicle.rb b/activerecord/test/models/vehicle.rb new file mode 100644 index 0000000000..ef26170f1f --- /dev/null +++ b/activerecord/test/models/vehicle.rb @@ -0,0 +1,7 @@ +class Vehicle < ActiveRecord::Base + self.abstract_class = true + default_scope -> { where("tires_count IS NOT NULL") } +end + +class Bus < Vehicle +end
\ No newline at end of file diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7bab675b2a..994ece9244 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -131,6 +131,8 @@ ActiveRecord::Schema.define do t.timestamps null: false end + create_table :carriers, force: true + create_table :categories, force: true do |t| t.string :name, null: false t.string :type @@ -237,6 +239,11 @@ ActiveRecord::Schema.define do t.string :gps_location end + create_table :customer_carriers, force: true do |t| + t.references :customer + t.references :carrier + end + create_table :dashboards, force: true, id: false do |t| t.string :dashboard_id t.string :name @@ -666,7 +673,10 @@ ActiveRecord::Schema.define do create_table :ships, force: true do |t| t.string :name t.integer :pirate_id + t.belongs_to :developer t.integer :update_only_pirate_id + # Conventionally named column for counter_cache + t.integer :treasures_count, default: 0 t.datetime :created_at t.datetime :created_on t.datetime :updated_at @@ -683,6 +693,11 @@ ActiveRecord::Schema.define do t.belongs_to :ship end + create_table :shop_accounts, force: true do |t| + t.references :customer + t.references :customer_carrier + end + create_table :speedometers, force: true, id: false do |t| t.string :speedometer_id t.string :name @@ -939,3 +954,12 @@ end College.connection.create_table :colleges, force: true do |t| t.column :name, :string, null: false end + +Professor.connection.create_table :professors, force: true do |t| + t.column :name, :string, null: false +end + +Professor.connection.create_table :courses_professors, id: false, force: true do |t| + t.references :course + t.references :professor +end diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index d11fd9cfc1..c5334e8596 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -1,6 +1,7 @@ require 'active_support/logger' require 'models/college' require 'models/course' +require 'models/professor' module ARTest def self.connection_name |