diff options
Diffstat (limited to 'activerecord')
86 files changed, 776 insertions, 473 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 1d0e4b32a3..dfd9a07997 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,27 @@ +* Hashes can once again be passed to setters of `composed_of`, if all of the + mapping methods are methods implemented on `Hash`. + + Fixes #25978. + + *Sean Griffin* + +* Fix the SELECT statement in `#table_comment` for MySQL. + + *Takeshi Akima* + +* Virtual attributes will no longer raise when read on models loaded from the + database + + *Sean Griffin* + +* Support calling the method `merge` in `scope`'s lambda. + + *Yasuhiro Sugino* + +* Fixes multi-parameter attributes conversion with invalid params. + + *Hiroyuki Ishii* + * Add newline between each migration in `structure.sql`. Keeps schema migration inserts as a single commit, but allows for easier diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 012db35765..8fbea61335 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -51,7 +51,7 @@ end adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] t.libs << 'test' t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { - |x| x =~ /\/adapters\// + |x| x.include?('/adapters/') } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")) t.warning = true @@ -64,7 +64,7 @@ end adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] puts [adapter, adapter_short].inspect (Dir["test/cases/**/*_test.rb"].reject { - |x| x =~ /\/adapters\// + |x| x.include?('/adapters/') } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| sh(Gem.ruby, '-w' ,"-Itest", file) end or raise "Failures" diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 8bed5bca28..9f965052c5 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -261,8 +261,10 @@ module ActiveRecord part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part) end - if part.is_a?(Hash) - raise ArgumentError unless part.size == part.keys.max + hash_from_multiparameter_assignment = part.is_a?(Hash) && + part.each_key.all? { |k| k.is_a?(Integer) } + if hash_from_multiparameter_assignment + raise ArgumentError unless part.size == part.each_key.max part = klass.new(*part.sort.map(&:last)) end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 3729e22e64..c91b5d3fe3 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -362,7 +362,7 @@ module ActiveRecord # end # end # - # If your model class is <tt>Project</tt>, the module is + # If your model class is <tt>Project</tt>, then the module is # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ module is # included in the model class immediately after the (anonymous) generated attributes methods # module, meaning an association will override the methods for an attribute with the same name. @@ -845,8 +845,8 @@ module ActiveRecord # Post.includes(:author).each do |post| # # This references the name of the #belongs_to association that also used the <tt>:author</tt> - # symbol. After loading the posts, find will collect the +author_id+ from each one and load - # all the referenced authors with one query. Doing so will cut down the number of queries + # symbol. After loading the posts, +find+ will collect the +author_id+ from each one and load + # all of the referenced authors with one query. Doing so will cut down the number of queries # from 201 to 102. # # We can improve upon the situation further by referencing both associations in the finder with: @@ -873,7 +873,7 @@ module ActiveRecord # # Since only one table is loaded at a time, conditions or orders cannot reference tables # other than the main one. If this is the case, Active Record falls back to the previously - # used LEFT OUTER JOIN based strategy. For example: + # used <tt>LEFT OUTER JOIN</tt> based strategy. For example: # # Post.includes([:author, :comments]).where(['comments.approved = ?', true]) # @@ -881,18 +881,18 @@ module ActiveRecord # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions # like this can have unintended consequences. - # In the above example posts with no approved comments are not returned at all, because + # In the above example, posts with no approved comments are not returned at all because # the conditions apply to the SQL statement as a whole and not just to the association. # # You must disambiguate column references for this fallback to happen, for example # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. # - # If you want to load all posts (including posts with no approved comments) then write - # your own LEFT OUTER JOIN query using ON + # If you want to load all posts (including posts with no approved comments), then write + # your own <tt>LEFT OUTER JOIN</tt> query using <tt>ON</tt>: # # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'") # - # In this case it is usually more natural to include an association which has conditions defined on it: + # In this case, it is usually more natural to include an association which has conditions defined on it: # # class Post < ActiveRecord::Base # has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment' @@ -924,7 +924,7 @@ module ActiveRecord # # This will execute one query to load the addresses and load the addressables with one # query per addressable type. - # For example if all the addressables are either of class Person or Company then a total + # For example, if all the addressables are either of class Person or Company, then a total # of 3 queries will be executed. The list of addressable types to load is determined on # the back of the addresses loaded. This is not supported if Active Record has to fallback # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. @@ -1015,7 +1015,7 @@ module ActiveRecord # # == Bi-directional associations # - # When you specify an association there is usually an association on the associated model + # When you specify an association, there is usually an association on the associated model # that specifies the same relationship in reverse. For example, with the following models: # # class Dungeon < ActiveRecord::Base @@ -1032,7 +1032,7 @@ module ActiveRecord # end # # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are - # the inverse of each other and the inverse of the +dungeon+ association on +EvilWizard+ + # the inverse of each other, and the inverse of the +dungeon+ association on +EvilWizard+ # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, # Active Record can guess the inverse of the association based on the name # of the class. The result is the following: @@ -1062,7 +1062,7 @@ module ActiveRecord # # * does not work with <tt>:through</tt> associations. # * does not work with <tt>:polymorphic</tt> associations. - # * for #belongs_to associations #has_many inverse associations are ignored. + # * inverse associations for #belongs_to associations #has_many are ignored. # # For more information, see the documentation for the +:inverse_of+ option. # @@ -1070,7 +1070,7 @@ module ActiveRecord # # === Dependent associations # - # #has_many, #has_one and #belongs_to associations support the <tt>:dependent</tt> option. + # #has_many, #has_one, and #belongs_to associations support the <tt>:dependent</tt> option. # This allows you to specify that associated records should be deleted when the owner is # deleted. # 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 5fbd79d118..dd5fd607a2 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 @@ -76,8 +76,10 @@ module ActiveRecord::Associations::Builder # :nodoc: left_model.retrieve_connection end - def self.primary_key - false + private + + def self.suppress_composite_primary_key(pk) + pk unless pk.is_a?(Array) end } diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 0eaa0a4f36..c8805d107b 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -152,9 +152,7 @@ module ActiveRecord if loaded? n ? target.take(n) : target.first else - scope.take(n).tap do |record| - set_inverse_instance record if record.is_a? ActiveRecord::Base - end + scope.take(n) end end @@ -457,23 +455,20 @@ module ActiveRecord end private - def get_records - return scope.to_a if skip_statement_cache? - - conn = klass.connection - sc = reflection.association_scope_cache(conn, owner) do - StatementCache.create(conn) { |params| - as = AssociationScope.create { params.bind } - target_scope.merge as.scope(self, conn) - } - end - - binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute binds, klass, klass.connection - end def find_target - records = get_records + return scope.to_a if skip_statement_cache? + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge as.scope(self, conn) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + records = sc.execute(binds, klass, conn) records.each { |record| set_inverse_instance(record) } records end @@ -656,9 +651,7 @@ module ActiveRecord args.shift if args.first.is_a?(Hash) && args.first.empty? collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target - collection.send(type, *args).tap do |record| - set_inverse_instance record if record.is_a? ActiveRecord::Base - end + collection.send(type, *args) end end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 17ccf5a86c..806a905323 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -29,7 +29,7 @@ module ActiveRecord # instantiation of the actual post records. class CollectionProxy < Relation delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) - delegate :find_nth, to: :scope + delegate :exists?, :update_all, :arel, to: :scope def initialize(klass, association) #:nodoc: @association = association @@ -897,10 +897,6 @@ module ActiveRecord !!@association.include?(record) end - def arel #:nodoc: - scope.arel - end - def proxy_association @association end @@ -1074,6 +1070,12 @@ module ActiveRecord proxy_association.reset_scope self end + + private + + def exec_queries + load_target + end end end end 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 36fc381343..ddd0b59cc1 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -196,7 +196,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - get_records + super end # NOTE - not sure that we can actually cope with inverses here diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 08e0ec691f..604904abcc 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -15,7 +15,7 @@ module ActiveRecord ensure_not_nested through_proxy = owner.association(through_reflection.name) - through_record = through_proxy.send(:load_target) + through_record = through_proxy.load_target if through_record && !record through_record.destroy diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index c5fbe0d1d1..bdf77009eb 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -56,7 +56,9 @@ module ActiveRecord klass_scope = if klass.current_scope - klass.current_scope.clone + klass.current_scope.clone.tap { |scope| + scope.joins_values = [] + } else relation = ActiveRecord::Relation.create( klass, diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index f913f0852a..1fe9a23263 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -44,8 +44,8 @@ module ActiveRecord scope.scope_for_create.stringify_keys.except(klass.primary_key) end - def get_records - return scope.limit(1).records if skip_statement_cache? + def find_target + return scope.take if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do @@ -56,11 +56,7 @@ module ActiveRecord end binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute binds, klass, klass.connection - end - - def find_target - if record = get_records.first + if record = sc.execute(binds, klass, conn).first set_inverse_instance record end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 9530f134d0..2f41e9e45b 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -152,7 +152,7 @@ module ActiveRecord end def _original_value_for_database - value_for_database + type.serialize(original_value) end class FromDatabase < Attribute # :nodoc: @@ -180,7 +180,7 @@ module ActiveRecord value end - def changed_in_place_from?(old_value) + def changed_in_place? false end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 0d5cb8b37c..d28edfb003 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -95,7 +95,8 @@ module ActiveRecord base_name.foreign_key else if ActiveRecord::Base != self && table_exists? - connection.schema_cache.primary_keys(table_name) + pk = connection.schema_cache.primary_keys(table_name) + suppress_composite_primary_key(pk) else 'id' end @@ -122,6 +123,18 @@ module ActiveRecord @quoted_primary_key = nil @attributes_builder = nil end + + private + + def suppress_composite_primary_key(pk) + return pk unless pk.is_a?(Array) + + warn <<-WARNING.strip_heredoc + WARNING: Active Record does not support composite primary key. + + #{table_name} has composite primary key. Composite primary key is ignored. + WARNING + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index e160460286..cbb4f0cff8 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -39,7 +39,7 @@ module ActiveRecord end def set_time_zone_without_conversion(value) - ::Time.zone.local_to_utc(value).in_time_zone + ::Time.zone.local_to_utc(value).in_time_zone if value end def map_avoiding_infinite_recursion(value) diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 720d5f8b7c..f868f23845 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -3,7 +3,7 @@ require 'active_record/attribute_set/yaml_encoder' module ActiveRecord class AttributeSet # :nodoc: - delegate :each_value, to: :attributes + delegate :each_value, :fetch, to: :attributes def initialize(attributes) @attributes = attributes diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 24a255efc1..4ffd39d82d 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -3,11 +3,12 @@ require 'active_record/attribute' module ActiveRecord class AttributeSet # :nodoc: class Builder # :nodoc: - attr_reader :types, :always_initialized + attr_reader :types, :always_initialized, :default - def initialize(types, always_initialized = nil) + def initialize(types, always_initialized = nil, &default) @types = types @always_initialized = always_initialized + @default = default end def build_from_database(values = {}, additional_types = {}) @@ -15,21 +16,22 @@ module ActiveRecord values[always_initialized] = nil end - attributes = LazyAttributeHash.new(types, values, additional_types) + attributes = LazyAttributeHash.new(types, values, additional_types, &default) AttributeSet.new(attributes) end end end class LazyAttributeHash # :nodoc: - delegate :transform_values, :each_key, :each_value, to: :materialize + delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize - def initialize(types, values, additional_types) + def initialize(types, values, additional_types, &default) @types = types @values = values @additional_types = additional_types @materialized = false @delegate_hash = {} + @default = default || proc {} end def key?(key) @@ -76,9 +78,21 @@ module ActiveRecord end end + def marshal_dump + materialize + end + + def marshal_load(delegate_hash) + @delegate_hash = delegate_hash + @types = {} + @values = {} + @additional_types = {} + @materialized = true + end + protected - attr_reader :types, :values, :additional_types, :delegate_hash + attr_reader :types, :values, :additional_types, :delegate_hash, :default def materialize unless @materialized @@ -101,7 +115,7 @@ module ActiveRecord if value_present delegate_hash[name] = Attribute.from_database(name, value, type) elsif types.key?(name) - delegate_hash[name] = Attribute.uninitialized(name, type) + delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type) end end end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 3211b6eaeb..ed0302e763 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -253,7 +253,7 @@ module ActiveRecord name, value, type, - _default_attributes[name], + _default_attributes.fetch(name.to_s) { nil }, ) else default_attribute = Attribute.from_database(name, value, type) diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 2456b8ad8c..5ac1e0c001 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -1,4 +1,5 @@ require 'yaml' +require 'active_support/core_ext/regexp' module ActiveRecord module Coders # :nodoc: @@ -20,7 +21,7 @@ module ActiveRecord def load(yaml) return object_class.new if object_class != Object && yaml.nil? - return yaml unless yaml.is_a?(String) && yaml =~ /^---/ + return yaml unless yaml.is_a?(String) && /^---/.match?(yaml) obj = YAML.load(yaml) assert_valid_value(obj) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 1d9579f1a8..526adc9efe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -848,14 +848,13 @@ module ActiveRecord spec = resolver.spec(config) remove_connection(spec.name) - owner_to_pool[spec.name] = ConnectionAdapters::ConnectionPool.new(spec) message_bus = ActiveSupport::Notifications.instrumenter payload = { connection_id: object_id } if spec - payload[:class_name] = spec.name + payload[:spec_name] = spec.name payload[:config] = spec.config end @@ -896,7 +895,7 @@ module ActiveRecord # for (not necessarily the current class). def retrieve_connection(spec_name) #:nodoc: pool = retrieve_connection_pool(spec_name) - raise ConnectionNotEstablished, "No connection pool with id '#{spec_name}' found." unless pool + raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool conn = pool.connection raise ConnectionNotEstablished, "No connection for '#{spec_name}' in connection pool" unless conn conn 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 507a925d32..c2d02a6cc2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -10,7 +10,7 @@ module ActiveRecord def to_sql(arel, binds = []) if arel.respond_to?(:ast) collected = visitor.accept(arel.ast, collector) - collected.compile(binds.dup, self) + collected.compile(binds, self) else arel end @@ -18,11 +18,12 @@ module ActiveRecord # This is used in the StatementCache object. It returns an object that # can be used to query the database repeatedly. - def cacheable_query(arel) # :nodoc: + def cacheable_query(klass, arel) # :nodoc: + collected = visitor.accept(arel.ast, collector) if prepared_statements - ActiveRecord::StatementCache.query visitor, arel.ast + klass.query(collected.value) else - ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector + klass.partial_query(collected.value) end end @@ -88,14 +89,14 @@ module ActiveRecord # Executes insert +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) exec_query(sql, name, binds) end # Executes delete +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_delete(sql, name, binds) + def exec_delete(sql, name = nil, binds = []) exec_query(sql, name, binds) end @@ -107,7 +108,7 @@ module ActiveRecord # Executes update +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. - def exec_update(sql, name, binds) + def exec_update(sql, name = nil, binds = []) exec_query(sql, name, binds) end @@ -315,7 +316,7 @@ module ActiveRecord end end key_list = fixture.keys.map { |name| quote_column_name(name) } - value_list = prepare_binds_for_database(binds).map do |value| + value_list = binds.map(&:value_for_database).map do |value| begin quote(value) rescue TypeError diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 860ef17dca..eda6af08e3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -112,19 +112,19 @@ module ActiveRecord end def quoted_true - "'t'" + "'t'".freeze end def unquoted_true - 't' + 't'.freeze end def quoted_false - "'f'" + "'f'".freeze end def unquoted_false - 'f' + 'f'.freeze end # Quote date/time values for use in SQL input. Includes microseconds @@ -150,12 +150,12 @@ module ActiveRecord quoted_date(value).sub(/\A2000-01-01 /, '') end - def prepare_binds_for_database(binds) # :nodoc: - binds.map(&:value_for_database) - end - private + def type_casted_binds(binds) + binds.map { |attr| type_cast(attr.value_for_database) } + end + def types_which_need_no_typecasting [nil, Numeric, String] 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 9e9ace49db..19c265db6e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -129,14 +129,9 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table_name) - pks = primary_keys(table_name) - warn <<-WARNING.strip_heredoc if pks.count > 1 - WARNING: Rails does not support composite primary key. - - #{table_name} has composite primary key. Composite primary key is ignored. - WARNING - - pks.first if pks.one? + pk = primary_keys(table_name) + pk = pk.first unless pk.size > 1 + pk end # Creates a new table with the name +table_name+. +table_name+ may either @@ -790,7 +785,7 @@ module ActiveRecord # [<tt>:type</tt>] # The reference column type. Defaults to +:integer+. # [<tt>:index</tt>] - # Add an appropriate index. Defaults to false. + # Add an appropriate index. Defaults to true. # See #add_index for usage of this option. # [<tt>:foreign_key</tt>] # Add an appropriate foreign key constraint. Defaults to false. diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 5747e4d1ee..0b8bacff4e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -130,7 +130,7 @@ module ActiveRecord class BindCollector < Arel::Collectors::Bind def compile(bvs, conn) - casted_binds = conn.prepare_binds_for_database(bvs) + casted_binds = bvs.map(&:value_for_database) super(casted_binds.map { |value| conn.quote(value) }) end end @@ -579,14 +579,15 @@ module ActiveRecord exception end - def log(sql, name = "SQL", binds = [], statement_name = nil) + def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) @instrumenter.instrument( "sql.active_record", - :sql => sql, - :name => name, - :connection_id => object_id, - :statement_name => statement_name, - :binds => binds) { yield } + sql: sql, + name: name, + binds: binds, + type_casted_binds: type_casted_binds, + statement_name: statement_name, + connection_id: object_id) { yield } rescue => e raise translate_exception_class(e, sql) 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 3e77b92141..5e9705e02f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -9,6 +9,7 @@ require 'active_record/connection_adapters/mysql/schema_dumper' require 'active_record/connection_adapters/mysql/type_metadata' require 'active_support/core_ext/string/strip' +require 'active_support/core_ext/regexp' module ActiveRecord module ConnectionAdapters @@ -85,7 +86,7 @@ module ActiveRecord end def mariadb? # :nodoc: - full_version =~ /mariadb/i + /mariadb/i.match?(full_version) end # Returns true, since this connection adapter supports migrations. @@ -409,10 +410,13 @@ module ActiveRecord end def table_comment(table_name) # :nodoc: + schema, name = extract_schema_qualified_name(table_name) + select_value(<<-SQL.strip_heredoc, 'SCHEMA') SELECT table_comment FROM information_schema.tables - WHERE table_name=#{quote(table_name)} + WHERE table_schema = #{quote(schema)} + AND table_name = #{quote(name)} SQL end @@ -543,7 +547,9 @@ module ActiveRecord end end - def table_options(table_name) + def table_options(table_name) # :nodoc: + table_options = {} + create_table_info = create_table_info(table_name) # strip create_definitions and partition_options @@ -552,10 +558,14 @@ module ActiveRecord # strip AUTO_INCREMENT raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') + table_options[:options] = raw_table_options + # strip COMMENT - raw_table_options.sub!(/ COMMENT='.+'/, '') + if raw_table_options.sub!(/ COMMENT='.+'/, '') + table_options[:comment] = table_comment(table_name) + end - raw_table_options + table_options end # Maps logical Rails types to MySQL-specific data types. @@ -743,7 +753,7 @@ module ActiveRecord when ER_DATA_TOO_LONG ValueTooLong.new(message) when ER_LOCK_DEADLOCK - TransactionSerializationError.new(message) + Deadlocked.new(message) else super end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index 87f0ff7d85..fb0eda753f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -45,7 +45,7 @@ module ActiveRecord end end - def exec_delete(sql, name, binds) + def exec_delete(sql, name = nil, binds = []) if without_prepared_statement?(binds) execute_and_free(sql, name) { @connection.affected_rows } else @@ -77,9 +77,9 @@ module ActiveRecord @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone end - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } + type_casted_binds = type_casted_binds(binds) - log(sql, name, binds) do + log(sql, name, binds, type_casted_binds) do if cache_stmt cache = @statements[sql] ||= { stmt: @connection.prepare(sql) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index fbab654112..af1db30047 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -2,14 +2,14 @@ module ActiveRecord module ConnectionAdapters module MySQL module Quoting # :nodoc: - QUOTED_TRUE, QUOTED_FALSE = '1', '0' + QUOTED_TRUE, QUOTED_FALSE = '1'.freeze, '0'.freeze def quote_column_name(name) - @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`" + @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`".freeze end def quote_table_name(name) - @quoted_table_names[name] ||= super.gsub('.', '`.`') + @quoted_table_names[name] ||= super.gsub('.', '`.`').freeze end def quoted_true diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 6f2e03b370..da533463bf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -112,7 +112,7 @@ module ActiveRecord end end - def exec_delete(sql, name = 'SQL', binds = []) + def exec_delete(sql, name = nil, binds = []) execute_and_clear(sql, name, binds) {|result| result.cmd_tuples } end alias :exec_update :exec_delete @@ -124,6 +124,8 @@ module ActiveRecord pk = primary_key(table_ref) if table_ref end + pk = suppress_composite_primary_key(pk) + if pk && use_insert_returning? sql = "#{sql} RETURNING #{quote_column_name(pk)}" end @@ -131,7 +133,7 @@ module ActiveRecord super end - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) + def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) val = exec_query(sql, name, binds) if !use_insert_returning? && pk unless sequence_name @@ -164,6 +166,12 @@ module ActiveRecord def exec_rollback_db_transaction execute "ROLLBACK" end + + private + + def suppress_composite_primary_key(pk) + pk unless pk.is_a?(Array) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 6414459cd1..9fcf8dbb95 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -28,7 +28,7 @@ module ActiveRecord # - "schema.name".table_name # - "schema.name"."table.name" def quote_table_name(name) # :nodoc: - @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted + @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze end # Quotes schema names for use in SQL queries. @@ -42,7 +42,7 @@ module ActiveRecord # Quotes column names for use in SQL queries. def quote_column_name(name) # :nodoc: - @quoted_column_names[name] ||= PGconn.quote_ident(super) + @quoted_column_names[name] ||= PGconn.quote_ident(super).freeze end # Quote date/time values for use in SQL input. @@ -58,7 +58,7 @@ module ActiveRecord def quote_default_expression(value, column) # :nodoc: if value.is_a?(Proc) value.call - elsif column.type == :uuid && value =~ /\(\)/ + elsif column.type == :uuid && value.include?('()') value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) 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 45507e206a..4cf6d4b14a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -238,6 +238,10 @@ module ActiveRecord PostgreSQLColumn.new(*args) end + def table_options(table_name) # :nodoc: + { comment: table_comment(table_name) } + end + # Returns a comment stored in database for given table def table_comment(table_name) # :nodoc: name = Utils.extract_schema_qualified_name(table_name.to_s) @@ -327,12 +331,12 @@ module ActiveRecord end # Returns the sequence name for a table's primary key or some other specified key. - def default_sequence_name(table_name, pk = nil) #:nodoc: - result = serial_sequence(table_name, pk || 'id') + def default_sequence_name(table_name, pk = 'id') #:nodoc: + result = serial_sequence(table_name, pk) return nil unless result Utils.extract_schema_qualified_name(result).to_s rescue ActiveRecord::StatementInvalid - PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s + PostgreSQL::Name.new(nil, "#{table_name}_#{pk}_seq").to_s end def serial_sequence(table, column) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index ddfc560747..8a1fdc9f92 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -407,6 +407,7 @@ module ActiveRecord FOREIGN_KEY_VIOLATION = "23503" UNIQUE_VIOLATION = "23505" SERIALIZATION_FAILURE = "40001" + DEADLOCK_DETECTED = "40P01" def translate_exception(exception, message) return exception unless exception.respond_to?(:result) @@ -419,7 +420,9 @@ module ActiveRecord when VALUE_LIMIT_VIOLATION ValueTooLong.new(message) when SERIALIZATION_FAILURE - TransactionSerializationError.new(message) + SerializationFailure.new(message) + when DEADLOCK_DETECTED + Deadlocked.new(message) else super end @@ -597,15 +600,15 @@ module ActiveRecord end def exec_no_cache(sql, name, binds) - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - log(sql, name, binds) { @connection.async_exec(sql, type_casted_binds) } + type_casted_binds = type_casted_binds(binds) + log(sql, name, binds, type_casted_binds) { @connection.async_exec(sql, type_casted_binds) } end def exec_cache(sql, name, binds) stmt_key = prepare_statement(sql) - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } + type_casted_binds = type_casted_binds(binds) - log(sql, name, binds, stmt_key) do + log(sql, name, binds, type_casted_binds, stmt_key) do @connection.exec_prepared(stmt_key, type_casted_binds) end rescue ActiveRecord::StatementInvalid => e diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index d5a181d3e2..fa20175b0e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -11,7 +11,7 @@ module ActiveRecord end def quote_column_name(name) - @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") + @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}").freeze end def quoted_time(value) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 3384012c49..41ed784d2e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -188,9 +188,9 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = [], prepare: false) - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } + type_casted_binds = type_casted_binds(binds) - log(sql, name, binds) do + log(sql, name, binds, type_casted_binds) do # Don't cache statements if they are not prepared unless prepare stmt = @connection.prepare(sql) @@ -203,7 +203,6 @@ module ActiveRecord ensure stmt.close end - stmt = records else cache = @statements[sql] ||= { :stmt => @connection.prepare(sql) @@ -212,9 +211,10 @@ module ActiveRecord cols = cache[:cols] ||= stmt.columns stmt.reset! stmt.bind_params(type_casted_binds) + records = stmt.to_a end - ActiveRecord::Result.new(cols, stmt.to_a) + ActiveRecord::Result.new(cols, records) end end @@ -548,7 +548,7 @@ module ActiveRecord columns_string.split(',').each do |column_string| # This regex will match the column name and collation type and will save # the value in $1 and $2 respectively. - collation_hash[$1] = $2 if (COLLATE_REGEX =~ column_string) + collation_hash[$1] = $2 if COLLATE_REGEX =~ column_string end basic_structure.map! do |column| diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 2c94463acd..3e3a7679ac 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -78,7 +78,18 @@ module ActiveRecord mattr_accessor :error_on_ignored_order, instance_writer: false self.error_on_ignored_order = false - mattr_accessor :error_on_ignored_order_or_limit, instance_writer: false + def self.error_on_ignored_order_or_limit + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The flag error_on_ignored_order_or_limit is deprecated. Limits are + now supported. Please use error_on_ignored_order instead. + MSG + self.error_on_ignored_order + end + + def error_on_ignored_order_or_limit + self.class.error_on_ignored_order_or_limit + end + def self.error_on_ignored_order_or_limit=(value) ActiveSupport::Deprecation.warn(<<-MSG.squish) The flag error_on_ignored_order_or_limit is deprecated. Limits are diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index b6dd6814db..0bdebb3989 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/regexp' + module ActiveRecord module DynamicMatchers #:nodoc: def respond_to?(name, include_private = false) @@ -29,7 +31,7 @@ module ActiveRecord attr_reader :matchers def match(model, name) - klass = matchers.find { |k| name =~ k.pattern } + klass = matchers.find { |k| k.pattern.match?(name) } klass.new(model, name) if klass end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 38e4fbec8b..03e6e1eee3 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -285,14 +285,24 @@ module ActiveRecord class TransactionIsolationError < ActiveRecordError end - # TransactionSerializationError will be raised when a transaction is rolled + # TransactionRollbackError will be raised when a transaction is rolled # back by the database due to a serialization failure or a deadlock. # # See the following: # # * http://www.postgresql.org/docs/current/static/transaction-iso.html # * https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_lock_deadlock - class TransactionSerializationError < ActiveRecordError + class TransactionRollbackError < StatementInvalid + end + + # SerializationFailure will be raised when a transaction is rolled + # back by the database due to a serialization failure. + class SerializationFailure < TransactionRollbackError + end + + # Deadlocked will be raised when a transaction is rolled + # back by the database when a deadlock is encountered. + class Deadlocked < TransactionRollbackError end # IrreversibleOrderError is raised when a relation's order is too complex for diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 727a9befc1..ac27e72f2b 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -16,15 +16,14 @@ module ActiveRecord # Makes the adapter execute EXPLAIN for the tuples of queries and bindings. # Returns a formatted string ready to be logged. def exec_explain(queries) # :nodoc: - str = queries.map do |sql, bind| - [].tap do |msg| - msg << "EXPLAIN for: #{sql}" - unless bind.empty? - bind_msg = bind.map {|col, val| [col.name, val]}.inspect - msg.last << " #{bind_msg}" - end - msg << connection.explain(sql, bind) - end.join("\n") + str = queries.map do |sql, binds| + msg = "EXPLAIN for: #{sql}" + unless binds.empty? + msg << " " + msg << binds.map { |attr| render_bind(attr) }.inspect + end + msg << "\n" + msg << connection.explain(sql, binds) end.join("\n") # Overriding inspect to be more human readable, especially in the console. @@ -34,5 +33,17 @@ module ActiveRecord str end + + private + + def render_bind(attr) + value = if attr.type.binary? && attr.value + "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" + else + connection.type_cast(attr.value_for_database) + end + + [attr.name, value] + end end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 6d142285cd..0ec33f7d87 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -987,16 +987,11 @@ module ActiveRecord # When connections are established in the future, begin a transaction too @connection_subscriber = ActiveSupport::Notifications.subscribe('!connection.active_record') do |_, _, _, _, payload| - model_class = nil - begin - model_class = payload[:class_name].constantize if payload[:class_name] - rescue NameError - model_class = nil - end + spec_name = payload[:spec_name] if payload.key?(:spec_name) - if model_class + if spec_name begin - connection = ActiveRecord::Base.connection_handler.retrieve_connection(model_class) + connection = ActiveRecord::Base.connection_handler.retrieve_connection(spec_name) rescue ConnectionNotEstablished connection = nil end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 8e32af1c49..c4998ba686 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -20,18 +20,14 @@ module ActiveRecord @odd = false end - def render_bind(attribute) - value = if attribute.type.binary? && attribute.value - if attribute.value.is_a?(Hash) - "<#{attribute.value_for_database.to_s.bytesize} bytes of binary data>" - else - "<#{attribute.value.bytesize} bytes of binary data>" - end + def render_bind(attr, type_casted_value) + value = if attr.type.binary? && attr.value + "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" else - attribute.value_for_database + type_casted_value end - [attribute.name, value] + [attr.name, value] end def sql(event) @@ -48,7 +44,9 @@ module ActiveRecord binds = nil unless (payload[:binds] || []).empty? - binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect + binds = " " + payload[:binds].zip(payload[:type_casted_binds]).map { |attr, value| + render_bind(attr, value) + }.inspect end name = colorize_payload_name(name, payload[:name]) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 81fe053fe1..1bb4688717 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,5 +1,6 @@ -require "active_support/core_ext/module/attribute_accessors" require 'set' +require "active_support/core_ext/module/attribute_accessors" +require 'active_support/core_ext/regexp' module ActiveRecord class MigrationError < ActiveRecordError#:nodoc: @@ -126,9 +127,9 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize(message = nil) if !message && defined?(Rails.env) - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate RAILS_ENV=#{::Rails.env}") + super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate RAILS_ENV=#{::Rails.env}") elsif !message - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate") + super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate") else super end @@ -145,7 +146,7 @@ module ActiveRecord class NoEnvironmentInSchemaError < MigrationError #:nodoc: def initialize - msg = "Environment data not found in the schema. To resolve this issue, run: \n\n\tbin/rails db:environment:set" + msg = "Environment data not found in the schema. To resolve this issue, run: \n\n bin/rails db:environment:set" if defined?(Rails.env) super("#{msg} RAILS_ENV=#{::Rails.env}") else @@ -168,7 +169,7 @@ module ActiveRecord msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n" msg << "You are running in `#{ current }` environment. " msg << "If you are sure you want to continue, first set the environment using:\n\n" - msg << "\tbin/rails db:environment:set" + msg << " bin/rails db:environment:set" if defined?(Rails.env) super("#{msg} RAILS_ENV=#{::Rails.env}\n\n") else @@ -1057,7 +1058,7 @@ module ActiveRecord end def match_to_migration_filename?(filename) # :nodoc: - File.basename(filename) =~ Migration::MigrationFilenameRegexp + Migration::MigrationFilenameRegexp.match?(File.basename(filename)) end def parse_migration_filename(filename) # :nodoc: diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 114686c5d3..41cebb0e34 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -249,7 +249,11 @@ module ActiveRecord end def attributes_builder # :nodoc: - @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) + @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) do |name| + unless columns_hash.key?(name) + _default_attributes[name].dup + end + end end def columns_hash # :nodoc: @@ -309,7 +313,12 @@ module ActiveRecord # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", # and columns used for single table inheritance have been removed. def content_columns - @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } + @content_columns ||= columns.reject do |c| + c.name == primary_key || + c.name == inheritance_column || + c.name.end_with?('_id') || + c.name.end_with?('_count') + end end # Resets all the cached information about columns, which will cause them diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 1ab4e0404f..254550c378 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,9 +1,5 @@ module ActiveRecord module NullRelation # :nodoc: - def exec_queries - @records = [].freeze - end - def pluck(*column_names) [] end @@ -20,10 +16,6 @@ module ActiveRecord 0 end - def size - calculate :size, nil - end - def empty? true end @@ -48,28 +40,8 @@ module ActiveRecord "" end - def count(*) - calculate :count, nil - end - - def sum(*) - calculate :sum, nil - end - - def average(*) - calculate :average, nil - end - - def minimum(*) - calculate :minimum, nil - end - - def maximum(*) - calculate :maximum, nil - end - def calculate(operation, _column_name) - if [:count, :sum, :size].include? operation + if [:count, :sum].include? operation group_values.any? ? Hash.new : 0 elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? Hash.new @@ -85,5 +57,11 @@ module ActiveRecord def or(other) other.spawn end + + private + + def exec_queries + @records = [].freeze + end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 53ddd95bb0..f8dd35df0f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -9,7 +9,7 @@ module ActiveRecord delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all + :having, :create_with, :uniq, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all delegate :pluck, :ids, to: :all diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 93baa882ad..0792ba8f76 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,5 +1,3 @@ -require "arel/collectors/bind" - module ActiveRecord # = Active Record \Relation class Relation @@ -597,19 +595,16 @@ module ActiveRecord # # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar' def to_sql @to_sql ||= begin - relation = self - connection = klass.connection - visitor = connection.visitor + relation = self if eager_loading? find_with_associations { |rel| relation = rel } end - binds = relation.bound_attributes - binds = connection.prepare_binds_for_database(binds) - binds.map! { |value| connection.quote(value) } - collect = visitor.accept(relation.arel.ast, Arel::Collectors::Bind.new) - collect.substitute_binds(binds).join + conn = klass.connection + conn.unprepared_statement { + conn.to_sql(relation.arel, relation.bound_attributes) + } end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 2484cb3264..ad74659cba 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,4 +1,5 @@ require 'active_support/concern' +require 'active_support/core_ext/regexp' module ActiveRecord module Delegation # :nodoc: @@ -58,7 +59,7 @@ module ActiveRecord @delegation_mutex.synchronize do return if method_defined?(method) - if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/ + if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method) module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(*args, &block) scoping { @klass.#{method}(*args, &block) } diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index d255cad91b..916dca33bd 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -514,7 +514,7 @@ module ActiveRecord def find_take if loaded? - @records.first + records.first else @take ||= limit(1).records.first end @@ -531,7 +531,7 @@ module ActiveRecord MSG end if loaded? - @records[index] + records[index] else offset ||= offset_index @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first @@ -557,7 +557,7 @@ module ActiveRecord def find_nth_from_last(index) if loaded? - @records[-index] + records[-index] else relation = if order_values.empty? && primary_key order(arel_attribute(primary_key).asc) @@ -578,7 +578,7 @@ module ActiveRecord def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc: if loaded? - @records[index, limit] + records[index, limit] else index += offset find_nth_with_limit(index, limit) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 8a87015e44..0749bb30b5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -4,6 +4,7 @@ require "active_record/relation/where_clause" require "active_record/relation/where_clause_factory" require 'active_model/forbidden_attributes_protection' require 'active_support/core_ext/string/filters' +require 'active_support/core_ext/regexp' module ActiveRecord module QueryMethods @@ -1135,10 +1136,10 @@ module ActiveRecord end def does_not_support_reverse?(order) - #uses sql function with multiple arguments - order =~ /\([^()]*,[^()]*\)/ || - # uses "nulls first" like construction - order =~ /nulls (first|last)\Z/i + # Uses SQL function with multiple arguments. + /\([^()]*,[^()]*\)/.match?(order) || + # Uses "nulls first" like construction. + /nulls (first|last)\Z/i.match?(order) end def build_order(arel) diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index a9e1fd0dad..f007e9e733 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/regexp' + module ActiveRecord module Sanitization extend ActiveSupport::Concern @@ -153,7 +155,7 @@ module ActiveRecord # # => "name='foo''bar' and group_id='4'" def sanitize_sql_array(ary) statement, *values = ary - if values.first.is_a?(Hash) && statement =~ /:\w+/ + if values.first.is_a?(Hash) && /:\w+/.match?(statement) replace_named_bind_variables(statement, values.first) elsif statement.include?('?') replace_bind_variables(statement, values) diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index d769376d1a..cc3bb1cd06 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -105,12 +105,7 @@ HEADER tbl = StringIO.new # first dump primary key column - if @connection.respond_to?(:primary_keys) - pk = @connection.primary_keys(table) - pk = pk.first unless pk.size > 1 - else - pk = @connection.primary_key(table) - end + pk = @connection.primary_key(table) tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" @@ -132,10 +127,10 @@ HEADER tbl.print ", force: :cascade" table_options = @connection.table_options(table) - tbl.print ", options: #{table_options.inspect}" unless table_options.blank? - - if comment = @connection.table_comment(table).presence - tbl.print ", comment: #{comment.inspect}" + if table_options.present? + table_options.each do |key, value| + tbl.print ", #{key}: #{value.inspect}" if value.present? + end end tbl.puts " do |t|" diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 6c896ccea6..8a29547bda 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -40,7 +40,7 @@ module ActiveRecord end class PartialQuery < Query # :nodoc: - def initialize values + def initialize(values) @values = values @indexes = values.each_with_index.find_all { |thing,i| Arel::Nodes::BindParam === thing @@ -49,19 +49,18 @@ module ActiveRecord def sql_for(binds, connection) val = @values.dup - binds = connection.prepare_binds_for_database(binds) - @indexes.each { |i| val[i] = connection.quote(binds.shift) } + casted_binds = binds.map(&:value_for_database) + @indexes.each { |i| val[i] = connection.quote(casted_binds.shift) } val.join end end - def self.query(visitor, ast) - Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value + def self.query(sql) + Query.new(sql) end - def self.partial_query(visitor, ast, collector) - collected = visitor.accept(ast, collector).value - PartialQuery.new collected + def self.partial_query(values) + PartialQuery.new(values) end class Params # :nodoc: @@ -92,7 +91,7 @@ module ActiveRecord def self.create(connection, block = Proc.new) relation = block.call Params.new bind_map = BindMap.new relation.bound_attributes - query_builder = connection.cacheable_query relation.arel + query_builder = connection.cacheable_query(self, relation.arel) new query_builder, bind_map end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index a1326aa359..5fe0d8b5e4 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -42,11 +42,11 @@ module ActiveRecord end def associated_table(table_name) - return self if table_name == arel_table.name - association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize) - if association && !association.polymorphic? + if !association && table_name == arel_table.name + return self + elsif association && !association.polymorphic? association_klass = association.klass arel_table = association_klass.arel_table.alias(table_name) else diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index b1a0ad0115..8574807fd2 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -90,7 +90,7 @@ module ActiveRecord end def error_class - if configuration['adapter'] =~ /jdbc/ + if configuration['adapter'].include?('jdbc') require 'active_record/railties/jdbcmysql_error' ArJdbcMySQL::Error elsif defined?(Mysql2) diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 4e5872b585..de03550ec2 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -1,4 +1,5 @@ require 'rails/generators/active_record' +require 'active_support/core_ext/regexp' module ActiveRecord module Generators # :nodoc: @@ -60,7 +61,7 @@ module ActiveRecord # A migration file name can only contain underscores (_), lowercase characters, # and numbers 0-9. Any other file name will raise an IllegalMigrationNameError. def validate_file_name! - unless file_name =~ /^[_a-z0-9]+$/ + unless /^[_a-z0-9]+$/.match?(file_name) raise IllegalMigrationNameError.new(file_name) end end diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index 0e37c70e5c..5d8765d63a 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -27,32 +27,25 @@ module ActiveRecord @connection.drop_table 'samples', if_exists: true end - test "raises error when a serialization failure occurs" do - assert_raises(ActiveRecord::TransactionSerializationError) do - thread = Thread.new do - Sample.transaction isolation: :serializable do - Sample.delete_all - - 10.times do |i| - sleep 0.1 + test "raises Deadlocked when a deadlock is encountered" do + assert_raises(ActiveRecord::Deadlocked) do + s1 = Sample.create value: 1 + s2 = Sample.create value: 2 - Sample.create value: i - end + thread = Thread.new do + Sample.transaction do + s1.lock! + sleep 1 + s2.update_attributes value: 1 end end - sleep 0.1 - - Sample.transaction isolation: :serializable do - Sample.delete_all - - 10.times do |i| - sleep 0.1 - - Sample.create value: i - end + sleep 0.5 + Sample.transaction do + s2.lock! sleep 1 + s1.update_attributes value: 2 end thread.join diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 29bf2c15ea..b00f8f5706 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -7,14 +7,14 @@ class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase def test_explain_for_one_query explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = \$?1), explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %(QUERY PLAN), explain end def test_explain_with_eager_loading explain = Developer.where(id: 1).includes(:audit_logs).explain assert_match %(QUERY PLAN), explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = \$?1), explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index e76705a802..8dea92c785 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -26,9 +26,9 @@ module ActiveRecord @connection.drop_table 'samples', if_exists: true end - test "raises error when a serialization failure occurs" do + test "raises SerializationFailure when a serialization failure occurs" do with_warning_suppression do - assert_raises(ActiveRecord::TransactionSerializationError) do + assert_raises(ActiveRecord::SerializationFailure) do thread = Thread.new do Sample.transaction isolation: :serializable do Sample.delete_all @@ -51,8 +51,33 @@ module ActiveRecord Sample.create value: i end + end + + thread.join + end + end + end + + test "raises Deadlocked when a deadlock is encountered" do + with_warning_suppression do + assert_raises(ActiveRecord::Deadlocked) do + s1 = Sample.create value: 1 + s2 = Sample.create value: 2 + + thread = Thread.new do + Sample.transaction do + s1.lock! + sleep 1 + s2.update_attributes value: 1 + end + end + + sleep 0.5 + Sample.transaction do + s2.lock! sleep 1 + s1.update_attributes value: 2 end thread.join diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index a1a6e5f16a..4d25bd615d 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -7,13 +7,13 @@ class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase def test_explain_for_one_query explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\? \[\["id", 1\]\]|1)), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) end def test_explain_with_eager_loading explain = Developer.where(id: 1).includes(:audit_logs).explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\? \[\["id", 1\]\]|1)), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb index 8a728902a8..4e865264c7 100644 --- a/activerecord/test/cases/aggregations_test.rb +++ b/activerecord/test/cases/aggregations_test.rb @@ -143,6 +143,11 @@ class AggregationsTest < ActiveRecord::TestCase customers(:barney).fullname = { first: "Barney", last: "Stinson" } assert_equal "Barney STINSON", customers(:barney).name end + + def test_assigning_hash_without_custom_converter + customers(:barney).fullname_no_converter = { first: "Barney", last: "Stinson" } + assert_equal({ first: "Barney", last: "Stinson" }.to_s, customers(:barney).name) + end end class OverridingAggregationsTest < ActiveRecord::TestCase 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 1bbca84bb2..a959f3c06a 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 @@ -19,6 +19,7 @@ require 'models/professor' require 'models/treasure' require 'models/price_estimate' require 'models/club' +require 'models/user' require 'models/member' require 'models/membership' require 'models/sponsor' @@ -156,7 +157,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase warning = capture(:stderr) do country.treaties << treaty end - assert_no_match(/WARNING: Rails does not support composite primary key\./, warning) + assert_no_match(/WARNING: Active Record does not support composite primary key\./, warning) end def test_has_and_belongs_to_many @@ -995,4 +996,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase Project.first.developers_required_by_default.create!(name: "Sean", salary: 50000) end end + + def test_association_name_is_the_same_as_join_table_name + user = User.create! + assert_nothing_raised { user.jobs_pool.clear } + 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 7ec0dfce7a..113131b28c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -244,8 +244,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_do_not_call_callbacks_for_delete_all car = Car.create(:name => 'honda') car.funky_bulbs.create! + assert_equal 1, car.funky_bulbs.count assert_nothing_raised { car.reload.funky_bulbs.delete_all } - assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategy" + assert_equal 0, car.funky_bulbs.count, "bulbs should have been deleted using :delete_all strategy" end def test_delete_all_on_association_is_the_same_as_not_loaded @@ -438,8 +439,24 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal person, person.readers.first.person end - def force_signal37_to_load_all_clients_of_firm - companies(:first_firm).clients_of_firm.each {|f| } + def test_update_all_respects_association_scope + person = Person.new + person.first_name = 'Naruto' + person.references << Reference.new + person.id = 10 + person.references + person.save! + assert_equal 1, person.references.update_all(favourite: true) + end + + def test_exists_respects_association_scope + person = Person.new + person.first_name = 'Sasuke' + person.references << Reference.new + person.id = 10 + person.references + person.save! + assert_predicate person.references, :exists? end # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first @@ -604,6 +621,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_ids_and_inverse_of force_signal37_to_load_all_clients_of_firm + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + firm = companies(:first_firm) client = firm.clients_of_firm.find(3) assert_kind_of Client, client @@ -728,6 +747,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_adding force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + natural = Client.new("name" => "Natural Company") companies(:first_firm).clients_of_firm << natural assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection @@ -784,6 +806,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_adding_a_collection force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")]) assert_equal 4, companies(:first_firm).clients_of_firm.size assert_equal 4, companies(:first_firm).clients_of_firm.reload.size @@ -927,6 +952,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert new_client.persisted? assert_equal new_client, companies(:first_firm).clients_of_firm.last @@ -946,6 +974,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first) assert_equal 1, companies(:first_firm).clients_of_firm.size assert_equal 1, companies(:first_firm).clients_of_firm.reload.size @@ -1100,6 +1131,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_a_collection force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert_equal 3, companies(:first_firm).clients_of_firm.size companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]]) @@ -1109,6 +1143,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client") clients = companies(:first_firm).dependent_clients_of_firm.to_a assert_equal 3, clients.count @@ -1120,6 +1157,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all_with_not_yet_loaded_association_collection force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert_equal 3, companies(:first_firm).clients_of_firm.size companies(:first_firm).clients_of_firm.reset @@ -1308,6 +1348,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_a_item_which_is_not_in_the_collection force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + summit = Client.find_by_name('Summit') companies(:first_firm).clients_of_firm.delete(summit) assert_equal 2, companies(:first_firm).clients_of_firm.size @@ -1344,6 +1387,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroying force_signal37_to_load_all_clients_of_firm + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + assert_difference "Client.count", -1 do companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first) end @@ -1355,6 +1400,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroying_by_integer_id force_signal37_to_load_all_clients_of_firm + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + assert_difference "Client.count", -1 do companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id) end @@ -1366,6 +1413,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroying_by_string_id force_signal37_to_load_all_clients_of_firm + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + assert_difference "Client.count", -1 do companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id.to_s) end @@ -1376,6 +1425,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroying_a_collection force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert_equal 3, companies(:first_firm).clients_of_firm.size @@ -1389,6 +1441,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroy_all force_signal37_to_load_all_clients_of_firm + + assert_predicate companies(:first_firm).clients_of_firm, :loaded? + clients = companies(:first_firm).clients_of_firm.to_a assert !clients.empty?, "37signals has clients after load" destroyed = companies(:first_firm).clients_of_firm.destroy_all @@ -2407,4 +2462,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb.id], car.bulb_ids assert_no_queries { car.bulb_ids } end + + private + + def force_signal37_to_load_all_clients_of_firm + companies(:first_firm).clients_of_firm.load_target + end end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index b3fe759ad9..a2158e4f3b 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -86,7 +86,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase end def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions - real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length + real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title.start_with?('Welcome')} }.length authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, 'authors.id') assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb index eee135cfb8..e3b257efb2 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -5,6 +5,7 @@ require 'models/author' require 'models/essay' require 'models/categorization' require 'models/person' +require 'active_support/core_ext/regexp' class LeftOuterJoinAssociationTest < ActiveRecord::TestCase fixtures :authors, :essays, :posts, :comments, :categorizations, :people @@ -20,7 +21,7 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase Person.left_outer_joins(:agents => {:agents => :agents}) .left_outer_joins(:agents => {:agents => {:primary_contact => :agents}}).to_a end - assert queries.any? { |sql| /agents_people_4/i =~ sql } + assert queries.any? { |sql| /agents_people_4/i.match?(sql) } end end @@ -36,12 +37,12 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase def test_construct_finder_sql_ignores_empty_left_outer_joins_hash queries = capture_sql { Author.left_outer_joins({}) } - assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql } + assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) } end def test_construct_finder_sql_ignores_empty_left_outer_joins_array queries = capture_sql { Author.left_outer_joins([]) } - assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql } + assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) } end def test_left_outer_joins_forbids_to_use_string_as_argument @@ -50,8 +51,8 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase def test_join_conditions_added_to_join_clause queries = capture_sql { Author.left_outer_joins(:essays).to_a } - assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1|\:a1)/i =~ sql } - assert queries.none? { |sql| /WHERE/i =~ sql } + assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1|\:a1)/i.match?(sql) } + assert queries.none? { |sql| /WHERE/i.match?(sql) } end def test_find_with_sti_join diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 01a058918a..7efacb44f3 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -45,7 +45,6 @@ class AssociationsTest < ActiveRecord::TestCase ship = Ship.create!(:name => "The good ship Dollypop") part = ship.parts.create!(:name => "Mast") part.mark_for_destruction - ship.parts.send(:load_target) assert ship.parts[0].marked_for_destruction? end @@ -54,7 +53,6 @@ class AssociationsTest < ActiveRecord::TestCase part = ship.parts.create!(:name => "Mast") part.mark_for_destruction ShipPart.find(part.id).update_columns(name: 'Deck') - ship.parts.send(:load_target) assert_equal 'Deck', ship.parts[0].name end @@ -183,6 +181,14 @@ class AssociationProxyTest < ActiveRecord::TestCase assert !david.projects.loaded? end + def test_load_does_load_target + david = developers(:david) + + assert !david.projects.loaded? + david.projects.load + assert david.projects.loaded? + end + def test_inspect_does_not_reload_a_not_yet_loaded_target andreas = Developer.new :name => 'Andreas', :log => 'new developer added' assert !andreas.audit_logs.loaded? @@ -248,6 +254,8 @@ class AssociationProxyTest < ActiveRecord::TestCase test "first! works on loaded associations" do david = authors(:david) assert_equal david.posts.first, david.posts.reload.first! + assert david.posts.loaded? + assert_no_queries { david.posts.first! } end def test_reset_unloads_target diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 04126e87e4..9d66c9964c 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -27,34 +27,34 @@ class AttributeMethodsTest < ActiveRecord::TestCase ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers) end - def test_attribute_for_inspect_string + test 'attribute_for_inspect with a string' do t = topics(:first) t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters" assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title) end - def test_attribute_for_inspect_date + test 'attribute_for_inspect with a date' do t = topics(:first) assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on) end - def test_attribute_for_inspect_array + test 'attribute_for_inspect with an array' do t = topics(:first) t.content = [Object.new] assert_match %r(\[#<Object:0x[0-9a-f]+>\]), t.attribute_for_inspect(:content) end - def test_attribute_for_inspect_long_array + test 'attribute_for_inspect with a long array' do t = topics(:first) t.content = (1..11).to_a assert_equal "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", t.attribute_for_inspect(:content) end - def test_attribute_present + test 'attribute_present' do t = Topic.new t.title = "hello there!" t.written_on = Time.now @@ -65,7 +65,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert !t.attribute_present?("author_name") end - def test_attribute_present_with_booleans + test 'attribute_present with booleans' do b1 = Boolean.new b1.value = false assert b1.attribute_present?(:value) @@ -83,44 +83,44 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert Boolean.find(b4.id).attribute_present?(:value) end - def test_caching_nil_primary_key + test 'caching a nil primary key' do klass = Class.new(Minimalistic) assert_called(klass, :reset_primary_key, returns: nil) do 2.times { klass.primary_key } end end - def test_attribute_keys_on_new_instance + test 'attribute keys on a new instance' do t = Topic.new assert_equal nil, t.title, "The topics table has a title column, so it should be nil" assert_raise(NoMethodError) { t.title2 } end - def test_boolean_attributes + test 'boolean attributes' do assert !Topic.find(1).approved? assert Topic.find(2).approved? end - def test_set_attributes + test 'set attributes' do topic = Topic.find(1) - topic.attributes = { "title" => "Budget", "author_name" => "Jason" } + topic.attributes = { title: 'Budget', author_name: 'Jason' } topic.save - assert_equal("Budget", topic.title) - assert_equal("Jason", topic.author_name) + assert_equal('Budget', topic.title) + assert_equal('Jason', topic.author_name) assert_equal(topics(:first).author_email_address, Topic.find(1).author_email_address) end - def test_set_attributes_without_hash + test 'set attributes without a hash' do topic = Topic.new assert_raise(ArgumentError) { topic.attributes = '' } end - def test_integers_as_nil - test = AutoId.create('value' => '') + test 'integers as nil' do + test = AutoId.create(value: '') assert_nil AutoId.find(test.id).value end - def test_set_attributes_with_block + test 'set attributes with a block' do topic = Topic.new do |t| t.title = "Budget" t.author_name = "Jason" @@ -130,7 +130,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal("Jason", topic.author_name) end - def test_respond_to? + test 'respond_to?' do topic = Topic.find(1) assert_respond_to topic, "title" assert_respond_to topic, "title?" @@ -144,7 +144,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert !topic.respond_to?(:nothingness) end - def test_respond_to_with_custom_primary_key + test 'respond_to? with a custom primary key' do keyboard = Keyboard.create assert_not_nil keyboard.key_number assert_equal keyboard.key_number, keyboard.id @@ -152,7 +152,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert keyboard.respond_to?('id') end - def test_id_before_type_cast_with_custom_primary_key + test 'id_before_type_cast with a custom primary key' do keyboard = Keyboard.create keyboard.key_number = '10' assert_equal '10', keyboard.id_before_type_cast @@ -161,8 +161,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal '10', keyboard.read_attribute_before_type_cast(:key_number) end - # Syck calls respond_to? before actually calling initialize - def test_respond_to_with_allocated_object + # Syck calls respond_to? before actually calling initialize. + test 'respond_to? with an allocated object' do klass = Class.new(ActiveRecord::Base) do self.table_name = 'topics' end @@ -174,31 +174,32 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_respond_to topic, :title end - # IRB inspects the return value of "MyModel.allocate". - def test_allocated_object_can_be_inspected + # IRB inspects the return value of MyModel.allocate. + test 'allocated objects can be inspected' do topic = Topic.allocate assert_equal "#<Topic not initialized>", topic.inspect end - def test_array_content + test 'array content' do + content = %w( one two three ) topic = Topic.new - topic.content = %w( one two three ) + topic.content = content topic.save - assert_equal(%w( one two three ), Topic.find(topic.id).content) + assert_equal content, Topic.find(topic.id).content end - def test_read_attributes_before_type_cast - category = Category.new({:name=>"Test category", :type => nil}) - category_attrs = {"name"=>"Test category", "id" => nil, "type" => nil, "categorizations_count" => nil} - assert_equal category_attrs , category.attributes_before_type_cast + test 'read attributes_before_type_cast' do + category = Category.new(name: 'Test category', type: nil) + category_attrs = { 'name' => 'Test category', 'id' => nil, 'type' => nil, 'categorizations_count' => nil } + assert_equal category_attrs, category.attributes_before_type_cast end if current_adapter?(:Mysql2Adapter) - def test_read_attributes_before_type_cast_on_boolean + test 'read attributes_before_type_cast on a boolean' do bool = Boolean.create!({ "value" => false }) - if RUBY_PLATFORM =~ /java/ - # JRuby will return the value before typecast as string + if RUBY_PLATFORM.include?('java') + # JRuby will return the value before typecast as string. assert_equal "0", bool.reload.attributes_before_type_cast["value"] else assert_equal 0, bool.reload.attributes_before_type_cast["value"] @@ -206,7 +207,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_read_attributes_before_type_cast_on_datetime + test 'read attributes_before_type_cast on a datetime' do in_time_zone "Pacific Time (US & Canada)" do record = @target.new @@ -221,7 +222,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_read_attributes_after_type_cast_on_datetime + test 'read attributes_after_type_cast on a date' do tz = "Pacific Time (US & Canada)" in_time_zone tz do @@ -242,7 +243,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_hash_content + test 'hash content' do topic = Topic.new topic.content = { "one" => 1, "two" => 2 } topic.save @@ -256,7 +257,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal 3, Topic.find(topic.id).content["three"] end - def test_update_array_content + test 'update array content' do topic = Topic.new topic.content = %w( one two three ) @@ -270,14 +271,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal(%w( one two three four five ), topic.content) end - def test_case_sensitive_attributes_hash - # DB2 is not case-sensitive + test 'case-sensitive attributes hash' do + # DB2 is not case-sensitive. return true if current_adapter?(:DB2Adapter) assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes end - def test_attributes_without_primary_key + test 'attributes without primary key' do klass = Class.new(ActiveRecord::Base) do self.table_name = 'developers_projects' end @@ -286,9 +287,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_not klass.new.has_attribute?('id') end - def test_hashes_not_mangled - new_topic = { :title => "New Topic" } - new_topic_values = { :title => "AnotherTopic" } + test 'hashes are not mangled' do + new_topic = { title: 'New Topic' } + new_topic_values = { title: 'AnotherTopic' } topic = Topic.new(new_topic) assert_equal new_topic[:title], topic.title @@ -297,13 +298,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal new_topic_values[:title], topic.title end - def test_create_through_factory - topic = Topic.create("title" => "New Topic") + test 'create through factory' do + topic = Topic.create(title: 'New Topic') topicReloaded = Topic.find(topic.id) assert_equal(topic, topicReloaded) end - def test_write_attribute + test 'write_attribute' do topic = Topic.new topic.send(:write_attribute, :title, "Still another topic") assert_equal "Still another topic", topic.title @@ -318,7 +319,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "Still another topic: part 4", topic.title end - def test_read_attribute + test 'read_attribute' do topic = Topic.new topic.title = "Don't change the topic" assert_equal "Don't change the topic", topic.read_attribute("title") @@ -328,7 +329,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "Don't change the topic", topic[:title] end - def test_read_attribute_raises_missing_attribute_error_when_not_exists + test 'read_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist' do computer = Computer.select('id').first assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] } assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] } @@ -336,7 +337,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_nothing_raised { computer[:developer] = 'Hello!' } end - def test_read_attribute_when_false + test 'read_attribute when false' do topic = topics(:first) topic.approved = false assert !topic.approved?, "approved should be false" @@ -344,7 +345,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert !topic.approved?, "approved should be false" end - def test_read_attribute_when_true + test 'read_attribute when true' do topic = topics(:first) topic.approved = true assert topic.approved?, "approved should be true" @@ -352,7 +353,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert topic.approved?, "approved should be true" end - def test_read_write_boolean_attribute + test 'boolean attributes writing and reading' do topic = Topic.new topic.approved = "false" assert !topic.approved?, "approved should be false" @@ -367,7 +368,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert topic.approved?, "approved should be true" end - def test_overridden_write_attribute + test 'overridden write_attribute' do topic = Topic.new def topic.write_attribute(attr_name, value) super(attr_name, value.downcase) @@ -386,7 +387,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "yet another topic: part 4", topic.title end - def test_overridden_read_attribute + test 'overridden read_attribute' do topic = Topic.new topic.title = "Stop changing the topic" def topic.read_attribute(attr_name) @@ -400,40 +401,40 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "STOP CHANGING THE TOPIC", topic[:title] end - def test_read_overridden_attribute - topic = Topic.new(:title => 'a') + test 'read overridden attribute' do + topic = Topic.new(title: 'a') def topic.title() 'b' end assert_equal 'a', topic[:title] end - def test_query_attribute_string + test 'string attribute predicate' do [nil, "", " "].each do |value| - assert_equal false, Topic.new(:author_name => value).author_name? + assert_equal false, Topic.new(author_name: value).author_name? end - assert_equal true, Topic.new(:author_name => "Name").author_name? + assert_equal true, Topic.new(author_name: 'Name').author_name? end - def test_query_attribute_number - [nil, 0, "0"].each do |value| - assert_equal false, Developer.new(:salary => value).salary? + test 'number attribute predicate' do + [nil, 0, '0'].each do |value| + assert_equal false, Developer.new(salary: value).salary? end - assert_equal true, Developer.new(:salary => 1).salary? - assert_equal true, Developer.new(:salary => "1").salary? + assert_equal true, Developer.new(salary: 1).salary? + assert_equal true, Developer.new(salary: '1').salary? end - def test_query_attribute_boolean + test 'boolean attribute predicate' do [nil, "", false, "false", "f", 0].each do |value| - assert_equal false, Topic.new(:approved => value).approved? + assert_equal false, Topic.new(approved: value).approved? end [true, "true", "1", 1].each do |value| - assert_equal true, Topic.new(:approved => value).approved? + assert_equal true, Topic.new(approved: value).approved? end end - def test_query_attribute_with_custom_fields + test 'custom field attribute predicate' do object = Company.find_by_sql(<<-SQL).first SELECT c1.*, c2.type as string_value, c2.rating as int_value FROM companies c1, companies c2 @@ -454,23 +455,23 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert !object.int_value? end - def test_non_attribute_access_and_assignment + test 'non-attribute read and write' do topic = Topic.new assert !topic.respond_to?("mumbo") assert_raise(NoMethodError) { topic.mumbo } assert_raise(NoMethodError) { topic.mumbo = 5 } end - def test_undeclared_attribute_method_does_not_affect_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') + test 'undeclared attribute method does not affect respond_to? and method_missing' do + topic = @target.new(title: 'Budget') assert topic.respond_to?('title') assert_equal 'Budget', topic.title assert !topic.respond_to?('title_hello_world') assert_raise(NoMethodError) { topic.title_hello_world } end - def test_declared_prefixed_attribute_method_affects_respond_to_and_method_missing - topic = @target.new(:title => 'Budget') + test 'declared prefixed attribute method affects respond_to? and method_missing' do + topic = @target.new(title: 'Budget') %w(default_ title_).each do |prefix| @target.class_eval "def #{prefix}attribute(*args) args end" @target.attribute_method_prefix prefix @@ -483,11 +484,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing + test 'declared suffixed attribute method affects respond_to? and method_missing' do %w(_default _title_default _it! _candidate= able?).each do |suffix| @target.class_eval "def attribute#{suffix}(*args) args end" @target.attribute_method_suffix suffix - topic = @target.new(:title => 'Budget') + topic = @target.new(title: 'Budget') meth = "title#{suffix}" assert topic.respond_to?(meth) @@ -497,11 +498,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing + test 'declared affixed attribute method affects respond_to? and method_missing' do [['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix| @target.class_eval "def #{prefix}attribute#{suffix}(*args) args end" - @target.attribute_method_affix({ :prefix => prefix, :suffix => suffix }) - topic = @target.new(:title => 'Budget') + @target.attribute_method_affix(prefix: prefix, suffix: suffix) + topic = @target.new(title: 'Budget') meth = "#{prefix}title#{suffix}" assert topic.respond_to?(meth) @@ -511,38 +512,38 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_should_unserialize_attributes_for_frozen_records - myobj = {:value1 => :value2} - topic = Topic.create("content" => myobj) + test 'should unserialize attributes for frozen records' do + myobj = { value1: :value2 } + topic = Topic.create(content: myobj) topic.freeze assert_equal myobj, topic.content end - def test_typecast_attribute_from_select_to_false - Topic.create(:title => 'Budget') - # Oracle does not support boolean expressions in SELECT + test 'typecast attribute from select to false' do + Topic.create(title: 'Budget') + # Oracle does not support boolean expressions in SELECT. if current_adapter?(:OracleAdapter, :FbAdapter) - topic = Topic.all.merge!(:select => "topics.*, 0 as is_test").first + topic = Topic.all.merge!(select: 'topics.*, 0 as is_test').first else - topic = Topic.all.merge!(:select => "topics.*, 1=2 as is_test").first + topic = Topic.all.merge!(select: 'topics.*, 1=2 as is_test').first end assert !topic.is_test? end - def test_typecast_attribute_from_select_to_true - Topic.create(:title => 'Budget') - # Oracle does not support boolean expressions in SELECT + test 'typecast attribute from select to true' do + Topic.create(title: 'Budget') + # Oracle does not support boolean expressions in SELECT. if current_adapter?(:OracleAdapter, :FbAdapter) - topic = Topic.all.merge!(:select => "topics.*, 1 as is_test").first + topic = Topic.all.merge!(select: 'topics.*, 1 as is_test').first else - topic = Topic.all.merge!(:select => "topics.*, 2=2 as is_test").first + topic = Topic.all.merge!(select: 'topics.*, 2=2 as is_test').first end assert topic.is_test? end - def test_raises_dangerous_attribute_error_when_defining_activerecord_method_in_model + test 'raises ActiveRecord::DangerousAttributeError when defining an AR method in a model' do %w(save create_or_update).each do |method| - klass = Class.new ActiveRecord::Base + klass = Class.new(ActiveRecord::Base) klass.class_eval "def #{method}() 'defined #{method}' end" assert_raise ActiveRecord::DangerousAttributeError do klass.instance_method_already_implemented?(method) @@ -550,7 +551,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_converted_values_are_returned_after_assignment + test 'converted values are returned after assignment' do developer = Developer.new(name: 1337, salary: "50000") assert_equal "50000", developer.salary_before_type_cast @@ -565,7 +566,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "1337", developer.name end - def test_write_nil_to_time_attributes + test 'write nil to time attribute' do in_time_zone "Pacific Time (US & Canada)" do record = @target.new record.written_on = nil @@ -573,7 +574,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_write_time_to_date_attributes + test 'write time to date attribute' do in_time_zone "Pacific Time (US & Canada)" do record = @target.new record.last_read = Time.utc(2010, 1, 1, 10) @@ -581,7 +582,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_time_attributes_are_retrieved_in_current_time_zone + test 'time attributes are retrieved in the current time zone' do in_time_zone "Pacific Time (US & Canada)" do utc_time = Time.utc(2008, 1, 1) record = @target.new @@ -593,7 +594,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_setting_time_zone_aware_attribute_to_utc + test 'setting a time zone-aware attribute to UTC' do in_time_zone "Pacific Time (US & Canada)" do utc_time = Time.utc(2008, 1, 1) record = @target.new @@ -604,7 +605,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_setting_time_zone_aware_attribute_in_other_time_zone + test 'setting time zone-aware attribute in other time zone' do utc_time = Time.utc(2008, 1, 1) cst_time = utc_time.in_time_zone("Central Time (US & Canada)") in_time_zone "Pacific Time (US & Canada)" do @@ -616,18 +617,18 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_setting_time_zone_aware_read_attribute + test 'setting time zone-aware read attribute' do utc_time = Time.utc(2008, 1, 1) cst_time = utc_time.in_time_zone("Central Time (US & Canada)") in_time_zone "Pacific Time (US & Canada)" do - record = @target.create(:written_on => cst_time).reload + record = @target.create(written_on: cst_time).reload assert_equal utc_time, record[:written_on] assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record[:written_on].time_zone assert_equal Time.utc(2007, 12, 31, 16), record[:written_on].time end end - def test_setting_time_zone_aware_attribute_with_string + test 'setting time zone-aware attribute with a string' do utc_time = Time.utc(2008, 1, 1) (-11..13).each do |timezone_offset| time_string = utc_time.in_time_zone(timezone_offset).to_s @@ -641,9 +642,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_time_zone_aware_attribute_saved + test 'time zone-aware attribute saved' do in_time_zone 1 do - record = @target.create(:written_on => '2012-02-20 10:00') + record = @target.create(written_on: '2012-02-20 10:00') record.written_on = '2012-02-20 09:00' record.save @@ -651,7 +652,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_setting_time_zone_aware_attribute_to_blank_string_returns_nil + test 'setting a time zone-aware attribute to a blank string returns nil' do in_time_zone "Pacific Time (US & Canada)" do record = @target.new record.written_on = ' ' @@ -660,7 +661,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_setting_time_zone_aware_attribute_interprets_time_zone_unaware_string_in_time_zone + test 'setting a time zone-aware attribute interprets time zone-unaware string in time zone' do time_string = 'Tue Jan 01 00:00:00 2008' (-11..13).each do |timezone_offset| in_time_zone timezone_offset do @@ -673,7 +674,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_setting_time_zone_aware_datetime_in_current_time_zone + test 'setting a time zone-aware datetime in the current time zone' do utc_time = Time.utc(2008, 1, 1) in_time_zone "Pacific Time (US & Canada)" do record = @target.new @@ -684,7 +685,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_yaml_dumping_record_with_time_zone_aware_attribute + test 'YAML dumping a record with time zone-aware attribute' do in_time_zone "Pacific Time (US & Canada)" do record = Topic.new(id: 1) record.written_on = "Jan 01 00:00:00 2014" @@ -692,7 +693,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_setting_time_zone_aware_time_in_current_time_zone + test 'setting a time zone-aware time in the current time zone' do in_time_zone "Pacific Time (US & Canada)" do record = @target.new time_string = "10:00:00" @@ -707,7 +708,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_setting_time_zone_aware_time_with_dst + test 'setting a time zone-aware time with DST' do in_time_zone "Pacific Time (US & Canada)" do current_time = Time.zone.local(2014, 06, 15, 10) record = @target.new(bonus_time: current_time) @@ -721,7 +722,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_removing_time_zone_aware_types + test 'removing time zone-aware types' do with_time_zone_aware_types(:datetime) do in_time_zone "Pacific Time (US & Canada)" do record = @target.new(bonus_time: "10:00:00") @@ -733,14 +734,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_time_zone_aware_attributes_dont_recurse_infinitely_on_invalid_values + test 'time zone-aware attributes do not recurse infinitely on invalid values' do in_time_zone "Pacific Time (US & Canada)" do record = @target.new(bonus_time: []) assert_equal nil, record.bonus_time end end - def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable + test 'setting a time_zone_conversion_for_attributes should write the value on a class variable' do Topic.skip_time_zone_conversion_for_attributes = [:field_a] Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b] @@ -748,44 +749,44 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal [:field_b], Minimalistic.skip_time_zone_conversion_for_attributes end - def test_read_attributes_respect_access_control - privatize("title") + test 'attribute readers respect access control' do + privatize('title') - topic = @target.new(:title => "The pros and cons of programming naked.") + topic = @target.new(title: 'The pros and cons of programming naked.') assert !topic.respond_to?(:title) exception = assert_raise(NoMethodError) { topic.title } - assert exception.message.include?("private method") + assert exception.message.include?('private method') assert_equal "I'm private", topic.send(:title) end - def test_write_attributes_respect_access_control - privatize("title=(value)") + test 'attribute writers respect access control' do + privatize('title=(value)') topic = @target.new assert !topic.respond_to?(:title=) - exception = assert_raise(NoMethodError) { topic.title = "Pants"} - assert exception.message.include?("private method") - topic.send(:title=, "Very large pants") + exception = assert_raise(NoMethodError) { topic.title = 'Pants' } + assert exception.message.include?('private method') + topic.send(:title=, 'Very large pants') end - def test_question_attributes_respect_access_control - privatize("title?") + test 'attribute predicates respect access control' do + privatize('title?') - topic = @target.new(:title => "Isaac Newton's pants") + topic = @target.new(title: "Isaac Newton's pants") assert !topic.respond_to?(:title?) exception = assert_raise(NoMethodError) { topic.title? } - assert exception.message.include?("private method") + assert exception.message.include?('private method') assert topic.send(:title?) end - def test_bulk_update_respects_access_control - privatize("title=(value)") + test 'bulk updates respect access control' do + privatize('title=(value)') - assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(:title => "Rants about pants") } - assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } + assert_raise(ActiveRecord::UnknownAttributeError) { @target.new(title: 'Rants about pants') } + assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { title: 'Ants in pants' } } end - def test_bulk_update_raise_unknown_attribute_error + test 'bulk update raises ActiveRecord::UnknownAttributeError' do error = assert_raises(ActiveRecord::UnknownAttributeError) { Topic.new(hello: "world") } @@ -794,20 +795,20 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "unknown attribute 'hello' for Topic.", error.message end - def test_methods_override_in_multi_level_subclass + test 'method overrides in multi-level subclasses' do klass = Class.new(Developer) do def name "dev:#{read_attribute(:name)}" end end - 2.times { klass = Class.new klass } + 2.times { klass = Class.new(klass) } dev = klass.new(name: 'arthurnn') dev.save! assert_equal 'dev:arthurnn', dev.reload.name end - def test_global_methods_are_overwritten + test 'global methods are overwritten' do klass = Class.new(ActiveRecord::Base) do self.table_name = 'computers' end @@ -817,8 +818,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_nil computer.system end - def test_global_methods_are_overwritten_when_subclassing - klass = Class.new(ActiveRecord::Base) { self.abstract_class = true } + test 'global methods are overwritten when subclassing' do + klass = Class.new(ActiveRecord::Base) do + self.abstract_class = true + end subklass = Class.new(klass) do self.table_name = 'computers' @@ -830,7 +833,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_nil computer.system end - def test_instance_method_should_be_defined_on_the_base_class + test 'instance methods should be defined on the base class' do subklass = Class.new(Topic) Topic.define_attribute_methods @@ -846,14 +849,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert subklass.method_defined?(:id), "subklass is missing id method" end - def test_read_attribute_with_nil_should_not_asplode - assert_equal nil, Topic.new.read_attribute(nil) + test 'read_attribute with nil should not asplode' do + assert_nil Topic.new.read_attribute(nil) end # If B < A, and A defines an accessor for 'foo', we don't want to override # that by defining a 'foo' method in the generated methods module for B. # (That module will be inserted between the two, e.g. [B, <GeneratedAttributes>, A].) - def test_inherited_custom_accessors + test 'inherited custom accessors' do klass = new_topic_like_ar_class do self.abstract_class = true def title; "omg"; end @@ -869,7 +872,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "lol", topic.author_name end - def test_inherited_custom_accessors_with_reserved_names + test 'inherited custom accessors with reserved names' do klass = Class.new(ActiveRecord::Base) do self.table_name = 'computers' self.abstract_class = true @@ -887,7 +890,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal 99, computer.developer end - def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing + test 'on_the_fly_super_invokable_generated_attribute_methods_via_method_missing' do klass = new_topic_like_ar_class do def title super + '!' @@ -898,7 +901,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal real_topic.title + '!', klass.find(real_topic.id).title end - def test_on_the_fly_super_invokable_generated_predicate_attribute_methods_via_method_missing + test 'on-the-fly super-invokable generated attribute predicates via method_missing' do klass = new_topic_like_ar_class do def title? !super @@ -909,7 +912,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal !real_topic.title?, klass.find(real_topic.id).title? end - def test_calling_super_when_parent_does_not_define_method_raises_error + test 'calling super when the parent does not define method raises NoMethodError' do klass = new_topic_like_ar_class do def some_method_that_is_not_on_super super @@ -921,38 +924,38 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end - def test_attribute_method? + test 'attribute_method?' do assert @target.attribute_method?(:title) assert @target.attribute_method?(:title=) assert_not @target.attribute_method?(:wibble) end - def test_attribute_method_returns_false_if_table_does_not_exist + test 'attribute_method? returns false if the table does not exist' do @target.table_name = 'wibble' assert_not @target.attribute_method?(:title) end - def test_attribute_names_on_new_record + test 'attribute_names on a new record' do model = @target.new assert_equal @target.column_names, model.attribute_names end - def test_attribute_names_on_queried_record + test 'attribute_names on a queried record' do model = @target.last! assert_equal @target.column_names, model.attribute_names end - def test_attribute_names_with_custom_select + test 'attribute_names with a custom select' do model = @target.select('id').last! assert_equal ['id'], model.attribute_names - # Sanity check, make sure other columns exist + # Sanity check, make sure other columns exist. assert_not_equal ['id'], @target.column_names end - def test_came_from_user + test 'came_from_user?' do model = @target.first assert_not model.id_came_from_user? @@ -960,7 +963,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert model.id_came_from_user? end - def test_accessed_fields + test 'accessed_fields' do model = @target.first assert_equal [], model.accessed_fields diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 7bcaa53aa2..604411da97 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -205,5 +205,49 @@ module ActiveRecord assert_equal(:bar, child.new(foo: :bar).foo) end + + test "attributes not backed by database columns are not dirty when unchanged" do + refute OverloadedType.new.non_existent_decimal_changed? + end + + test "attributes not backed by database columns are always initialized" do + OverloadedType.create! + model = OverloadedType.first + + assert_nil model.non_existent_decimal + model.non_existent_decimal = "123" + assert_equal 123, model.non_existent_decimal + end + + test "attributes not backed by database columns return the default on models loaded from database" do + child = Class.new(OverloadedType) do + attribute :non_existent_decimal, :decimal, default: 123 + end + child.create! + model = child.first + + assert_equal 123, model.non_existent_decimal + end + + test "attributes not backed by database columns properly interact with mutation and dirty" do + child = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + attribute :foo, :string, default: "lol" + end + child.create! + model = child.first + + assert_equal "lol", model.foo + + model.foo << "asdf" + assert_equal "lolasdf", model.foo + assert model.foo_changed? + + model.reload + assert_equal "lol", model.foo + + model.foo = "lol" + refute model.changed? + end end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 2c6011489e..8a4c1bd615 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -587,7 +587,7 @@ class EachTest < ActiveRecord::TestCase end end - test 'error_on_ignored_order_or_limit= is deprecated' do + test '.error_on_ignored_order_or_limit= is deprecated' do begin prev = ActiveRecord::Base.error_on_ignored_order assert_deprecated 'Please use error_on_ignored_order= instead.' do @@ -598,4 +598,20 @@ class EachTest < ActiveRecord::TestCase ActiveRecord::Base.error_on_ignored_order = prev end end + + test '.error_on_ignored_order_or_limit is deprecated' do + expected = ActiveRecord::Base.error_on_ignored_order + actual = assert_deprecated 'Please use error_on_ignored_order instead.' do + ActiveRecord::Base.error_on_ignored_order_or_limit + end + assert_equal expected, actual + end + + test '#error_on_ignored_order_or_limit is deprecated' do + expected = ActiveRecord::Base.error_on_ignored_order + actual = assert_deprecated 'Please use error_on_ignored_order instead.' do + Post.new.error_on_ignored_order_or_limit + end + assert_equal expected, actual + end end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index fa924fe4cb..3f01885489 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -57,10 +57,13 @@ module ActiveRecord end def test_logs_bind_vars_after_type_cast + binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)] + type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } payload = { :name => 'SQL', :sql => 'select * from topics where id = ?', - :binds => [Relation::QueryAttribute.new("id", "10", Type::Integer.new)] + :binds => binds, + :type_casted_binds => type_casted_binds } event = ActiveSupport::Notifications::Event.new( 'foo', @@ -84,6 +87,12 @@ module ActiveRecord logger.sql event assert_match([[@pk.name, 10]].inspect, logger.debugs.first) end + + private + + def type_cast(value) + ActiveRecord::Base.connection.type_cast(value) + end end end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 4f70ae3a1d..8a722b4f22 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -31,7 +31,7 @@ class CallbackDeveloper < ActiveRecord::Base end ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| - next if callback_method.to_s =~ /^around_/ + next if callback_method.to_s.start_with?('around_') define_callback_method(callback_method) ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) } send(callback_method, callback_proc(callback_method)) diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 2bdabaee62..bbcb42d58e 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -347,8 +347,8 @@ module ActiveRecord payloads << payload end ActiveRecord::Base.establish_connection :arunit - assert_equal [:class_name, :config, :connection_id], payloads[0].keys.sort - assert_equal 'primary', payloads[0][:class_name] + assert_equal [:config, :connection_id, :spec_name], payloads[0].keys.sort + assert_equal 'primary', payloads[0][:spec_name] ensure ActiveSupport::Notifications.unsubscribe(subscription) if subscription end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index f9794518c7..3c58b6ad09 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -604,7 +604,7 @@ class DirtyTest < ActiveRecord::TestCase jon = Person.create! first_name: 'Jon' end - assert ActiveRecord::SQLCounter.log_all.none? { |sql| sql =~ /followers_count/ } + assert ActiveRecord::SQLCounter.log_all.none? { |sql| sql.include?('followers_count') } jon.reload assert_equal 'Jon', jon.first_name diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 64dfd86ce2..6409290e8d 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -46,11 +46,8 @@ if ActiveRecord::Base.connection.supports_explain? end def test_exec_explain_with_binds - object = Struct.new(:name) - cols = [object.new('wadus'), object.new('chaflan')] - sqls = %w(foo bar) - binds = [[[cols[0], 1]], [[cols[1], 2]]] + binds = [[bind_param('wadus', 1)], [bind_param('chaflan', 2)]] queries = sqls.zip(binds) stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do @@ -83,5 +80,8 @@ if ActiveRecord::Base.connection.supports_explain? end end + def bind_param(name, value) + ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new) + end end end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index a06aa4b247..df53bbf950 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -650,10 +650,10 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase private def fire_connection_notification(connection) - ActiveRecord::Base.connection_handler.stubs(:retrieve_connection).with(Book).returns(connection) + ActiveRecord::Base.connection_handler.stubs(:retrieve_connection).with('book').returns(connection) message_bus = ActiveSupport::Notifications.instrumenter payload = { - class_name: 'Book', + spec_name: 'book', config: nil, connection_id: connection.object_id } diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 29546525f3..2f71ba870d 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -156,14 +156,7 @@ module ActiveRecord assert_equal String, bob.bio.class assert_kind_of Integer, bob.age assert_equal Time, bob.birthday.class - - if current_adapter?(:OracleAdapter) - # Oracle doesn't differentiate between date/time - assert_equal Time, bob.favorite_day.class - else - assert_equal Date, bob.favorite_day.class - end - + assert_equal Date, bob.favorite_day.class assert_instance_of TrueClass, bob.male? assert_kind_of BigDecimal, bob.wealth end diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index d05cb22740..59c340ceb7 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -202,6 +202,20 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase Topic.reset_column_information end + def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_and_invalid_time_params + with_timezone_config aware_attributes: true do + Topic.reset_column_information + attributes = { + "written_on(1i)" => "2004", "written_on(2i)" => "", "written_on(3i)" => "" + } + topic = Topic.find(1) + topic.attributes = attributes + assert_nil topic.written_on + end + ensure + Topic.reset_column_information + end + def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false with_timezone_config default: :local, aware_attributes: false, zone: -28800 do attributes = { diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 11fb164d50..8a08056bbe 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -642,13 +642,13 @@ module NestedAttributesOnACollectionAssociationTests def test_should_not_overwrite_unsaved_updates_when_loading_association @pirate.reload @pirate.send(association_setter, [{ :id => @child_1.id, :name => 'Grace OMalley' }]) - assert_equal 'Grace OMalley', @pirate.send(@association_name).send(:load_target).find { |r| r.id == @child_1.id }.name + assert_equal 'Grace OMalley', @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }.name end def test_should_preserve_order_when_not_overwriting_unsaved_updates @pirate.reload @pirate.send(association_setter, [{ :id => @child_1.id, :name => 'Grace OMalley' }]) - assert_equal @child_1.id, @pirate.send(@association_name).send(:load_target).first.id + assert_equal @child_1.id, @pirate.send(@association_name).load_target.first.id end def test_should_refresh_saved_records_when_not_overwriting_unsaved_updates @@ -657,13 +657,13 @@ module NestedAttributesOnACollectionAssociationTests @pirate.send(@association_name) << record record.save! @pirate.send(@association_name).last.update!(name: 'Polly') - assert_equal 'Polly', @pirate.send(@association_name).send(:load_target).last.name + assert_equal 'Polly', @pirate.send(@association_name).load_target.last.name end def test_should_not_remove_scheduled_destroys_when_loading_association @pirate.reload @pirate.send(association_setter, [{ :id => @child_1.id, :_destroy => '1' }]) - assert @pirate.send(@association_name).send(:load_target).find { |r| r.id == @child_1.id }.marked_for_destruction? + assert @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }.marked_for_destruction? end def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 4267ad4a24..ea36c199f4 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -255,6 +255,7 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + @connection.schema_cache.clear! @connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t| t.string :region t.integer :code @@ -270,10 +271,15 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end def test_primary_key_issues_warning + model = Class.new(ActiveRecord::Base) do + def self.table_name + "barcodes" + end + end warning = capture(:stderr) do - assert_nil @connection.primary_key("barcodes") + assert_nil model.primary_key end - assert_match(/WARNING: Rails does not support composite primary key\./, warning) + assert_match(/WARNING: Active Record does not support composite primary key\./, warning) end def test_collectly_dump_composite_primary_key diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 03ec063671..085636553e 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -6,6 +6,8 @@ require 'models/post' require 'rack' class QueryCacheTest < ActiveRecord::TestCase + self.use_transactional_tests = false + fixtures :tasks, :topics, :categories, :posts, :categories_posts teardown do diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index c01c82f4f5..225e23bc83 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -149,5 +149,21 @@ module ActiveRecord assert_equal "1800", @quoter.quote(30.minutes) end end + + class QuoteBooleanTest < ActiveRecord::TestCase + def setup + @connection = ActiveRecord::Base.connection + end + + def test_quote_returns_frozen_string + assert_predicate @connection.quote(true), :frozen? + assert_predicate @connection.quote(false), :frozen? + end + + def test_type_cast_returns_frozen_value + assert_predicate @connection.type_cast(true), :frozen? + assert_predicate @connection.type_cast(false), :frozen? + end + end end end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index dcd09b6973..6679f9415b 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -5,6 +5,7 @@ require 'models/developer' require 'models/computer' require 'models/vehicle' require 'models/cat' +require 'active_support/core_ext/regexp' class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments @@ -201,7 +202,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_order_to_unscope_reordering scope = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order) - assert !(scope.to_sql =~ /order/i) + assert !/order/i.match?(scope.to_sql) end def test_unscope_reverse_order diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 0e277ed235..f0dac07acc 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -46,6 +46,13 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count) end + def test_calling_merge_at_first_in_scope + Topic.class_eval do + scope :calling_merge_at_first_in_scope, Proc.new { merge(Topic.replied) } + end + assert_equal Topic.calling_merge_at_first_in_scope.to_a, Topic.replied.to_a + end + def test_method_missing_priority_when_delegating klazz = Class.new(ActiveRecord::Base) do self.table_name = "topics" diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index c15d57460b..ef46fd5d9a 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -228,6 +228,13 @@ class RelationScopingTest < ActiveRecord::TestCase assert SpecialComment.all.any? end end + + def test_circular_joins_with_current_scope_does_not_crash + posts = Post.joins(comments: :post).scoping do + Post.current_scope.first(10) + end + assert_equal posts, Post.joins(comments: :post).first(10) + end end class NestedRelationScopingTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index c8adc21bbc..fcb6552e5b 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,5 +1,6 @@ require 'active_support/test_case' require 'active_support/testing/stream' +require 'active_support/core_ext/regexp' module ActiveRecord # = Active Record Test Case @@ -115,7 +116,7 @@ module ActiveRecord return if 'CACHE' == values[:name] self.class.log_all << sql - self.class.log << sql unless ignore =~ sql + self.class.log << sql unless ignore.match?(sql) end end diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 3338aaf7e1..d464759430 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -7,6 +7,7 @@ class Customer < ActiveRecord::Base composed_of :non_blank_gps_location, :class_name => "GpsLocation", :allow_nil => true, :mapping => %w(gps_location gps_location), :converter => lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)} composed_of :fullname, :mapping => %w(name to_s), :constructor => Proc.new { |name| Fullname.parse(name) }, :converter => :parse + composed_of :fullname_no_converter, :mapping => %w(name to_s), class_name: "Fullname" end class Address diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 176bc79dc7..21c23ed2a2 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -43,12 +43,6 @@ class Topic < ActiveRecord::Base before_create :default_written_on before_destroy :destroy_children - # Explicitly define as :date column so that returned Oracle DATE values would be typecasted to Date and not Time. - # Some tests depend on assumption that this attribute will have Date values. - if current_adapter?(:OracleEnhancedAdapter) - set_date_columns :last_read - end - def parent Topic.find(parent_id) end diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb index f5dc93e994..97107a35a0 100644 --- a/activerecord/test/models/user.rb +++ b/activerecord/test/models/user.rb @@ -1,6 +1,12 @@ +require 'models/job' + class User < ActiveRecord::Base has_secure_token has_secure_token :auth_token + + has_and_belongs_to_many :jobs_pool, + class_name: Job, + join_table: 'jobs_pool' end class UserWithNotification < User diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 24713f722a..030ad73621 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -88,7 +88,7 @@ _SQL FOR EACH ROW EXECUTE PROCEDURE partitioned_insert_trigger(); _SQL rescue ActiveRecord::StatementInvalid => e - if e.message =~ /language "plpgsql" does not exist/ + if e.message.include?('language "plpgsql" does not exist') execute "CREATE LANGUAGE 'plpgsql';" retry else diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 2f2993ce18..a5155e9cdb 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -390,6 +390,11 @@ ActiveRecord::Schema.define do t.integer :ideal_reference_id end + create_table :jobs_pool, force: true, id: false do |t| + t.references :job, null: false, index: true + t.references :user, null: false, index: true + end + create_table :keyboards, force: true, id: false do |t| t.primary_key :key_number t.string :name |